@mycadet/cli 0.12.8 → 0.12.9-dev.202610131620.d833e01

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.
Files changed (67) hide show
  1. package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +2 -2
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
  3. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
  4. package/node_modules/@mycadet/service-contracts/package.json +1 -1
  5. package/package.json +4 -3
  6. package/src/__tests__/client-web-config.test.ts +0 -1
  7. package/src/__tests__/cloud-releases.test.ts +23 -13
  8. package/src/__tests__/ensure-binary.test.ts +182 -0
  9. package/src/__tests__/nginx-ingress.test.ts +6 -11
  10. package/src/__tests__/ngrok-spawn.test.ts +49 -0
  11. package/src/__tests__/pair.test.ts +149 -78
  12. package/src/__tests__/ps-cloud-status.test.ts +4 -3
  13. package/src/__tests__/ps-table.test.ts +79 -0
  14. package/src/__tests__/tailscale.test.ts +1 -1
  15. package/src/__tests__/teleport.test.ts +1 -16
  16. package/src/__tests__/tunnel.test.ts +274 -82
  17. package/src/__tests__/uninstall.test.ts +141 -9
  18. package/src/__tests__/wake.test.ts +2 -2
  19. package/src/commands/backup.ts +3 -3
  20. package/src/commands/client.ts +4 -6
  21. package/src/commands/completions.test.ts +104 -0
  22. package/src/commands/completions.ts +87 -0
  23. package/src/commands/exec.ts +7 -7
  24. package/src/commands/flags.ts +2 -2
  25. package/src/commands/hatch.ts +5 -2
  26. package/src/commands/logs.ts +7 -7
  27. package/src/commands/pair.ts +140 -37
  28. package/src/commands/ps.ts +63 -26
  29. package/src/commands/restore.ts +2 -2
  30. package/src/commands/retire.ts +2 -2
  31. package/src/commands/rollback.ts +2 -2
  32. package/src/commands/sleep.ts +2 -2
  33. package/src/commands/teleport.ts +18 -37
  34. package/src/commands/terminal.ts +1 -1
  35. package/src/commands/tunnel.ts +101 -78
  36. package/src/commands/uninstall.ts +108 -73
  37. package/src/commands/upgrade.ts +8 -8
  38. package/src/commands/use.ts +1 -1
  39. package/src/commands/wake.ts +6 -6
  40. package/src/index.ts +15 -0
  41. package/src/lib/__tests__/docker.test.ts +168 -20
  42. package/src/lib/__tests__/job-polling.test.ts +1 -1
  43. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  44. package/src/lib/cloud-client.ts +0 -3
  45. package/src/lib/cloud-login.ts +0 -1
  46. package/src/lib/cloud-releases.ts +6 -4
  47. package/src/lib/color.ts +4 -0
  48. package/src/lib/docker.ts +190 -102
  49. package/src/lib/ensure-binary.ts +171 -0
  50. package/src/lib/environments/__tests__/paths.test.ts +27 -1
  51. package/src/lib/environments/paths.ts +16 -8
  52. package/src/lib/environments/resolve.ts +2 -4
  53. package/src/lib/flag-args.test.ts +1 -1
  54. package/src/lib/flag-args.ts +1 -1
  55. package/src/lib/guardian-token.ts +1 -1
  56. package/src/lib/hatch-local.ts +15 -8
  57. package/src/lib/hatch-next-steps.ts +5 -9
  58. package/src/lib/job-polling.ts +1 -1
  59. package/src/lib/local.ts +37 -4
  60. package/src/lib/nginx-ingress.ts +3 -5
  61. package/src/lib/pair.ts +1 -1
  62. package/src/lib/step-runner.ts +6 -1
  63. package/src/lib/sync-cloud-assistants.ts +2 -2
  64. package/src/lib/tunnels/cloudflare.ts +21 -11
  65. package/src/lib/tunnels/ngrok.ts +198 -108
  66. package/src/lib/tunnels/tailscale.ts +34 -4
  67. package/src/lib/upgrade-lifecycle.ts +5 -4
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `show_platform_login` SSE event.
2
+ * `show_cloud_login` SSE event.
3
3
  *
4
4
  * Emitted by the cloud-connect route when no cloud credentials are
