Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d7c9f1a
feat: Added includeSensitive flag for remote inits
kevinwang5658 Apr 23, 2026
1cc245b
feat: Added distro filtering
kevinwang5658 Apr 23, 2026
6c84899
feat: Add verbosity toggle
kevinwang5658 Apr 24, 2026
fab9858
feat: Add defaulting to plain reporter if tty is not available
kevinwang5658 Apr 24, 2026
c644025
feat: Added auto approve flag for scripts
kevinwang5658 Apr 24, 2026
a0f241c
feat: Added toggle for adding sudo password during apply
kevinwang5658 Apr 24, 2026
f9d342c
feat: Added toggle for adding sudo password during apply
kevinwang5658 Apr 24, 2026
2a30c3f
feat: Changed to custom component
kevinwang5658 Apr 24, 2026
92b47ca
feat: Changed to custom components intead of inkjs
kevinwang5658 Apr 24, 2026
9054e65
feat: Added checking state to password input
kevinwang5658 Apr 24, 2026
58e62e8
Made the password input for plugins un-cancellable
kevinwang5658 Apr 24, 2026
0358b6d
Made updateRenderState async and render to nothing first to avoid mem…
kevinwang5658 Apr 24, 2026
02cf2ef
Couple of fixes:
kevinwang5658 Apr 24, 2026
1cb1c10
Refactoring to fix structural issues and improvements
kevinwang5658 Apr 24, 2026
2a43158
feat: Added sudoAskpass support. Removed unnecessary log event emitte…
kevinwang5658 Apr 25, 2026
0971cee
feat: Refactoring setRawMode and disableRawMode into the reporter
kevinwang5658 Apr 25, 2026
96a5488
fix: Fix password dialog not disappearing for plugin sudo requests
kevinwang5658 Apr 25, 2026
876eb0e
feat: Add verbosity level for commands ran on local cli
kevinwang5658 Apr 25, 2026
4d4e8d5
feat: Improved log output
kevinwang5658 Apr 25, 2026
b4e176c
feat: Improved log output (add strip ansi)
kevinwang5658 Apr 26, 2026
5b1432d
feat: Added CLI version to plugin search. This gates breaking changes…
kevinwang5658 Apr 26, 2026
9eab689
feat: Added terminal resize handler
kevinwang5658 Apr 26, 2026
baf0b0f
Kevin/improved errors (#61)
kevinwang5658 Apr 27, 2026
d266cb7
Kevin/improve launcher-and-destroy (#62)
kevinwang5658 Apr 30, 2026
56621d7
feat: Improved subprogress display
kevinwang5658 Apr 30, 2026
33b7e6b
feat: Improved edit to install the codify desktop app instead
kevinwang5658 Apr 30, 2026
55906be
feat: refactored and improve the new edit changes
kevinwang5658 Apr 30, 2026
8619ec6
feat: improve installation script to create /usr/local/bin if it does…
kevinwang5658 Apr 30, 2026
d40df9e
feat: added another QOL change to prevent sleep while codify apply or…
kevinwang5658 Apr 30, 2026
7e22775
feat: improved text for sudo password and progress display
kevinwang5658 Apr 30, 2026
7c18fec
feat: added status effects for sub-progresses
kevinwang5658 May 1, 2026
a9a3d42
fix: ws.close instead of ws.terminate. Improved progress display
kevinwang5658 May 6, 2026
9bb09f9
feat: added skip banner flag to init command
kevinwang5658 May 6, 2026
8cb2e44
feat: add test fixes
kevinwang5658 May 6, 2026
5336b7c
feat: add skip banner flag to connect
kevinwang5658 May 6, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
node_modules
oclif.manifest.json
.env
.codify-files
25 changes: 24 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,27 @@ Parent Process Plugin Process
3. **Plugin IPC**: Plugins cannot directly read stdin (security isolation)
4. **Sudo Caching**: Password cached in memory during session unless `--secure` flag used
5. **File Watcher**: Use `persistent: false` option to prevent hanging processes
6. **Linting**: ESLint enforces single quotes, specific import ordering, and strict type safety
6. **Linting**: ESLint enforces single quotes, specific import ordering, and strict type safety
7. **Reporter display methods are async**: All `Reporter` interface display methods (`displayPlan`, `displayImportResult`, `displayFileModifications`, `displayMessage`, `displayPluginError`) return `Promise<void>`. Always `await` them at call sites — `DefaultReporter.updateRenderState()` has a 50ms sleep, so unawaited calls cause `process.exit(1)` to fire before the UI renders.
8. **Mock reporter async assertions**: Assertions inside `MockReporter` config callbacks (e.g. `displayFileModifications`) will silently pass if the call isn't awaited. Making display methods async surfaced latent bugs where expected file paths were wrong.

## Plugin Error Handling Architecture

Plugin errors flow as structured `PluginErrorData` over IPC and are caught as `PluginError` instances on the CLI side:

**IPC envelope** (`@codifycli/schemas`):
```typescript
interface PluginErrorData {
errorType: string; // 'apply_validation' | 'sudo_error' | 'unknown'
message: string;
data?: unknown;
}
```

**CLI carrier** (`src/common/errors.ts`): `PluginError extends CodifyError` holds `pluginName`, `resourceType`, and `errorData: PluginErrorData`.

**Reporter as view model**: Reporters (not components) decide how to render each `errorType`. `DefaultReporter.displayPluginError()` branches on `errorType` to set the appropriate `RenderStatus` (`APPLY_VALIDATION_ERROR` with a `ResourcePlan` for plan diffs, `PLUGIN_ERROR` with a message string for generic errors). The `DefaultComponent` is purely display.

**Shared formatter**: `src/ui/plugin-error-formatter.ts` exports `formatApplyValidationError(error: PluginError): string` used by both `PlainReporter` and `DefaultComponent`.

**Backward compat**: `plugin.ts#toErrorData()` validates IPC data against `ErrorResponseDataSchema` (AJV); falls back to `{ errorType: 'unknown', message: data }` for old plugins sending bare strings.
66 changes: 26 additions & 40 deletions package-lock.json

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

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
},
"dependencies": {
"@codifycli/ink-form": "0.0.12",
"@codifycli/schemas": "1.0.0",
"@codifycli/schemas": "1.1.0-beta8",
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
"@inkjs/ui": "^2",
"@mischnic/json-sourcemap": "^0.1.1",
"@oclif/core": "^4.0.8",
"@oclif/plugin-autocomplete": "^3.2.24",
Expand Down Expand Up @@ -44,7 +43,7 @@
},
"description": "Codify is a configuration-as-code tool that declaratively installs and manages developer tools and applications. Check out https://dashboard.codifycli.com for an editor.",
"devDependencies": {
"@codifycli/plugin-core": "^1.0.0",
"@codifycli/plugin-core": "^1.1.0-beta19",
"@oclif/prettier-config": "^0.2.1",
"@types/chalk": "^2.2.0",
"@types/cors": "^2.8.19",
Expand Down Expand Up @@ -128,6 +127,7 @@
},
"repository": "codifycli/codify",
"scripts": {
"postinstall": "[ -f node_modules/oclif/lib/tarballs/bin.js ] && tsx scripts/patch-oclif.ts || true",
"build": "shx rm -rf dist && tsc -b",
"build:release": "npm run pkg && ./scripts/notarize.sh",
"lint": "tsc",
Expand All @@ -145,7 +145,7 @@
"deploy": "npm run pkg && npm run notarize && npm run upload",
"prepublishOnly": "npm run build"
},
"version": "1.0.2",
"version": "1.1.0-beta.3",
"bugs": "https://github.com/codifycli/codify/issues",
"keywords": [
"oclif",
Expand Down
2 changes: 2 additions & 0 deletions scripts/install-beta.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
fi

mkdir -p /usr/local/lib
mkdir -p /usr/local/bin

cd /usr/local/lib
rm -rf codify
rm -rf ~/.local/share/codify/client
Expand Down
2 changes: 2 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
fi

mkdir -p /usr/local/lib
mkdir -p /usr/local/bin

cd /usr/local/lib
rm -rf codify
rm -rf ~/.local/share/codify/client
Expand Down
132 changes: 132 additions & 0 deletions scripts/patch-oclif.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Patches node_modules/oclif/lib/tarballs/bin.js to inject bash logic into the shell script
// that oclif generates during `oclif pack tarballs`. This runs via the `postinstall` npm script
// so it re-applies automatically after any `npm install` that updates oclif.
//
// Why: Node.js takes 500ms–1s to start. By handling simple cases in the shell script we can
// give instant feedback before Node launches.
//
// What the injected bash does (inside the else block, before the "$NODE ... $DIR/run" line):
// - codify --help / -h → cats dist/static/help.txt and exits (no Node startup)
// - codify <cmd> --help / -h → cats dist/static/<cmd>-help.txt and exits
// - codify --version / -v → cats dist/static/version.txt and exits
// - codify apply/destroy/plan → prints "Running Codify <cmd>..." immediately
// (suppressed when --output json or -o json is passed)
// - everything else → falls through to normal Node.js launch
//
// Static files (dist/static/*.txt) are generated in scripts/pkg.ts after the esbuild step.
// Missing static files are guarded by [ -f ] so all cases fall back to Node gracefully.
//
// Note: console.log('Running Codify apply/destroy...') was removed from src/commands/apply.ts
// and src/commands/destroy.ts to prevent double-printing (shell prints first, Node would repeat it).
//
// Also patches node_modules/oclif/lib/commands/pack/macos.js to add
// `sudo rm -rf ~/.local/share/codify` to the macOS installer's preinstall script.
// This fixes an oclif bug where the auto-updater cache (~/.local/share/codify) isn't cleared
// on fresh installs, causing the old cached version to be used. The patch must happen before
// `oclif pack macos` runs — modifying the .pkg after the fact breaks notarization.
//
// If oclif upgrades and changes either file's structure, this script exits with code 1 so the
// breakage is immediately visible.
import { existsSync } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BIN_JS = path.join(__dirname, '../node_modules/oclif/lib/tarballs/bin.js');
const MACOS_JS = path.join(__dirname, '../node_modules/oclif/lib/commands/pack/macos.js');

if (!existsSync(BIN_JS)) {
console.log('oclif bin.js not found (likely production install). Skipping.');
process.exit(0);
}

let content = await fs.readFile(BIN_JS, 'utf8');

if (content.includes('CODIFY_PATCH_START')) {
console.log('Removing existing patch to reapply...');
content = content.replace(/ # CODIFY_PATCH_START[\s\S]*?# CODIFY_PATCH_END[^\n]*\n/, '');
}

const SEARCH = ' if [ "\\$DEBUG" == "*" ]; then\n echoerr';
const idx = content.lastIndexOf(SEARCH);
if (idx === -1) {
console.error('ERROR: Could not find insertion point in oclif bin.js. The oclif version may have changed.');
process.exit(1);
}

// Patch uses \\$ so that it survives the JS string — in the generated shell script each \\$ becomes \$
// which bash then interprets as a literal $ (not a template substitution in the JS template literal).
// Bash default-value syntax ${1:-} is avoided since ${...} would be evaluated as a JS template expression.
const PATCH = ` # CODIFY_PATCH_START — do not remove this marker
_first_arg=""
if [ "\\$#" -gt 0 ]; then _first_arg="\\$1"; fi
_second_arg=""
if [ "\\$#" -gt 1 ]; then _second_arg="\\$2"; fi
if [ "\\$_first_arg" = "--help" ] || [ "\\$_first_arg" = "-h" ]; then
_help_file="\\$DIR/../dist/static/help.txt"
if [ -f "\\$_help_file" ]; then cat "\\$_help_file"; exit 0; fi
fi
if [ "\\$_second_arg" = "--help" ] || [ "\\$_second_arg" = "-h" ]; then
_cmd_help_file="\\$DIR/../dist/static/\\$_first_arg-help.txt"
if [ -f "\\$_cmd_help_file" ]; then cat "\\$_cmd_help_file"; exit 0; fi
fi
if [ "\\$_first_arg" = "--version" ] || [ "\\$_first_arg" = "-v" ] || [ "\\$_first_arg" = "version" ]; then
_version_file="\\$DIR/../dist/static/version.txt"
if [ -f "\\$_version_file" ]; then cat "\\$_version_file"; exit 0; fi
fi
_cmd="\\$_first_arg"
if [ "\\$_cmd" = "apply" ] || [ "\\$_cmd" = "destroy" ] || [ "\\$_cmd" = "plan" ]; then
_json_output=0
_prev=""
for _a in "\\$@"; do
if [ "\\$_a" = "--output=json" ] || [ "\\$_a" = "-o=json" ]; then _json_output=1; break; fi
if [ "\\$_prev" = "--output" ] || [ "\\$_prev" = "-o" ]; then
if [ "\\$_a" = "json" ]; then _json_output=1; break; fi
fi
_prev="\\$_a"
done
if [ "\\$_json_output" -eq 0 ]; then echo "Running Codify \\$_cmd..."; fi
fi
# CODIFY_PATCH_END — do not remove this marker
`;

const patched = content.slice(0, idx) + PATCH + content.slice(idx);

// Use exec to replace the shell process with Node rather than spawning a child.
// This avoids an extra process in memory and ensures signals go directly to Node.
const NODE_LAUNCH = ' "\\$NODE" ';
const NODE_LAUNCH_EXEC = ' exec "\\$NODE" ';
let withExec = patched;
if (patched.includes(NODE_LAUNCH) && !patched.includes(NODE_LAUNCH_EXEC)) {
withExec = patched.replace(NODE_LAUNCH, NODE_LAUNCH_EXEC);
} else if (!patched.includes(NODE_LAUNCH_EXEC)) {
console.error('ERROR: Could not find Node launch line to add exec. The oclif version may have changed.');
process.exit(1);
}

await fs.writeFile(BIN_JS, withExec, 'utf8');
console.log('Successfully patched oclif bin.js');

// Patch macos.js preinstall script to also clear the auto-updater cache directory.
// Oclif's auto-updater stores binaries in ~/.local/share/codify and the macOS installer
// doesn't clean this up, so fresh installs still run the old cached version.
// We must patch the template before `oclif pack macos` runs — modifying the .pkg after
// the fact breaks notarization since the binary has been tampered with.
const SEARCH_PREINSTALL = 'sudo rm -rf /usr/local/bin/${config.bin}\n${additionalCLI';
const PATCH_PREINSTALL = 'sudo rm -rf /usr/local/bin/${config.bin}\nsudo rm -rf ~/.local/share/${config.dirname}\n${additionalCLI';

if (!existsSync(MACOS_JS)) {
console.log('oclif macos.js not found. Skipping preinstall patch.');
} else {
const macosContent = await fs.readFile(MACOS_JS, 'utf8');
if (macosContent.includes(PATCH_PREINSTALL)) {
console.log('oclif macos.js preinstall already patched. Skipping.');
} else if (!macosContent.includes(SEARCH_PREINSTALL)) {
console.error('ERROR: Could not find preinstall insertion point in oclif macos.js. The oclif version may have changed.');
process.exit(1);
} else {
await fs.writeFile(MACOS_JS, macosContent.replace(SEARCH_PREINSTALL, PATCH_PREINSTALL), 'utf8');
console.log('Successfully patched oclif macos.js preinstall script');
}
}
Loading
Loading