Skip to content

installer: add node-based install/upgrade/doctor for Codex/OpenCode#409

Open
kundeng wants to merge 1 commit intoobra:mainfrom
kundeng:pr/installer-core
Open

installer: add node-based install/upgrade/doctor for Codex/OpenCode#409
kundeng wants to merge 1 commit intoobra:mainfrom
kundeng:pr/installer-core

Conversation

@kundeng
Copy link

@kundeng kundeng commented Feb 4, 2026

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

  • Introduces lib/installer-core.js with install, upgrade, and doctor commands.
    • Defaults to obra/superpowers@main
    • Installs to ~/.superpowers
  • Adds .opencode/superpowers-opencode for OpenCode linking.
  • Extends .codex/superpowers-codex with install, upgrade, and doctor support.

Notes

  • Documentation in INSTALL.md is intentionally unchanged and will be updated separately for clearer review.
  • KiloCode files are excluded because they do not exist in upstream/main yet.

Summary by CodeRabbit

  • New Features
    • Added install, upgrade, and doctor CLI commands for managing Codex and OpenCode integrations.
    • New command-line interface with support for repository, reference, directory, and force configuration options.
    • Includes diagnostic tools to verify installation status and system health across IDE environments.

@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

These 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

Cohort / File(s) Summary
CLI Integration
.codex/superpowers-codex, .opencode/superpowers-opencode
Add early command-line parsing and delegation for install, upgrade, and doctor commands. Integrate parseArgs and installer functions from installer-core, with documentation in CLI help output.
Core Installer Module
lib/installer-core.js
Implement complete installer infrastructure: utility helpers (file/path operations), Git operations (clone, checkout, status checks), symlink management with force handling, argument parsing, configuration resolution, IDE-specific path mappings, doctor verification routine, and install/upgrade workflows with error handling.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hop, skip, and symlink we go,
Three IDEs now steal the show!
Installer-core's the magic spell,
Git and folders work so well,
Codex, OpenCode—a perfect dwell! 🔗

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding Node-based install/upgrade/doctor commands for Codex and OpenCode IDEs, matching the core additions in installer-core.js and CLI integrations.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 runInstall or runUpgrade throws (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);
+    }
 }

Comment on lines +246 to +252
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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +128 to +133
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`);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +164 to +171
const k = a.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith('--')) {
args[k] = next;
i += 1;
} else {
args[k] = true;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

@obra obra added codex Issues related to OpenAI Codex support opencode enhancement New feature or request labels Feb 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex Issues related to OpenAI Codex support enhancement New feature or request opencode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants