@testsmith/api-spector 0.2.0 → 0.2.1

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/bin/cli.js CHANGED
@@ -8,7 +8,7 @@ const [, , cmd = 'ui', ...rest] = process.argv
8
8
 
9
9
  function printHelp() {
10
10
  console.log('')
11
- console.log(' api Spector — local-first API testing tool')
11
+ console.log(' API Spector — local-first API testing tool')
12
12
  console.log('')
13
13
  console.log(' Usage:')
14
14
  console.log(' api-spector ui Launch the app')
@@ -17,6 +17,9 @@ function printHelp() {
17
17
  console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
18
18
  console.log('')
19
19
  console.log(' Options:')
20
+ console.log(' api-spector agents init <name> Initialize AI agent files')
21
+ console.log(' api-spector agents list Show available agents')
22
+ console.log('')
20
23
  console.log(' api-spector run --help Show run options')
21
24
  console.log(' api-spector mock --help Show mock options')
22
25
  console.log(' api-spector record --help Show record options')
@@ -59,8 +62,15 @@ if (cmd === '--help' || cmd === '-h') {
59
62
  env: process.env,
60
63
  })
61
64
  proc.on('close', code => process.exit(code ?? 0))
65
+ } else if (cmd === 'agents') {
66
+ const agentsPath = path.join(__dirname, '..', 'out', 'main', 'agents.js')
67
+ const proc = spawn(process.execPath, [agentsPath, ...rest], {
68
+ stdio: 'inherit',
69
+ env: process.env,
70
+ })
71
+ proc.on('close', code => process.exit(code ?? 0))
62
72
  } else {
63
- console.error(`api Spector — unknown command: "${cmd}"`)
73
+ console.error(`API Spector — unknown command: "${cmd}"`)
64
74
  printHelp()
65
75
  process.exit(1)
66
76
  }
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const promises = require("fs/promises");
4
+ const path = require("path");
5
+ const C = {
6
+ reset: "\x1B[0m",
7
+ bold: "\x1B[1m",
8
+ green: "\x1B[32m",
9
+ cyan: "\x1B[36m",
10
+ yellow: "\x1B[33m",
11
+ gray: "\x1B[90m",
12
+ red: "\x1B[31m"
13
+ };
14
+ function color(text, ...codes) {
15
+ return codes.join("") + text + C.reset;
16
+ }
17
+ const AGENTS = {
18
+ claude: {
19
+ name: "Claude Code",
20
+ description: "Skills for Claude Code (.claude/skills/)",
21
+ files: [
22
+ { src: "claude/skills/api-spector-functional-tests.md", dest: ".claude/skills/api-spector-functional-tests/SKILL.md" },
23
+ { src: "claude/skills/api-spector-security-tests.md", dest: ".claude/skills/api-spector-security-tests/SKILL.md" },
24
+ { src: "claude/skills/api-spector-generate-mocks.md", dest: ".claude/skills/api-spector-generate-mocks/SKILL.md" },
25
+ { src: "claude/skills/api-spector-api-audit.md", dest: ".claude/skills/api-spector-api-audit/SKILL.md" }
26
+ ]
27
+ },
28
+ copilot: {
29
+ name: "GitHub Copilot",
30
+ description: "Instructions for Copilot (.github/copilot-instructions.md)",
31
+ files: [
32
+ { src: "copilot/copilot-instructions.md", dest: ".github/copilot-instructions.md" }
33
+ ]
34
+ },
35
+ cursor: {
36
+ name: "Cursor",
37
+ description: "Rules for Cursor (.cursor/rules/api-spector.mdc)",
38
+ files: [
39
+ { src: "cursor/rules/api-spector.mdc", dest: ".cursor/rules/api-spector.mdc" }
40
+ ]
41
+ },
42
+ windsurf: {
43
+ name: "Windsurf",
44
+ description: "Rules for Windsurf (.windsurfrules)",
45
+ files: [
46
+ { src: "windsurf/windsurfrules", dest: ".windsurfrules" }
47
+ ]
48
+ },
49
+ aider: {
50
+ name: "Aider",
51
+ description: "Conventions for Aider (conventions.md)",
52
+ files: [
53
+ { src: "aider/conventions.md", dest: "conventions.md" }
54
+ ]
55
+ }
56
+ };
57
+ function sharedDocsForAgent(agentName) {
58
+ const destDir = {
59
+ claude: ".claude/docs",
60
+ copilot: ".github/docs",
61
+ cursor: ".cursor/docs",
62
+ windsurf: ".windsurf/docs",
63
+ aider: ".aider/docs"
64
+ };
65
+ const dir = destDir[agentName] ?? ".api-spector/docs";
66
+ return [
67
+ { src: "api-spector-scripting-reference.md", dest: `${dir}/api-spector-scripting-reference.md` },
68
+ { src: "collection-file-format.md", dest: `${dir}/collection-file-format.md` },
69
+ { src: "functional-testing-guide.md", dest: `${dir}/functional-testing-guide.md` },
70
+ { src: "security-testing-guide.md", dest: `${dir}/security-testing-guide.md` }
71
+ ];
72
+ }
73
+ function getPackageRoot() {
74
+ let dir = __dirname;
75
+ for (let i = 0; i < 5; i++) {
76
+ try {
77
+ require.resolve(path.join(dir, "package.json"));
78
+ return dir;
79
+ } catch {
80
+ dir = path.dirname(dir);
81
+ }
82
+ }
83
+ return path.join(__dirname, "..", "..");
84
+ }
85
+ function getTemplatesDir() {
86
+ return path.join(getPackageRoot(), "src", "cli", "agent-templates");
87
+ }
88
+ function getDocsDir() {
89
+ return path.join(getPackageRoot(), "docs", "ai");
90
+ }
91
+ async function fileExists(path2) {
92
+ try {
93
+ await promises.stat(path2);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ async function copyFile(src, dest, cwd) {
100
+ const destPath = path.join(cwd, dest);
101
+ const existed = await fileExists(destPath);
102
+ await promises.mkdir(path.dirname(destPath), { recursive: true });
103
+ const content = await promises.readFile(src, "utf8");
104
+ if (existed) {
105
+ const existing = await promises.readFile(destPath, "utf8");
106
+ if (existing === content) return "exists";
107
+ }
108
+ await promises.writeFile(destPath, content, "utf8");
109
+ return existed ? "updated" : "created";
110
+ }
111
+ async function initAgent(agentName, cwd) {
112
+ const names = agentName === "all" ? Object.keys(AGENTS) : [agentName];
113
+ for (const name of names) {
114
+ const agent = AGENTS[name];
115
+ if (!agent) {
116
+ console.error(color(` Unknown agent: "${name}"`, C.red));
117
+ console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
118
+ process.exit(1);
119
+ }
120
+ console.log(color(`
121
+ ${agent.name}`, C.bold, C.cyan));
122
+ const templatesDir = getTemplatesDir();
123
+ for (const file of agent.files) {
124
+ const srcPath = path.join(templatesDir, file.src);
125
+ if (!await fileExists(srcPath)) {
126
+ console.log(color(` skip ${file.dest} (template not found)`, C.yellow));
127
+ continue;
128
+ }
129
+ const result = await copyFile(srcPath, file.dest, cwd);
130
+ const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
131
+ const label = result === "exists" ? "unchanged" : result;
132
+ console.log(` ${icon} ${file.dest} ${color(`(${label})`, C.gray)}`);
133
+ }
134
+ }
135
+ console.log(color(`
136
+ Shared documentation`, C.bold, C.cyan));
137
+ const docsDir = getDocsDir();
138
+ const allDocDests = /* @__PURE__ */ new Set();
139
+ for (const name of names) {
140
+ for (const doc of sharedDocsForAgent(name)) {
141
+ if (allDocDests.has(doc.dest)) continue;
142
+ allDocDests.add(doc.dest);
143
+ const srcPath = path.join(docsDir, doc.src);
144
+ if (!await fileExists(srcPath)) {
145
+ console.log(color(` skip ${doc.dest} (not found)`, C.yellow));
146
+ continue;
147
+ }
148
+ const result = await copyFile(srcPath, doc.dest, cwd);
149
+ const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
150
+ const label = result === "exists" ? "unchanged" : result;
151
+ console.log(` ${icon} ${doc.dest} ${color(`(${label})`, C.gray)}`);
152
+ }
153
+ }
154
+ console.log(color("\n Done. Your AI agent can now generate API Spector tests.\n", C.green));
155
+ }
156
+ function listAgents() {
157
+ console.log(color("\n Available agents:\n", C.bold));
158
+ for (const [key, agent] of Object.entries(AGENTS)) {
159
+ console.log(` ${color(key.padEnd(12), C.cyan)} ${agent.description}`);
160
+ }
161
+ console.log(` ${color("all".padEnd(12), C.cyan)} Initialize all agents at once`);
162
+ console.log(color("\n Usage: api-spector agents init <name>\n", C.gray));
163
+ }
164
+ function printHelp() {
165
+ console.log(`
166
+ ${color("api-spector agents", C.bold)} — manage AI agent configurations
167
+
168
+ ${color("Commands:", C.bold)}
169
+ agents init <name> Scaffold agent instruction files in the current directory
170
+ agents list Show available agents
171
+ agents --help Show this message
172
+
173
+ ${color("Examples:", C.bold)}
174
+ api-spector agents init claude Set up Claude Code skills
175
+ api-spector agents init copilot Set up GitHub Copilot instructions
176
+ api-spector agents init all Set up all agents at once
177
+
178
+ ${color("What this does:", C.gray)}
179
+ Copies AI instruction files into your project so your LLM coding tool
180
+ understands the API Spector scripting API and can generate functional
181
+ and security test plans.
182
+ `);
183
+ }
184
+ async function main() {
185
+ const args = process.argv.slice(2);
186
+ const subCmd = args[0];
187
+ if (!subCmd || subCmd === "--help" || subCmd === "-h") {
188
+ printHelp();
189
+ process.exit(0);
190
+ }
191
+ if (subCmd === "list") {
192
+ listAgents();
193
+ process.exit(0);
194
+ }
195
+ if (subCmd === "init") {
196
+ const agentName = args[1]?.toLowerCase();
197
+ if (!agentName) {
198
+ console.error(color(" Missing agent name. Use: api-spector agents init <name>", C.red));
199
+ console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
200
+ process.exit(1);
201
+ }
202
+ await initAgent(agentName, process.cwd());
203
+ process.exit(0);
204
+ }
205
+ console.error(color(` Unknown sub-command: "${subCmd}"`, C.red));
206
+ printHelp();
207
+ process.exit(1);
208
+ }
209
+ main().catch((err) => {
210
+ console.error(color(` Error: ${err.message}`, C.red));
211
+ process.exit(2);
212
+ });
@@ -192,7 +192,7 @@ function buildUrl(baseUrl, params, vars) {
192
192
  templateTokens.add(String(name).trim());
193
193
  return "";
194
194
  });
195
- const enabled = params.filter((p) => p.enabled && p.key);
195
+ const enabled = (params ?? []).filter((p) => p.enabled && p.key);
196
196
  const pathRows = [];
197
197
  const queryRows = [];
198
198
  for (const p of enabled) {
@@ -965,6 +965,10 @@ function registerRequestHandler(ipc) {
965
965
  tls,
966
966
  piiMaskPatterns = []
967
967
  } = payload;
968
+ if (!req.headers) req.headers = [];
969
+ if (!req.params) req.params = [];
970
+ if (!req.body) req.body = { mode: "none" };
971
+ if (!req.auth) req.auth = { type: "none" };
968
972
  const start = Date.now();
969
973
  const liveGlobals = getGlobals();
970
974
  const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
@@ -1190,7 +1194,7 @@ function registerRequestHandler(ipc) {
1190
1194
  await persistGlobals();
1191
1195
  }
1192
1196
  const combinedTestResults = [...schemaTestResults, ...postTestResults];
1193
- if (!response.error && response.status >= 400 && !combinedTestResults.some((t) => !t.passed)) {
1197
+ if (!response.error && response.status >= 400 && combinedTestResults.length === 0) {
1194
1198
  combinedTestResults.push({
1195
1199
  name: `HTTP status ${response.status} ${response.statusText}`.trim(),
1196
1200
  passed: false,
@@ -1224,13 +1228,78 @@ function registerRequestHandler(ipc) {
1224
1228
  };
1225
1229
  });
1226
1230
  }
1231
+ function collectTagged(folder, requests, collectionVars, filterTags, parentPath = [], isRoot = true) {
1232
+ const results = [];
1233
+ const scopePath = isRoot ? parentPath : [...parentPath, folder.name];
1234
+ for (const reqId of folder.requestIds) {
1235
+ const req = requests[reqId];
1236
+ if (!req || req.hookType || req.disabled) continue;
1237
+ const tags = req.meta?.tags ?? [];
1238
+ if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
1239
+ results.push({ request: req, collectionVars, scopePath });
1240
+ }
1241
+ for (const sub of folder.folders) {
1242
+ const folderTags = sub.tags ?? [];
1243
+ const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
1244
+ results.push(...collectTagged(sub, requests, collectionVars, effectiveTags, scopePath, false));
1245
+ }
1246
+ return results;
1247
+ }
1248
+ function folderPathTo(root, requestId) {
1249
+ if (root.requestIds.includes(requestId)) return [root];
1250
+ for (const sub of root.folders) {
1251
+ const path2 = folderPathTo(sub, requestId);
1252
+ if (path2.length > 0) return [root, ...path2];
1253
+ }
1254
+ return [];
1255
+ }
1256
+ function getAllApplicableHooks(folderId, collection) {
1257
+ function chainToFolder(root, targetId) {
1258
+ if (root.id === targetId) return [root];
1259
+ for (const sub of root.folders) {
1260
+ const chain2 = chainToFolder(sub, targetId);
1261
+ if (chain2.length) return [root, ...chain2];
1262
+ }
1263
+ return [];
1264
+ }
1265
+ const chain = chainToFolder(collection.rootFolder, folderId);
1266
+ const beforeAll = [];
1267
+ const before = [];
1268
+ const after = [];
1269
+ const afterAll = [];
1270
+ for (const folder of chain) {
1271
+ const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
1272
+ beforeAll.push(...reqs.filter((r) => r.hookType === "beforeAll"));
1273
+ before.push(...reqs.filter((r) => r.hookType === "before"));
1274
+ }
1275
+ for (const folder of [...chain].reverse()) {
1276
+ const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
1277
+ after.push(...reqs.filter((r) => r.hookType === "after"));
1278
+ afterAll.push(...reqs.filter((r) => r.hookType === "afterAll"));
1279
+ }
1280
+ return { beforeAll, before, after, afterAll };
1281
+ }
1282
+ function resolveInheritedAuthAndHeaders(requestId, collection) {
1283
+ let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
1284
+ let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
1285
+ const path2 = folderPathTo(collection.rootFolder, requestId);
1286
+ for (const folder of path2) {
1287
+ if (folder.auth && folder.auth.type !== "none") inheritedAuth = folder.auth;
1288
+ if (folder.headers?.length) {
1289
+ inheritedHeaders = [...inheritedHeaders, ...folder.headers.filter((h) => h.enabled && h.key)];
1290
+ }
1291
+ }
1292
+ return { auth: inheritedAuth, headers: inheritedHeaders };
1293
+ }
1227
1294
  exports.buildAuthHeaders = buildAuthHeaders;
1228
1295
  exports.buildDispatcher = buildDispatcher;
1229
1296
  exports.buildDynamicVars = buildDynamicVars;
1230
1297
  exports.buildEnvVars = buildEnvVars;
1231
1298
  exports.buildSchemaTestResults = buildSchemaTestResults;
1232
1299
  exports.buildUrl = buildUrl;
1300
+ exports.collectTagged = collectTagged;
1233
1301
  exports.fetchOAuth2Token = fetchOAuth2Token;
1302
+ exports.getAllApplicableHooks = getAllApplicableHooks;
1234
1303
  exports.getGlobals = getGlobals;
1235
1304
  exports.getSecret = getSecret;
1236
1305
  exports.initSecretStore = initSecretStore;
@@ -1245,5 +1314,6 @@ exports.performNtlmRequest = performNtlmRequest;
1245
1314
  exports.persistGlobals = persistGlobals;
1246
1315
  exports.registerRequestHandler = registerRequestHandler;
1247
1316
  exports.registerSecretHandlers = registerSecretHandlers;
1317
+ exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;
1248
1318
  exports.runScript = runScript;
1249
1319
  exports.setGlobals = setGlobals;