feat(cli): Phase 2 — 2.A CLI skeleton + 2.B config resolution (ADR-0047/0048)#40
Conversation
Record the two new runtime-dependency decisions gating Phase-2 workstreams 2.A/2.B (CLAUDE.md non-negotiable #2 — no new runtime dependency without an ADR): - ADR-0047 — the CLI command/UI framework: commander (2.A) + ink (2.E) + @clack/prompts (2.G/2.J), confined to apps/cli; tsup is the build tool (toolchain). - ADR-0048 — smol-toml for config files, confined to the apps/cli config loader (mirrors ADR-0035 for YAML); strict Zod (ADR-0033) stays the validation truth. Also states TOML 1.0 as the config format in config-spec.md (its canonical home) and indexes both ADRs as Accepted. Refs: ADR-0047, ADR-0048 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stand up apps/cli (the `relavium` bin) as the first engine consumer (build phase 2): - A commander program (tsup-bundled single ESM bin + shebang) registering the full commands.md surface (run/list/logs/status/gate/create/import/export/provider/ agent/init) with clean "not-yet-available" stubs until each command's workstream. - A framework-free process contract, testable with no TTY: position-independent global flags (--json/--no-color/--cwd/--config/--verbose/--quiet) extracted before parsing, output-mode detection (TTY/--json/CI), the deterministic exit-code map (0-3; 4 at 2.M), a typed CliError family + centralized error rendering (human→stderr, JSON envelope→ stdout, never a stack trace as primary output). - 31 unit tests (output-mode, errors, options/extraction, run integration). Green across typecheck, lint, test, build, format, the seam fence, and engine-deps. Renames the monorepo root package to `relavium-monorepo` to free the published name `relavium` for apps/cli; pins commander/smol-toml/tsup in the pnpm catalog; documents the finalized global options in commands.md. Refs: ADR-0047, ADR-0048 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the apps/cli config loader the rest of the CLI builds on (config-spec.md): - Discovery (config/paths.ts): the ~/.relavium global dir + walk-up discovery of the project .relavium/ from cwd (a non-directory `.relavium` is ignored). - Loading (config/load.ts): smol-toml parse (ADR-0048) behind a hardened wrapper — a pre-parse size cap, an absent layer → undefined, and every fault normalized to a typed, file-attributed ConfigError (exit 2) whose detail is a TOML position or a Zod field path, never the file's contents. Strict Zod (Global/Project schemas, ADR-0033) stays the validation truth. - Pure merge (config/resolve.ts): last-writer-wins across global → workspace → project (config-spec.md), MCP servers merged by name; framework-free and IO-free, so extractable to a shared package later. - 16 unit tests: merge precedence, discovery walk-up, load/validate, --config override, and unknown-key rejection (proving the strict schema accepts no stray/secret keys). `--config` overrides the global layer (project layers still apply), documented in commands.md. NB: `.relaviumignore` is a no-op for config resolution and lands with its consumer (2.J export). Toolchain green: typecheck, lint, test (47), build, format, fence, engine-deps. Refs: ADR-0048 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent code reviews of apps/cli (2.A/2.B). Fixes: - [MEDIUM] Honor POSIX `--` in the global-flag extractor: `--` and everything after it are passed to commander verbatim, so a literal `--json` can be a command argument; a lone `--` is a bare invocation → help, exit 0 (options.ts, run.ts). - [MEDIUM] Config schema errors no longer echo the offending value: formatZodError derives a value-free reason from the Zod issue code + schema-side data (expected type, allowed enum options, unknown key names) — never issue.message/issue.received — so the loader's "never the file's contents" guarantee now holds on the schema path too (load.ts). - [LOW] Empty `=`-form values (`--cwd=`, `--config=`) are rejected as invalid_invocation rather than silently becoming "" (options.ts). - [LOW] extractGlobalOptions returns its error instead of throwing, so a `--json` parsed before a failing global flag is still honored when rendering that error (options.ts, run.ts). - [LOW] Tests added for the MAX_CONFIG_BYTES size cap, the unreadable-path (EISDIR) branch, the value-non-echo hygiene guard, ensureGlobalConfigDir, and the `--`/empty-= cases. - [docs] Correct the specs.ts "whole surface" comment (chat family + gate-list/budget-resume land with their workstreams); ADR-0048 dated amendment reconciling the implemented `ConfigError` name and the value-free schema detail; config-spec.md documents project-scoped `[[mcp_servers]]`. 57 tests pass; typecheck / lint / build / format / fence / engine-deps green. Refs: ADR-0047, ADR-0048 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer's GuideImplements the initial Sequence diagram for CLI run flow and process contractsequenceDiagram
actor User
participant NodeProcess as index_ts
participant Run as run
participant Options as extractGlobalOptions
participant Program as buildProgram
participant Commander as Command_parseAsync
participant Errors as renderError
User->>NodeProcess: invoke relavium (process.argv)
NodeProcess->>Run: run(argv, processIo())
Run->>Options: extractGlobalOptions(argv)
Options-->>Run: { raw, rest, error }
alt extractError exists
Run->>Errors: renderError(error, renderCtx, io)
Run-->>NodeProcess: error.exitCode
else
Run->>Run: assertNoGlobalOptionConflicts(raw)
alt no subcommand tokens in rest
Run-->>NodeProcess: EXIT_CODES.success (print help)
else
Run->>Program: buildProgram(io)
Program-->>Run: Command instance
Run->>Commander: program.parseAsync(rest)
alt parse succeeds
Commander-->>Run: (resolved)
Run-->>NodeProcess: EXIT_CODES.success
else CommanderError
Commander-->>Run: CommanderError
Run-->>NodeProcess: EXIT_CODES.invalidInvocation or success
else other error
Run->>Errors: renderError(err, renderCtx, io)
Run-->>NodeProcess: toUserFacing(err).exitCode
end
end
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughIntroduces the complete ChangesCLI Package Bootstrap
Sequence Diagram(s)sequenceDiagram
actor User
participant index.ts
participant run
participant extractGlobalOptions
participant buildProgram
participant registerCommands
participant loadResolvedConfig
User->>index.ts: executes relavium <args>
index.ts->>run: run(process.argv, processIo())
run->>extractGlobalOptions: argv
extractGlobalOptions-->>run: ExtractedArgv {raw, rest, error?}
alt extraction error or --verbose+--quiet conflict
run-->>index.ts: exitCode 2 (invalidInvocation)
else bare invocation (no subcommand)
run-->>index.ts: exitCode 0 (help printed)
else normal subcommand
run->>buildProgram: io
buildProgram->>registerCommands: program
registerCommands-->>buildProgram: stubs registered
buildProgram-->>run: Command
run->>loadResolvedConfig: cwd, configPath
loadResolvedConfig-->>run: ResolvedConfig
run->>run: program.parseAsync(rest)
run-->>index.ts: exitCode (0–4)
end
index.ts->>index.ts: process.exitCode = exitCode
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="docs/decisions/0048-toml-config-parser.md" line_range="56-61" />
<code_context>
+[tech-stack.md](../tech-stack.md) for the version (added under the §9a cooling window).**
+
+- The parser is wrapped in a **hardened, defensive loader**, not called raw: a pre-parse
+ source-size cap, and **every** parse throw normalized to a typed, secret-free
+ `ConfigSyntaxError` carrying the file path and the library's line/column when present (never
+ the source text). The strict Zod config schemas ([ADR-0033](0033-strict-config-files-amends-0023.md))
+ remain the **single source of validation truth** on the parsed object; the parser's only job
+ is string → plain data. Relavium config files are **TOML 1.0**
+ ([config-spec.md](../reference/contracts/config-spec.md)); `ConfigSyntaxError` is a CLI-local
+ typed error (the loader is CLI-local) and would be promoted to a shared error only if the
+ loader is later extracted.
</code_context>
<issue_to_address>
**issue:** Error type name is inconsistent within the ADR (`ConfigError` vs `ConfigSyntaxError`).
The amendment block says the implemented error type is `ConfigError`, but this section still refers to `ConfigSyntaxError` (for normalized parse errors and as a CLI-local typed error). Please align these references to `ConfigError` and update the surrounding wording as needed to reflect the broader config/load scope rather than syntax-only.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This pull request scaffolds the Relavium CLI (apps/cli), implementing command registration with commander and a two-level TOML configuration loader using smol-toml and Zod. It also includes comprehensive unit tests, error handling, and updated documentation (ADRs 0047 and 0048). Feedback on the configuration loader suggests checking file sizes using statSync before reading them into memory to prevent potential out-of-memory issues with extremely large files.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @@ -0,0 +1,152 @@ | |||
| import { readFileSync } from 'node:fs'; | |||
| export function loadConfigFile<T>(filePath: string, schema: ZodType<T>): T | undefined { | ||
| let text: string; | ||
| try { | ||
| text = readFileSync(filePath, 'utf8'); | ||
| } catch (err) { | ||
| if (errnoCode(err) === 'ENOENT') { | ||
| return undefined; | ||
| } | ||
| throw new ConfigError(filePath, 'could not be read.', { cause: err }); | ||
| } | ||
|
|
||
| if (Buffer.byteLength(text, 'utf8') > MAX_CONFIG_BYTES) { | ||
| throw new ConfigError(filePath, `exceeds the ${MAX_CONFIG_BYTES}-byte config size limit.`); | ||
| } |
There was a problem hiding this comment.
To prevent potential out-of-memory (OOM) errors or performance issues when encountering extremely large files, check the file size using statSync before reading the entire file into memory with readFileSync.
export function loadConfigFile<T>(filePath: string, schema: ZodType<T>): T | undefined {
try {
const stat = statSync(filePath);
if (stat.size > MAX_CONFIG_BYTES) {
throw new ConfigError(filePath, 'exceeds the ' + MAX_CONFIG_BYTES + '-byte config size limit.');
}
} catch (err) {
if (errnoCode(err) === 'ENOENT') {
return undefined;
}
throw new ConfigError(filePath, 'could not be read.', { cause: err });
}
let text: string;
try {
text = readFileSync(filePath, 'utf8');
} catch (err) {
throw new ConfigError(filePath, 'could not be read.', { cause: err });
}There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/cli/src/index.ts (1)
17-17: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winUse the shared exit-code constant instead of a numeric literal.
Line 17 hardcodes
1in the fallback path. Prefer the centralized constant fromprocess/exit-codes.tsto keep this path aligned with the deterministic exit-code contract if values evolve.🤖 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 `@apps/cli/src/index.ts` at line 17, Replace the hardcoded numeric literal `1` in the `process.exitCode = 1` statement with the appropriate exit-code constant from the `process/exit-codes.ts` module. First, import the relevant exit-code constant at the top of the file, then substitute the literal value with that constant to ensure consistency with the centralized exit-code definitions.
🤖 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 `@apps/cli/src/process/options.ts`:
- Around line 73-76: The validation check for --cwd and --config options in the
token parsing logic does not reject empty string values passed with quotes (like
--cwd ""). Add an additional condition to the existing validation that checks if
the value is empty or contains only whitespace in addition to the existing
checks for undefined and options starting with a dash. This will ensure that
quoted-empty arguments are properly rejected and do not propagate downstream as
invalid paths or configurations.
In `@apps/cli/src/run.test.ts`:
- Line 68: The code at line 68 uses an unsafe TypeScript `as` cast when parsing
JSON, which doesn't verify the actual runtime shape of the data. Replace the
unsafe type assertion on the JSON.parse() result with a proper type guard
function. Create a type guard function that validates the parsed object has the
expected properties (type and code as strings) using the `is` keyword, then use
that function to check the parsed result before assignment. Apply this same fix
to line 82 as well, ensuring both locations use consistent runtime shape
validation instead of type assertions.
In `@docs/decisions/0048-toml-config-parser.md`:
- Around line 55-63: In the decision document section describing the hardened,
defensive loader (around the paragraph mentioning parse throw normalization),
replace all references to `ConfigSyntaxError` with `ConfigError` to align with
the amendment above and the actual loader implementation. The terminology should
be consistent across the document, as `ConfigError` is the typed error used for
all failure modes including unreadable files, oversized files, TOML parse
failures, and schema validation failures, not just syntax errors.
---
Nitpick comments:
In `@apps/cli/src/index.ts`:
- Line 17: Replace the hardcoded numeric literal `1` in the `process.exitCode =
1` statement with the appropriate exit-code constant from the
`process/exit-codes.ts` module. First, import the relevant exit-code constant at
the top of the file, then substitute the literal value with that constant to
ensure consistency with the centralized exit-code definitions.
🪄 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: 7bd9fd0a-b1a3-445a-93af-f8eaa4666b9c
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (32)
apps/cli/package.jsonapps/cli/src/commands/specs.tsapps/cli/src/config/errors.tsapps/cli/src/config/load.test.tsapps/cli/src/config/load.tsapps/cli/src/config/paths.test.tsapps/cli/src/config/paths.tsapps/cli/src/config/resolve.test.tsapps/cli/src/config/resolve.tsapps/cli/src/index.tsapps/cli/src/process/errors.test.tsapps/cli/src/process/errors.tsapps/cli/src/process/exit-codes.tsapps/cli/src/process/io.tsapps/cli/src/process/options.test.tsapps/cli/src/process/options.tsapps/cli/src/process/output-mode.test.tsapps/cli/src/process/output-mode.tsapps/cli/src/process/render-error.tsapps/cli/src/program.tsapps/cli/src/run.test.tsapps/cli/src/run.tsapps/cli/tsconfig.jsonapps/cli/tsup.config.tsdocs/decisions/0047-cli-framework-commander-ink-clack.mddocs/decisions/0048-toml-config-parser.mddocs/decisions/README.mddocs/reference/cli/commands.mddocs/reference/contracts/config-spec.mddocs/roadmap/phases/phase-2-cli.mdpackage.jsonpnpm-workspace.yaml
- options.ts: reject empty/whitespace-only spaced value flags (`--cwd ""`), not just the `=`-form; extract the verbosity nested ternary; refactor the extractor into a BOOLEAN_FLAGS table + `takeValueFlag` helper to drop cognitive complexity below the threshold. - load.ts: stat the file size with `statSync` BEFORE `readFileSync` (OOM hardening) and reject a non-regular-file path; drop the post-read byteLength check. - index.ts: top-level await instead of a promise chain; map the fatal-guard exit to `EXIT_CODES.workflowFailed` rather than a literal `1`. - run.test.ts: replace the unsafe `as` JSON casts with an `isErrorEnvelope` type guard; simplify the no-stack-leak assertion to a non-backtracking regex. - resolve.test.ts: explicit sort comparator. - ADR-0048: rename the loader bullet's `ConfigSyntaxError` → `ConfigError` (consistent with the amendment + implementation; reflects the stat-before-read change). 57 tests pass; typecheck / lint / build / format / fence / engine-deps green. Refs: ADR-0048 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…5852 hotspot Final review pass on PR #40: - Consistency fix (§2.A error contract): a commander parse error (e.g. `--json bogus`) now renders the structured JSON envelope on stdout under --json — commander's human stderr is suppressed (buildProgram `suppressErrorOutput`), the envelope is emitted in run.ts, and its redundant `error:` prefix is stripped. In human mode commander still writes to stderr. - Sonar S5852 (slow-regex security hotspot): de-regex apps/cli to remove every repetition-bearing regex — the `error:`-prefix strip is now `startsWith`+`slice`+`trimStart`, the no-stack-leak assertion splits lines + `startsWith('at ')`, and the literal-match test regexes became `toContain`/`startsWith`/string `toThrowError`. No regex remains in apps/cli/src, so there is no S5852 surface to review. - isErrorEnvelope type guard now validates `message` too (it is asserted on). 58 tests pass; typecheck / lint / build / format / fence / engine-deps green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
PR #40 merged: build-phase-2 workstreams 2.A (CLI skeleton + process contract) and 2.B (config resolution loader) are complete. - phase-2-cli.md: Status → In progress; §2.A and §2.B headings → ✅ Done (PR #40). - current.md: "What is active now" reflects 2.A/2.B landed + the next spine step (2.D → engine). - README.md / CLAUDE.md: high-level status refreshed (Phase 2 underway / in progress), still pointing at current.md as the canonical live-status home. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>



Summary
Builds the foundation of the
relaviumCLI (apps/cli) — Phase-2 workstreams 2.A (CLIskeleton + process contract) and 2.B (two-level config resolution) — plus the two gating
ADRs and the phase doc's sequencing plan. The CLI is the engine's first real consumer and its
regression harness; this PR lands the skeleton + config plumbing (no engine call yet — that is
2.D).
Decisions (ADRs, Accepted before implementation)
commander(2.A) +ink(2.E) +@clack/prompts(2.G/2.J), confined toapps/cli;tsupis the build tool.smol-tomlfor config files, confined to theapps/cliconfig loader (mirrors ADR-0035 for YAML); strict Zod (Global/Project schemas, ADR-0033) stays the validation truth.Both name a concrete rationale over the alternatives;
config-spec.mdnow states TOML 1.0; the monorepo root was renamedrelavium→relavium-monorepoto free the published name for the CLI.2.A — CLI skeleton & process contract
commanderprogram (tsup → single ESMbin+ shebang) registering the confirmed pre-chat surface (run/list/logs/status/gate/create/import/export/provider/agent/init) with clean "not-yet-available" stubs.--json/--no-color/--cwd/--config/--verbose/--quiet, POSIX--honored), output-mode detection (TTY/--json/CI), the deterministic exit-code map (0–3; 4 at 2.M), a typedCliErrorfamily + centralized error rendering (human→stderr, JSON envelope→stdout, never a stack/secret as primary output).2.B — Two-level config resolution
~/.relaviumglobal dir + walk-up discovery of the project.relavium/(a non-directory.relaviumis ignored).smol-tomlparse behind a hardened wrapper — size cap, absent→undefined, every fault → typed file-attributedConfigError(exit 2) whose detail is a TOML position or a Zod code-derived, value-free reason (never the file's contents).Review
Three independent code reviews ran against 2.A/2.B; all findings addressed in
29d0d08:--end-of-options gap in the extractor; Zod error messages echoing the offending config value (contradicted the loader's hygiene guarantee). Both fixed + regression-tested + empirically verified.=-form values,--json-on-extraction-error, size-cap/unreadable-path/ensureGlobalConfigDirtest gaps, the specs.ts comment, the ConfigError-vs-ConfigSyntaxError ADR drift (dated amendment), project-scoped[[mcp_servers]]doc drift.Verification
typecheck·lint·test·build·format· seam-fence · engine-deps · tools-typecheck (full root turbo: 20/20).any, no unsafeas(in-narrowing for errno/TOML position), typed discriminated errors (error-handling.md), Zod at the boundary, secrets never read/written (strict schema rejects stray keys — tested).apps/cliconfinement: no engine-internal or vendor-SDK imports; the engine-deps guard correctly excludes apps.Notes / deferred (no silent shortcuts)
runconsumes it in 2.D..relaviumignoreis a no-op for config resolution; it lands with its consumer (2.J export).phase-2-cli.md) is not flipped to Done — that happens after merge.🤖 Generated with Claude Code
Summary by Sourcery
Introduce the initial
relaviumCLI application with a framework-agnostic process layer, deterministic exit codes, and documented command surface, along with TOML-based config resolution and associated ADRs.New Features:
apps/clipackage providing therelaviumCLI entrypoint, wired through a single ESM binary built with tsup.run,list,logs,status,gate,create,import,export,provider,agent,init) with clean not-yet-implemented stubs.--json,--no-color,--cwd,--config,--verbose,--quiet) and output-mode detection (TTY/JSON/CI) for all commands..relaviumconfig, parses TOML config files withsmol-toml, validates via shared Zod schemas, and merges layers into a resolved config shape.Enhancements:
commander/ink/@clack/prompts) and TOML parsing (smol-toml), confined to the CLI app.relavium-monorepoand clarify that the published CLI package name isrelavium.Build:
commander,smol-toml,tsup) pinned via the catalog and confined toapps/cli.Documentation:
Tests:
runbehavior, and config path discovery, loading, and resolution.Summary by CodeRabbit
--json,--verbose/--quiet,--cwd,--config, color control) plus automatic output-mode switching (TUI vs plain, CI-aware).