5
5
  * stored yet, signalling connected clients to surface the cloud login UI.
@@ -12,7 +12,7 @@
12
12
  import { z } from "zod";
13
13
 
14
14
  export const ShowCloudLoginEventSchema = z.object({
15
- type: z.literal("show_platform_login"),
15
+ type: z.literal("show_cloud_login"),
16
16
  });
17
17
 
18
18
  export type ShowCloudLoginEvent = z.infer<typeof ShowCloudLoginEventSchema>;
@@ -40,6 +40,6 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bun": "1.2.4",
43
- "typescript": "5.7.3"
43
+ "typescript": "5.9.3"
44
44
  }
45
45
  }
@@ -40,6 +40,6 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bun": "1.2.4",
43
- "typescript": "5.7.3"
43
+ "typescript": "5.9.3"
44
44
  }
45
45
  }
@@ -40,6 +40,6 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bun": "1.2.4",
43
- "typescript": "5.7.3"
43
+ "typescript": "5.9.3"
44
44
  }
45
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/cli",
3
- "version": "0.12.8",
3
+ "version": "0.12.9-dev.202610131620.d833e01",
4
4
  "description": "CLI tools for mycadet",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,7 +15,7 @@
15
15
  "./src/commands/*": "./src/commands/*.ts"
16
16
  },
17
17
  "bin": {
18
- "cadet": "./src/index.ts"
18
+ "cadet-dev": "./src/index.ts"
19
19
  },
20
20
  "scripts": {
21
21
  "format": "prettier --write .",
@@ -24,7 +24,7 @@
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",
27
- "typecheck": "bunx tsc --noEmit"
27
+ "typecheck": "bunx tsgo --noEmit"
28
28
  },
29
29
  "author": "Cadet AI",
30
30
  "license": "MIT",
@@ -57,6 +57,7 @@
57
57
  "@types/bun": "1.3.11",
58
58
  "@types/qrcode-terminal": "0.12.2",
59
59
  "@types/react": "19.2.14",
60
+ "@typescript/native-preview": "7.0.0-dev.20260624.1",
60
61
  "eslint": "10.1.0",
61
62
  "knip": "5.88.1",
62
63
  "prettier": "3.8.1",
@@ -13,7 +13,6 @@ import { buildWebInterfaceConfig } from "../commands/client.js";
13
13
  const BASE = {
14
14
  webUrl: "https://app.example.com",
15
15
  cloudUrl: "https://platform.example.com",
16
- disableCloud: true,
17
16
  };
18
17
 
