Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-all-tests-linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
# push:
workflow_dispatch:
schedule:
- cron: '0 0 * * 1,3,6' # Mon/Wed/Sat at midnight UTC
- cron: '0 0 * * *' # Every day at midnight UTC

jobs:
build-and-test:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-all-tests-macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
# push:
workflow_dispatch:
schedule:
- cron: '0 0 * * 1,3,6' # Mon/Wed/Sat at midnight UTC
- cron: '0 0 * * *' # Every day at midnight UTC


jobs:
Expand Down
41 changes: 36 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ const result = await $.spawnSafe('my-tool --version')
await $.spawn('my-tool configure', { interactive: true })
```

**`interactive: true` vs `stdin: true`** — these are distinct options:

- `interactive: true` — passes `-i` to the shell so it sources the user's RC file (`.zshrc`, `.bashrc`). Use this when the command needs PATH entries or shell aliases from the RC. It does **not** allow the user to type input.
- `stdin: true` — connects the user's terminal stdin directly to the spawned process. Use this when the command requires real user input (e.g. browser-based OAuth prompts, interactive wizards, password entry). Without this flag, interactive prompts will hang.

```typescript
// Command needs PATH from shell RC but no user input
await $.spawn('my-tool configure', { interactive: true })

// Command requires the user to interact with it directly (e.g. browser login flow)
await $.spawn('gh auth login --web', { interactive: true, stdin: true })
```

**Never use `sudo` inside `$.spawn` or `$.spawnSafe`.** Use `{ requiresRoot: true }` in the options instead. The framework handles privilege escalation through the parent process.

```typescript
Expand Down Expand Up @@ -287,16 +300,34 @@ Utils.isWindows()

**Package Installation:**

Always use `Utils.installViaPkgMgr(pkg)` from `@codifycli/plugin-core` to install system packages. This is platform-agnostic and automatically dispatches to the correct package manager (Homebrew on macOS, apt on Debian/Ubuntu, etc.). Never hardcode package manager calls like `brew install`, `apt-get install -y`, or `sudo apt install` in resource code.
Always use `Utils.installViaPkgMgr(pkg)` from `@codifycli/plugin-core` to install system packages. This is platform-agnostic and automatically dispatches to the correct package manager (Homebrew on macOS, apt on Debian/Ubuntu, etc.). Never hardcode package manager calls like `brew install`, `apt-get install -y`, or `sudo apt install` in resource code — not even inside macOS-only branches.

The function accepts an optional `PkgMgrOptionsMap` (second arg) for per-PM flags, and an optional `forcePackageManager` (third arg) to skip OS detection:

```typescript
// Correct — works on macOS and Linux
import { Utils, PackageManager } from '@codifycli/plugin-core';

// Auto-detect OS — works on macOS (brew) and Linux (apt/dnf/etc.)
await Utils.installViaPkgMgr('curl');
await Utils.uninstallViaPkgMgr('curl');

// Wrong — hardcoded to a specific platform/package manager
await $.spawn('sudo apt-get install -y curl');
await $.spawn('brew install curl');
// Force brew for a macOS-only formula resource
await Utils.installViaPkgMgr('syncthing', undefined, PackageManager.BREW);
await Utils.uninstallViaPkgMgr('syncthing', undefined, PackageManager.BREW);

// Force brew + cask for a macOS GUI app
await Utils.installViaPkgMgr('cursor', { [PackageManager.BREW]: { cask: true } }, PackageManager.BREW);
await Utils.uninstallViaPkgMgr('cursor', { [PackageManager.BREW]: { cask: true } }, PackageManager.BREW);

// Auto-detect OS but pass custom flags per PM
await Utils.installViaPkgMgr('github-cli', {
[PackageManager.APT]: { flags: ['--allow-unauthenticated'] },
[PackageManager.DNF]: { flags: ['--repo', 'gh-cli'] },
});

// Wrong — direct brew calls are forbidden even in macOS-only code
await $.spawn('brew install syncthing', { ... });
await $.spawn('brew install --cask cursor', { ... });
```

This applies to prerequisite checks too. When a resource needs a system dependency (e.g. `curl`, `git`, `make`), always install via `Utils.installViaPkgMgr` rather than spawning a package manager directly.
Expand Down
123 changes: 123 additions & 0 deletions docs/resources/(resources)/github-cli.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: github-cli
description: Reference pages for the GitHub CLI (gh) resources
---

