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.
- package/README.md +177 -68
- package/config/.env.prod +1 -0
- package/package.json +1 -1
- package/skills/requestshield/SKILL.md +20 -17
- package/skills/requestshield/assets/AGENTS.codex.md +5 -3
- package/skills/requestshield/references/backend-java-core.md +3 -3
- package/skills/requestshield/references/backend-spring-boot.md +3 -3
- package/skills/requestshield/references/browser-manual.md +1 -1
- package/skills/requestshield/references/cli.md +92 -28
- package/skills/requestshield/references/integration-planning.md +10 -8
- package/skills/requestshield/references/troubleshooting.md +2 -2
- package/src/api-client.mjs +1 -1
- package/src/args.mjs +98 -106
- package/src/cli.mjs +69 -243
- package/src/command-registry.mjs +97 -0
- package/src/commands/agent-setup.mjs +24 -17
- package/src/commands/agent-status.mjs +60 -0
- package/src/commands/application-mutations.mjs +8 -4
- package/src/commands/apps-get.mjs +12 -4
- package/src/commands/apps-list.mjs +20 -13
- package/src/commands/contract.mjs +25 -0
- package/src/commands/keys-create.mjs +5 -2
- package/src/commands/mutation-support.mjs +26 -12
- package/src/commands/output.mjs +21 -0
- package/src/commands/secret-commands.mjs +13 -6
- package/src/commands/signin.mjs +17 -8
- package/src/commands/signout.mjs +7 -3
- package/src/commands/update-check.mjs +97 -43
- package/src/config.mjs +29 -3
- package/src/entrypoint.mjs +20 -13
- package/src/integration-contract-client.mjs +81 -0
- package/src/integration-contract.mjs +104 -0
- package/src/oauth-client.mjs +2 -2
- package/src/oauth-loopback.mjs +1 -1
- package/src/session-files.mjs +4 -4
- package/src/session-store.mjs +2 -2
package/src/cli.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
|
-
|
|
4
3
|
import { parseArgs } from "./args.mjs";
|
|
4
|
+
import { getCommandDefinition } from "./command-registry.mjs";
|
|
5
5
|
import { ManagementApiClient } from "./api-client.mjs";
|
|
6
6
|
import { SessionStore } from "./session-store.mjs";
|
|
7
7
|
import { OAuthClient } from "./oauth-client.mjs";
|
|
8
|
-
|
|
9
8
|
import { signin } from "./commands/signin.mjs";
|
|
10
9
|
import { createKeys } from "./commands/keys-create.mjs";
|
|
11
10
|
import { renameApp, setAppEnabled } from "./commands/application-mutations.mjs";
|
|
@@ -15,263 +14,90 @@ import { signout } from "./commands/signout.mjs";
|
|
|
15
14
|
import { listApps } from "./commands/apps-list.mjs";
|
|
16
15
|
import { getApp } from "./commands/apps-get.mjs";
|
|
17
16
|
import { setupAgent } from "./commands/agent-setup.mjs";
|
|
18
|
-
import {
|
|
19
|
-
|
|
17
|
+
import { showAgentStatus } from "./commands/agent-status.mjs";
|
|
18
|
+
import { checkForUpdate, applyUpdate } from "./commands/update-check.mjs";
|
|
19
|
+
import { showContract } from "./commands/contract.mjs";
|
|
20
20
|
import packageJson from "../package.json" with { type: "json" };
|
|
21
21
|
import { getApiUrl, getAuthorizationIssuer, getCommandInvocation, getCommandName, getOAuthConfig } from "./config.mjs";
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {{
|
|
62
|
-
* requiresApiSession: boolean,
|
|
63
|
-
* handler: (
|
|
64
|
-
* parsed: any,
|
|
65
|
-
* context: CommandContext
|
|
66
|
-
* ) => unknown | Promise<unknown>
|
|
67
|
-
* }} CommandHandler
|
|
68
|
-
*/
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Put every CLI command handler here.
|
|
72
|
-
*
|
|
73
|
-
* @type {Record<string, CommandHandler>}
|
|
74
|
-
*/
|
|
75
|
-
const COMMAND_HANDLERS = {
|
|
76
|
-
"help": {
|
|
77
|
-
requiresApiSession: false,
|
|
78
|
-
handler: (parsed, context) =>
|
|
79
|
-
context.log(parsed.help.replace(/^ requestshield /gm, ` ${getCommandInvocation(context.profile)} `)),
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
"version": {
|
|
83
|
-
requiresApiSession: false,
|
|
84
|
-
handler: (_parsed, context) =>
|
|
85
|
-
context.log(`${getCommandName(context.profile)} ${packageJson.version}`),
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
"update-check": {
|
|
89
|
-
requiresApiSession: false,
|
|
90
|
-
handler: (_parsed, context) =>
|
|
91
|
-
checkForUpdate({
|
|
92
|
-
profile: context.profile,
|
|
93
|
-
currentVersion: packageJson.version,
|
|
94
|
-
packageName: packageJson.name,
|
|
95
|
-
log: context.log,
|
|
96
|
-
fetchImpl: context.fetchImpl,
|
|
97
|
-
confirm: context.confirmUpdate,
|
|
98
|
-
install: context.installLatest,
|
|
99
|
-
}),
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
"agent-setup": {
|
|
103
|
-
requiresApiSession: false,
|
|
104
|
-
handler: (parsed, context) =>
|
|
105
|
-
setupAgent(parsed, context),
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
"signin": {
|
|
109
|
-
requiresApiSession: false,
|
|
110
|
-
handler: (parsed, context) => {
|
|
111
|
-
const env = context.env ?? process.env;
|
|
112
|
-
const oauth = context.oauth ?? new OAuthClient({config: getOAuthConfig(context.profile), fetchImpl: context.fetchImpl});
|
|
113
|
-
const authorizationIssuer = context.authorizationIssuer ?? (context.oauth
|
|
114
|
-
? oauth.config.issuer : getAuthorizationIssuer(context.profile, oauth.config.issuer));
|
|
115
|
-
const sessions = context.sessions ?? new SessionStore({env, homeDir: context.homeDir, oauth, config: oauth.config, profile: context.profile});
|
|
116
|
-
return signin(parsed, {...context, oauth, sessions, authorizationIssuer});
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
"keys-create": {
|
|
121
|
-
requiresApiSession: true,
|
|
122
|
-
handler: (parsed, context) =>
|
|
123
|
-
createKeys(parsed, requireApiSession(context)),
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
"keys-rotate": {requiresApiSession: true, handler: (parsed, context) => rotateSecret(parsed, requireApiSession(context))},
|
|
127
|
-
"keys-reveal": {requiresApiSession: true, handler: (parsed, context) => revealSecret(parsed, requireApiSession(context))},
|
|
128
|
-
"keys-revoke": {requiresApiSession: true, handler: (parsed, context) => revokeSecret(parsed, requireApiSession(context))},
|
|
129
|
-
"apps-rename": {requiresApiSession: true, handler: (parsed, context) => renameApp(parsed, requireApiSession(context))},
|
|
130
|
-
"apps-enable": {requiresApiSession: true, handler: (parsed, context) => setAppEnabled(parsed, requireApiSession(context))},
|
|
131
|
-
"apps-disable": {requiresApiSession: true, handler: (parsed, context) => setAppEnabled(parsed, requireApiSession(context))},
|
|
132
|
-
"auth-status": {requiresApiSession: false, handler: (parsed, context) => showAuthStatus(parsed, {...context, sessions: localSessions(context)})},
|
|
133
|
-
"signout": {requiresApiSession: false, handler: (parsed, context) => signout(parsed, {...context, sessions: localSessions(context)})},
|
|
134
|
-
|
|
135
|
-
"apps-list": {
|
|
136
|
-
requiresApiSession: true,
|
|
137
|
-
handler: (parsed, context) =>
|
|
138
|
-
listApps(parsed, requireApiSession(context)),
|
|
139
|
-
},
|
|
23
|
+
/** @typedef {{
|
|
24
|
+
* log: (message: string) => void, warn?: (message: string) => void,
|
|
25
|
+
* api?: ManagementApiClient, sessions?: SessionStore, oauth?: OAuthClient,
|
|
26
|
+
* authorizationIssuer?: string, openBrowser?: typeof import('./browser-opener.mjs').openBrowser,
|
|
27
|
+
* signal?: AbortSignal, env?: NodeJS.ProcessEnv, profile?: import('./config.mjs').Profile,
|
|
28
|
+
* homeDir?: string, sourceDir?: string, executablePath?: string,
|
|
29
|
+
* detectAgents?: () => Promise<Array<'codex' | 'claude'>>,
|
|
30
|
+
* selectAgent?: (detected: Array<'codex' | 'claude'>) => Promise<'codex' | 'claude'>,
|
|
31
|
+
* confirm?: () => Promise<boolean>, fetchImpl?: typeof fetch,
|
|
32
|
+
* confirmUpdate?: () => Promise<boolean>, installLatest?: (packageName: string, version: string) => Promise<void>
|
|
33
|
+
* }} CommandContext */
|
|
34
|
+
|
|
35
|
+
/** Runtime bindings only. Syntax, help and dependency policy live in the registry.
|
|
36
|
+
* @type {Record<string, (parsed: any, context: CommandContext) => unknown | Promise<unknown>>} */
|
|
37
|
+
export const COMMAND_HANDLERS = {
|
|
38
|
+
help: (parsed, context) => context.log(parsed.help.replace(/^ requestshield /gm, ` ${getCommandInvocation(context.profile)} `)),
|
|
39
|
+
version: (_parsed, context) => context.log(`${getCommandName(context.profile)} ${packageJson.version}`),
|
|
40
|
+
login: login,
|
|
41
|
+
logout: (parsed, context) => signout(parsed, {...context, sessions: localSessions(context)}),
|
|
42
|
+
"auth-status": (parsed, context) => showAuthStatus(parsed, {...context, sessions: localSessions(context)}),
|
|
43
|
+
contract: (parsed, context) => showContract(parsed, context),
|
|
44
|
+
"app-create": (parsed, context) => createKeys(parsed, requireApiSession(context)),
|
|
45
|
+
"app-list": (parsed, context) => listApps(parsed, requireApiSession(context)),
|
|
46
|
+
"app-get": (parsed, context) => getApp(parsed, requireApiSession(context)),
|
|
47
|
+
"app-rename": (parsed, context) => renameApp(parsed, requireApiSession(context)),
|
|
48
|
+
"app-enable": (parsed, context) => setAppEnabled(parsed, requireApiSession(context)),
|
|
49
|
+
"app-disable": (parsed, context) => setAppEnabled(parsed, requireApiSession(context)),
|
|
50
|
+
"secret-rotate": (parsed, context) => rotateSecret(parsed, requireApiSession(context)),
|
|
51
|
+
"secret-reveal": (parsed, context) => revealSecret(parsed, requireApiSession(context)),
|
|
52
|
+
"secret-revoke": (parsed, context) => revokeSecret(parsed, requireApiSession(context)),
|
|
53
|
+
"agent-setup": (parsed, context) => setupAgent(parsed, context),
|
|
54
|
+
"agent-status": (parsed, context) => showAgentStatus(parsed, context),
|
|
55
|
+
"update-check": (parsed, context) => checkForUpdate(updateOptions(parsed, context)),
|
|
56
|
+
"update-apply": (parsed, context) => applyUpdate(updateOptions(parsed, context)),
|
|
57
|
+
};
|
|
140
58
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
59
|
+
/** @param {{json?: boolean, yes?: boolean}} parsed @param {CommandContext} context */
|
|
60
|
+
function updateOptions(parsed, context) {
|
|
61
|
+
return {...parsed, profile: context.profile, currentVersion: packageJson.version, packageName: packageJson.name,
|
|
62
|
+
log: context.log, warn: context.warn, fetchImpl: context.fetchImpl, confirm: context.confirmUpdate, install: context.installLatest};
|
|
63
|
+
}
|
|
146
64
|
|
|
147
|
-
}
|
|
65
|
+
/** @param {{noOpen?: boolean, json?: boolean}} parsed @param {CommandContext} context */
|
|
66
|
+
function login(parsed, context) {
|
|
67
|
+
const env = context.env ?? process.env;
|
|
68
|
+
const oauth = context.oauth ?? new OAuthClient({config: getOAuthConfig(context.profile), fetchImpl: context.fetchImpl});
|
|
69
|
+
const authorizationIssuer = context.authorizationIssuer ?? (context.oauth
|
|
70
|
+
? oauth.config.issuer : getAuthorizationIssuer(context.profile, oauth.config.issuer));
|
|
71
|
+
const sessions = context.sessions ?? new SessionStore({env, homeDir: context.homeDir, oauth, config: oauth.config, profile: context.profile});
|
|
72
|
+
return signin(parsed, {...context, oauth, sessions, authorizationIssuer});
|
|
73
|
+
}
|
|
148
74
|
|
|
149
75
|
/** @param {CommandContext} context */
|
|
150
76
|
function localSessions(context) {
|
|
151
77
|
return context.sessions ?? new SessionStore({env: context.env, homeDir: context.homeDir, profile: context.profile});
|
|
152
78
|
}
|
|
153
79
|
|
|
154
|
-
/**
|
|
155
|
-
* Narrow the shared context before an authenticated handler can use it.
|
|
156
|
-
*
|
|
157
|
-
* @param {CommandContext} context
|
|
158
|
-
* @returns {AuthenticatedCommandContext}
|
|
159
|
-
*/
|
|
80
|
+
/** @param {CommandContext} context */
|
|
160
81
|
function requireApiSession(context) {
|
|
161
|
-
if (!context.api || !context.sessions)
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
...context,
|
|
167
|
-
api: context.api,
|
|
168
|
-
sessions: context.sessions,
|
|
169
|
-
};
|
|
82
|
+
if (!context.api || !context.sessions) throw new Error("Authenticated command context was not initialized");
|
|
83
|
+
return {...context, api: context.api, sessions: context.sessions};
|
|
170
84
|
}
|
|
171
85
|
|
|
172
|
-
/**
|
|
173
|
-
* @param {string[]} argv
|
|
174
|
-
* @param {{
|
|
175
|
-
* log?: (message: string) => void,
|
|
176
|
-
* warn?: (message: string) => void,
|
|
177
|
-
* api?: ManagementApiClient,
|
|
178
|
-
* sessions?: SessionStore,
|
|
179
|
-
* oauth?: OAuthClient,
|
|
180
|
-
* authorizationIssuer?: string,
|
|
181
|
-
* openBrowser?: typeof import('./browser-opener.mjs').openBrowser,
|
|
182
|
-
* signal?: AbortSignal,
|
|
183
|
-
* env?: NodeJS.ProcessEnv,
|
|
184
|
-
* profile?: import('./config.mjs').Profile,
|
|
185
|
-
* homeDir?: string,
|
|
186
|
-
* sourceDir?: string,
|
|
187
|
-
* executablePath?: string,
|
|
188
|
-
* detectAgents?: () => Promise<Array<"codex" | "claude">>,
|
|
189
|
-
* selectAgent?: (
|
|
190
|
-
* detected: Array<"codex" | "claude">
|
|
191
|
-
* ) => Promise<"codex" | "claude">,
|
|
192
|
-
* wait?: (milliseconds: number) => Promise<unknown>,
|
|
193
|
-
* confirm?: () => Promise<boolean>,
|
|
194
|
-
* fetchImpl?: typeof fetch,
|
|
195
|
-
* confirmUpdate?: () => Promise<boolean>,
|
|
196
|
-
* installLatest?: (
|
|
197
|
-
* packageName: string,
|
|
198
|
-
* version: string
|
|
199
|
-
* ) => Promise<void>
|
|
200
|
-
* }} [deps]
|
|
201
|
-
*/
|
|
86
|
+
/** @param {string[]} argv @param {Partial<CommandContext>} [deps] */
|
|
202
87
|
export async function run(argv, deps = {}) {
|
|
88
|
+
// Parse and reject unavailable commands before reading configuration or sessions.
|
|
89
|
+
const parsed = parseArgs(argv);
|
|
203
90
|
const profile = deps.profile ?? "prod";
|
|
204
91
|
getCommandName(profile);
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const command = COMMAND_HANDLERS[parsed.command];
|
|
215
|
-
|
|
216
|
-
if (!command) {
|
|
217
|
-
throw new Error(
|
|
218
|
-
`Unsupported command: ${String(parsed.command)}`
|
|
219
|
-
);
|
|
92
|
+
const definition = getCommandDefinition(parsed.command);
|
|
93
|
+
const handler = definition?.handler ? COMMAND_HANDLERS[definition.handler] : undefined;
|
|
94
|
+
if (!handler) throw new Error("Unsupported command handler");
|
|
95
|
+
const context = {...deps, profile, log: deps.log ?? console.log, warn: deps.warn ?? console.error};
|
|
96
|
+
if (definition?.requiresApiSession) {
|
|
97
|
+
return handler(parsed, {...context,
|
|
98
|
+
api: deps.api ?? new ManagementApiClient({baseUrl: getApiUrl(profile), fetchImpl: deps.fetchImpl}),
|
|
99
|
+
sessions: localSessions(context),
|
|
100
|
+
});
|
|
220
101
|
}
|
|
221
|
-
|
|
222
|
-
/*
|
|
223
|
-
* Dependencies available to every command.
|
|
224
|
-
*/
|
|
225
|
-
/** @type {CommandContext} */
|
|
226
|
-
const context = {
|
|
227
|
-
...deps,
|
|
228
|
-
profile,
|
|
229
|
-
log,
|
|
230
|
-
warn: deps.warn ?? ((message) => console.error(message)),
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/*
|
|
234
|
-
* Only initialize the Management API and session
|
|
235
|
-
* for commands that require them.
|
|
236
|
-
*/
|
|
237
|
-
if (command.requiresApiSession) {
|
|
238
|
-
const env =
|
|
239
|
-
deps.env ??
|
|
240
|
-
process.env;
|
|
241
|
-
|
|
242
|
-
const api =
|
|
243
|
-
deps.api ??
|
|
244
|
-
new ManagementApiClient({
|
|
245
|
-
baseUrl: getApiUrl(profile),
|
|
246
|
-
fetchImpl: deps.fetchImpl,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const sessions =
|
|
250
|
-
deps.sessions ??
|
|
251
|
-
new SessionStore({
|
|
252
|
-
env,
|
|
253
|
-
homeDir: deps.homeDir,
|
|
254
|
-
profile,
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const authenticatedContext = {
|
|
258
|
-
...context,
|
|
259
|
-
env,
|
|
260
|
-
api,
|
|
261
|
-
sessions,
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
return command.handler(
|
|
265
|
-
parsed,
|
|
266
|
-
authenticatedContext
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/*
|
|
271
|
-
* Execute commands that do not need an API session.
|
|
272
|
-
*/
|
|
273
|
-
return command.handler(
|
|
274
|
-
parsed,
|
|
275
|
-
context
|
|
276
|
-
);
|
|
102
|
+
return handler(parsed, context);
|
|
277
103
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @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"|"secret-status"|"usage-challenges"|"billing-get"} CommandId
|
|
5
|
+
* @typedef {{name:string,field:string,required:boolean,kind:"app-name"|"app-key",description:string}} ArgumentDefinition
|
|
6
|
+
* @typedef {{name:string,kind:"boolean"|"value",description:string,value?:string,choices?:string[]}} FlagDefinition
|
|
7
|
+
* @typedef {{id:CommandId,path:string[],handler:CommandId|null,requiresApiSession:boolean,availability:"implemented"|"placeholder",arguments:ArgumentDefinition[],flags:FlagDefinition[],summary:string,examples:string[],notes?:string[],shortcuts?:string[]}} CommandDefinition
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @type {FlagDefinition} */
|
|
11
|
+
const json = {name:"--json",kind:"boolean",description:"Write one JSON result to stdout."};
|
|
12
|
+
/** @type {FlagDefinition} */
|
|
13
|
+
const yes = {name:"--yes",kind:"boolean",description:"Confirm this operation without prompting."};
|
|
14
|
+
/** @type {FlagDefinition} */
|
|
15
|
+
const idempotency = {name:"--idempotency-key",kind:"value",value:"key",description:"Reuse the same key when retrying the identical request within seven days."};
|
|
16
|
+
/** @type {FlagDefinition} */
|
|
17
|
+
const noOpen = {name:"--no-open",kind:"boolean",description:"Print the login URL without opening a browser."};
|
|
18
|
+
/** @type {FlagDefinition[]} */
|
|
19
|
+
const agents = [
|
|
20
|
+
{name:"--codex",kind:"boolean",description:"Use the Codex RequestShield Skill."},
|
|
21
|
+
{name:"--claude",kind:"boolean",description:"Use the Claude RequestShield Skill."},
|
|
22
|
+
];
|
|
23
|
+
/** @type {ArgumentDefinition} */
|
|
24
|
+
const appKey = {name:"app-key",field:"appKey",required:true,kind:"app-key",description:"The exact App Key, not the application name."};
|
|
25
|
+
/** @param {string} field @param {boolean} [required] @returns {ArgumentDefinition} */
|
|
26
|
+
const appName = (field, required = true) => ({name:"name",field,required,kind:"app-name",description:"Application name (1–100 characters); quote names containing spaces."});
|
|
27
|
+
const propagation = "Accepted changes propagate asynchronously. Acceptance does not mean the change is globally active.";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The complete public surface. Parsing, help and dispatch share this registry.
|
|
31
|
+
* @type {CommandDefinition[]}
|
|
32
|
+
*/
|
|
33
|
+
export const COMMAND_REGISTRY = [
|
|
34
|
+
{id:"help",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"]},
|
|
35
|
+
{id:"version",path:["--version"],shortcuts:["-v"],handler:"version",requiresApiSession:false,availability:"implemented",arguments:[],flags:[],summary:"Print the installed CLI version.",examples:["--version"]},
|
|
36
|
+
{id:"login",path:["login"],handler:"login",requiresApiSession:false,availability:"implemented",arguments:[],flags:[noOpen,json],summary:"Sign in through the browser using OAuth.",examples:["login","login --no-open"]},
|
|
37
|
+
{id:"logout",path:["logout"],handler:"logout",requiresApiSession:false,availability:"implemented",arguments:[],flags:[json],summary:"Remove the saved session for this profile.",examples:["logout"],notes:["Does not end your browser session or revoke the provider grant."]},
|
|
38
|
+
{id:"auth-status",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 refresh the session or verify that the provider accepts it."]},
|
|
39
|
+
{id:"app-create",path:["app","create"],handler:"app-create",requiresApiSession:true,availability:"implemented",arguments:[appName("appName")],flags:[idempotency,json],summary:"Create an app and its first secret.",examples:["app create \"Checkout API\""],notes:["Copy the returned secret into protected backend configuration; the CLI does not save it.","Receipt replay can return apiSecret: null and never automatically reveals a secret.",propagation]},
|
|
40
|
+
{id:"app-list",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."]},
|
|
41
|
+
{id:"app-get",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"]},
|
|
42
|
+
{id:"app-rename",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\""]},
|
|
43
|
+
{id:"app-enable",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]},
|
|
44
|
+
{id:"app-disable",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]},
|
|
45
|
+
{id:"secret-rotate",path:["secret","rotate"],handler:"secret-rotate",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[idempotency,yes,json],summary:"Replace the current secret, keeping the App Key and enabled state.",examples:["secret rotate pk_example --yes"],notes:["Copy the returned secret into protected backend configuration. A receipt replay can return apiSecret: null.",propagation]},
|
|
46
|
+
{id:"secret-reveal",path:["secret","reveal"],handler:"secret-reveal",requiresApiSession:true,availability:"implemented",arguments:[appKey],flags:[yes,json],summary:"Explicitly retrieve the current active secret.",examples:["secret reveal pk_example --yes"],notes:["This command discloses sensitive backend credentials and records the retrieval in the audit log."]},
|
|
47
|
+
{id:"secret-revoke",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]},
|
|
48
|
+
{id:"agent-setup",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."]},
|
|
49
|
+
{id:"agent-status",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."]},
|
|
50
|
+
{id:"update-check",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:["QAT/STG runners return local source-update guidance without querying npm."]},
|
|
51
|
+
{id:"update-apply",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. QAT/STG runners only provide source-update guidance."]},
|
|
52
|
+
{id:"contract",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 the selected profile's public documentation 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."]},
|
|
53
|
+
{id:"service-status",path:["service","status"],handler:null,requiresApiSession:false,availability:"placeholder",arguments:[],flags:[json],summary:"Check service availability.",examples:["service status"]},
|
|
54
|
+
{id:"secret-status",path:["secret","status"],handler:null,requiresApiSession:false,availability:"placeholder",arguments:[appKey],flags:[json],summary:"Get secret status metadata without revealing the secret.",examples:["secret status pk_example"]},
|
|
55
|
+
{id:"usage-challenges",path:["usage","challenges"],handler:null,requiresApiSession:false,availability:"placeholder",arguments:[appKey],flags:[{name:"--from",kind:"value",value:"time",description:"Start of the requested time range."},{name:"--to",kind:"value",value:"time",description:"End of the requested time range."},{name:"--granularity",kind:"value",value:"hour|day",choices:["hour","day"],description:"Requested aggregation period."},json],summary:"Get challenge usage for an app.",examples:["usage challenges pk_example --granularity day"]},
|
|
56
|
+
{id:"billing-get",path:["billing","get"],handler:null,requiresApiSession:false,availability:"placeholder",arguments:[appKey],flags:[json],summary:"Get an app's plan and billing information.",examples:["billing get pk_example"]},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** @param {string} id */
|
|
60
|
+
export function getCommandDefinition(id) { return COMMAND_REGISTRY.find((command) => command.id === id); }
|
|
61
|
+
|
|
62
|
+
/** @param {CommandDefinition} command */
|
|
63
|
+
export function commandUsage(command) {
|
|
64
|
+
return [command.path.join(" "),...command.arguments.map((argument) => argument.required ? `<${argument.name}>` : `[${argument.name}]`),
|
|
65
|
+
...command.flags.map((flag) => `[${flag.name}${flag.value ? ` <${flag.value}>` : ""}]`)].join(" ");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {CommandDefinition|string} [scope] */
|
|
69
|
+
export function renderHelp(scope) {
|
|
70
|
+
if (typeof scope === "object") {
|
|
71
|
+
const unavailable = scope.availability === "placeholder";
|
|
72
|
+
return [
|
|
73
|
+
`RequestShield CLI — ${scope.path.join(" ")}${unavailable ? " [coming soon]" : ""}`,
|
|
74
|
+
"",scope.summary,"","Usage:",` requestshield ${commandUsage(scope)}`,
|
|
75
|
+
...(scope.arguments.length ? ["","Arguments:",...scope.arguments.map((arg) => ` ${arg.required ? `<${arg.name}>` : `[${arg.name}]`} ${arg.description}`)] : []),
|
|
76
|
+
"","Options:",...scope.flags.map((flag) => ` ${flag.name}${flag.value ? ` <${flag.value}>` : ""} ${flag.description}`),
|
|
77
|
+
" --help, -h Show help for this command.",
|
|
78
|
+
"","Examples:",...scope.examples.map((example) => ` requestshield ${example}`),
|
|
79
|
+
...(scope.notes?.length ? ["",...scope.notes] : []),
|
|
80
|
+
...(unavailable ? ["","This command is not available yet. It returns COMMAND_UNAVAILABLE (exit code 2) without configuration, authentication or network access."] : []),
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
const commands = scope ? COMMAND_REGISTRY.filter((command) => command.path[0] === scope) : COMMAND_REGISTRY;
|
|
84
|
+
const groups = scope ? [scope] : ["General",...new Set(commands.filter((command) => command.path.length > 1).map((command) => command.path[0]))];
|
|
85
|
+
const lines = [`RequestShield CLI${scope ? ` — ${scope}` : ""}`,"","Usage:",` requestshield ${scope ? `${scope} <command>` : "<command>"} [options]`];
|
|
86
|
+
for (const group of groups) {
|
|
87
|
+
const members = commands.filter((command) => group === "General" ? command.path.length === 1 : command.path[0] === group);
|
|
88
|
+
if (!members.length) continue;
|
|
89
|
+
lines.push("",`${group === "General" ? group : `${group[0].toUpperCase()}${group.slice(1)}`} commands:`);
|
|
90
|
+
for (const command of members) {
|
|
91
|
+
const syntax = commandUsage(command);
|
|
92
|
+
lines.push(` requestshield ${syntax}${command.shortcuts?.length ? ` (${command.shortcuts.join(", ")})` : ""}${command.availability === "placeholder" ? " [coming soon]" : ""}`,` ${command.summary}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
lines.push("","Use --help after a group or command for detailed usage and examples.","Commands marked [coming soon] return COMMAND_UNAVAILABLE and perform no work.");
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { cp, lstat, mkdir, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { randomUUID } from "node:crypto";
|
|
7
6
|
import { createInterface } from "node:readline/promises";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
9
8
|
import { CliError } from "../errors.mjs";
|
|
10
9
|
import { detectAgents } from "../agent-detector.mjs";
|
|
10
|
+
import { agentSkillPath } from "./agent-status.mjs";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @param {{ agent?: "codex" | "claude", force: boolean }} options
|
|
13
|
+
* @param {{ agent?: "codex" | "claude", force: boolean, json?: boolean }} options
|
|
14
14
|
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, log: (message: string) => void, sourceDir?: string, executablePath?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
15
15
|
*/
|
|
16
16
|
export async function setupAgent(options, deps) {
|
|
17
|
-
const agent = await resolveAgent(options.agent, deps);
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
// Codex and Claude discover personal skills from different directories.
|
|
21
|
-
const skillsRoot = agent === "codex"
|
|
22
|
-
? path.join(homeDir, ".agents", "skills")
|
|
23
|
-
: path.join(homeDir, ".claude", "skills");
|
|
24
|
-
const destination = path.join(skillsRoot, "requestshield");
|
|
17
|
+
const agent = await resolveAgent(options.agent, deps, !options.json);
|
|
18
|
+
const destination = agentSkillPath(agent, deps);
|
|
19
|
+
const skillsRoot = path.dirname(destination);
|
|
25
20
|
await mkdir(skillsRoot, { recursive: true });
|
|
26
21
|
|
|
27
22
|
const exists = await pathExists(destination);
|
|
@@ -56,8 +51,13 @@ export async function setupAgent(options, deps) {
|
|
|
56
51
|
throw error;
|
|
57
52
|
}
|
|
58
53
|
const agentLabel = agent === "codex" ? "Codex" : "Claude";
|
|
59
|
-
|
|
60
|
-
deps.log(
|
|
54
|
+
const result = { agent, path: destination, status: /** @type {const} */ ("installed") };
|
|
55
|
+
if (options.json) deps.log(JSON.stringify({ data: result }, null, 2));
|
|
56
|
+
else {
|
|
57
|
+
deps.log(`Installed RequestShield skill for ${agentLabel} at ${destination}`);
|
|
58
|
+
deps.log(`Restart ${agentLabel} or start a new task before using the skill.`);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** @param {string} [executablePath] */
|
|
@@ -93,9 +93,10 @@ async function resolveSkillSource(executablePath) {
|
|
|
93
93
|
*
|
|
94
94
|
* @param {"codex" | "claude" | undefined} requested
|
|
95
95
|
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
96
|
+
* @param {boolean} allowPrompt
|
|
96
97
|
* @returns {Promise<"codex" | "claude">}
|
|
97
98
|
*/
|
|
98
|
-
async function resolveAgent(requested, deps) {
|
|
99
|
+
async function resolveAgent(requested, deps, allowPrompt) {
|
|
99
100
|
// A command-line choice is authoritative and avoids probing the machine.
|
|
100
101
|
if (requested) return requested;
|
|
101
102
|
|
|
@@ -119,6 +120,12 @@ async function resolveAgent(requested, deps) {
|
|
|
119
120
|
|
|
120
121
|
// Never choose silently when both agents are present. Interactive users get
|
|
121
122
|
// a prompt; non-interactive callers receive a stable conflict error.
|
|
123
|
+
if (!allowPrompt) {
|
|
124
|
+
throw new CliError("Multiple coding agents detected. Choose --codex or --claude explicitly when using --json.", {
|
|
125
|
+
code: "INSTALL_CONFLICT",
|
|
126
|
+
exitCode: 2,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
122
129
|
const selectAgent = deps.selectAgent ?? promptForAgent;
|
|
123
130
|
const selected = await selectAgent(detected);
|
|
124
131
|
if (!detected.includes(selected)) {
|
|
@@ -136,7 +143,7 @@ async function resolveAgent(requested, deps) {
|
|
|
136
143
|
* @returns {Promise<"codex" | "claude">}
|
|
137
144
|
*/
|
|
138
145
|
async function promptForAgent() {
|
|
139
|
-
if (process.stdin.isTTY !== true || process.
|
|
146
|
+
if (process.stdin.isTTY !== true || process.stderr.isTTY !== true) {
|
|
140
147
|
throw new CliError(
|
|
141
148
|
[
|
|
142
149
|
"Multiple coding agents detected.",
|
|
@@ -151,10 +158,10 @@ async function promptForAgent() {
|
|
|
151
158
|
|
|
152
159
|
const prompt = createInterface({
|
|
153
160
|
input: process.stdin,
|
|
154
|
-
output: process.
|
|
161
|
+
output: process.stderr,
|
|
155
162
|
});
|
|
156
163
|
try {
|
|
157
|
-
process.
|
|
164
|
+
process.stderr.write(
|
|
158
165
|
"Multiple coding agents detected.\n\n 1. Codex\n 2. Claude\n\n",
|
|
159
166
|
);
|
|
160
167
|
while (true) {
|
|
@@ -163,7 +170,7 @@ async function promptForAgent() {
|
|
|
163
170
|
.toLowerCase();
|
|
164
171
|
if (answer === "1" || answer === "codex") return "codex";
|
|
165
172
|
if (answer === "2" || answer === "claude") return "claude";
|
|
166
|
-
process.
|
|
173
|
+
process.stderr.write("Enter 1 for Codex or 2 for Claude.\n");
|
|
167
174
|
}
|
|
168
175
|
} finally {
|
|
169
176
|
prompt.close();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { lstat, readFile, stat } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
/** @typedef {"codex" | "claude"} Agent */
|
|
8
|
+
/** @typedef {{ agent: Agent, path: string, status: "installed" | "missing" | "invalid" }} AgentInstallation */
|
|
9
|
+
|
|
10
|
+
/** @param {Agent} agent @param {{ homeDir?: string }} [deps] */
|
|
11
|
+
export function agentSkillPath(agent, deps = {}) {
|
|
12
|
+
return path.join(deps.homeDir ?? os.homedir(), agent === "codex" ? ".agents" : ".claude", "skills", "requestshield");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Inspect the personal Skill installation only; this does not prove an agent
|
|
17
|
+
* has loaded the Skill, and never probes executables or creates directories.
|
|
18
|
+
* @param {Agent} agent
|
|
19
|
+
* @param {{ homeDir?: string }} [deps]
|
|
20
|
+
* @returns {Promise<AgentInstallation>}
|
|
21
|
+
*/
|
|
22
|
+
export async function inspectAgent(agent, deps = {}) {
|
|
23
|
+
const destination = agentSkillPath(agent, deps);
|
|
24
|
+
try {
|
|
25
|
+
await lstat(destination);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return { agent, path: destination, status: /** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT" ? "missing" : "invalid" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// stat follows an intentional directory symlink; broken links are invalid.
|
|
32
|
+
if (!(await stat(destination)).isDirectory()) return { agent, path: destination, status: "invalid" };
|
|
33
|
+
const contents = await readFile(path.join(destination, "SKILL.md"), "utf8");
|
|
34
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(contents);
|
|
35
|
+
const valid = frontmatter !== null
|
|
36
|
+
&& /^name:\s*(?:requestshield|"requestshield"|'requestshield')\s*$/m.test(frontmatter[1])
|
|
37
|
+
&& frontmatter[2].trim().length > 0;
|
|
38
|
+
return { agent, path: destination, status: valid ? "installed" : "invalid" };
|
|
39
|
+
} catch {
|
|
40
|
+
return { agent, path: destination, status: "invalid" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {{ agent?: Agent, json?: boolean }} options
|
|
46
|
+
* @param {{ homeDir?: string, log: (message: string) => void }} deps
|
|
47
|
+
*/
|
|
48
|
+
export async function showAgentStatus(options, deps) {
|
|
49
|
+
const agents = options.agent ? [options.agent] : /** @type {Agent[]} */ (["codex", "claude"]);
|
|
50
|
+
const result = await Promise.all(agents.map((agent) => inspectAgent(agent, deps)));
|
|
51
|
+
if (options.json) {
|
|
52
|
+
deps.log(JSON.stringify({ data: result }, null, 2));
|
|
53
|
+
} else {
|
|
54
|
+
for (const installation of result) {
|
|
55
|
+
deps.log(`${installation.agent === "codex" ? "Codex" : "Claude"}: ${installation.status} (${installation.path})`);
|
|
56
|
+
}
|
|
57
|
+
deps.log("This checks installed Skill files, not whether an agent has loaded them.");
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|