requestshield 0.1.6 → 0.1.8

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 (36) hide show
  1. package/README.md +177 -68
  2. package/config/.env.prod +1 -0
  3. package/package.json +1 -1
  4. package/skills/requestshield/SKILL.md +20 -17
  5. package/skills/requestshield/assets/AGENTS.codex.md +5 -3
  6. package/skills/requestshield/references/backend-java-core.md +3 -3
  7. package/skills/requestshield/references/backend-spring-boot.md +3 -3
  8. package/skills/requestshield/references/browser-manual.md +1 -1
  9. package/skills/requestshield/references/cli.md +92 -28
  10. package/skills/requestshield/references/integration-planning.md +10 -8
  11. package/skills/requestshield/references/troubleshooting.md +2 -2
  12. package/src/api-client.mjs +1 -1
  13. package/src/args.mjs +98 -106
  14. package/src/cli.mjs +69 -243
  15. package/src/command-registry.mjs +97 -0
  16. package/src/commands/agent-setup.mjs +24 -17
  17. package/src/commands/agent-status.mjs +60 -0
  18. package/src/commands/application-mutations.mjs +8 -4
  19. package/src/commands/apps-get.mjs +12 -4
  20. package/src/commands/apps-list.mjs +20 -13
  21. package/src/commands/contract.mjs +25 -0
  22. package/src/commands/keys-create.mjs +5 -2
  23. package/src/commands/mutation-support.mjs +26 -12
  24. package/src/commands/output.mjs +21 -0
  25. package/src/commands/secret-commands.mjs +13 -6
  26. package/src/commands/signin.mjs +17 -8
  27. package/src/commands/signout.mjs +7 -3
  28. package/src/commands/update-check.mjs +97 -43
  29. package/src/config.mjs +29 -3
  30. package/src/entrypoint.mjs +20 -13
  31. package/src/integration-contract-client.mjs +81 -0
  32. package/src/integration-contract.mjs +104 -0
  33. package/src/oauth-client.mjs +2 -2
  34. package/src/oauth-loopback.mjs +1 -1
  35. package/src/session-files.mjs +4 -4
  36. package/src/session-store.mjs +2 -2
@@ -3,14 +3,42 @@
3
3
  Check `requestshield --version` and `requestshield --help` against this guide.
4
4
  An older published version may not contain these commands. Use the CLI for its
5
5
  supported application and credential operations; do not invent endpoints for
6
- unavailable commands.
6
+ unavailable commands. Command/group help comes from the same registry as parsing.
7
+ Legacy aliases are removed; use singular app/secret groups and positional names.
8
+ All result commands support --json, with one document on stdout and diagnostics
9
+ on stderr. Human output is the default; help/version remain text. JSON does not
10
+ skip confirmation: supply --yes only when the action is authorized.
11
+
12
+ ## Public integration contract
13
+
14
+ ```console
15
+ requestshield contract
16
+ requestshield contract --json
17
+ ```
18
+
19
+ This public read fetches the selected environment's customer-docs manifest without
20
+ login, local session access or file writes. It accepts no version-selection flag.
21
+ JSON returns `{data: ...}` with `schemaVersion`, `contractVersion`, `environment`,
22
+ `services.challengeUrl`, `browser`, `backend` and `documentation`.
23
+ Use `browser.scriptUrl` with its matching `integrity`, `tokenHeader` and
24
+ `availableModes`. Merge `browser.cspAdditions` into the application's existing CSP.
25
+ Use `backend.sdkVersion`, `minJdk`, `mavenRepository`, `core` and `springBoot3`
26
+ coordinates for the selected backend path. Configure the same returned challenge
27
+ URL in both SDKs. Metadata describes a selected release, not service health or
28
+ application protection.
29
+
30
+ `CONTRACT_NOT_PUBLISHED` means the docs host returned 404; `CONTRACT_FETCH_FAILED`
31
+ means network/HTTP failure; `CONTRACT_INVALID` means the response failed validation.
32
+ These exit 1. Do not invent a manifest or silently substitute another environment
33
+ or SDK version. An older CLI may not implement this command: use verified release
34
+ documentation for a manual integration and report the missing command or manifest.
7
35
 
