installer: add node-based install/upgrade/doctor for Codex/OpenCode#409
installer: add node-based install/upgrade/doctor for Codex/OpenCode#409
Conversation
…ate INSTALL docs to use flags
📝 WalkthroughWalkthroughThese changes introduce a comprehensive installer-core module that manages Git-based central repository cloning, IDE-specific symlink configuration, and health checks. The module supports install, upgrade, and doctor operations for three IDE environments (Codex, OpenCode, and Kilocode) with Git operations, filesystem management, and configuration parsing. Changes
Sequence DiagramsequenceDiagram
participant CLI as CLI Handler
participant Config as Config Resolver
participant Git as Git Operations
participant FS as Filesystem
participant Symlink as Symlink Manager
CLI->>Config: Resolve install config (repo, ref, dir)
Config->>Config: Merge defaults with arguments
Config-->>CLI: Return config
CLI->>Git: Ensure repo cloned at central location
Git->>FS: Check if repo exists
alt Repo missing
Git->>Git: Clone repository
else Repo exists
Git->>Git: Fetch updates
end
Git-->>CLI: Repo ready
CLI->>Git: Verify no uncommitted changes
Git-->>CLI: Status check passed
CLI->>Git: Checkout specified ref
Git-->>CLI: Ref checked out
CLI->>FS: Ensure IDE-specific directories exist
FS->>FS: Create directories with permissions
FS-->>CLI: Directories ready
CLI->>Symlink: Create IDE-specific symlinks
Symlink->>FS: Check existing symlinks/files
alt Force update or missing
Symlink->>FS: Remove old target
Symlink->>FS: Create new symlink
end
Symlink-->>CLI: Symlinks configured
CLI-->>CLI: Exit with status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In @.codex/superpowers-codex:
- Around line 246-252: The unconditional process.exit(0) after dispatching
runInstall/runUpgrade/runDoctor overrides any nonzero exit status set by
runDoctor; change the shutdown logic so it preserves runDoctor's outcome — e.g.,
call process.exit(process.exitCode || 0) or only exit with 0 when
process.exitCode is undefined/zero, and ensure runDoctor (and the dispatch path)
is awaited or returns before exiting; update the block around
runInstall/runUpgrade/runDoctor and the process.exit call accordingly so nonzero
exit codes from runDoctor are honored.
In `@lib/installer-core.js`:
- Around line 164-171: The parser currently assigns the next argv token to any
long flag (k) if it doesn't start with '--', causing boolean flags like --force
to grab positional args; modify the parsing in the loop that handles long
options (where k = a.slice(2), next = argv[i+1], args) to consult a small
set/array of known boolean flags (e.g., BOOLEAN_FLAGS containing "force", etc.)
and, if k is in that set, always set args[k] = true and do not consume the next
token (do not increment i); otherwise keep the existing behavior of assigning
next when it’s a value and incrementing i. Ensure the boolean flag set is
defined near the parser so maintainers can update it.
- Around line 128-133: gitCheckoutRef can fail when the target branch exists
only on the remote because runInstallOrUpgrade calls it before any fetch; update
gitCheckoutRef (and/or runInstallOrUpgrade) to ensure the branch is available
before checkout: perform a git fetch (e.g., run('git', ['fetch', 'origin'],
{cwd: repoDir})) prior to attempting git checkout, and if checkout still fails
try checking out the remote branch directly (git checkout -b <ref> origin/<ref>
or git checkout origin/<ref> and then create a local branch). Reference
gitCheckoutRef and runInstallOrUpgrade when making the change so the
fetch-before-checkout or fallback-to-origin/<ref> logic is applied where
appropriate.
🧹 Nitpick comments (2)
.opencode/superpowers-opencode (1)
43-43: Uncaught exceptions from command runners will crash with stack trace.If
runner(parsedArgs)throws (e.g., git not installed, network failure), the error will propagate as an uncaught exception with a full stack trace, which isn't user-friendly.Consider wrapping in try-catch to print a cleaner error message:
Proposed fix
-runner(parsedArgs); +try { + runner(parsedArgs); +} catch (err) { + console.error(`Error: ${err.message}`); + process.exit(1); +}.codex/superpowers-codex (1)
248-250: Missing error handling for install/upgrade command failures.Similar to the opencode CLI, if
runInstallorrunUpgradethrows (e.g., git error, permission denied), the exception will propagate with a raw stack trace.Proposed fix
if (command === 'install' || command === 'upgrade' || command === 'doctor') { const parsedArgs = parseArgs(process.argv.slice(3)); - if (command === 'install') runInstall('codex', parsedArgs); - if (command === 'upgrade') runUpgrade('codex', parsedArgs); - if (command === 'doctor') runDoctor('codex', parsedArgs); - process.exit(0); + try { + if (command === 'install') runInstall('codex', parsedArgs); + if (command === 'upgrade') runUpgrade('codex', parsedArgs); + if (command === 'doctor') runDoctor('codex', parsedArgs); + process.exit(process.exitCode || 0); + } catch (err) { + console.error(`Error: ${err.message}`); + process.exit(1); + } }
| if (command === 'install' || command === 'upgrade' || command === 'doctor') { | ||
| const parsedArgs = parseArgs(process.argv.slice(3)); | ||
| if (command === 'install') runInstall('codex', parsedArgs); | ||
| if (command === 'upgrade') runUpgrade('codex', parsedArgs); | ||
| if (command === 'doctor') runDoctor('codex', parsedArgs); | ||
| process.exit(0); | ||
| } |
There was a problem hiding this comment.
process.exit(0) overrides exitCode set by runDoctor on failure.
When runDoctor detects issues, it sets process.exitCode = 1 (line 310 in installer-core.js). However, line 251 unconditionally calls process.exit(0), which overrides this and reports success even when the doctor check fails.
Proposed fix
if (command === 'install' || command === 'upgrade' || command === 'doctor') {
const parsedArgs = parseArgs(process.argv.slice(3));
if (command === 'install') runInstall('codex', parsedArgs);
if (command === 'upgrade') runUpgrade('codex', parsedArgs);
if (command === 'doctor') runDoctor('codex', parsedArgs);
- process.exit(0);
+ process.exit(process.exitCode || 0);
}🤖 Prompt for AI Agents
In @.codex/superpowers-codex around lines 246 - 252, The unconditional
process.exit(0) after dispatching runInstall/runUpgrade/runDoctor overrides any
nonzero exit status set by runDoctor; change the shutdown logic so it preserves
runDoctor's outcome — e.g., call process.exit(process.exitCode || 0) or only
exit with 0 when process.exitCode is undefined/zero, and ensure runDoctor (and
the dispatch path) is awaited or returns before exiting; update the block around
runInstall/runUpgrade/runDoctor and the process.exit call accordingly so nonzero
exit codes from runDoctor are honored.
| function gitCheckoutRef({ repoDir, ref }) { | ||
| const r = run('git', ['checkout', ref], { cwd: repoDir }); | ||
| if (r.status !== 0) { | ||
| throw new Error(r.stderr.trim() || `git checkout ${ref} failed`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Checkout of remote-only branches may fail before fetch.
gitCheckoutRef is called before gitFastForwardUpdate (which does the fetch) in runInstallOrUpgrade. If the user specifies a branch that exists only on the remote and hasn't been fetched yet, git checkout will fail.
Consider fetching before checkout, or using git fetch + git checkout origin/<ref> pattern.
Possible fix in runInstallOrUpgrade
gitEnsureCloned({ repoUrl: config.repoUrl, repoDir: config.dir });
const dirty = gitStatusPorcelain(config.dir);
if (dirty && !config.force) {
throw new Error(`Central repo has uncommitted changes. Re-run with --force to continue.\n${dirty}`);
}
- gitCheckoutRef({ repoDir: config.dir, ref: config.ref });
- if (config.update) gitFastForwardUpdate({ repoDir: config.dir });
+ if (config.update) {
+ // Fetch first so remote branches are available for checkout
+ run('git', ['fetch', '--all', '--prune'], { cwd: config.dir });
+ }
+ gitCheckoutRef({ repoDir: config.dir, ref: config.ref });
+ if (config.update) {
+ run('git', ['pull', '--ff-only'], { cwd: config.dir });
+ }🤖 Prompt for AI Agents
In `@lib/installer-core.js` around lines 128 - 133, gitCheckoutRef can fail when
the target branch exists only on the remote because runInstallOrUpgrade calls it
before any fetch; update gitCheckoutRef (and/or runInstallOrUpgrade) to ensure
the branch is available before checkout: perform a git fetch (e.g., run('git',
['fetch', 'origin'], {cwd: repoDir})) prior to attempting git checkout, and if
checkout still fails try checking out the remote branch directly (git checkout
-b <ref> origin/<ref> or git checkout origin/<ref> and then create a local
branch). Reference gitCheckoutRef and runInstallOrUpgrade when making the change
so the fetch-before-checkout or fallback-to-origin/<ref> logic is applied where
appropriate.
| const k = a.slice(2); | ||
| const next = argv[i + 1]; | ||
| if (next && !next.startsWith('--')) { | ||
| args[k] = next; | ||
| i += 1; | ||
| } else { | ||
| args[k] = true; | ||
| } |
There was a problem hiding this comment.
Boolean flags followed by positional args may be misinterpreted.
If a user runs superpowers-opencode install --force somepath, the parser sets force='somepath' instead of force=true. The current help text suggests flags come after the command, which could lead to this scenario.
Since --force is expected to be boolean, consider checking against a known list of boolean flags, or documenting that flags must come after all positional arguments.
🤖 Prompt for AI Agents
In `@lib/installer-core.js` around lines 164 - 171, The parser currently assigns
the next argv token to any long flag (k) if it doesn't start with '--', causing
boolean flags like --force to grab positional args; modify the parsing in the
loop that handles long options (where k = a.slice(2), next = argv[i+1], args) to
consult a small set/array of known boolean flags (e.g., BOOLEAN_FLAGS containing
"force", etc.) and, if k is in that set, always set args[k] = true and do not
consume the next token (do not increment i); otherwise keep the existing
behavior of assigning next when it’s a value and incrementing i. Ensure the
boolean flag set is defined near the parser so maintainers can update it.
Adds a lightweight Node-based installer core and integrates it with the Codex CLI (and an OpenCode CLI entrypoint) to enable an idempotent central-clone and linking workflow.
Changes
lib/installer-core.jswithinstall,upgrade, anddoctorcommands.obra/superpowers@main~/.superpowers.opencode/superpowers-opencodefor OpenCode linking..codex/superpowers-codexwithinstall,upgrade, anddoctorsupport.Notes
INSTALL.mdis intentionally unchanged and will be updated separately for clearer review.upstream/mainyet.Summary by CodeRabbit