requestshield 0.0.0-stage → 1.0.0-stg.276

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +292 -2
  2. package/config/.env.stg +8 -0
  3. package/package.json +25 -4
  4. package/skills/requestshield/SKILL.md +240 -0
  5. package/skills/requestshield/assets/AGENTS.codex.md +107 -0
  6. package/skills/requestshield/references/backend-java-core.md +152 -0
  7. package/skills/requestshield/references/backend-spring-boot.md +158 -0
  8. package/skills/requestshield/references/browser-manual.md +244 -0
  9. package/skills/requestshield/references/browser-seamless.md +213 -0
  10. package/skills/requestshield/references/cli.md +314 -0
  11. package/skills/requestshield/references/integration-planning.md +357 -0
  12. package/skills/requestshield/references/troubleshooting.md +131 -0
  13. package/src/agent-detector.mjs +102 -0
  14. package/src/api-client.mjs +155 -0
  15. package/src/args.mjs +147 -0
  16. package/src/browser-opener.mjs +32 -0
  17. package/src/cli.mjs +115 -0
  18. package/src/command-completion.mjs +10 -0
  19. package/src/command-registry.mjs +151 -0
  20. package/src/commands/agent-setup.mjs +195 -0
  21. package/src/commands/agent-status.mjs +65 -0
  22. package/src/commands/application-mutations.mjs +37 -0
  23. package/src/commands/application-response.mjs +55 -0
  24. package/src/commands/apps-get.mjs +28 -0
  25. package/src/commands/apps-list.mjs +93 -0
  26. package/src/commands/auth-status.mjs +42 -0
  27. package/src/commands/billing.mjs +106 -0
  28. package/src/commands/contract.mjs +59 -0
  29. package/src/commands/keys-create.mjs +22 -0
  30. package/src/commands/mutation-support.mjs +135 -0
  31. package/src/commands/output.mjs +26 -0
  32. package/src/commands/secret-commands.mjs +66 -0
  33. package/src/commands/secret-output.mjs +52 -0
  34. package/src/commands/service-status.mjs +34 -0
  35. package/src/commands/signin.mjs +95 -0
  36. package/src/commands/signout.mjs +14 -0
  37. package/src/commands/update-check.mjs +205 -0
  38. package/src/commands/usage.mjs +72 -0
  39. package/src/config.mjs +200 -0
  40. package/src/entrypoint.mjs +44 -0
  41. package/src/errors.mjs +16 -0
  42. package/src/integration-contract-client.mjs +85 -0
  43. package/src/integration-contract.mjs +114 -0
  44. package/src/main.mjs +6 -0
  45. package/src/oauth-client.mjs +153 -0
  46. package/src/oauth-loopback.mjs +120 -0
  47. package/src/package-profile.mjs +24 -0
  48. package/src/private-path.mjs +33 -0
  49. package/src/secret-output.mjs +121 -0
  50. package/src/service-status-client.mjs +87 -0
  51. package/src/session-files.mjs +192 -0
  52. package/src/session-store.mjs +204 -0
  53. package/src/terminal.mjs +143 -0
  54. package/src/usage-time.mjs +26 -0
