Skip to content

Add cmux config doctor CLI - #3454

Merged
lawrencecchen merged 8 commits into
mainfrom
task-omux-feature-parity
May 6, 2026
Merged

lawrencecchen merged 8 commits into
mainfrom
task-omux-feature-parity

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Compared OpenMUX's feature set and added the most portable missing piece: cmux config doctor.
  • Added cmux config path, cmux config docs, and cmux config reload aliases around existing settings and reload flows.
  • Reused cmux JSONC preprocessing in the bundled CLI and documented the no-socket CLI contract.

Testing

  • ./scripts/reload.sh --tag omuxcfg passed.
  • Direct tagged CLI checks: valid JSONC exits 0, invalid JSON exits 1 with JSON output, explicit missing --path exits 1.
  • cmux config --help and cmux config path passed against the tagged CLI.

Issues


Summary by cubic

Adds cmux config doctor to validate cmux.json (JSONC) without a socket, plus config path|paths, config docs, and config reload. Documents the JSON output contract and tightens trailing comma checks.

  • New Features

    • cmux config doctor (check, validate): validates JSONC for default targets (primary, project-level .cmux/cmux.json or cmux.json discovered up to HOME, legacy files when present) or explicit --path/--path=.... Supports --json, prints ok, error_count, findings (with label, display_path, path, status, ok, keys, optional message and bytes), plus reload_command, docs_url, and schema_url; exits non‑zero on errors.
    • Discovery and paths: expands ~ and relative paths; rejects unknown flags, bare positional paths, and directory paths with clear errors.
    • Routing/help/docs: config help|path|paths|docs|doctor run without a socket; config reload uses the socket flow. cmux --help and topic help include config. config docs mirrors docs settings. config path|paths print docs/schema URLs and the reload command.
  • Bug Fixes

    • JSONCParser: rejects invalid trailing comma sequences with a clear "invalid trailing comma" error; flags empty files and non‑object top‑level values as errors.

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

Summary by CodeRabbit

  • New Features

    • Added top-level config command with subcommands: doctor, check/validate, path/paths, docs/documentation, and reload. Commands can run without a socket where applicable.
    • Added settings/docs argument parsing to improve docs discovery.
  • Documentation

    • Updated CLI contract and help output to document the new config commands and usage.
  • Tests

    • Added end-to-end tests for config doctor covering discovery, validation, JSONC inputs, and error cases.
  • Improvements

    • Improved JSONC parsing with clearer invalid trailing-comma errors.

@vercel

vercel Bot commented May 4, 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 May 6, 2026 0:49am
cmux-staging Building Building Preview, Comment May 6, 2026 0:49am

@coderabbitai

coderabbitai Bot commented May 4, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8b5615be-8187-4af5-95d0-834f725ab954

📥 Commits

Reviewing files that changed from the base of the PR and between f990275 and bd00624.

📒 Files selected for processing (1)
  • docs/cli-contract.md

📝 Walkthrough

Walkthrough

Adds a top-level cmux config command with subcommands (doctor/check/validate, path/paths, docs/documentation, reload), implements discovery and JSONC validation with JSON or human output, wires CLI dispatch (no-socket and socket paths), tightens JSONC trailing-comma detection (now throws), exposes several settings/docs constants and helpers, removes an old settings path printer, adds end-to-end tests, and updates project build to include the new config source.

Changes

Config command, doctor workflow, JSONC and CLI wiring

