@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64
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 +6 -4
- 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 +1 -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 +15 -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 +149 -85
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -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 +180 -211
- package/src/__tests__/tunnel.test.ts +490 -270
- package/src/__tests__/uninstall.test.ts +150 -9
- 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 -78
- 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 +27 -24
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +232 -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 +110 -129
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +156 -175
- package/src/commands/uninstall.ts +137 -54
- 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-gateway-cli.test.ts +42 -0
- 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} +105 -99
- package/src/lib/cloud-login.ts +23 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/color.ts +4 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +300 -111
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +29 -3
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +10 -12
- 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 +32 -29
- package/src/lib/hatch-next-steps.ts +5 -9
- 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 +46 -34
- package/src/lib/nginx-ingress.ts +40 -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/step-runner.ts +6 -1
- 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,8 @@ describe("buildIngressNginxConfig", () => {
|
|
|
158
158
|
remoteWebIngress: {
|
|
159
159
|
webDistDir: "/tmp/cadet web/dist",
|
|
160
160
|
config: {
|
|
161
|
-
|
|
161
|
+
remote: "docker",
|
|
162
162
|
apiBaseUrl: "/v1",
|
|
163
|
-
platformDisabled: true,
|
|
164
163
|
},
|
|
165
164
|
},
|
|
166
165
|
});
|
|
@@ -330,7 +329,7 @@ describe("buildIngressNginxConfig", () => {
|
|
|
330
329
|
expect(remoteConf).toContain("default_type application/json;");
|
|
331
330
|
expect(remoteConf).toContain('add_header Cache-Control "no-store";');
|
|
332
331
|
expect(remoteConf).toContain(
|
|
333
|
-
'return 200 "{\\"
|
|
332
|
+
'return 200 "{\\"remote\\":\\"docker\\",\\"apiBaseUrl\\":\\"/v1\\"}";',
|
|
334
333
|
);
|
|
335
334
|
expect(remoteConf).not.toContain("assistantId");
|
|
336
335
|
});
|
|
@@ -347,7 +346,7 @@ describe("buildIngressNginxConfig", () => {
|
|
|
347
346
|
},
|
|
348
347
|
});
|
|
349
348
|
expect(named).toContain(
|
|
350
|
-
'return 200 "{\\"
|
|
349
|
+
'return 200 "{\\"remote\\":\\"custom\\",\\"apiBaseUrl\\":\\"/v1\\",\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
|
|
351
350
|
);
|
|
352
351
|
});
|
|
353
352
|
|
|
@@ -447,19 +446,18 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
447
446
|
const html =
|
|
448
447
|
'<html><head><script>window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}</script></head><body></body></html>';
|
|
449
448
|
const result = buildRemoteWebIndexHtml(html, {
|
|
450
|
-
|
|
449
|
+
remote: "docker",
|
|
451
450
|
apiBaseUrl: "/v1",
|
|
452
|
-
disablePlatform: true,
|
|
453
451
|
});
|
|
454
452
|
|
|
455
453
|
expect(result).toContain(
|
|
456
454
|
'window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}',
|
|
457
455
|
);
|
|
458
456
|
expect(result).toContain(
|
|
459
|
-
'window.__CADET_CONFIG__={"
|
|
457
|
+
'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1"}',
|
|
460
458
|
);
|
|
461
459
|
expect(result.indexOf('"webUrl"')).toBeLessThan(
|
|
462
|
-
result.indexOf('"
|
|
460
|
+
result.indexOf('"docker"'),
|
|
463
461
|
);
|
|
464
462
|
});
|
|
465
463
|
|
|
@@ -484,7 +482,7 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
484
482
|
"</head><body></body></html>",
|
|
485
483
|
].join("\n");
|
|
486
484
|
|
|
487
|
-
const result = buildRemoteWebIndexHtml(html, {
|
|
485
|
+
const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
|
|
488
486
|
|
|
489
487
|
expect(result.match(/rel="modulepreload"/g)).toHaveLength(4);
|
|
490
488
|
expect(result).toContain('href="/assistant/assets/a-1.js"');
|
|
@@ -506,7 +504,7 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
506
504
|
"</head><body></body></html>",
|
|
507
505
|
].join("\n");
|
|
508
506
|
|
|
509
|
-
const result = buildRemoteWebIndexHtml(html, {
|
|
507
|
+
const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
|
|
510
508
|
|
|
511
509
|
expect(result).not.toContain("modulepreload");
|
|
512
510
|
expect(result).not.toContain("/assistant/assets/chunk-0.js");
|
|
@@ -522,7 +520,7 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
522
520
|
);
|
|
523
521
|
const html = `<html><head>${preloads.join("\n")}</head><body></body></html>`;
|
|
524
522
|
|
|
525
|
-
const result = buildRemoteWebIndexHtml(html, {
|
|
523
|
+
const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
|
|
526
524
|
|
|
527
525
|
expect(result.match(/rel="modulepreload"/g)).toHaveLength(
|
|
528
526
|
MAX_PRESERVED_MODULE_PRELOADS,
|
|
@@ -862,10 +860,8 @@ function spaConfigHash(
|
|
|
862
860
|
JSON.stringify({
|
|
863
861
|
template: EDGE_TEMPLATE_VERSION,
|
|
864
862
|
config: {
|
|
865
|
-
|
|
863
|
+
remote: "custom",
|
|
866
864
|
apiBaseUrl: "/v1",
|
|
867
|
-
platformDisabled: true,
|
|
868
|
-
disablePlatform: true,
|
|
869
865
|
...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
|
|
870
866
|
...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
|
|
871
867
|
hubUrl: PRODUCTION_HUB_URL,
|
|
@@ -988,7 +984,7 @@ describe("startRemoteWebIngress", () => {
|
|
|
988
984
|
join(ws, "data", "ingress", "assistant-index.html"),
|
|
989
985
|
"utf-8",
|
|
990
986
|
);
|
|
991
|
-
expect(indexHtml).toContain("remote
|
|
987
|
+
expect(indexHtml).toContain('"remote":"custom"');
|
|
992
988
|
expect(indexHtml).toContain(`"hubUrl":"${PRODUCTION_HUB_URL}"`);
|
|
993
989
|
// No assistant name was provided, so the served config omits the label.
|
|
994
990
|
expect(indexHtml).not.toContain("assistantName");
|
|
@@ -1352,10 +1348,8 @@ describe("startRemoteWebIngress", () => {
|
|
|
1352
1348
|
JSON.stringify({
|
|
1353
1349
|
template: 1,
|
|
1354
1350
|
config: {
|
|
1355
|
-
|
|
1351
|
+
remote: "custom",
|
|
1356
1352
|
apiBaseUrl: "/v1",
|
|
1357
|
-
platformDisabled: true,
|
|
1358
|
-
disablePlatform: true,
|
|
1359
1353
|
hubUrl: PRODUCTION_HUB_URL,
|
|
1360
1354
|
},
|
|
1361
1355
|
}),
|
|
@@ -2095,8 +2089,9 @@ describe("ensureTunnelEdge", () => {
|
|
|
2095
2089
|
gatewayPort: 7830,
|
|
2096
2090
|
});
|
|
2097
2091
|
|
|
2098
|
-
await expect(promise).rejects.toThrow(
|
|
2099
|
-
|
|
2092
|
+
await expect(promise).rejects.toThrow(
|
|
2093
|
+
"See https://nginx.org/en/docs/install.html for how to install",
|
|
2094
|
+
);
|
|
2100
2095
|
await expect(promise).rejects.toThrow("NGINX_BIN");
|
|
2101
2096
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
2102
2097
|
});
|
|
@@ -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
|
};
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
beforeEach,
|
|
5
5
|
describe,
|
|
6
6
|
expect,
|
|
7
|
+
mock,
|
|
7
8
|
spyOn,
|
|
8
9
|
test,
|
|
9
10
|
} from "bun:test";
|
|
@@ -19,15 +20,35 @@ process.env.CADET_LOCKFILE_DIR = testDir;
|
|
|
19
20
|
const originalEnvironment = process.env.CADET_ENVIRONMENT;
|
|
20
21
|
process.env.CADET_ENVIRONMENT = "production";
|
|
21
22
|
|
|
22
|
-
import {
|
|
23
|
+
import { withPreconnect } from "./helpers/http.js";
|
|
24
|
+
|
|
25
|
+
// The gateway-CLI mint runs in a SUBPROCESS for local and docker assistants;
|
|
26
|
+
// answer it in-process and record the invocations (the fetch stubs below
|
|
27
|
+
// still serve the loopback HTTP mint the other topologies keep using).
|
|
28
|
+
const mintCalls: Array<{ command: string; args: readonly string[] }> = [];
|
|
29
|
+
const actualStepRunner = await import("../lib/step-runner.js");
|
|
30
|
+
const mockExecOutput = mock(
|
|
31
|
+
(command: string, args: readonly string[]): Promise<string> => {
|
|
32
|
+
mintCalls.push({ command, args });
|
|
33
|
+
const mintedUrl = args[args.length - 1] ?? "";
|
|
34
|
+
return Promise.resolve(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
deviceCode: "device-code",
|
|
37
|
+
userCode: "ABCD-EFGH",
|
|
38
|
+
verificationUri: `${mintedUrl}/assistant/pair`,
|
|
39
|
+
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
40
|
+
expiresInSeconds: 600,
|
|
41
|
+
intervalSeconds: 5,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
mock.module("../lib/step-runner.js", () => ({
|
|
47
|
+
...actualStepRunner,
|
|
48
|
+
execOutput: mockExecOutput,
|
|
49
|
+
}));
|
|
23
50
|
|
|
24
|
-
|
|
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
|
-
}
|
|
51
|
+
const { buildAppConnectUrl, pair } = await import("../commands/pair.js");
|
|
31
52
|
|
|
32
53
|
// Distinct loopback (mint) vs reachable (advertised) URLs to verify the split.
|
|
33
54
|
const LOCAL_URL = "http://127.0.0.1:7830";
|
|
@@ -100,6 +121,7 @@ const ORIGINAL_ARGV = [...process.argv];
|
|
|
100
121
|
|
|
101
122
|
describe("pair command", () => {
|
|
102
123
|
beforeEach(() => {
|
|
124
|
+
mintCalls.length = 0;
|
|
103
125
|
writeLockfile();
|
|
104
126
|
});
|
|
105
127
|
|
|
@@ -137,18 +159,16 @@ describe("pair command", () => {
|
|
|
137
159
|
globalThis.fetch = origFetch;
|
|
138
160
|
}
|
|
139
161
|
|
|
140
|
-
// Mint
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
// Mint through the gateway CLI subprocess: process execution IS the
|
|
163
|
+
// local-presence proof and the approval, so the link alone completes
|
|
164
|
+
// the pairing — no HTTP at all for a local assistant.
|
|
165
|
+
expect(mintCalls).toHaveLength(1);
|
|
166
|
+
expect(mintCalls[0]?.args.slice(-3)).toEqual([
|
|
167
|
+
"pairing",
|
|
168
|
+
"mint",
|
|
169
|
+
PUBLIC_URL,
|
|
145
170
|
]);
|
|
146
|
-
expect(
|
|
147
|
-
publicBaseUrl: PUBLIC_URL,
|
|
148
|
-
});
|
|
149
|
-
expect(JSON.parse(String(calls[1][1]?.body))).toEqual({
|
|
150
|
-
userCode: "ABCD-EFGH",
|
|
151
|
-
});
|
|
171
|
+
expect(calls).toHaveLength(0);
|
|
152
172
|
|
|
153
173
|
const output = logs.join("\n");
|
|
154
174
|
// One artifact, two renderings: the link and the same link as a QR.
|
|
@@ -398,10 +418,9 @@ describe("pair command", () => {
|
|
|
398
418
|
}
|
|
399
419
|
|
|
400
420
|
// Resolution succeeded (no exit), so the challenge was minted.
|
|
401
|
-
expect(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
]);
|
|
421
|
+
expect(mintCalls).toHaveLength(1);
|
|
422
|
+
expect(mintCalls[0]?.args.at(-1)).toBe(PUBLIC_URL);
|
|
423
|
+
expect(calls).toHaveLength(0);
|
|
405
424
|
});
|
|
406
425
|
|
|
407
426
|
test("rejects an unknown --flag before any network call", async () => {
|
|
@@ -594,11 +613,8 @@ describe("pair command", () => {
|
|
|
594
613
|
globalThis.fetch = origFetch;
|
|
595
614
|
}
|
|
596
615
|
|
|
597
|
-
// Mint still
|
|
598
|
-
expect(
|
|
599
|
-
expect(JSON.parse(String(calls[0][1]?.body))).toEqual({
|
|
600
|
-
publicBaseUrl: OVERRIDE,
|
|
601
|
-
});
|
|
616
|
+
// Mint still through the gateway CLI; the link advertises the override.
|
|
617
|
+
expect(mintCalls[0]?.args.at(-1)).toBe(OVERRIDE);
|
|
602
618
|
expect(JSON.parse(logs.join("\n")).pairUrl).toBe(
|
|
603
619
|
`${OVERRIDE}/assistant/pair#device_code=device-code`,
|
|
604
620
|
);
|
|
@@ -1076,35 +1092,8 @@ describe("pair command", () => {
|
|
|
1076
1092
|
|
|
1077
1093
|
const calls: string[] = [];
|
|
1078
1094
|
const origFetch = globalThis.fetch;
|
|
1079
|
-
globalThis.fetch = withPreconnect(async (input: RequestInfo | URL
|
|
1080
|
-
|
|
1081
|
-
calls.push(url);
|
|
1082
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1083
|
-
// The minted challenge must advertise the entry's recorded URL.
|
|
1084
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
1085
|
-
publicBaseUrl: "https://saved.example.ts.net",
|
|
1086
|
-
});
|
|
1087
|
-
return new Response(
|
|
1088
|
-
JSON.stringify({
|
|
1089
|
-
deviceCode: "device-code",
|
|
1090
|
-
userCode: "ABCD-EFGH",
|
|
1091
|
-
verificationUri: "https://saved.example.ts.net/assistant/pair",
|
|
1092
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1093
|
-
expiresInSeconds: 600,
|
|
1094
|
-
}),
|
|
1095
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1096
|
-
);
|
|
1097
|
-
}
|
|
1098
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
1099
|
-
return new Response(
|
|
1100
|
-
JSON.stringify({
|
|
1101
|
-
status: "approved",
|
|
1102
|
-
verificationUri: "https://saved.example.ts.net/assistant/pair",
|
|
1103
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1104
|
-
}),
|
|
1105
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1095
|
+
globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
|
|
1096
|
+
calls.push(String(input));
|
|
1108
1097
|
return new Response("not found", { status: 404 });
|
|
1109
1098
|
});
|
|
1110
1099
|
|
|
@@ -1123,7 +1112,9 @@ describe("pair command", () => {
|
|
|
1123
1112
|
globalThis.fetch = origFetch;
|
|
1124
1113
|
}
|
|
1125
1114
|
|
|
1126
|
-
|
|
1115
|
+
// The minted challenge must advertise the entry's recorded URL.
|
|
1116
|
+
expect(mintCalls[0]?.args.at(-1)).toBe("https://saved.example.ts.net");
|
|
1117
|
+
expect(calls).toHaveLength(0);
|
|
1127
1118
|
const output = logs.join("\n");
|
|
1128
1119
|
expect(output).toContain(
|
|
1129
1120
|
"Using saved ingress URL https://saved.example.ts.net",
|
|
@@ -1137,33 +1128,7 @@ describe("pair command", () => {
|
|
|
1137
1128
|
writeLockfileWithIngress("https://saved.example.ts.net");
|
|
1138
1129
|
|
|
1139
1130
|
const origFetch = globalThis.fetch;
|
|
1140
|
-
globalThis.fetch = withPreconnect(async (
|
|
1141
|
-
const url = String(input);
|
|
1142
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1143
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
1144
|
-
publicBaseUrl: "https://explicit.example.ts.net",
|
|
1145
|
-
});
|
|
1146
|
-
return new Response(
|
|
1147
|
-
JSON.stringify({
|
|
1148
|
-
deviceCode: "device-code",
|
|
1149
|
-
userCode: "ABCD-EFGH",
|
|
1150
|
-
verificationUri: "https://explicit.example.ts.net/assistant/pair",
|
|
1151
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1152
|
-
expiresInSeconds: 600,
|
|
1153
|
-
}),
|
|
1154
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
1158
|
-
return new Response(
|
|
1159
|
-
JSON.stringify({
|
|
1160
|
-
status: "approved",
|
|
1161
|
-
verificationUri: "https://explicit.example.ts.net/assistant/pair",
|
|
1162
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1163
|
-
}),
|
|
1164
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1131
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
1167
1132
|
return new Response("not found", { status: 404 });
|
|
1168
1133
|
});
|
|
1169
1134
|
|
|
@@ -1189,6 +1154,8 @@ describe("pair command", () => {
|
|
|
1189
1154
|
}
|
|
1190
1155
|
|
|
1191
1156
|
expect(logs.join("\n")).not.toContain("Using saved ingress URL");
|
|
1157
|
+
// The override reached the mint.
|
|
1158
|
+
expect(mintCalls[0]?.args.at(-1)).toBe("https://explicit.example.ts.net");
|
|
1192
1159
|
});
|
|
1193
1160
|
|
|
1194
1161
|
test("a non-https recorded ingress URL is ignored", async () => {
|
|
@@ -1270,4 +1237,101 @@ describe("pair command", () => {
|
|
|
1270
1237
|
expect(errors.join("\n")).toContain(RUNTIME_URL);
|
|
1271
1238
|
expect(minted).toBe(false);
|
|
1272
1239
|
});
|
|
1240
|
+
|
|
1241
|
+
test("refuses a paired (connect-import) assistant before minting", async () => {
|
|
1242
|
+
writeFileSync(
|
|
1243
|
+
join(testDir, "lockfile.json"),
|
|
1244
|
+
JSON.stringify({
|
|
1245
|
+
assistants: [
|
|
1246
|
+
{
|
|
1247
|
+
assistantId: "pair-test",
|
|
1248
|
+
runtimeUrl: RUNTIME_URL,
|
|
1249
|
+
cloud: "paired",
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
activeAssistant: "pair-test",
|
|
1253
|
+
}),
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
const errors: string[] = [];
|
|
1257
|
+
const errSpy = spyOn(console, "error").mockImplementation(
|
|
1258
|
+
(...a: unknown[]) => {
|
|
1259
|
+
errors.push(a.join(" "));
|
|
1260
|
+
},
|
|
1261
|
+
);
|
|
1262
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1263
|
+
(code?: number | string | undefined): never => {
|
|
1264
|
+
throw new Error(`exit:${code}`);
|
|
1265
|
+
},
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
process.argv = ["bun", "cadet", "pair", "--url", PUBLIC_URL];
|
|
1269
|
+
let exited = false;
|
|
1270
|
+
try {
|
|
1271
|
+
await pair();
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
exited = e instanceof Error && e.message === "exit:1";
|
|
1274
|
+
} finally {
|
|
1275
|
+
errSpy.mockRestore();
|
|
1276
|
+
exitSpy.mockRestore();
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
expect(exited).toBe(true);
|
|
1280
|
+
expect(errors.join("\n")).toContain("was paired as a remote");
|
|
1281
|
+
// Keyed on the entry's cloud, not on which fields happen to exist.
|
|
1282
|
+
expect(mintCalls).toHaveLength(0);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
test("a custom (ssh-managed) assistant keeps the loopback HTTP mint", async () => {
|
|
1286
|
+
writeFileSync(
|
|
1287
|
+
join(testDir, "lockfile.json"),
|
|
1288
|
+
JSON.stringify({
|
|
1289
|
+
assistants: [
|
|
1290
|
+
{
|
|
1291
|
+
assistantId: "pair-test",
|
|
1292
|
+
runtimeUrl: LOCAL_URL,
|
|
1293
|
+
cloud: "custom",
|
|
1294
|
+
},
|
|
1295
|
+
],
|
|
1296
|
+
activeAssistant: "pair-test",
|
|
1297
|
+
}),
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
1301
|
+
const origFetch = globalThis.fetch;
|
|
1302
|
+
globalThis.fetch = withPreconnect(
|
|
1303
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
1304
|
+
const url = String(input);
|
|
1305
|
+
calls.push([url, init]);
|
|
1306
|
+
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1307
|
+
return challengeResponse(PUBLIC_URL);
|
|
1308
|
+
}
|
|
1309
|
+
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
1310
|
+
return approvalResponse(PUBLIC_URL);
|
|
1311
|
+
}
|
|
1312
|
+
return new Response("not found", { status: 404 });
|
|
1313
|
+
},
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
1317
|
+
|
|
1318
|
+
process.argv = ["bun", "cadet", "pair", "--url", PUBLIC_URL];
|
|
1319
|
+
try {
|
|
1320
|
+
await pair();
|
|
1321
|
+
} finally {
|
|
1322
|
+
logSpy.mockRestore();
|
|
1323
|
+
globalThis.fetch = origFetch;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Not local/docker: no CLI execution — the mint rides the loopback HTTP
|
|
1327
|
+
// route, keyed on the entry's cloud, not on containerInfo.
|
|
1328
|
+
expect(mintCalls).toHaveLength(0);
|
|
1329
|
+
expect(calls.map((c) => c[0])).toEqual([
|
|
1330
|
+
`${LOCAL_URL}/v1/remote-web/pairing-challenge`,
|
|
1331
|
+
`${LOCAL_URL}/v1/remote-web/pairing-verification`,
|
|
1332
|
+
]);
|
|
1333
|
+
expect(JSON.parse(String(calls[0]?.[1]?.body))).toEqual({
|
|
1334
|
+
publicBaseUrl: PUBLIC_URL,
|
|
1335
|
+
});
|
|
1336
|
+
});
|
|
1273
1337
|
});
|