Skip to content

feat(cli): add interactive setup command wizard#110

Open
jaythegeek wants to merge 2 commits into
mainfrom
refactor/the-setup-command
Open

feat(cli): add interactive setup command wizard#110
jaythegeek wants to merge 2 commits into
mainfrom
refactor/the-setup-command

Conversation

@jaythegeek
Copy link
Copy Markdown
Contributor

@jaythegeek jaythegeek commented Apr 7, 2026

Summary

Adds a new interactive setup command that guides users through initial configuration and onboarding. The wizard is platform-aware, handles API key validation, and returns appropriate exit codes for automation.

Changes

  • Add interactive setup command with step-by-step configuration flow
  • Implement platform-aware connectivity detection and API key validation
  • Enhance CLI output utilities for improved user feedback
  • Add comprehensive test coverage (525 lines) for setup wizard scenarios
  • Proper exit code handling for integration with automation tools

Files

  • cli/src/commands/setup.ts (+340)
  • cli/src/tests/commands/setup.test.ts (+525)
  • shared/src/cli-output.ts (+23)
  • shared/src/index.ts (+6)
  • cli/bin/agentver.ts (+3)

Review Notes

Addressed review feedback regarding platform-aware connectivity detection, exit code handling, and API key validation tests.

Add a new `setup` command that guides first-time users through platform
configuration, connectivity checks, authentication, agent detection, and
default organisation setup. Includes a `setup-check` companion for
non-interactive JSON output.

Adds SetupResult/SetupStep/SetupStepStatus types to @agentver/shared
with Zod schemas following established CLI output patterns.

14 tests covering both interactive and JSON modes.

Jeff the AI helped out!

Co-Authored-By: Jeff <[email protected]>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

A new interactive CLI setup wizard is introduced with a five-step configuration sequence (platform URL, connectivity, authentication, agent detection, organisation confirmation), alongside a non-interactive JSON-mode variant. Shared type definitions and command registration complete the feature.

Changes

