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 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,8 @@ function createAPIContext(): APIContext {
|
|||||||
origin: TEST_RP.origin,
|
origin: TEST_RP.origin,
|
||||||
allowedOrigins: [...TEST_RP.allowedOrigins],
|
allowedOrigins: [...TEST_RP.allowedOrigins],
|
||||||
rpName: TEST_RP.rpName,
|
rpName: TEST_RP.rpName,
|
||||||
|
reqHeaders: new Headers(),
|
||||||
|
resHeaders: new Headers(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ function createAuthenticatedContext(
|
|||||||
isSuperuser: false,
|
isSuperuser: false,
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
id: 1,
|
id: "1",
|
||||||
trustedMode: false,
|
trustedMode: false,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -120,5 +120,5 @@ export async function countOwners(
|
|||||||
.where("role", "=", "owner")
|
.where("role", "=", "owner")
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
return Number(result.count);
|
return result.count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"reviq": "./dist/index.js"
|
"reviq": "./dist/index.js",
|
||||||
|
"__reviq_bash_complete": "./dist/bash-complete.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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",
|
"cli": "bun run src/bin/reviq.ts",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint . --cache",
|
"lint": "eslint . --cache",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stricli/core": "^1.2.5",
|
"@stricli/core": "^1.2.5",
|
||||||
|
"@stricli/auto-complete": "^1.0.0",
|
||||||
"@reviq/db": "workspace:*",
|
"@reviq/db": "workspace:*",
|
||||||
"@noble/hashes": "^2.0.1"
|
"@noble/hashes": "^2.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
9
apps/cli/src/app.ts
Normal file
9
apps/cli/src/app.ts
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
22
apps/cli/src/bin/bash-complete.ts
Normal file
22
apps/cli/src/bin/bash-complete.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import type { LocalContext } from "../context.js";
|
import type { LocalContext } from "../context.js";
|
||||||
import { buildApplication, run } from "@stricli/core";
|
import { run } from "@stricli/core";
|
||||||
import { rootRouteMap } from "../routes/_command.js";
|
import { app } from "../app.js";
|
||||||
|
|
||||||
const app = buildApplication(rootRouteMap, {
|
|
||||||
name: "reviq",
|
|
||||||
versionInfo: {
|
|
||||||
currentVersion: "0.0.0",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const context: LocalContext = {
|
const context: LocalContext = {
|
||||||
process,
|
process,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { buildRouteMap } from "@stricli/core";
|
import { buildRouteMap } from "@stricli/core";
|
||||||
import { authRouteMap } from "./auth/_command.js";
|
import { authRouteMap } from "./auth/_command.js";
|
||||||
import { bootstrapCommand } from "./bootstrap.js";
|
import { bootstrapCommand } from "./bootstrap.js";
|
||||||
|
import { completionsCommand } from "./completions.js";
|
||||||
import { orgRouteMap } from "./org/_command.js";
|
import { orgRouteMap } from "./org/_command.js";
|
||||||
import { userRouteMap } from "./user/_command.js";
|
import { userRouteMap } from "./user/_command.js";
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export const rootRouteMap = buildRouteMap({
|
|||||||
auth: authRouteMap,
|
auth: authRouteMap,
|
||||||
user: userRouteMap,
|
user: userRouteMap,
|
||||||
org: orgRouteMap,
|
org: orgRouteMap,
|
||||||
|
completions: completionsCommand,
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
brief: "RevIQ CLI for database and user management",
|
brief: "RevIQ CLI for database and user management",
|
||||||
|
|||||||
104
apps/cli/src/routes/completions.ts
Normal file
104
apps/cli/src/routes/completions.ts
Normal file
@@ -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<string, never>,
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
});
|
||||||
4
bun.lock
4
bun.lock
@@ -44,10 +44,12 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"reviq": "./dist/index.js",
|
"reviq": "./dist/index.js",
|
||||||
|
"__reviq_bash_complete": "./dist/bash-complete.js",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@reviq/db": "workspace:*",
|
"@reviq/db": "workspace:*",
|
||||||
|
"@stricli/auto-complete": "^1.0.0",
|
||||||
"@stricli/core": "^1.2.5",
|
"@stricli/core": "^1.2.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -432,6 +434,8 @@
|
|||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@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=="],
|
"@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=="],
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user