19
18
  describe("cadet client web __config", () => {
@@ -7,7 +7,11 @@ import {
7
7
  resolveOwnImageRefs,
8
8
  } from "../lib/cloud-releases.js";
9
9
  import { dockerhubImages } from "../lib/docker.js";
10
- import { isDevVersion, isLocalVersion, isStagingVersion } from "../lib/version-compat.js";
10
+ import {
11
+ isDevVersion,
12
+ isLocalVersion,
13
+ isStagingVersion,
14
+ } from "../lib/version-compat.js";
11
15
 
12
16
  const originalFetch = globalThis.fetch;
13
17
 
@@ -79,12 +83,12 @@ describe("fetchReleases", () => {
79
83
  });
80
84
 
81
85
  describe("resolveImageRefs", () => {
82
- test("released CLI resolves un-suffixed repos tagged with the target version", () => {
86
+ test("released CLI resolves un-suffixed repos tagged with the bare version", () => {
83
87
  const tags = resolveImageRefs("v0.12.6", "0.12.6");
84
- expect(tags.assistant).toBe("mycadet/mycadet-assistant:v0.12.6");
85
- expect(tags.gateway).toBe("mycadet/mycadet-gateway:v0.12.6");
88
+ expect(tags.assistant).toBe("mycadet/mycadet-assistant:0.12.6");
89
+ expect(tags.gateway).toBe("mycadet/mycadet-gateway:0.12.6");
86
90
  expect(tags["credential-executor"]).toBe(
87
- "mycadet/mycadet-credential-executor:v0.12.6",
91
+ "mycadet/mycadet-credential-executor:0.12.6",
88
92
  );
89
93
  });
90
94
 
@@ -94,31 +98,32 @@ describe("resolveImageRefs", () => {
94
98
  "0.12.6-dev.202610081023.3a27b77",
95
99
  );
96
100
  expect(tags.assistant).toBe(
97
- "mycadet/mycadet-assistant-dev:v0.12.6-dev.202610081023.3a27b77",
101
+ "mycadet/mycadet-assistant-dev:0.12.6-dev.202610081023.3a27b77",
98
102
  );
99
103
  expect(tags.gateway).toBe(
100
- "mycadet/mycadet-gateway-dev:v0.12.6-dev.202610081023.3a27b77",
104
+ "mycadet/mycadet-gateway-dev:0.12.6-dev.202610081023.3a27b77",
101
105
  );
102
106
  expect(tags["credential-executor"]).toBe(
103
- "mycadet/mycadet-credential-executor-dev:v0.12.6-dev.202610081023.3a27b77",
107
+ "mycadet/mycadet-credential-executor-dev:0.12.6-dev.202610081023.3a27b77",
104
108
  );
105
109
  });
106
110
 
107
111
  test("repo suffix follows the CLI version, tag follows the target", () => {
108
112
  const tags = resolveImageRefs("v0.12.5", "0.12.6-dev.202610081023.3a27b77");
109
- expect(tags.assistant).toBe("mycadet/mycadet-assistant-dev:v0.12.5");
113
+ expect(tags.assistant).toBe("mycadet/mycadet-assistant-dev:0.12.5");
110
114
  });
111
115
 
112
116
  test("does not consult the network", async () => {
113
117
  mockFetchError();
114
118
  const tags = resolveImageRefs("v0.12.6", "0.12.6");
115
- expect(tags.assistant).toContain(":v0.12.6");
119
+ expect(tags.assistant).toContain(":0.12.6");
116
120
  });
117
121
 
118
- test("normalizes the v-prefix on the tag", () => {
122
+ test("normalizes the v-prefix off the tag", () => {
119
123
  const withoutPrefix = resolveImageRefs("0.12.6", "0.12.6");
120
124
  const withPrefix = resolveImageRefs("v0.12.6", "0.12.6");
121
- expect(withoutPrefix.assistant).toBe(withPrefix.assistant);
125
+ expect(withoutPrefix.assistant).toBe("mycadet/mycadet-assistant:0.12.6");
126
+ expect(withPrefix.assistant).toBe(withoutPrefix.assistant);
122
127
  });
123
128
  });
124
129
 
@@ -126,9 +131,14 @@ describe("resolveOwnImageRefs", () => {
126
131
  test("uses the CLI's own version for both tag and repo suffix", () => {
127
132
  const tags = resolveOwnImageRefs("0.12.6-dev.202610081023.3a27b77");
128
133
  expect(tags.assistant).toBe(
129
- "mycadet/mycadet-assistant-dev:v0.12.6-dev.202610081023.3a27b77",
134
+ "mycadet/mycadet-assistant-dev:0.12.6-dev.202610081023.3a27b77",
130
135
  );
131
136
  });
137
+
138
+ test("falls back to the literal latest tag when the CLI version is unknown", () => {
139
+ const tags = resolveOwnImageRefs("");
140
+ expect(tags.assistant).toBe("mycadet/mycadet-assistant:latest");
141
+ });
132
142
  });
133
143
 
134
144
  describe("dockerhubImages", () => {
@@ -0,0 +1,182 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Readable, Writable } from "node:stream";
3
+
4
+ import { ensureBinaryInstalled, readLineText } from "../lib/ensure-binary.js";
5
+
6
+ /** A real Writable capturing everything written, as a test output stream. */
7
+ function makeOutput(): { writable: Writable; text(): string } {
8
+ const captured: string[] = [];
9
+ const writable = new Writable({
10
+ write(chunk, _encoding, callback) {
11
+ captured.push(chunk.toString());
12
+ callback();
13
+ },
14
+ });
15
+ return { writable, text: () => captured.join("") };
16
+ }
17
+
18
+ describe("ensureBinaryInstalled", () => {
19
+ test("returns the existing probe result without prompting", async () => {
20
+ const output = makeOutput();
21
+ const version = await ensureBinaryInstalled({
22
+ binary: "nginx",
23
+ purpose: "the tunnel edge",
24
+ packages: { brew: ["brew", "install", "nginx"] },
25
+ manualHint: "install nginx",
26
+ probe: () => "nginx version 1.2",
27
+ confirmInstall: () => {
28
+ throw new Error("must not prompt for a present binary");
29
+ },
30
+ streams: { output: output.writable },
31
+ });
32
+ expect(version).toBe("nginx version 1.2");
33
+ expect(output.text()).toBe("");
34
+ });
35
+
36
+ test("offers to install and runs the first available package manager", async () => {
37
+ const output = makeOutput();
38
+ const installed: string[][] = [];
39
+ let probeCalls = 0;
40
+ const version = await ensureBinaryInstalled({
41
+ binary: "nginx",
42
+ purpose: "the tunnel edge",
43
+ packages: {
44
+ brew: ["brew", "install", "nginx"],
45
+ "apt-get": ["sudo", "apt-get", "install", "-y", "nginx"],
46
+ },
47
+ manualHint: "install nginx yourself",
48
+ probe: () => (++probeCalls > 1 ? "nginx version 1.2" : null),
49
+ pmAvailable: (pm) => pm === "apt-get",
50
+ runInstall: (argv) => {
51
+ installed.push(argv);
52
+ return true;
53
+ },
54
+ confirmInstall: async () => true,
55
+ streams: { output: output.writable },
56
+ });
57
+ expect(version).toBe("nginx version 1.2");
58
+ expect(installed).toEqual([["sudo", "apt-get", "install", "-y", "nginx"]]);
59
+ expect(output.text()).toContain("isn't installed");
60
+ expect(output.text()).not.toContain("apt-get install");
61
+ });
62
+
63
+ test("the default confirm takes Enter as yes", async () => {
64
+ const output = makeOutput();
65
+ const input = Object.assign(new Readable({ read() {} }), {
66
+ isTTY: true,
67
+ });
68
+ const installed: string[][] = [];
69
+ let probeCalls = 0;
70
+ const pending = ensureBinaryInstalled({
71
+ binary: "nginx",
72
+ purpose: "the tunnel edge",
73
+ packages: { brew: ["brew", "install", "nginx"] },
74
+ manualHint: "install nginx yourself",
75
+ probe: () => (++probeCalls > 1 ? "nginx version 1.2" : null),
76
+ pmAvailable: () => true,
77
+ runInstall: (argv) => {
78
+ installed.push(argv);
79
+ return true;
80
+ },
81
+ streams: { input, output: output.writable },
82
+ });
83
+ input.emit("data", "\n");
84
+ expect(await pending).toBe("nginx version 1.2");
85
+ expect(installed).toEqual([["brew", "install", "nginx"]]);
86
+ expect(output.text()).toContain("Install nginx now?");
87
+ });
88
+
89
+ test("a declined offer prints the manual hint and installs nothing", async () => {
90
+ const output = makeOutput();
91
+ let ran = false;
92
+ const version = await ensureBinaryInstalled({
93
+ binary: "ngrok",
94
+ purpose: "the ngrok tunnel",
95
+ packages: { brew: ["brew", "install", "ngrok/ngrok/ngrok"] },
96
+ manualHint: "get ngrok from ngrok.com",
97
+ probe: () => null,
98
+ runInstall: () => {
99
+ ran = true;
100
+ return true;
101
+ },
102
+ confirmInstall: async () => false,
103
+ streams: { output: output.writable },
104
+ });
105
+ expect(version).toBe(null);
106
+ expect(ran).toBe(false);
107
+ expect(output.text()).toContain("get ngrok from ngrok.com");
108
+ });
109
+
110
+ test("a failed install falls back to the manual hint", async () => {
111
+ const output = makeOutput();
112
+ const version = await ensureBinaryInstalled({
113
+ binary: "nginx",
114
+ purpose: "the tunnel edge",
115
+ packages: { brew: ["brew", "install", "nginx"] },
116
+ manualHint: "install nginx yourself",
117
+ probe: () => null,
118
+ runInstall: () => false,
119
+ confirmInstall: async () => true,
120
+ streams: { output: output.writable },
121
+ });
122
+ expect(version).toBe(null);
123
+ expect(output.text()).toContain("install nginx yourself");
124
+ });
125
+
126
+ test("no matching package manager prints the hint", async () => {
127
+ const output = makeOutput();
128
+ let ran = false;
129
+ const version = await ensureBinaryInstalled({
130
+ binary: "cloudflared",
131
+ purpose: "the cloudflare tunnel",
132
+ packages: { brew: ["brew", "install", "cloudflared"] },
133
+ manualHint: "cloudflared install docs",
134
+ probe: () => null,
135
+ pmAvailable: () => false,
136
+ runInstall: () => {
137
+ ran = true;
138
+ return true;
139
+ },
140
+ confirmInstall: async () => true,
141
+ streams: { output: output.writable },
142
+ });
143
+ expect(version).toBe(null);
144
+ expect(ran).toBe(false);
145
+ expect(output.text()).toContain("cloudflared install docs");
146
+ });
147
+
148
+ test("a non-interactive terminal declines instead of hanging", async () => {
149
+ const version = await ensureBinaryInstalled({
150
+ binary: "nginx",
151
+ purpose: "the tunnel edge",
152
+ packages: { brew: ["brew", "install", "nginx"] },
153
+ manualHint: "install nginx yourself",
154
+ probe: () => null,
155
+ confirmInstall: async () => {
156
+ throw new Error("Install confirm requires an interactive terminal.");
157
+ },
158
+ });
159
+ expect(version).toBe(null);
160
+ });
161
+ });
162
+
163
+ describe("readLineText", () => {
164
+ test("resolves the first line and ignores the rest", async () => {
165
+ const output = makeOutput();
166
+ const input = new Readable({ read() {} });
167
+ const pending = readLineText("token: ", {
168
+ input,
169
+ output: output.writable,
170
+ });
171
+ input.emit("data", "abc123\nsecond line\n");
172
+ expect(await pending).toBe("abc123");
173
+ expect(output.text()).toBe("token: ");
174
+ });
175
+
176
+ test("resolves an empty line on an immediate newline", async () => {
177
+ const input = new Readable({ read() {} });
178
+ const pending = readLineText("[Y/n] ", { input });
179
+ input.emit("data", "\n");
180
+ expect(await pending).toBe("");
181
+ });
182
+ });
@@ -160,7 +160,6 @@ describe("buildIngressNginxConfig", () => {
160
160
  config: {
161
161
  remote: "docker",
162
162
  apiBaseUrl: "/v1",
163
- cloudDisabled: 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 "{\\"remote\\":\\"docker\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true}";',
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 "{\\"remote\\":\\"custom\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true,\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
349
+ 'return 200 "{\\"remote\\":\\"custom\\",\\"apiBaseUrl\\":\\"/v1\\",\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
351
350
  );
352
351
  });
353
352
 
@@ -449,14 +448,13 @@ describe("buildRemoteWebIndexHtml", () => {
449
448
  const result = buildRemoteWebIndexHtml(html, {
450
449
  remote: "docker",
451
450
  apiBaseUrl: "/v1",
452
- disableCloud: 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__={"remote":"docker","apiBaseUrl":"/v1","disableCloud":true}',
457
+ 'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1"}',
460
458
  );
461
459
  expect(result.indexOf('"webUrl"')).toBeLessThan(
462
460
  result.indexOf('"docker"'),
@@ -864,8 +862,6 @@ function spaConfigHash(
864
862
  config: {
865
863
  remote: "custom",
866
864
  apiBaseUrl: "/v1",
867
- cloudDisabled: true,
868
- disableCloud: true,
869
865
  ...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
870
866
  ...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
871
867
  hubUrl: PRODUCTION_HUB_URL,
@@ -1354,8 +1350,6 @@ describe("startRemoteWebIngress", () => {
1354
1350
  config: {
1355
1351
  remote: "custom",
1356
1352
  apiBaseUrl: "/v1",
1357
- cloudDisabled: true,
1358
- disableCloud: 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("brew install nginx");
2099
- await expect(promise).rejects.toThrow("apt install nginx");
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
+ });