8
36
  ## Authentication and local commands
9
37
 
10
38
  ```console
11
- requestshield signin [--no-open]
39
+ requestshield login [--no-open]
12
40
  requestshield auth status [--json]
13
- requestshield signout
41
+ requestshield logout
14
42
  requestshield --help
15
43
  requestshield --version
16
44
  ```
@@ -28,11 +56,11 @@ provider grant.
28
56
  ## Applications
29
57
 
30
58
  ```console
31
- requestshield apps list [--json] [--limit <1-100>] [--cursor <cursor> | --all]
32
- requestshield apps get <app-key>
33
- requestshield apps rename <app-key> --name <name> [--idempotency-key <key>]
34
- requestshield apps enable <app-key> [--idempotency-key <key>]
35
- requestshield apps disable <app-key> [--idempotency-key <key>] [--yes]
59
+ requestshield app list [--json] [--limit <1-100>] [--cursor <cursor> | --all]
60
+ requestshield app get <app-key>
61
+ requestshield app rename <app-key> <name> [--idempotency-key <key>]
62
+ requestshield app enable <app-key> [--idempotency-key <key>]
63
+ requestshield app disable <app-key> [--idempotency-key <key>] [--yes]
36
64
  ```
37
65
 
38
66
  These commands require sign-in. Existing-app commands use exact App Keys; names
@@ -41,8 +69,8 @@ fails for repeated cursors or unfinished traversal at that bound. It cannot be
41
69
  combined with `--cursor`. For name discovery, inspect all necessary pages and
42
70
  resolve ambiguity with the user before choosing an App Key.
43
71
 
44
- JSON list output is `{data:Application[],nextCursor}`. Detail and rename output
45
- is `{data:Application}`. Application has exactly `appKey`, `name`, `status`,
72
+ JSON list output is `{data:Application[],nextCursor}`. Detail and rename with
73
+ `--json` return `{data:Application}`. Application has exactly `appKey`, `name`, `status`,
46
74
  `createdAt`, `updatedAt`. Status is `pending`, `enabled`, `disabled`, `revoked`
47
75
  or `attention_required`; it describes configuration, not traffic or enforcement.
48
76
  Pending, attention and disabled states do not expose the exact credential state.
@@ -55,10 +83,10 @@ values stay out of tool output and transcripts. Give the exact command and
55
83
  confirm storage by presence only.
56
84
 
57
85
  ```console
58
- requestshield keys create --app-name <name> [--idempotency-key <key>]
59
- requestshield keys rotate <app-key> [--idempotency-key <key>] [--yes]
60
- requestshield keys reveal <app-key> [--yes]
61
- requestshield keys revoke <app-key> [--idempotency-key <key>] [--yes]
86
+ requestshield app create <name> [--idempotency-key <key>]
87
+ requestshield secret rotate <app-key> [--idempotency-key <key>] [--yes]
88
+ requestshield secret reveal <app-key> [--yes]
89
+ requestshield secret revoke <app-key> [--idempotency-key <key>] [--yes]
62
90
  ```
63
91
 
64
92
  Create makes a new application and initial secret. Rotate preserves the App Key
@@ -83,7 +111,9 @@ without running it. HTTP 202 and `enabled` do not prove global propagation.
83
111
 
84
112
  ```console
85
113
  requestshield agent setup [--codex | --claude] [--force]
114
+ requestshield agent status [--codex | --claude]
86
115
  requestshield update check
116
+ requestshield update apply [--yes]
87
117
  ```
88
118
 
89
119
  Agent setup copies the skill to `~/.agents/skills/requestshield` for Codex or
@@ -91,17 +121,51 @@ Agent setup copies the skill to `~/.agents/skills/requestshield` for Codex or
91
121
  agent or asks if both are available. `--force` replaces an existing installation.
92
122
  It does not edit repository `AGENTS.md` or customer application code.
93
123
 
