Skip to content

Commit efeaac1

Browse files
Kevin/v1.1.0 (#63)
* feat: Added includeSensitive flag for remote inits * feat: Added distro filtering * feat: Add verbosity toggle * feat: Add defaulting to plain reporter if tty is not available * feat: Added auto approve flag for scripts * feat: Added toggle for adding sudo password during apply * feat: Added toggle for adding sudo password during apply * feat: Changed to custom component * feat: Changed to custom components intead of inkjs * feat: Added checking state to password input * Made the password input for plugins un-cancellable * Made updateRenderState async and render to nothing first to avoid memory crashes. Improved the progress output for destroy * Couple of fixes: - Previous sudo check blocked the main thread - Move title of sudo inside of the box - Make sure not to cache password for regular plugin requests * Refactoring to fix structural issues and improvements * feat: Added sudoAskpass support. Removed unnecessary log event emitter. Changed raw logs to not be cyan * feat: Refactoring setRawMode and disableRawMode into the reporter * fix: Fix password dialog not disappearing for plugin sudo requests * feat: Add verbosity level for commands ran on local cli * feat: Improved log output * feat: Improved log output (add strip ansi) * feat: Added CLI version to plugin search. This gates breaking changes and prevents them from breaking older versions * feat: Added terminal resize handler * Kevin/improved errors (#61) * feat: Improved apply validation errors * feat: Refactored to share the same plugin error type. Use the reporter to determine how to present it. * fix: Fixed display functions to be async * feat: Improve the copy and formatting for apply validation error. Fix pretty print error for NOOP. Added to CLAUDE.md * feat: Improved copy again * feat: Added partial applys. If a resource fails to apply, skip it and all transitive dependent resources. Apply un-related resources. * feat: Further refactored the error messages. Combined it with a final display message change. The final display message will now tell the user a list of all changes that were made. * feat: refactored the code so that the final apply message is handled entirely inside the reporters. * feat: Additional refactoring. Cleaned up dead APPLY_VALIDATION_ERROR dead code. Moved rendering logic to the ApplyComplete component instead. * fix: Fix /etc/os-release not present * Kevin/improve launcher-and-destroy (#62) * feat: Patch the launcher script to handle simple tasks so that the NodeJS startup delay is less noticeable. * fix: For unescaped brackets in the patch file * fix: Added static help files for top level commands as well: codify apply, codify plan, codify ... * feat: Patch the NodeJS subshell launch in the bin script as well. Replace with exec * feat: Added MacOS pkg installer patching as well. Removed it from the pkg script. * feat: Added destroy to connect * feat: Improved subprogress display * feat: Improved edit to install the codify desktop app instead * feat: refactored and improve the new edit changes * feat: improve installation script to create /usr/local/bin if it doesn't exist * feat: added another QOL change to prevent sleep while codify apply or destroy is running * feat: improved text for sudo password and progress display * feat: added status effects for sub-progresses * fix: ws.close instead of ws.terminate. Improved progress display * feat: added skip banner flag to init command * feat: add test fixes * feat: add skip banner flag to connect * feat: added additional commands to quick launch message * feat: added log messages for any commands that failed * feat: improved error logs. Made it visually more appealing * feat: improve the summary list to remove noops and skips * feat: remove applying from log...
1 parent 61b3e8f commit efeaac1

71 files changed

Lines changed: 2028 additions & 457 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
node_modules
1212
oclif.manifest.json
1313
.env
14+
.codify-files

CLAUDE.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,27 @@ Parent Process Plugin Process
184184
3. **Plugin IPC**: Plugins cannot directly read stdin (security isolation)
185185
4. **Sudo Caching**: Password cached in memory during session unless `--secure` flag used
186186
5. **File Watcher**: Use `persistent: false` option to prevent hanging processes
187-
6. **Linting**: ESLint enforces single quotes, specific import ordering, and strict type safety
187+
6. **Linting**: ESLint enforces single quotes, specific import ordering, and strict type safety
188+
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.
189+
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.
190+
191+
## Plugin Error Handling Architecture
192+
193+
Plugin errors flow as structured `PluginErrorData` over IPC and are caught as `PluginError` instances on the CLI side:
194+
195+
**IPC envelope** (`@codifycli/schemas`):
196+
```typescript
197+
interface PluginErrorData {
198+
errorType: string; // 'apply_validation' | 'sudo_error' | 'unknown'
199+
message: string;
200+
data?: unknown;
201+
}
202+
```
203+
204+
**CLI carrier** (`src/common/errors.ts`): `PluginError extends CodifyError` holds `pluginName`, `resourceType`, and `errorData: PluginErrorData`.
205+
206+
**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.
207+
208+
**Shared formatter**: `src/ui/plugin-error-formatter.ts` exports `formatApplyValidationError(error: PluginError): string` used by both `PlainReporter` and `DefaultComponent`.
209+
210+
**Backward compat**: `plugin.ts#toErrorData()` validates IPC data against `ErrorResponseDataSchema` (AJV); falls back to `{ errorType: 'unknown', message: data }` for old plugins sending bare strings.

package-lock.json

Lines changed: 26 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
},
66
"dependencies": {
77
"@codifycli/ink-form": "0.0.12",
8-
"@codifycli/schemas": "1.0.0",
8+
"@codifycli/schemas": "1.1.0-beta8",
99
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
10-
"@inkjs/ui": "^2",
1110
"@mischnic/json-sourcemap": "^0.1.1",
1211
"@oclif/core": "^4.0.8",
1312
"@oclif/plugin-autocomplete": "^3.2.24",
@@ -44,7 +43,7 @@
4443
},
4544
"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.",
4645
"devDependencies": {
47-
"@codifycli/plugin-core": "^1.0.0",
46+
"@codifycli/plugin-core": "^1.1.0-beta19",
4847
"@oclif/prettier-config": "^0.2.1",
4948
"@types/chalk": "^2.2.0",
5049
"@types/cors": "^2.8.19",
@@ -128,6 +127,7 @@
128127
},
129128
"repository": "codifycli/codify",
130129
"scripts": {
130+
"postinstall": "[ -f node_modules/oclif/lib/tarballs/bin.js ] && tsx scripts/patch-oclif.ts || true",
131131
"build": "shx rm -rf dist && tsc -b",
132132
"build:release": "npm run pkg && ./scripts/notarize.sh",
133133
"lint": "tsc",
@@ -145,7 +145,7 @@
145145
"deploy": "npm run pkg && npm run notarize && npm run upload",
146146
"prepublishOnly": "npm run build"
147147
},
148-
"version": "1.0.2",
148+
"version": "1.1.0-beta.4",
149149
"bugs": "https://github.com/codifycli/codify/issues",
150150
"keywords": [
151151
"oclif",

scripts/install-beta.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
fi
4545
4646
mkdir -p /usr/local/lib
47+
mkdir -p /usr/local/bin
48+
4749
cd /usr/local/lib
4850
rm -rf codify
4951
rm -rf ~/.local/share/codify/client

scripts/install.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
fi
4545
4646
mkdir -p /usr/local/lib
47+
mkdir -p /usr/local/bin
48+
4749
cd /usr/local/lib
4850
rm -rf codify
4951
rm -rf ~/.local/share/codify/client

scripts/patch-oclif.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Patches node_modules/oclif/lib/tarballs/bin.js to inject bash logic into the shell script
2+
// that oclif generates during `oclif pack tarballs`. This runs via the `postinstall` npm script
3+
// so it re-applies automatically after any `npm install` that updates oclif.
4+
//
5+
// Why: Node.js takes 500ms–1s to start. By handling simple cases in the shell script we can
6+
// give instant feedback before Node launches.
7+
//
8+
// What the injected bash does (inside the else block, before the "$NODE ... $DIR/run" line):
9+
// - codify --help / -h → cats dist/static/help.txt and exits (no Node startup)
10+
// - codify <cmd> --help / -h → cats dist/static/<cmd>-help.txt and exits
11+
// - codify --version / -v → cats dist/static/version.txt and exits
12+
// - codify apply/destroy/plan → prints "Running Codify <cmd>..." immediately
13+
// (suppressed when --output json or -o json is passed)
14+
// - everything else → falls through to normal Node.js launch
15+
//
16+
// Static files (dist/static/*.txt) are generated in scripts/pkg.ts after the esbuild step.
17+
// Missing static files are guarded by [ -f ] so all cases fall back to Node gracefully.
18+
//
19+
// Note: console.log('Running Codify apply/destroy...') was removed from src/commands/apply.ts
20+
// and src/commands/destroy.ts to prevent double-printing (shell prints first, Node would repeat it).
21+
//
22+
// Also patches node_modules/oclif/lib/commands/pack/macos.js to add
23+
// `sudo rm -rf ~/.local/share/codify` to the macOS installer's preinstall script.
24+
// This fixes an oclif bug where the auto-updater cache (~/.local/share/codify) isn't cleared
25+
// on fresh installs, causing the old cached version to be used. The patch must happen before
26+
// `oclif pack macos` runs — modifying the .pkg after the fact breaks notarization.
27+
//
28+
// If oclif upgrades and changes either file's structure, this script exits with code 1 so the
29+
// breakage is immediately visible.
30+
import { existsSync } from 'node:fs';
31+
import fs from 'node:fs/promises';
32+
import path from 'node:path';
33+
import { fileURLToPath } from 'node:url';
34+
35+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
36+
const BIN_JS = path.join(__dirname, '../node_modules/oclif/lib/tarballs/bin.js');
37+
const MACOS_JS = path.join(__dirname, '../node_modules/oclif/lib/commands/pack/macos.js');
38+
39+
if (!existsSync(BIN_JS)) {
40+
console.log('oclif bin.js not found (likely production install). Skipping.');
41+
process.exit(0);
42+
}
43+
44+
let content = await fs.readFile(BIN_JS, 'utf8');
45+
46+
if (content.includes('CODIFY_PATCH_START')) {
47+
console.log('Removing existing patch to reapply...');
48+
content = content.replace(/ # CODIFY_PATCH_START[\s\S]*?# CODIFY_PATCH_END[^\n]*\n/, '');
49+
}
50+
51+
const SEARCH = ' if [ "\\$DEBUG" == "*" ]; then\n echoerr';
52+
const idx = content.lastIndexOf(SEARCH);
53+
if (idx === -1) {
54+
console.error('ERROR: Could not find insertion point in oclif bin.js. The oclif version may have changed.');
55+
process.exit(1);
56+
}
57+
58+
// Patch uses \\$ so that it survives the JS string — in the generated shell script each \\$ becomes \$
59+
// which bash then interprets as a literal $ (not a template substitution in the JS template literal).
60+
// Bash default-value syntax ${1:-} is avoided since ${...} would be evaluated as a JS template expression.
61+
const PATCH = ` # CODIFY_PATCH_START — do not remove this marker
62+
_first_arg=""
63+
if [ "\\$#" -gt 0 ]; then _first_arg="\\$1"; fi
64+
_second_arg=""
65+
if [ "\\$#" -gt 1 ]; then _second_arg="\\$2"; fi
66+
if [ "\\$_first_arg" = "--help" ] || [ "\\$_first_arg" = "-h" ]; then
67+
_help_file="\\$DIR/../dist/static/help.txt"
68+
if [ -f "\\$_help_file" ]; then cat "\\$_help_file"; exit 0; fi
69+
fi
70+
if [ "\\$_second_arg" = "--help" ] || [ "\\$_second_arg" = "-h" ]; then
71+
_cmd_help_file="\\$DIR/../dist/static/\\$_first_arg-help.txt"
72+
if [ -f "\\$_cmd_help_file" ]; then cat "\\$_cmd_help_file"; exit 0; fi
73+
fi
74+
if [ "\\$_first_arg" = "--version" ] || [ "\\$_first_arg" = "-v" ] || [ "\\$_first_arg" = "version" ]; then
75+
_version_file="\\$DIR/../dist/static/version.txt"
76+
if [ -f "\\$_version_file" ]; then cat "\\$_version_file"; exit 0; fi
77+
fi
78+
_cmd="\\$_first_arg"
79+
if [ "\\$_cmd" = "apply" ] || [ "\\$_cmd" = "destroy" ] || [ "\\$_cmd" = "plan" ] || [ "\\$_cmd" = "login" ] || [ "\\$_cmd" = "logout" ] || [ "\\$_cmd" = "import" ] || [ "\\$_cmd" = "refresh" ] || [ "\\$_cmd" = "init" ] || [ "\\$_cmd" = "validate" ] || [ "\\$_cmd" = "test" ] || [ "\\$_cmd" = "edit" ] || [ "\\$_cmd" = "connect" ]; then
80+
_json_output=0
81+
_prev=""
82+
for _a in "\\$@"; do
83+
if [ "\\$_a" = "--output=json" ] || [ "\\$_a" = "-o=json" ]; then _json_output=1; break; fi
84+
if [ "\\$_prev" = "--output" ] || [ "\\$_prev" = "-o" ]; then
85+
if [ "\\$_a" = "json" ]; then _json_output=1; break; fi
86+
fi
87+
_prev="\\$_a"
88+
done
89+
if [ "\\$_json_output" -eq 0 ]; then echo "Running Codify \\$_cmd..."; fi
90+
fi
91+
# CODIFY_PATCH_END — do not remove this marker
92+
`;
93+
94+
const patched = content.slice(0, idx) + PATCH + content.slice(idx);
95+
96+
// Use exec to replace the shell process with Node rather than spawning a child.
97+
// This avoids an extra process in memory and ensures signals go directly to Node.
98+
const NODE_LAUNCH = ' "\\$NODE" ';
99+
const NODE_LAUNCH_EXEC = ' exec "\\$NODE" ';
100+
let withExec = patched;
101+
if (patched.includes(NODE_LAUNCH) && !patched.includes(NODE_LAUNCH_EXEC)) {
102+
withExec = patched.replace(NODE_LAUNCH, NODE_LAUNCH_EXEC);
103+
} else if (!patched.includes(NODE_LAUNCH_EXEC)) {
104+
console.error('ERROR: Could not find Node launch line to add exec. The oclif version may have changed.');
105+
process.exit(1);
106+
}
107+
108+
await fs.writeFile(BIN_JS, withExec, 'utf8');
109+
console.log('Successfully patched oclif bin.js');
110+
111+
// Patch macos.js preinstall script to also clear the auto-updater cache directory.
112+
// Oclif's auto-updater stores binaries in ~/.local/share/codify and the macOS installer
113+
// doesn't clean this up, so fresh installs still run the old cached version.
114+
// We must patch the template before `oclif pack macos` runs — modifying the .pkg after
115+
// the fact breaks notarization since the binary has been tampered with.
116+
const SEARCH_PREINSTALL = 'sudo rm -rf /usr/local/bin/${config.bin}\n${additionalCLI';
117+
const PATCH_PREINSTALL = 'sudo rm -rf /usr/local/bin/${config.bin}\nsudo rm -rf ~/.local/share/${config.dirname}\n${additionalCLI';
118+
119+
if (!existsSync(MACOS_JS)) {
120+
console.log('oclif macos.js not found. Skipping preinstall patch.');
121+
} else {
122+
const macosContent = await fs.readFile(MACOS_JS, 'utf8');
123+
if (macosContent.includes(PATCH_PREINSTALL)) {
124+
console.log('oclif macos.js preinstall already patched. Skipping.');
125+
} else if (!macosContent.includes(SEARCH_PREINSTALL)) {
126+
console.error('ERROR: Could not find preinstall insertion point in oclif macos.js. The oclif version may have changed.');
127+
process.exit(1);
128+
} else {
129+
await fs.writeFile(MACOS_JS, macosContent.replace(SEARCH_PREINSTALL, PATCH_PREINSTALL), 'utf8');
130+
console.log('Successfully patched oclif macos.js preinstall script');
131+
}
132+
}

0 commit comments

Comments
 (0)