Skip to content

Commit feaaf32

Browse files
kevinwang5658test2
andauthored
Kevin/v1.2.0 (#64)
* fix: fixed codify edit desktop installation. Fixed apply message * chore: update download links for desktop app * feat: Added apply notes system * feat: Made modifications to apply note. Improved the pretty printing of object plans * feat: Changed the diff display to be a structural diff * feat: Improved plan printing more * fix: noops in object plans * feat: added shell check for plugins so that we know that shell output is clean before proceeding to running plugins * feat: added disable auto correct for zsh shells * feat: improved name and id matching for dependsOn. Allow type only, name only, and type + name matching. Also fix bug with dependsOn and distro and os filters. * fix: error key issue. Improved codify test to resolve current latest cli version * fix: stdin mode not working for scirpts that actually require stdin. Fix raw mode as well * fix: pkg for the new patch ink script * feat: improve stdin ux * feat: show cursor when stdin is true * fix: raw mode fixes * chore: bump version * testing test fixes * testing test fixes * testing test fixes * Fix initialize tests * fix: ink tests --------- Co-authored-by: test2 <test2@test.com>
1 parent 1527a48 commit feaaf32

29 files changed

Lines changed: 1300 additions & 128 deletions

package-lock.json

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

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
},
66
"dependencies": {
77
"@codifycli/ink-form": "0.0.12",
8-
"@codifycli/schemas": "1.1.0-beta8",
9-
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
8+
"@codifycli/schemas": "1.2.0",
9+
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
1010
"@mischnic/json-sourcemap": "^0.1.1",
1111
"@oclif/core": "^4.0.8",
1212
"@oclif/plugin-autocomplete": "^3.2.24",
@@ -15,6 +15,7 @@
1515
"ajv": "^8.12.0",
1616
"ajv-formats": "^3.0.1",
1717
"chalk": "^5.3.0",
18+
"cli-cursor": "^4.0.0",
1819
"cli-spinners": "^3.4.0",
1920
"cors": "^2.8.5",
2021
"debug": "^4.3.4",
@@ -43,7 +44,7 @@
4344
},
4445
"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.",
4546
"devDependencies": {
46-
"@codifycli/plugin-core": "^1.1.0-beta19",
47+
"@codifycli/plugin-core": "^1.2.0",
4748
"@oclif/prettier-config": "^0.2.1",
4849
"@types/chalk": "^2.2.0",
4950
"@types/cors": "^2.8.19",
@@ -127,7 +128,7 @@
127128
},
128129
"repository": "codifycli/codify",
129130
"scripts": {
130-
"postinstall": "[ -f node_modules/oclif/lib/tarballs/bin.js ] && tsx scripts/patch-oclif.ts || true",
131+
"postinstall": "[ -f node_modules/oclif/lib/tarballs/bin.js ] && tsx scripts/patch-oclif.ts || true; [ -f dist/patch-ink.mjs ] && node dist/patch-ink.mjs || true",
131132
"build": "shx rm -rf dist && tsc -b",
132133
"build:release": "npm run pkg && ./scripts/notarize.sh",
133134
"lint": "tsc",
@@ -145,7 +146,7 @@
145146
"deploy": "npm run pkg && npm run notarize && npm run upload",
146147
"prepublishOnly": "npm run build"
147148
},
148-
"version": "1.1.0",
149+
"version": "1.2.1",
149150
"bugs": "https://github.com/codifycli/codify/issues",
150151
"keywords": [
151152
"oclif",

scripts/patch-ink.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Patches ink to add suspendStdin/resumeStdin to the render() return value.
2+
//
3+
// Why: when Codify needs to hand a raw PTY session to the user (e.g. `gh auth login`),
4+
// Ink's internal 'readable' listener on process.stdin must be fully removed and the
5+
// libuv fd watcher released before our stdinListener can receive data events. Simply
6+
// removing the listener isn't enough — the stream stays in pull-mode and libuv stops
7+
// polling fd 0. The only clean fix is to let Ink tear down stdin via its own internal
8+
// handleSetRawMode(false) path (which calls stdin.unref()), then re-add it afterward.
9+
//
10+
// What the patch adds:
11+
// App.js — suspendStdin() calls handleSetRawMode(false) to fully release stdin;
12+
// resumeStdin() calls handleSetRawMode(true) to restore it.
13+
// ink.js — suspendStdin()/resumeStdin() methods that delegate to the App instance.
14+
// render.js — includes suspendStdin/resumeStdin in the return value of render().
15+
16+
import fs from 'node:fs/promises';
17+
import path from 'node:path';
18+
import { fileURLToPath } from 'node:url';
19+
import { existsSync } from 'node:fs';
20+
21+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
22+
const INK_DIR = path.join(__dirname, '../node_modules/ink/build');
23+
const APP_JS = path.join(INK_DIR, 'components/App.js');
24+
const INK_JS = path.join(INK_DIR, 'ink.js');
25+
const RENDER_JS = path.join(INK_DIR, 'render.js');
26+
27+
if (!existsSync(APP_JS)) {
28+
console.log('ink App.js not found. Skipping.');
29+
process.exit(0);
30+
}
31+
32+
// ── Patch App.js ─────────────────────────────────────────────────────────────
33+
let appContent = await fs.readFile(APP_JS, 'utf8');
34+
35+
if (!appContent.includes('CODIFY_INK_PATCH')) {
36+
// Remove any orphaned fragments from previous partial patch attempts
37+
appContent = appContent.replace(/\n stdin\.setRawMode\(false\);\n stdin\.removeListener\('readable', this\.handleReadable\);\n stdin\.unref\(\);\n \}\n \};\n resumeStdin[\s\S]*?\};\n(?= \/\/ CODIFY_INK_PATCH)/, '\n');
38+
39+
// Also patch handleSetRawMode to snapshot kState before addListener('readable')
40+
const SNAPSHOT_SEARCH = 'stdin.addListener(\'readable\', this.handleReadable);';
41+
const snapshotIdx = appContent.indexOf(SNAPSHOT_SEARCH);
42+
if (snapshotIdx !== -1) {
43+
const SNAPSHOT_PATCH = `const _ks = Object.getOwnPropertySymbols(stdin._readableState)[0];
44+
if (_ks !== undefined) { this._kStateBeforeReadable = stdin._readableState[_ks]; }
45+
`;
46+
appContent = appContent.slice(0, snapshotIdx) + SNAPSHOT_PATCH + appContent.slice(snapshotIdx);
47+
}
48+
49+
// Insert suspendStdin/resumeStdin just before the closing brace of the class
50+
const SEARCH = 'findPreviousFocusable = (state) => {';
51+
const idx = appContent.indexOf(SEARCH);
52+
if (idx === -1) {
53+
console.error('ERROR: Could not find insertion point in ink App.js.');
54+
process.exit(1);
55+
}
56+
57+
const PATCH = `// CODIFY_INK_PATCH — suspendStdin/resumeStdin
58+
_kStateBeforeReadable = undefined;
59+
suspendStdin = () => {
60+
if (this.isRawModeSupported() && this.rawModeEnabledCount > 0) {
61+
const { stdin } = this.props;
62+
stdin.setRawMode(false);
63+
stdin.removeListener('readable', this.handleReadable);
64+
stdin.unref();
65+
// In Node 24, removeListener does not clear internal kState bits set by
66+
// addListener('readable'), so isPaused() stays true and resume() won't
67+
// switch to flowing mode. Restore the kState value from before Ink added
68+
// its readable listener to fully undo the listener registration.
69+
const kState = Object.getOwnPropertySymbols(stdin._readableState)[0];
70+
if (kState !== undefined && this._kStateBeforeReadable !== undefined) {
71+
stdin._readableState[kState] = this._kStateBeforeReadable;
72+
}
73+
}
74+
};
75+
resumeStdin = () => {
76+
if (this.isRawModeSupported() && this.rawModeEnabledCount > 0) {
77+
const { stdin } = this.props;
78+
stdin.ref();
79+
stdin.setRawMode(true);
80+
stdin.setEncoding('utf8');
81+
stdin.addListener('readable', this.handleReadable);
82+
}
83+
};
84+
`;
85+
86+
appContent = appContent.slice(0, idx) + PATCH + appContent.slice(idx);
87+
await fs.writeFile(APP_JS, appContent, 'utf8');
88+
console.log('Patched ink App.js');
89+
} else {
90+
console.log('ink App.js already patched. Skipping.');
91+
}
92+
93+
// ── Patch ink.js ─────────────────────────────────────────────────────────────
94+
let inkContent = await fs.readFile(INK_JS, 'utf8');
95+
96+
if (!inkContent.includes('suspendStdin')) {
97+
// Add suspendStdin/resumeStdin methods that reach into the React tree via the
98+
// container's current fiber to call the App component's methods.
99+
// Simpler approach: store the App ref. But since we don't have a ref, we access
100+
// the fiber's stateNode. Add methods to the Ink class that call into the container.
101+
const SEARCH = 'async waitUntilExit() {';
102+
const idx = inkContent.indexOf(SEARCH);
103+
if (idx === -1) {
104+
console.error('ERROR: Could not find insertion point in ink ink.js.');
105+
process.exit(1);
106+
}
107+
108+
const PATCH = `suspendStdin() {
109+
// Walk the fiber tree to find the App component instance and call suspendStdin
110+
let fiber = this.container.current;
111+
while (fiber) {
112+
if (fiber.stateNode && typeof fiber.stateNode.suspendStdin === 'function') {
113+
fiber.stateNode.suspendStdin();
114+
return;
115+
}
116+
fiber = fiber.child;
117+
}
118+
}
119+
resumeStdin() {
120+
let fiber = this.container.current;
121+
while (fiber) {
122+
if (fiber.stateNode && typeof fiber.stateNode.resumeStdin === 'function') {
123+
fiber.stateNode.resumeStdin();
124+
return;
125+
}
126+
fiber = fiber.child;
127+
}
128+
}
129+
pauseRendering() {
130+
// Temporarily stop all stdout writes without tearing down the React tree.
131+
this.isUnmounted = true;
132+
this.log.clear();
133+
}
134+
resumeRendering() {
135+
this.isUnmounted = false;
136+
this.onRender();
137+
}
138+
`;
139+
140+
inkContent = inkContent.slice(0, idx) + PATCH + inkContent.slice(idx);
141+
await fs.writeFile(INK_JS, inkContent, 'utf8');
142+
console.log('Patched ink ink.js');
143+
} else {
144+
console.log('ink ink.js already patched. Skipping.');
145+
}
146+
147+
// ── Patch render.js ───────────────────────────────────────────────────────────
148+
let renderContent = await fs.readFile(RENDER_JS, 'utf8');
149+
150+
if (!renderContent.includes('suspendStdin')) {
151+
const SEARCH = 'clear: instance.clear,';
152+
const idx = renderContent.indexOf(SEARCH);
153+
if (idx === -1) {
154+
console.error('ERROR: Could not find insertion point in ink render.js.');
155+
process.exit(1);
156+
}
157+
158+
const PATCH = `clear: instance.clear,
159+
suspendStdin: instance.suspendStdin.bind(instance),
160+
resumeStdin: instance.resumeStdin.bind(instance),
161+
pauseRendering: instance.pauseRendering.bind(instance),
162+
resumeRendering: instance.resumeRendering.bind(instance),`;
163+
164+
renderContent = renderContent.slice(0, idx) + PATCH + renderContent.slice(idx + SEARCH.length);
165+
await fs.writeFile(RENDER_JS, renderContent, 'utf8');
166+
console.log('Patched ink render.js');
167+
} else {
168+
console.log('ink render.js already patched. Skipping.');
169+
}