Layer / File(s) Summary
Data Shape / Types
CLI/CMUXCLI+Config.swift
Adds ConfigDoctorOptions, ConfigDoctorTarget, ConfigDoctorFinding, ConfigDoctorReport.
Argument parsing / docs helpers
CLI/CMUXCLI+DocsSettings.swift
Exposes settingsDocsURL, settingsSchemaURL, primarySettingsDisplayPath, legacySettingsDisplayPath, fallbackSettingsDisplayPath; inserts cmux config doctor into docs references; adds docsSettingsArguments(_:); makes hasHelpRequest internal; removes private printSettingsPaths.
Core Implementation: discovery, validation, formatting
CLI/CMUXCLI+Config.swift
Implements runConfigCommand(...), option parsing, target discovery (primary/project/legacy), runConfigDoctor(...), configDoctorFinding(...), report payloads, JSON/human output formatting, configUsage, path helpers, and reload via socket.
JSONC parsing behavior
Sources/JSONCParser.swift
stripTrailingCommas now throws; preprocess propagates errors; added JSONCError.invalidTrailingComma and descriptive message; trailing-comma detection tightened.
CLI Dispatch / Socket wiring
CLI/cmux.swift
Routes top-level config to runConfigCommand(...) on both early no-socket path and normal resolved-socket path; adds config to usage/help output.
Tests / E2E
tests/test_cli_config_doctor.py
Adds end-to-end script to locate CLI binary, run cmux config doctor (with/without --path and --json), assert JSON payloads and error cases (invalid JSON, directory path, positional usage).
Docs / Contract
docs/cli-contract.md
Adds config top-level command, documents subcommands and no-socket help probes, and documents --json report shape.
Build/project wiring
GhosttyTabs.xcodeproj/project.pbxproj
Adds CMUXCLI+Config.swift to cmux-cli target and appends a new JSONCParser.swift build file entry.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User as "User (shell)"
  participant CLI as "cmux CLI"
  participant FS as "File system"
  participant JSONC as "JSONCParser"
  participant Socket as "App socket"

  rect rgba(200,200,255,0.5)
  User->>CLI: cmux config doctor [--path <file>] [--json]
  CLI->>FS: discover candidate config paths
  FS-->>CLI: candidate path list
  CLI->>JSONC: preprocess & parse target file
  JSONC-->>CLI: parsed payload or error
  CLI->>User: print JSON or human-readable report
  end

  rect rgba(200,255,200,0.5)
  User->>CLI: cmux config reload
  CLI->>Socket: connect (socketPath)
  CLI->>Socket: send "reload_config"
  Socket-->>CLI: ack / error
  CLI->>User: print reload result
  end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • manaflow-ai/cmux#3295: Overlapping edits to docs/settings CLI helpers and exposure of settings constants.
  • manaflow-ai/cmux#3409: Related changes touching JSONC parsing and CLI docs/settings internals.
  • manaflow-ai/cmux#3246: Prior addition/documentation of the config subcommands and no-socket behavior in CLI contract.

Poem

