@mycadet/cli 0.12.7 → 0.12.9
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/README.md +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +3 -2
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +2 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/ensure-binary.test.ts +182 -0
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +21 -20
- package/src/__tests__/ngrok-spawn.test.ts +49 -0
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +1 -8
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
- package/src/__tests__/ps-table.test.ts +79 -0
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +181 -197
- package/src/__tests__/tunnel.test.ts +257 -222
- package/src/__tests__/uninstall.test.ts +13 -4
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +260 -43
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +24 -24
- package/src/commands/client.ts +76 -76
- package/src/commands/completions.test.ts +104 -0
- package/src/commands/completions.ts +87 -0
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +12 -12
- package/src/commands/flags.ts +2 -2
- package/src/commands/hatch.ts +23 -23
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/ps.ts +244 -86
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +140 -22
- package/src/commands/retire.ts +48 -150
- package/src/commands/rollback.ts +20 -26
- package/src/commands/setup.ts +2 -2
- package/src/commands/sleep.ts +2 -2
- package/src/commands/teleport.ts +109 -109
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +138 -157
- package/src/commands/uninstall.ts +57 -9
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +30 -54
- package/src/commands/use.ts +1 -1
- package/src/commands/wake.ts +166 -50
- package/src/index.ts +18 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/docker.test.ts +168 -20
- package/src/lib/__tests__/job-polling.test.ts +2 -2
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
- package/src/lib/cloud-login.ts +24 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +260 -53
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +2 -2
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/resolve.ts +8 -8
- package/src/lib/flag-args.test.ts +1 -1
- package/src/lib/flag-args.ts +1 -1
- package/src/lib/guardian-token.ts +37 -5
- package/src/lib/hatch-local.ts +19 -23
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +2 -2
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +10 -31
- package/src/lib/nginx-ingress.ts +42 -15
- package/src/lib/pair.ts +1 -1
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/sync-cloud-assistants.ts +19 -19
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
- package/src/lib/upgrade-lifecycle.ts +8 -21
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- package/src/lib/managed-cloud.ts +0 -3
|
@@ -292,7 +292,7 @@ describe("hatch provider secrets", () => {
|
|
|
292
292
|
test("HATCH_PROVIDER_CHOICES only lists labeled providers", () => {
|
|
293
293
|
for (const provider of HATCH_PROVIDER_CHOICES) {
|
|
294
294
|
expect(formatProviderName(provider)).toBeTruthy();
|
|
295
|
-
// Cadet Cloud is the
|
|
295
|
+
// Cadet Cloud is the cloud-connection exception: no env var, no
|
|
296
296
|
// key collection — everything else must have a supported setup flow.
|
|
297
297
|
if (provider !== "cadet") {
|
|
298
298
|
expect(isSupportedLlmProvider(provider)).toBe(true);
|
|
@@ -313,7 +313,7 @@ describe("hatch provider secrets", () => {
|
|
|
313
313
|
]);
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
-
test("Cadet Cloud configures silently through the
|
|
316
|
+
test("Cadet Cloud configures silently through the cloud connection", async () => {
|
|
317
317
|
const { calls, fetchImpl } = makeFetch([]);
|
|
318
318
|
const logs: string[] = [];
|
|
319
319
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GuardianTokenData } from "../../lib/guardian-token.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A complete `GuardianTokenData` fixture with per-test overrides — the type's
|
|
5
|
+
* nine fields are all required, so partial fakes need this to stand in.
|
|
6
|
+
*/
|
|
7
|
+
export function makeGuardianTokenData(
|
|
8
|
+
over: Partial<GuardianTokenData> = {},
|
|
9
|
+
): GuardianTokenData {
|
|
10
|
+
return {
|
|
11
|
+
guardianPrincipalId: "principal-1",
|
|
12
|
+
accessToken: "local-token",
|
|
13
|
+
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
14
|
+
refreshToken: "refresh-token",
|
|
15
|
+
refreshTokenExpiresAt: new Date(Date.now() + 3_600_000).toISOString(),
|
|
16
|
+
refreshAfter: new Date(Date.now() + 30_000).toISOString(),
|
|
17
|
+
isNew: false,
|
|
18
|
+
deviceId: "device-1",
|
|
19
|
+
leasedAt: new Date().toISOString(),
|
|
20
|
+
...over,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bun-types' global fetch carries a `preconnect` member; these mocks only need
|
|
3
|
+
* the call shape, so hand the assignment a member-complete callable. The SUT
|
|
4
|
+
* never calls `preconnect`.
|
|
5
|
+
*/
|
|
6
|
+
export function withPreconnect(
|
|
7
|
+
impl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
|
|
8
|
+
): typeof globalThis.fetch {
|
|
9
|
+
return Object.assign(impl, { preconnect: () => {} });
|
|
10
|
+
}
|
|
@@ -158,9 +158,9 @@ describe("buildIngressNginxConfig", () => {
|
|
|
158
158
|
remoteWebIngress: {
|
|
159
159
|
webDistDir: "/tmp/cadet web/dist",
|
|
160
160
|
config: {
|
|
161
|
-
|
|
161
|
+
remote: "docker",
|
|
162
162
|
apiBaseUrl: "/v1",
|
|
163
|
-
|
|
163
|
+
cloudDisabled: true,
|
|
164
164
|
},
|
|
165
165
|
},
|
|
166
166
|
});
|
|
@@ -330,7 +330,7 @@ describe("buildIngressNginxConfig", () => {
|
|
|
330
330
|
expect(remoteConf).toContain("default_type application/json;");
|
|
331
331
|
expect(remoteConf).toContain('add_header Cache-Control "no-store";');
|
|
332
332
|
expect(remoteConf).toContain(
|
|
333
|
-
'return 200 "{\\"
|
|
333
|
+
'return 200 "{\\"remote\\":\\"docker\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true}";',
|
|
334
334
|
);
|
|
335
335
|
expect(remoteConf).not.toContain("assistantId");
|
|
336
336
|
});
|
|
@@ -347,7 +347,7 @@ describe("buildIngressNginxConfig", () => {
|
|
|
347
347
|
},
|
|
348
348
|
});
|
|
349
349
|
expect(named).toContain(
|
|
350
|
-
'return 200 "{\\"
|
|
350
|
+
'return 200 "{\\"remote\\":\\"custom\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true,\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
|
|
351
351
|
);
|
|
352
352
|
});
|
|
353
353
|
|
|
@@ -447,19 +447,19 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
447
447
|
const html =
|
|
448
448
|
'<html><head><script>window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}</script></head><body></body></html>';
|
|
449
449
|
const result = buildRemoteWebIndexHtml(html, {
|
|
450
|
-
|
|
450
|
+
remote: "docker",
|
|
451
451
|
apiBaseUrl: "/v1",
|
|
452
|
-
|
|
452
|
+
disableCloud: true,
|
|
453
453
|
});
|
|
454
454
|
|
|
455
455
|
expect(result).toContain(
|
|
456
456
|
'window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}',
|
|
457
457
|
);
|
|
458
458
|
expect(result).toContain(
|
|
459
|
-
'window.__CADET_CONFIG__={"
|
|
459
|
+
'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1","disableCloud":true}',
|
|
460
460
|
);
|
|
461
461
|
expect(result.indexOf('"webUrl"')).toBeLessThan(
|
|
462
|
-
result.indexOf('"
|
|
462
|
+
result.indexOf('"docker"'),
|
|
463
463
|
);
|
|
464
464
|
});
|
|
465
465
|
|
|
@@ -484,7 +484,7 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
484
484
|
"</head><body></body></html>",
|
|
485
485
|
].join("\n");
|
|
486
486
|
|
|
487
|
-
const result = buildRemoteWebIndexHtml(html, {
|
|
487
|
+
const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
|
|
488
488
|
|
|
489
489
|
expect(result.match(/rel="modulepreload"/g)).toHaveLength(4);
|
|
490
490
|
expect(result).toContain('href="/assistant/assets/a-1.js"');
|
|
@@ -506,7 +506,7 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
506
506
|
"</head><body></body></html>",
|
|
507
507
|
].join("\n");
|
|
508
508
|
|
|
509
|
-
const result = buildRemoteWebIndexHtml(html, {
|
|
509
|
+
const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
|
|
510
510
|
|
|
511
511
|
expect(result).not.toContain("modulepreload");
|
|
512
512
|
expect(result).not.toContain("/assistant/assets/chunk-0.js");
|
|
@@ -522,7 +522,7 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
522
522
|
);
|
|
523
523
|
const html = `<html><head>${preloads.join("\n")}</head><body></body></html>`;
|
|
524
524
|
|
|
525
|
-
const result = buildRemoteWebIndexHtml(html, {
|
|
525
|
+
const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
|
|
526
526
|
|
|
527
527
|
expect(result.match(/rel="modulepreload"/g)).toHaveLength(
|
|
528
528
|
MAX_PRESERVED_MODULE_PRELOADS,
|
|
@@ -862,10 +862,10 @@ function spaConfigHash(
|
|
|
862
862
|
JSON.stringify({
|
|
863
863
|
template: EDGE_TEMPLATE_VERSION,
|
|
864
864
|
config: {
|
|
865
|
-
|
|
865
|
+
remote: "custom",
|
|
866
866
|
apiBaseUrl: "/v1",
|
|
867
|
-
|
|
868
|
-
|
|
867
|
+
cloudDisabled: true,
|
|
868
|
+
disableCloud: true,
|
|
869
869
|
...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
|
|
870
870
|
...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
|
|
871
871
|
hubUrl: PRODUCTION_HUB_URL,
|
|
@@ -988,7 +988,7 @@ describe("startRemoteWebIngress", () => {
|
|
|
988
988
|
join(ws, "data", "ingress", "assistant-index.html"),
|
|
989
989
|
"utf-8",
|
|
990
990
|
);
|
|
991
|
-
expect(indexHtml).toContain("remote
|
|
991
|
+
expect(indexHtml).toContain('"remote":"custom"');
|
|
992
992
|
expect(indexHtml).toContain(`"hubUrl":"${PRODUCTION_HUB_URL}"`);
|
|
993
993
|
// No assistant name was provided, so the served config omits the label.
|
|
994
994
|
expect(indexHtml).not.toContain("assistantName");
|
|
@@ -1352,10 +1352,10 @@ describe("startRemoteWebIngress", () => {
|
|
|
1352
1352
|
JSON.stringify({
|
|
1353
1353
|
template: 1,
|
|
1354
1354
|
config: {
|
|
1355
|
-
|
|
1355
|
+
remote: "custom",
|
|
1356
1356
|
apiBaseUrl: "/v1",
|
|
1357
|
-
|
|
1358
|
-
|
|
1357
|
+
cloudDisabled: true,
|
|
1358
|
+
disableCloud: true,
|
|
1359
1359
|
hubUrl: PRODUCTION_HUB_URL,
|
|
1360
1360
|
},
|
|
1361
1361
|
}),
|
|
@@ -2095,8 +2095,9 @@ describe("ensureTunnelEdge", () => {
|
|
|
2095
2095
|
gatewayPort: 7830,
|
|
2096
2096
|
});
|
|
2097
2097
|
|
|
2098
|
-
await expect(promise).rejects.toThrow(
|
|
2099
|
-
|
|
2098
|
+
await expect(promise).rejects.toThrow(
|
|
2099
|
+
"See https://nginx.org/en/docs/install.html for how to install",
|
|
2100
|
+
);
|
|
2100
2101
|
await expect(promise).rejects.toThrow("NGINX_BIN");
|
|
2101
2102
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
2102
2103
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { buildNgrokSpawnArgs, isNgrokAuthError } from "../lib/tunnels/ngrok.js";
|
|
4
|
+
|
|
5
|
+
describe("buildNgrokSpawnArgs", () => {
|
|
6
|
+
test("spawns a plain http agent against the loopback edge", () => {
|
|
7
|
+
expect(buildNgrokSpawnArgs(7830)).toEqual([
|
|
8
|
+
"http",
|
|
9
|
+
"127.0.0.1:7830",
|
|
10
|
+
"--log=stdout",
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("never passes --config so the default (authtoken-bearing) config loads", () => {
|
|
15
|
+
const args = buildNgrokSpawnArgs(7830, "me.ngrok.app");
|
|
16
|
+
expect(args.some((arg) => arg.startsWith("--config"))).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("binds a reserved domain when given", () => {
|
|
20
|
+
expect(buildNgrokSpawnArgs(7830, "my-assistant.ngrok.app")).toEqual([
|
|
21
|
+
"http",
|
|
22
|
+
"127.0.0.1:7830",
|
|
23
|
+
"--log=stdout",
|
|
24
|
+
"--domain=my-assistant.ngrok.app",
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("isNgrokAuthError", () => {
|
|
30
|
+
test("recognizes the free-account authtoken requirement", () => {
|
|
31
|
+
expect(
|
|
32
|
+
isNgrokAuthError(
|
|
33
|
+
"FATAL: authentication failed: Usage of ngrok requires a free authtoken. ERR_NGROK_105",
|
|
34
|
+
),
|
|
35
|
+
).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("recognizes an unauthorized token", () => {
|
|
39
|
+
expect(isNgrokAuthError("unauthorized authtoken ERR_NGROK_401")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("does not claim other startup failures are auth errors", () => {
|
|
43
|
+
expect(
|
|
44
|
+
isNgrokAuthError(
|
|
45
|
+
"creating tunnel session failed: dial tcp 3.219.x.x:443: connection refused",
|
|
46
|
+
),
|
|
47
|
+
).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -141,7 +141,7 @@ describe("loadAllAssistantsAcrossEnvs", () => {
|
|
|
141
141
|
function makeEnv(name: string, lockfileDir: string): EnvironmentDefinition {
|
|
142
142
|
return {
|
|
143
143
|
name,
|
|
144
|
-
|
|
144
|
+
cloudUrl: "https://example.invalid",
|
|
145
145
|
webUrl: "https://example.invalid",
|
|
146
146
|
lockfileDirOverride: lockfileDir,
|
|
147
147
|
};
|
|
@@ -20,14 +20,7 @@ const originalEnvironment = process.env.CADET_ENVIRONMENT;
|
|
|
20
20
|
process.env.CADET_ENVIRONMENT = "production";
|
|
21
21
|
|
|
22
22
|
import { buildAppConnectUrl, pair } from "../commands/pair.js";
|
|
23
|
-
|
|
24
|
-
// bun-types' global fetch carries a `preconnect` member; these mocks only need
|
|
25
|
-
// the call shape, so hand the assignment a member-complete callable.
|
|
26
|
-
function withPreconnect(
|
|
27
|
-
impl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
|
|
28
|
-
): typeof fetch {
|
|
29
|
-
return Object.assign(impl, { preconnect: () => {} });
|
|
30
|
-
}
|
|
23
|
+
import { withPreconnect } from "./helpers/http.js";
|
|
31
24
|
|
|
32
25
|
// Distinct loopback (mint) vs reachable (advertised) URLs to verify the split.
|
|
33
26
|
const LOCAL_URL = "http://127.0.0.1:7830";
|
|
@@ -26,12 +26,16 @@ process.env.CADET_LOCKFILE_DIR = testDir;
|
|
|
26
26
|
import * as assistantConfig from "../lib/assistant-config.js";
|
|
27
27
|
import * as healthCheck from "../lib/health-check.js";
|
|
28
28
|
import * as orphanDetection from "../lib/orphan-detection.js";
|
|
29
|
-
import * as
|
|
29
|
+
import * as cloudClient from "../lib/cloud-client.js";
|
|
30
30
|
|
|
31
31
|
const loadAllAssistantsMock = spyOn(
|
|
32
32
|
assistantConfig,
|
|
33
33
|
"loadAllAssistants",
|
|
34
34
|
).mockReturnValue([]);
|
|
35
|
+
const readRootProcessesMock = spyOn(
|
|
36
|
+
assistantConfig,
|
|
37
|
+
"readRootProcesses",
|
|
38
|
+
).mockReturnValue({});
|
|
35
39
|
const getActiveAssistantMock = spyOn(
|
|
36
40
|
assistantConfig,
|
|
37
41
|
"getActiveAssistant",
|
|
@@ -40,31 +44,31 @@ const detectOrphanedProcessesMock = spyOn(
|
|
|
40
44
|
orphanDetection,
|
|
41
45
|
"detectOrphanedProcesses",
|
|
42
46
|
).mockResolvedValue([]);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
"
|
|
47
|
+
const getCloudUrlMock = spyOn(
|
|
48
|
+
cloudClient,
|
|
49
|
+
"getCloudUrl",
|
|
46
50
|
).mockReturnValue("http://platform.test");
|
|
47
51
|
|
|
48
|
-
// Per-test toggle for `
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
+
// Per-test toggle for `readCloudToken`.
|
|
53
|
+
const readCloudTokenMock = spyOn(
|
|
54
|
+
cloudClient,
|
|
55
|
+
"readCloudToken",
|
|
52
56
|
).mockReturnValue(null);
|
|
53
57
|
|
|
54
|
-
// `fetchCurrentUser` + `
|
|
58
|
+
// `fetchCurrentUser` + `fetchCloudAssistants` are spied so we can assert
|
|
55
59
|
// they're never invoked on the no-token path, and re-shaped per-test for the
|
|
56
60
|
// token-but-unreachable path.
|
|
57
61
|
const fetchCurrentUserMock = spyOn(
|
|
58
|
-
|
|
62
|
+
cloudClient,
|
|
59
63
|
"fetchCurrentUser",
|
|
60
64
|
).mockResolvedValue({
|
|
61
65
|
id: "u1",
|
|
62
66
|
email: "test@example.com",
|
|
63
67
|
display: "Test",
|
|
64
68
|
});
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
"
|
|
69
|
+
const fetchCloudAssistantsMock = spyOn(
|
|
70
|
+
cloudClient,
|
|
71
|
+
"fetchCloudAssistants",
|
|
68
72
|
).mockResolvedValue([]);
|
|
69
73
|
const checkManagedHealthMock = spyOn(
|
|
70
74
|
healthCheck,
|
|
@@ -99,33 +103,36 @@ beforeEach(() => {
|
|
|
99
103
|
}) as typeof process.stdout.write;
|
|
100
104
|
});
|
|
101
105
|
|
|
106
|
+
const NL = String.fromCharCode(10);
|
|
107
|
+
|
|
102
108
|
afterEach(() => {
|
|
103
109
|
console.log = originalLog;
|
|
104
110
|
console.error = originalError;
|
|
105
111
|
process.stdout.write = originalStdoutWrite;
|
|
106
112
|
loadAllAssistantsMock.mockReturnValue([]);
|
|
107
113
|
getActiveAssistantMock.mockReturnValue(null);
|
|
108
|
-
|
|
114
|
+
readCloudTokenMock.mockReturnValue(null);
|
|
109
115
|
fetchCurrentUserMock.mockReset();
|
|
110
116
|
fetchCurrentUserMock.mockResolvedValue({
|
|
111
117
|
id: "u1",
|
|
112
118
|
email: "test@example.com",
|
|
113
119
|
display: "Test",
|
|
114
120
|
});
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
fetchCloudAssistantsMock.mockReset();
|
|
122
|
+
fetchCloudAssistantsMock.mockResolvedValue([]);
|
|
117
123
|
checkManagedHealthMock.mockReset();
|
|
118
124
|
checkManagedHealthMock.mockResolvedValue({ status: "healthy", detail: null });
|
|
125
|
+
readRootProcessesMock.mockReturnValue({});
|
|
119
126
|
});
|
|
120
127
|
|
|
121
128
|
afterAll(() => {
|
|
122
129
|
loadAllAssistantsMock.mockRestore();
|
|
123
130
|
getActiveAssistantMock.mockRestore();
|
|
124
131
|
detectOrphanedProcessesMock.mockRestore();
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
getCloudUrlMock.mockRestore();
|
|
133
|
+
readCloudTokenMock.mockRestore();
|
|
127
134
|
fetchCurrentUserMock.mockRestore();
|
|
128
|
-
|
|
135
|
+
fetchCloudAssistantsMock.mockRestore();
|
|
129
136
|
checkManagedHealthMock.mockRestore();
|
|
130
137
|
rmSync(testDir, { recursive: true, force: true });
|
|
131
138
|
});
|
|
@@ -142,7 +149,7 @@ import { listAllAssistants } from "../commands/ps.js";
|
|
|
142
149
|
|
|
143
150
|
describe("cadet ps — platform status line", () => {
|
|
144
151
|
test("no local token: no login-status line, skips ALL network fetches", async () => {
|
|
145
|
-
|
|
152
|
+
readCloudTokenMock.mockReturnValue(null);
|
|
146
153
|
|
|
147
154
|
await listAllAssistants(false);
|
|
148
155
|
|
|
@@ -155,20 +162,20 @@ describe("cadet ps — platform status line", () => {
|
|
|
155
162
|
false,
|
|
156
163
|
);
|
|
157
164
|
|
|
158
|
-
// Structural guarantee: we never even tried to talk to the
|
|
165
|
+
// Structural guarantee: we never even tried to talk to the cloud.
|
|
159
166
|
expect(fetchCurrentUserMock).not.toHaveBeenCalled();
|
|
160
|
-
expect(
|
|
167
|
+
expect(fetchCloudAssistantsMock).not.toHaveBeenCalled();
|
|
161
168
|
});
|
|
162
169
|
|
|
163
170
|
test("local token present but platform is unreachable: no status line, no leaked org-fetch error", async () => {
|
|
164
|
-
|
|
171
|
+
readCloudTokenMock.mockReturnValue("session_abc123");
|
|
165
172
|
// Simulate the exact Bun connect failure the user reported:
|
|
166
173
|
// "Unable to connect. Is the computer able to access the url?"
|
|
167
174
|
const connectError = new Error(
|
|
168
175
|
"Unable to connect. Is the computer able to access the url?",
|
|
169
176
|
);
|
|
170
177
|
fetchCurrentUserMock.mockRejectedValue(connectError);
|
|
171
|
-
|
|
178
|
+
fetchCloudAssistantsMock.mockRejectedValue(connectError);
|
|
172
179
|
|
|
173
180
|
await listAllAssistants(false);
|
|
174
181
|
|
|
@@ -183,13 +190,13 @@ describe("cadet ps — platform status line", () => {
|
|
|
183
190
|
});
|
|
184
191
|
|
|
185
192
|
test("local token present and platform reachable: no login-status line", async () => {
|
|
186
|
-
|
|
193
|
+
readCloudTokenMock.mockReturnValue("session_abc123");
|
|
187
194
|
fetchCurrentUserMock.mockResolvedValue({
|
|
188
195
|
id: "u1",
|
|
189
196
|
email: "user@example.com",
|
|
190
197
|
display: "Example User",
|
|
191
198
|
});
|
|
192
|
-
|
|
199
|
+
fetchCloudAssistantsMock.mockResolvedValue([]);
|
|
193
200
|
|
|
194
201
|
await listAllAssistants(false);
|
|
195
202
|
|
|
@@ -217,6 +224,52 @@ describe("cadet ps — platform status line", () => {
|
|
|
217
224
|
}
|
|
218
225
|
});
|
|
219
226
|
|
|
227
|
+
test("CLI-managed root processes nest under the assistant they front", async () => {
|
|
228
|
+
loadAllAssistantsMock.mockReturnValue([
|
|
229
|
+
{
|
|
230
|
+
assistantId: "assistant-123",
|
|
231
|
+
name: "Alice",
|
|
232
|
+
runtimeUrl: "https://platform.example",
|
|
233
|
+
cloud: "cadet",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
assistantId: "assistant-456",
|
|
237
|
+
name: "Bob",
|
|
238
|
+
runtimeUrl: "https://platform.example/b",
|
|
239
|
+
cloud: "cadet",
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
getActiveAssistantMock.mockReturnValue(null);
|
|
243
|
+
checkManagedHealthMock.mockImplementation(async () => ({
|
|
244
|
+
status: "healthy",
|
|
245
|
+
detail: null,
|
|
246
|
+
}));
|
|
247
|
+
// The tunnel fronts Alice (recorded id); the edge records no assistant.
|
|
248
|
+
readRootProcessesMock.mockReturnValue({
|
|
249
|
+
tunnel: {
|
|
250
|
+
pid: 4242,
|
|
251
|
+
provider: "ngrok",
|
|
252
|
+
startedAt: "2026-10-11T00:00:00.000Z",
|
|
253
|
+
assistantId: "assistant-123",
|
|
254
|
+
},
|
|
255
|
+
app: { pid: 77, startedAt: "2026-10-11T00:00:00.000Z" },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await listAllAssistants(false);
|
|
259
|
+
|
|
260
|
+
const output = stdout.join(NL);
|
|
261
|
+
// The tunnel nests under Alice — its name is just the provider now…
|
|
262
|
+
expect(output).toContain("└ ngrok");
|
|
263
|
+
// …the unattributed edge renders machine-level…
|
|
264
|
+
expect(output).toContain("nginx edge");
|
|
265
|
+
// …the type column distinguishes the three process kinds…
|
|
266
|
+
expect(output).toContain("🧑🚀 Assistant");
|
|
267
|
+
expect(output).toContain("🚇 Tunnel");
|
|
268
|
+
expect(output).toContain("🌐 App");
|
|
269
|
+
// …and the second table is gone: one header row only.
|
|
270
|
+
expect(output.match(/NAME/g)?.length).toBe(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
220
273
|
test("assistant rows use display name as primary label and keep id visible", async () => {
|
|
221
274
|
loadAllAssistantsMock.mockReturnValue([
|
|
222
275
|
{
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { displayWidth, formatRow } from "../commands/ps.js";
|
|
4
|
+
|
|
5
|
+
describe("displayWidth", () => {
|
|
6
|
+
test("ascii strings count one cell per character", () => {
|
|
7
|
+
expect(displayWidth("healthy")).toBe(7);
|
|
8
|
+
expect(displayWidth("")).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("emoji count two cells", () => {
|
|
12
|
+
expect(displayWidth("🟢")).toBe(2);
|
|
13
|
+
expect(displayWidth("🚀")).toBe(2);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("the ZWJ astronaut sequence counts two cells, not its UTF-16 length", () => {
|
|
17
|
+
// "🧑🚀" is 5 UTF-16 units but renders 2 cells wide.
|
|
18
|
+
expect(displayWidth("🧑🚀 Assistant")).toBe(12);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a variation selector widens a narrow symbol to two cells", () => {
|
|
22
|
+
expect(displayWidth("⚠️ warn")).toBe(7);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("ps table alignment", () => {
|
|
27
|
+
const colWidths = { name: 15, type: 14, status: 10, info: 0 };
|
|
28
|
+
|
|
29
|
+
/** The terminal column where `marker` starts (string indices shift under
|
|
30
|
+
* emoji, which render 2 cells but span 2 UTF-16 units or more). */
|
|
31
|
+
const columnOf = (s: string, marker: string): number =>
|
|
32
|
+
displayWidth(s.slice(0, s.indexOf(marker)));
|
|
33
|
+
|
|
34
|
+
test("emoji cells keep every later column aligned", () => {
|
|
35
|
+
const emojiRow = formatRow(
|
|
36
|
+
{
|
|
37
|
+
name: "slim-yak",
|
|
38
|
+
type: "🧑🚀 Assistant",
|
|
39
|
+
status: "🟢 healthy",
|
|
40
|
+
info: "http://x",
|
|
41
|
+
},
|
|
42
|
+
colWidths,
|
|
43
|
+
);
|
|
44
|
+
const asciiRow = formatRow(
|
|
45
|
+
{
|
|
46
|
+
name: "plain",
|
|
47
|
+
type: "Assistant",
|
|
48
|
+
status: "healthy",
|
|
49
|
+
info: "http://y",
|
|
50
|
+
},
|
|
51
|
+
colWidths,
|
|
52
|
+
);
|
|
53
|
+
expect(columnOf(emojiRow, "http://x")).toBe(columnOf(asciiRow, "http://y"));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("the header separator matches the emoji row's column starts", () => {
|
|
57
|
+
const widths = {
|
|
58
|
+
name: displayWidth("slim-yak-yhpdq6"),
|
|
59
|
+
type: displayWidth("🧑🚀 Assistant"),
|
|
60
|
+
status: displayWidth("🟢 healthy"),
|
|
61
|
+
info: 0,
|
|
62
|
+
};
|
|
63
|
+
const header = formatRow(
|
|
64
|
+
{ name: "NAME", type: "TYPE", status: "STATUS", info: "INFO" },
|
|
65
|
+
widths,
|
|
66
|
+
);
|
|
67
|
+
const row = formatRow(
|
|
68
|
+
{
|
|
69
|
+
name: "slim-yak-yhpdq6",
|
|
70
|
+
type: "🧑🚀 Assistant",
|
|
71
|
+
status: "🟢 healthy",
|
|
72
|
+
info: "http://127.0.0.1:7830",
|
|
73
|
+
},
|
|
74
|
+
widths,
|
|
75
|
+
);
|
|
76
|
+
expect(columnOf(header, "TYPE")).toBe(columnOf(row, "🧑🚀"));
|
|
77
|
+
expect(columnOf(header, "STATUS")).toBe(columnOf(row, "🟢"));
|
|
78
|
+
});
|
|
79
|
+
});
|