Skip to content

feat(cli): add cli package#138

Merged
gaboesquivel merged 2 commits intomainfrom
cli
Mar 12, 2026
Merged

feat(cli): add cli package#138
gaboesquivel merged 2 commits intomainfrom
cli

Conversation

@gaboesquivel
Copy link
Member

@gaboesquivel gaboesquivel commented Mar 12, 2026

  • Add packages/cli with Commander, API key auth, config persistence
  • Exclude auth endpoints; mirror core API nesting
  • Generate commands from OpenAPI (scripts/generate-cli.mjs)
  • Config: API_KEY/BASILIC_API_KEY env or ~/.config/basilic/config.json
  • Docs: README, development/cli.mdx, agentic integrations note

Summary by CodeRabbit

  • New Features
    • Introduced a new CLI for the Basilic API with API key auth, dynamic commands reflecting API endpoints, and JSON output for scripting/agent use.
  • Documentation
    • Added user docs and README describing CLI usage, auth methods, commands, local testing, and agentic integration notes.
  • Chores
    • Added package manifest, build config, generation task, lint overrides, and generation tooling to support the new CLI.

- Add packages/cli with Commander, API key auth, config persistence
- Exclude auth endpoints; mirror core API nesting
- Generate commands from OpenAPI (scripts/generate-cli.mjs)
- Config: API_KEY/BASILIC_API_KEY env or ~/.config/basilic/config.json
- Docs: README, development/cli.mdx, agentic integrations note
@vercel
Copy link

vercel bot commented Mar 12, 2026

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

Project Deployment Actions Updated (UTC)
basilic-docs Ready Ready Preview, Comment Mar 12, 2026 0:39am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
basilic-fastify Ignored Ignored Preview Mar 12, 2026 0:39am
basilic-next Ignored Ignored Preview Mar 12, 2026 0:39am

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Walkthrough

A new CLI package, @repo/cli, is added. It includes a TypeScript CLI entrypoint, OpenAPI-driven command generation, config management for API key/base URL, build and lint configs, documentation, and CI task integration for generating CLI sources.

Changes

Cohort / File(s) Summary
Docs & Catalog
README.md, apps/docu/content/docs/development/packages.mdx, packages/cli/README.md, apps/docu/content/docs/development/cli.mdx
Add CLI package entry and comprehensive documentation describing auth resolution, commands, testing, and agentic integration guidance.
CLI Implementation
packages/cli/src/cli.ts, packages/cli/src/config.ts, packages/cli/src/run-command.ts
Add CLI program (Commander) with interactive/env/config API key resolution, cross-platform config persistence, dynamic command registration from generated specs, parameter handling, and JSON output/error handling.
Codegen & Generated Outputs
packages/cli/scripts/generate-cli.mjs, packages/cli/src/gen/*
Add OpenAPI parser to generate command metadata and TypeScript exports (operationMeta, commandSpecs) under src/gen.
Package & Build Config
packages/cli/package.json, packages/cli/tsup.config.ts, packages/cli/eslint.config.js
Introduce package manifest with bin/exports, build/lint scripts, tsup build config for ESM/node20, and ESLint overrides for the CLI package.
Lint Overrides & Tooling
biome.json, packages/cli/eslint.config.js
Add Biome override to relax console/no-non-null rules for CLI files and provide ESLint config extending shared rules.
Task Graph
turbo.json
Add @repo/cli#generate Turbo task depending on core generation, consuming OpenAPI input and producing src/gen/**.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI Program
    participant Config as Config Manager
    participant Gen as Generated Metadata
    participant Client as API Client
    participant Server as Basilic API Server

    User->>CLI: invoke command (args/flags)
    CLI->>Config: resolve API key (env → config → prompt)
    Config-->>CLI: apiKey, baseUrl
    CLI->>Gen: lookup commandSpec & operationMeta
    Gen-->>CLI: command metadata
    CLI->>Client: construct client (baseUrl, apiKey) and call runCommand
    Client->>Server: HTTP request
    Server-->>Client: response
    Client-->>CLI: result
    CLI->>User: output JSON / error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped a branch and wrote some code — hooray!
A tiny CLI to call the API today,
Keys from env or file, prompts if you please,
Commands from OpenAPI, quick as a breeze 🥕
Run, script, and sparkle — a rabbit's small bouquet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% 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 accurately describes the main change: adding a new CLI package to the repository with TypeScript and Commander implementation.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cli

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
biome.json (1)

149-157: Narrow the CLI-wide lint carve-out.

This disables noConsole and noNonNullAssertion for every file in packages/cli, including secret-handling code like src/config.ts. Please scope the exception to the specific command/output files that actually need raw console access or non-null assertions.

♻️ Suggested scope reduction
-    {
-      "includes": ["**/packages/cli/**"],
+    {
+      "includes": [
+        "**/packages/cli/src/cli.ts",
+        "**/packages/cli/src/run-command.ts",
+        "**/packages/cli/scripts/**"
+      ],
       "linter": {
         "rules": {
           "suspicious": { "noConsole": "off" },
           "style": { "noNonNullAssertion": "off" }
         }
       }
     },

