@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 +6 -2
- package/out/main/chunks/{mock-server-C0RlwZf_.js → mock-server-Cx-xG4pJ.js} +8 -3
- package/out/main/index.js +122 -2
- package/out/main/mock.js +41 -4
- package/out/main/runner.js +11 -2
- package/out/preload/index.js +20 -0
- package/out/renderer/assets/{index-C67sPzTG.js → index-B7b-1sj6.js} +1159 -146
- package/out/renderer/assets/index-QI8AWMd3.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +2 -1
- package/out/renderer/assets/index-YkI5DFME.css +0 -2
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(
|
|
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-
|
|
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-
|
|
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
|
|
46
|
-
|
|
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() {
|
package/out/main/runner.js
CHANGED
|
@@ -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
|
|
128
|
-
|
|
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 = [];
|
package/out/preload/index.js
CHANGED
|
@@ -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 ─────────────────────────────────────────────────────────────
|