requestshield 0.1.3 → 0.1.4

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.
@@ -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,139 @@
1
+ // @ts-check
2
+
3
+ import readline from "node:readline/promises";
4
+ import { spawn } from "node:child_process";
5
+ import { stdin, stdout } from "node:process";
6
+ import { CliError } from "../errors.mjs";
7
+
8
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
9
+
10
+ /**
11
+ * @param {{ currentVersion: string, packageName: string, log: (message: string) => void, fetchImpl?: typeof fetch, confirm?: () => Promise<boolean>, install?: (packageName: string, version: string) => Promise<void> }} options
12
+ */
13
+ export async function checkForUpdate(options) {
14
+ const latestVersion = await fetchLatestVersion(
15
+ options.packageName,
16
+ options.fetchImpl ?? fetch,
17
+ );
18
+
19
+ options.log(`Current version: ${options.currentVersion}`);
20
+ options.log(`Latest version: ${latestVersion}`);
21
+
22
+ if (compareVersions(latestVersion, options.currentVersion) <= 0) {
23
+ options.log("RequestShield is already up to date.");
24
+ return;
25
+ }
26
+
27
+ const accepted = await (options.confirm ?? confirmUpdate)(latestVersion);
28
+ if (!accepted) {
29
+ options.log("Update cancelled.");
30
+ return;
31
+ }
32
+
33
+ options.log(`Updating RequestShield to ${latestVersion}...`);
34
+ await (options.install ?? installWithNpm)(options.packageName, latestVersion);
35
+ options.log(`RequestShield ${latestVersion} was installed successfully.`);
36
+ options.log("Open a new terminal before running RequestShield again.");
37
+ }
38
+
39
+ /** @param {string} packageName @param {typeof fetch} fetchImpl */
40
+ async function fetchLatestVersion(packageName, fetchImpl) {
41
+ const url = `${DEFAULT_REGISTRY}/${encodeURIComponent(packageName)}/latest`;
42
+ let response;
43
+ try {
44
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(15_000) });
45
+ } catch (error) {
46
+ throw new CliError(`Could not check the latest RequestShield version: ${messageOf(error)}`, {
47
+ code: "UPDATE_CHECK_FAILED",
48
+ exitCode: 7,
49
+ });
50
+ }
51
+
52
+ if (!response.ok) {
53
+ throw new CliError(`Could not check the latest RequestShield version: HTTP ${response.status}`, {
54
+ code: "UPDATE_CHECK_FAILED",
55
+ exitCode: 7,
56
+ });
57
+ }
58
+
59
+ const body = await response.json();
60
+ const version = body && typeof body === "object" ? Reflect.get(body, "version") : undefined;
61
+ if (typeof version !== "string" || !parseVersion(version)) {
62
+ throw new CliError("The npm registry returned an invalid RequestShield version", {
63
+ code: "INVALID_UPDATE_RESPONSE",
64
+ });
65
+ }
66
+ return version;
67
+ }
68
+
69
+ /** @param {string} latestVersion */
70
+ async function confirmUpdate(latestVersion) {
71
+ if (!stdin.isTTY || !stdout.isTTY) {
72
+ throw new CliError(`RequestShield ${latestVersion} is available, but confirmation requires an interactive terminal`, {
73
+ code: "UPDATE_CONFIRMATION_REQUIRED",
74
+ exitCode: 2,
75
+ });
76
+ }
77
+ const prompt = readline.createInterface({ input: stdin, output: stdout });
78
+ try {
79
+ const answer = await prompt.question(`Update to RequestShield ${latestVersion}? (y/N): `);
80
+ return answer.trim().toLowerCase() === "y";
81
+ } finally {
82
+ prompt.close();
83
+ }
84
+ }
85
+
86
+ /** @param {string} packageName @param {string} version */
87
+ async function installWithNpm(packageName, version) {
88
+ await new Promise((resolve, reject) => {
89
+ const child = spawn("npm", ["install", "--global", `${packageName}@${version}`], {
90
+ shell: process.platform === "win32",
91
+ stdio: "inherit",
92
+ windowsHide: true,
93
+ });
94
+ child.once("error", (error) => {
95
+ reject(new CliError(`Could not start npm: ${error.message}`, {
96
+ code: "UPDATE_FAILED",
97
+ exitCode: 1,
98
+ }));
99
+ });
100
+ child.once("exit", (code) => {
101
+ if (code === 0) resolve(undefined);
102
+ else reject(new CliError(`npm update failed with exit code ${code ?? "unknown"}`, {
103
+ code: "UPDATE_FAILED",
104
+ exitCode: 1,
105
+ }));
106
+ });
107
+ });
108
+ }
109
+
110
+ /** @param {string} left @param {string} right */
111
+ function compareVersions(left, right) {
112
+ const a = parseVersion(left);
113
+ const b = parseVersion(right);
114
+ if (!a || !b) throw new CliError("RequestShield has an invalid semantic version");
115
+ for (let index = 0; index < 3; index++) {
116
+ if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] - b.numbers[index];
117
+ }
118
+ if (a.prerelease === b.prerelease) return 0;
119
+ if (a.prerelease === undefined) return 1;
120
+ if (b.prerelease === undefined) return -1;
121
+ return a.prerelease.localeCompare(b.prerelease, "en", { numeric: true });
122
+ }
123
+
124
+ /** @param {string} value */
125
+ function parseVersion(value) {
126
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
127
+ if (!match) return undefined;
128
+ return {
129
+ numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
130
+ prerelease: match[4],
131
+ };
132
+ }
133
+
134
+ /** @param {unknown} error */
135
+ function messageOf(error) {
136
+ return error instanceof Error ? error.message : String(error);
137
+ }
138
+
139
+ export { compareVersions };
package/src/main.mjs CHANGED
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
- // @ts-check
3
-
4
- import { run } from "./cli.mjs";
5
- import { CliError } from "./errors.mjs";
6
-
7
- async function main() {
8
- try {
9
- await run(process.argv.slice(2));
10
- } catch (error) {
11
- const message =
12
- error instanceof Error
13
- ? error.message
14
- : String(error);
15
-
16
- console.error(`requestshield: ${message}`);
17
-
18
- process.exitCode =
19
- error instanceof CliError
20
- ? error.exitCode
21
- : 1;
22
- }
23
- }
24
-
25
- void main();
2
+ // @ts-check
3
+
4
+ import { run } from "./cli.mjs";
5
+ import { CliError } from "./errors.mjs";
6
+
7
+ async function main() {
8
+ try {
9
+ await run(process.argv.slice(2));
10
+ } catch (error) {
11
+ const message =
12
+ error instanceof Error
13
+ ? error.message
14
+ : String(error);
15
+
16
+ console.error(`requestshield: ${message}`);
17
+
18
+ process.exitCode =
19
+ error instanceof CliError
20
+ ? error.exitCode
21
+ : 1;
22
+ }
23
+ }
24
+
25
+ void main();