94
- Production update check queries npm and asks before a global installation.
95
- Explain that target before accepting an update; it does not update a pinned
96
- invocation or source checkout. Repository-only QAT/STG runners return local
97
- source-update guidance without registry access or installation.
98
-
99
- ## Unavailable commands
100
-
101
- Integration contract, service health, credential-status metadata, challenge
102
- volume and billing are not implemented CLI capabilities. `contract`,
103
- `challenge volume` and `get billing` fail with `COMMAND_UNAVAILABLE` before
104
- configuration, authentication or network access. `server` and
105
- `credentials status` are unsupported. Use the published SDK documentation for
106
- the chosen release and safe browser/backend observations for integration checks.
107
- Do not infer traffic or enforcement from `apps get` status.
124
+ Agent status inspects both agents by default and returns `installed`, `missing`
125
+ or `invalid` file state. It does not establish that an agent has loaded the Skill.
126
+
127
+ Production update check queries npm without prompting or installing. Update apply
128
+ checks and asks before installing the exact available version globally; --yes
129
+ explicitly confirms. Explain the target when an update is requested. A global
130
+ update does not change a pinned invocation or source checkout. Repository-only
131
+ QAT/STG runners return local source-update guidance for both commands without
132
+ registry access or installation.
133
+
134
+ ## Complete command catalog
135
+
136
+ All result commands support --json. See command help for additional flags.
137
+
138
+ | Command after `requestshield` | Availability |
139
+ | --- | --- |
140
+ | `--help`, `-h` | Implemented |
141
+ | `--version`, `-v` | Implemented |
142
+ | `login [--no-open]` | Implemented |
143
+ | `logout` | Implemented |
144
+ | `auth status` | Implemented |
145
+ | `app create <name>` | Implemented |
146
+ | `app list` | Implemented |
147
+ | `app get <app-key>` | Implemented |
148
+ | `app rename <app-key> <name>` | Implemented |
149
+ | `app enable <app-key>` | Implemented |
150
+ | `app disable <app-key>` | Implemented |
151
+ | `secret rotate <app-key>` | Implemented |
152
+ | `secret reveal <app-key>` | Implemented |
153
+ | `secret revoke <app-key>` | Implemented |
154
+ | `agent setup [--codex \| --claude] [--force]` | Implemented |
155
+ | `agent status [--codex \| --claude]` | Implemented |
156
+ | `update check` | Implemented |
157
+ | `update apply [--yes]` | Implemented |
158
+ | `contract` | Implemented |
159
+ | `service status` | [coming soon] |
160
+ | `secret status <app-key>` | [coming soon] |
161
+ | `usage challenges <app-key>` | [coming soon] |
162
+ | `billing get <app-key>` | [coming soon] |
163
+
164
+ The four placeholders validate syntax and return COMMAND_UNAVAILABLE with exit
165
+ code 2 before configuration, authentication, network or writes. Usage accepts
166
+ --from, --to and --granularity hour|day as placeholder syntax only. Never use
167
+ placeholder results as evidence, infer secret state from application status or
168
+ call reveal as a status probe. Doctor and app watch are not commands.
169
+
170
+ Use published SDK documentation for the selected release and safe browser/backend
171
+ observations for integration checks. App status is not traffic or enforcement.
@@ -72,7 +72,7 @@ Confirm that the backend actually reads or maps that variable before treating it
72
72
  valid RequestShield credential.
73
73
 
74
74
  If the App Key is known, inspect its aggregate application configuration with
75
- `requestshield apps get <app-key>`. That status does not prove the exact credential
75
+ `requestshield app get <app-key>`. That status does not prove the exact credential
76
76
  state or that the deployed backend has received its secret. The CLI has no separate
77
77
  credential-status command; check runtime injection by presence only.
78
78
 
@@ -91,7 +91,7 @@ Report only what was confirmed:
91
91
  | No credential pair has been provisioned | Ask the user to provision credentials before continuing with backend setup. |
92
92
 
93
93
  If no pair has been provisioned yet, the user creates it. You may run the read-only