scripts/pkg.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import chalk from 'chalk'
22
import { execSync } from 'node:child_process'
33
import fs from 'node:fs/promises'
4-
import path from 'node:path';
54

65
// Create .build folder if it does not exist
76
try {
@@ -27,6 +26,13 @@ await Promise.all([
2726
fs.cp('README.md', './.build/README.md'),
2827
]);
2928

29+
console.log(chalk.magenta('Compiling patch-ink.ts to .build/dist/patch-ink.mjs'))
30+
execSync(
31+
'tsc --module nodenext --moduleResolution nodenext --target es2022 --outDir .build/dist scripts/patch-ink.ts',
32+
{ shell: 'zsh' }
33+
);
34+
await fs.rename('./.build/dist/patch-ink.js', './.build/dist/patch-ink.mjs');
35+
3036
console.log(chalk.magenta('Esbuild src'))
3137
execSync('tsx esbuild.ts', { shell: 'zsh' })
3238

src/common/base-command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export abstract class BaseCommand extends Command {
7777
}
7878

7979
if (data.options.stdin) {
80-
console.log(chalk.blue(`Plugin "${pluginName}" is requesting stdin`));
80+
console.log(chalk.bgBlue.bold(' Your input may be needed '));
8181
await this.reporter.setRawMode();
8282
}
8383

0 commit comments

Comments
 (0)