@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.
- package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +2 -2
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/package.json +4 -3
- package/src/__tests__/client-web-config.test.ts +0 -1
- package/src/__tests__/cloud-releases.test.ts +23 -13
- package/src/__tests__/ensure-binary.test.ts +182 -0
- package/src/__tests__/nginx-ingress.test.ts +6 -11
- package/src/__tests__/ngrok-spawn.test.ts +49 -0
- package/src/__tests__/pair.test.ts +149 -78
- package/src/__tests__/ps-cloud-status.test.ts +4 -3
- package/src/__tests__/ps-table.test.ts +79 -0
- package/src/__tests__/tailscale.test.ts +1 -1
- package/src/__tests__/teleport.test.ts +1 -16
- package/src/__tests__/tunnel.test.ts +274 -82
- package/src/__tests__/uninstall.test.ts +141 -9
- package/src/__tests__/wake.test.ts +2 -2
- package/src/commands/backup.ts +3 -3
- package/src/commands/client.ts +4 -6
- package/src/commands/completions.test.ts +104 -0
- package/src/commands/completions.ts +87 -0
- package/src/commands/exec.ts +7 -7
- package/src/commands/flags.ts +2 -2
- package/src/commands/hatch.ts +5 -2
- package/src/commands/logs.ts +7 -7
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +63 -26
- package/src/commands/restore.ts +2 -2
- package/src/commands/retire.ts +2 -2
- package/src/commands/rollback.ts +2 -2
- package/src/commands/sleep.ts +2 -2
- package/src/commands/teleport.ts +18 -37
- package/src/commands/terminal.ts +1 -1
- package/src/commands/tunnel.ts +101 -78
- package/src/commands/uninstall.ts +108 -73
- package/src/commands/upgrade.ts +8 -8
- package/src/commands/use.ts +1 -1
- package/src/commands/wake.ts +6 -6
- package/src/index.ts +15 -0
- package/src/lib/__tests__/docker.test.ts +168 -20
- package/src/lib/__tests__/job-polling.test.ts +1 -1
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/cloud-client.ts +0 -3
- package/src/lib/cloud-login.ts +0 -1
- package/src/lib/cloud-releases.ts +6 -4
- package/src/lib/color.ts +4 -0
- package/src/lib/docker.ts +190 -102
- package/src/lib/ensure-binary.ts +171 -0
- 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/flag-args.test.ts +1 -1
- package/src/lib/flag-args.ts +1 -1
- package/src/lib/guardian-token.ts +1 -1
- package/src/lib/hatch-local.ts +15 -8
- package/src/lib/hatch-next-steps.ts +5 -9
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/local.ts +37 -4
- package/src/lib/nginx-ingress.ts +3 -5
- package/src/lib/pair.ts +1 -1
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/sync-cloud-assistants.ts +2 -2
- package/src/lib/tunnels/cloudflare.ts +21 -11
- package/src/lib/tunnels/ngrok.ts +198 -108
- package/src/lib/tunnels/tailscale.ts +34 -4
- package/src/lib/upgrade-lifecycle.ts +5 -4
|
@@ -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"),
|
|
@@ -102,10 +134,10 @@ describe("cadet uninstall", () => {
|
|
|
102
134
|
// not an assistant guard.
|
|
103
135
|
await expect(uninstall()).rejects.toThrow("process.exit:1");
|
|
104
136
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
105
|
-
expect.stringContaining("1 assistant
|
|
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
|
});
|
|
@@ -1503,14 +1503,14 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
1503
1503
|
loadRawConfigMock.mockReturnValue(webhookConfig);
|
|
1504
1504
|
ensureTunnelEdgeMock.mockRejectedValue(
|
|
1505
1505
|
new Error(
|
|
1506
|
-
"nginx is not installed, so the tunnel edge cannot start.
|
|
1506
|
+
"nginx is not installed, so the tunnel edge cannot start. See https://nginx.org/en/docs/install.html for how to install, or point NGINX_BIN at an existing binary.",
|
|
1507
1507
|
),
|
|
1508
1508
|
);
|
|
1509
1509
|
|
|
1510
1510
|
await wake();
|
|
1511
1511
|
|
|
1512
1512
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
1513
|
-
expect.stringContaining("
|
|
1513
|
+
expect.stringContaining("https://nginx.org/en/docs/install.html"),
|
|
1514
1514
|
);
|
|
1515
1515
|
// No tunnel is recorded here, so no provider can be named and the advice
|
|
1516
1516
|
// points at the help that lists them.
|
package/src/commands/backup.ts
CHANGED
|
@@ -42,10 +42,10 @@ export const backup = Object.assign(
|
|
|
42
42
|
console.log("Options:");
|
|
43
43
|
console.log(" --output <path> Path to save the .vbundle file");
|
|
44
44
|
console.log(
|
|
45
|
-
"
|
|
45
|
+
" defaults to ~/.local/share/cadet/backups/<name>-<timestamp>.vbundle",
|
|
46
46
|
);
|
|
47
47
|
console.log(
|
|
48
|
-
` --export-timeout <secs> Export request timeout in seconds
|
|
48
|
+
` --export-timeout <secs> Export request timeout in seconds, defaults to ${DEFAULT_EXPORT_TIMEOUT_MS / 1000}`,
|
|
49
49
|
);
|
|
50
50
|
console.log("");
|
|
51
51
|
console.log("Examples:");
|
|
@@ -254,7 +254,7 @@ async function backupPlatform(
|
|
|
254
254
|
const cloudToken = readCloudToken();
|
|
255
255
|
if (!cloudToken) {
|
|
256
256
|
console.error(
|
|
257
|
-
"Not logged in. Run 'cadet login' first
|
|
257
|
+
"Not logged in. Run 'cadet login' first.",
|
|
258
258
|
);
|
|
259
259
|
process.exit(1);
|
|
260
260
|
}
|
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,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
import { saveAssistantEntry } from "../lib/assistant-config.js";
|
|
7
|
+
import { completeCadet } from "./completions.js";
|
|
8
|
+
|
|
9
|
+
const COMMAND_NAMES = [
|
|
10
|
+
"backup",
|
|
11
|
+
"clean",
|
|
12
|
+
"client",
|
|
13
|
+
"completions",
|
|
14
|
+
"confirm",
|
|
15
|
+
"connect",
|
|
16
|
+
"hatch",
|
|
17
|
+
"ps",
|
|
18
|
+
"retire",
|
|
19
|
+
"sleep",
|
|
20
|
+
"use",
|
|
21
|
+
"wake",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
describe("completeCadet", () => {
|
|
25
|
+
let lockfileDir: string;
|
|
26
|
+
let prevLockfileDir: string | undefined;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
lockfileDir = mkdtempSync(join(tmpdir(), "cadet-completions-"));
|
|
30
|
+
prevLockfileDir = process.env.CADET_LOCKFILE_DIR;
|
|
31
|
+
process.env.CADET_LOCKFILE_DIR = lockfileDir;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
if (prevLockfileDir === undefined) delete process.env.CADET_LOCKFILE_DIR;
|
|
36
|
+
else process.env.CADET_LOCKFILE_DIR = prevLockfileDir;
|
|
37
|
+
rmSync(lockfileDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function seed(id: string): void {
|
|
41
|
+
saveAssistantEntry({
|
|
42
|
+
assistantId: id,
|
|
43
|
+
cloud: "docker",
|
|
44
|
+
runtimeUrl: "http://localhost:7830",
|
|
45
|
+
hatchedAt: new Date().toISOString(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("no words yet offers every command name, sorted", () => {
|
|
50
|
+
const input: string[] = [];
|
|
51
|
+
expect(completeCadet(input, COMMAND_NAMES)).toEqual(
|
|
52
|
+
[...COMMAND_NAMES].sort(),
|
|
53
|
+
);
|
|
54
|
+
// The word under the cursor is popped from a COPY — the caller's array
|
|
55
|
+
// (bash's COMP_WORDS) must come back untouched.
|
|
56
|
+
expect(input).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("first positional of an assistant-name command offers known assistant names", () => {
|
|
60
|
+
seed("quill-owl-d4e5f6");
|
|
61
|
+
seed("fable-fox-a1b2c3");
|
|
62
|
+
// `cadet retire <TAB>` — the empty trailing word is the word being completed.
|
|
63
|
+
expect(completeCadet(["retire", ""], COMMAND_NAMES)).toEqual([
|
|
64
|
+
"fable-fox-a1b2c3",
|
|
65
|
+
"quill-owl-d4e5f6",
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("partial words are returned unfiltered — the shell does prefix filtering", () => {
|
|
70
|
+
seed("quill-owl-d4e5f6");
|
|
71
|
+
seed("fable-fox-a1b2c3");
|
|
72
|
+
expect(completeCadet(["retire", "fable"], COMMAND_NAMES)).toEqual([
|
|
73
|
+
"fable-fox-a1b2c3",
|
|
74
|
+
"quill-owl-d4e5f6",
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("flag prefixes fall through to the shell default (no candidates)", () => {
|
|
79
|
+
seed("fable-fox-a1b2c3");
|
|
80
|
+
expect(completeCadet(["retire", "--"], COMMAND_NAMES)).toEqual([]);
|
|
81
|
+
expect(completeCadet(["retire", "--name"], COMMAND_NAMES)).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("deeper positionals of an assistant-name command offer nothing", () => {
|
|
85
|
+
seed("fable-fox-a1b2c3");
|
|
86
|
+
expect(
|
|
87
|
+
completeCadet(["exec", "fable-fox-a1b2c3", ""], COMMAND_NAMES),
|
|
88
|
+
).toEqual([]);
|
|
89
|
+
expect(
|
|
90
|
+
completeCadet(["logs", "fable-fox-a1b2c3", "tail", ""], COMMAND_NAMES),
|
|
91
|
+
).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("commands that take no assistant name offer nothing after the command word", () => {
|
|
95
|
+
seed("fable-fox-a1b2c3");
|
|
96
|
+
expect(completeCadet(["hatch", ""], COMMAND_NAMES)).toEqual([]);
|
|
97
|
+
expect(completeCadet(["ps", ""], COMMAND_NAMES)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("an unreadable/empty lockfile completes quietly with no names", () => {
|
|
101
|
+
// No entries seeded — the lockfile dir exists but has no lockfile.json.
|
|
102
|
+
expect(completeCadet(["retire", ""], COMMAND_NAMES)).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { loadAllAssistants } from "../lib/assistant-config.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Commands whose first positional argument is an assistant name — completing
|
|
5
|
+
* their first word offers the names of known assistants (from the lockfile)
|
|
6
|
+
* instead of falling through to shell file completion.
|
|
7
|
+
*/
|
|
8
|
+
const ASSISTANT_NAME_COMMANDS = new Set([
|
|
9
|
+
"exec",
|
|
10
|
+
"logs",
|
|
11
|
+
"message",
|
|
12
|
+
"restart",
|
|
13
|
+
"restore",
|
|
14
|
+
"retire",
|
|
15
|
+
"rollback",
|
|
16
|
+
"sleep",
|
|
17
|
+
"teleport",
|
|
18
|
+
"terminal",
|
|
19
|
+
"tunnel",
|
|
20
|
+
"unpair",
|
|
21
|
+
"upgrade",
|
|
22
|
+
"use",
|
|
23
|
+
"wake",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Assistant names known to this CLI's lockfile; [] when the lockfile is unreadable. */
|
|
27
|
+
function assistantNames(): string[] {
|
|
28
|
+
try {
|
|
29
|
+
return loadAllAssistants()
|
|
30
|
+
.map((entry) => entry.assistantId)
|
|
31
|
+
.sort();
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve completion candidates for the word the cursor is on.
|
|
39
|
+
*
|
|
40
|
+
* `words` carries every command-line word after `cadet` up to the cursor,
|
|
41
|
+
* INCLUDING the (possibly empty) word being completed — the same shape bash
|
|
42
|
+
* passes via `COMP_WORDS[@]:1`. The returned list is UNFILTERED: the shell
|
|
43
|
+
* (`compgen -W`) does the prefix filtering, so the CLI only decides WHAT
|
|
44
|
+
* KIND of thing completes here.
|
|
45
|
+
*/
|
|
46
|
+
export function completeCadet(
|
|
47
|
+
input: string[],
|
|
48
|
+
commandNames: readonly string[],
|
|
49
|
+
): string[] {
|
|
50
|
+
const words = [...input];
|
|
51
|
+
const toComplete = words.pop() ?? "";
|
|
52
|
+
const preceding = words;
|
|
53
|
+
|
|
54
|
+
// Completing the command word itself (or nothing typed yet).
|
|
55
|
+
if (preceding.length === 0) {
|
|
56
|
+
return [...commandNames].sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// First positional of an assistant-name command: offer instance names.
|
|
60
|
+
// Flag prefixes fall through to shell default (no flag completion yet).
|
|
61
|
+
if (
|
|
62
|
+
preceding.length === 1 &&
|
|
63
|
+
ASSISTANT_NAME_COMMANDS.has(preceding[0]) &&
|
|
64
|
+
!toComplete.startsWith("-")
|
|
65
|
+
) {
|
|
66
|
+
return assistantNames();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const BASH_COMPLETION_SCRIPT = `# bash completion for the cadet CLI — load with: eval "$(cadet completions)"
|
|
73
|
+
_cadet_completions() {
|
|
74
|
+
local candidates
|
|
75
|
+
candidates=$(cadet __complete "\${COMP_WORDS[@]:1}" 2>/dev/null)
|
|
76
|
+
COMPREPLY=($(compgen -W "$candidates" -- "\${COMP_WORDS[COMP_CWORD]}"))
|
|
77
|
+
}
|
|
78
|
+
complete -F _cadet_completions cadet`;
|
|
79
|
+
|
|
80
|
+
// ── Entry point ─────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export const completions = Object.assign(
|
|
83
|
+
async function completions(): Promise<void> {
|
|
84
|
+
console.log(BASH_COMPLETION_SCRIPT);
|
|
85
|
+
},
|
|
86
|
+
{ description: `Print bash completion setup (eval "$(cadet completions)")` },
|
|
87
|
+
);
|
package/src/commands/exec.ts
CHANGED
|
@@ -75,28 +75,28 @@ export const exec = Object.assign(
|
|
|
75
75
|
console.log("");
|
|
76
76
|
console.log("Arguments:");
|
|
77
77
|
console.log(
|
|
78
|
-
" <name> Name of the assistant
|
|
78
|
+
" <name> Name of the assistant, defaults to active",
|
|
79
79
|
);
|
|
80
80
|
console.log(
|
|
81
81
|
" <command...> Command and arguments to run (after --)",
|
|
82
82
|
);
|
|
83
83
|
console.log("");
|
|
84
84
|
console.log("Options:");
|
|
85
|
-
console.log(" --service <svc> Target service
|
|
85
|
+
console.log(" --service <svc> Target service, defaults to assistant");
|
|
86
86
|
console.log(
|
|
87
87
|
" -it Interactive mode with TTY (like docker exec -it)",
|
|
88
88
|
);
|
|
89
89
|
console.log(
|
|
90
|
-
" --timeout <secs> Timeout in seconds
|
|
90
|
+
" --timeout <secs> Timeout in seconds — default 30, 0 = no timeout",
|
|
91
91
|
);
|
|
92
92
|
console.log(
|
|
93
|
-
" --verbose Show debug output
|
|
93
|
+
" --verbose Show debug output",
|
|
94
94
|
);
|
|
95
95
|
console.log("");
|
|
96
96
|
console.log("Services:");
|
|
97
|
-
console.log(" assistant
|
|
98
|
-
console.log(" gateway
|
|
99
|
-
console.log(" credential-executor
|
|
97
|
+
console.log(" assistant");
|
|
98
|
+
console.log(" gateway");
|
|
99
|
+
console.log(" credential-executor");
|
|
100
100
|
console.log("");
|
|
101
101
|
console.log("Examples:");
|
|
102
102
|
console.log(" cadet exec -- ls -la /workspace");
|
package/src/commands/flags.ts
CHANGED
|
@@ -59,7 +59,7 @@ function printFlagTable(flags: FeatureFlagEntry[]): void {
|
|
|
59
59
|
console.log(formatRow(row));
|
|
60
60
|
}
|
|
61
61
|
console.log("");
|
|
62
|
-
console.log("* = overridden
|
|
62
|
+
console.log("* = overridden");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function printHelp(): void {
|
|
@@ -67,7 +67,7 @@ function printHelp(): void {
|
|
|
67
67
|
console.log("");
|
|
68
68
|
console.log("Show and toggle feature flags for the active assistant.");
|
|
69
69
|
console.log(
|
|
70
|
-
"Reads from the gateway's merged flag state
|
|
70
|
+
"Reads from the gateway's merged flag state.",
|
|
71
71
|
);
|
|
72
72
|
console.log("");
|
|
73
73
|
console.log("Subcommands:");
|
package/src/commands/hatch.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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";
|
|
@@ -76,7 +77,7 @@ function parseArgs(): HatchArgs {
|
|
|
76
77
|
" --watch Run assistant and gateway in watch mode (hot reload on source changes)",
|
|
77
78
|
);
|
|
78
79
|
console.log(
|
|
79
|
-
" --source <path> Build images from a local source tree at <path
|
|
80
|
+
" --source <path> Build images from a local source tree at <path>. Useful for callers that want each run to pick up local CLI changes.",
|
|
80
81
|
);
|
|
81
82
|
console.log(
|
|
82
83
|
" --keep-alive Stay alive after hatch, exit when gateway stops",
|
|
@@ -347,7 +348,9 @@ async function hatchCadetPlatform(): Promise<void> {
|
|
|
347
348
|
});
|
|
348
349
|
setActiveAssistant(result.id);
|
|
349
350
|
|
|
350
|
-
console.log(
|
|
351
|
+
console.log(
|
|
352
|
+
" " + green(`${HATCHED_EMOJI} Your assistant has hatched!`, process.stdout),
|
|
353
|
+
);
|
|
351
354
|
console.log("");
|
|
352
355
|
console.log(` ID: ${result.id}`);
|
|
353
356
|
console.log(` Name: ${result.name}`);
|
package/src/commands/logs.ts
CHANGED
|
@@ -32,19 +32,19 @@ function printHelp(): void {
|
|
|
32
32
|
console.log("");
|
|
33
33
|
console.log("Arguments:");
|
|
34
34
|
console.log(
|
|
35
|
-
" <name> Name of the assistant
|
|
35
|
+
" <name> Name of the assistant, defaults to latest",
|
|
36
36
|
);
|
|
37
37
|
console.log("");
|
|
38
38
|
console.log("Options:");
|
|
39
|
-
console.log(" -f, --follow Follow log output
|
|
40
|
-
console.log(" -n, --tail <N> Show last N lines
|
|
39
|
+
console.log(" -f, --follow Follow log output");
|
|
40
|
+
console.log(" -n, --tail <N> Show last N lines");
|
|
41
41
|
console.log(" -t, --timestamps Show timestamps on each line");
|
|
42
42
|
console.log(
|
|
43
|
-
" --since <time> Show logs since timestamp or relative
|
|
43
|
+
" --since <time> Show logs since timestamp or relative, e.g. 10m or 2h",
|
|
44
44
|
);
|
|
45
45
|
console.log(" --until <time> Show logs until timestamp or relative");
|
|
46
46
|
console.log(
|
|
47
|
-
" -s, --service <name> Filter to a specific service
|
|
47
|
+
" -s, --service <name> Filter to a specific service",
|
|
48
48
|
);
|
|
49
49
|
console.log(" -h, --help Show this help");
|
|
50
50
|
}
|
|
@@ -186,13 +186,13 @@ async function showLocalLogs(
|
|
|
186
186
|
|
|
187
187
|
if (opts.since && !sinceDate) {
|
|
188
188
|
console.error(
|
|
189
|
-
`Error: Could not parse --since value '${opts.since}'. Use relative
|
|
189
|
+
`Error: Could not parse --since value '${opts.since}'. Use a relative value such as 10m or 2h, or ISO format.`,
|
|
190
190
|
);
|
|
191
191
|
process.exit(1);
|
|
192
192
|
}
|
|
193
193
|
if (opts.until && !untilDate) {
|
|
194
194
|
console.error(
|
|
195
|
-
`Error: Could not parse --until value '${opts.until}'. Use relative
|
|
195
|
+
`Error: Could not parse --until value '${opts.until}'. Use a relative value such as 10m or 2h, or ISO format.`,
|
|
196
196
|
);
|
|
197
197
|
process.exit(1);
|
|
198
198
|
}
|