94
- `requestshield apps list` to see whether an application already exists. Everything that
94
+ `requestshield app list` to see whether an application already exists. Everything that
95
95
  mints or changes a secret stays with the user; see **Key and secret management** in
96
96
  `SKILL.md`.
97
97
 
@@ -180,11 +180,13 @@ last one to run wins rather than the two merging.
180
180
 
181
181
  ## Contract check
182
182
 
183
- After detecting the current integration, consult the customer Browser SDK and Java
184
- SDK documentation for the selected release. Confirm the script URL, token header,
185
- supported browser modes, backend languages and minimum Java version before editing.
186
- The CLI integration-contract command is unavailable; do not require it as a preflight
187
- or fabricate its output.
183
+ After detecting the current integration, run `requestshield contract --json`
184
+ with the runner for the intended environment. It requires no login. Confirm the
185
+ script URL and matching integrity, token header, available modes, CSP additions,
186
+ challenge URL and Java/Maven requirements, then consult its documentation links.
187
+ See `cli.md` for field names and failure handling. If the command or manifest is
188
+ unavailable, report that gap and use verified customer documentation for the
189
+ selected release; never fabricate values or substitute another environment.
188
190
 
189
191
  The documented backend paths here require Java 17 or newer. The Spring Boot starter
190
192
  also requires Spring Boot 3 with Spring MVC; Java core is a separate supported path.
@@ -320,7 +322,7 @@ the page issues the protected request.
320
322
  Always report the evidence with file and line references:
321
323
 
322
324
  ```text
323
- Backend: theair-customer-backend/build.gradle.kts
325
+ Backend: customer-backend/build.gradle.kts
324
326
  Java 21 - java.toolchain.languageVersion (line 14)
325
327
  Spring Boot 3.2.1 - org.springframework.boot plugin (line 3)
326
328
  Spring MVC - spring-boot-starter-web (line 20)
@@ -8,7 +8,7 @@ header, the backend never verified it, or one token was verified twice.
8
8
 
9
9
  ```console
10
10
  requestshield auth status --json
11
- requestshield apps get <app-key>
11
+ requestshield app get <app-key>
12
12
  ```
13
13
 
14
14
  Session status is local only. Application status is configuration state, not
@@ -105,7 +105,7 @@ If the API Secret appears in a repo, a log, a build artifact, or a chat transcri
105
105
  rotate it — deleting the line does not un-expose it:
106
106
 
107
107
  ```bash
