Skip to content

Fix cmux omo bootstrap when user pins yanked plugin deps - #2280

Merged
austinywang merged 1 commit into
mainfrom
issue-2278-omo-yanked-dep
Mar 28, 2026
Merged

austinywang merged 1 commit into
mainfrom
issue-2278-omo-yanked-dep

Conversation

@austinywang

@austinywang austinywang commented Mar 28, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2278

Summary

  • stop symlinking the user's opencode package metadata into ~/.cmuxterm/omo-config and write an isolated shadow package.json instead
  • retry bun add oh-my-opencode once after clearing the shadow bun.lock and node_modules
  • keep cmux omo from getting stuck behind a stale opencode port by preferring 4096 only when it is free and otherwise falling back to an available loopback port while keeping OPENCODE_PORT aligned
  • auto-skip the Ghostty CLI helper zig build in reload.sh on the current macOS 26.3 + zig 0.15.2 host so tagged dev rebuilds complete locally

Verification

  • ./scripts/reload.sh --tag omo-yanked-dep --launch
  • ./scripts/reload.sh --tag omo-yanked-dep
  • local tests not run (per repo policy)

Summary by cubic

Fixes #2278 by isolating the oh-my-opencode install from user-pinned/yanked deps and choosing a free API port instead of always forcing 4096. Also auto-skips the Ghostty CLI helper zig build on macOS 26 + zig 0.15.2 to keep dev reloads working.

  • Bug Fixes
    • Use a shadow package.json in ~/.cmuxterm/omo-config (no more symlinking user package.json/bun.lock) so yanked/stale pins can’t break oh-my-opencode install.
    • Make install more robust: run bun add oh-my-opencode, then retry once after clearing the shadow bun.lock and node_modules; fallback to npm install if bun isn’t available.
    • Smarter port handling: honor --port or OPENCODE_PORT when valid, otherwise prefer 4096 if free, else pick any free loopback port; set OPENCODE_PORT and inject --port to match so subagent attach works reliably.
    • Dev script: auto-enable CMUX_SKIP_ZIG_BUILD=1 and skip the Ghostty CLI helper zig build on macOS 26.x with zig 0.15.2 (still respects a manual override).

Written for commit b55bd15. Summary will update on new commits.

Summary by CodeRabbit

  • Improvements
    • Enhanced OpenCode port resolution with support for explicit configuration, environment variables, and automatic fallback detection.
    • Improved package installation reliability with automatic recovery mechanisms.
    • Optimized build process for specific development environments.

@vercel

vercel Bot commented Mar 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 28, 2026 7:40am

@coderabbitai

coderabbitai Bot commented Mar 28, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes refactor the OpenCode (omo) setup in CMUXCLI by replacing symlinked user package.json with a dedicated shadow manifest, managing node_modules symlinks separately, implementing install retry logic, and adding port resolution logic. Additionally, shell scripts add conditional zig build skipping based on macOS and zig version detection.

Changes

