@testsmith/api-spector 0.3.1 → 0.3.3

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
@@ -72,11 +72,27 @@ if (command.runner === 'electron') {
72
72
  electron = require('electron')
73
73
  } catch (err) {
74
74
  const msg = err && err.message ? err.message : String(err)
75
- const looksLikeBinaryMissing = /Electron failed to install correctly|Cannot find module 'electron'/i.test(msg)
75
+ const notInstalled = /Cannot find module 'electron'/i.test(msg)
76
+ const binaryMissing = /Electron failed to install correctly/i.test(msg)
76
77
  console.error('')
77
78
  console.error(' API Spector — failed to launch the UI.')
78
79
  console.error('')
79
- if (looksLikeBinaryMissing) {
80
+ if (notInstalled) {
81
+ console.error(' The electron package is not installed alongside API Spector.')
82
+ console.error(' Versions 0.3.1 and 0.3.2 shipped without it by mistake.')
83
+ console.error('')
84
+ console.error(' Fix options:')
85
+ console.error('')
86
+ console.error(' 1. Update API Spector (0.3.3 or later includes electron):')
87
+ console.error(' npm install -D @testsmith/api-spector@latest')
88
+ console.error(' (use -g instead of -D if you installed globally)')
89
+ console.error('')
90
+ console.error(' 2. Or keep this version and install electron yourself:')
91
+ console.error(' npm install -D electron@31')
92
+ console.error('')
93
+ console.error(' CLI subcommands (run / mock / record / contract / wsdl) do not')
94
+ console.error(' need electron and work even while this is broken.')
95
+ } else if (binaryMissing) {
80
96
  const installDir = path.dirname(__dirname)
81
97
  console.error(' Electron is installed, but its platform binary is missing — the')
82
98
  console.error(' download during `npm install` did not complete (often a proxy or')
@@ -85,13 +101,14 @@ if (command.runner === 'electron') {
85
101
  console.error(' Fix options (try in order):')
86
102
  console.error('')
87
103
  console.error(' 1. Reinstall and force the postinstall script to run:')
88
- console.error(' npm install -g @testsmith/api-spector --force')
104
+ console.error(' npm install -D @testsmith/api-spector --force')
105
+ console.error(' (use -g instead of -D if you installed globally)')
89
106
  console.error('')
90
107
  console.error(' 2. Behind a proxy? Set npm + electron mirrors and reinstall:')
91
108
  console.error(' npm config set proxy http://your-proxy:port')
92
109
  console.error(' npm config set https-proxy http://your-proxy:port')
93
110
  console.error(' set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/')
94
- console.error(' npm install -g @testsmith/api-spector --force')
111
+ console.error(' npm install -D @testsmith/api-spector --force')
95
112
  console.error('')
96
113
  console.error(' 3. Re-run electron\'s postinstall manually:')
97
114
  console.error(` cd "${path.join(installDir, 'node_modules', 'electron')}"`)
@@ -2,18 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
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
- }
5
+ const cliCommon = require("./chunks/cli-common-CDQY1erJ.js");
17
6
  const AGENTS = {
18
7
  claude: {
19
8
  name: "Claude Code",
@@ -113,27 +102,27 @@ async function initAgent(agentName, cwd) {
113
102
  for (const name of names) {
114
103
  const agent = AGENTS[name];
115
104
  if (!agent) {
116
- console.error(color(` Unknown agent: "${name}"`, C.red));
105
+ console.error(cliCommon.colorAlways(` Unknown agent: "${name}"`, cliCommon.C.red));
117
106
  console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
118
107
  process.exit(1);
119
108
  }
120
- console.log(color(`
121
- ${agent.name}`, C.bold, C.cyan));
109
+ console.log(cliCommon.colorAlways(`
110
+ ${agent.name}`, cliCommon.C.bold, cliCommon.C.cyan));
122
111
  const templatesDir = getTemplatesDir();
123
112
  for (const file of agent.files) {
124
113
  const srcPath = path.join(templatesDir, file.src);
125
114
  if (!await fileExists(srcPath)) {
126
- console.log(color(` skip ${file.dest} (template not found)`, C.yellow));
115
+ console.log(cliCommon.colorAlways(` skip ${file.dest} (template not found)`, cliCommon.C.yellow));
127
116
  continue;
128
117
  }
129
118
  const result = await copyFile(srcPath, file.dest, cwd);
130
- const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
119
+ const icon = result === "created" ? cliCommon.colorAlways("+", cliCommon.C.green) : result === "updated" ? cliCommon.colorAlways("~", cliCommon.C.yellow) : cliCommon.colorAlways("=", cliCommon.C.gray);
131
120
  const label = result === "exists" ? "unchanged" : result;
132
- console.log(` ${icon} ${file.dest} ${color(`(${label})`, C.gray)}`);
121
+ console.log(` ${icon} ${file.dest} ${cliCommon.colorAlways(`(${label})`, cliCommon.C.gray)}`);
133
122
  }
134
123
  }
135
- console.log(color(`
136
- Shared documentation`, C.bold, C.cyan));
124
+ console.log(cliCommon.colorAlways(`
125
+ Shared documentation`, cliCommon.C.bold, cliCommon.C.cyan));
137
126
  const docsDir = getDocsDir();
138
127
  const allDocDests = /* @__PURE__ */ new Set();
139
128
  for (const name of names) {
@@ -142,40 +131,40 @@ async function initAgent(agentName, cwd) {
142
131
  allDocDests.add(doc.dest);
143
132
  const srcPath = path.join(docsDir, doc.src);
144
133
  if (!await fileExists(srcPath)) {
145
- console.log(color(` skip ${doc.dest} (not found)`, C.yellow));
134
+ console.log(cliCommon.colorAlways(` skip ${doc.dest} (not found)`, cliCommon.C.yellow));
146
135
  continue;
147
136
  }
148
137
  const result = await copyFile(srcPath, doc.dest, cwd);
149
- const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
138
+ const icon = result === "created" ? cliCommon.colorAlways("+", cliCommon.C.green) : result === "updated" ? cliCommon.colorAlways("~", cliCommon.C.yellow) : cliCommon.colorAlways("=", cliCommon.C.gray);
150
139
  const label = result === "exists" ? "unchanged" : result;
151
- console.log(` ${icon} ${doc.dest} ${color(`(${label})`, C.gray)}`);
140
+ console.log(` ${icon} ${doc.dest} ${cliCommon.colorAlways(`(${label})`, cliCommon.C.gray)}`);
152
141
  }
153
142
  }
154
- console.log(color("\n Done. Your AI agent can now generate API Spector tests.\n", C.green));
143
+ console.log(cliCommon.colorAlways("\n Done. Your AI agent can now generate API Spector tests.\n", cliCommon.C.green));
155
144
  }
156
145
  function listAgents() {
157
- console.log(color("\n Available agents:\n", C.bold));
146
+ console.log(cliCommon.colorAlways("\n Available agents:\n", cliCommon.C.bold));
158
147
  for (const [key, agent] of Object.entries(AGENTS)) {
159
- console.log(` ${color(key.padEnd(12), C.cyan)} ${agent.description}`);
148
+ console.log(` ${cliCommon.colorAlways(key.padEnd(12), cliCommon.C.cyan)} ${agent.description}`);
160
149
  }
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));
150
+ console.log(` ${cliCommon.colorAlways("all".padEnd(12), cliCommon.C.cyan)} Initialize all agents at once`);
151
+ console.log(cliCommon.colorAlways("\n Usage: api-spector agents init <name>\n", cliCommon.C.gray));
163
152
  }
164
153
  function printHelp() {
165
154
  console.log(`
166
- ${color("api-spector agents", C.bold)} — manage AI agent configurations
155
+ ${cliCommon.colorAlways("api-spector agents", cliCommon.C.bold)} — manage AI agent configurations
167
156
 
168
- ${color("Commands:", C.bold)}
157
+ ${cliCommon.colorAlways("Commands:", cliCommon.C.bold)}
169
158
  agents init <name> Scaffold agent instruction files in the current directory
170
159
  agents list Show available agents
171
160
  agents --help Show this message
172
161
 
173
- ${color("Examples:", C.bold)}
162
+ ${cliCommon.colorAlways("Examples:", cliCommon.C.bold)}
174
163
  api-spector agents init claude Set up Claude Code skills
175
164
  api-spector agents init copilot Set up GitHub Copilot instructions
176
165
  api-spector agents init all Set up all agents at once
177
166
 
178
- ${color("What this does:", C.gray)}
167
+ ${cliCommon.colorAlways("What this does:", cliCommon.C.gray)}
179
168
  Copies AI instruction files into your project so your LLM coding tool
180
169
  understands the API Spector scripting API and can generate functional
181
170
  and security test plans.
@@ -195,18 +184,18 @@ async function main() {
195
184
  if (subCmd === "init") {
196
185
  const agentName = args[1]?.toLowerCase();
197
186
  if (!agentName) {
198
- console.error(color(" Missing agent name. Use: api-spector agents init <name>", C.red));
187
+ console.error(cliCommon.colorAlways(" Missing agent name. Use: api-spector agents init <name>", cliCommon.C.red));
199
188
  console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
200
189
  process.exit(1);
201
190
  }
202
191
  await initAgent(agentName, process.cwd());
203
192
  process.exit(0);
204
193
  }
205
- console.error(color(` Unknown sub-command: "${subCmd}"`, C.red));
194
+ console.error(cliCommon.colorAlways(` Unknown sub-command: "${subCmd}"`, cliCommon.C.red));
206
195
  printHelp();
207
196
  process.exit(1);
208
197
  }
209
198
  main().catch((err) => {
210
- console.error(color(` Error: ${err.message}`, C.red));
199
+ console.error(cliCommon.colorAlways(` Error: ${err.message}`, cliCommon.C.red));
211
200
  process.exit(2);
212
201
  });
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  const crypto = require("crypto");
25
25
  const http = require("http");
26
26
  const promises = require("fs/promises");
27
+ const handle = require("./handle-C0IQL-Vl.js");
27
28
  const path = require("path");
28
29
  const dayjs = require("dayjs");
29
30
  const vm = require("vm");
@@ -70,13 +71,13 @@ function getSafeStorage() {
70
71
  }
71
72
  }
72
73
  function registerSecretHandlers(ipc) {
73
- ipc.handle("secret:checkMasterKey", () => {
74
+ handle.handleIpc(ipc, handle.IPC.secret.checkMasterKey, () => {
74
75
  return { set: Boolean(process.env[MASTER_KEY_ENV]) };
75
76
  });
76
- ipc.handle("secret:setMasterKey", (_e, value) => {
77
+ handle.handleIpc(ipc, handle.IPC.secret.setMasterKey, (_e, value) => {
77
78
  process.env[MASTER_KEY_ENV] = value;
78
79
  });
79
- ipc.handle("secret:set", async (_e, ref, value) => {
80
+ handle.handleIpc(ipc, handle.IPC.secret.set, async (_e, ref, value) => {
80
81
  const ss = getSafeStorage();
81
82
  if (!ss || !ss.isEncryptionAvailable()) {
82
83
  throw new Error("OS encryption is not available — set the secret via environment variable instead");
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ const promises = require("fs/promises");
3
+ const path = require("path");
4
+ const C = {
5
+ reset: "\x1B[0m",
6
+ bold: "\x1B[1m",
7
+ dim: "\x1B[2m",
8
+ green: "\x1B[32m",
9
+ red: "\x1B[31m",
10
+ yellow: "\x1B[33m",
11
+ cyan: "\x1B[36m",
12
+ gray: "\x1B[90m",
13
+ white: "\x1B[97m"
14
+ };
15
+ function color(str, ...codes) {
16
+ return process.stdout.isTTY ? codes.join("") + str + C.reset : str;
17
+ }
18
+ function colorAlways(str, ...codes) {
19
+ return codes.join("") + str + C.reset;
20
+ }
21
+ function parseArgs(argv, repeatableKeys = []) {
22
+ const args = {};
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const arg = argv[i];
25
+ if (!arg.startsWith("--")) continue;
26
+ const key = arg.slice(2);
27
+ const next = argv[i + 1];
28
+ if (!next || next.startsWith("--")) {
29
+ args[key] = true;
30
+ } else {
31
+ if (repeatableKeys.includes(key)) {
32
+ const prev = args[key];
33
+ args[key] = Array.isArray(prev) ? [...prev, next] : [next];
34
+ } else {
35
+ args[key] = next;
36
+ }
37
+ i++;
38
+ }
39
+ }
40
+ return args;
41
+ }
42
+ async function resolveWorkspacePath(wsPath) {
43
+ const s = await promises.stat(wsPath);
44
+ if (!s.isDirectory()) return wsPath;
45
+ const entries = await promises.readdir(wsPath);
46
+ const spector = entries.find((e) => e.endsWith(".spector"));
47
+ if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
48
+ return path.join(wsPath, spector);
49
+ }
50
+ async function loadWorkspace(wsPath) {
51
+ const resolved = await resolveWorkspacePath(wsPath);
52
+ const raw = await promises.readFile(resolved, "utf8");
53
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)), file: resolved };
54
+ }
55
+ async function loadCollections(workspace, dir, opts = {}) {
56
+ const cols = [];
57
+ for (const relPath of workspace.collections) {
58
+ try {
59
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
60
+ const col = JSON.parse(raw);
61
+ if (!opts.filterName || col.name === opts.filterName) cols.push(col);
62
+ } catch {
63
+ opts.onError?.(relPath);
64
+ }
65
+ }
66
+ return cols;
67
+ }
68
+ async function loadEnvironments(workspace, dir) {
69
+ const envs = [];
70
+ for (const relPath of workspace.environments) {
71
+ try {
72
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
73
+ envs.push(JSON.parse(raw));
74
+ } catch {
75
+ }
76
+ }
77
+ return envs;
78
+ }
79
+ async function loadMocks(workspace, dir, onError) {
80
+ const mocks = [];
81
+ for (const relPath of workspace.mocks ?? []) {
82
+ try {
83
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
84
+ mocks.push(JSON.parse(raw));
85
+ } catch {
86
+ onError?.(relPath);
87
+ }
88
+ }
89
+ return mocks;
90
+ }
91
+ exports.C = C;
92
+ exports.color = color;
93
+ exports.colorAlways = colorAlways;
94
+ exports.loadCollections = loadCollections;
95
+ exports.loadEnvironments = loadEnvironments;
96
+ exports.loadMocks = loadMocks;
97
+ exports.loadWorkspace = loadWorkspace;
98
+ exports.parseArgs = parseArgs;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ const IPC = {
3
+ // ─── Workspace / File ──────────────────────────────────────────────────────
4
+ file: {
5
+ openWorkspace: "file:openWorkspace",
6
+ getLastWorkspace: "file:getLastWorkspace",
7
+ saveWorkspace: "file:saveWorkspace",
8
+ newWorkspace: "file:newWorkspace",
9
+ closeWorkspace: "file:closeWorkspace",
10
+ loadCollection: "file:loadCollection",
11
+ saveCollection: "file:saveCollection",
12
+ loadEnvironment: "file:loadEnvironment",
13
+ saveEnvironment: "file:saveEnvironment",
14
+ deleteWorkspaceFile: "file:deleteWorkspaceFile",
15
+ saveMock: "file:saveMock",
16
+ loadMock: "file:loadMock"
17
+ },
18
+ // ─── Native dialogs ────────────────────────────────────────────────────────
19
+ dialog: {
20
+ pickDir: "dialog:pickDir"
21
+ },
22
+ // ─── HTTP execution ────────────────────────────────────────────────────────
23
+ request: {
24
+ send: "request:send"
25
+ },
26
+ // ─── Secrets ───────────────────────────────────────────────────────────────
27
+ secret: {
28
+ checkMasterKey: "secret:checkMasterKey",
29
+ setMasterKey: "secret:setMasterKey",
30
+ set: "secret:set"
31
+ },
32
+ // ─── Global variables ──────────────────────────────────────────────────────
33
+ globals: {
34
+ get: "globals:get",
35
+ set: "globals:set"
36
+ },
37
+ // ─── Collection runner ─────────────────────────────────────────────────────
38
+ runner: {
39
+ start: "runner:start",
40
+ /** Event: per-request progress pushed from main during a run. */
41
+ progress: "runner:progress"
42
+ },
43
+ // ─── Run results export ────────────────────────────────────────────────────
44
+ results: {
45
+ save: "results:save"
46
+ },
47
+ // ─── Import ────────────────────────────────────────────────────────────────
48
+ import: {
49
+ postman: "import:postman",
50
+ openapi: "import:openapi",
51
+ openapiUrl: "import:openapi-url",
52
+ insomnia: "import:insomnia",
53
+ bruno: "import:bruno",
54
+ http: "import:http",
55
+ openapiSchemas: "import:openapi-schemas",
56
+ openapiSchemasUrl: "import:openapi-schemas-url"
57
+ },
58
+ // ─── Code generation ───────────────────────────────────────────────────────
59
+ generate: {
60
+ code: "generate:code",
61
+ save: "generate:save",
62
+ saveZip: "generate:saveZip"
63
+ },
64
+ // ─── OAuth 2.0 ─────────────────────────────────────────────────────────────
65
+ oauth2: {
66
+ startFlow: "oauth2:startFlow",
67
+ refreshToken: "oauth2:refreshToken"
68
+ },
69
+ // ─── Mock servers ──────────────────────────────────────────────────────────
70
+ mock: {
71
+ start: "mock:start",
72
+ stop: "mock:stop",
73
+ isRunning: "mock:isRunning",
74
+ runningIds: "mock:runningIds",
75
+ updateRoutes: "mock:updateRoutes",
76
+ /** Event: a mock server route was hit. */
77
+ hit: "mock:hit"
78
+ },
79
+ // ─── WebSocket ─────────────────────────────────────────────────────────────
80
+ ws: {
81
+ connect: "ws:connect",
82
+ send: "ws:send",
83
+ disconnect: "ws:disconnect",
84
+ /** Event: inbound/outbound WS message. */
85
+ message: "ws:message",
86
+ /** Event: connection status change. */
87
+ status: "ws:status"
88
+ },
89
+ // ─── SOAP / WSDL ───────────────────────────────────────────────────────────
90
+ wsdl: {
91
+ fetch: "wsdl:fetch",
92
+ import: "wsdl:import"
93
+ },
94
+ // ─── Docs generation ───────────────────────────────────────────────────────
95
+ docs: {
96
+ generate: "docs:generate"
97
+ },
98
+ // ─── Contract testing ──────────────────────────────────────────────────────
99
+ contract: {
100
+ run: "contract:run",
101
+ inferSchema: "contract:inferSchema",
102
+ exportReportHtml: "contract:exportReportHtml",
103
+ captureSnapshot: "contract:captureSnapshot",
104
+ listSnapshots: "contract:listSnapshots",
105
+ loadSnapshot: "contract:loadSnapshot",
106
+ deleteSnapshot: "contract:deleteSnapshot"
107
+ },
108
+ // ─── Script hooks ──────────────────────────────────────────────────────────
109
+ script: {
110
+ runHook: "script:run-hook"
111
+ },
112
+ // ─── Git ───────────────────────────────────────────────────────────────────
113
+ git: {
114
+ isRepo: "git:isRepo",
115
+ init: "git:init",
116
+ status: "git:status",
117
+ diff: "git:diff",
118
+ diffStaged: "git:diffStaged",
119
+ stage: "git:stage",
120
+ unstage: "git:unstage",
121
+ stageAll: "git:stageAll",
122
+ commit: "git:commit",
123
+ log: "git:log",
124
+ branches: "git:branches",
125
+ checkout: "git:checkout",
126
+ deleteBranch: "git:deleteBranch",
127
+ pull: "git:pull",
128
+ push: "git:push",
129
+ remotes: "git:remotes",
130
+ addRemote: "git:addRemote",
131
+ setRemoteUrl: "git:setRemoteUrl",
132
+ removeRemote: "git:removeRemote",
133
+ writeCiFile: "git:writeCiFile",
134
+ resolveOurs: "git:resolveOurs",
135
+ resolveTheirs: "git:resolveTheirs",
136
+ markResolved: "git:markResolved"
137
+ },
138
+ // ─── Recorder ──────────────────────────────────────────────────────────────
139
+ record: {
140
+ start: "record:start",
141
+ stop: "record:stop",
142
+ isRunning: "record:isRunning",
143
+ entries: "record:entries",
144
+ toMock: "record:toMock",
145
+ /** Event: a recorded proxy entry was captured. */
146
+ hit: "record:hit"
147
+ },
148
+ // ─── Shell ─────────────────────────────────────────────────────────────────
149
+ shell: {
150
+ openExternal: "shell:openExternal"
151
+ }
152
+ };
153
+ function handleIpc(ipc, channel, fn) {
154
+ ipc.handle(channel, async (event, ...args) => {
155
+ try {
156
+ return await fn(event, ...args);
157
+ } catch (err) {
158
+ console.error(`[${channel}]`, err);
159
+ throw err;
160
+ }
161
+ });
162
+ }
163
+ exports.IPC = IPC;
164
+ exports.handleIpc = handleIpc;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const uuid = require("uuid");
4
- const soapHandler = require("./soap-handler-Cpj-JwyA.js");
4
+ const soapHandler = require("./soap-handler-B9x_YCtj.js");
5
+ require("./handle-C0IQL-Vl.js");
5
6
  require("https");
6
7
  require("http");
7
8
  require("@xmldom/xmldom");
@@ -61,14 +61,16 @@ const validateSendRequestPayload = compile({
61
61
  const validateContractRunPayload = compile({
62
62
  type: "object",
63
63
  properties: {
64
- mode: { type: "string", enum: ["consumer", "provider", "bidirectional"] },
64
+ mode: { type: "string", enum: ["consumer", "provider", "provider-live", "bidirectional"] },
65
65
  requests: { type: "array", items: apiRequestSchema },
66
66
  envVars: stringMap,
67
67
  collectionVars: stringMap,
68
68
  specUrl: { type: "string" },
69
69
  specPath: { type: "string" },
70
70
  specSnapshotRelPath: { type: "string" },
71
- requestBaseUrl: { type: "string" }
71
+ requestBaseUrl: { type: "string" },
72
+ providerBaseUrl: { type: "string" },
73
+ stateHandlerUrl: { type: "string" }
72
74
  },
73
75
  required: ["mode", "requests"],
74
76
  additionalProperties: true