108
- requestshield keys rotate <app-key>
108
+ requestshield secret rotate <app-key>
109
109
  ```
110
110
 
111
111
  The App Key is unchanged, so no browser edit is needed. Deploy the new secret to backend
@@ -107,7 +107,7 @@ export class ManagementApiClient {
107
107
  if (!response.ok) {
108
108
  const providerCode = objectString(body?.error, "code");
109
109
  const safeCode = providerCode && API_ERRORS.has(providerCode) ? providerCode : undefined;
110
- const guidance = response.status === 401 ? "; run `requestshield signin` again"
110
+ const guidance = response.status === 401 ? "; run `requestshield login` again"
111
111
  : safeCode === "insufficient_scope" ? "; this session lacks the required permissions" : "";
112
112
  throw new CliError(`RequestShield API error: HTTP ${response.status}${safeCode ? ` (${safeCode})` : ""}${guidance}`, {
113
113
  code: response.status === 401 ? "UNAUTHENTICATED" : "API_ERROR",
package/src/args.mjs CHANGED
@@ -1,145 +1,137 @@
1
1
  // @ts-check
2
2
  import { CliError } from "./errors.mjs";
3
+ import { COMMAND_REGISTRY, commandUsage, renderHelp } from "./command-registry.mjs";
3
4
 
4
- const HELP = `RequestShield CLI
5
-
6
- Usage:
7
- requestshield signin [--no-open]
8
- requestshield auth status [--json]
9
- requestshield signout
10
- requestshield keys create --app-name <name> [--idempotency-key <key>]
11
- requestshield keys rotate <app-key> [--idempotency-key <key>] [--yes]
12
- requestshield keys reveal <app-key> [--yes]
13
- requestshield keys revoke <app-key> [--idempotency-key <key>] [--yes]
14
- requestshield apps list [--json] [--limit <1-100>] [--cursor <cursor> | --all]
15
- requestshield apps get <app-key>
16
- requestshield apps rename <app-key> --name <name> [--idempotency-key <key>]
17
- requestshield apps enable <app-key> [--idempotency-key <key>]
18
- requestshield apps disable <app-key> [--idempotency-key <key>] [--yes]
19
- requestshield agent setup [--force]
20
- requestshield agent setup --codex [--force]
21
- requestshield agent setup --claude [--force]
22
- requestshield update check
23
- requestshield --help
24
- requestshield --version
25
-
26
- List defaults to one page. Use --all for a bounded complete listing.
27
- contract, challenge volume and get billing are not available yet.`;
5
+ export const HELP = renderHelp();
28
6
 
29
7
  /**
30
- * @typedef {{command:"help",help:string} | {command:"version"|"update-check"|"signout"}
31
- * | {command:"signin",noOpen:boolean} | {command:"auth-status",json:boolean}
32
- * | {command:"keys-create",appName:string,yes:boolean,idempotencyKey?:string}
33
- * | {command:"keys-rotate"|"keys-reveal"|"keys-revoke",appKey:string,yes:boolean,idempotencyKey?:string}
34
- * | {command:"apps-list",json:boolean,all:boolean,limit?:number,cursor?:string}
35
- * | {command:"apps-get",appKey:string}
36
- * | {command:"apps-rename",appKey:string,name:string,yes:boolean,idempotencyKey?:string}
37
- * | {command:"apps-enable"|"apps-disable",appKey:string,enabled:boolean,yes:boolean,idempotencyKey?:string}
38
- * | {command:"agent-setup",agent?:"codex"|"claude",force:boolean}} ParsedArgs
8
+ * @typedef {{command:"help",help:string} | {command:"version"}
9
+ * | {command:"update-check"|"logout"|"auth-status"|"contract",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}
13
+ * | {command:"secret-rotate"|"secret-reveal"|"secret-revoke",appKey:string,yes:boolean,json:boolean,idempotencyKey?:string}
14
+ * | {command:"app-list",json:boolean,all:boolean,limit?:number,cursor?:string}
15
+ * | {command:"app-get",appKey:string,json:boolean}
16
+ * | {command:"app-rename",appKey:string,name:string,yes:boolean,json:boolean,idempotencyKey?:string}
17
+ * | {command:"app-enable"|"app-disable",appKey:string,enabled:boolean,yes:boolean,json:boolean,idempotencyKey?:string}
18
+ * | {command:"agent-setup",agent?:"codex"|"claude",force:boolean,json:boolean}
19
+ * | {command:"agent-status",agent?:"codex"|"claude",json:boolean}} ParsedArgs
39
20
  */
40
21
 
41
22
  /** @param {string[]} argv @returns {ParsedArgs} */
42
23
  export function parseArgs(argv) {
43
- if (!argv.length || argv.includes("--help") || argv.includes("-h")) return {command:"help",help:HELP};
44
- if (argv.length === 1 && ["--version","-v","-V"].includes(argv[0])) return {command:"version"};
45
- const [group, action] = argv;
46
- if (group === "contract" || group === "challenge" && action === "volume" || group === "get" && action === "billing") {
47
- throw new CliError("This command is not available: its Management API endpoint has not been implemented.", {code:"COMMAND_UNAVAILABLE",exitCode:2});
48
- }
49
- if (group === "signin") {
50
- const flags = parseFlags(argv.slice(1), ["--no-open"]);
51
- return {command:"signin",noOpen:flags.has("--no-open")};
52
- }
53
- if (group === "signout") { parseFlags(argv.slice(1), []); return {command:"signout"}; }
54
- if (group === "auth" && action === "status") {
55
- const flags = parseFlags(argv.slice(2), ["--json"]);
56
- return {command:"auth-status",json:flags.has("--json")};
24
+ if (!argv.length) return {command:"help",help:HELP};
25
+ const definition = COMMAND_REGISTRY.find((command) => command.path.every((part,index) => argv[index] === part)
26
+ || command.shortcuts?.includes(argv[0]) && command.path.length === 1);
27
+ if (!definition) {
28
+ const group = argv[0];
29
+ const isGroup = COMMAND_REGISTRY.some((command) => command.path.length > 1 && command.path[0] === group);
30
+ if (isGroup && (argv.length === 1 || argv.length === 2 && ["--help","-h"].includes(argv[1]))) {
31
+ return {command:"help",help:renderHelp(group)};
32
+ }
33
+ throw invalid("Unknown command. Use requestshield --help to list commands.");
57
34
  }
58
- if (group === "update" && action === "check") { parseFlags(argv.slice(2), []); return {command:"update-check"}; }
59
- if (group === "keys" && action === "create") {
60
- const flags = parseFlags(argv.slice(2), ["--yes"], ["--app-name","--idempotency-key"]);
61
- const name = flags.get("--app-name");
62
- if (typeof name !== "string") usage("keys create --app-name <name> [--idempotency-key <key>]");
63
- return {command:"keys-create",appName:applicationName(name),...mutationFlags(flags)};
35
+ const args = argv.slice(definition.path.length);
36
+ const endOfOptions = args.indexOf("--");
37
+ if (args.slice(0,endOfOptions === -1 ? args.length : endOfOptions).some((value) => ["--help","-h"].includes(value))) {
38
+ return {command:"help",help:renderHelp(definition)};
64
39
  }
65
- if (group === "keys" && ["rotate","reveal","revoke"].includes(action)) {
66
- const appKey = requiredAppKey(argv, `keys ${action} <app-key>`);
67
- const flags = parseFlags(argv.slice(3), ["--yes"], action === "reveal" ? [] : ["--idempotency-key"]);
68
- return {command:/** @type {"keys-rotate"|"keys-reveal"|"keys-revoke"} */ (`keys-${action}`),appKey,...mutationFlags(flags)};
40
+ const {flags,positionals} = parseOptions(args,definition.flags);
41
+ const requiredCount = definition.arguments.filter((argument) => argument.required).length;
42
+ if (positionals.length < requiredCount || positionals.length > definition.arguments.length) {
43
+ throw invalid(`Usage: requestshield ${commandUsage(definition)}`);
69
44
  }
70
- if (group === "apps" && action === "list") {
71
- const flags = parseFlags(argv.slice(2), ["--json","--all"], ["--limit","--cursor"]);
72
- const limit = flags.get("--limit");
73
- if (limit !== undefined && (typeof limit !== "string" || !/^[1-9][0-9]{0,2}$/.test(limit) || Number(limit) > 100)) {
74
- throw invalid("--limit must be an integer from 1 to 100");
75
- }
76
- const cursor = flags.get("--cursor");
77
- if (cursor !== undefined && (typeof cursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(cursor))) throw invalid("Invalid --cursor");
78
- if (cursor !== undefined && flags.has("--all")) throw invalid("--cursor and --all cannot be combined");
79
- return {command:"apps-list",json:flags.has("--json"),all:flags.has("--all"),
80
- ...(limit !== undefined ? {limit:Number(limit)} : {}),...(typeof cursor === "string" ? {cursor} : {})};
45
+ /** @type {Record<string,string>} */
46
+ const values = {};
47
+ for (const [index,argument] of definition.arguments.entries()) {
48
+ if (positionals[index] !== undefined) values[argument.field] = argument.kind === "app-name" ? applicationName(positionals[index]) : validateAppKey(positionals[index]);
81
49
  }
82
- if (group === "apps" && action === "get") {
83
- const appKey = requiredAppKey(argv, "apps get <app-key>");
84
- parseFlags(argv.slice(3), []);
85
- return {command:"apps-get",appKey};
50
+ if (definition.availability === "placeholder") {
51
+ throw new CliError("This command is not available yet.",{code:"COMMAND_UNAVAILABLE",exitCode:2});
86
52
  }
87
- if (group === "apps" && ["rename","enable","disable"].includes(action)) {
88
- const appKey = requiredAppKey(argv, `apps ${action} <app-key>${action === "rename" ? " --name <name>" : ""}`);
89
- const flags = parseFlags(argv.slice(3), ["--yes"], ["--idempotency-key",...(action === "rename" ? ["--name"] : [])]);
90
- if (action === "rename") {
91
- const name = flags.get("--name");
92
- if (typeof name !== "string") usage("apps rename <app-key> --name <name>");
93
- return {command:"apps-rename",appKey,name:applicationName(name),...mutationFlags(flags)};
53
+ const json = flags.has("--json");
54
+ switch (definition.id) {
55
+ case "help": return {command:"help",help:HELP};
56
+ case "version": return {command:"version"};
57
+ case "login": return {command:"login",noOpen:flags.has("--no-open"),json};
58
+ case "logout": case "auth-status": case "update-check": case "contract": return {command:definition.id,json};
59
+ case "update-apply": return {command:"update-apply",yes:flags.has("--yes"),json};
60
+ case "app-create": return {command:"app-create",appName:values.appName,json,...mutationFlags(flags)};
61
+ case "app-get": return {command:"app-get",appKey:values.appKey,json};
62
+ case "app-rename": return {command:"app-rename",appKey:values.appKey,name:values.name,json,...mutationFlags(flags)};
63
+ case "app-enable": case "app-disable": return {command:definition.id,appKey:values.appKey,enabled:definition.id === "app-enable",json,...mutationFlags(flags)};
64
+ case "secret-rotate": case "secret-reveal": case "secret-revoke": return {command:definition.id,appKey:values.appKey,json,...mutationFlags(flags)};
65
+ case "app-list": {
66
+ const limit = flags.get("--limit");
67
+ 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");
68
+ const cursor = flags.get("--cursor");
69
+ if (cursor !== undefined && (typeof cursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(cursor))) throw invalid("Invalid --cursor");
70
+ if (cursor !== undefined && flags.has("--all")) throw invalid("--cursor and --all cannot be combined");
71
+ return {command:"app-list",json,all:flags.has("--all"),...(limit !== undefined ? {limit:Number(limit)} : {}),...(typeof cursor === "string" ? {cursor} : {})};
94
72
  }
95
- return {command:action === "enable" ? "apps-enable" : "apps-disable",appKey,enabled:action === "enable",...mutationFlags(flags)};
73
+ case "agent-setup": return {command:"agent-setup",force:flags.has("--force"),json,...agentOption(flags)};
74
+ case "agent-status": return {command:"agent-status",json,...agentOption(flags)};
75
+ default: throw new Error("Implemented command has no parser result mapping");
96
76
  }
97
- if (group === "agent" && action === "setup") {
98
- const flags = parseFlags(argv.slice(2), ["--codex","--claude","--force"]);
99
- if (flags.has("--codex") && flags.has("--claude")) throw invalid("Only one of --codex or --claude may be provided");
100
- return {command:"agent-setup",force:flags.has("--force"),
101
- ...(flags.has("--codex") ? {agent:/** @type {const} */ ("codex")} : flags.has("--claude") ? {agent:/** @type {const} */ ("claude")} : {})};
102
- }
103
- throw invalid(`Unknown command.\n\n${HELP}`);
104
77
  }
105
78
 
106
- /** @param {string[]} args @param {string[]} booleans @param {string[]} [values] */
107
- function parseFlags(args, booleans, values = []) {
79
+ /** @param {string[]} args @param {import('./command-registry.mjs').FlagDefinition[]} options */
80
+ function parseOptions(args,options) {
108
81
  /** @type {Map<string,string|true>} */
109
82
  const flags = new Map();
83
+ /** @type {string[]} */
84
+ const positionals = [];
85
+ let positionalOnly = false;
110
86
  for (let i = 0; i < args.length; i++) {
111
87
  const arg = args[i];
112
- if (!booleans.includes(arg) && !values.includes(arg)) throw invalid(`Unknown option: ${arg}`);
113
- if (flags.has(arg)) throw invalid(`${arg} may only be provided once`);
114
- if (booleans.includes(arg)) flags.set(arg, true);
115
- else {
116
- const value = args[++i];
117
- if (!value || value.startsWith("--")) throw invalid(`${arg} requires a value`);
118
- flags.set(arg,value);
88
+ if (positionalOnly) { positionals.push(arg); continue; }
89
+ if (arg === "--") { positionalOnly = true; continue; }
90
+ if (!arg.startsWith("-")) { positionals.push(arg); continue; }
91
+ const equals = arg.indexOf("=");
92
+ const name = equals === -1 ? arg : arg.slice(0,equals);
93
+ const option = options.find((item) => item.name === name);
94
+ if (!option) throw invalid(`Unknown option: ${name}`);
95
+ if (flags.has(name)) throw invalid(`${name} may only be provided once`);
96
+ if (option.kind === "boolean") {
97
+ if (equals !== -1) throw invalid(`${name} does not accept a value`);
98
+ flags.set(name,true);
99
+ } else {
100
+ const value = equals === -1 ? args[++i] : arg.slice(equals + 1);
101
+ if (!value || equals === -1 && value.startsWith("-")) throw invalid(`${name} requires a value`);
102
+ if (option.choices && !option.choices.includes(value)) throw invalid(`${name} must be one of: ${option.choices.join(", ")}`);
103
+ flags.set(name,value);
119
104
  }
120
105
  }
121
- return flags;
106
+ return {flags,positionals};
122
107
  }
108
+
109
+ /** @param {Map<string,string|true>} flags */
110
+ function agentOption(flags) {
111
+ const selections = ["--codex","--claude"];
112
+ if (selections.filter((name) => flags.has(name)).length > 1) throw invalid(`Only one of ${selections.join(", ")} may be provided`);
113
+ return flags.has("--codex") ? {agent:/** @type {const} */ ("codex")} : flags.has("--claude") ? {agent:/** @type {const} */ ("claude")} : {};
114
+ }
115
+
123
116
  /** @param {Map<string,string|true>} flags */
124
117
  function mutationFlags(flags) {
125
118
  const key = flags.get("--idempotency-key");
126
119
  if (key !== undefined && (typeof key !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/.test(key))) throw invalid("Invalid --idempotency-key");
127
120
  return {yes:flags.has("--yes"),...(typeof key === "string" ? {idempotencyKey:key} : {})};
128
121
  }
129
- /** @param {string[]} argv @param {string} help */
130
- function requiredAppKey(argv, help) {
131
- if (!argv[2] || argv[2].startsWith("--")) usage(help);
132
- if (!/^[A-Za-z0-9._~-]{1,128}$/.test(argv[2])) throw invalid("App Key must be 1-128 letters, numbers, '.', '_', '~' or '-'");
133
- return argv[2];
122
+
123
+ /** @param {string} value */
124
+ export function validateAppKey(value) {
125
+ if (!/^[A-Za-z0-9._~-]{1,128}$/.test(value)) throw invalid("App Key must be 1-128 letters, numbers, '.', '_', '~' or '-'");
126
+ return value;
134
127
  }
128
+
135
129
  /** @param {string} value */
136
- function applicationName(value) {
130
+ export function applicationName(value) {
137
131
  const name = value.trim();
138
132
  if (!name || [...name].length > 100 || /[\p{Cc}\p{Cs}]/u.test(name)) throw invalid("Application name must contain 1-100 characters without control characters");
139
133
  return name;
140
134
  }
141
- /** @param {string} command @returns {never} */
142
- function usage(command) { throw invalid(`Usage: requestshield ${command}`); }
135
+
143
136
  /** @param {string} message */
144
137
  function invalid(message) { return new CliError(message,{code:"INVALID_ARGUMENT",exitCode:2}); }
145
- export { HELP };