🐰 I hop through configs, sniff each line and comma,
I find the paths where quiet settings dream.
I doctor, report, and wake the socket’s hum—
A tidy JSONC, no trailing seam.
Hooray, the CLI sings; I twitch my whiskers in gleam!


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Blocking Runtime ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (11 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main feature added: cmux config doctor CLI. It is concise and clearly describes the primary change.
Description check ✅ Passed The description covers the summary and testing sections adequately but lacks explicit details on manual verification and missing the demo video and checklist completion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation issues. CLI code is synchronous with immutable static constants, value-type helper structs, and no shared mutable state or MainActor isolation problems.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. New code uses synchronous throws-based error handling throughout. No DispatchQueue, fire-and-forget Tasks, @escaping callbacks, or Combine patterns added.
Cmux Swift @Concurrent ✅ Passed No @concurrent annotation violations. All new functions are synchronous. File I/O and JSON parsing happen synchronously on CLI main thread—no async/await patterns requiring @concurrent annotation.
Cmux Swift File And Package Boundaries ✅ Passed New file has single coherent responsibility: config CLI command subsystem. No prohibited concern mixing. Follows existing CMUXCLI patterns.
Cmux Swift Logging ✅ Passed All print() calls in CLI code for user-facing output. No debugPrint/dump/NSLog in production, no ad hoc file logging, no MainActor Logger issues, no secrets.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI code. All modified files are CLI extensions, utilities, documentation, or Python tests with zero SwiftUI patterns, View definitions, or state management annotations.
Cmux Architecture Rethink ✅ Passed Dual dispatch paths converge via runConfigCommand(socketPath:) with clear gating. No mutable state, no timing patterns, no split ownership. Architecturally sound.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-omux-feature-parity

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.

@greptile-apps

greptile-apps Bot commented May 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds cmux config doctor — a no-socket JSONC validator — plus config path|paths, config docs, and config reload aliases that mirror the existing settings subcommand surface. The JSONC preprocessor is tightened to throw on invalid trailing-comma patterns (double commas, commas after {/[/:).

  • CLI/CMUXCLI+Config.swift (new, 464 lines): implements all config subcommands including file discovery (findProjectConfigPath walks up to HOME), per-target validation via JSONCParser, and structured JSON output with ok/error_count/findings.
  • Sources/JSONCParser.swift: stripTrailingCommas is now throwing; lastSignificantCharacter tracking detects ,,, {,}, [,], and {key:,} patterns and raises invalidTrailingComma.
  • Routing in cmux.swift: config subcommands without a socket take an early return before socket resolution; config reload falls through to the resolved-socket path.

Confidence Score: 5/5

Safe to merge — the new config command is cleanly isolated, the socket-routing logic is correct, and the JSONCParser tightening is well-scoped.

All changed paths behave correctly under their tested inputs. The two findings are minor inconsistencies (a missing byte count in one error branch, and an unguarded empty-string argument) that don't affect exit codes, discovery, or correctness of the validation result. No blocking concerns in routing, parsing, or file I/O.

No files require special attention.

Important Files Changed

Filename Overview
CLI/CMUXCLI+Config.swift New 464-line file implementing all config subcommands (doctor, path, docs, reload). Logic is sound and well-structured; two minor schema/validation gaps noted: byteCount is nil for parse errors despite the file size being in scope, and --path "" is accepted without validation in the space-separated form.
CLI/CMUXCLI+DocsSettings.swift Visibility of five constants and two helper functions widened from private to internal to support the new Config extension; printSettingsPaths moved to the Config file; cmux config doctor added to the settings docs reference. No logic changes.
Sources/JSONCParser.swift Adds invalidTrailingComma error thrown when a trailing comma follows nil, ',', '{', '[', or ':' as the last significant character — correctly catches double-commas and empty-array/object commas. The lastSignificantCharacter tracking is consistent across string boundaries and the stripping path.
CLI/cmux.swift Adds two dispatch blocks: an early no-socket path for config subcommands that don't need a socket, and a socket-backed path for config reload. Wiring correctly reuses configCommandDoesNotNeedSocket as a gate. configUsage() registered in the per-topic help switch.
tests/test_cli_config_doctor.py End-to-end tests cover explicit --path (valid JSONC, invalid JSON, directory path), the no-flag default discovery scan (primary config + home-level exclusion), and rejected positional paths. Covers the major functional branches.
docs/cli-contract.md Adds config command to the command table, full config subcommand contract table, --json output schema description, and three no-socket help probe assertions. Documentation matches the implementation.
GhosttyTabs.xcodeproj/project.pbxproj Registers CMUXCLI+Config.swift in the CLI target and adds JSONCParser.swift to the CLI build target so the doctor command can use the JSONC preprocessor. Mechanical change; UUIDs follow the existing pattern.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["cmux config <args>"] --> B{configCommandDoesNotNeedSocket?}
    B -- yes --> C[runConfigCommand\nsocketPath: nil]
    B -- no --> D[Resolve socket path]
    D --> E[runConfigCommand\nsocketPath: resolved]
    C --> F{subcommand}
    E --> F
    F -- "help / --help" --> G[print configUsage]
    F -- "path / paths" --> H[printSettingsPaths]
    F -- "docs / documentation" --> I[runDocsCommand settings]
    F -- "doctor / check / validate" --> J[runConfigDoctor]
    F -- reload --> K{socketPath present?}
    F -- unknown --> L[throw CLIError]
    K -- no --> M[throw: requires socket]
    K -- yes --> N[client.send reload_config]
    J --> O{--path given?}
    O -- yes --> P[explicit targets\nmissingIsError: true]
    O -- no --> Q[defaultConfigDoctorTargets\nprimary + project walk + legacy]
    P --> R[configDoctorFinding per target]
    Q --> R
    R --> S{errorCount > 0?}
    S -- yes --> T[print report\nthrow CLIError exit 1]
    S -- no --> U[print report\nexit 0]
Loading

Reviews (4): Last reviewed commit: "Document config doctor output contract" | Re-trigger Greptile

Comment thread CLI/CMUXCLI+DocsSettings.swift Outdated
Comment on lines +199 to +206
guard args.count == 1 else {
throw CLIError(message: "Usage: cmux config docs")
}
if wantsJSON, let reference = docsReference(for: "settings") {
print(jsonString(docsPayload(reference)))
} else if let reference = docsReference(for: "settings") {
printDocsReference(reference)
}

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 Silent no-output on docs subcommand when reference is absent

Both branches of the docs case are if wantsJSON, let reference = ... / else if let reference = ..., so if docsReference(for: "settings") ever returns nil the command exits 0 with no output and no error message. The runDocsCommand path correctly throws a CLIError when the reference is missing; the same guard-and-throw pattern should be used here for consistency and debuggability.

// suggested approach
guard let reference = docsReference(for: "settings") else {
    throw CLIError(message: "Settings docs reference not found.")
}
if wantsJSON {
    print(jsonString(docsPayload(reference)))
} else {
    printDocsReference(reference)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by routing config docs through runDocsCommand, which already guards the missing reference path and throws a CLIError.

— Claude Code

Comment thread CLI/CMUXCLI+DocsSettings.swift Outdated
Comment on lines +220 to +224
launchIfNeeded: false
)
defer { client.close() }
let response = try client.send(command: "reload_config")
if response.hasPrefix("ERROR:") {

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 Maintenance trap: configCommandDoesNotNeedSocket defaults to true for unrecognized subcommands

configCommandDoesNotNeedSocket returns true for every subcommand that is not literally "reload". If a future subcommand added to runConfigCommand needs a real socket and the developer forgets to update this function, it will silently receive CLISocketPathResolver.defaultSocketPath instead of the env-resolved path — wrong socket, no compile-time or runtime warning. Consider an allowlist of no-socket subcommands, or at minimum add a comment cross-referencing both functions so authors know they must be kept in sync.

Comment on lines +57 to +95
config_path = home / ".config" / "cmux" / "cmux.json"
config_path.parent.mkdir(parents=True)
config_path.write_text(
"""
{
// JSONC comments and trailing commas are valid in cmux.json.
"schemaVersion": 1,
"app": {
"appearance": "system",
},
}
""",
encoding="utf-8",
)

ok_result = run_cli(cli_path, ["--json", "config", "doctor", "--path", str(config_path)], home)
if ok_result.returncode != 0:
failures.append(f"valid JSONC returned {ok_result.returncode}: {ok_result.stderr}")
else:
payload = json.loads(ok_result.stdout)
finding = payload["findings"][0]
if payload["ok"] is not True or finding["status"] != "ok":
failures.append(f"valid JSONC was not ok: {ok_result.stdout}")
if "app" not in finding["keys"] or "schemaVersion" not in finding["keys"]:
failures.append(f"valid JSONC keys missing: {ok_result.stdout}")

config_path.write_text("{\n", encoding="utf-8")
bad_result = run_cli(cli_path, ["--json", "config", "doctor", "--path", str(config_path)], home)
if bad_result.returncode == 0:
failures.append("invalid JSON returned success")
else:
payload = json.loads(bad_result.stdout)
finding = payload["findings"][0]
if payload["ok"] is not False or finding["status"] != "error":
failures.append(f"invalid JSON did not report an error: {bad_result.stdout}")
if "cmux config doctor found 1 error(s)" not in bad_result.stderr:
failures.append(f"invalid JSON stderr was unexpected: {bad_result.stderr}")

if failures:

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 Default-scan path is entirely untested

The test only exercises the --path <explicit> code path. defaultConfigDoctorTargets, findProjectConfigPath, and the legacy-path deduplication logic (the most complex parts of the feature) have zero coverage. Running cmux config doctor with no flags — the most common invocation — is never called. At minimum, a third case invoking run_cli(cli_path, ["--json", "config", "doctor"], home) against the synthesised ~/.config/cmux/cmux.json would cover the primary-config scan path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by adding the no-flag config doctor path to tests/test_cli_config_doctor.py, including primary config discovery from a nested workspace.

— Claude Code

Comment thread CLI/CMUXCLI+DocsSettings.swift Outdated
Comment on lines +570 to +574
var paths: [String] = []
var index = 0
while index < arguments.count {
let argument = arguments[index]
if argument == "--path" {

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 Undocumented positional-path syntax accepted silently

parseConfigDoctorOptions appends any non-flag argument directly to paths, so cmux config doctor /path/to/file works as a positional shorthand for --path /path/to/file. Neither configUsage() nor docs/cli-contract.md documents this form. Either remove the positional fallback to keep the interface tight, or add it to the help string and CLI contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by rejecting positional config doctor paths and keeping --path as the only file argument form.

— Claude Code

@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 (2)
CLI/cmux.swift (1)

2066-2075: 💤 Low value

Consider adding a clarifying comment for the two-phase config dispatch.

The intent here is an optimization: skip socket resolution for subcommands that don't need it (doctor, path, docs, help). The same command == "config" check appears again after socket resolution (Line 2117) for the reload case. Without a comment, a reader may not immediately understand why there are two separate config blocks.

Also, CLISocketPathResolver.defaultSocketPath is passed as a placeholder since it won't be used by any branch reached via the configCommandDoesNotNeedSocket guard, but a comment would make that intention explicit and prevent a future maintainer from accidentally adding a socket-dependent subcommand to runConfigCommand without updating configCommandDoesNotNeedSocket.

✏️ Suggested clarification
+        // Early-return for config subcommands that don't need a live socket
+        // (doctor, path, docs, help, unknown). The socketPath arg is unused by
+        // these branches; "reload" falls through to the post-resolution block below.
         if command == "config",
            configCommandDoesNotNeedSocket(commandArgs) {
             try runConfigCommand(
                 commandArgs: commandArgs,
-                socketPath: CLISocketPathResolver.defaultSocketPath,
+                socketPath: CLISocketPathResolver.defaultSocketPath,  // unused for no-socket cmds
                 explicitPassword: socketPasswordArg,
                 jsonOutput: jsonOutput
             )
             return
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 2066 - 2075, Add a clarifying inline comment
above the first `if command == "config",
configCommandDoesNotNeedSocket(commandArgs) { ... }` explaining this is a
two-phase dispatch: we short-circuit and call `runConfigCommand` with a
placeholder `CLISocketPathResolver.defaultSocketPath` for subcommands that do
not need the socket (e.g., doctor/path/docs/help) to avoid premature socket
resolution, and note that `config` is checked again later after socket
resolution to handle socket-dependent subcommands like `reload`; also add a
reminder to update `configCommandDoesNotNeedSocket` if any new config subcommand
begins to require the socket so callers of `runConfigCommand` aren’t
inadvertently passed a placeholder path.
docs/cli-contract.md (1)

316-318: ⚡ Quick win

Consider adding a "Config subcommands" section to "Command Families".

Every other multi-subcommand family (Auth, VM, Themes, Browser, Hooks, Docs, Settings) has a dedicated table in the "Command Families" section documenting each subcommand's contract. config only appears in the help probe string, leaving doctor, path, docs, and reload contracts undocumented in a scannable form.

📄 Suggested addition (after the Settings subcommands table, ~line 292)
+Config subcommands:
+
+| Command | Contract |
+| --- | --- |
+| `config doctor [--path <file>]`, `config check`, `config validate` | Validate JSONC syntax of one or more config files. Uses default discovery when `--path` is absent. Exits 0 on success, 1 on any error. Supports `--json`. Works without a socket. |
+| `config path` | Print cmux.json paths, docs URL, schema URL, backup reminder, and reload command without a socket. |
+| `config docs` | Print the same output as `docs settings` without a socket. |
+| `config reload` | Ask cmux to reload configuration. Requires a socket. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/cli-contract.md` around lines 316 - 318, Add a new "Config subcommands"
section in docs/cli-contract.md (placed after the Settings subcommands table)
that documents each config subcommand in a scannable table format: list
`doctor`, `path`, `docs`, and `reload` as rows with their Usage, Description,
and Flags/Args columns; ensure the table mirrors the style and column names used
by other command-family tables (e.g., Auth/VM/Themes) so readers can quickly see
the contract for Config subcommands.
🤖 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/CMUXCLI`+DocsSettings.swift:
- Around line 784-790: In configDoctorErrorMessage(_ error: Error) prefer
extracting the parser-specific debug message from (error as
NSError).userInfo[NSDebugDescriptionErrorKey] first and return it if non-empty;
if that key is absent or empty, fall back to String(describing: error) and then
finally to nsError.localizedDescription, ensuring parser-specific JSON errors
are preserved for cmux config doctor.
- Around line 646-664: The search in findProjectConfigPath currently walks up to
root and can match directories; change it to stop before entering the user's
home directory (use FileManager.default.homeDirectoryForCurrentUser.path and
compare standardized paths and return nil when current == home) and when testing
each candidate, use fileExists(atPath:isDirectory:) to ensure the candidate
exists and is not a directory (isDirectory == false) before returning its
standardized path; update references in the function to use these checks for the
candidates array (the ".cmux/cmux.json" and "cmux.json" entries).

In `@tests/test_cli_config_doctor.py`:
- Around line 76-81: Wrap the unguarded JSON parsing and list access in
try/except blocks to catch json.JSONDecodeError and IndexError: around the
json.loads(ok_result.stdout) and the finding = payload["findings"][0] access,
catch these exceptions and append a helpful failure string to failures
(including ok_result.stdout and the exception message) instead of letting the
exception propagate; repeat the same pattern for the second block that parses
the other result (the second json.loads and findings[0]) so both locations use
guarded parsing and clearly report FAIL entries on parse/index errors.

---

Nitpick comments:
In `@CLI/cmux.swift`:
- Around line 2066-2075: Add a clarifying inline comment above the first `if
command == "config", configCommandDoesNotNeedSocket(commandArgs) { ... }`
explaining this is a two-phase dispatch: we short-circuit and call
`runConfigCommand` with a placeholder `CLISocketPathResolver.defaultSocketPath`
for subcommands that do not need the socket (e.g., doctor/path/docs/help) to
avoid premature socket resolution, and note that `config` is checked again later
after socket resolution to handle socket-dependent subcommands like `reload`;
also add a reminder to update `configCommandDoesNotNeedSocket` if any new config
subcommand begins to require the socket so callers of `runConfigCommand` aren’t
inadvertently passed a placeholder path.

In `@docs/cli-contract.md`:
- Around line 316-318: Add a new "Config subcommands" section in
docs/cli-contract.md (placed after the Settings subcommands table) that
documents each config subcommand in a scannable table format: list `doctor`,
`path`, `docs`, and `reload` as rows with their Usage, Description, and
Flags/Args columns; ensure the table mirrors the style and column names used by
other command-family tables (e.g., Auth/VM/Themes) so readers can quickly see
the contract for Config subcommands.
🪄 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: 1ad9b1f9-2539-4fa0-8f3e-438fc5f851f4

📥 Commits

Reviewing files that changed from the base of the PR and between 0c532c0 and 5091730.

📒 Files selected for processing (5)
  • CLI/CMUXCLI+DocsSettings.swift
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • docs/cli-contract.md
  • tests/test_cli_config_doctor.py

Comment thread CLI/CMUXCLI+DocsSettings.swift Outdated
Comment thread CLI/CMUXCLI+DocsSettings.swift Outdated
Comment thread tests/test_cli_config_doctor.py Outdated
Comment on lines +76 to +81
payload = json.loads(ok_result.stdout)
finding = payload["findings"][0]
if payload["ok"] is not True or finding["status"] != "ok":
failures.append(f"valid JSONC was not ok: {ok_result.stdout}")
if "app" not in finding["keys"] or "schemaVersion" not in finding["keys"]:
failures.append(f"valid JSONC keys missing: {ok_result.stdout}")

@coderabbitai coderabbitai Bot May 4, 2026 •

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 | ⚡ Quick win

Unhandled json.JSONDecodeError and IndexError produce opaque CI failures instead of clean FAIL: messages.

Both json.loads() calls (lines 76 and 88) and both findings[0] accesses (lines 77 and 89) are unguarded. If the CLI emits non-JSON output (e.g., an unexpected crash message, a migration banner, or empty stdout on error), the exception propagates out of main() and exits with a Python traceback rather than a diagnostic FAIL: entry — making it harder to distinguish a test-infrastructure problem from a feature regression.

🛡️ Proposed fix with guarded JSON parsing helper
+def _parse_json_output(raw: str, label: str, failures: list[str]) -> dict | None:
+    try:
+        return json.loads(raw)
+    except json.JSONDecodeError as exc:
+        failures.append(f"{label}: stdout is not valid JSON ({exc}): {raw!r}")
+        return None
+
+
 def main() -> int:
     ...
         ok_result = run_cli(cli_path, ["--json", "config", "doctor", "--path", str(config_path)], home)
         if ok_result.returncode != 0:
             failures.append(f"valid JSONC returned {ok_result.returncode}: {ok_result.stderr}")
         else:
-            payload = json.loads(ok_result.stdout)
-            finding = payload["findings"][0]
-            if payload["ok"] is not True or finding["status"] != "ok":
-                failures.append(f"valid JSONC was not ok: {ok_result.stdout}")
-            if "app" not in finding["keys"] or "schemaVersion" not in finding["keys"]:
-                failures.append(f"valid JSONC keys missing: {ok_result.stdout}")
+            payload = _parse_json_output(ok_result.stdout, "valid JSONC", failures)
+            if payload is not None:
+                findings = payload.get("findings", [])
+                if not findings:
+                    failures.append(f"valid JSONC: findings array is empty: {ok_result.stdout}")
+                else:
+                    finding = findings[0]
+                    if payload.get("ok") is not True or finding.get("status") != "ok":
+                        failures.append(f"valid JSONC was not ok: {ok_result.stdout}")
+                    if "app" not in finding.get("keys", []) or "schemaVersion" not in finding.get("keys", []):
+                        failures.append(f"valid JSONC keys missing: {ok_result.stdout}")

         config_path.write_text("{\n", encoding="utf-8")
         bad_result = run_cli(cli_path, ["--json", "config", "doctor", "--path", str(config_path)], home)
         if bad_result.returncode == 0:
             failures.append("invalid JSON returned success")
         else:
-            payload = json.loads(bad_result.stdout)
-            finding = payload["findings"][0]
-            if payload["ok"] is not False or finding["status"] != "error":
-                failures.append(f"invalid JSON did not report an error: {bad_result.stdout}")
-            if "cmux config doctor found 1 error(s)" not in bad_result.stderr:
-                failures.append(f"invalid JSON stderr was unexpected: {bad_result.stderr}")
+            payload = _parse_json_output(bad_result.stdout, "invalid JSON", failures)
+            if payload is not None:
+                findings = payload.get("findings", [])
+                if not findings:
+                    failures.append(f"invalid JSON: findings array is empty: {bad_result.stdout}")
+                else:
+                    finding = findings[0]
+                    if payload.get("ok") is not False or finding.get("status") != "error":
+                        failures.append(f"invalid JSON did not report an error: {bad_result.stdout}")
+            if "cmux config doctor found 1 error(s)" not in bad_result.stderr:
+                failures.append(f"invalid JSON stderr was unexpected: {bad_result.stderr}")

Also applies to: 88-93

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

In `@tests/test_cli_config_doctor.py` around lines 76 - 81, Wrap the unguarded
JSON parsing and list access in try/except blocks to catch json.JSONDecodeError
and IndexError: around the json.loads(ok_result.stdout) and the finding =
payload["findings"][0] access, catch these exceptions and append a helpful
failure string to failures (including ok_result.stdout and the exception
message) instead of letting the exception propagate; repeat the same pattern for
the second block that parses the other result (the second json.loads and
findings[0]) so both locations use guarded parsing and clearly report FAIL
entries on parse/index errors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by adding guarded JSON parsing and finding extraction helpers so failures report clean FAIL entries instead of tracebacks.

— Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@lawrencecchen, great — the guarded parsing helpers (parse_json_output and first_finding) are exactly the right pattern here. Clean FAIL: diagnostics will make CI failures much easier to triage. Thanks for the follow-up!


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 6, 2026

@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: 5

♻️ Duplicate comments (1)
CLI/CMUXCLI+Config.swift (1)

431-444: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prefer String(describing:) before localizedDescription here.

localizedDescription frequently collapses JSON/Cocoa parse failures into a generic message, which makes config doctor less actionable. Keep the debug description first, then fall back to String(describing: error), and only use localizedDescription last.

Suggested fix
     private static func configDoctorErrorMessage(_ error: Error) -> String {
         let nsError = error as NSError
         if let debug = nsError.userInfo[NSDebugDescriptionErrorKey] as? String {
             let trimmed = debug.trimmingCharacters(in: .whitespacesAndNewlines)
             if !trimmed.isEmpty {
                 return trimmed
             }
         }
-        let localized = nsError.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
-        if !localized.isEmpty {
-            return localized
-        }
         let described = String(describing: error).trimmingCharacters(in: .whitespacesAndNewlines)
-        return described.isEmpty ? "unknown config parse error" : described
+        if !described.isEmpty {
+            return described
+        }
+        let localized = nsError.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
+        return localized.isEmpty ? "unknown config parse error" : localized
     }

Based on learnings: Use String(describing: error) instead of error.localizedDescription when formatting errors in the cmux Swift CLI because it preserves the full cause.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+Config.swift around lines 431 - 444, The error formatting in
configDoctorErrorMessage currently falls back to localizedDescription before
String(describing:), which can hide useful parse details; update the function to
check NSDebugDescription first (nsError.userInfo[NSDebugDescriptionErrorKey]),
then use String(describing: error) trimmed for non-empty content, and only if
that is empty use nsError.localizedDescription, finally defaulting to "unknown
config parse error"; ensure you reference and modify the private static func
configDoctorErrorMessage(_ error: Error) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 19951-19956: The help/usage text block in CLI/cmux.swift omits the
`check` and `validate` aliases for the `doctor` command; locate the usage string
or array that currently reads "config <doctor|path|docs|reload>" and update it
to include the aliases (e.g. "config <doctor|check|validate|path|docs|reload>"
or otherwise list "doctor (aliases: check, validate)") so the `--help` output
shows those aliases; update the single place where that usage snippet appears
(search for the exact string in CLI/cmux.swift) and ensure formatting matches
the surrounding help lines.
- Around line 2067-2078: The early-path branch correctly keeps "reload" out of
the no-socket flow via configCommandDoesNotNeedSocket; update two things to fix
the help text and remove fragile coupling: 1) Change
runConfigCommand(socketPath: String) to accept an optional socketPath: String?
and, in the no-socket branch where you currently pass
CLISocketPathResolver.defaultSocketPath, pass nil instead (the call site using
runConfigCommand in this snippet). 2) Inside runConfigCommand add an explicit
guard that rejects/avoids any socket operations when socketPath == nil (return
an error or print and exit), making the no-socket intent explicit. Also update
configUsage() to list the undocumented aliases (help, check, validate, paths,
documentation) so usage output matches accepted subcommands. Ensure references:
configCommandDoesNotNeedSocket, runConfigCommand,
CLISocketPathResolver.defaultSocketPath, and configUsage are changed
accordingly.

In `@CLI/CMUXCLI`+Config.swift:
- Around line 315-331: The guard using fileManager.fileExists(atPath:
target.path) treats directories as files, so when target.path is a directory
Data(contentsOf:) fails; change the existence check to detect directories (use
FileManager.fileExists(atPath:isDirectory:) or attributesOfItem) and if
isDirectory return a ConfigDoctorFinding for that target (use the same shape as
the missing error: label: target.label, displayPath: target.displayPath, path:
target.path, status: "error" or "invalid", and a message like "path is a
directory, expected a file") instead of attempting Data(contentsOf:
URL(fileURLWithPath: target.path)).

In `@GhosttyTabs.xcodeproj/project.pbxproj`:
- Line 1759: Move the shared JSONCParser.swift and any dependent doctor core
files into a new SwiftPM library target (create/update Package.swift with a new
product and target), make the parser and any used types public if needed, update
the app and cmux-cli targets to import the new module instead of referencing the
file, remove JSONCParser.swift from the app target source list in Xcode (and any
duplicate copies), and adjust unit tests to depend on the new package target;
ensure the new package target builds for local Xcode integration and update any
import statements that referenced internal symbols to the new module name.

In `@tests/test_cli_config_doctor.py`:
- Around line 16-18: The current check accepts directories because
os.access(..., os.X_OK) is true for executable/searchable dirs; update the
conditional that assigns/returns explicit (the variable explicit derived from
os.environ.get("CMUX_CLI_BIN") or os.environ.get("CMUX_CLI")) to also require it
is a regular file (use os.path.isfile(explicit)) before checking os.access and
returning it, i.e. only return explicit when os.path.exists(explicit) and
os.path.isfile(explicit) and os.access(explicit, os.X_OK).

---

Duplicate comments:
In `@CLI/CMUXCLI`+Config.swift:
- Around line 431-444: The error formatting in configDoctorErrorMessage
currently falls back to localizedDescription before String(describing:), which
can hide useful parse details; update the function to check NSDebugDescription
first (nsError.userInfo[NSDebugDescriptionErrorKey]), then use
String(describing: error) trimmed for non-empty content, and only if that is
empty use nsError.localizedDescription, finally defaulting to "unknown config
parse error"; ensure you reference and modify the private static func
configDoctorErrorMessage(_ error: Error) accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ee0fbf8-ba0d-4c59-a0cf-a328ccaef3fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5091730 and 7cdf43b.

📒 Files selected for processing (7)
  • CLI/CMUXCLI+Config.swift
  • CLI/CMUXCLI+DocsSettings.swift
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/JSONCParser.swift
  • docs/cli-contract.md
  • tests/test_cli_config_doctor.py

Comment thread CLI/cmux.swift
Comment thread CLI/cmux.swift
Comment thread CLI/CMUXCLI+Config.swift Outdated
B900002FA1B2C3D4E5F60719 /* CMUXCLI+ThemeSupport.swift in Sources */,
B900002EA1B2C3D4E5F60719 /* CMUXCLI+Themes.swift in Sources */,
B9000033A1B2C3D4E5F60719 /* CMUXCLI+TopRendering.swift in Sources */,
C0DEF0B10000000000000003 /* JSONCParser.swift in Sources */,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move the shared config parser behind a package boundary.

JSONCParser.swift is now compiled into both the app and cmux-cli, but it still lives in the app target source tree. Since this is pure Foundation parsing logic reused across surfaces, keeping it wired directly through the Xcode targets will keep future config-doctor changes coupled to project wiring. Please extract the parser (and ideally the doctor core that depends on it) into a SwiftPM target and import it from the app/CLI/tests instead.

As per coding guidelines "Extract reusable domain logic used by more than one surface (Mac app, CLI, daemon, tests, previews, debug tooling, future iOS/shared code) into a dedicated SwiftPM package target instead of duplicating or centralizing in the app target".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GhosttyTabs.xcodeproj/project.pbxproj` at line 1759, Move the shared
JSONCParser.swift and any dependent doctor core files into a new SwiftPM library
target (create/update Package.swift with a new product and target), make the
parser and any used types public if needed, update the app and cmux-cli targets
to import the new module instead of referencing the file, remove
JSONCParser.swift from the app target source list in Xcode (and any duplicate
copies), and adjust unit tests to depend on the new package target; ensure the
new package target builds for local Xcode integration and update any import
statements that referenced internal symbols to the new module name.

Comment thread tests/test_cli_config_doctor.py
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Comment thread CLI/CMUXCLI+Config.swift
Comment on lines +187 to +209
private func runConfigDoctor(arguments: [String], jsonOutput: Bool) throws -> ConfigDoctorReport {
let options = try parseConfigDoctorOptions(arguments)
let targets = options.paths.isEmpty
? defaultConfigDoctorTargets()
: options.paths.enumerated().map { index, rawPath in
let path = Self.absoluteConfigPath(rawPath)
return ConfigDoctorTarget(
label: "custom \(index + 1)",
displayPath: Self.tildePath(path),
path: path,
missingIsError: true
)
}
let findings = targets.map(configDoctorFinding(for:))
let report = ConfigDoctorReport(findings: findings)

if jsonOutput {
print(jsonString(report.payload))
} else {
printConfigDoctorReport(report)
}
return report
}

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.

P1 --path flag is stripped before parseConfigDoctorOptions can see it

docsSettingsArguments constructs arguments via head.filter { !$0.hasPrefix("-") }, which discards every --prefixed token. For commandArgs = ["doctor", "--path", "/tmp/cmux.json"] this produces doctorArgs = ["/tmp/cmux.json"]; parseConfigDoctorOptions then throws on the orphaned path value. The --path=<file> form is also silently stripped. Every invocation matching the documented usage (cmux config doctor --path .cmux/cmux.json) exits 1, and the test suite ok_result case fails on any correctly compiled build. The fix is to parse commandArgs directly in the doctor branch rather than routing through docsSettingsArguments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not applicable on the current head. docsSettingsArguments preserves --path and --path=..., only removing --json; both documented forms pass against the tagged pr3454 CLI.

— Claude Code

coderabbitai[bot]
coderabbitai Bot previously requested changes May 6, 2026

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a73032af-4c9e-4c2b-9fd5-7046c2ac4bc7

📥 Commits

Reviewing files that changed from the base of the PR and between 7cdf43b and f990275.

📒 Files selected for processing (4)
  • CLI/CMUXCLI+Config.swift
  • CLI/cmux.swift
  • docs/cli-contract.md
  • tests/test_cli_config_doctor.py

Comment thread docs/cli-contract.md
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@lawrencecchen
lawrencecchen dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] May 6, 2026 01:02

Dismissed as stale after the requested CodeRabbit fixes were addressed in follow-up commits and the CodeRabbit check passed.

@lawrencecchen
lawrencecchen merged commit 308ed5a into main May 6, 2026
23 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — bd006242 Deployed May 6, 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.

1 participant