The GitHub CLI resources install and configure the [GitHub CLI (`gh`)](https://cli.github.com/manual/) tool. Four resources are provided to manage distinct concerns: installation and global configuration, authentication, command aliases, and GitHub account SSH keys.

---

## github-cli

Installs `gh` and manages global configuration settings such as the default git protocol, editor, pager, and browser.

### Parameters

- **gitProtocol**: *(string: `https` | `ssh`)* Default protocol for git operations. Defaults to `https`.
- **editor**: *(string)* Default text editor for gh commands (e.g. `vim`, `nano`, `code --wait`).
- **prompt**: *(string: `enabled` | `disabled`)* Whether interactive prompts are shown. Defaults to `enabled`.
- **pager**: *(string)* Pager program used to display long output (e.g. `less`).
- **browser**: *(string)* Default browser to open URLs (e.g. `firefox`).

### Example usage

```json title="codify.jsonc"
[
{
"type": "github-cli",
"gitProtocol": "ssh",
"editor": "vim"
}
]
```

---

## github-cli-auth

Authenticates the GitHub CLI using a Personal Access Token (PAT). Supports multiple accounts and GitHub Enterprise Server hostnames.

> **Security note:** The `token` field is marked sensitive and is never logged or displayed by Codify. Store PATs in a secrets manager and reference them via environment variables where possible.

### Parameters

- **token** *(required)*: *(string)* GitHub personal access token (classic or fine-grained).
- **hostname**: *(string)* GitHub hostname. Defaults to `github.com`. Set to your GHE hostname (e.g. `github.mycompany.com`) for enterprise instances.

### Example usage

```json title="codify.jsonc"
[
{
"type": "github-cli",
"gitProtocol": "https"
},
{
"type": "github-cli-auth",
"token": "<Replace me here!>"
}
]
```

---

## github-cli-alias

Creates a short-hand alias for a `gh` command. Each alias is an independent resource, identified by its name.

### Parameters

- **alias** *(required)*: *(string)* The alias name used to invoke the command (e.g. `prc`).
- **expansion** *(required)*: *(string)* The gh command or shell command this alias expands to (e.g. `pr create`).
- **shell**: *(boolean)* When `true`, the expansion is executed as a shell command via `sh`, enabling pipes, redirects, and other shell features. Defaults to `false`.

### Example usage

```json title="codify.jsonc"
[
{
"type": "github-cli-alias",
"alias": "prc",
"expansion": "pr create"
},
{
"type": "github-cli-alias",
"alias": "prs",
"expansion": "pr status"
}
]
```

---

## github-cli-ssh-key

Uploads a local SSH public key to your GitHub account. This is distinct from the `ssh-key` resource, which manages local key files — this resource registers an existing key with GitHub via the `gh ssh-key add` command.

Requires authentication (`github-cli-auth`) to be configured.

### Parameters

- **title** *(required)*: *(string)* Display name for the key on GitHub (e.g. `My Laptop`).
- **keyFile** *(required)*: *(string)* Path to the local SSH public key file (e.g. `~/.ssh/id_ed25519.pub`).
- **keyType**: *(string: `authentication` | `signing`)* Key usage type. Use `authentication` (default) for git over SSH, or `signing` for commit signing.

### Example usage

```json title="codify.jsonc"
[
{
"type": "github-cli"
},
{
"type": "github-cli-auth",
"token": "<Replace me here!>"
},
{
"type": "github-cli-ssh-key",
"title": "My Laptop",
"keyFile": "~/.ssh/id_ed25519.pub",
"keyType": "authentication"
}
]
```
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "default",
"version": "1.10.0",
"version": "1.11.0",
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
"main": "dist/index.js",
"scripts": {
Expand Down Expand Up @@ -41,7 +41,7 @@
"license": "ISC",
"type": "module",
"dependencies": {
"@codifycli/plugin-core": "1.2.3",
"@codifycli/plugin-core": "^1.2.5",
"@codifycli/schemas": "1.2.0",
"ajv": "^8.18.0",
"ajv-formats": "^2.1.1",
Expand Down
4 changes: 3 additions & 1 deletion scripts/cleanup-github-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ if (Utils.isLinux()) {
// Uninstall resources that have Codify resource definitions
await PluginTester.uninstall(pluginPath, [
{ type: 'docker' },
{ type: 'aws-cli'}
{ type: 'aws-cli'},
{ type: 'github-cli' },
]);

await testSpawn('apt-get autoremove -y ruby rpm python awscli needrestart', { requiresRoot: true }); // remove needrestart to keep logs clean.
Expand All @@ -26,6 +27,7 @@ if (Utils.isLinux()) {
} else {
await PluginTester.uninstall(pluginPath, [
{ type: 'aws-cli' },
{ type: 'github-cli' }
]);

await testSpawn('brew uninstall ant gradle kotlin maven selenium-server google-chrome pipx $(brew list | grep -E \'^python(@|$)\') $(brew list | grep -E \'^ruby(@|$)\') aws-sam-cli azure-cli rustup git-lfs $(brew list | grep -E \'^openjdk(@|$)\')', { interactive: true });
Expand Down
20 changes: 20 additions & 0 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,26 @@ const versionRow = await client.from('registry_plugin_versions').upsert({

await uploadResources(isBeta);

if (isBeta) {
// Generate embeddings for prerelease resources so the AI agent can find them via semantic search
console.log('Triggering vector reindex for prerelease resources...')
const reindexKey = process.env.REINDEX_API_KEY
if (!reindexKey) {
console.warn('REINDEX_API_KEY not set — skipping prerelease reindex')
} else {
const res = await fetch('https://api.codifycli.com/v1/embeddings/reindex', {
method: 'POST',
headers: { Authorization: `Bearer ${reindexKey}` },
})
if (!res.ok) {
console.error(`Prerelease reindex failed: ${res.status} ${await res.text()}`)
} else {
const body = await res.json() as { resources_processed: number; templates_processed: number }
console.log(`Prerelease reindex complete — resources: ${body.resources_processed}`)
}
}
}

if (!isBeta) {
// Build and deploy completions as well.
console.log('Deploying completions...')
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { EnvFileResource } from './resources/file/env-file/env-file-resource.js'
import { EnvFilesResource } from './resources/file/env-file/env-files-resource.js';
import { FileResource } from './resources/file/file.js';
import { RemoteFileResource } from './resources/file/remote-file.js';
import { GithubCliResource } from './resources/github-cli/github-cli.js';
import { GithubCliAuthResource } from './resources/github-cli/github-cli-auth.js';
import { GithubCliAliasResource } from './resources/github-cli/github-cli-alias.js';
import { GithubCliSshKeyResource } from './resources/github-cli/github-cli-ssh-key.js';
import { GitResource } from './resources/git/git/git-resource.js';
import { GitLfsResource } from './resources/git/lfs/git-lfs.js';
import { GitRepositoriesResource } from './resources/git/repositories/git-repositories.js';
Expand Down Expand Up @@ -147,6 +151,10 @@ runPlugin(Plugin.create(
new RbenvResource(),
new OpenClawResource(),
new RustResource(),
new GithubCliResource(),
new GithubCliAuthResource(),
new GithubCliAliasResource(),
new GithubCliSshKeyResource(),
],
{ minSupportedCliVersion: MIN_SUPPORTED_CLI_VERSION }
))
6 changes: 3 additions & 3 deletions src/resources/asdf/asdf.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CreatePlan, ExampleConfig, FileUtils, Resource, ResourceSettings, SpawnStatus, Utils as CoreUtils, getPty, z, Utils } from '@codifycli/plugin-core';
import { CreatePlan, ExampleConfig, FileUtils, Resource, ResourceSettings, SpawnStatus, Utils as CoreUtils, getPty, z, Utils, PackageManager } from '@codifycli/plugin-core';
import { OS } from '@codifycli/schemas';
import fs from 'node:fs/promises';
import os from 'node:os';
Expand Down Expand Up @@ -83,7 +83,7 @@ export class AsdfResource extends Resource<AsdfConfig> {
throw new Error('Homebrew is not installed. Please install Homebrew before installing asdf.');
}

await $.spawn('brew install asdf', { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1 } });
await Utils.installViaPkgMgr('asdf', undefined, PackageManager.BREW);
}

if (Utils.isLinux()) {
Expand Down Expand Up @@ -124,7 +124,7 @@ export class AsdfResource extends Resource<AsdfConfig> {
return;
}

await $.spawn('brew uninstall asdf', { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1 } });
await Utils.uninstallViaPkgMgr('asdf', undefined, PackageManager.BREW);
} else {
await fs.rm(asdfDir, { recursive: true, force: true });
}
Expand Down
6 changes: 3 additions & 3 deletions src/resources/aws-cli/cli/aws-cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Resource, ResourceSettings, SpawnStatus, Utils, getPty, FileUtils } from '@codifycli/plugin-core';
import { Resource, ResourceSettings, SpawnStatus, Utils, PackageManager, getPty, FileUtils } from '@codifycli/plugin-core';
import { OS, StringIndexedObject } from '@codifycli/schemas';
import fs from 'node:fs/promises';
import os from 'node:os';
Expand Down Expand Up @@ -52,7 +52,7 @@ export class AwsCliResource extends Resource<AwsCliConfig> {

if (isArmArch && isHomebrewInstalled) {
console.log('Resource: \'aws-cli\'. Detected that mac is aarch64. Installing AWS-CLI via homebrew')
await $.spawn('HOMEBREW_NO_AUTO_UPDATE=1 brew install awscli', { interactive: true })
await Utils.installViaPkgMgr('awscli', undefined, PackageManager.BREW)

} else if (!isArmArch || isRosettaInstalled) {
console.log('Resource: \'aws-cli\'. Detected that mac is not ARM or Rosetta is installed. Installing AWS-CLI standalone version')
Expand Down Expand Up @@ -105,7 +105,7 @@ softwareupdate --install-rosetta
}

if (installLocation.includes('homebrew')) {
await $.spawn('brew uninstall awscli', { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1 } });
await Utils.uninstallViaPkgMgr('awscli', undefined, PackageManager.BREW);
return;
}

Expand Down
3 changes: 2 additions & 1 deletion src/resources/cursor/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ResourceSettings,
SpawnStatus,
Utils,
PackageManager,
getPty,
z,
} from '@codifycli/plugin-core';
Expand Down Expand Up @@ -170,7 +171,7 @@ export class CursorResource extends Resource<CursorConfig> {

private async installMacOS(): Promise<void> {
const $ = getPty();
await $.spawn('brew install --cask cursor', { interactive: true });
await Utils.installViaPkgMgr('cursor', { [PackageManager.BREW]: { cask: true } }, PackageManager.BREW);
}

private async installLinux(plan: CreatePlan<CursorConfig>): Promise<void> {
Expand Down
Loading
Loading