@mycadet/cli 0.12.6-dev.202610091338.ee7f3e3 → 0.12.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/cli",
3
- "version": "0.12.6-dev.202610091338.ee7f3e3",
3
+ "version": "0.12.7",
4
4
  "description": "CLI tools for mycadet",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "format": "prettier --write .",
22
22
  "format:check": "prettier --check .",
23
- "lint": "eslint --max-warnings 696",
23
+ "lint": "eslint --max-warnings 519",
24
24
  "lint:unused": "knip --include files,dependencies,unlisted",
25
25
  "compile:check": "bun build --compile src/index.ts --outfile /dev/null",
26
26
  "test": "bun run scripts/run-tests.ts",
@@ -40,7 +40,8 @@
40
40
  "nanoid": "5.1.7",
41
41
  "qrcode-terminal": "0.12.0",
42
42
  "react": "19.2.6",
43
- "react-devtools-core": "7.0.1"
43
+ "react-devtools-core": "7.0.1",
44
+ "zod": "4.3.6"
44
45
  },
45
46
  "bundledDependencies": [
46
47
  "@mycadet/assistant-api",
@@ -330,10 +330,10 @@ describe("hatch provider secrets", () => {
330
330
  expect(logs.join("\n")).toBe("");
331
331
  });
332
332
 
333
- test("endpoint-supplied providers are listed last but cannot back llm.defaultProvider", () => {
333
+ test("endpoint-supplied providers are listed last but cannot back llm.default", () => {
334
334
  // Listed so their keys can be collected, but hatch must not write them
335
- // as llm.default.provider — the assistant's DefaultProviderEnum rejects
336
- // them and hatch would fail at config parse.
335
+ // as llm.default.provider — the assistant's selection schema rejects
336
+ // them and the defaults would fall back to anthropic at resolution.
337
337
  expect(HATCH_PROVIDER_CHOICES.slice(-2)).toEqual(["litellm", "opencode"]);
338
338
  });
339
339
 
@@ -1,5 +1,4 @@
1
1
  import * as childProcess from "node:child_process";
2
- import type { ChildProcess } from "node:child_process";
3
2
  import { createHash } from "node:crypto";
4
3
  import * as fsModule from "node:fs";
5
4
  import {
@@ -23,6 +22,8 @@ import {
23
22
  test,
24
23
  } from "bun:test";
25
24
 
25
+ import { z } from "zod";
26
+
26
27
  import * as httpClient from "../lib/http-client.js";
27
28
  import type { AssistantEntry } from "../lib/assistant-config.js";
28
29
 
@@ -91,7 +92,7 @@ const workspaces: string[] = [];
91
92
  const fakePids = new Map<number, { alive: boolean; unkillable?: boolean }>();
92
93
 
93
94
  function installKillMock(): void {
94
- process.kill = mock((targetPid: number, signal?: string | number) => {
95
+ process.kill = mock((targetPid: number, signal?: string | number): true => {
95
96
  const entry = fakePids.get(targetPid);
96
97
  if (!entry) {
97
98
  return originalKill(targetPid, signal);
@@ -107,7 +108,7 @@ function installKillMock(): void {
107
108
  }
108
109
  entry.alive = false;
109
110
  return true;
110
- }) as unknown as typeof process.kill;
111
+ });
111
112
  }
112
113
 
113
114
  afterEach(() => {
@@ -135,10 +136,18 @@ function makeWorkspace(): string {
135
136
  return dir;
136
137
  }
137
138
 
138
- function readConfig(workspaceDir: string): Record<string, unknown> {
139
- return JSON.parse(
140
- readFileSync(join(workspaceDir, "config.json"), "utf-8"),
141
- ) as Record<string, unknown>;
139
+ const IngressTestConfigSchema = z.object({
140
+ ingress: z
141
+ .object({
142
+ nginx: z.record(z.string(), z.unknown()).optional(),
143
+ })
144
+ .optional(),
145
+ });
146
+
147
+ function readConfig(workspaceDir: string) {
148
+ return IngressTestConfigSchema.parse(
149
+ JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8")),
150
+ );
142
151
  }
143
152
 
144
153
  describe("buildIngressNginxConfig", () => {
@@ -571,7 +580,7 @@ describe("nginx ingress process state", () => {
571
580
  writeIngressState(ws, 7841);
572
581
  writePidFile(ws, pid);
573
582
  execFileSyncMock.mockReturnValue(nginxCommand(ws));
574
- process.kill = mock((targetPid: number, signal?: string | number) => {
583
+ process.kill = mock((targetPid: number, signal?: string | number): true => {
575
584
  if (targetPid !== pid) return originalKill(targetPid, signal);
576
585
  if (signal === 0) {
577
586
  if (!alive) throw new Error("dead");
@@ -582,12 +591,12 @@ describe("nginx ingress process state", () => {
582
591
  return true;
583
592
  }
584
593
  return true;
585
- }) as unknown as typeof process.kill;
594
+ });
586
595
 
587
596
  await expect(stopIngressNginx(ws)).resolves.toBe(true);
588
597
 
589
598
  const config = readConfig(ws);
590
- expect((config.ingress as Record<string, unknown>).nginx).toBeUndefined();
599
+ expect(config.ingress?.nginx).toBeUndefined();
591
600
  expect(existsSync(pidPath(ws))).toBe(false);
592
601
  });
593
602
 
@@ -597,16 +606,16 @@ describe("nginx ingress process state", () => {
597
606
  writeIngressState(ws, 7841);
598
607
  writePidFile(ws, pid);
599
608
  execFileSyncMock.mockReturnValue(nginxCommand(ws));
600
- process.kill = mock((targetPid: number, signal?: string | number) => {
609
+ process.kill = mock((targetPid: number, signal?: string | number): true => {
601
610
  if (targetPid !== pid) return originalKill(targetPid, signal);
602
611
  if (signal === 0) return true;
603
612
  throw new Error("operation not permitted");
604
- }) as unknown as typeof process.kill;
613
+ });
605
614
 
606
615
  await expect(stopIngressNginx(ws)).resolves.toBe(false);
607
616
 
608
617
  const config = readConfig(ws);
609
- expect((config.ingress as Record<string, unknown>).nginx).toEqual({
618
+ expect(config.ingress?.nginx).toEqual({
610
619
  listenPort: 7841,
611
620
  });
612
621
  expect(existsSync(pidPath(ws))).toBe(true);
@@ -619,7 +628,7 @@ describe("nginx ingress process state", () => {
619
628
  writeIngressState(ws, 7841);
620
629
  writePidFile(ws, pid);
621
630
  execFileSyncMock.mockReturnValue(nginxCommand(ws));
622
- process.kill = mock((targetPid: number, signal?: string | number) => {
631
+ process.kill = mock((targetPid: number, signal?: string | number): true => {
623
632
  if (targetPid !== pid) return originalKill(targetPid, signal);
624
633
  if (signal === 0) {
625
634
  aliveChecks++;
@@ -627,12 +636,12 @@ describe("nginx ingress process state", () => {
627
636
  throw new Error("dead");
628
637
  }
629
638
  throw new Error("no such process");
630
- }) as unknown as typeof process.kill;
639
+ });
631
640
 
632
641
  await expect(stopIngressNginx(ws)).resolves.toBe(true);
633
642
 
634
643
  const config = readConfig(ws);
635
- expect((config.ingress as Record<string, unknown>).nginx).toBeUndefined();
644
+ expect(config.ingress?.nginx).toBeUndefined();
636
645
  expect(existsSync(pidPath(ws))).toBe(false);
637
646
  });
638
647
 
@@ -644,16 +653,16 @@ describe("nginx ingress process state", () => {
644
653
  execFileSyncMock.mockReturnValue(
645
654
  "nginx: master process nginx -p /tmp/other-ingress -c /tmp/other-ingress/nginx.conf",
646
655
  );
647
- process.kill = mock((targetPid: number, signal?: string | number) => {
656
+ process.kill = mock((targetPid: number, signal?: string | number): true => {
648
657
  if (targetPid !== pid) return originalKill(targetPid, signal);
649
658
  if (signal === 0) return true;
650
659
  throw new Error("should not kill another nginx instance");
651
- }) as unknown as typeof process.kill;
660
+ });
652
661
 
653
662
  await expect(stopIngressNginx(ws)).resolves.toBe(false);
654
663
 
655
664
  const config = readConfig(ws);
656
- expect((config.ingress as Record<string, unknown>).nginx).toBeUndefined();
665
+ expect(config.ingress?.nginx).toBeUndefined();
657
666
  expect(existsSync(pidPath(ws))).toBe(false);
658
667
  });
659
668
  });
@@ -708,25 +717,20 @@ function mockNginxSpawn(): void {
708
717
  psArgs?.includes(String(SPAWNED_NGINX_PID))
709
718
  ? `nginx: master process nginx -p ${dir} -c ${join(dir, "nginx.conf")} -g daemon off;`
710
719
  : "") as typeof childProcess.execFileSync);
711
- return {
712
- unref: () => {},
713
- once: () => {},
714
- exitCode: null,
715
- pid: SPAWNED_NGINX_PID,
716
- } as unknown as ChildProcess;
720
+ const child = new childProcess.ChildProcess();
721
+ Object.assign(child, { pid: SPAWNED_NGINX_PID });
722
+ return child;
717
723
  }) as unknown as typeof childProcess.spawn);
718
724
  }
719
725
 
720
726
  /** Spawned nginx that exits on startup, as when the listen port is already bound. */
721
727
  function mockNginxSpawnExitsOnStartup(): void {
722
- spawnMock.mockReturnValue({
723
- unref: () => {},
724
- once: (event: string, listener: () => void) => {
725
- if (event === "exit") listener();
726
- },
727
- exitCode: 1,
728
- pid: SPAWNED_NGINX_PID,
729
- } as unknown as ChildProcess);
728
+ spawnMock.mockReturnValue(
729
+ Object.assign(new childProcess.ChildProcess(), {
730
+ pid: SPAWNED_NGINX_PID,
731
+ exitCode: 1,
732
+ }),
733
+ );
730
734
  }
731
735
 
732
736
  /**
@@ -735,20 +739,13 @@ function mockNginxSpawnExitsOnStartup(): void {
735
739
  */
736
740
  function mockNginxSpawnExitsAfterProbe(): void {
737
741
  spawnMock.mockImplementation((() => {
738
- const child = {
739
- unref: () => {},
740
- exitCode: null as number | null,
741
- pid: SPAWNED_NGINX_PID,
742
- once: (event: string, listener: () => void) => {
743
- if (event === "exit") {
744
- setTimeout(() => {
745
- child.exitCode = 1;
746
- listener();
747
- }, 150);
748
- }
749
- },
750
- };
751
- return child as unknown as ChildProcess;
742
+ const child = new childProcess.ChildProcess();
743
+ Object.assign(child, { pid: SPAWNED_NGINX_PID });
744
+ setTimeout(() => {
745
+ Object.assign(child, { exitCode: 1 });
746
+ child.emit("exit", 1);
747
+ }, 150);
748
+ return child;
752
749
  }) as unknown as typeof childProcess.spawn);
753
750
  }
754
751
 
@@ -944,7 +941,7 @@ describe("startRemoteWebIngress", () => {
944
941
  expect(conf).not.toContain("__remote-index.html");
945
942
  expect(conf).not.toContain("location ^~ /assistant/assets/");
946
943
  expect(conf).not.toContain("alias ");
947
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
944
+ expect(readConfig(ws).ingress?.nginx).toEqual({
948
945
  listenPort: 7845,
949
946
  includeWebApp: false,
950
947
  gatewayPort: 7830,
@@ -995,7 +992,7 @@ describe("startRemoteWebIngress", () => {
995
992
  expect(indexHtml).toContain(`"hubUrl":"${PRODUCTION_HUB_URL}"`);
996
993
  // No assistant name was provided, so the served config omits the label.
997
994
  expect(indexHtml).not.toContain("assistantName");
998
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
995
+ expect(readConfig(ws).ingress?.nginx).toEqual({
999
996
  listenPort: 7845,
1000
997
  includeWebApp: true,
1001
998
  gatewayPort: 7830,
@@ -1206,7 +1203,7 @@ describe("startRemoteWebIngress", () => {
1206
1203
  }),
1207
1204
  );
1208
1205
  expect(conf).toContain("proxy_pass http://127.0.0.1:7830;");
1209
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1206
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1210
1207
  listenPort: 7845,
1211
1208
  includeWebApp: false,
1212
1209
  gatewayPort: 7830,
@@ -1243,7 +1240,7 @@ describe("startRemoteWebIngress", () => {
1243
1240
  ipv6Loopback: hasIpv6Loopback(),
1244
1241
  }),
1245
1242
  );
1246
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1243
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1247
1244
  listenPort: 7845,
1248
1245
  includeWebApp: false,
1249
1246
  gatewayPort: 7900,
@@ -1335,7 +1332,7 @@ describe("startRemoteWebIngress", () => {
1335
1332
  "utf-8",
1336
1333
  );
1337
1334
  expect(indexHtml).toContain('"assistantName":"New Name"');
1338
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1335
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1339
1336
  listenPort: 7845,
1340
1337
  includeWebApp: true,
1341
1338
  gatewayPort: 7830,
@@ -1382,7 +1379,7 @@ describe("startRemoteWebIngress", () => {
1382
1379
  expect(edge.killed()).toBe(true);
1383
1380
  const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
1384
1381
  expect(conf).toContain("location = /readyz {");
1385
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1382
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1386
1383
  listenPort: 7845,
1387
1384
  includeWebApp: true,
1388
1385
  gatewayPort: 7830,
@@ -1410,7 +1407,7 @@ describe("startRemoteWebIngress", () => {
1410
1407
 
1411
1408
  expect(result.status).toBe("started");
1412
1409
  expect(edge.killed()).toBe(true);
1413
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1410
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1414
1411
  listenPort: 7845,
1415
1412
  includeWebApp: true,
1416
1413
  gatewayPort: 7830,
@@ -1479,7 +1476,7 @@ describe("startRemoteWebIngress", () => {
1479
1476
  ipv6Loopback: hasIpv6Loopback(),
1480
1477
  }),
1481
1478
  );
1482
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1479
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1483
1480
  listenPort: 7845,
1484
1481
  includeWebApp: false,
1485
1482
  gatewayPort: 7830,
@@ -1495,11 +1492,11 @@ describe("startRemoteWebIngress", () => {
1495
1492
  // Alive for the initial running check, dead by the time the stop helper
1496
1493
  // probes it: the race where the old edge exits on its own mid-switch.
1497
1494
  let liveChecks = 0;
1498
- process.kill = mock((targetPid: number, signal?: string | number) => {
1495
+ process.kill = mock((targetPid: number, signal?: string | number): true => {
1499
1496
  if (targetPid !== pid) return originalKill(targetPid, signal);
1500
1497
  if (signal === 0 && ++liveChecks === 1) return true;
1501
1498
  throw new Error("dead");
1502
- }) as unknown as typeof process.kill;
1499
+ });
1503
1500
 
1504
1501
  const result = await startRemoteWebIngress({
1505
1502
  workspaceDir: ws,
@@ -1523,7 +1520,7 @@ describe("startRemoteWebIngress", () => {
1523
1520
  ipv6Loopback: hasIpv6Loopback(),
1524
1521
  }),
1525
1522
  );
1526
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1523
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1527
1524
  listenPort: 7845,
1528
1525
  includeWebApp: false,
1529
1526
  gatewayPort: 7830,
@@ -1552,7 +1549,7 @@ describe("startRemoteWebIngress", () => {
1552
1549
  expect(spawnMock).toHaveBeenCalledTimes(1);
1553
1550
  const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
1554
1551
  expect(conf).toContain("location ^~ /assistant/ {");
1555
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1552
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1556
1553
  listenPort: 7845,
1557
1554
  includeWebApp: true,
1558
1555
  gatewayPort: 7830,
@@ -1577,7 +1574,7 @@ describe("startRemoteWebIngress", () => {
1577
1574
  expect(result).toEqual({ status: "web-dist-missing" });
1578
1575
  expect(edge.killed()).toBe(false);
1579
1576
  expect(spawnMock).not.toHaveBeenCalled();
1580
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
1577
+ expect(readConfig(ws).ingress?.nginx).toEqual({
1581
1578
  listenPort: 7845,
1582
1579
  includeWebApp: false,
1583
1580
  });
@@ -1606,9 +1603,7 @@ describe("startRemoteWebIngress", () => {
1606
1603
  logPath: join(ws, "data", "logs", "nginx-ingress.log"),
1607
1604
  });
1608
1605
  expect(spawnMock).toHaveBeenCalledTimes(1);
1609
- const ingress = readConfig(ws).ingress as
1610
- | Record<string, unknown>
1611
- | undefined;
1606
+ const ingress = readConfig(ws).ingress;
1612
1607
  expect(ingress?.nginx).toBeUndefined();
1613
1608
  });
1614
1609
 
@@ -1635,9 +1630,7 @@ describe("startRemoteWebIngress", () => {
1635
1630
  logPath: join(ws, "data", "logs", "nginx-ingress.log"),
1636
1631
  });
1637
1632
  expect(spawnMock).toHaveBeenCalledTimes(1);
1638
- const ingress = readConfig(ws).ingress as
1639
- | Record<string, unknown>
1640
- | undefined;
1633
+ const ingress = readConfig(ws).ingress;
1641
1634
  expect(ingress?.nginx).toBeUndefined();
1642
1635
  });
1643
1636
 
@@ -1698,16 +1691,15 @@ describe("startRemoteWebIngress", () => {
1698
1691
  // Alive for the whole settle window (nginx retrying its bind), never
1699
1692
  // writes a pid file; an orphan left running could win the bind later.
1700
1693
  spawnMock.mockImplementation((() => {
1701
- return {
1702
- unref: () => {},
1703
- exitCode: null,
1694
+ const child = new childProcess.ChildProcess();
1695
+ Object.assign(child, {
1704
1696
  pid: SPAWNED_NGINX_PID,
1705
- once: () => {},
1706
1697
  kill: (signal: string) => {
1707
1698
  kills.push(signal);
1708
1699
  return true;
1709
1700
  },
1710
- } as unknown as ChildProcess;
1701
+ });
1702
+ return child;
1711
1703
  }) as unknown as typeof childProcess.spawn);
1712
1704
 
1713
1705
  const result = await startRemoteWebIngress({
@@ -2036,7 +2028,7 @@ describe("ensureTunnelEdge", () => {
2036
2028
  const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
2037
2029
  expect(conf).toContain("location ^~ /assistant/ {");
2038
2030
  expect(conf).toContain("location ^~ /webhooks/ {");
2039
- expect((readConfig(ws).ingress as Record<string, unknown>).nginx).toEqual({
2031
+ expect(readConfig(ws).ingress?.nginx).toEqual({
2040
2032
  listenPort: REQUESTED_PORT,
2041
2033
  includeWebApp: true,
2042
2034
  gatewayPort: 7830,
@@ -2180,7 +2172,7 @@ describe("stopContainerTunnelEdge", () => {
2180
2172
  assistantId: "docker-1",
2181
2173
  runtimeUrl: `http://localhost:${port}`,
2182
2174
  cloud: "docker",
2183
- } as AssistantEntry;
2175
+ };
2184
2176
  }
2185
2177
 
2186
2178
  test("stops the default-workspace edge fronting this assistant", async () => {
@@ -2224,7 +2216,7 @@ describe("stopContainerTunnelEdge", () => {
2224
2216
  assistantId: "docker-1",
2225
2217
  runtimeUrl: "https://runtime.example.com/docker-1",
2226
2218
  cloud: "docker",
2227
- } as AssistantEntry),
2219
+ }),
2228
2220
  ).toBe(false);
2229
2221
  expect(killed()).toBe(false);
2230
2222
  });