@@ -0,0 +1,155 @@
1
+ // @ts-check
2
+ import { CliError } from "./errors.mjs";
3
+ import { validateBearerToken, validatedUrl } from "./config.mjs";
4
+
5
+ const APPLICATIONS = "/v1/applications";
6
+ const MAX_BODY_BYTES = 1024 * 1024;
7
+ const API_ERRORS = new Set(["invalid_request", "invalid_token", "insufficient_scope", "application_not_found",
8
+ "idempotency_conflict", "secret_revoked", "email_not_verified", "identity_not_found", "dependency_unavailable", "rate_limited"]);
9
+
10
+ export class ManagementApiClient {
11
+ /** @param {{baseUrl: string, fetchImpl?: typeof fetch, timeoutMs?: number}} options */
12
+ constructor({baseUrl, fetchImpl = fetch, timeoutMs = 15_000}) {
13
+ this.baseUrl = validatedUrl(baseUrl, "API_URL");
14
+ this.fetchImpl = fetchImpl;
15
+ this.timeoutMs = timeoutMs;
16
+ }
17
+
18
+ /** @param {string} token @param {{limit?: number, cursor?: string}} [options] */
19
+ listApps(token, options = {}) {
20
+ const query = new URLSearchParams();
21
+ if (options.limit !== undefined) query.set("limit", String(options.limit));
22
+ if (options.cursor !== undefined) query.set("cursor", options.cursor);
23
+ return this.#request(`${APPLICATIONS}${query.size ? `?${query}` : ""}`, token, "GET");
24
+ }
25
+
26
+ /** @param {string} token @param {string} appKey */
27
+ getApp(token, appKey) { return this.#request(appPath(appKey), token, "GET"); }
28
+
29
+ /** @param {string} token @param {string} appKey */
30
+ getBilling(token, appKey) { return this.#request(`${appPath(appKey)}/billing`, token, "GET"); }
31
+
32
+ /** @param {string} token @param {string} appKey @param {{from?: string, to?: string}} [options] */
33
+ getUsage(token, appKey, options = {}) {
34
+ const query = new URLSearchParams();
35
+ if (options.from !== undefined) query.set("from", options.from);
36
+ if (options.to !== undefined) query.set("to", options.to);
37
+ return this.#request(`${appPath(appKey)}/usage${query.size ? `?${query}` : ""}`, token, "GET");
38
+ }
39
+
40
+ /** @param {string} token @param {{name: string, idempotencyKey: string}} options */
41
+ createApp(token, {name, idempotencyKey}) {
42
+ return this.#request(APPLICATIONS, token, "POST", {name}, idempotencyKey);
43
+ }
44
+
45
+ /** @param {string} token @param {string} appKey @param {{name: string, idempotencyKey: string}} options */
46
+ renameApp(token, appKey, {name, idempotencyKey}) {
47
+ return this.#request(appPath(appKey), token, "PATCH", {name}, idempotencyKey);
48
+ }
49
+
50
+ /** @param {string} token @param {string} appKey @param {{enabled: boolean, idempotencyKey: string}} options */
51
+ setAppEnabled(token, appKey, {enabled, idempotencyKey}) {
52
+ return this.#request(`${appPath(appKey)}/${enabled ? "enable" : "disable"}`, token, "POST", undefined, idempotencyKey);
53
+ }
54
+
55
+ /** @param {string} token @param {string} appKey @param {{idempotencyKey: string}} options */
56
+ rotateSecret(token, appKey, {idempotencyKey}) {
57
+ return this.#request(`${appPath(appKey)}/secret/rotate`, token, "POST", undefined, idempotencyKey);
58
+ }
59
+
60
+ /** @param {string} token @param {string} appKey */
61
+ revealSecret(token, appKey) { return this.#request(`${appPath(appKey)}/secret/reveal`, token, "POST"); }
62
+
63
+ /** @param {string} token @param {string} appKey @param {{idempotencyKey: string}} options */
64
+ revokeSecret(token, appKey, {idempotencyKey}) {
65
+ return this.#request(`${appPath(appKey)}/secret/revoke`, token, "POST", undefined, idempotencyKey);
66
+ }
67
+
68
+ /** No retries: a lost mutation response may represent a committed operation.
69
+ * @param {string} path @param {string} token @param {string} method
70
+ * @param {{name: string}} [payload] @param {string} [idempotencyKey]
71
+ */
72
+ async #request(path, token, method, payload, idempotencyKey) {
73
+ const headers = {Accept: "application/json", Authorization: `Bearer ${validateBearerToken(token)}`};
74
+ if (idempotencyKey !== undefined) {
75
+ if (!/^[A-Za-z0-9._~-]{1,128}$/.test(idempotencyKey)) {
76
+ throw new CliError("Invalid idempotency key", {code: "INVALID_ARGUMENT", exitCode: 2});
77
+ }
78
+ Object.assign(headers, {"Idempotency-Key": idempotencyKey});
79
+ }
80
+ if (payload !== undefined) Object.assign(headers, {"Content-Type": "application/json"});
81
+ const controller = new AbortController();
82
+ /** @type {ReadableStreamDefaultReader<Uint8Array> | undefined} */
83
+ let reader;
84
+ let expired = false;
85
+ const deadline = new Promise((_, reject) => {
86
+ controller.signal.addEventListener("abort", () => reject(new CliError(
87
+ "The RequestShield API request timed out. Check connectivity and try again.",
88
+ {code: "REQUEST_TIMEOUT", exitCode: 7},
89
+ )), {once: true});
90
+ });
91
+ const timer = setTimeout(() => { expired = true; controller.abort(); }, this.timeoutMs);
92
+ try {
93
+ const operation = (async () => {
94
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
95
+ method, headers, ...(payload !== undefined ? {body: JSON.stringify(payload)} : {}),
96
+ signal: controller.signal, redirect: "error", credentials: "omit", cache: "no-store",
97
+ });
98
+ if (expired) { void response.body?.cancel().catch(() => {}); throw new Error(); }
99
+ if (Number(response.headers.get("content-length")) > MAX_BODY_BYTES) {
100
+ void response.body?.cancel().catch(() => {}); throw invalidResponse();
101
+ }
102
+ const chunks = [];
103
+ let size = 0;
104
+ if (response.body) {
105
+ reader = response.body.getReader();
106
+ while (true) {
107
+ const {done, value} = await reader.read();
108
+ if (expired) throw new Error();
109
+ if (done) break;
110
+ size += value.length;
111
+ if (size > MAX_BODY_BYTES) throw invalidResponse();
112
+ chunks.push(value);
113
+ }
114
+ }
115
+ let body;
116
+ try { body = JSON.parse(new TextDecoder("utf-8", {fatal: true}).decode(Buffer.concat(chunks))); }
117
+ catch { if (response.ok) throw invalidResponse(); }
118
+ if (!response.ok) {
119
+ const providerCode = objectString(body?.error, "code");
120
+ const safeCode = providerCode && API_ERRORS.has(providerCode) ? providerCode : undefined;
121
+ const guidance = response.status === 401 ? "; run `requestshield login` again"
122
+ : safeCode === "insufficient_scope" ? "; this session lacks the required permissions"
123
+ : safeCode === "email_not_verified" ? "; verify your email, then retry app creation"
124
+ : safeCode === "identity_not_found" ? "; run `requestshield login` again with a valid account" : "";
125
+ throw new CliError(`RequestShield API error: HTTP ${response.status}${safeCode ? ` (${safeCode})` : ""}${guidance}`, {
126
+ code: response.status === 401 ? "UNAUTHENTICATED" : "API_ERROR",
127
+ exitCode: response.status === 401 || response.status === 403 ? 3 : 1,
128
+ httpStatus: response.status, apiErrorCode: safeCode,
129
+ });
130
+ }
131
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw invalidResponse();
132
+ return {ok: true, status: response.status, body, replayed: response.headers.get("Idempotency-Replayed") === "true"};
133
+ })();
134
+ return await Promise.race([operation, /** @type {Promise<never>} */ (deadline)]);
135
+ } catch (error) {
136
+ if (error instanceof CliError) throw error;
137
+ throw new CliError("Could not reach the RequestShield API", {code: "NETWORK_ERROR", exitCode: 7});
138
+ } finally {
139
+ clearTimeout(timer);
140
+ if (reader) { void reader.cancel().catch(() => {}); }
141
+ controller.abort();
142
+ }
143
+ }
144
+ }
145
+
146
+ /** @param {string} appKey */
147
+ function appPath(appKey) { return `${APPLICATIONS}/${encodeURIComponent(appKey)}`; }
148
+ function invalidResponse() { return new CliError("The RequestShield API returned an invalid or oversized response", {code: "INVALID_RESPONSE"}); }
149
+
150
+ /** @param {unknown} value @param {string} property */
151
+ export function objectString(value, property) {
152
+ if (!value || typeof value !== "object") return undefined;
153
+ const found = Reflect.get(value, property);
154
+ return typeof found === "string" ? found : undefined;
155
+ }
package/src/args.mjs ADDED
@@ -0,0 +1,147 @@
1
+ // @ts-check
2
+ import { CliError } from "./errors.mjs";
3
+ import { COMMAND_REGISTRY, commandUsage } from "./command-registry.mjs";
4
+ import { validateUsageRange } from "./usage-time.mjs";
5
+ import { validateOutputPath } from "./secret-output.mjs";
6
+
7
+ /**
8
+ * @typedef {{command:"help",scope?:import('./command-registry.mjs').CommandDefinition|string} | {command:"version"}
9
+ * | {command:"update-check"|"logout"|"auth-status"|"contract"|"service-status",json:boolean}
10
+ * | {command:"update-apply",yes:boolean,json:boolean}
11
+ * | {command:"login",noOpen:boolean,json:boolean}
12
+ * | {command:"app-create",appName:string,yes:boolean,json:boolean,idempotencyKey?:string,output?:string}
13
+ * | {command:"secret-rotate"|"secret-reveal",appKey:string,yes:boolean,json:boolean,idempotencyKey?:string,output?:string}
14
+ * | {command:"secret-revoke",appKey:string,yes:boolean,json:boolean,idempotencyKey?:string}
15
+ * | {command:"app-list",json:boolean,all:boolean,limit?:number,cursor?:string}
16
+ * | {command:"app-get"|"billing-get",appKey:string,json:boolean}
17
+ * | {command:"usage",appKey:string,json:boolean,from?:string,to?:string}
18
+ * | {command:"app-rename",appKey:string,name:string,yes:boolean,json:boolean,idempotencyKey?:string}
19
+ * | {command:"app-enable"|"app-disable",appKey:string,enabled:boolean,yes:boolean,json:boolean,idempotencyKey?:string}
20
+ * | {command:"agent-setup",agent?:"codex"|"claude",force:boolean,json:boolean}
21
+ * | {command:"agent-status",agent?:"codex"|"claude",json:boolean}} ParsedArgs
22
+ */
23
+
24
+ /** @param {string[]} argv @returns {ParsedArgs} */
25
+ export function parseArgs(argv) {
26
+ if (!argv.length) return {command:"help"};
27
+ const definition = COMMAND_REGISTRY.find((command) => command.path.every((part,index) => argv[index] === part)
28
+ || command.shortcuts?.includes(argv[0]) && command.path.length === 1);
29
+ if (!definition) {
30
+ const group = argv[0];
31
+ const isGroup = COMMAND_REGISTRY.some((command) => command.path.length > 1 && command.path[0] === group);
32
+ if (isGroup && (argv.length === 1 || argv.length === 2 && ["--help","-h"].includes(argv[1]))) {
33
+ return {command:"help",scope:group};
34
+ }
35
+ throw invalid("Unknown command. Use requestshield --help to list commands.");
36
+ }
37
+ const args = argv.slice(definition.path.length);
38
+ const endOfOptions = args.indexOf("--");
39
+ if (args.slice(0,endOfOptions === -1 ? args.length : endOfOptions).some((value) => ["--help","-h"].includes(value))) {
40
+ return {command:"help",scope:definition};
41
+ }
42
+ const {flags,positionals} = parseOptions(args,definition.flags);
43
+ const requiredCount = definition.arguments.filter((argument) => argument.required).length;
44
+ if (positionals.length < requiredCount || positionals.length > definition.arguments.length) {
45
+ throw invalid(`Usage: requestshield ${commandUsage(definition)}`);
46
+ }
47
+ /** @type {Record<string,string>} */
48
+ const values = {};
49
+ for (const [index,argument] of definition.arguments.entries()) {
50
+ if (positionals[index] !== undefined) values[argument.field] = argument.kind === "app-name" ? applicationName(positionals[index]) : validateAppKey(positionals[index]);
51
+ }
52
+ const json = flags.has("--json");
53
+ switch (definition.id) {
54
+ case "help": return {command:"help"};
55
+ case "version": return {command:"version"};
56
+ case "login": return {command:"login",noOpen:flags.has("--no-open"),json};
57
+ case "logout": case "auth-status": case "update-check": case "contract": case "service-status": return {command:definition.id,json};
58
+ case "update-apply": return {command:"update-apply",yes:flags.has("--yes"),json};
59
+ case "app-create": return {command:"app-create",appName:values.appName,json,...mutationFlags(flags),...outputFlag(flags)};
60
+ case "app-get": case "billing-get": return {command:definition.id,appKey:values.appKey,json};
61
+ case "usage": return {command:"usage",appKey:values.appKey,json,...validateUsageRange({
62
+ from:/** @type {string | undefined} */ (flags.get("--from")),
63
+ to:/** @type {string | undefined} */ (flags.get("--to")),
64
+ })};
65
+ case "app-rename": return {command:"app-rename",appKey:values.appKey,name:values.name,json,...mutationFlags(flags)};
66
+ case "app-enable": case "app-disable": return {command:definition.id,appKey:values.appKey,enabled:definition.id === "app-enable",json,...mutationFlags(flags)};
67
+ case "secret-rotate": case "secret-reveal": return {command:definition.id,appKey:values.appKey,json,...mutationFlags(flags),...outputFlag(flags)};
68
+ case "secret-revoke": return {command:definition.id,appKey:values.appKey,json,...mutationFlags(flags)};
69
+ case "app-list": {
70
+ const limit = flags.get("--limit");
71
+ if (limit !== undefined && (typeof limit !== "string" || !/^[1-9][0-9]{0,2}$/.test(limit) || Number(limit) > 100)) throw invalid("--limit must be an integer from 1 to 100");
72
+ const cursor = flags.get("--cursor");
73
+ if (cursor !== undefined && (typeof cursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(cursor))) throw invalid("Invalid --cursor");
74
+ if (cursor !== undefined && flags.has("--all")) throw invalid("--cursor and --all cannot be combined");
75
+ return {command:"app-list",json,all:flags.has("--all"),...(limit !== undefined ? {limit:Number(limit)} : {}),...(typeof cursor === "string" ? {cursor} : {})};
76
+ }
77
+ case "agent-setup": return {command:"agent-setup",force:flags.has("--force"),json,...agentOption(flags)};
78
+ case "agent-status": return {command:"agent-status",json,...agentOption(flags)};
79
+ default: throw new Error("Implemented command has no parser result mapping");
80
+ }
81
+ }
82
+
83
+ /** @param {string[]} args @param {import('./command-registry.mjs').FlagDefinition[]} options */
84
+ function parseOptions(args,options) {
85
+ /** @type {Map<string,string|true>} */
86
+ const flags = new Map();
87
+ /** @type {string[]} */
88
+ const positionals = [];
89
+ let positionalOnly = false;
90
+ for (let i = 0; i < args.length; i++) {
91
+ const arg = args[i];
92
+ if (positionalOnly) { positionals.push(arg); continue; }
93
+ if (arg === "--") { positionalOnly = true; continue; }
94
+ if (!arg.startsWith("-")) { positionals.push(arg); continue; }
95
+ const equals = arg.indexOf("=");
96
+ const name = equals === -1 ? arg : arg.slice(0,equals);
97
+ const option = options.find((item) => item.name === name);
98
+ if (!option) throw invalid(`Unknown option: ${name}`);
99
+ if (flags.has(name)) throw invalid(`${name} may only be provided once`);
100
+ if (option.kind === "boolean") {
101
+ if (equals !== -1) throw invalid(`${name} does not accept a value`);
102
+ flags.set(name,true);
103
+ } else {
104
+ const value = equals === -1 ? args[++i] : arg.slice(equals + 1);
105
+ if (!value || equals === -1 && value.startsWith("-")) throw invalid(`${name} requires a value`);
106
+ if (option.choices && !option.choices.includes(value)) throw invalid(`${name} must be one of: ${option.choices.join(", ")}`);
107
+ flags.set(name,value);
108
+ }
109
+ }
110
+ return {flags,positionals};
111
+ }
112
+
113
+ /** @param {Map<string,string|true>} flags */
114
+ function agentOption(flags) {
115
+ const selections = ["--codex","--claude"];
116
+ if (selections.filter((name) => flags.has(name)).length > 1) throw invalid(`Only one of ${selections.join(", ")} may be provided`);
117
+ return flags.has("--codex") ? {agent:/** @type {const} */ ("codex")} : flags.has("--claude") ? {agent:/** @type {const} */ ("claude")} : {};
118
+ }
119
+
120
+ /** @param {Map<string,string|true>} flags */
121
+ function mutationFlags(flags) {
122
+ const key = flags.get("--idempotency-key");
123
+ if (key !== undefined && (typeof key !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/.test(key))) throw invalid("Invalid --idempotency-key");
124
+ return {yes:flags.has("--yes"),...(typeof key === "string" ? {idempotencyKey:key} : {})};
125
+ }
126
+
127
+ /** @param {Map<string,string|true>} flags */
128
+ function outputFlag(flags) {
129
+ const output = flags.get("--output");
130
+ return typeof output === "string" ? {output: validateOutputPath(output)} : {};
131
+ }
132
+
133
+ /** @param {string} value */
134
+ export function validateAppKey(value) {
135
+ if (!/^[A-Za-z0-9._~-]{1,128}$/.test(value)) throw invalid("App Key must be 1-128 letters, numbers, '.', '_', '~' or '-'");
136
+ return value;
137
+ }
138
+
139
+ /** @param {string} value */
140
+ export function applicationName(value) {
141
+ const name = value.trim();
142
+ if (!name || [...name].length > 100 || /[\p{Cc}\p{Cs}]/u.test(name)) throw invalid("Application name must contain 1-100 characters without control characters");
143
+ return name;
144
+ }
145
+
146
+ /** @param {string} message */
147
+ function invalid(message) { return new CliError(message,{code:"INVALID_ARGUMENT",exitCode:2}); }
@@ -0,0 +1,32 @@
1
+ // @ts-check
2
+ import { spawn } from "node:child_process";
3
+ import { CliError } from "./errors.mjs";
4
+
5
+ /** Uses argument arrays and a fixed Windows script; never interpolate URL into shell code.
6
+ * @param {string} url
7
+ * @param {{signal?: AbortSignal, platform?: NodeJS.Platform, spawnImpl?: typeof spawn}} [options]
8
+ */
9
+ export async function openBrowser(url, {signal, platform = process.platform, spawnImpl = spawn} = {}) {
10
+ const failure = () => new CliError("Could not open the browser automatically", {code: "BROWSER_OPEN_FAILED"});
11
+ const command = platform === "win32" ? "powershell.exe" : platform === "darwin" ? "open" : "xdg-open";
12
+ const args = platform === "win32"
13
+ ? ["-NoProfile", "-NonInteractive", "-Command", "Start-Process -FilePath $env:REQUESTSHIELD_SIGNIN_BROWSER_URL"]
14
+ : [url];
15
+ await new Promise((resolve, reject) => {
16
+ if (signal?.aborted) { reject(failure()); return; }
17
+ let child;
18
+ try {
19
+ child = spawnImpl(command, args, {
20
+ windowsHide: true, stdio: "ignore", shell: false,
21
+ env: platform === "win32" ? {...process.env, REQUESTSHIELD_SIGNIN_BROWSER_URL: url} : process.env,
22
+ });
23
+ } catch { reject(failure()); return; }
24
+ const abort = () => { cleanup(); child.kill(); reject(failure()); };
25
+ const timer = setTimeout(abort, 10_000);
26
+ const cleanup = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); };
27
+ child.once("error", () => { cleanup(); reject(failure()); });
28
+ child.once("exit", code => { cleanup(); code === 0 ? resolve(undefined) : reject(failure()); });
29
+ signal?.addEventListener("abort", abort, {once: true});
30
+ if (signal?.aborted) abort();
31
+ });
32
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ import { parseArgs } from "./args.mjs";
4
+ import { getCommandDefinition, renderHelp } from "./command-registry.mjs";
5
+ import { rendererFor, resolveTerminal } from "./terminal.mjs";
6
+ import { ManagementApiClient } from "./api-client.mjs";
7
+ import { SessionStore } from "./session-store.mjs";
8
+ import { OAuthClient } from "./oauth-client.mjs";
9
+ import { signin } from "./commands/signin.mjs";
10
+ import { createKeys } from "./commands/keys-create.mjs";
11
+ import { renameApp, setAppEnabled } from "./commands/application-mutations.mjs";
12
+ import { rotateSecret, revealSecret, revokeSecret } from "./commands/secret-commands.mjs";
13
+ import { showAuthStatus } from "./commands/auth-status.mjs";
14
+ import { signout } from "./commands/signout.mjs";
15
+ import { listApps } from "./commands/apps-list.mjs";
16
+ import { getApp } from "./commands/apps-get.mjs";
17
+ import { showUsage } from "./commands/usage.mjs";
18
+ import { showBilling } from "./commands/billing.mjs";
19
+ import { setupAgent } from "./commands/agent-setup.mjs";
20
+ import { showAgentStatus } from "./commands/agent-status.mjs";
21
+ import { checkForUpdate, applyUpdate } from "./commands/update-check.mjs";
22
+ import { showContract } from "./commands/contract.mjs";
23
+ import { showServiceStatus } from "./commands/service-status.mjs";
24
+ import packageJson from "../package.json" with { type: "json" };
25
+ import { getPackageProfile } from "./package-profile.mjs";
26
+ import { getApiUrl, getAuthorizationIssuer, getCommandInvocation, getCommandName, getOAuthConfig } from "./config.mjs";
27
+
28
+ /** @typedef {{
29
+ * log: (message: string) => void, warn?: (message: string) => void,
30
+ * terminal?: import('./terminal.mjs').TerminalContext,
31
+ * api?: ManagementApiClient, sessions?: SessionStore, oauth?: OAuthClient,
32
+ * authorizationIssuer?: string, openBrowser?: typeof import('./browser-opener.mjs').openBrowser,
33
+ * signal?: AbortSignal, env?: NodeJS.ProcessEnv, profile?: import('./config.mjs').Profile,
34
+ * homeDir?: string, sourceDir?: string, executablePath?: string,
35
+ * detectAgents?: () => Promise<Array<'codex' | 'claude'>>,
36
+ * selectAgent?: (detected: Array<'codex' | 'claude'>) => Promise<'codex' | 'claude'>,
37
+ * confirm?: () => Promise<boolean>, fetchImpl?: typeof fetch,
38
+ * secretOutput?: import('./secret-output.mjs').SecretOutputOptions,
39
+ * confirmUpdate?: () => Promise<boolean>, installLatest?: (packageName: string, version: string) => Promise<void>
40
+ * }} CommandContext */
41
+
42
+ /** Runtime bindings only. Syntax, help and dependency policy live in the registry.
43
+ * @type {Record<string, (parsed: any, context: CommandContext) => unknown | Promise<unknown>>} */
44
+ export const COMMAND_HANDLERS = {
45
+ help: (parsed, context) => context.log(renderHelp(parsed.scope, {
46
+ invocation: getCommandInvocation(context.profile), version: packageJson.version, renderer: rendererFor(context),
47
+ })),
48
+ version: (_parsed, context) => context.log(`${getCommandName(context.profile)} ${packageJson.version}`),
49
+ login: login,
50
+ logout: (parsed, context) => signout(parsed, {...context, sessions: localSessions(context)}),
51
+ "auth-status": (parsed, context) => showAuthStatus(parsed, {...context, sessions: localSessions(context)}),
52
+ contract: (parsed, context) => showContract(parsed, context),
53
+ "service-status": (parsed, context) => showServiceStatus(parsed, context),
54
+ "app-create": (parsed, context) => createKeys(parsed, requireApiSession(context)),
55
+ "app-list": (parsed, context) => listApps(parsed, requireApiSession(context)),
56
+ "app-get": (parsed, context) => getApp(parsed, requireApiSession(context)),
57
+ usage: (parsed, context) => showUsage(parsed, requireApiSession(context)),
58
+ "billing-get": (parsed, context) => showBilling(parsed, requireApiSession(context)),
59
+ "app-rename": (parsed, context) => renameApp(parsed, requireApiSession(context)),
60
+ "app-enable": (parsed, context) => setAppEnabled(parsed, requireApiSession(context)),
61
+ "app-disable": (parsed, context) => setAppEnabled(parsed, requireApiSession(context)),
62
+ "secret-rotate": (parsed, context) => rotateSecret(parsed, requireApiSession(context)),
63
+ "secret-reveal": (parsed, context) => revealSecret(parsed, requireApiSession(context)),
64
+ "secret-revoke": (parsed, context) => revokeSecret(parsed, requireApiSession(context)),
65
+ "agent-setup": (parsed, context) => setupAgent(parsed, context),
66
+ "agent-status": (parsed, context) => showAgentStatus(parsed, context),
67
+ "update-check": (parsed, context) => checkForUpdate(updateOptions(parsed, context)),
68
+ "update-apply": (parsed, context) => applyUpdate(updateOptions(parsed, context)),
69
+ };
70
+
71
+ /** @param {{json?: boolean, yes?: boolean}} parsed @param {CommandContext} context */
72
+ function updateOptions(parsed, context) {
73
+ return {...parsed, profile: context.profile, currentVersion: packageJson.version, packageName: packageJson.name,
74
+ log: context.log, warn: context.warn, terminal: context.terminal, fetchImpl: context.fetchImpl, confirm: context.confirmUpdate, install: context.installLatest};
75
+ }
76
+
77
+ /** @param {{noOpen?: boolean, json?: boolean}} parsed @param {CommandContext} context */
78
+ function login(parsed, context) {
79
+ const env = context.env ?? process.env;
80
+ const oauth = context.oauth ?? new OAuthClient({config: getOAuthConfig(context.profile), fetchImpl: context.fetchImpl});
81
+ const authorizationIssuer = context.authorizationIssuer ?? (context.oauth
82
+ ? oauth.config.issuer : getAuthorizationIssuer(context.profile, oauth.config.issuer));
83
+ const sessions = context.sessions ?? new SessionStore({env, homeDir: context.homeDir, oauth, config: oauth.config, profile: context.profile});
84
+ return signin(parsed, {...context, oauth, sessions, authorizationIssuer});
85
+ }
86
+
87
+ /** @param {CommandContext} context */
88
+ function localSessions(context) {
89
+ return context.sessions ?? new SessionStore({env: context.env, homeDir: context.homeDir, profile: context.profile});
90
+ }
91
+
92
+ /** @param {CommandContext} context */
93
+ function requireApiSession(context) {
94
+ if (!context.api || !context.sessions) throw new Error("Authenticated command context was not initialized");
95
+ return {...context, api: context.api, sessions: context.sessions};
96
+ }
97
+
98
+ /** @param {string[]} argv @param {Partial<CommandContext>} [deps] */
99
+ export async function run(argv, deps = {}) {
100
+ // Parse and reject unknown commands before reading configuration or sessions.
101
+ const parsed = parseArgs(argv);
102
+ const profile = deps.profile ?? getPackageProfile();
103
+ getCommandName(profile);
104
+ const definition = getCommandDefinition(parsed.command);
105
+ const handler = definition?.handler ? COMMAND_HANDLERS[definition.handler] : undefined;
106
+ if (!handler) throw new Error("Unsupported command handler");
107
+ const context = {...deps, profile, terminal: resolveTerminal(deps), log: deps.log ?? console.log, warn: deps.warn ?? console.error};
108
+ if (definition?.requiresApiSession) {
109
+ return handler(parsed, {...context,
110
+ api: deps.api ?? new ManagementApiClient({baseUrl: getApiUrl(profile), fetchImpl: deps.fetchImpl}),
111
+ sessions: localSessions(context),
112
+ });
113
+ }
114
+ return handler(parsed, context);
115
+ }
@@ -0,0 +1,10 @@
1
+ // @ts-check
2
+
3
+ /** A command that has already printed its result can set its process outcome. */
4
+ export class CommandCompletion {
5
+ /** @param {0 | 1} exitCode */
6
+ constructor(exitCode) {
7
+ this.exitCode = exitCode;
8
+ Object.freeze(this);
9
+ }
10
+ }
@@ -0,0 +1,151 @@
1
+ // @ts-check
2
+ import { createRenderer, displayWidth } from "./terminal.mjs";
3
+ import packageJson from "../package.json" with { type: "json" };
4
+
5
+ /**
6
+ * @typedef {"help"|"version"|"login"|"logout"|"auth-status"|"app-create"|"app-list"|"app-get"|"app-rename"|"app-enable"|"app-disable"|"secret-rotate"|"secret-reveal"|"secret-revoke"|"agent-setup"|"agent-status"|"update-check"|"update-apply"|"contract"|"service-status"|"usage"|"billing-get"} CommandId
7
+ * @typedef {{name:string,field:string,required:boolean,kind:"app-name"|"app-key",description:string}} ArgumentDefinition
8
+ * @typedef {{name:string,kind:"boolean"|"value",description:string,value?:string,choices?:string[]}} FlagDefinition
9
+ * @typedef {"account"|"applications"|"developer"} HelpCategory
10
+ * @typedef {{id:CommandId,path:string[],handler:CommandId,requiresApiSession:boolean,availability:"implemented",arguments:ArgumentDefinition[],flags:FlagDefinition[],summary:string,examples:string[],notes?:string[],shortcuts?:string[],order:number,category?:HelpCategory,icon?:string,navigationSummary?:string}} CommandDefinition
11
+ * @typedef {{name:string,summary:string,category:HelpCategory,icon?:string,order:number}} GroupDefinition
12
+ * @typedef {{invocation?:string,version?:string,renderer?:ReturnType<typeof createRenderer>}} HelpOptions
13
+ */
14
+
15
+ /** @type {{id:HelpCategory,label:string,order:number}[]} */
16
+ export const HELP_CATEGORIES = [
17
+ {id:"account",label:"ACCOUNT",order:10},
18
+ {id:"applications",label:"APPLICATIONS & SECRETS",order:20},
19
+ {id:"developer",label:"DEVELOPER TOOLS",order:30},
20
+ ];
21
+
22
+ /** Navigation metadata for the immediate command groups. @type {GroupDefinition[]} */
23
+ export const COMMAND_GROUPS = [
24
+ {name:"auth",summary:"Inspect local authentication",category:"account",icon:"👤",order:30},
25
+ {name:"app",summary:"Manage applications",category:"applications",icon:"📦",order:10},
26
+ {name:"secret",summary:"Manage application secrets",category:"applications",icon:"🔐",order:20},
27
+ {name:"agent",summary:"Set up coding-agent Skills",category:"developer",icon:"🤖",order:20},
28
+ {name:"update",summary:"Check for and install CLI updates",category:"developer",icon:"📥",order:30},
29
+ {name:"service",summary:"Check service availability",category:"developer",order:40},
30
+ {name:"billing",summary:"View account usage and estimated cost",category:"account",order:40},
31
+ ];
32
+
33
+ /** @type {FlagDefinition} */
34
+ const json = {name:"--json",kind:"boolean",description:"Write one JSON result to stdout."};
35
+ /** @type {FlagDefinition} */
36
+ const yes = {name:"--yes",kind:"boolean",description:"Confirm this operation without prompting."};
37
+ /** @type {FlagDefinition} */
38
+ const idempotency = {name:"--idempotency-key",kind:"value",value:"key",description:"Reuse the same key when retrying the identical request within seven days."};
39
+ /** @type {FlagDefinition} */
40
+ const output = {name:"--output",kind:"value",value:"file",description:"Save App Key and API Secret in a new private ENV file instead of printing the secret."};
41
+ /** @type {FlagDefinition} */
42
+ const noOpen = {name:"--no-open",kind:"boolean",description:"Print the login URL without opening a browser."};
43
+ /** @type {FlagDefinition[]} */
44
+ const agents = [
45
+ {name:"--codex",kind:"boolean",description:"Use the Codex RequestShield Skill."},
46
+ {name:"--claude",kind:"boolean",description:"Use the Claude RequestShield Skill."},
47
+ ];
48
+ /** @type {ArgumentDefinition} */
49
+ const appKey = {name:"app-key",field:"appKey",required:true,kind:"app-key",description:"The exact App Key, not the application name."};
50
+ /** @param {string} field @param {boolean} [required] @returns {ArgumentDefinition} */
51
+ const appName = (field, required = true) => ({name:"name",field,required,kind:"app-name",description:"Application name (1–100 characters); quote names containing spaces."});
52
+ const propagation = "Changes may take a moment. Check the application status before relying on the change.";
53
+
54
+ /**
55
+ * The complete public surface. Parsing, help and dispatch share this registry.
56
+ * @type {CommandDefinition[]}
57
+ */
58
+ export const COMMAND_REGISTRY = [
59
+ {id:"help",order:10,path:["--help"],shortcuts:["-h"],handler:"help",requiresApiSession:false,availability:"implemented",arguments:[],flags:[],summary:"Show help for the CLI, a group or a command.",examples:["--help","app create --help"]},
60
+ {id:"version",order:20,path:["--version"],shortcuts:["-v"],handler:"version",requiresApiSession:false,availability:"implemented",arguments:[],flags:[],summary:"Print the installed CLI version.",examples:["--version"]},
61
+ {id:"login",order:10,category:"account",icon:"🔑",navigationSummary:"Sign in to RequestShield",path:["login"],handler:"login",requiresApiSession:false,availability:"implemented",arguments:[],flags:[noOpen,json],summary:"Sign in through your browser.",examples:["login","login --no-open"]},
62
+ {id:"logout",order:20,category:"account",icon:"🚪",navigationSummary:"Remove your saved session",path:["logout"],handler:"logout",requiresApiSession:false,availability:"implemented",arguments:[],flags:[json],summary:"Remove your saved local session.",examples:["logout"],notes:["Your browser stays signed in."]},
63
+ {id:"auth-status",order:10,path:["auth","status"],handler:"auth-status",requiresApiSession:false,availability:"implemented",arguments:[],flags:[json],summary:"Inspect saved local session metadata.",examples:["auth status --json"],notes:["Does not renew the session or check online access."]},
64
+ {id:"app-create",order:10,path:["app","create"],handler:"app-create",requiresApiSession:true,availability:"implemented",arguments:[appName("appName")],flags:[idempotency,output,json],summary:"Create an app and its first secret.",examples:["app create \"Checkout API\"","app create \"Checkout API\" --output ./private/checkout.env --json"],notes:["Without --output, copy the printed secret into protected backend configuration.","--output never overwrites a file. Configure your backend to load the saved ENV file.","Receipt replay can return no secret; --output then fails with OUTPUT_SECRET_UNAVAILABLE. Reveal explicitly to save it.",propagation]},
65
+ {id:"app-list",order:20,path:["app","list"],handler:"app-list",requiresApiSession:true,availability:"implemented",arguments:[],flags:[{name:"--limit",kind:"value",value:"1-100",description:"Number of applications per page."},{name:"--cursor",kind:"value",value:"cursor",description:"Continue from an opaque page cursor."},{name:"--all",kind:"boolean",description:"Read every page, up to 100 pages."},json],summary:"List your apps.",examples:["app list","app list --all --json"],notes:["Defaults to one page. --cursor and --all are mutually exclusive.","App status is configuration state, not evidence of traffic or a working integration."]},
66
+ {id:"app-get",order:30,path:["app","get"],handler:"app-get",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[json],summary:"Show an app's information.",examples:["app get pk_example --json"]},
67
+ {id:"app-rename",order:40,path:["app","rename"],handler:"app-rename",requiresApiSession:true,availability:"implemented",arguments:[appKey,appName("name")],flags:[idempotency,json],summary:"Change an app's display name.",examples:["app rename pk_example \"New name\""]},
68
+ {id:"app-enable",order:50,path:["app","enable"],handler:"app-enable",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[idempotency,json],summary:"Enable an app.",examples:["app enable pk_example"],notes:[propagation]},
69
+ {id:"app-disable",order:60,path:["app","disable"],handler:"app-disable",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[idempotency,yes,json],summary:"Disable an app while retaining its secret.",examples:["app disable pk_example --yes"],notes:[propagation]},
70
+ {id:"secret-rotate",order:10,path:["secret","rotate"],handler:"secret-rotate",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[idempotency,yes,output,json],summary:"Replace the current secret, keeping the App Key and enabled state.",examples:["secret rotate pk_example --yes","secret rotate pk_example --output ./private/checkout-new.env --yes"],notes:["Without --output, copy the printed secret into protected backend configuration.","--output requires a new file. Load that file into the backend after rotation.","Receipt replay can return no secret; --output then fails with OUTPUT_SECRET_UNAVAILABLE. Reveal explicitly to save it.",propagation]},
71
+ {id:"secret-reveal",order:20,path:["secret","reveal"],handler:"secret-reveal",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[yes,output,json],summary:"Explicitly retrieve the current active secret.",examples:["secret reveal pk_example --yes","secret reveal pk_example --output ./private/checkout.env --yes"],notes:["This command retrieves sensitive backend credentials and records the retrieval in the audit log.","--output saves credentials in a new private ENV file and does not print the secret. Configure your backend to load it."]},
72
+ {id:"secret-revoke",order:30,path:["secret","revoke"],handler:"secret-revoke",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[idempotency,yes,json],summary:"Revoke the current secret.",examples:["secret revoke pk_example --yes"],notes:[propagation]},
73
+ {id:"agent-setup",order:10,path:["agent","setup"],handler:"agent-setup",requiresApiSession:false,availability:"implemented",arguments:[],flags:[...agents,{name:"--force",kind:"boolean",description:"Replace the selected existing Skill installation."},json],summary:"Install the RequestShield Skill for Codex or Claude.",examples:["agent setup --codex","agent setup --claude --force"],notes:["--codex and --claude are mutually exclusive."]},
74
+ {id:"agent-status",order:20,path:["agent","status"],handler:"agent-status",requiresApiSession:false,availability:"implemented",arguments:[],flags:[...agents,json],summary:"Check whether Agent Skill files are installed, missing or invalid.",examples:["agent status","agent status --codex --json"],notes:["Checks both agents by default. File presence does not prove an agent has loaded the Skill."]},
75
+ {id:"update-check",order:10,path:["update","check"],handler:"update-check",requiresApiSession:false,availability:"implemented",arguments:[],flags:[json],summary:"Check for a newer CLI version without installing it.",examples:["update check"],notes:[]},
76
+ {id:"update-apply",order:20,path:["update","apply"],handler:"update-apply",requiresApiSession:false,availability:"implemented",arguments:[],flags:[yes,json],summary:"Check and install the available version into the global npm installation.",examples:["update apply","update apply --yes"],notes:["Requires confirmation unless --yes is supplied."]},
77
+ {id:"contract",order:10,category:"developer",icon:"📖",navigationSummary:"View integration settings and SDK versions",path:["contract"],handler:"contract",requiresApiSession:false,availability:"implemented",arguments:[],flags:[json],summary:"Get the public integration contract and SDK versions.",examples:["contract","contract --json"],notes:["Reads public integration settings without signing in or accessing a saved session.","Returns published integration metadata; it does not install SDKs, modify project files or verify your application integration."]},
78
+ {id:"service-status",order:10,path:["service","status"],handler:"service-status",requiresApiSession:false,availability:"implemented",arguments:[],flags:[json],summary:"Check Management API and Challenge Service availability.",examples:["service status","service status --json"],notes:["Checks both public /health endpoints without signing in or reading a saved session.","Each request has a five-second deadline. No retries or redirects are followed.","Test a protected request to verify your application integration.","Exits 0 when both checks pass, 1 when either check fails, or 2 for invalid configuration."]},
79
+ {id:"usage",order:30,category:"applications",navigationSummary:"Monitor recent application requests",path:["usage"],handler:"usage",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[{name:"--from",kind:"value",value:"UTC-time",description:"Start of the time range; provide together with --to."},{name:"--to",kind:"value",value:"UTC-time",description:"End of the time range; provide together with --from."},json],summary:"Monitor recent request totals for an app.",examples:["usage pk_example","usage pk_example --json"],notes:["Defaults to the last 24 hours. Requested times must fall within the last seven days and end in Z, with at most three fractional digits.","Counts are approximate monitoring data; data can be delayed or incomplete. They are not billing totals. Usage reports cover API v2 requests only. API v1 activity is not included.","Retries count separately. Successful verifies are successful attempts, not unique challenges. Requests without an identified app are excluded."]},
80
+ {id:"billing-get",order:10,path:["billing","get"],handler:"billing-get",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[json],summary:"Show account billing for an owned App Key.",examples:["billing get pk_example","billing get pk_example --json"],notes:["Uses your saved session. All applications in your account share the plan, billing period, allowance and totals shown here.","Counts authenticated API v2 token verification requests, including rejected requests and retries. API v1 and service errors are excluded.","Usage, remaining quota and overage are approximate. Data may be delayed or incomplete; the usage cutoff is not a guarantee of complete reporting.","Estimated cost includes the period's base charge and usage reported so far, before taxes. It is not a final invoice.","Missing current-period data is shown as Unavailable (null in JSON), never as zero."]},
81
+ ];
82
+
83
+ /** @param {string} id */
84
+ export function getCommandDefinition(id) { return COMMAND_REGISTRY.find((command) => command.id === id); }
85
+
86
+ /** @param {CommandDefinition} command */
87
+ export function commandUsage(command) {
88
+ return [command.path.join(" "),...command.arguments.map((argument) => argument.required ? `<${argument.name}>` : `[${argument.name}]`),
89
+ ...command.flags.map((flag) => `[${flag.name}${flag.value ? ` <${flag.value}>` : ""}]`)].join(" ");
90
+ }
91
+
92
+ /** Command syntax without flag details, for progressive-disclosure help.
93
+ * @param {CommandDefinition} command */
94
+ function positionalUsage(command) {
95
+ return [command.path.join(" "),...command.arguments.map((argument) => argument.required ? `<${argument.name}>` : `[${argument.name}]`)].join(" ");
96
+ }
97
+
98
+ /** @param {CommandDefinition|string} [scope] @param {HelpOptions} [options] */
99
+ export function renderHelp(scope, options = {}) {
100
+ const {invocation = "requestshield", version = packageJson.version, renderer = createRenderer()} = options;
101
+ const title = `RequestShield ${version}${scope ? ` — ${typeof scope === "string" ? scope : scope.path.join(" ")}` : ""}`;
102
+ const sections = [renderer.heading(title,"🛡")];
103
+ /** @param {string} title @param {string} contents */
104
+ const section = (title, contents) => `${renderer.section(title)}\n${contents}`;
105
+ const helpOption = {label:"-h, --help",description:"Show help"};
106
+ if (typeof scope === "object") {
107
+ sections.push(renderer.paragraph(scope.summary));
108
+ sections.push(section("USAGE",` ${renderer.command(`${invocation} ${positionalUsage(scope)} [options]`)}`));
109
+ if (scope.arguments.length) {
110
+ sections.push(section("ARGUMENTS",renderer.rows(scope.arguments.map((argument) => ({
111
+ label:argument.required ? `<${argument.name}>` : `[${argument.name}]`,description:argument.description,
112
+ })))));
113
+ }
114
+ sections.push(section("OPTIONS",renderer.rows([
115
+ ...scope.flags.map((flag) => ({label:`${flag.name}${flag.value ? ` <${flag.value}>` : ""}`,description:flag.description})),
116
+ {...helpOption,description:"Show help for this command"},
117
+ ])));
118
+ sections.push(section("EXAMPLES",scope.examples.map((example) => ` ${renderer.command(`${invocation} ${example}`)}`).join("\n")));
119
+ const notes = scope.notes ?? [];
120
+ if (notes.length) sections.push(section("NOTES",notes.map((note) => renderer.paragraph(note)).join("\n\n")));
121
+ return sections.join("\n\n");
122
+ }
123
+ if (scope) {
124
+ const group = COMMAND_GROUPS.find((candidate) => candidate.name === scope);
125
+ if (!group) throw new Error(`Unknown help group: ${scope}`);
126
+ sections.push(renderer.paragraph(group.summary));
127
+ sections.push(section("USAGE",` ${renderer.command(`${invocation} ${scope} <command> [options]`)}`));
128
+ const commands = COMMAND_REGISTRY.filter((command) => command.path[0] === scope).sort((a,b) => a.order - b.order);
129
+ sections.push(section("COMMANDS",renderer.rows(commands.map((command) => ({
130
+ label:`${invocation} ${positionalUsage(command)}`,description:command.summary,icon:command.icon,
131
+ })))));
132
+ sections.push(section("OPTIONS",renderer.rows([{...helpOption,description:"Show help for this group"}])));
133
+ sections.push(renderer.muted(renderer.paragraph(`Use ${invocation} ${scope} <command> --help for details.`)));
134
+ return sections.join("\n\n");
135
+ }
136
+ sections.push(section("USAGE",` ${renderer.command(`${invocation} <command> [options]`)}`));
137
+ const navigation = [
138
+ ...COMMAND_GROUPS.map((group) => ({label:`${invocation} ${group.name}`,description:group.summary,category:group.category,icon:group.icon,order:group.order})),
139
+ ...COMMAND_REGISTRY.filter((command) => command.category).map((command) => ({
140
+ label:`${invocation} ${command.path.join(" ")}`,description:command.navigationSummary ?? command.summary,category:command.category,icon:command.icon,order:command.order,
141
+ })),
142
+ ];
143
+ const labelWidth = Math.max(...navigation.map(entry => displayWidth(entry.label)));
144
+ for (const category of [...HELP_CATEGORIES].sort((a,b) => a.order - b.order)) {
145
+ const members = navigation.filter((entry) => entry.category === category.id).sort((a,b) => a.order - b.order);
146
+ if (members.length) sections.push(section(category.label,renderer.rows(members,{labelWidth})));
147
+ }
148
+ sections.push(section("OPTIONS",renderer.rows([helpOption,{label:"-v, --version",description:"Show version"}])));
149
+ sections.push(renderer.muted(renderer.paragraph(`Use ${invocation} <command> --help for details.\nAdd --json to result commands for machine-readable output.`)));
150
+ return sections.join("\n\n");
151
+ }