@mycadet/cli 0.12.9 → 0.12.11
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/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/package.json +4 -3
- package/src/__tests__/client-web-config.test.ts +0 -1
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/devices.test.ts +62 -0
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +58 -26
- package/src/__tests__/pair.test.ts +149 -78
- package/src/__tests__/ps-cloud-status.test.ts +6 -4
- package/src/__tests__/setup.test.ts +14 -4
- package/src/__tests__/teleport.test.ts +0 -15
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +265 -70
- package/src/__tests__/uninstall.test.ts +140 -8
- package/src/commands/client.ts +4 -6
- package/src/commands/devices.ts +66 -30
- package/src/commands/hatch.ts +14 -53
- package/src/commands/pair.ts +147 -38
- package/src/commands/ps.ts +10 -20
- package/src/commands/teleport.ts +2 -21
- package/src/commands/tunnel.ts +67 -28
- package/src/commands/uninstall.ts +106 -71
- package/src/commands/upgrade.ts +6 -1
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/assistant-config.ts +20 -5
- package/src/lib/cloud-client.ts +0 -3
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +3 -2
- package/src/lib/color.ts +4 -0
- package/src/lib/docker.ts +46 -70
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/environments/__tests__/paths.test.ts +27 -1
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +2 -4
- package/src/lib/hatch-local.ts +16 -59
- package/src/lib/hatch-next-steps.ts +18 -9
- package/src/lib/live-voice/tunnel.ts +2 -2
- package/src/lib/local.ts +42 -9
- package/src/lib/nginx-ingress.ts +124 -24
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/tunnel.ts +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/xdg-log.ts +11 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import * as processes from "../lib/process.js";
|
|
12
|
+
import {
|
|
13
|
+
TAILSCALE_PROVIDER,
|
|
14
|
+
stopTailscaleTunnel,
|
|
15
|
+
type TailscaleDeps,
|
|
16
|
+
} from "../lib/tunnels/tailscale.js";
|
|
17
|
+
import * as ngrok from "../lib/tunnels/ngrok.js";
|
|
18
|
+
import { tunnel } from "../commands/tunnel.js";
|
|
19
|
+
import {
|
|
20
|
+
readRootProcesses,
|
|
21
|
+
saveAssistantEntry,
|
|
22
|
+
stampRootProcess,
|
|
23
|
+
findAssistantByName,
|
|
24
|
+
} from "../lib/assistant-config.js";
|
|
25
|
+
|
|
26
|
+
const originalArgv = process.argv;
|
|
27
|
+
const originalLock = process.env.CADET_LOCKFILE_DIR;
|
|
28
|
+
const originalConfig = process.env.XDG_CONFIG_HOME;
|
|
29
|
+
let root = "";
|
|
30
|
+
let workspace = "";
|
|
31
|
+
let stop: ReturnType<typeof spyOn<typeof processes, "stopProcess">>;
|
|
32
|
+
let identity: ReturnType<
|
|
33
|
+
typeof spyOn<typeof processes, "readProcessCommandLine">
|
|
34
|
+
>;
|
|
35
|
+
let logs: ReturnType<typeof spyOn<typeof console, "log">>;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
root = mkdtempSync(join(tmpdir(), "cadet-tunnel-stop-"));
|
|
39
|
+
workspace = join(root, "instance", "workspace");
|
|
40
|
+
mkdirSync(workspace, { recursive: true });
|
|
41
|
+
process.env.CADET_LOCKFILE_DIR = root;
|
|
42
|
+
process.env.XDG_CONFIG_HOME = root;
|
|
43
|
+
saveAssistantEntry({
|
|
44
|
+
assistantId: "fixture",
|
|
45
|
+
name: "fixture",
|
|
46
|
+
cloud: "local",
|
|
47
|
+
runtimeUrl: "http://127.0.0.1:7830",
|
|
48
|
+
ingressUrl: "https://old.ngrok.app",
|
|
49
|
+
resources: {
|
|
50
|
+
instanceDir: join(root, "instance"),
|
|
51
|
+
gatewayPort: 7830,
|
|
52
|
+
daemonPort: 7821,
|
|
53
|
+
cesPort: 8090,
|
|
54
|
+
qdrantPort: 6333,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
writeFileSync(
|
|
58
|
+
join(workspace, "config.json"),
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
ingress: {
|
|
61
|
+
publicBaseUrl: "https://old.ngrok.app",
|
|
62
|
+
lastTunnel: {
|
|
63
|
+
provider: "ngrok",
|
|
64
|
+
publicBaseUrl: "https://old.ngrok.app",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
stampRootProcess("tunnel", {
|
|
70
|
+
pid: 424242,
|
|
71
|
+
provider: "ngrok",
|
|
72
|
+
assistantId: "fixture",
|
|
73
|
+
startedAt: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
stampRootProcess("app", { pid: 424243, startedAt: new Date().toISOString() });
|
|
76
|
+
ngrok.saveNgrokDomain("old.ngrok.app");
|
|
77
|
+
stop = spyOn(processes, "stopProcess").mockResolvedValue(true);
|
|
78
|
+
identity = spyOn(processes, "readProcessCommandLine").mockReturnValue(
|
|
79
|
+
"ngrok http 7840",
|
|
80
|
+
);
|
|
81
|
+
logs = spyOn(console, "log").mockImplementation(() => {});
|
|
82
|
+
process.argv = ["bun", "cadet", "tunnel", "stop", "fixture"];
|
|
83
|
+
});
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
stop.mockRestore();
|
|
86
|
+
identity.mockRestore();
|
|
87
|
+
logs.mockRestore();
|
|
88
|
+
process.argv = originalArgv;
|
|
89
|
+
if (originalLock === undefined) delete process.env.CADET_LOCKFILE_DIR;
|
|
90
|
+
else process.env.CADET_LOCKFILE_DIR = originalLock;
|
|
91
|
+
if (originalConfig === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
92
|
+
else process.env.XDG_CONFIG_HOME = originalConfig;
|
|
93
|
+
rmSync(root, { recursive: true, force: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("stop terminates the owned agent and clears ingress, retaining nginx and domain; repeated stop is harmless", async () => {
|
|
97
|
+
await tunnel();
|
|
98
|
+
expect(stop).toHaveBeenCalledWith(424242, "tunnel");
|
|
99
|
+
expect(readRootProcesses().tunnel).toBeUndefined();
|
|
100
|
+
expect(readRootProcesses().app?.pid).toBe(424243);
|
|
101
|
+
expect(findAssistantByName("fixture")?.ingressUrl).toBeUndefined();
|
|
102
|
+
expect(
|
|
103
|
+
JSON.parse(readFileSync(join(workspace, "config.json"), "utf8")).ingress
|
|
104
|
+
.publicBaseUrl,
|
|
105
|
+
).toBeUndefined();
|
|
106
|
+
expect(ngrok.loadNgrokDomain()).toBe("old.ngrok.app");
|
|
107
|
+
await tunnel();
|
|
108
|
+
expect(stop).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
test("a recycled PID is not signaled and its stale record is cleared", async () => {
|
|
111
|
+
identity.mockReturnValue("unrelated-process");
|
|
112
|
+
await tunnel();
|
|
113
|
+
expect(stop).not.toHaveBeenCalled();
|
|
114
|
+
expect(readRootProcesses().tunnel).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
test("another assistant's tunnel is left intact", async () => {
|
|
117
|
+
stampRootProcess("tunnel", {
|
|
118
|
+
pid: 424242,
|
|
119
|
+
provider: "ngrok",
|
|
120
|
+
assistantId: "another",
|
|
121
|
+
startedAt: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
await expect(tunnel()).rejects.toThrow("belongs to 'another'");
|
|
124
|
+
expect(stop).not.toHaveBeenCalled();
|
|
125
|
+
expect(readRootProcesses().tunnel?.assistantId).toBe("another");
|
|
126
|
+
});
|
|
127
|
+
test("failure to stop retains process and ingress state", async () => {
|
|
128
|
+
stop.mockResolvedValue(false);
|
|
129
|
+
await expect(tunnel()).rejects.toThrow("Could not stop ngrok");
|
|
130
|
+
expect(readRootProcesses().tunnel?.pid).toBe(424242);
|
|
131
|
+
expect(findAssistantByName("fixture")?.ingressUrl).toBe(
|
|
132
|
+
"https://old.ngrok.app",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("Cloudflare dispatch stops cloudflared and clears its ingress", async () => {
|
|
137
|
+
stampRootProcess("tunnel", {
|
|
138
|
+
pid: 424242,
|
|
139
|
+
provider: "cloudflare",
|
|
140
|
+
assistantId: "fixture",
|
|
141
|
+
startedAt: new Date().toISOString(),
|
|
142
|
+
});
|
|
143
|
+
identity.mockReturnValue(
|
|
144
|
+
"/usr/local/bin/cloudflared tunnel --url http://localhost:7840",
|
|
145
|
+
);
|
|
146
|
+
await tunnel();
|
|
147
|
+
expect(stop).toHaveBeenCalledWith(424242, "tunnel");
|
|
148
|
+
expect(readRootProcesses().tunnel).toBeUndefined();
|
|
149
|
+
expect(findAssistantByName("fixture")?.ingressUrl).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("Tailscale dispatch disables serve and preserves a separate webhook ingress", async () => {
|
|
153
|
+
stampRootProcess("tunnel", {
|
|
154
|
+
pid: 424242,
|
|
155
|
+
provider: "tailscale",
|
|
156
|
+
previous: {
|
|
157
|
+
pid: 424240,
|
|
158
|
+
provider: "ngrok",
|
|
159
|
+
assistantId: "fixture",
|
|
160
|
+
startedAt: new Date().toISOString(),
|
|
161
|
+
},
|
|
162
|
+
assistantId: "fixture",
|
|
163
|
+
startedAt: new Date().toISOString(),
|
|
164
|
+
});
|
|
165
|
+
writeFileSync(
|
|
166
|
+
join(workspace, "config.json"),
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
ingress: {
|
|
169
|
+
publicBaseUrl: "https://old.ngrok.app",
|
|
170
|
+
pairingTunnel: { provider: "tailscale" },
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
const calls: string[][] = [];
|
|
175
|
+
const deps: TailscaleDeps = {
|
|
176
|
+
findBinary: async () => "tailscale",
|
|
177
|
+
run: (_bin, args) => {
|
|
178
|
+
calls.push(args);
|
|
179
|
+
return { status: 0, stdout: "", stderr: "" };
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
const provider = spyOn(TAILSCALE_PROVIDER, "stop").mockImplementation(
|
|
183
|
+
(opts) => stopTailscaleTunnel(opts, deps),
|
|
184
|
+
);
|
|
185
|
+
try {
|
|
186
|
+
await tunnel();
|
|
187
|
+
expect(calls).toEqual([["serve", "--https=443", "off"]]);
|
|
188
|
+
expect(stop).not.toHaveBeenCalled();
|
|
189
|
+
expect(readRootProcesses().tunnel?.pid).toBe(424240);
|
|
190
|
+
expect(findAssistantByName("fixture")?.ingressUrl).toBe(
|
|
191
|
+
"https://old.ngrok.app",
|
|
192
|
+
);
|
|
193
|
+
const state = JSON.parse(
|
|
194
|
+
readFileSync(join(workspace, "config.json"), "utf8"),
|
|
195
|
+
);
|
|
196
|
+
expect(state.ingress.publicBaseUrl).toBe("https://old.ngrok.app");
|
|
197
|
+
expect(state.ingress.pairingTunnel).toBeUndefined();
|
|
198
|
+
} finally {
|
|
199
|
+
provider.mockRestore();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("Tailscale shutdown failure preserves the recorded tunnel and ingress", async () => {
|
|
204
|
+
stampRootProcess("tunnel", {
|
|
205
|
+
pid: 424242,
|
|
206
|
+
provider: "tailscale",
|
|
207
|
+
assistantId: "fixture",
|
|
208
|
+
startedAt: new Date().toISOString(),
|
|
209
|
+
});
|
|
210
|
+
const deps: TailscaleDeps = {
|
|
211
|
+
findBinary: async () => "tailscale",
|
|
212
|
+
run: () => ({ status: 1, stdout: "", stderr: "permission denied" }),
|
|
213
|
+
};
|
|
214
|
+
const provider = spyOn(TAILSCALE_PROVIDER, "stop").mockImplementation(
|
|
215
|
+
(opts) => stopTailscaleTunnel(opts, deps),
|
|
216
|
+
);
|
|
217
|
+
try {
|
|
218
|
+
await expect(tunnel()).rejects.toThrow("Could not stop tailscale serve");
|
|
219
|
+
expect(readRootProcesses().tunnel?.provider).toBe("tailscale");
|
|
220
|
+
expect(findAssistantByName("fixture")?.ingressUrl).toBe(
|
|
221
|
+
"https://old.ngrok.app",
|
|
222
|
+
);
|
|
223
|
+
} finally {
|
|
224
|
+
provider.mockRestore();
|
|
225
|
+
}
|
|
226
|
+
});
|