requestshield 0.1.4 → 0.1.5
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 +211 -13
- package/package.json +16 -10
- package/skills/requestshield/SKILL.md +307 -307
- package/skills/requestshield/assets/AGENTS.codex.md +62 -62
- package/skills/requestshield/references/backend-java-core.md +128 -128
- package/skills/requestshield/references/backend-spring-boot.md +145 -145
- package/skills/requestshield/references/browser-manual.md +210 -210
- package/skills/requestshield/references/browser-seamless.md +164 -164
- package/skills/requestshield/references/cli.md +183 -182
- package/skills/requestshield/references/integration-planning.md +389 -389
- package/skills/requestshield/references/troubleshooting.md +118 -118
- package/src/agent-detector.mjs +102 -74
- package/src/api-client.mjs +100 -5
- package/src/args.mjs +182 -79
- package/src/cli.mjs +255 -51
- package/src/commands/agent-setup.mjs +185 -185
- package/src/commands/apps-get.mjs +64 -0
- package/src/commands/apps-list.mjs +90 -0
- package/src/commands/billing-get.mjs +110 -0
- package/src/commands/challenge-volume.mjs +81 -0
- package/src/commands/contract.mjs +106 -0
- package/src/config.mjs +8 -0
- package/src/main.mjs +24 -24
|
@@ -1,185 +1,185 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
import { cp, lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { createInterface } from "node:readline/promises";
|
|
8
|
-
import { CliError } from "../errors.mjs";
|
|
9
|
-
import { detectAgents } from "../agent-detector.mjs";
|
|
10
|
-
import { getAsset, isSea } from "node:sea";
|
|
11
|
-
|
|
12
|
-
const skillAsset = "requestshield-skill.md";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {{ agent?: "codex" | "claude", force: boolean }} options
|
|
16
|
-
* @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
|
|
17
|
-
*/
|
|
18
|
-
export async function setupAgent(options, deps) {
|
|
19
|
-
const agent = await resolveAgent(options.agent, deps);
|
|
20
|
-
const homeDir = deps.homeDir ?? os.homedir();
|
|
21
|
-
|
|
22
|
-
// Codex and Claude discover personal skills from different directories.
|
|
23
|
-
const skillsRoot = agent === "codex"
|
|
24
|
-
? path.join(homeDir, ".agents", "skills")
|
|
25
|
-
: path.join(homeDir, ".claude", "skills");
|
|
26
|
-
const destination = path.join(skillsRoot, "requestshield");
|
|
27
|
-
await mkdir(skillsRoot, { recursive: true });
|
|
28
|
-
|
|
29
|
-
const exists = await pathExists(destination);
|
|
30
|
-
if (exists && !options.force) {
|
|
31
|
-
throw new CliError(`RequestShield skill already exists at ${destination}; use --force to replace it`, {
|
|
32
|
-
code: "SKILL_EXISTS",
|
|
33
|
-
exitCode: 2,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Copy into a staging directory so an interrupted copy cannot leave a
|
|
38
|
-
// partially installed skill at the final destination.
|
|
39
|
-
const staging = path.join(skillsRoot, `.requestshield-${randomUUID()}.tmp`);
|
|
40
|
-
const backup = `${destination}.${randomUUID()}.backup`;
|
|
41
|
-
try {
|
|
42
|
-
if (deps.sourceDir) {
|
|
43
|
-
// Tests and alternate packagers can provide an explicit skill source.
|
|
44
|
-
await cp(deps.sourceDir, staging, {
|
|
45
|
-
recursive: true,
|
|
46
|
-
errorOnExist: true,
|
|
47
|
-
});
|
|
48
|
-
} else if (isSea()) {
|
|
49
|
-
// A single-executable build reads the skill from its embedded assets.
|
|
50
|
-
await mkdir(staging);
|
|
51
|
-
|
|
52
|
-
await writeFile(
|
|
53
|
-
path.join(staging, "SKILL.md"),
|
|
54
|
-
Buffer.from(getAsset(skillAsset)),
|
|
55
|
-
);
|
|
56
|
-
} else {
|
|
57
|
-
// Source, npm, and on-disk CJS builds keep the skill beside the program.
|
|
58
|
-
//
|
|
59
|
-
// src/main.mjs -> ../skills/requestshield
|
|
60
|
-
// dist/requestshield.cjs -> ../skills/requestshield
|
|
61
|
-
// npm link and global npm installs expose a symlink in their bin folder.
|
|
62
|
-
// Follow it before resolving the adjacent packaged skills directory.
|
|
63
|
-
const executable = await realpath(deps.executablePath ?? process.argv[1]);
|
|
64
|
-
const executableDir = path.dirname(executable);
|
|
65
|
-
const source = path.resolve(
|
|
66
|
-
executableDir,
|
|
67
|
-
"../skills/requestshield",
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
await cp(source, staging, {
|
|
71
|
-
recursive: true,
|
|
72
|
-
errorOnExist: true,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Keep the old installation recoverable until the staged copy is in place.
|
|
77
|
-
if (exists) await rename(destination, backup);
|
|
78
|
-
await rename(staging, destination);
|
|
79
|
-
if (exists) await rm(backup, { recursive: true, force: true });
|
|
80
|
-
} catch (error) {
|
|
81
|
-
await rm(staging, { recursive: true, force: true });
|
|
82
|
-
if (await pathExists(backup)) {
|
|
83
|
-
await rm(destination, { recursive: true, force: true });
|
|
84
|
-
await rename(backup, destination);
|
|
85
|
-
}
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
const agentLabel = agent === "codex" ? "Codex" : "Claude";
|
|
89
|
-
deps.log(`Installed RequestShield skill for ${agentLabel} at ${destination}`);
|
|
90
|
-
deps.log(`Restart ${agentLabel} or start a new task before using the skill.`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Explicit selection bypasses detection. Automatic setup selects a single
|
|
95
|
-
* detected agent or asks the user when both agents are present.
|
|
96
|
-
*
|
|
97
|
-
* @param {"codex" | "claude" | undefined} requested
|
|
98
|
-
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
99
|
-
* @returns {Promise<"codex" | "claude">}
|
|
100
|
-
*/
|
|
101
|
-
async function resolveAgent(requested, deps) {
|
|
102
|
-
// A command-line choice is authoritative and avoids probing the machine.
|
|
103
|
-
if (requested) return requested;
|
|
104
|
-
|
|
105
|
-
const detected = deps.detectAgents
|
|
106
|
-
? await deps.detectAgents()
|
|
107
|
-
: await detectAgents({ env: deps.env, homeDir: deps.homeDir });
|
|
108
|
-
|
|
109
|
-
if (detected.length === 1) return detected[0];
|
|
110
|
-
if (detected.length === 0) {
|
|
111
|
-
throw new CliError(
|
|
112
|
-
[
|
|
113
|
-
"No supported coding agent detected.",
|
|
114
|
-
"",
|
|
115
|
-
"Choose one explicitly:",
|
|
116
|
-
" requestshield agent setup --codex",
|
|
117
|
-
" requestshield agent setup --claude",
|
|
118
|
-
].join("\n"),
|
|
119
|
-
{ code: "AGENT_NOT_FOUND", exitCode: 2 },
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Never choose silently when both agents are present. Interactive users get
|
|
124
|
-
// a prompt; non-interactive callers receive a stable conflict error.
|
|
125
|
-
const selectAgent = deps.selectAgent ?? promptForAgent;
|
|
126
|
-
const selected = await selectAgent(detected);
|
|
127
|
-
if (!detected.includes(selected)) {
|
|
128
|
-
throw new CliError(`Selected agent is not available: ${selected}`, {
|
|
129
|
-
code: "INVALID_AGENT",
|
|
130
|
-
exitCode: 2,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return selected;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Ask an interactive user which detected agent should receive the skill.
|
|
138
|
-
*
|
|
139
|
-
* @returns {Promise<"codex" | "claude">}
|
|
140
|
-
*/
|
|
141
|
-
async function promptForAgent() {
|
|
142
|
-
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
143
|
-
throw new CliError(
|
|
144
|
-
[
|
|
145
|
-
"Multiple coding agents detected.",
|
|
146
|
-
"",
|
|
147
|
-
"Choose one explicitly:",
|
|
148
|
-
" requestshield agent setup --codex",
|
|
149
|
-
" requestshield agent setup --claude",
|
|
150
|
-
].join("\n"),
|
|
151
|
-
{ code: "INSTALL_CONFLICT", exitCode: 2 },
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const prompt = createInterface({
|
|
156
|
-
input: process.stdin,
|
|
157
|
-
output: process.stdout,
|
|
158
|
-
});
|
|
159
|
-
try {
|
|
160
|
-
process.stdout.write(
|
|
161
|
-
"Multiple coding agents detected.\n\n 1. Codex\n 2. Claude\n\n",
|
|
162
|
-
);
|
|
163
|
-
while (true) {
|
|
164
|
-
const answer = (await prompt.question("Select an agent [1-2]: "))
|
|
165
|
-
.trim()
|
|
166
|
-
.toLowerCase();
|
|
167
|
-
if (answer === "1" || answer === "codex") return "codex";
|
|
168
|
-
if (answer === "2" || answer === "claude") return "claude";
|
|
169
|
-
process.stdout.write("Enter 1 for Codex or 2 for Claude.\n");
|
|
170
|
-
}
|
|
171
|
-
} finally {
|
|
172
|
-
prompt.close();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** @param {string} target */
|
|
177
|
-
async function pathExists(target) {
|
|
178
|
-
try {
|
|
179
|
-
await lstat(target);
|
|
180
|
-
return true;
|
|
181
|
-
} catch (error) {
|
|
182
|
-
if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") return false;
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { cp, lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { CliError } from "../errors.mjs";
|
|
9
|
+
import { detectAgents } from "../agent-detector.mjs";
|
|
10
|
+
import { getAsset, isSea } from "node:sea";
|
|
11
|
+
|
|
12
|
+
const skillAsset = "requestshield-skill.md";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ agent?: "codex" | "claude", force: boolean }} options
|
|
16
|
+
* @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
|
|
17
|
+
*/
|
|
18
|
+
export async function setupAgent(options, deps) {
|
|
19
|
+
const agent = await resolveAgent(options.agent, deps);
|
|
20
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
21
|
+
|
|
22
|
+
// Codex and Claude discover personal skills from different directories.
|
|
23
|
+
const skillsRoot = agent === "codex"
|
|
24
|
+
? path.join(homeDir, ".agents", "skills")
|
|
25
|
+
: path.join(homeDir, ".claude", "skills");
|
|
26
|
+
const destination = path.join(skillsRoot, "requestshield");
|
|
27
|
+
await mkdir(skillsRoot, { recursive: true });
|
|
28
|
+
|
|
29
|
+
const exists = await pathExists(destination);
|
|
30
|
+
if (exists && !options.force) {
|
|
31
|
+
throw new CliError(`RequestShield skill already exists at ${destination}; use --force to replace it`, {
|
|
32
|
+
code: "SKILL_EXISTS",
|
|
33
|
+
exitCode: 2,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Copy into a staging directory so an interrupted copy cannot leave a
|
|
38
|
+
// partially installed skill at the final destination.
|
|
39
|
+
const staging = path.join(skillsRoot, `.requestshield-${randomUUID()}.tmp`);
|
|
40
|
+
const backup = `${destination}.${randomUUID()}.backup`;
|
|
41
|
+
try {
|
|
42
|
+
if (deps.sourceDir) {
|
|
43
|
+
// Tests and alternate packagers can provide an explicit skill source.
|
|
44
|
+
await cp(deps.sourceDir, staging, {
|
|
45
|
+
recursive: true,
|
|
46
|
+
errorOnExist: true,
|
|
47
|
+
});
|
|
48
|
+
} else if (isSea()) {
|
|
49
|
+
// A single-executable build reads the skill from its embedded assets.
|
|
50
|
+
await mkdir(staging);
|
|
51
|
+
|
|
52
|
+
await writeFile(
|
|
53
|
+
path.join(staging, "SKILL.md"),
|
|
54
|
+
Buffer.from(getAsset(skillAsset)),
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
// Source, npm, and on-disk CJS builds keep the skill beside the program.
|
|
58
|
+
//
|
|
59
|
+
// src/main.mjs -> ../skills/requestshield
|
|
60
|
+
// dist/requestshield.cjs -> ../skills/requestshield
|
|
61
|
+
// npm link and global npm installs expose a symlink in their bin folder.
|
|
62
|
+
// Follow it before resolving the adjacent packaged skills directory.
|
|
63
|
+
const executable = await realpath(deps.executablePath ?? process.argv[1]);
|
|
64
|
+
const executableDir = path.dirname(executable);
|
|
65
|
+
const source = path.resolve(
|
|
66
|
+
executableDir,
|
|
67
|
+
"../skills/requestshield",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await cp(source, staging, {
|
|
71
|
+
recursive: true,
|
|
72
|
+
errorOnExist: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Keep the old installation recoverable until the staged copy is in place.
|
|
77
|
+
if (exists) await rename(destination, backup);
|
|
78
|
+
await rename(staging, destination);
|
|
79
|
+
if (exists) await rm(backup, { recursive: true, force: true });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
await rm(staging, { recursive: true, force: true });
|
|
82
|
+
if (await pathExists(backup)) {
|
|
83
|
+
await rm(destination, { recursive: true, force: true });
|
|
84
|
+
await rename(backup, destination);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
const agentLabel = agent === "codex" ? "Codex" : "Claude";
|
|
89
|
+
deps.log(`Installed RequestShield skill for ${agentLabel} at ${destination}`);
|
|
90
|
+
deps.log(`Restart ${agentLabel} or start a new task before using the skill.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Explicit selection bypasses detection. Automatic setup selects a single
|
|
95
|
+
* detected agent or asks the user when both agents are present.
|
|
96
|
+
*
|
|
97
|
+
* @param {"codex" | "claude" | undefined} requested
|
|
98
|
+
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
99
|
+
* @returns {Promise<"codex" | "claude">}
|
|
100
|
+
*/
|
|
101
|
+
async function resolveAgent(requested, deps) {
|
|
102
|
+
// A command-line choice is authoritative and avoids probing the machine.
|
|
103
|
+
if (requested) return requested;
|
|
104
|
+
|
|
105
|
+
const detected = deps.detectAgents
|
|
106
|
+
? await deps.detectAgents()
|
|
107
|
+
: await detectAgents({ env: deps.env, homeDir: deps.homeDir });
|
|
108
|
+
|
|
109
|
+
if (detected.length === 1) return detected[0];
|
|
110
|
+
if (detected.length === 0) {
|
|
111
|
+
throw new CliError(
|
|
112
|
+
[
|
|
113
|
+
"No supported coding agent detected.",
|
|
114
|
+
"",
|
|
115
|
+
"Choose one explicitly:",
|
|
116
|
+
" requestshield agent setup --codex",
|
|
117
|
+
" requestshield agent setup --claude",
|
|
118
|
+
].join("\n"),
|
|
119
|
+
{ code: "AGENT_NOT_FOUND", exitCode: 2 },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Never choose silently when both agents are present. Interactive users get
|
|
124
|
+
// a prompt; non-interactive callers receive a stable conflict error.
|
|
125
|
+
const selectAgent = deps.selectAgent ?? promptForAgent;
|
|
126
|
+
const selected = await selectAgent(detected);
|
|
127
|
+
if (!detected.includes(selected)) {
|
|
128
|
+
throw new CliError(`Selected agent is not available: ${selected}`, {
|
|
129
|
+
code: "INVALID_AGENT",
|
|
130
|
+
exitCode: 2,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return selected;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ask an interactive user which detected agent should receive the skill.
|
|
138
|
+
*
|
|
139
|
+
* @returns {Promise<"codex" | "claude">}
|
|
140
|
+
*/
|
|
141
|
+
async function promptForAgent() {
|
|
142
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
143
|
+
throw new CliError(
|
|
144
|
+
[
|
|
145
|
+
"Multiple coding agents detected.",
|
|
146
|
+
"",
|
|
147
|
+
"Choose one explicitly:",
|
|
148
|
+
" requestshield agent setup --codex",
|
|
149
|
+
" requestshield agent setup --claude",
|
|
150
|
+
].join("\n"),
|
|
151
|
+
{ code: "INSTALL_CONFLICT", exitCode: 2 },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const prompt = createInterface({
|
|
156
|
+
input: process.stdin,
|
|
157
|
+
output: process.stdout,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
process.stdout.write(
|
|
161
|
+
"Multiple coding agents detected.\n\n 1. Codex\n 2. Claude\n\n",
|
|
162
|
+
);
|
|
163
|
+
while (true) {
|
|
164
|
+
const answer = (await prompt.question("Select an agent [1-2]: "))
|
|
165
|
+
.trim()
|
|
166
|
+
.toLowerCase();
|
|
167
|
+
if (answer === "1" || answer === "codex") return "codex";
|
|
168
|
+
if (answer === "2" || answer === "claude") return "claude";
|
|
169
|
+
process.stdout.write("Enter 1 for Codex or 2 for Claude.\n");
|
|
170
|
+
}
|
|
171
|
+
} finally {
|
|
172
|
+
prompt.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @param {string} target */
|
|
177
|
+
async function pathExists(target) {
|
|
178
|
+
try {
|
|
179
|
+
await lstat(target);
|
|
180
|
+
return true;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") return false;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { CliError } from "../errors.mjs";
|
|
4
|
+
import { objectString } from "../api-client.mjs";
|
|
5
|
+
|
|
6
|
+
const APPLICATION_STATUSES = new Set(["ready", "active", "deactivated"]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{ appKey: string }} options
|
|
10
|
+
* @param {{ api: { getApp(accessToken: string, appKey: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
|
|
11
|
+
*/
|
|
12
|
+
export async function getApp(options, deps) {
|
|
13
|
+
const accessToken =
|
|
14
|
+
await deps.sessions.loadToken();
|
|
15
|
+
|
|
16
|
+
const result =
|
|
17
|
+
await deps.api.getApp(accessToken, options.appKey);
|
|
18
|
+
|
|
19
|
+
const app =
|
|
20
|
+
parseApp(result.body, options.appKey);
|
|
21
|
+
|
|
22
|
+
deps.log(JSON.stringify({ ok: true, data: app }, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {unknown} body @param {string} requestedAppKey */
|
|
26
|
+
function parseApp(body, requestedAppKey) {
|
|
27
|
+
const ok = body && typeof body === "object" ?
|
|
28
|
+
Reflect.get(body, "ok") : undefined;
|
|
29
|
+
|
|
30
|
+
const data = body && typeof body === "object" ?
|
|
31
|
+
Reflect.get(body, "data") : undefined;
|
|
32
|
+
|
|
33
|
+
if (ok !== true || !data || typeof data !== "object" || Array.isArray(data)) {
|
|
34
|
+
throw invalidResponse(
|
|
35
|
+
"The application response did not contain ok=true and data"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const appKey = objectString(data, "app_key");
|
|
40
|
+
const appName = objectString(data, "name");
|
|
41
|
+
const status = objectString(data, "status");
|
|
42
|
+
|
|
43
|
+
if (appKey !== requestedAppKey) {
|
|
44
|
+
throw invalidResponse("The application response did not match the requested App Key");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!appName) {
|
|
48
|
+
throw invalidResponse("The application response did not contain name");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!status || !APPLICATION_STATUSES.has(status)) {
|
|
52
|
+
throw invalidResponse(
|
|
53
|
+
"The application response status must be ready, active, or deactivated",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Construct a new object so unexpected response fields, especially secrets, cannot be printed.
|
|
58
|
+
return { app_key: appKey, name: appName, status };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @param {string} message */
|
|
62
|
+
function invalidResponse(message) {
|
|
63
|
+
return new CliError(message, { code: "INVALID_RESPONSE" });
|
|
64
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { CliError } from "../errors.mjs";
|
|
4
|
+
import { objectString } from "../api-client.mjs";
|
|
5
|
+
|
|
6
|
+
const HEADERS = ["APP KEY", "NAME", "STATUS"];
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ appKey: string, name: string, status: string, createdAt: string, updatedAt: string }} App */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ json: boolean }} options
|
|
12
|
+
* @param {{ api: { listApps(accessToken: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
|
|
13
|
+
*/
|
|
14
|
+
export async function listApps(options, deps) {
|
|
15
|
+
const accessToken =
|
|
16
|
+
await deps.sessions.loadToken();
|
|
17
|
+
|
|
18
|
+
const result =
|
|
19
|
+
await deps.api.listApps(accessToken);
|
|
20
|
+
|
|
21
|
+
const { data: apps, nextCursor } = parseApps(result.body);
|
|
22
|
+
|
|
23
|
+
if (options.json) {
|
|
24
|
+
deps.log(JSON.stringify({ data: apps, nextCursor }, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (apps.length === 0) {
|
|
29
|
+
deps.log("No applications are available to this account.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const line of formatTable(apps)) deps.log(line);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {unknown} body @returns {{ data: App[], nextCursor: string | null }} */
|
|
37
|
+
function parseApps(body) {
|
|
38
|
+
const data = body && typeof body === "object" ?
|
|
39
|
+
Reflect.get(body, "data") : undefined;
|
|
40
|
+
|
|
41
|
+
const nextCursor = body && typeof body === "object" ?
|
|
42
|
+
Reflect.get(body, "nextCursor") : undefined;
|
|
43
|
+
|
|
44
|
+
if (!Array.isArray(data)) {
|
|
45
|
+
throw new CliError("The applications response did not contain a data array", {
|
|
46
|
+
code: "INVALID_RESPONSE",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (nextCursor !== null && (typeof nextCursor !== "string" || nextCursor.length === 0)) {
|
|
51
|
+
throw new CliError("The applications response contained an invalid nextCursor", {
|
|
52
|
+
code: "INVALID_RESPONSE",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Rebuild every item from approved metadata so unexpected API fields never reach output.
|
|
57
|
+
const apps = data.map((entry, index) => {
|
|
58
|
+
const appKey = objectString(entry, "appKey");
|
|
59
|
+
const name = objectString(entry, "name");
|
|
60
|
+
const status = objectString(entry, "status");
|
|
61
|
+
const createdAt = objectString(entry, "createdAt");
|
|
62
|
+
const updatedAt = objectString(entry, "updatedAt");
|
|
63
|
+
|
|
64
|
+
if (!appKey || !name || !status || !createdAt || !updatedAt) {
|
|
65
|
+
throw new CliError(
|
|
66
|
+
`Application at position ${index + 1} is missing appKey, name, status, createdAt, or updatedAt`,
|
|
67
|
+
{ code: "INVALID_RESPONSE" },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { appKey, name, status, createdAt, updatedAt };
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return { data: apps, nextCursor };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @param {App[]} apps @returns {string[]} */
|
|
78
|
+
function formatTable(apps) {
|
|
79
|
+
const rows = [HEADERS, ...apps.map((app) =>
|
|
80
|
+
[app.appKey, app.name, app.status])];
|
|
81
|
+
|
|
82
|
+
const widths = HEADERS.map((_, column) =>
|
|
83
|
+
Math.max(...rows.map((row) => row[column].length)));
|
|
84
|
+
|
|
85
|
+
return rows.map((row) =>
|
|
86
|
+
row
|
|
87
|
+
.map((cell, column) => (column === HEADERS.length - 1 ? cell : cell.padEnd(widths[column])))
|
|
88
|
+
.join(" "),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { CliError } from "../errors.mjs";
|
|
4
|
+
import { objectString } from "../api-client.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{ appKey: string }} options
|
|
8
|
+
* @param {{ api: { getBilling(accessToken: string, appKey: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
|
|
9
|
+
*/
|
|
10
|
+
export async function showBilling(options, deps) {
|
|
11
|
+
const accessToken =
|
|
12
|
+
await deps.sessions.loadToken();
|
|
13
|
+
|
|
14
|
+
const result =
|
|
15
|
+
await deps.api.getBilling(accessToken, options.appKey);
|
|
16
|
+
|
|
17
|
+
const billing = parseBilling(result.body);
|
|
18
|
+
|
|
19
|
+
deps.log(JSON.stringify({ ok: true, data: billing }, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @param {unknown} body */
|
|
23
|
+
function parseBilling(body) {
|
|
24
|
+
const ok = body && typeof body === "object" ?
|
|
25
|
+
Reflect.get(body, "ok") : undefined;
|
|
26
|
+
|
|
27
|
+
const data = objectValue(body, "data");
|
|
28
|
+
|
|
29
|
+
const plan = objectValue(data, "plan");
|
|
30
|
+
|
|
31
|
+
const usage = objectValue(data, "usage");
|
|
32
|
+
|
|
33
|
+
if (ok !== true || !data || !plan || !usage) {
|
|
34
|
+
throw invalidResponse("The billing response did not contain ok=true, data.plan, and data.usage");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tier = objectString(plan, "tier");
|
|
38
|
+
const currentCycle = Reflect.get(plan, "current_cycle");
|
|
39
|
+
const billing = Reflect.get(plan, "billing");
|
|
40
|
+
const nextCharge = objectString(plan, "next_charge");
|
|
41
|
+
const monthlyQuota = Reflect.get(usage, "monthly_quota");
|
|
42
|
+
const currentUsage = Reflect.get(usage, "current_usage");
|
|
43
|
+
const overageCharge = Reflect.get(usage, "overage_charge");
|
|
44
|
+
|
|
45
|
+
if (!tier) throw invalidResponse("The billing response did not contain plan.tier");
|
|
46
|
+
if (
|
|
47
|
+
!Array.isArray(currentCycle)
|
|
48
|
+
|| currentCycle.length !== 2
|
|
49
|
+
|| !currentCycle.every((value) =>
|
|
50
|
+
typeof value === "string" && isIsoDateTime(value))
|
|
51
|
+
|| Date.parse(currentCycle[0]) > Date.parse(currentCycle[1])
|
|
52
|
+
) {
|
|
53
|
+
throw invalidResponse("The billing response contained an invalid plan.current_cycle");
|
|
54
|
+
}
|
|
55
|
+
if (!nonNegativeNumber(billing)) {
|
|
56
|
+
throw invalidResponse("The billing response contained an invalid plan.billing");
|
|
57
|
+
}
|
|
58
|
+
if (!nextCharge || !isIsoDateTime(nextCharge)) {
|
|
59
|
+
throw invalidResponse("The billing response contained an invalid plan.next_charge");
|
|
60
|
+
}
|
|
61
|
+
if (!nonNegativeInteger(monthlyQuota) || !nonNegativeInteger(currentUsage)) {
|
|
62
|
+
throw invalidResponse("The billing response contained invalid quota or usage values");
|
|
63
|
+
}
|
|
64
|
+
if (!nonNegativeNumber(overageCharge)) {
|
|
65
|
+
throw invalidResponse("The billing response contained an invalid usage.overage_charge");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Rebuild nested output so unexpected response fields cannot reach the terminal.
|
|
69
|
+
return {
|
|
70
|
+
plan: {
|
|
71
|
+
tier,
|
|
72
|
+
current_cycle: currentCycle,
|
|
73
|
+
billing,
|
|
74
|
+
next_charge: nextCharge,
|
|
75
|
+
},
|
|
76
|
+
usage: {
|
|
77
|
+
monthly_quota: monthlyQuota,
|
|
78
|
+
current_usage: currentUsage,
|
|
79
|
+
overage_charge: overageCharge,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @param {unknown} value @param {string} property */
|
|
85
|
+
function objectValue(value, property) {
|
|
86
|
+
if (!value || typeof value !== "object") return undefined;
|
|
87
|
+
const found = Reflect.get(value, property);
|
|
88
|
+
return found && typeof found === "object" && !Array.isArray(found) ? found : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @param {unknown} value */
|
|
92
|
+
function nonNegativeNumber(value) {
|
|
93
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {unknown} value */
|
|
97
|
+
function nonNegativeInteger(value) {
|
|
98
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @param {string} value */
|
|
102
|
+
function isIsoDateTime(value) {
|
|
103
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})?$/.test(value)
|
|
104
|
+
&& Number.isFinite(Date.parse(value));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {string} message */
|
|
108
|
+
function invalidResponse(message) {
|
|
109
|
+
return new CliError(message, { code: "INVALID_RESPONSE" });
|
|
110
|
+
}
|