Cohort / File(s) Summary
OpenCode Plugin Setup Refactoring
CLI/cmux.swift
Extracted helper functions for file type inspection, shadow manifest management (no longer symlinks user's package.json), node_modules symlink handling, process-based package installation with exit status capture and retry on failure, and OpenCode port resolution from args/env/bindable port. Updated omoEnsurePlugin() and configureOMOEnvironment() to support these changes.
Build Script Zig Skip Logic
scripts/reload.sh
Added conditional zig build skipping based on macOS version (≥26) and zig version (0.15.2) detection via new should_skip_ghostty_cli_helper_zig_build() helper; auto-exports CMUX_SKIP_ZIG_BUILD=1 when conditions match.

Sequence Diagram

sequenceDiagram
    actor User
    participant CMUXCLI
    participant ShadowDir as Shadow Dir<br/>(filesystem)
    participant PackageManager as Package Manager<br/>(bun)
    participant OpenCode
    
    User->>CMUXCLI: cmux omo [args]
    
    CMUXCLI->>ShadowDir: Inspect file types & symlinks
    CMUXCLI->>ShadowDir: Write fresh package.json manifest<br/>(oh-my-opencode@latest only)
    CMUXCLI->>ShadowDir: Remove stale bun.lock symlink
    CMUXCLI->>ShadowDir: Ensure node_modules symlink<br/>→ user's node_modules
    
    CMUXCLI->>PackageManager: Run: bun install in shadow dir
    
    alt Install succeeds
        PackageManager-->>CMUXCLI: Exit 0
    else Install fails
        PackageManager-->>CMUXCLI: Exit non-zero
        CMUXCLI->>ShadowDir: Clear bun.lock
        CMUXCLI->>ShadowDir: Recreate node_modules symlink
        CMUXCLI->>PackageManager: Retry: bun install (once)
        PackageManager-->>CMUXCLI: Exit status
    end
    
    CMUXCLI->>CMUXCLI: Resolve OpenCode port<br/>(--port arg / env / bindable)
    CMUXCLI->>CMUXCLI: Configure environment<br/>with OPENCODE_PORT
    CMUXCLI->>OpenCode: Launch with --port & args
    OpenCode-->>User: Running
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A shadow manifest blooms so bright,
No symlinks tangled, clean and tight,
When installs stumble, retry with grace,
Fresh node_modules find their place,
Port resolving through the night! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main issue being fixed: handling yanked plugin dependencies in cmux omo bootstrap.
Description check ✅ Passed The PR description is complete with summary of changes, verification steps, and includes additional context from cubic's summary; follows template structure.
Linked Issues check ✅ Passed The PR directly addresses issue #2278 by implementing all key objectives: shadow package.json instead of symlinking, retry logic for failed installs, and smart port handling.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #2278 or supporting infrastructure; the zig build auto-skip is contextual to local development on the specific host.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2278-omo-yanked-dep

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 2 files

@greptile-apps

greptile-apps Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes cmux omo bootstrap failures when the user's opencode package.json has yanked plugin dependencies pinned. The fix has three parts: (1) the shadow config directory now owns an isolated package.json (always set to "latest") instead of symlinking the user's — preventing stale/yanked pins from poisoning bun installation; (2) a retry path clears the shadow bun.lock and node_modules on first-attempt failure before re-running bun add; (3) port selection now probes whether port 4096 (and the inherited OPENCODE_PORT) is actually bindable before committing to it, falling back to a random loopback port when stale. The reload.sh change auto-skips the Ghostty CLI helper zig build on macOS 26+ with zig 0.15.2.

  • Shadow package.json isolation (omoEnsureShadowPackageManifest): correctly replaces any pre-existing symlink and writes a cmux-owned manifest; the inline comment claiming "bun will update this manifest" is misleading — the file is intentionally reset to \"latest\" on every run.
  • Retry cleanup edge case: try? fm.removeItem(at: shadowNodeModules) silently ignores errors; if bun partially created shadowNodeModules as a real directory and removal fails, omoEnsureShadowNodeModulesSymlink returns early without re-creating the symlink and the retry runs against the same partial state.
  • Port fallback TOCTOU: the ephemeral port returned by omoBindableLoopbackPort(0) is released immediately, leaving a small window before opencode binds it.
  • reload.sh zig skip is exact-matched to \"0.15.2\" — the auto-skip will stop applying on the next zig patch without any visible warning.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/edge-case suggestions with no impact on the primary fix path.

The core logic is sound: shadow package.json isolation correctly prevents yanked-pin poisoning, the retry on bun failure is well-structured, and the port-probing fallback is a clear improvement over hardcoding 4096. All review findings are P2 — a misleading comment, a silent-failure edge case in the retry cleanup that requires an unlikely combination of events, a TOCTOU window inherent to any probe-then-use port strategy, and a narrowly pinned zig version check in a dev script. None of these block the intended fix.

No files require special attention; the retry-cleanup silent-failure path in CLI/cmux.swift around line 9922 is the most subtle edge case but is very unlikely in practice.

Important Files Changed

Filename Overview
CLI/cmux.swift Refactors omo bootstrap: shadow package.json is now isolated and owned by cmux (not symlinked from user), bun add retries after clearing bun.lock/node_modules, and port selection prefers 4096 only when free with a free-port fallback. All P2: retry cleanup has a silent-failure edge case, ephemeral port has a TOCTOU window, and an inline comment misrepresents the "latest" reset behavior.
scripts/reload.sh Adds should_skip_ghostty_cli_helper_zig_build to auto-skip the zig CLI-helper build on macOS 26+ with zig 0.15.2; exact-version match will silently stop applying on the next zig patch release.

Sequence Diagram

sequenceDiagram
    participant U as cmux omo (user)
    participant S as omoSetupShadowConfig
    participant PM as omoEnsureShadowPackageManifest
    participant NM as omoEnsureShadowNodeModulesSymlink
    participant PR as omoResolvedPort
    participant BUN as bun add

    U->>S: run omo bootstrap
    S->>PM: write shadow package.json ("latest")
    PM-->>S: symlink removed / file written
    S->>NM: ensure node_modules symlink → userNodeModules
    NM-->>S: symlink created or verified
    S->>PR: resolve port (args → OPENCODE_PORT → 4096 → ephemeral)
    PR-->>S: openCodePort string
    S->>S: launcherEnvironment["OPENCODE_PORT"] = openCodePort

    alt plugin NOT in node_modules
        S->>BUN: bun add oh-my-opencode (attempt 1)
        BUN-->>S: firstAttemptStatus

        alt firstAttemptStatus != 0
            S->>S: remove shadow bun.lock + node_modules (try?)
            S->>NM: re-create node_modules symlink
            S->>BUN: bun add oh-my-opencode (retry)
            BUN-->>S: retryStatus
            alt retryStatus != 0
                S-->>U: throw CLIError
            end
        end
    end

    S->>U: execve opencode --port openCodePort
Loading

Reviews (1): Last reviewed commit: "Fix cmux omo bootstrap with yanked deps" | Re-trigger Greptile

Comment thread CLI/cmux.swift
Comment on lines +9919 to +9932
if firstAttemptStatus != 0 {
FileHandle.standardError.write("Retrying oh-my-opencode install with a clean shadow package state...\n".data(using: .utf8)!)
try? fm.removeItem(at: shadowBunLockURL)
try? fm.removeItem(at: shadowNodeModules)
try omoEnsureShadowNodeModulesSymlink(shadowNodeModules: shadowNodeModules, userNodeModules: userNodeModules)
let retryStatus = try omoRunPackageInstall(
executablePath: bunPath,
arguments: installArguments,
currentDirectoryURL: installDir
)
if retryStatus != 0 {
throw CLIError(message: "Failed to install oh-my-opencode. Try manually: npm install -g oh-my-opencode")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Retry may not clear partial node_modules if removal silently fails

On the first failed bun add, bun may have partially created shadowNodeModules as a real directory. If try? fm.removeItem(at: shadowNodeModules) then silently fails (e.g. a locked sub-file), omoEnsureShadowNodeModulesSymlink detects a non-symlink type and returns early:

} else {
    return  // real directory — leaves it alone
}

The retry then runs against the same incomplete node_modules, likely producing the same failure. The try? on removeItem is appropriate for the common case, but if removal fails for a reason other than "file doesn't exist," the retry has no additional benefit over the first attempt.

A diagnostic log line on removal failure would at least make this visible to users who hit it:

if fm.fileExists(atPath: shadowNodeModules.path) {
    do {
        try fm.removeItem(at: shadowNodeModules)
    } catch {
        FileHandle.standardError.write(
            "Warning: could not remove shadow node_modules (\(error)); retry may not help\n"
                .data(using: .utf8)!)
    }
}

Comment thread CLI/cmux.swift
Comment on lines +9838 to +9845
return String(preferredPort)
}

if let fallbackPort = omoBindableLoopbackPort(0) {
return String(fallbackPort)
}

return "4096"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Ephemeral port TOCTOU window

omoBindableLoopbackPort(0) binds to port 0, reads back the OS-assigned port via getsockname, then immediately closes the socket via defer { close(socketDescriptor) }. The port number is returned and used as openCodePort, but it is no longer held — another process (or the OS ephemeral-port recycler) can reassign it before opencode starts and calls bind().

This is a standard limitation of any "probe then use" port-selection strategy and is unlikely to matter in practice, but worth documenting in a comment so the next reader understands why a startup bind failure is possible even after this function returns a value:

// Note: the port is released immediately after discovery; there is a
// small TOCTOU window before opencode claims it.
if let fallbackPort = omoBindableLoopbackPort(0) {
    return String(fallbackPort)
}

Comment thread CLI/cmux.swift
Comment on lines +9702 to +9704
// Keep the shadow package isolated from stale/yanked pins in the user's
// opencode package.json. bun will update this manifest with the resolved
// oh-my-opencode version when installation succeeds.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Misleading inline comment

The comment says "bun will update this manifest with the resolved oh-my-opencode version when installation succeeds," implying the pinned version persists. But omoEnsureShadowPackageManifest is called on every cmux omo invocation, and the existing != output check will detect that bun's pin (e.g. "oh-my-opencode": "1.2.3") differs from the template "latest" — so the manifest is reset back to "latest" on every run.

The actual design intent is the opposite: the shadow package.json deliberately stays pinned to "latest" so that if node_modules is ever absent, a fresh install always pulls the current published version rather than a potentially yanked pin. The comment should reflect this:

// Keep the shadow package.json fixed at "latest" so that if node_modules
// is absent and bun re-runs, it always installs the current published version
// rather than a potentially yanked pin from a stale lockfile.

Comment thread scripts/reload.sh
zig_version="$(zig version 2>/dev/null || true)"
major_version="${product_version%%.*}"

if [[ "$zig_version" == "0.15.2" ]] && [[ "$major_version" =~ ^[0-9]+$ ]] && (( major_version >= 26 )); then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Exact-version match will silently stop applying on next zig patch

The auto-skip is gated on "$zig_version" == "0.15.2" (exact string match). If the host upgrades to 0.15.3 or any subsequent patch while the Ghostty CLI helper zig build is still broken on macOS 26, the guard won't fire and developers will see a build failure without an obvious reason.

Consider a prefix/range check, or at least document why the exact version is required:

# Skip if zig is 0.15.x on macOS >= 26 (cli-helper build broken for this zig minor)
if [[ "$zig_version" == 0.15.* ]] && [[ "$major_version" =~ ^[0-9]+$ ]] && (( major_version >= 26 )); then

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
scripts/reload.sh (1)

458-462: Consider warning if no prior ghostty binary exists when skipping the build.

When skipping the build, if GHOSTTY_HELPER_SRC doesn't exist from a prior build, the app will silently lack the ghostty helper. For dev builds this is an acceptable tradeoff, but a warning could help developers understand why ghostty features might not work.

💡 Optional: Add a warning when skipping with no prior binary
   if [[ "${CMUX_SKIP_ZIG_BUILD:-}" == "1" ]]; then
     echo "Skipping direct ghostty CLI helper zig build (CMUX_SKIP_ZIG_BUILD=1)"
+    if [[ ! -x "$GHOSTTY_HELPER_SRC" ]]; then
+      echo "  Warning: No prior ghostty binary at $GHOSTTY_HELPER_SRC; ghostty features may be unavailable"
+    fi
   else
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/reload.sh` around lines 458 - 462, When CMUX_SKIP_ZIG_BUILD is set
and the script skips the zig build (the if branch checking CMUX_SKIP_ZIG_BUILD),
add a check for the ghostty helper artifact referenced by GHOSTTY_HELPER_SRC (or
the expected binary path used later) and emit a clear warning if that file does
not exist; update the conditional block where CMUX_SKIP_ZIG_BUILD is evaluated
so that before echoing the skip message you test [ -f "$GHOSTTY_HELPER_SRC" ]
(or equivalent) and log a warning that the ghostty helper is missing and ghostty
features may not work when the file is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 9760-9776: The parser currently returns nil for both "no --port
flag" and "flag present but malformed", so change omoRequestedPort(from:) to
distinguish these cases: if it sees "--port" with no next token or the next
token is empty or starts with "-" (another flag), or sees "--port=" with an
empty value, return an explicit sentinel (e.g. an empty String) instead of nil;
keep returning nil only when the flag is absent. Then update runOMO() to treat
that sentinel (empty String) as a CLI error and surface a helpful message rather
than appending a default "--port" again. Reference: omoRequestedPort(from:) and
runOMO().
- Around line 9723-9741: The current omoEnsureShadowNodeModulesSymlink leaves a
dangling or inappropriate shadow symlink when shadowNodeModules points at
userNodeModules that has since disappeared, allowing retries to reuse the user's
partial node_modules; change omoEnsureShadowNodeModulesSymlink so that if
shadowNodeModules is a symlink targeting userNodeModules but userNodeModules
does not exist (or is incomplete), remove the symlink and create a clean local
shadow node_modules (or recreate the correct symlink) before returning; apply
the same logic to the analogous npm retry path referenced around the other block
(lines handling shadowNodeModules / userNodeModules at 9919-9923) so retries
always operate on a freshly cleaned local shadow directory rather than a stale/
dangling symlink target.
- Around line 9929-9942: The recovery hint is misleading because the code
installs the plugin into the shadow config directory (pluginPackageDir /
installDir) but the CLIError messages suggest a global install; update the error
text in both places where we throw CLIError after omoRunPackageInstall (and any
other throw referencing npm install -g) to point users to the local shadow
install location (reference pluginPackageDir and installDir) and offer either a
local npm install command using those paths (or instructions to delete/recreate
the shadow dir and re-run bootstrap) so the hint matches how
omoRunPackageInstall and Self.omoPluginName actually install the package.

---

Nitpick comments:
In `@scripts/reload.sh`:
- Around line 458-462: When CMUX_SKIP_ZIG_BUILD is set and the script skips the
zig build (the if branch checking CMUX_SKIP_ZIG_BUILD), add a check for the
ghostty helper artifact referenced by GHOSTTY_HELPER_SRC (or the expected binary
path used later) and emit a clear warning if that file does not exist; update
the conditional block where CMUX_SKIP_ZIG_BUILD is evaluated so that before
echoing the skip message you test [ -f "$GHOSTTY_HELPER_SRC" ] (or equivalent)
and log a warning that the ghostty helper is missing and ghostty features may
not work when the file is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19e0a7fd-54cd-451d-91ef-aa11a1ad5088

📥 Commits

Reviewing files that changed from the base of the PR and between 97fee25 and b55bd15.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • scripts/reload.sh

Comment thread CLI/cmux.swift
Comment on lines +9723 to +9741
let fm = FileManager.default
guard fm.fileExists(atPath: userNodeModules.path) else { return }

if let type = omoFileType(at: shadowNodeModules) {
if type == .typeSymbolicLink {
let target = try? fm.destinationOfSymbolicLink(atPath: shadowNodeModules.path)
if target != userNodeModules.path {
try? fm.removeItem(at: shadowNodeModules)
} else {
return
}
} else {
return
}
}

if !fm.fileExists(atPath: shadowNodeModules.path) {
try fm.createSymbolicLink(at: shadowNodeModules, withDestinationURL: userNodeModules)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The retry is not isolated while shadow/node_modules points at the user's tree.

omoEnsureShadowNodeModulesSymlink() leaves a dangling shadow symlink untouched when ~/.config/opencode/node_modules disappears, and the bun retry later only removes the shadow link itself. So a failed first install can still reuse partial state from the user's node_modules, and the npm path has no retry at all. Bootstrap needs a real clean local shadow/node_modules for the retry, or it should explicitly clean the symlink target before rerunning.

Also applies to: 9919-9923

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 9723 - 9741, The current
omoEnsureShadowNodeModulesSymlink leaves a dangling or inappropriate shadow
symlink when shadowNodeModules points at userNodeModules that has since
disappeared, allowing retries to reuse the user's partial node_modules; change
omoEnsureShadowNodeModulesSymlink so that if shadowNodeModules is a symlink
targeting userNodeModules but userNodeModules does not exist (or is incomplete),
remove the symlink and create a clean local shadow node_modules (or recreate the
correct symlink) before returning; apply the same logic to the analogous npm
retry path referenced around the other block (lines handling shadowNodeModules /
userNodeModules at 9919-9923) so retries always operate on a freshly cleaned
local shadow directory rather than a stale/ dangling symlink target.

Comment thread CLI/cmux.swift
Comment on lines +9760 to +9776
private func omoRequestedPort(from commandArgs: [String]) -> String? {
for (index, arg) in commandArgs.enumerated() {
if arg == "--port" {
let nextIndex = commandArgs.index(after: index)
guard nextIndex < commandArgs.endIndex else { return nil }
let value = commandArgs[nextIndex].trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}

if arg.hasPrefix("--port=") {
let value = String(arg.dropFirst("--port=".count)).trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}

return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Malformed --port input turns into duplicated flags.

When the argv contains --port or --port= without a usable value, omoRequestedPort() returns nil, so runOMO() appends a second --port <default> instead of surfacing a CLI error. That makes cmux omo --port fail in a much more confusing way downstream.

Also applies to: 10085-10089

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 9760 - 9776, The parser currently returns nil
for both "no --port flag" and "flag present but malformed", so change
omoRequestedPort(from:) to distinguish these cases: if it sees "--port" with no
next token or the next token is empty or starts with "-" (another flag), or sees
"--port=" with an empty value, return an explicit sentinel (e.g. an empty
String) instead of nil; keep returning nil only when the flag is absent. Then
update runOMO() to treat that sentinel (empty String) as a CLI error and surface
a helpful message rather than appending a default "--port" again. Reference:
omoRequestedPort(from:) and runOMO().

Comment thread CLI/cmux.swift
Comment on lines +9929 to +9942
if retryStatus != 0 {
throw CLIError(message: "Failed to install oh-my-opencode. Try manually: npm install -g oh-my-opencode")
}
}
} else if let npmPath = resolveExecutableInPath("npm") {
process.executableURL = URL(fileURLWithPath: npmPath)
process.arguments = ["install", Self.omoPluginName]
FileHandle.standardError.write("Installing oh-my-opencode plugin (this may take a minute on first run)...\n".data(using: .utf8)!)
let status = try omoRunPackageInstall(
executablePath: npmPath,
arguments: ["install", Self.omoPluginName],
currentDirectoryURL: installDir
)
if status != 0 {
throw CLIError(message: "Failed to install oh-my-opencode. Try manually: npm install -g oh-my-opencode")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The recovery hint points users at an install location this code never reads.

pluginPackageDir is always resolved under the shadow config directory, so npm install -g oh-my-opencode will not satisfy the next existence check. This should point users at a local install in the shadow dir, or at deleting/recreating that dir and rerunning bootstrap.

Suggested fix
-                        throw CLIError(message: "Failed to install oh-my-opencode. Try manually: npm install -g oh-my-opencode")
+                        throw CLIError(
+                            message: "Failed to install oh-my-opencode in \(shadowDir.path). Remove that directory and rerun, or install it locally there with `bun add \(Self.omoPluginName)`."
+                        )
@@
-                    throw CLIError(message: "Failed to install oh-my-opencode. Try manually: npm install -g oh-my-opencode")
+                    throw CLIError(
+                        message: "Failed to install oh-my-opencode in \(shadowDir.path). Remove that directory and rerun, or install it locally there with `npm install \(Self.omoPluginName)`."
+                    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 9929 - 9942, The recovery hint is misleading
because the code installs the plugin into the shadow config directory
(pluginPackageDir / installDir) but the CLIError messages suggest a global
install; update the error text in both places where we throw CLIError after
omoRunPackageInstall (and any other throw referencing npm install -g) to point
users to the local shadow install location (reference pluginPackageDir and
installDir) and offer either a local npm install command using those paths (or
instructions to delete/recreate the shadow dir and re-run bootstrap) so the hint
matches how omoRunPackageInstall and Self.omoPluginName actually install the
package.

This branch was successfully deployed

1 active deployment
Preview — b55bd155 Deployed Mar 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cmux omo: fails when user package.json pins yanked @opencode-ai/plugin version

1 participant