Cohort / File(s) Summary
CLI Command Registration
packages/cli/bin/agentver.ts
Added imports and registration calls for both interactive (setup) and JSON-mode (setup-check) setup commands to the Commander program.
Setup Command Implementation
packages/cli/src/commands/setup.ts
Added interactive registerSetupCommand(program) implementing five sequential steps (platform-url, connectivity, authentication, detect-agents, default-org) and registerSetupCommandJSON(program) for non-interactive JSON setup-check.
Type Definitions & Exports
packages/shared/src/cli-output.ts, packages/shared/src/index.ts
Introduced Zod schemas and TypeScript types for setup step status (pass|fail|skip), individual steps, and overall setup results; re-exported these from the shared package.
Test Suite
packages/cli/src/__tests__/commands/setup.test.ts
Added comprehensive Vitest tests for interactive and JSON flows, mocking auth/config/agent detection and asserting output, exit codes, prompts, connectivity checks and aggregated JSON schema compliance.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI Program
    participant Setup as Setup Command
    participant Config as Config Service
    participant Platform as Platform API
    participant Auth as Auth Service
    participant Agents as Agent Detector

    User->>CLI: agentver setup
    CLI->>Setup: Execute setup wizard

    Setup->>Config: Get platform URL
    alt URL not set
        Setup->>User: Prompt for platform URL
        User-->>Setup: Provide URL
        Setup->>Config: Save platform URL
    end

    Setup->>Platform: Check connectivity (HEAD /api/v1/health)
    alt Online
        Platform-->>Setup: Success
    else Offline
        Platform-->>Setup: Failure
    end

    Setup->>Auth: Check authentication status
    alt Already authenticated
        Auth-->>Setup: isAuthenticated = true
    else Not authenticated
        Setup->>User: Prompt for OAuth/API key
        User-->>Setup: Provide credentials
        Setup->>Auth: Save API key
    end

    Setup->>Agents: Detect agents (project + global)
    Agents-->>Setup: Agent list

    Setup->>Config: Get default organisation
    Config-->>Setup: Organisation (if set)

    Setup->>Setup: Aggregate results
    Setup->>User: Display summary with per-step status
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): add interactive setup command wizard' clearly and concisely describes the main change: adding an interactive setup command to the CLI.

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


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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/__tests__/commands/setup.test.ts`:
- Around line 200-208: Add a new unit test that covers the API key
authentication path by mocking prompts to return the platform URL, then method:
'apikey', then an apiKey value, importing and spying on saveCredentials from
registry/auth.js, running buildProgram() and invoking
program.parseAsync(['node','agentver','setup']), and finally asserting
saveCredentials was called with the provided apiKey; reference the setup.ts path
where saveCredentials is called and the functions prompts, isAuthenticated (mock
to false), buildProgram, and saveCredentials to locate where to add the test.
- Around line 22-24: Remove the unused vi.mock for '../../registry/client.js' in
the setup.test.ts test; specifically delete the mock declaration that defines
getRegistryUrl as a vi.fn().mockReturnValue('https://app.agentver.com/api/v1')
since setup.ts does not import or rely on client.js, leaving no behavior to
mock.

In `@packages/cli/src/commands/setup.ts`:
- Around line 108-109: Replace the unnecessary dynamic import of saveCredentials
with a static import: add saveCredentials to the existing static import from
../registry/auth.js and then call saveCredentials({ apiKey: apiKey.trim() })
directly (remove the await import(...) block and direct destructuring). This
uses the already-exported saveCredentials symbol and simplifies the code path.
🪄 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: fe6eac83-8a9b-48d0-88a5-25818bf424c1

📥 Commits

Reviewing files that changed from the base of the PR and between ad37bc6 and 3641c5b.

📒 Files selected for processing (5)
  • packages/cli/bin/agentver.ts
  • packages/cli/src/__tests__/commands/setup.test.ts
  • packages/cli/src/commands/setup.ts
  • packages/shared/src/cli-output.ts
  • packages/shared/src/index.ts

Comment thread packages/cli/src/__tests__/commands/setup.test.ts Outdated
Comment thread packages/cli/src/__tests__/commands/setup.test.ts
Comment thread packages/cli/src/commands/setup.ts Outdated
…y tests

- Replace checkOnline() with checkPlatformReachable() that probes the
  configured platformUrl directly, not the default registry URL
- Add process.exit(failed > 0 ? 1 : 0) to setup-check for non-zero
  exit on failure, mirroring doctor command behaviour
- Add tests for API key save path (trimmed key, blank input, cancelled
  prompt) and setup-check exit codes
- Replace dynamic import of saveCredentials with static import
- Improve test isolation with explicit mock resets in beforeEach

Jeff the AI helped out!

Co-Authored-By: Jeff <[email protected]>
@jaythegeek jaythegeek changed the title feat(cli): add setup command — interactive onboarding wizard feat(cli): add interactive setup command wizard Apr 7, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/__tests__/commands/setup.test.ts`:
- Around line 175-202: Add a test that covers the "change: true" branch when a
platform URL already exists: mock getPlatformUrl to return
'https://existing.example.com', mock prompts to first resolve { change: true }
and then resolve the follow-up URL prompt with an empty/undefined value
(simulating user accepting change but not entering a new URL), run
buildProgram().parseAsync(['node','agentver','setup']), and assert writeConfig
is either not called or called with an object containing platformUrl:
'https://existing.example.com' (use the existing test helpers getPlatformUrl,
prompts, writeConfig, buildProgram, parseAsync to locate and implement this
case).

