|
| 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" ]; 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