@testsmith/api-spector 0.0.9 → 0.1.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
@@ -3,7 +3,6 @@
3
3
 
4
4
  const path = require('path')
5
5
  const { spawn } = require('child_process')
6
- const electron = require('electron')
7
6
 
8
7
  const [, , cmd = 'ui', ...rest] = process.argv
9
8
 
@@ -20,14 +19,19 @@ function printHelp() {
20
19
  console.log(' api-spector run --help Show run options')
21
20
  console.log(' api-spector mock --help Show mock options')
22
21
  console.log('')
22
+ console.log(' Environment:')
23
+ console.log(' ELECTRON_NO_SANDBOX=1 Disable Chromium sandbox')
24
+ console.log(' (needed on locked-down Linux)')
25
+ console.log('')
23
26
  }
24
27
 
25
28
  if (cmd === '--help' || cmd === '-h') {
26
29
  printHelp()
27
30
  process.exit(0)
28
31
  } else if (cmd === 'ui') {
32
+ const electron = require('electron')
29
33
  const appDir = path.join(__dirname, '..')
30
- const proc = spawn(String(electron), [appDir], {
34
+ const proc = spawn(String(electron), [appDir, ...rest], {
31
35
  stdio: 'inherit',
32
36
  env: process.env,
33
37
  })
@@ -128,8 +128,9 @@ async function handleRequest(serverId, req, res, reqStart) {
128
128
  const routes = liveRoutes.get(serverId) ?? [];
129
129
  const route = findRoute(routes, method, urlPath);
130
130
  if (!route) {
131
+ const notFoundBody = JSON.stringify({ error: "No matching mock route", method, path: urlPath });
131
132
  res.writeHead(404, { "Content-Type": "application/json" });
132
- res.end(JSON.stringify({ error: "No matching mock route", method, path: urlPath }));
133
+ res.end(notFoundBody);
133
134
  hitCallback?.({
134
135
  id: crypto.randomUUID(),
135
136
  serverId,
@@ -138,7 +139,9 @@ async function handleRequest(serverId, req, res, reqStart) {
138
139
  path: urlPath,
139
140
  matchedRouteId: null,
140
141
  status: 404,
141
- durationMs: Date.now() - reqStart
142
+ durationMs: Date.now() - reqStart,
143
+ responseBody: notFoundBody,
144
+ responseHeaders: { "Content-Type": "application/json" }
142
145
  });
143
146
  return;
144
147
  }
@@ -188,7 +191,9 @@ async function handleRequest(serverId, req, res, reqStart) {
188
191
  path: urlPath,
189
192
  matchedRouteId: route.id,
190
193
  status: responseDraft.statusCode,
191
- durationMs: Date.now() - reqStart
194
+ durationMs: Date.now() - reqStart,
195
+ responseBody: finalBody,
196
+ responseHeaders: responseDraft.headers
192
197
  });
193
198
  };
194
199
  if (route.delay && route.delay > 0) setTimeout(respond, route.delay);
package/out/main/index.js CHANGED
@@ -30,11 +30,12 @@ const uuid = require("uuid");
30
30
  const jsYaml = require("js-yaml");
31
31
  const undici = require("undici");
32
32
  const JSZip = require("jszip");
33
- const mockServer = require("./chunks/mock-server-C0RlwZf_.js");
33
+ const mockServer = require("./chunks/mock-server-Cx-xG4pJ.js");
34
34
  const http = require("http");
35
35
  const WebSocket = require("ws");
36
36
  const https = require("https");
37
37
  const Ajv = require("ajv");
38
+ const simpleGit = require("simple-git");
38
39
  require("crypto");
39
40
  require("dayjs");
40
41
  require("vm");
@@ -86,7 +87,7 @@ function registerFileHandlers(ipc) {
86
87
  await requestHandler.loadGlobals(workspaceDir);
87
88
  await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
88
89
  await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
89
- const gitignore = "# api Spector — never commit secrets\n*.secrets\n.env.local\n";
90
+ const gitignore = "# api Spector — never commit secrets\n*.secrets\n.env.local\n\n# Dependencies\nnode_modules/\n";
90
91
  await atomicWrite(path.join(workspaceDir, ".gitignore"), gitignore);
91
92
  const ws = {
92
93
  version: "1.0",
@@ -3436,6 +3437,124 @@ function registerContractHandlers(ipc) {
3436
3437
  return schema ? JSON.stringify(schema, null, 2) : null;
3437
3438
  });
3438
3439
  }
3440
+ function git() {
3441
+ const dir = getWorkspaceDir();
3442
+ if (!dir) throw new Error("No workspace open");
3443
+ return simpleGit.simpleGit(dir);
3444
+ }
3445
+ function registerGitHandlers(ipc) {
3446
+ ipc.handle("git:isRepo", async () => {
3447
+ try {
3448
+ await git().revparse(["--git-dir"]);
3449
+ return true;
3450
+ } catch {
3451
+ return false;
3452
+ }
3453
+ });
3454
+ ipc.handle("git:init", async () => {
3455
+ await git().init();
3456
+ });
3457
+ ipc.handle("git:status", async () => {
3458
+ const result = await git().status();
3459
+ return {
3460
+ staged: result.staged.map((f) => ({ path: f, status: resolveStatus(result, f, true) })),
3461
+ unstaged: result.modified.filter((f) => !result.staged.includes(f)).concat(result.deleted.filter((f) => !result.staged.includes(f))).map((f) => ({ path: f, status: resolveStatus(result, f, false) })),
3462
+ untracked: result.not_added.map((f) => ({ path: f, status: "untracked" })),
3463
+ branch: result.current ?? "",
3464
+ ahead: result.ahead,
3465
+ behind: result.behind,
3466
+ remote: result.tracking ?? null
3467
+ };
3468
+ });
3469
+ ipc.handle("git:diff", async (_e, filePath) => {
3470
+ if (filePath) return git().diff(["--", filePath]);
3471
+ return git().diff();
3472
+ });
3473
+ ipc.handle("git:diffStaged", async (_e, filePath) => {
3474
+ if (filePath) return git().diff(["--cached", "--", filePath]);
3475
+ return git().diff(["--cached"]);
3476
+ });
3477
+ ipc.handle("git:stage", async (_e, paths) => {
3478
+ await git().add(paths);
3479
+ });
3480
+ ipc.handle("git:unstage", async (_e, paths) => {
3481
+ await git().reset(["HEAD", "--", ...paths]);
3482
+ });
3483
+ ipc.handle("git:stageAll", async () => {
3484
+ await git().add(["."]);
3485
+ });
3486
+ ipc.handle("git:commit", async (_e, message) => {
3487
+ await git().commit(message);
3488
+ });
3489
+ ipc.handle("git:log", async (_e, limit = 50) => {
3490
+ const result = await git().log({ maxCount: limit });
3491
+ return result.all.map((c) => ({
3492
+ hash: c.hash,
3493
+ short: c.hash.slice(0, 7),
3494
+ message: c.message,
3495
+ author: c.author_name,
3496
+ email: c.author_email,
3497
+ date: c.date
3498
+ }));
3499
+ });
3500
+ ipc.handle("git:branches", async () => {
3501
+ const result = await git().branch(["-a", "--format=%(refname:short)|%(objectname:short)|%(upstream:short)|%(upstream:track)"]);
3502
+ return result.all.filter((name) => !name.includes("HEAD")).map((name) => ({
3503
+ name: name.replace(/^remotes\//, ""),
3504
+ current: name === result.current,
3505
+ remote: name.startsWith("remotes/")
3506
+ }));
3507
+ });
3508
+ ipc.handle("git:checkout", async (_e, branch, create) => {
3509
+ if (create) await git().checkoutLocalBranch(branch);
3510
+ else await git().checkout(branch);
3511
+ });
3512
+ ipc.handle("git:pull", async () => {
3513
+ await git().pull();
3514
+ });
3515
+ ipc.handle("git:push", async (_e, setUpstream) => {
3516
+ if (setUpstream) {
3517
+ const status = await git().status();
3518
+ await git().push(["--set-upstream", "origin", status.current ?? "main"]);
3519
+ } else {
3520
+ await git().push();
3521
+ }
3522
+ });
3523
+ ipc.handle("git:remotes", async () => {
3524
+ const result = await git().getRemotes(true);
3525
+ return result.map((r) => ({ name: r.name, url: r.refs.fetch || r.refs.push || "" }));
3526
+ });
3527
+ ipc.handle("git:addRemote", async (_e, name, url) => {
3528
+ await git().addRemote(name, url);
3529
+ });
3530
+ ipc.handle("git:setRemoteUrl", async (_e, name, url) => {
3531
+ await git().remote(["set-url", name, url]);
3532
+ });
3533
+ ipc.handle("git:removeRemote", async (_e, name) => {
3534
+ await git().removeRemote(name);
3535
+ });
3536
+ ipc.handle("git:writeCiFile", async (_e, relPath, content) => {
3537
+ const wsDir = getWorkspaceDir();
3538
+ if (!wsDir) throw new Error("No workspace open");
3539
+ const fullPath = path.join(wsDir, relPath);
3540
+ await promises.mkdir(path.dirname(fullPath), { recursive: true });
3541
+ await promises.writeFile(fullPath, content, "utf8");
3542
+ });
3543
+ }
3544
+ function resolveStatus(result, filePath, staged) {
3545
+ if (staged) {
3546
+ if (result.created.includes(filePath)) return "added";
3547
+ if (result.deleted.includes(filePath)) return "deleted";
3548
+ if (result.renamed.find((r) => r.to === filePath || r.from === filePath)) return "renamed";
3549
+ return "modified";
3550
+ }
3551
+ if (result.deleted.includes(filePath)) return "deleted";
3552
+ return "modified";
3553
+ }
3554
+ if (process.env.ELECTRON_NO_SANDBOX === "1") {
3555
+ electron.app.commandLine.appendSwitch("no-sandbox");
3556
+ electron.app.commandLine.appendSwitch("disable-features", "RendererCodeIntegrity");
3557
+ }
3439
3558
  function createSplashWindow() {
3440
3559
  const splash = new electron.BrowserWindow({
3441
3560
  width: 420,
@@ -3532,6 +3651,7 @@ electron.app.whenReady().then(async () => {
3532
3651
  registerSoapHandlers(electron.ipcMain);
3533
3652
  registerDocsHandlers(electron.ipcMain);
3534
3653
  registerContractHandlers(electron.ipcMain);
3654
+ registerGitHandlers(electron.ipcMain);
3535
3655
  createWindow();
3536
3656
  electron.app.on("activate", () => {
3537
3657
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
package/out/main/mock.js CHANGED
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const mockServer = require("./chunks/mock-server-C0RlwZf_.js");
5
+ const mockServer = require("./chunks/mock-server-Cx-xG4pJ.js");
6
6
  require("http");
7
7
  require("crypto");
8
8
  require("vm");
@@ -41,9 +41,18 @@ function parseArgs(argv) {
41
41
  }
42
42
  return args;
43
43
  }
44
+ async function resolveWorkspacePath(wsPath) {
45
+ const s = await promises.stat(wsPath);
46
+ if (!s.isDirectory()) return wsPath;
47
+ const entries = await promises.readdir(wsPath);
48
+ const spector = entries.find((e) => e.endsWith(".spector"));
49
+ if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
50
+ return path.join(wsPath, spector);
51
+ }
44
52
  async function loadWorkspace(wsPath) {
45
- const raw = await promises.readFile(wsPath, "utf8");
46
- return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(wsPath)) };
53
+ const resolved = await resolveWorkspacePath(wsPath);
54
+ const raw = await promises.readFile(resolved, "utf8");
55
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)) };
47
56
  }
48
57
  async function loadMocks(workspace, dir) {
49
58
  const mocks = [];
@@ -61,7 +70,7 @@ async function main() {
61
70
  const args = parseArgs(process.argv.slice(2));
62
71
  if (args.help) {
63
72
  console.log(
64
- "\nUsage:\n api-spector mock --workspace <path> [--name <server-name>]\n\nOptions:\n --workspace <path> Path to workspace.json (required)\n --name <name> Start only the named server (repeat for multiple)\n --help Show this message\n"
73
+ "\nUsage:\n api-spector mock --workspace <path> [--name <server-name>] [--log]\n\nOptions:\n --workspace <path> Path to workspace.json (required)\n --name <name> Start only the named server (repeat for multiple)\n --log Print each incoming request (matched and unmatched)\n --help Show this message\n"
65
74
  );
66
75
  process.exit(0);
67
76
  }
@@ -117,6 +126,34 @@ async function main() {
117
126
  if (started === 0) {
118
127
  process.exit(1);
119
128
  }
129
+ if (args.log) {
130
+ const serverNames = {};
131
+ const routePaths = {};
132
+ for (const mock of toStart) {
133
+ serverNames[mock.id] = mock.name;
134
+ for (const route of mock.routes) {
135
+ routePaths[route.id] = `${route.method} ${route.path}`;
136
+ }
137
+ }
138
+ mockServer.setHitCallback((hit) => {
139
+ const ts = new Date(hit.timestamp).toISOString().slice(11, 23);
140
+ const server = color(serverNames[hit.serverId] ?? hit.serverId, C.white);
141
+ const method = hit.method.padEnd(7);
142
+ const matched = hit.matchedRouteId !== null;
143
+ const status = color(String(hit.status), hit.status < 400 ? C.green : C.red);
144
+ const dur = color(`${hit.durationMs}ms`, C.gray);
145
+ if (matched) {
146
+ const route = color(routePaths[hit.matchedRouteId] ?? hit.matchedRouteId, C.gray);
147
+ console.log(
148
+ color(` ${ts}`, C.gray) + ` ${server} ` + color(method, C.cyan) + color(hit.path, C.white) + ` ${status} ${dur}` + color(` → ${route}`, C.gray)
149
+ );
150
+ } else {
151
+ console.log(
152
+ color(` ${ts}`, C.gray) + ` ${server} ` + color(method, C.yellow) + color(hit.path, C.yellow) + ` ${status} ${dur}` + color(" (no match)", C.yellow)
153
+ );
154
+ }
155
+ });
156
+ }
120
157
  console.log("");
121
158
  console.log(color(" Press Ctrl+C to stop all servers.\n", C.gray));
122
159
  async function shutdown() {
@@ -123,9 +123,18 @@ function parseArgs(argv) {
123
123
  }
124
124
  return args;
125
125
  }
126
+ async function resolveWorkspacePath(wsPath) {
127
+ const s = await promises.stat(wsPath);
128
+ if (!s.isDirectory()) return wsPath;
129
+ const entries = await promises.readdir(wsPath);
130
+ const spector = entries.find((e) => e.endsWith(".spector"));
131
+ if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
132
+ return path.join(wsPath, spector);
133
+ }
126
134
  async function loadWorkspace(wsPath) {
127
- const raw = await promises.readFile(wsPath, "utf8");
128
- return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(wsPath)) };
135
+ const resolved = await resolveWorkspacePath(wsPath);
136
+ const raw = await promises.readFile(resolved, "utf8");
137
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)) };
129
138
  }
130
139
  async function loadCollections(workspace, dir) {
131
140
  const cols = [];
@@ -81,6 +81,26 @@ const api = {
81
81
  inferContractSchema: (jsonBody) => electron.ipcRenderer.invoke("contract:inferSchema", jsonBody),
82
82
  // ─── Script hooks ─────────────────────────────────────────────────────────
83
83
  runScriptHook: (payload) => electron.ipcRenderer.invoke("script:run-hook", payload),
84
+ // ─── Git ──────────────────────────────────────────────────────────────────
85
+ gitIsRepo: () => electron.ipcRenderer.invoke("git:isRepo"),
86
+ gitInit: () => electron.ipcRenderer.invoke("git:init"),
87
+ gitStatus: () => electron.ipcRenderer.invoke("git:status"),
88
+ gitDiff: (filePath) => electron.ipcRenderer.invoke("git:diff", filePath),
89
+ gitDiffStaged: (filePath) => electron.ipcRenderer.invoke("git:diffStaged", filePath),
90
+ gitStage: (paths) => electron.ipcRenderer.invoke("git:stage", paths),
91
+ gitUnstage: (paths) => electron.ipcRenderer.invoke("git:unstage", paths),
92
+ gitStageAll: () => electron.ipcRenderer.invoke("git:stageAll"),
93
+ gitCommit: (message) => electron.ipcRenderer.invoke("git:commit", message),
94
+ gitLog: (limit) => electron.ipcRenderer.invoke("git:log", limit),
95
+ gitBranches: () => electron.ipcRenderer.invoke("git:branches"),
96
+ gitCheckout: (branch, create) => electron.ipcRenderer.invoke("git:checkout", branch, create),
97
+ gitPull: () => electron.ipcRenderer.invoke("git:pull"),
98
+ gitPush: (setUpstream) => electron.ipcRenderer.invoke("git:push", setUpstream),
99
+ gitRemotes: () => electron.ipcRenderer.invoke("git:remotes"),
100
+ gitAddRemote: (name, url) => electron.ipcRenderer.invoke("git:addRemote", name, url),
101
+ gitSetRemoteUrl: (name, url) => electron.ipcRenderer.invoke("git:setRemoteUrl", name, url),
102
+ gitRemoveRemote: (name) => electron.ipcRenderer.invoke("git:removeRemote", name),
103
+ gitWriteCiFile: (relPath, content) => electron.ipcRenderer.invoke("git:writeCiFile", relPath, content),
84
104
  // ─── Zoom ─────────────────────────────────────────────────────────────────
85
105
  setZoomFactor: (factor) => electron.webFrame.setZoomFactor(factor),
86
106
  // ─── Platform ─────────────────────────────────────────────────────────────