In `@packages/cli/src/commands/setup.ts`:
- Around line 178-191: stepDefaultOrg currently only reports or skips when no
defaultOrg is set; modify it to prompt and persist a defaultOrg during setup so
onboarding can complete. In stepDefaultOrg use readConfig() to detect missing
defaultOrg, prompt the user (e.g., via existing interactive prompt helper used
elsewhere or a new prompt) to enter a default org slug, validate input, set
config.defaultOrg, call the config persistence function (writeConfig or
saveConfig) and then return a 'pass' StepResult with the configured value; keep
the existing behavior of returning 'pass' immediately when config.defaultOrg
already exists. Ensure you reference and reuse the existing helpers (readConfig,
writeConfig/saveConfig and any interactive prompt utility) to implement this
flow.
- Around line 24-38: The abort timer may remain active if fetch() throws; in
checkPlatformReachable declare timeoutId (and controller if needed) in the outer
scope and move clearTimeout(timeoutId) into a finally block so the timer is
always cleared whether fetch resolves or throws; ensure the finally runs after
the try/catch and still returns response.ok (or false) as before.
- Around line 44-61: When an existing platform URL is present and the user
confirms they want to change it (the prompts flow that sets the local variable
change), the follow-up prompt for url still uses DEFAULT_PLATFORM_URL; update
the prompts call that captures { url } so its initial value is existing when
existing is defined and change is true (fall back to DEFAULT_PLATFORM_URL only
if existing is falsy). Locate the confirmation prompt logic using the variables
existing and change and the subsequent prompts(...) that sets { url }, and make
initial conditional on existing so pressing Enter preserves the current value
rather than reverting to DEFAULT_PLATFORM_URL.
- Around line 221-265: The code uses multiple console.log(...) calls in the
setup flow (inside the top-level setup routine that calls stepPlatformUrl(),
stepConnectivity(), stepAuthentication(), stepDetectAgents(projectRoot),
stepDefaultOrg(), and prints formatStep(step)), which violates CLI packaging
rules; replace all console.log(...) usages in this file with the repository's
CLI output abstraction (or fallback to process.stdout.write) so interactive
messages and the summary use the unified output API (e.g.,
output.write/chalk-wrapped strings) instead of console.log, preserving the same
text/formatting and newlines for the header, per-step output, summary counts,
and the doctor hint. Ensure you update every console.log call in this function
(including the blank console.log() lines and those that log formatStep(step)) so
no console.log remains.
🪄 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: 36de6f88-f868-4bd4-9cee-e0623ae3b23f

📥 Commits

Reviewing files that changed from the base of the PR and between 3641c5b and 6ac7cde.

📒 Files selected for processing (2)
  • packages/cli/src/__tests__/commands/setup.test.ts
  • packages/cli/src/commands/setup.ts

Comment on lines +175 to +202
it('writes platform URL to config when provided', async () => {
vi.mocked(prompts)
.mockResolvedValueOnce({ url: 'https://custom.example.com' }) // platform URL
.mockResolvedValueOnce({ method: 'skip' }) // auth method

const program = buildProgram()
await program.parseAsync(['node', 'agentver', 'setup'])

expect(writeConfig).toHaveBeenCalledWith(
expect.objectContaining({ platformUrl: 'https://custom.example.com' })
)
})

// -------------------------------------------------------------------------
// Platform URL — skips when already set and user declines change
// -------------------------------------------------------------------------

it('skips platform URL when already set and user declines change', async () => {
vi.mocked(getPlatformUrl).mockReturnValue('https://existing.example.com')
vi.mocked(prompts)
.mockResolvedValueOnce({ change: false }) // decline change
.mockResolvedValueOnce({ method: 'skip' }) // auth method

const program = buildProgram()
await program.parseAsync(['node', 'agentver', 'setup'])

expect(writeConfig).not.toHaveBeenCalled()
})
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

Add coverage for the existing-URL change path.

The suite only exercises the decline branch when a platform URL already exists. A regression in the change: true flow would currently slip through, so please add a case that accepts the change prompt and verifies the previous URL is preserved unless the user explicitly replaces it. As per coding guidelines, "If new code paths were added (functions, endpoints, branches) but no corresponding tests exist, flag as SHOULD_FIX."

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