As per coding guidelines "SECURITY: Verify no secrets logged (use logger redaction features)."

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

In `@biome.json` around lines 149 - 157, The current biome.json broad rule
carve-out disables suspicious.noConsole and style.noNonNullAssertion for every
file under the packages/cli include; narrow this by changing the include/glob so
the exceptions only apply to the actual CLI command/output files (e.g., CLI
command modules and entrypoint scripts) instead of the entire package, and keep
the rest of the package governed by the default linter rules; update the linter
rules block so suspicious.noConsole and style.noNonNullAssertion are scoped to
that narrower include (leave config files like src/config.ts outside the
exception).
packages/cli/scripts/generate-cli.mjs (1)

65-76: Generate richer parameter metadata instead of only names.

This script currently emits pathParams and bodyParams as { name } only. That is why packages/cli/src/run-command.ts, Lines 48-68, has to special-case fields like messages, tools, stream, and temperature, and why there is nowhere to represent query params or schema-driven boolean/number/object parsing. Emitting in, required, and schema/type info here would keep the runtime generic as the OpenAPI spec evolves.

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

In `@packages/cli/scripts/generate-cli.mjs` around lines 65 - 76, The getParams
function currently returns only names for pathParams and bodyParams; update it
to emit richer metadata including the parameter location (in), required flag,
and schema/type information so the runtime can generically handle query params
and typed parsing. Specifically, extend getParams to (1) iterate
operation.parameters and push objects like { name: p.name, in: p.in, required:
!!p.required, schema: p.schema } into separate arrays for pathParams and
queryParams (create queryParams alongside pathParams), and (2) when reading
operation.requestBody?.content?.['application/json']?.schema, emit bodyParams as
{ name, required: (body.required?.includes(name) || false), schema:
body.properties[name] } instead of just { name }; keep the same function name
getParams and return shape { pathParams, queryParams, bodyParams } so callers
(e.g., run-command.ts) can remove ad-hoc special-casing and perform
schema-driven parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/docu/content/docs/development/cli.mdx`:
- Line 34: The markdown link currently uses the placeholder "your-org" which
creates a dead link; update the link target in the CLI docs line that reads "See
[packages/cli/README.md](https://github.com/your-org/basilic/blob/main/packages/cli/README.md)"
to the real repository/org or an internal docs route (e.g., replace "your-org"
with the actual GitHub org name or point to the internal docs path) so the URL
resolves correctly; ensure the visible link text remains accurate and that the
change appears in the CLI docs MDX file where that link is defined.

In `@packages/cli/README.md`:
- Around line 18-23: Update the README line that lists config file locations to
use the environment variable notation for XDG_CONFIG_HOME: replace the literal
path string "XDG_CONFIG_HOME/basilic/config.json" with
"$XDG_CONFIG_HOME/basilic/config.json" so it clearly indicates an environment
variable rather than a literal directory; ensure the other listed path
(`~/.config/basilic/config.json`) remains unchanged.
- Around line 7-14: Remove the misleading global install instruction that
suggests running "pnpm add -g `@repo/cli`" because the package is marked private;
update the README code block to only show the monorepo-local workflow (the pnpm
--filter `@repo/cli` build and node packages/cli/dist/cli.js --help commands) and
delete or replace any reference to installing `@repo/cli` globally so readers are
not instructed to publish/Install a private package.

In `@packages/cli/src/cli.ts`:
- Around line 47-52: The code is coercing every top-level value of cmdOpts.body
into strings (using String(v)) which loses arrays/objects/booleans; instead,
parse rawBody into an object and assign the parsed value(s) without
stringifying: e.g. keep the parsed object and set opts.body = parsed (or assign
opts[k] = v without String(v)) so types are preserved for downstream code (see
rawBody, cmdOpts.body, parsed, opts and the consumer in run-command.ts); update
the branch to return opts with the original parsed values rather than their
String() representations.
- Around line 10-17: The promptApiKey function currently uses readline.question
which echoes the API key; change promptApiKey to suppress echo by either
(preferred) replacing readline usage with a secure prompt library like prompts
or inquirer that supports hidden input, or implement manual echo suppression by
setting process.stdin.setRawMode(true), reading character-by-character, writing
a mask (or nothing) to stdout, and restoring raw mode before resolving; ensure
the function name promptApiKey and any callers (e.g., config set-api-key flow)
use the updated implementation and that stdin state is always restored on
errors.
- Around line 128-130: Replace the current use of this.parent?.opts() when
computing baseUrl inside the cmd.action handler so global/root options are
visible for nested commands: call this.optsWithGlobals() and read baseUrl from
that merged options object (fall back to resolveBaseUrl() only if the merged
optsWithGlobals().baseUrl is undefined); similarly replace this.opts?.() used to
populate optsFromCmd with this.optsWithGlobals() to ensure global options are
included. Ensure you update references in the cmd.action anonymous function (the
code that assigns baseUrl and optsFromCmd) and keep resolveBaseUrl() as the
final fallback.

In `@packages/cli/src/config.ts`:
- Around line 38-48: The saveConfig function writes secrets to disk with default
permissions; change directory creation and file write to use restrictive modes
so the config (apiKey) is not world-readable: when calling mkdirSync for the
config directory (in saveConfig using getConfigPath / dir) pass a mode of 0o700
(or equivalent) and when writing the file (writeFileSync) ensure the file is
created with mode 0o600 (or call chmodSync after write to set 0o600); also
consider detecting existing files and tightening their permissions if needed
(use loadConfig, getConfigPath, mkdirSync, writeFileSync, chmodSync as the
referenced symbols).

In `@packages/cli/src/run-command.ts`:
- Around line 58-66: The CLI currently silently mutates/omits flags on parse
errors; update the parsing in run-command so that when p.name === 'stream' you
explicitly accept only 'true'/'1' -> true and 'false'/'0' -> false and otherwise
throw/exit with a clear error about an invalid stream value, and when p.name ===
'tools' and typeof v === 'string' attempt JSON.parse(v) but on parse failure do
not set bodyParams[p.name] = undefined — instead throw/exit with a clear error
indicating malformed JSON for --tools; keep the existing handling for
temperature/model (using Number.parseFloat for 'temperature') but ensure invalid
numeric input also results in a validation error rather than silently skipping.

---

Nitpick comments:
In `@biome.json`:
- Around line 149-157: The current biome.json broad rule carve-out disables
suspicious.noConsole and style.noNonNullAssertion for every file under the
packages/cli include; narrow this by changing the include/glob so the exceptions
only apply to the actual CLI command/output files (e.g., CLI command modules and
entrypoint scripts) instead of the entire package, and keep the rest of the
package governed by the default linter rules; update the linter rules block so
suspicious.noConsole and style.noNonNullAssertion are scoped to that narrower
include (leave config files like src/config.ts outside the exception).

In `@packages/cli/scripts/generate-cli.mjs`:
- Around line 65-76: The getParams function currently returns only names for
pathParams and bodyParams; update it to emit richer metadata including the
parameter location (in), required flag, and schema/type information so the
runtime can generically handle query params and typed parsing. Specifically,
extend getParams to (1) iterate operation.parameters and push objects like {
name: p.name, in: p.in, required: !!p.required, schema: p.schema } into separate
arrays for pathParams and queryParams (create queryParams alongside pathParams),
and (2) when reading
operation.requestBody?.content?.['application/json']?.schema, emit bodyParams as
{ name, required: (body.required?.includes(name) || false), schema:
body.properties[name] } instead of just { name }; keep the same function name
getParams and return shape { pathParams, queryParams, bodyParams } so callers
(e.g., run-command.ts) can remove ad-hoc special-casing and perform
schema-driven parsing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1834fe99-2edf-401a-a889-54c86a78a06b

📥 Commits

Reviewing files that changed from the base of the PR and between 2680514 and 4a43447.

⛔ Files ignored due to path filters (3)
  • packages/cli/src/gen/commands.gen.ts is excluded by !**/gen/**, !**/gen/**, !**/*.gen.ts
  • packages/cli/tsconfig.json is excluded by !**/tsconfig*.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • README.md
  • apps/docu/content/docs/development/cli.mdx
  • apps/docu/content/docs/development/packages.mdx
  • biome.json
  • packages/cli/README.md
  • packages/cli/eslint.config.js
  • packages/cli/package.json
  • packages/cli/scripts/generate-cli.mjs
  • packages/cli/src/cli.ts
  • packages/cli/src/config.ts
  • packages/cli/src/run-command.ts
  • packages/cli/tsup.config.ts
  • turbo.json

Comment on lines +47 to +52
const rawBody = cmdOpts.body as string | undefined
if (rawBody)
try {
const parsed = JSON.parse(rawBody) as Record<string, unknown>
for (const [k, v] of Object.entries(parsed)) if (v !== undefined) opts[k] = String(v)
return opts
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--body is lossy here, not raw JSON.

After parsing, every top-level value is coerced with String(v). Arrays/objects become "[object Object]", booleans become strings, and nested payloads stop matching the API contract before they even reach packages/cli/src/run-command.ts.

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

In `@packages/cli/src/cli.ts` around lines 47 - 52, The code is coercing every
top-level value of cmdOpts.body into strings (using String(v)) which loses
arrays/objects/booleans; instead, parse rawBody into an object and assign the
parsed value(s) without stringifying: e.g. keep the parsed object and set
opts.body = parsed (or assign opts[k] = v without String(v)) so types are
preserved for downstream code (see rawBody, cmdOpts.body, parsed, opts and the
consumer in run-command.ts); update the branch to return opts with the original
parsed values rather than their String() representations.

- fix cli.mdx dead link (your-org -> blockmatic)
- fix README: use $XDG_CONFIG_HOME notation, remove global install
- add restrictive permissions for config (0o700 dir, 0o600 file)
- suppress API key echo in promptApiKey
- use optsWithGlobals for nested command options
- validate stream/tools/temperature in run-command
- narrow biome packages/cli carve-out to cli.ts and gen/
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/cli/src/cli.ts (1)

55-73: ⚠️ Potential issue | 🟠 Major

--body still breaks mixed path+body commands.

This branch returns before the meta.pathParams loop, so any command that needs both a path param and a body drops opts.path entirely when --body is used. String(v) also flattens arrays/objects into "[object Object]", so structured JSON is corrupted before it reaches packages/cli/src/run-command.ts. Keep the parsed body as-is, validate that it is an object, and still merge the path params.

💡 Direction for a fix
-function buildOpts(
+function buildOpts(
   meta: (typeof operationMeta)[keyof typeof operationMeta],
   cmdOpts: Record<string, unknown>,
-): Record<string, string | undefined> {
-  const opts: Record<string, string | undefined> = {}
+): Record<string, unknown> {
+  const opts: Record<string, unknown> = {}
   const rawBody = cmdOpts.body as string | undefined
-  if (rawBody)
+  if (rawBody)
     try {
-      const parsed = JSON.parse(rawBody) as Record<string, unknown>
-      for (const [k, v] of Object.entries(parsed)) if (v !== undefined) opts[k] = String(v)
-      return opts
+      const parsed = JSON.parse(rawBody) as Record<string, unknown>
+      if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
+        console.error('Invalid --body JSON: expected an object')
+        process.exit(1)
+      }
+      Object.assign(opts, parsed)
     } catch {
       console.error('Invalid --body JSON')
       process.exit(1)
     }
 
   for (const p of meta.pathParams) opts[p.name] = String(cmdOpts[p.name] ?? '')
-  for (const p of meta.bodyParams) opts[p.name] = String(cmdOpts[p.name] ?? '')
+  if (!rawBody) for (const p of meta.bodyParams) opts[p.name] = String(cmdOpts[p.name] ?? '')
   return opts
 }

You’ll also need to widen the opts type in packages/cli/src/run-command.ts so raw JSON values can flow through unchanged.

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

In `@packages/cli/src/cli.ts` around lines 55 - 73, The buildOpts function returns
early when --body is present and JSON-parses it, which drops meta.pathParams and
coerces structured values to strings; change buildOpts so that after JSON.parse
it validates parsed is a plain object, merges its entries into opts without
String(...) coercion (preserving arrays/objects), and then continues to set
meta.pathParams and meta.bodyParams defaults as needed; update the consumer type
in run-command.ts to accept non-string raw JSON values so the uncoerced values
flow through unchanged.
packages/cli/src/config.ts (1)

41-48: ⚠️ Potential issue | 🟠 Major

Tighten permissions on existing config paths too.

mode here only applies when the directory/file is created. If basilic/ or config.json already exists with broader permissions, this keeps the old mode and the stored apiKey stays readable to other local users. Follow the write with chmodSync(dir, 0o700) and chmodSync(path, 0o600). As per coding guidelines, "SECURITY: Verify no secrets exposed in code (API keys, tokens, passwords)".

🔒 Proposed hardening
-import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
@@
   const path = getConfigPath()
   const dir = join(path, '..')
   if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 })
+  chmodSync(dir, 0o700)
@@
   }
   writeFileSync(path, JSON.stringify(next, null, 2), { encoding: 'utf-8', mode: 0o600 })
+  chmodSync(path, 0o600)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/config.ts` around lines 41 - 48, The config write only sets
modes on creation; ensure existing paths are hardened by calling chmodSync(dir,
0o700) after ensuring the directory exists (regardless of whether mkdirSync
created it) and chmodSync(path, 0o600) immediately after writeFileSync; update
the function that builds and writes the Config (references: dir, path,
loadConfig, writeFileSync, mkdirSync) to always apply these chmodSync calls so
existing basilic/ and config.json get tightened too.
🤖 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/cli.ts`:
- Around line 12-24: The prompt is being suppressed because mutedStdout.muted is
set before rl.question, so the prompt text doesn't appear; fix by emitting the
prompt to the real stdout before enabling mute or by special-casing the prompt
in the custom Writable: either call process.stdout.write('API key: ') (or
rl.output.write) before setting mutedStdout.muted = true and then call
rl.question with the muted output, or change mutedStdout.write to detect the
prompt string (from rl.question) and forward it to process.stdout even when
muted; update references in this block (mutedStdout, createInterface,
rl.question) accordingly and leave mutedStdout.muted toggled back to false after
rl.close().
- Line 181: The CLI currently calls program.parse(), which does not await
promise-returning .action handlers (e.g., the async action callbacks registered
on the Command instance), so switch the entrypoint to await program.parseAsync()
instead of program.parse() to ensure async handlers are awaited and errors are
handled correctly; locate the call to program.parse() and replace it with an
awaited call to program.parseAsync() (or return the Promise from main) so
Commander will wait for the async .action(...) callbacks to complete.

---

Duplicate comments:
In `@packages/cli/src/cli.ts`:
- Around line 55-73: The buildOpts function returns early when --body is present
and JSON-parses it, which drops meta.pathParams and coerces structured values to
strings; change buildOpts so that after JSON.parse it validates parsed is a
plain object, merges its entries into opts without String(...) coercion
(preserving arrays/objects), and then continues to set meta.pathParams and
meta.bodyParams defaults as needed; update the consumer type in run-command.ts
to accept non-string raw JSON values so the uncoerced values flow through
unchanged.

In `@packages/cli/src/config.ts`:
- Around line 41-48: The config write only sets modes on creation; ensure
existing paths are hardened by calling chmodSync(dir, 0o700) after ensuring the
directory exists (regardless of whether mkdirSync created it) and
chmodSync(path, 0o600) immediately after writeFileSync; update the function that
builds and writes the Config (references: dir, path, loadConfig, writeFileSync,
mkdirSync) to always apply these chmodSync calls so existing basilic/ and
config.json get tightened too.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 545d5773-97a7-4a12-8578-409af9420ae6

📥 Commits

Reviewing files that changed from the base of the PR and between 4a43447 and 6ffe34e.

📒 Files selected for processing (6)
  • apps/docu/content/docs/development/cli.mdx
  • biome.json
  • packages/cli/README.md
  • packages/cli/src/cli.ts
  • packages/cli/src/config.ts
  • packages/cli/src/run-command.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • biome.json
  • apps/docu/content/docs/development/cli.mdx

Comment on lines +12 to +24
const mutedStdout = new Writable({
write(chunk, _encoding, callback) {
if (!(mutedStdout as Writable & { muted?: boolean }).muted) process.stdout.write(chunk)
callback()
},
}) as Writable & { muted?: boolean }
mutedStdout.muted = true
const rl = createInterface({ input: process.stdin, output: mutedStdout, terminal: true })
return new Promise((resolve, reject) => {
rl.question('API key: ', answer => {
rl.close()
mutedStdout.muted = false
resolve(answer.trim())
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The API key prompt is currently invisible.

mutedStdout.muted is enabled before rl.question(...), so the prompt string is suppressed together with the typed characters. Users end up typing blind. Print the label before muting, or special-case the prompt text in the custom Writable.

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

In `@packages/cli/src/cli.ts` around lines 12 - 24, The prompt is being suppressed
because mutedStdout.muted is set before rl.question, so the prompt text doesn't
appear; fix by emitting the prompt to the real stdout before enabling mute or by
special-casing the prompt in the custom Writable: either call
process.stdout.write('API key: ') (or rl.output.write) before setting
mutedStdout.muted = true and then call rl.question with the muted output, or
change mutedStdout.write to detect the prompt string (from rl.question) and
forward it to process.stdout even when muted; update references in this block
(mutedStdout, createInterface, rl.question) accordingly and leave
mutedStdout.muted toggled back to false after rl.close().

}
}

program.parse()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/cli/src/cli.ts | head -200

Repository: blockmatic/basilic

Length of output: 7340


🏁 Script executed:

rg "\.action\(" packages/cli/src/cli.ts -A 5 -B 2

Repository: blockmatic/basilic

Length of output: 1091


🌐 Web query:

Commander.js parseAsync async action handlers promises

💡 Result:

In Commander.js, if any of your command .action(...) handlers (or lifecycle hooks like preAction/postAction) are async / return a Promise, you should use parseAsync and await it, so Commander waits for those Promises to settle before your CLI process continues/exits. [1]

import { Command } from 'commander';

const program = new Command();

program
  .command('run')
  .action(async () => {
    await doWork();          // can await inside
  });

await program.parseAsync(process.argv); // waits for async action/hook(s)

parseAsync(...) itself is async and returns a Promise that resolves to the command (this), so await program.parseAsync(...) is the intended usage. [2]

Key point: program.parse(...) does not await async actions; program.parseAsync(...) does. [1]

Sources: [1] Commander README (async action handlers + parseAsync guidance), [2] Commander Command.parseAsync JSDoc/implementation.


Use await program.parseAsync() instead of program.parse().

This file registers async .action(...) callbacks (lines 92, 141), but the entrypoint uses program.parse(). Commander only awaits promise-returning handlers when you use parseAsync(), so rejected async work can escape the intended CLI error flow and the process may exit before operations complete.

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

In `@packages/cli/src/cli.ts` at line 181, The CLI currently calls
program.parse(), which does not await promise-returning .action handlers (e.g.,
the async action callbacks registered on the Command instance), so switch the
entrypoint to await program.parseAsync() instead of program.parse() to ensure
async handlers are awaited and errors are handled correctly; locate the call to
program.parse() and replace it with an awaited call to program.parseAsync() (or
return the Promise from main) so Commander will wait for the async .action(...)
callbacks to complete.

@gaboesquivel gaboesquivel merged commit ebd17e8 into main Mar 12, 2026
10 checks passed
@gaboesquivel gaboesquivel deleted the cli branch March 12, 2026 01:42
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