@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
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
mock,
|
|
7
|
+
spyOn,
|
|
8
|
+
test,
|
|
9
|
+
} from "bun:test";
|
|
2
10
|
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
11
|
import { tmpdir } from "os";
|
|
4
12
|
import { dirname, join } from "path";
|
|
@@ -8,6 +16,7 @@ import {
|
|
|
8
16
|
getConfigDirs,
|
|
9
17
|
getInputHistoryPath,
|
|
10
18
|
getLogDir,
|
|
19
|
+
getSharedStateDir,
|
|
11
20
|
getStateDir,
|
|
12
21
|
} from "../lib/environments/paths.js";
|
|
13
22
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
@@ -16,6 +25,20 @@ import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
|
16
25
|
// The command reads its argv at call time.
|
|
17
26
|
const ORIGINAL_ARGV = process.argv;
|
|
18
27
|
|
|
28
|
+
// retireDocker is the one retirement path with observable side effects
|
|
29
|
+
// (docker teardown), and uninstall is the only importer in its graph.
|
|
30
|
+
// Mocked module-wide so tests pin WHEN it runs — never before the
|
|
31
|
+
// confirmation gate — as much as whether it runs.
|
|
32
|
+
const retireDockerCalls: string[] = [];
|
|
33
|
+
let onRetireDocker: ((name: string) => void) | undefined;
|
|
34
|
+
|
|
35
|
+
mock.module("../lib/docker.js", () => ({
|
|
36
|
+
retireDocker: (name: string) => {
|
|
37
|
+
retireDockerCalls.push(name);
|
|
38
|
+
onRetireDocker?.(name);
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
19
42
|
// Every XDG location the command touches MUST be redirected: the homedir
|
|
20
43
|
// fallback would otherwise point at the real user's data dirs.
|
|
21
44
|
function redirectXdg(): string {
|
|
@@ -70,7 +93,15 @@ describe("cadet uninstall", () => {
|
|
|
70
93
|
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
71
94
|
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
72
95
|
|
|
96
|
+
function printedOutput(): string {
|
|
97
|
+
return logSpy.mock.calls
|
|
98
|
+
.map((args) => args.map(String).join(" "))
|
|
99
|
+
.join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
afterEach(() => {
|
|
103
|
+
retireDockerCalls.length = 0;
|
|
104
|
+
onRetireDocker = undefined;
|
|
74
105
|
logSpy.mockClear();
|
|
75
106
|
errorSpy.mockClear();
|
|
76
107
|
warnSpy.mockClear();
|
|
@@ -82,11 +113,12 @@ describe("cadet uninstall", () => {
|
|
|
82
113
|
warnSpy.mockRestore();
|
|
83
114
|
});
|
|
84
115
|
|
|
85
|
-
test("warns
|
|
116
|
+
test("warns that remaining assistants will be retired, then proceeds to the confirm gate", async () => {
|
|
86
117
|
await withRestoredEnv(async () => {
|
|
87
118
|
const root = redirectXdg();
|
|
88
119
|
// A remaining assistant is retired — with its data deleted — as part
|
|
89
|
-
// of the uninstall
|
|
120
|
+
// of the uninstall AFTER the user confirms; the pre-confirm summary
|
|
121
|
+
// says so up front (v0.0.10 run feedback: the hard refusal just
|
|
90
122
|
// blocked the uninstall; the fs sweep deletes the archives anyway).
|
|
91
123
|
writeFileSync(
|
|
92
124
|
join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
|
|
@@ -105,7 +137,7 @@ describe("cadet uninstall", () => {
|
|
|
105
137
|
expect.stringContaining("1 assistant still exists"),
|
|
106
138
|
);
|
|
107
139
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
108
|
-
expect.stringContaining("will be retired
|
|
140
|
+
expect.stringContaining("will be retired as part of this uninstall"),
|
|
109
141
|
);
|
|
110
142
|
expect(errorSpy).not.toHaveBeenCalledWith(
|
|
111
143
|
expect.stringContaining("Retire them first"),
|
|
@@ -138,6 +170,67 @@ describe("cadet uninstall", () => {
|
|
|
138
170
|
});
|
|
139
171
|
});
|
|
140
172
|
|
|
173
|
+
test("does not retire assistants when the confirm gate refuses", async () => {
|
|
174
|
+
await withRestoredEnv(async () => {
|
|
175
|
+
const root = redirectXdg();
|
|
176
|
+
writeFileSync(
|
|
177
|
+
join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
assistants: [{ ...makeEntry("docker-assistant"), cloud: "docker" }],
|
|
180
|
+
}) + "\n",
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const exitMock = spyOn(process, "exit");
|
|
184
|
+
exitMock.mockImplementation((code?: number | string | undefined): never => {
|
|
185
|
+
throw new Error(`process.exit:${code}`);
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
process.argv = ["bun", "cadet", "uninstall"];
|
|
189
|
+
await expect(uninstall()).rejects.toThrow("process.exit:1");
|
|
190
|
+
} finally {
|
|
191
|
+
exitMock.mockRestore();
|
|
192
|
+
}
|
|
193
|
+
// Retirement is part of the confirmed flow — a refusal must leave the
|
|
194
|
+
// assistant's docker resources untouched.
|
|
195
|
+
expect(retireDockerCalls).toEqual([]);
|
|
196
|
+
|
|
197
|
+
rmSync(root, { recursive: true, force: true });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("--yes retires remaining assistants after the deletion list is shown", async () => {
|
|
202
|
+
await withRestoredEnv(async () => {
|
|
203
|
+
const root = redirectXdg();
|
|
204
|
+
writeFileSync(
|
|
205
|
+
join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
|
|
206
|
+
JSON.stringify({
|
|
207
|
+
assistants: [{ ...makeEntry("docker-assistant"), cloud: "docker" }],
|
|
208
|
+
}) + "\n",
|
|
209
|
+
);
|
|
210
|
+
mkdirSync(getConfigDirs(getCurrentEnvironment())[0]!, {
|
|
211
|
+
recursive: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Capture what had been printed by the time retirement ran — the list
|
|
215
|
+
// the user confirms against must already be on screen.
|
|
216
|
+
let outputAtRetire = "";
|
|
217
|
+
onRetireDocker = () => {
|
|
218
|
+
outputAtRetire = printedOutput();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
process.argv = ["bun", "cadet", "uninstall", "--yes"];
|
|
222
|
+
await uninstall();
|
|
223
|
+
|
|
224
|
+
expect(retireDockerCalls).toEqual(["docker-assistant"]);
|
|
225
|
+
expect(outputAtRetire).toContain("It will permanently delete:");
|
|
226
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
227
|
+
expect.stringContaining("will be retired as part of this uninstall"),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
rmSync(root, { recursive: true, force: true });
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
141
234
|
test("--yes deletes the environment's XDG locations and leaves the source checkout alone", async () => {
|
|
142
235
|
await withRestoredEnv(async () => {
|
|
143
236
|
const root = redirectXdg();
|
|
@@ -166,15 +259,54 @@ describe("cadet uninstall", () => {
|
|
|
166
259
|
expect(existsSync(logDir)).toBe(false);
|
|
167
260
|
expect(existsSync(historyPath)).toBe(false);
|
|
168
261
|
|
|
262
|
+
// The list shows the shared state DIR (never the input-history FILE
|
|
263
|
+
// alone) and never names a nested path twice — the log dir goes with
|
|
264
|
+
// its parent's recursive delete.
|
|
265
|
+
const out = printedOutput();
|
|
266
|
+
expect(out).toContain(
|
|
267
|
+
` - ${getSharedStateDir()} — shared state, including the terminal input history`,
|
|
268
|
+
);
|
|
269
|
+
expect(out).not.toContain(logDir);
|
|
270
|
+
expect(out).not.toContain(historyPath);
|
|
271
|
+
expect(out).not.toContain("It will then remove the Cadet executable");
|
|
272
|
+
|
|
169
273
|
// Running under bun from a git checkout: self-removal must print the
|
|
170
274
|
// note and delete NOTHING outside the XDG targets.
|
|
171
|
-
|
|
172
|
-
.map((args) => args.map(String).join(" "))
|
|
173
|
-
.join("\n");
|
|
174
|
-
expect(note).toContain("source checkout");
|
|
275
|
+
expect(printedOutput()).toContain("source checkout");
|
|
175
276
|
expect(existsSync(process.execPath)).toBe(true);
|
|
176
277
|
|
|
177
278
|
rmSync(root, { recursive: true, force: true });
|
|
178
279
|
});
|
|
179
280
|
});
|
|
281
|
+
|
|
282
|
+
test("--yes lists and removes the Cadet executable itself", async () => {
|
|
283
|
+
await withRestoredEnv(async () => {
|
|
284
|
+
const root = redirectXdg();
|
|
285
|
+
const configDir = getConfigDirs(getCurrentEnvironment())[0]!;
|
|
286
|
+
mkdirSync(configDir, { recursive: true });
|
|
287
|
+
|
|
288
|
+
// A compiled-binary install: process.execPath IS the install, so the
|
|
289
|
+
// uninstall list must name it alongside the data dirs.
|
|
290
|
+
const fakeBinDir = join(root, "bin");
|
|
291
|
+
mkdirSync(fakeBinDir, { recursive: true });
|
|
292
|
+
const fakeBin = join(fakeBinDir, "cadet");
|
|
293
|
+
writeFileSync(fakeBin, "#!/bin/sh\n");
|
|
294
|
+
|
|
295
|
+
const realExecPath = process.execPath;
|
|
296
|
+
process.execPath = fakeBin;
|
|
297
|
+
try {
|
|
298
|
+
process.argv = ["bun", "cadet", "uninstall", "--yes"];
|
|
299
|
+
await uninstall();
|
|
300
|
+
} finally {
|
|
301
|
+
process.execPath = realExecPath;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
expect(printedOutput()).toContain(
|
|
305
|
+
` - ${fakeBin} — the Cadet executable`,
|
|
306
|
+
);
|
|
307
|
+
expect(existsSync(fakeBin)).toBe(false);
|
|
308
|
+
|
|
309
|
+
rmSync(root, { recursive: true, force: true });
|
|
310
|
+
});
|
|
311
|
+
});
|
|
180
312
|
});
|
package/src/commands/client.ts
CHANGED
|
@@ -1304,10 +1304,9 @@ async function spawnBackgroundWebInterface(
|
|
|
1304
1304
|
export function buildWebInterfaceConfig(opts: {
|
|
1305
1305
|
webUrl: string;
|
|
1306
1306
|
cloudUrl: string;
|
|
1307
|
-
disableCloud: boolean;
|
|
1308
1307
|
}): Record<string, unknown> {
|
|
1309
|
-
const { webUrl, cloudUrl
|
|
1310
|
-
return { webUrl, cloudUrl
|
|
1308
|
+
const { webUrl, cloudUrl } = opts;
|
|
1309
|
+
return { webUrl, cloudUrl };
|
|
1311
1310
|
}
|
|
1312
1311
|
|
|
1313
1312
|
async function runWebInterface(
|
|
@@ -1349,9 +1348,7 @@ async function runWebInterface(
|
|
|
1349
1348
|
const webUrl = getWebUrl();
|
|
1350
1349
|
const safeJson = (v: unknown) =>
|
|
1351
1350
|
JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
1352
|
-
const configJson = safeJson(
|
|
1353
|
-
buildWebInterfaceConfig({ webUrl, cloudUrl, disableCloud }),
|
|
1354
|
-
);
|
|
1351
|
+
const configJson = safeJson(buildWebInterfaceConfig({ webUrl, cloudUrl }));
|
|
1355
1352
|
const hasOverrides = Object.keys(parsedFlagOverrides).length > 0;
|
|
1356
1353
|
const flagOverridesSnippet = hasOverrides
|
|
1357
1354
|
? `;window.__CADET_FLAG_OVERRIDES__=${safeJson(parsedFlagOverrides)}`
|
|
@@ -1557,6 +1554,7 @@ async function runViteDevServer(
|
|
|
1557
1554
|
...viteFlagVars,
|
|
1558
1555
|
...(disableCloud ? { VITE_CADET_DISABLE_CLOUD: "true" } : {}),
|
|
1559
1556
|
VITE_CLOUD_MODE: "false",
|
|
1557
|
+
VITE_WEB_MODE: "full",
|
|
1560
1558
|
API_PROXY_TARGET: cloudUrl,
|
|
1561
1559
|
CADET_WEB_URL: getWebUrl(),
|
|
1562
1560
|
CADET_CLOUD_URL: cloudUrl,
|
package/src/commands/devices.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* that runs the assistant (server side).
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { z } from "zod";
|
|
16
17
|
import { createHash } from "node:crypto";
|
|
17
18
|
|
|
18
19
|
import type { DeviceRecord } from "@mycadet/local-mode";
|
|
@@ -33,8 +34,16 @@ import {
|
|
|
33
34
|
canPromptForConfirmation,
|
|
34
35
|
confirmAction,
|
|
35
36
|
} from "../lib/confirm-action.js";
|
|
37
|
+
import { GATEWAY_INTERNAL_PORT } from "../lib/environments/paths.js";
|
|
38
|
+
import { execOutput } from "../lib/step-runner.js";
|
|
39
|
+
import { dockerResourceNames } from "../lib/docker.js";
|
|
36
40
|
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
37
41
|
|
|
42
|
+
const dockerGatewayResponseSchema = z.object({
|
|
43
|
+
status: z.number().int().min(200).max(599),
|
|
44
|
+
body: z.string(),
|
|
45
|
+
});
|
|
46
|
+
|
|
38
47
|
function printUsage(): void {
|
|
39
48
|
console.log(`cadet devices [beta] - List and revoke devices paired to a local assistant
|
|
40
49
|
|
|
@@ -81,6 +90,9 @@ function resolveLoopbackBase(entry: AssistantEntry): string {
|
|
|
81
90
|
process.exit(1);
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
if (entry.cloud === "docker")
|
|
94
|
+
return `http://127.0.0.1:${GATEWAY_INTERNAL_PORT}`;
|
|
95
|
+
|
|
84
96
|
const base =
|
|
85
97
|
entry.localUrl ||
|
|
86
98
|
(entry.resources?.gatewayPort
|
|
@@ -96,6 +108,31 @@ function resolveLoopbackBase(entry: AssistantEntry): string {
|
|
|
96
108
|
return base.replace(/\/+$/, "");
|
|
97
109
|
}
|
|
98
110
|
|
|
111
|
+
/** Docker host management enters the gateway network namespace before calling loopback-only routes. */
|
|
112
|
+
async function deviceRequest(
|
|
113
|
+
entry: AssistantEntry,
|
|
114
|
+
url: string,
|
|
115
|
+
init: RequestInit,
|
|
116
|
+
): Promise<Response> {
|
|
117
|
+
if (entry.cloud !== "docker") return loopbackSafeFetch(url, init);
|
|
118
|
+
const script = `const r = await fetch(process.argv[1], {...JSON.parse(process.argv[2]), signal: AbortSignal.timeout(10000)}); console.log(JSON.stringify({status:r.status, body:await r.text()}));`;
|
|
119
|
+
const output = await execOutput(
|
|
120
|
+
"docker",
|
|
121
|
+
[
|
|
122
|
+
"exec",
|
|
123
|
+
dockerResourceNames(entry.assistantId).gatewayContainer,
|
|
124
|
+
"bun",
|
|
125
|
+
"-e",
|
|
126
|
+
script,
|
|
127
|
+
url,
|
|
128
|
+
JSON.stringify(init),
|
|
129
|
+
],
|
|
130
|
+
{ timeoutMs: 15000 },
|
|
131
|
+
);
|
|
132
|
+
const result = dockerGatewayResponseSchema.parse(JSON.parse(output));
|
|
133
|
+
return new Response(result.body, { status: result.status });
|
|
134
|
+
}
|
|
135
|
+
|
|
99
136
|
/**
|
|
100
137
|
* The hashed device id the gateway stores for THIS machine's own guardian
|
|
101
138
|
* credential (`leaseGuardianToken` registers it device-bound as
|
|
@@ -122,7 +159,7 @@ async function listDevices(
|
|
|
122
159
|
|
|
123
160
|
let response: Response;
|
|
124
161
|
try {
|
|
125
|
-
response = await
|
|
162
|
+
response = await deviceRequest(entry, `${base}/v1/devices`, {
|
|
126
163
|
method: "GET",
|
|
127
164
|
headers: getClientRegistrationHeaders(CLI_INTERFACE_ID),
|
|
128
165
|
});
|
|
@@ -239,7 +276,7 @@ async function revokeDevice(
|
|
|
239
276
|
|
|
240
277
|
let response: Response;
|
|
241
278
|
try {
|
|
242
|
-
response = await
|
|
279
|
+
response = await deviceRequest(entry, `${base}/v1/devices/revoke`, {
|
|
243
280
|
method: "POST",
|
|
244
281
|
headers: {
|
|
245
282
|
"Content-Type": "application/json",
|
|
@@ -281,38 +318,37 @@ async function revokeDevice(
|
|
|
281
318
|
|
|
282
319
|
export const devices = Object.assign(
|
|
283
320
|
async function devices(): Promise<void> {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
287
|
-
printUsage();
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
321
|
+
const rawArgs = process.argv.slice(3);
|
|
290
322
|
|
|
291
|
-
|
|
292
|
-
const args = rawArgs.filter((a) => a !== "--json");
|
|
293
|
-
|
|
294
|
-
if (args[0] === "revoke") {
|
|
295
|
-
const rest = args.slice(1);
|
|
296
|
-
const yes = rest.includes("--yes");
|
|
297
|
-
const positionals = rest.filter((a) => !a.startsWith("-"));
|
|
298
|
-
const hashedDeviceId = positionals[0];
|
|
299
|
-
if (!hashedDeviceId) {
|
|
300
|
-
console.error("Error: a hashedDeviceId is required to revoke.");
|
|
323
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
301
324
|
printUsage();
|
|
302
|
-
|
|
325
|
+
return;
|
|
303
326
|
}
|
|
304
|
-
|
|
327
|
+
|
|
328
|
+
const jsonOutput = rawArgs.includes("--json");
|
|
329
|
+
const args = rawArgs.filter((a) => a !== "--json");
|
|
330
|
+
|
|
331
|
+
if (args[0] === "revoke") {
|
|
332
|
+
const rest = args.slice(1);
|
|
333
|
+
const yes = rest.includes("--yes");
|
|
334
|
+
const positionals = rest.filter((a) => !a.startsWith("-"));
|
|
335
|
+
const hashedDeviceId = positionals[0];
|
|
336
|
+
if (!hashedDeviceId) {
|
|
337
|
+
console.error("Error: a hashedDeviceId is required to revoke.");
|
|
338
|
+
printUsage();
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
const nameArg = positionals.slice(1).join(" ") || undefined;
|
|
342
|
+
const entry = resolveTargetAssistant(nameArg);
|
|
343
|
+
const base = resolveLoopbackBase(entry);
|
|
344
|
+
await revokeDevice(entry, base, hashedDeviceId, yes, jsonOutput);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const nameArg = parseAssistantTargetArg(args, []);
|
|
305
349
|
const entry = resolveTargetAssistant(nameArg);
|
|
306
350
|
const base = resolveLoopbackBase(entry);
|
|
307
|
-
await
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const nameArg = parseAssistantTargetArg(args, []);
|
|
312
|
-
const entry = resolveTargetAssistant(nameArg);
|
|
313
|
-
const base = resolveLoopbackBase(entry);
|
|
314
|
-
await listDevices(entry, base, jsonOutput);
|
|
315
|
-
},
|
|
351
|
+
await listDevices(entry, base, jsonOutput);
|
|
352
|
+
},
|
|
316
353
|
{ description: "List or revoke devices paired to a local assistant [beta]" },
|
|
317
354
|
);
|
|
318
|
-
|
package/src/commands/hatch.ts
CHANGED
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
saveAssistantEntry,
|
|
7
7
|
setActiveAssistant,
|
|
8
8
|
} from "../lib/assistant-config";
|
|
9
|
+
import { green } from "../lib/color";
|
|
9
10
|
import { HATCH_ART, HATCHED_EMOJI, VALID_REMOTE_HOSTS } from "../lib/constants";
|
|
10
11
|
import type { RemoteHost } from "../lib/constants";
|
|
11
12
|
import { hatchDocker } from "../lib/docker";
|
|
12
|
-
import {
|
|
13
|
+
import { readAmbientFlagEnvVars } from "../lib/flag-args";
|
|
13
14
|
import { hatchLocal } from "../lib/hatch-local";
|
|
14
15
|
import {
|
|
15
16
|
getCloudUrl,
|
|
@@ -22,37 +23,26 @@ const DEFAULT_REMOTE: RemoteHost = "local";
|
|
|
22
23
|
|
|
23
24
|
interface HatchArgs {
|
|
24
25
|
verbose: boolean;
|
|
25
|
-
keepAlive: boolean;
|
|
26
26
|
name: string | null;
|
|
27
27
|
remote: RemoteHost;
|
|
28
28
|
watch: boolean;
|
|
29
29
|
sourcePath: string | null;
|
|
30
30
|
configValues: Record<string, string>;
|
|
31
31
|
flagEnvVars: Record<string, string>;
|
|
32
|
-
analyze: boolean;
|
|
33
|
-
disableCloud: boolean;
|
|
34
32
|
netnsContainer: string | null;
|
|
35
33
|
gatewayPort: number | null;
|
|
36
34
|
assistantCaCert: string | null;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
function parseArgs(): HatchArgs {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
);
|
|
43
|
-
const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
|
|
44
|
-
const disableCloudAmbient =
|
|
45
|
-
process.env.CADET_DISABLE_CLOUD?.trim().toLowerCase();
|
|
46
|
-
let disableCloud =
|
|
47
|
-
disableCloudAmbient === "true" || disableCloudAmbient === "1";
|
|
38
|
+
const args = process.argv.slice(3);
|
|
39
|
+
const flagEnvVars = readAmbientFlagEnvVars();
|
|
48
40
|
let verbose = false;
|
|
49
|
-
let keepAlive = false;
|
|
50
41
|
let name: string | null = null;
|
|
51
42
|
let remote: RemoteHost = DEFAULT_REMOTE;
|
|
52
43
|
let watch = false;
|
|
53
44
|
let sourcePath: string | null = null;
|
|
54
45
|
const configValues: Record<string, string> = {};
|
|
55
|
-
let analyze = false;
|
|
56
46
|
let netnsContainer: string | null = null;
|
|
57
47
|
let gatewayPort: number | null = null;
|
|
58
48
|
let assistantCaCert: string | null = null;
|
|
@@ -68,47 +58,33 @@ function parseArgs(): HatchArgs {
|
|
|
68
58
|
console.log(
|
|
69
59
|
" -v, --verbose Show service startup logs and extended next steps",
|
|
70
60
|
);
|
|
71
|
-
console.log(" --name <name> Custom
|
|
61
|
+
console.log(" --name <name> Custom name for this assistant");
|
|
72
62
|
console.log(
|
|
73
63
|
" --remote <host> Remote host (local, docker, custom, cloud)",
|
|
74
64
|
);
|
|
75
65
|
console.log(
|
|
76
|
-
" --watch Run
|
|
77
|
-
);
|
|
78
|
-
console.log(
|
|
79
|
-
" --source <path> Build images from a local source tree at <path>. Useful for callers that want each run to pick up local CLI changes.",
|
|
66
|
+
" --watch Run Cadet in watch mode with hot reload on source changes",
|
|
80
67
|
);
|
|
81
68
|
console.log(
|
|
82
|
-
" --
|
|
69
|
+
" --source <path> Build images from a local source tree at <path>",
|
|
83
70
|
);
|
|
84
71
|
console.log(
|
|
85
72
|
" --config <key=value> Set a workspace config value (repeatable)",
|
|
86
73
|
);
|
|
87
74
|
console.log(
|
|
88
|
-
" --
|
|
89
|
-
);
|
|
90
|
-
console.log(
|
|
91
|
-
" --analyze Emit a structured hatch-timing log line on stdout",
|
|
92
|
-
);
|
|
93
|
-
console.log(
|
|
94
|
-
" --disable-cloud Suppress all outbound platform API calls",
|
|
95
|
-
);
|
|
96
|
-
console.log(
|
|
97
|
-
" --netns-container <name> Join an existing container's network namespace (docker target only) instead of creating a per-instance network. The namespace owner publishes host ports, so --gateway-port is required.",
|
|
75
|
+
" --netns-container <name> Join an existing container's network namespace instead of creating a per-instance network.",
|
|
98
76
|
);
|
|
99
77
|
console.log(
|
|
100
|
-
" --gateway-port <port> Use an explicit host port for the
|
|
78
|
+
" --gateway-port <port> Use an explicit host port for accessing the assistant.",
|
|
101
79
|
);
|
|
102
80
|
console.log(
|
|
103
|
-
" --assistant-ca-cert <path> Trust an extra PEM CA bundle in the assistant container
|
|
81
|
+
" --assistant-ca-cert <path> Trust an extra PEM CA bundle in the assistant container from process start.",
|
|
104
82
|
);
|
|
105
83
|
process.exit(0);
|
|
106
84
|
} else if (arg === "-v" || arg == "--verbose") {
|
|
107
85
|
verbose = true;
|
|
108
86
|
} else if (arg === "--watch") {
|
|
109
87
|
watch = true;
|
|
110
|
-
} else if (arg === "--analyze") {
|
|
111
|
-
analyze = true;
|
|
112
88
|
} else if (arg === "--source") {
|
|
113
89
|
const next = args[i + 1];
|
|
114
90
|
if (!next || next.startsWith("-")) {
|
|
@@ -117,8 +93,6 @@ function parseArgs(): HatchArgs {
|
|
|
117
93
|
}
|
|
118
94
|
sourcePath = next;
|
|
119
95
|
i++;
|
|
120
|
-
} else if (arg === "--keep-alive") {
|
|
121
|
-
keepAlive = true;
|
|
122
96
|
} else if (arg === "--name") {
|
|
123
97
|
const next = args[i + 1];
|
|
124
98
|
if (!next || next.startsWith("-")) {
|
|
@@ -162,8 +136,6 @@ function parseArgs(): HatchArgs {
|
|
|
162
136
|
const value = next.slice(eqIndex + 1);
|
|
163
137
|
configValues[key] = value;
|
|
164
138
|
i++;
|
|
165
|
-
} else if (arg === "--disable-cloud") {
|
|
166
|
-
disableCloud = true;
|
|
167
139
|
} else if (arg === "--netns-container") {
|
|
168
140
|
const next = args[i + 1];
|
|
169
141
|
if (!next || next.startsWith("-")) {
|
|
@@ -193,7 +165,7 @@ function parseArgs(): HatchArgs {
|
|
|
193
165
|
i++;
|
|
194
166
|
} else {
|
|
195
167
|
console.error(
|
|
196
|
-
`Error: Unknown argument '${arg}'. Valid options: -v, --verbose, --watch, --source <path>, --
|
|
168
|
+
`Error: Unknown argument '${arg}'. Valid options: -v, --verbose, --watch, --source <path>, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
|
|
197
169
|
);
|
|
198
170
|
process.exit(1);
|
|
199
171
|
}
|
|
@@ -201,15 +173,12 @@ function parseArgs(): HatchArgs {
|
|
|
201
173
|
|
|
202
174
|
return {
|
|
203
175
|
verbose,
|
|
204
|
-
keepAlive,
|
|
205
176
|
name,
|
|
206
177
|
remote,
|
|
207
178
|
watch,
|
|
208
179
|
sourcePath,
|
|
209
180
|
configValues,
|
|
210
181
|
flagEnvVars,
|
|
211
|
-
analyze,
|
|
212
|
-
disableCloud,
|
|
213
182
|
netnsContainer,
|
|
214
183
|
gatewayPort,
|
|
215
184
|
assistantCaCert,
|
|
@@ -229,25 +198,17 @@ export const hatch = Object.assign(
|
|
|
229
198
|
|
|
230
199
|
const {
|
|
231
200
|
verbose,
|
|
232
|
-
keepAlive,
|
|
233
201
|
name,
|
|
234
202
|
remote,
|
|
235
203
|
watch,
|
|
236
204
|
sourcePath,
|
|
237
205
|
configValues,
|
|
238
206
|
flagEnvVars,
|
|
239
|
-
analyze,
|
|
240
|
-
disableCloud,
|
|
241
207
|
netnsContainer,
|
|
242
208
|
gatewayPort,
|
|
243
209
|
assistantCaCert,
|
|
244
210
|
} = parseArgs();
|
|
245
211
|
|
|
246
|
-
if (disableCloud) {
|
|
247
|
-
process.env.CADET_DISABLE_CLOUD = "true";
|
|
248
|
-
flagEnvVars.CADET_DISABLE_CLOUD = "true";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
212
|
if (watch && remote !== "local" && remote !== "docker") {
|
|
252
213
|
console.error(
|
|
253
214
|
"Error: --watch is only supported for local and docker hatch targets.",
|
|
@@ -285,7 +246,6 @@ export const hatch = Object.assign(
|
|
|
285
246
|
await hatchLocal({
|
|
286
247
|
name,
|
|
287
248
|
watch,
|
|
288
|
-
keepAlive,
|
|
289
249
|
configValues,
|
|
290
250
|
flagEnvVars,
|
|
291
251
|
verbose,
|
|
@@ -300,7 +260,6 @@ export const hatch = Object.assign(
|
|
|
300
260
|
configValues,
|
|
301
261
|
flagEnvVars,
|
|
302
262
|
sourcePath,
|
|
303
|
-
analyze,
|
|
304
263
|
netnsContainer: netnsContainer ?? undefined,
|
|
305
264
|
gatewayPort: gatewayPort ?? undefined,
|
|
306
265
|
assistantCaCertPath: assistantCaCert ?? undefined,
|
|
@@ -347,7 +306,9 @@ async function hatchCadetPlatform(): Promise<void> {
|
|
|
347
306
|
});
|
|
348
307
|
setActiveAssistant(result.id);
|
|
349
308
|
|
|
350
|
-
console.log(
|
|
309
|
+
console.log(
|
|
310
|
+
" " + green(`${HATCHED_EMOJI} Your assistant has hatched!`, process.stdout),
|
|
311
|
+
);
|
|
351
312
|
console.log("");
|
|
352
313
|
console.log(` ID: ${result.id}`);
|
|
353
314
|
console.log(` Name: ${result.name}`);
|