In `@packages/cli/src/__tests__/commands/setup.test.ts` around lines 175 - 202,
Add a test that covers the "change: true" branch when a platform URL already
exists: mock getPlatformUrl to return 'https://existing.example.com', mock
prompts to first resolve { change: true } and then resolve the follow-up URL
prompt with an empty/undefined value (simulating user accepting change but not
entering a new URL), run buildProgram().parseAsync(['node','agentver','setup']),
and assert writeConfig is either not called or called with an object containing
platformUrl: 'https://existing.example.com' (use the existing test helpers
getPlatformUrl, prompts, writeConfig, buildProgram, parseAsync to locate and
implement this case).

Comment on lines +24 to +38
async function checkPlatformReachable(platformUrl: string): Promise<boolean> {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), PLATFORM_CHECK_TIMEOUT_MS)

const response = await fetch(`${platformUrl}/api/v1/health`, {
method: 'HEAD',
signal: controller.signal,
})

clearTimeout(timeoutId)
return response.ok
} catch {
return false
}
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

Clear the abort timer in a finally block.

If fetch() fails before the timeout fires, clearTimeout() is never reached. That leaves a live timer behind, which can keep the interactive command open for up to three seconds after a fast failure.

🩹 Suggested fix
 async function checkPlatformReachable(platformUrl: string): Promise<boolean> {
+  const controller = new AbortController()
+  const timeoutId = setTimeout(() => controller.abort(), PLATFORM_CHECK_TIMEOUT_MS)
+
   try {
-    const controller = new AbortController()
-    const timeoutId = setTimeout(() => controller.abort(), PLATFORM_CHECK_TIMEOUT_MS)
-
     const response = await fetch(`${platformUrl}/api/v1/health`, {
       method: 'HEAD',
       signal: controller.signal,
     })
-
-    clearTimeout(timeoutId)
     return response.ok
   } catch {
     return false
+  } finally {
+    clearTimeout(timeoutId)
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/setup.ts` around lines 24 - 38, The abort timer may
remain active if fetch() throws; in checkPlatformReachable declare timeoutId
(and controller if needed) in the outer scope and move clearTimeout(timeoutId)
into a finally block so the timer is always cleared whether fetch resolves or
throws; ensure the finally runs after the try/catch and still returns
response.ok (or false) as before.

Comment on lines +44 to +61
if (existing) {
const { change } = (await prompts({
type: 'confirm',
name: 'change',
message: `Platform URL is already set to ${existing}. Change it?`,
initial: false,
})) as { change?: boolean }

if (!change) {
return { name: 'platform-url', status: 'skip', message: `Kept existing: ${existing}` }
}
}

const { url } = (await prompts({
type: 'text',
name: 'url',
message: 'Platform URL',
initial: DEFAULT_PLATFORM_URL,
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

Use the current platform URL as the edit default.

When existing is set and the user chooses to change it, the next prompt is still prefilled with DEFAULT_PLATFORM_URL. On a self-hosted install, pressing enter there silently rewrites the config back to the public default.

🩹 Suggested fix
   const { url } = (await prompts({
     type: 'text',
     name: 'url',
     message: 'Platform URL',
-    initial: DEFAULT_PLATFORM_URL,
+    initial: existing ?? DEFAULT_PLATFORM_URL,
     validate: (value: string) => {
       if (!value.startsWith('http://') && !value.startsWith('https://')) {
         return 'URL must start with http:// or https://'
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/setup.ts` around lines 44 - 61, When an existing
platform URL is present and the user confirms they want to change it (the
prompts flow that sets the local variable change), the follow-up prompt for url
still uses DEFAULT_PLATFORM_URL; update the prompts call that captures { url }
so its initial value is existing when existing is defined and change is true
(fall back to DEFAULT_PLATFORM_URL only if existing is falsy). Locate the
confirmation prompt logic using the variables existing and change and the
subsequent prompts(...) that sets { url }, and make initial conditional on
existing so pressing Enter preserves the current value rather than reverting to
DEFAULT_PLATFORM_URL.

Comment on lines +178 to +191
function stepDefaultOrg(): StepResult {
const config = readConfig()
if (config.defaultOrg) {
return {
name: 'default-org',
status: 'pass',
message: `Default organisation: ${config.defaultOrg}`,
}
}
return {
name: 'default-org',
status: 'skip',
message: 'No default organisation set — use `agentver config set defaultOrg <slug>` to set one',
}
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

This step never actually configures defaultOrg.

It only reports the current value or tells the user to run agentver config set ... later. First-time users therefore still cannot finish onboarding from agentver setup, even though this is one of the setup stages.

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

In `@packages/cli/src/commands/setup.ts` around lines 178 - 191, stepDefaultOrg
currently only reports or skips when no defaultOrg is set; modify it to prompt
and persist a defaultOrg during setup so onboarding can complete. In
stepDefaultOrg use readConfig() to detect missing defaultOrg, prompt the user
(e.g., via existing interactive prompt helper used elsewhere or a new prompt) to
enter a default org slug, validate input, set config.defaultOrg, call the config
persistence function (writeConfig or saveConfig) and then return a 'pass'
StepResult with the configured value; keep the existing behavior of returning
'pass' immediately when config.defaultOrg already exists. Ensure you reference
and reuse the existing helpers (readConfig, writeConfig/saveConfig and any
interactive prompt utility) to implement this flow.

Comment on lines +221 to +265
console.log(chalk.bold('\nAgentver Setup\n'))
console.log(chalk.dim('This wizard will help you configure Agentver.\n'))

const steps: StepResult[] = []

// Step 1: Platform URL
steps.push(await stepPlatformUrl())

// Step 2: Connectivity check
steps.push(await stepConnectivity())

// Step 3: Authentication
steps.push(await stepAuthentication())

// Step 4: Detect agents
steps.push(stepDetectAgents(projectRoot))

// Step 5: Default organisation
steps.push(stepDefaultOrg())

// Summary
const completed = steps.filter((s) => s.status === 'pass').length
const failed = steps.filter((s) => s.status === 'fail').length
const skipped = steps.filter((s) => s.status === 'skip').length

console.log(chalk.bold('\nSetup summary:\n'))

for (const step of steps) {
console.log(formatStep(step))
}

console.log()
console.log(
chalk.dim(
`${chalk.green(String(completed))} completed, ${chalk.red(String(failed))} failed, ${chalk.yellow(String(skipped))} skipped`
)
)

if (failed > 0) {
console.log(
chalk.dim(`\nRun ${chalk.bold('agentver doctor --fix')} to diagnose and repair issues.`)
)
}

console.log()
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 | 🔴 Critical

Avoid console.log() in shipped CLI commands.

These direct console.log() calls violate the repository boundary rules for packages/cli/src. Please route the interactive output through the existing output abstraction or process.stdout.write() instead. As per coding guidelines, "No commented-out code, console.log calls, or debug artefacts in production code." and "No debug logging or console.log statements."

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

In `@packages/cli/src/commands/setup.ts` around lines 221 - 265, The code uses
multiple console.log(...) calls in the setup flow (inside the top-level setup
routine that calls stepPlatformUrl(), stepConnectivity(), stepAuthentication(),
stepDetectAgents(projectRoot), stepDefaultOrg(), and prints formatStep(step)),
which violates CLI packaging rules; replace all console.log(...) usages in this
file with the repository's CLI output abstraction (or fallback to
process.stdout.write) so interactive messages and the summary use the unified
output API (e.g., output.write/chalk-wrapped strings) instead of console.log,
preserving the same text/formatting and newlines for the header, per-step
output, summary counts, and the doctor hint. Ensure you update every console.log
call in this function (including the blank console.log() lines and those that
log formatStep(step)) so no console.log remains.

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