From c4b0509023a7a1709d273cc8de60a2c0487e5c6e Mon Sep 17 00:00:00 2001 From: RevIQ Date: Fri, 9 Jan 2026 17:11:10 +0800 Subject: [PATCH 1/2] Implement shell completions for CLI (Workstream N-Completions) Add `reviq completions bash/zsh` command with dynamic shell completions: - Create bash-complete.ts entry point using stricli's proposeCompletions API - Add completions command with bash and zsh support (fish planned) - Extract app export to separate app.ts for shared imports - Add @stricli/auto-complete dependency and __reviq_bash_complete bin entry Also fix lint/type errors in api-server tests and helpers. Co-Authored-By: Claude Opus 4.5 --- .../src/__tests__/e2e/webauthn.test.ts | 4 +- .../api-server/src/procedures/orgs/helpers.ts | 2 +- apps/cli/package.json | 6 +- apps/cli/src/app.ts | 9 ++ apps/cli/src/bin/bash-complete.ts | 22 ++++ apps/cli/src/bin/reviq.ts | 11 +- apps/cli/src/routes/_command.ts | 2 + apps/cli/src/routes/completions.ts | 104 ++++++++++++++++++ bun.lock | 4 + 9 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 apps/cli/src/app.ts create mode 100644 apps/cli/src/bin/bash-complete.ts create mode 100644 apps/cli/src/routes/completions.ts diff --git a/apps/api-server/src/__tests__/e2e/webauthn.test.ts b/apps/api-server/src/__tests__/e2e/webauthn.test.ts index fce99dd..f3358e9 100644 --- a/apps/api-server/src/__tests__/e2e/webauthn.test.ts +++ b/apps/api-server/src/__tests__/e2e/webauthn.test.ts @@ -49,6 +49,8 @@ function createAPIContext(): APIContext { origin: TEST_RP.origin, allowedOrigins: [...TEST_RP.allowedOrigins], rpName: TEST_RP.rpName, + reqHeaders: new Headers(), + resHeaders: new Headers(), }; } @@ -69,7 +71,7 @@ function createAuthenticatedContext( isSuperuser: false, }, session: { - id: 1, + id: "1", trustedMode: false, createdAt: new Date(), }, diff --git a/apps/api-server/src/procedures/orgs/helpers.ts b/apps/api-server/src/procedures/orgs/helpers.ts index d791907..e3f71cd 100644 --- a/apps/api-server/src/procedures/orgs/helpers.ts +++ b/apps/api-server/src/procedures/orgs/helpers.ts @@ -120,5 +120,5 @@ export async function countOwners( .where("role", "=", "owner") .executeTakeFirstOrThrow(); - return Number(result.count); + return result.count; } diff --git a/apps/cli/package.json b/apps/cli/package.json index d51f86c..f9004f0 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -4,10 +4,11 @@ "private": true, "type": "module", "bin": { - "reviq": "./dist/index.js" + "reviq": "./dist/index.js", + "__reviq_bash_complete": "./dist/bash-complete.js" }, "scripts": { - "build": "bun build src/bin/reviq.ts --outdir dist --target bun", + "build": "bun build src/bin/reviq.ts --outfile dist/index.js --target bun && bun build src/bin/bash-complete.ts --outfile dist/bash-complete.js --target bun", "cli": "bun run src/bin/reviq.ts", "typecheck": "tsc --noEmit", "lint": "eslint . --cache", @@ -15,6 +16,7 @@ }, "dependencies": { "@stricli/core": "^1.2.5", + "@stricli/auto-complete": "^1.0.0", "@reviq/db": "workspace:*", "@noble/hashes": "^2.0.1" }, diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts new file mode 100644 index 0000000..747763f --- /dev/null +++ b/apps/cli/src/app.ts @@ -0,0 +1,9 @@ +import { buildApplication } from "@stricli/core"; +import { rootRouteMap } from "./routes/_command.js"; + +export const app = buildApplication(rootRouteMap, { + name: "reviq", + versionInfo: { + currentVersion: "0.0.0", + }, +}); diff --git a/apps/cli/src/bin/bash-complete.ts b/apps/cli/src/bin/bash-complete.ts new file mode 100644 index 0000000..8641c85 --- /dev/null +++ b/apps/cli/src/bin/bash-complete.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env bun + +import type { LocalContext } from "../context.js"; +import { proposeCompletions } from "@stricli/core"; +import { app } from "../app.js"; + +const inputs = process.argv.slice(3); +// eslint-disable-next-line turbo/no-undeclared-env-vars -- COMP_LINE is set by bash completion +if (process.env.COMP_LINE?.endsWith(" ")) { + inputs.push(""); +} + +const context: LocalContext = { process }; + +try { + const completions = await proposeCompletions(app, inputs, context); + for (const { completion } of completions) { + process.stdout.write(`${completion}\n`); + } +} catch { + // Silently ignore errors during completion +} diff --git a/apps/cli/src/bin/reviq.ts b/apps/cli/src/bin/reviq.ts index 1bb8608..3dd5108 100644 --- a/apps/cli/src/bin/reviq.ts +++ b/apps/cli/src/bin/reviq.ts @@ -1,15 +1,8 @@ #!/usr/bin/env bun import type { LocalContext } from "../context.js"; -import { buildApplication, run } from "@stricli/core"; -import { rootRouteMap } from "../routes/_command.js"; - -const app = buildApplication(rootRouteMap, { - name: "reviq", - versionInfo: { - currentVersion: "0.0.0", - }, -}); +import { run } from "@stricli/core"; +import { app } from "../app.js"; const context: LocalContext = { process, diff --git a/apps/cli/src/routes/_command.ts b/apps/cli/src/routes/_command.ts index 00204fc..35acde1 100644 --- a/apps/cli/src/routes/_command.ts +++ b/apps/cli/src/routes/_command.ts @@ -1,6 +1,7 @@ import { buildRouteMap } from "@stricli/core"; import { authRouteMap } from "./auth/_command.js"; import { bootstrapCommand } from "./bootstrap.js"; +import { completionsCommand } from "./completions.js"; import { orgRouteMap } from "./org/_command.js"; import { userRouteMap } from "./user/_command.js"; @@ -10,6 +11,7 @@ export const rootRouteMap = buildRouteMap({ auth: authRouteMap, user: userRouteMap, org: orgRouteMap, + completions: completionsCommand, }, docs: { brief: "RevIQ CLI for database and user management", diff --git a/apps/cli/src/routes/completions.ts b/apps/cli/src/routes/completions.ts new file mode 100644 index 0000000..06280f5 --- /dev/null +++ b/apps/cli/src/routes/completions.ts @@ -0,0 +1,104 @@ +import type { LocalContext } from "../context.js"; +import { buildCommand } from "@stricli/core"; + +type Shell = "bash" | "zsh" | "fish"; + +const SUPPORTED_SHELLS: readonly Shell[] = ["bash", "zsh", "fish"] as const; + +function parseShell(value: string): Shell { + const shell = value.toLowerCase(); + if (!SUPPORTED_SHELLS.includes(shell as Shell)) { + throw new Error( + `Invalid shell: ${value}. Supported shells: ${SUPPORTED_SHELLS.join(", ")}`, + ); + } + return shell as Shell; +} + +const ZSH_COMPLETION_SCRIPT = `#compdef reviq + +_reviq() { + local -a completions + local -a words_to_complete + + # Build array of words up to cursor + words_to_complete=("\${words[@]:1:$((CURRENT-1))}") + + # Add empty string if we're completing a new word + if [[ -z "\${words[CURRENT]}" ]] || [[ "\${BUFFER}" == *" " ]]; then + words_to_complete+=("") + fi + + # Call the completion helper + completions=(\${(f)"$(COMP_LINE="$BUFFER" __reviq_bash_complete reviq "\${words_to_complete[@]}" 2>/dev/null)"}) + + if [[ \${#completions[@]} -gt 0 ]]; then + _describe 'command' completions + fi +} + +_reviq "$@" +`; + +function completions( + this: LocalContext, + _flags: Record, + shell: Shell, +): void { + // biome-ignore lint/nursery/noUnnecessaryConditions: switch on union type is valid + switch (shell) { + case "bash": + console.log("To enable bash completions for reviq, run:\n"); + console.log( + " npx @stricli/auto-complete install reviq --bash __reviq_bash_complete\n", + ); + console.log( + "This will modify your ~/.bashrc to enable tab completion for reviq commands.", + ); + console.log("\nTo uninstall, run:\n"); + console.log(" npx @stricli/auto-complete uninstall reviq --bash"); + break; + + case "zsh": + console.log("# Add the following to your ~/.zshrc:\n"); + console.log(ZSH_COMPLETION_SCRIPT); + console.log("\n# Or save to a file and source it:"); + console.log( + "# reviq completions zsh > ~/.config/reviq/completions.zsh", + ); + console.log( + '# echo "source ~/.config/reviq/completions.zsh" >> ~/.zshrc', + ); + break; + + case "fish": + console.log(`Shell completions for ${shell} are not yet supported.`); + console.log( + "\nCurrently only bash and zsh are supported. fish support is planned.", + ); + this.process.exit(1); + break; + } +} + +export const completionsCommand = buildCommand({ + func: completions, + parameters: { + flags: {}, + positional: { + kind: "tuple", + parameters: [ + { + brief: "Shell to generate completions for (bash, zsh, fish)", + parse: parseShell, + placeholder: "shell", + }, + ], + }, + }, + docs: { + brief: "Generate shell completions", + fullDescription: + "Generate shell completion scripts for bash, zsh, or fish.\n\nCurrently bash and zsh are supported. fish support is planned.", + }, +}); diff --git a/bun.lock b/bun.lock index 2f3a879..173e447 100644 --- a/bun.lock +++ b/bun.lock @@ -44,10 +44,12 @@ "version": "0.0.0", "bin": { "reviq": "./dist/index.js", + "__reviq_bash_complete": "./dist/bash-complete.js", }, "dependencies": { "@noble/hashes": "^2.0.1", "@reviq/db": "workspace:*", + "@stricli/auto-complete": "^1.0.0", "@stricli/core": "^1.2.5", }, "devDependencies": { @@ -432,6 +434,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@stricli/auto-complete": ["@stricli/auto-complete@1.2.5", "", { "dependencies": { "@stricli/core": "^1.2.5" }, "bin": { "auto-complete": "dist/bin/cli.js" } }, "sha512-C6G88Hh4lUWBwiqsxbcA4I1ricSQwiLaOziTWW3NmBoX7WGTW7i7RvyooXMpZk1YMLf2olv5Odxmg127ik1DKQ=="], + "@stricli/core": ["@stricli/core@1.2.5", "", {}, "sha512-+afyztQW7fwWkqmU2WQZbdc3LjnZThWYdtE0l+hykZ1Rvy7YGxZSvsVCS/wZ/2BNv117pQ9TU1GZZRIcPnB4tw=="], "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], From df9b8808d054f17dc516038e4883cdf5aac290de Mon Sep 17 00:00:00 2001 From: RevIQ Date: Fri, 9 Jan 2026 17:11:56 +0800 Subject: [PATCH 2/2] Update docs/initial-app.md with Workstream N-Completions status Mark N17 (reviq completions bash/zsh/fish) as completed. Co-Authored-By: Claude Opus 4.5 --- docs/initial-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/initial-app.md b/docs/initial-app.md index 024837f..83d2dca 100644 --- a/docs/initial-app.md +++ b/docs/initial-app.md @@ -2397,7 +2397,7 @@ _Depends on: N3, K2, K4_ _Depends on: N1_ -- [ ] **N17**: Implement `reviq completions bash/zsh/fish` +- [x] **N17**: Implement `reviq completions bash/zsh/fish` ---