@testsmith/api-spector 0.1.0 → 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/out/main/index.js CHANGED
@@ -35,6 +35,7 @@ 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,120 @@ 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
+ }
3439
3554
  if (process.env.ELECTRON_NO_SANDBOX === "1") {
3440
3555
  electron.app.commandLine.appendSwitch("no-sandbox");
3441
3556
  electron.app.commandLine.appendSwitch("disable-features", "RendererCodeIntegrity");
@@ -3536,6 +3651,7 @@ electron.app.whenReady().then(async () => {
3536
3651
  registerSoapHandlers(electron.ipcMain);
3537
3652
  registerDocsHandlers(electron.ipcMain);
3538
3653
  registerContractHandlers(electron.ipcMain);
3654
+ registerGitHandlers(electron.ipcMain);
3539
3655
  createWindow();
3540
3656
  electron.app.on("activate", () => {
3541
3657
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
@@ -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 ─────────────────────────────────────────────────────────────