@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
package/src/commands/tunnel.ts
CHANGED
|
@@ -22,9 +22,10 @@ import {
|
|
|
22
22
|
} from "../lib/ingress-config.js";
|
|
23
23
|
import {
|
|
24
24
|
ensureTunnelEdge,
|
|
25
|
-
|
|
25
|
+
getNginxVersion,
|
|
26
26
|
type TunnelEdge,
|
|
27
27
|
} from "../lib/nginx-ingress.js";
|
|
28
|
+
import { ensureBinaryInstalled } from "../lib/ensure-binary.js";
|
|
28
29
|
import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
|
|
29
30
|
import {
|
|
30
31
|
ADOPTED_TUNNEL_RE,
|
|
@@ -46,7 +47,7 @@ interface TunnelArgs {
|
|
|
46
47
|
provider: TunnelProviderName;
|
|
47
48
|
domain: string | null;
|
|
48
49
|
clearDomain: boolean;
|
|
49
|
-
|
|
50
|
+
foreground: boolean;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const FLAGS_WITH_VALUES = ["--provider", "--domain"] as const;
|
|
@@ -56,7 +57,7 @@ function parseArgs(): TunnelArgs {
|
|
|
56
57
|
let provider: TunnelProviderName | null = null;
|
|
57
58
|
let domain: string | null = null;
|
|
58
59
|
let clearDomain = false;
|
|
59
|
-
let
|
|
60
|
+
let foreground = false;
|
|
60
61
|
|
|
61
62
|
for (let i = 0; i < args.length; i++) {
|
|
62
63
|
const arg = args[i];
|
|
@@ -73,29 +74,30 @@ function parseArgs(): TunnelArgs {
|
|
|
73
74
|
console.log(
|
|
74
75
|
"How far that URL reaches depends on the provider; omit --provider for",
|
|
75
76
|
);
|
|
76
|
-
console.log("the default. A tailscale tunnel is reachable only from your");
|
|
77
77
|
console.log(
|
|
78
|
-
"
|
|
78
|
+
"the default. A tailscale tunnel is reachable only from your",
|
|
79
79
|
);
|
|
80
|
-
console.log(
|
|
80
|
+
console.log(
|
|
81
|
+
"own tailnet; webhook integrations need a public tunnel. See Providers",
|
|
82
|
+
);
|
|
83
|
+
console.log("below.");
|
|
81
84
|
console.log("");
|
|
82
85
|
console.log(
|
|
83
86
|
"The tunnel always fronts the local nginx edge, which is started automatically.",
|
|
84
87
|
);
|
|
85
88
|
console.log(
|
|
86
|
-
"nginx must be installed
|
|
89
|
+
"nginx must be installed; if it is missing, cadet offers to install it.",
|
|
87
90
|
);
|
|
88
91
|
console.log("");
|
|
89
92
|
console.log("Arguments:");
|
|
90
93
|
console.log(
|
|
91
|
-
" <name> Name of the assistant
|
|
94
|
+
" <name> Name of the assistant, defaults to active or only local",
|
|
92
95
|
);
|
|
93
96
|
console.log("");
|
|
94
97
|
console.log("Options:");
|
|
95
98
|
console.log(
|
|
96
|
-
` --provider <provider> Tunnel provider: ${TUNNEL_PROVIDERS.join(", ")}`,
|
|
99
|
+
` --provider <provider> Tunnel provider: ${TUNNEL_PROVIDERS.join(", ")} — default ${DEFAULT_TUNNEL_PROVIDER}`,
|
|
97
100
|
);
|
|
98
|
-
console.log(` (default: ${DEFAULT_TUNNEL_PROVIDER})`);
|
|
99
101
|
console.log(
|
|
100
102
|
" --domain <domain> Reserved domain to bind, for providers that support",
|
|
101
103
|
);
|
|
@@ -110,19 +112,22 @@ function parseArgs(): TunnelArgs {
|
|
|
110
112
|
" tunnel without one. Cannot be combined with --domain.",
|
|
111
113
|
);
|
|
112
114
|
console.log(
|
|
113
|
-
`
|
|
115
|
+
` --foreground Stream the tunnel's logs to this terminal. By default the tunnel`,
|
|
114
116
|
);
|
|
115
117
|
console.log(
|
|
116
|
-
"
|
|
118
|
+
" runs in the background: the command returns after printing the public",
|
|
117
119
|
);
|
|
118
120
|
console.log(
|
|
119
|
-
|
|
121
|
+
` URL, and logs stream to tunnel.log in the CLI log directory. A background`,
|
|
120
122
|
);
|
|
121
123
|
console.log(
|
|
122
|
-
" adopted an already-running
|
|
124
|
+
" tunnel stops with `kill <pid>`, except when it adopted an already-running",
|
|
123
125
|
);
|
|
124
126
|
console.log(
|
|
125
|
-
" command): killing the supervisor then
|
|
127
|
+
" tunnel (started outside this command): killing the supervisor then",
|
|
128
|
+
);
|
|
129
|
+
console.log(
|
|
130
|
+
" leaves that tunnel running.",
|
|
126
131
|
);
|
|
127
132
|
console.log("");
|
|
128
133
|
console.log("Providers:");
|
|
@@ -137,7 +142,8 @@ function parseArgs(): TunnelArgs {
|
|
|
137
142
|
console.log("");
|
|
138
143
|
console.log("Examples:");
|
|
139
144
|
console.log(` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER}`);
|
|
140
|
-
const domainExample =
|
|
145
|
+
const domainExample =
|
|
146
|
+
PROVIDER_META[DEFAULT_TUNNEL_PROVIDER].domainExample;
|
|
141
147
|
if (domainExample) {
|
|
142
148
|
console.log(
|
|
143
149
|
` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --domain ${domainExample}`,
|
|
@@ -148,9 +154,6 @@ function parseArgs(): TunnelArgs {
|
|
|
148
154
|
}
|
|
149
155
|
console.log(" $ cadet tunnel --provider cloudflare");
|
|
150
156
|
console.log(" $ cadet tunnel my-assistant --provider tailscale");
|
|
151
|
-
console.log(
|
|
152
|
-
` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --detach`,
|
|
153
|
-
);
|
|
154
157
|
process.exit(0);
|
|
155
158
|
} else if (arg === "--provider") {
|
|
156
159
|
const next = args[i + 1];
|
|
@@ -174,7 +177,8 @@ function parseArgs(): TunnelArgs {
|
|
|
174
177
|
} else if (arg === "--domain") {
|
|
175
178
|
const next = args[i + 1];
|
|
176
179
|
if (!next || next.startsWith("-")) {
|
|
177
|
-
const domainExample =
|
|
180
|
+
const domainExample =
|
|
181
|
+
PROVIDER_META[DEFAULT_TUNNEL_PROVIDER].domainExample;
|
|
178
182
|
console.error(
|
|
179
183
|
`Error: --domain requires a value${domainExample ? `, e.g. --domain ${domainExample}` : ""}`,
|
|
180
184
|
);
|
|
@@ -184,8 +188,8 @@ function parseArgs(): TunnelArgs {
|
|
|
184
188
|
i++;
|
|
185
189
|
} else if (arg === "--clear-domain") {
|
|
186
190
|
clearDomain = true;
|
|
187
|
-
} else if (arg === "
|
|
188
|
-
|
|
191
|
+
} else if (arg === "--foreground") {
|
|
192
|
+
foreground = true;
|
|
189
193
|
} else if (arg.startsWith("-")) {
|
|
190
194
|
console.error(`Error: Unknown option '${arg}'.`);
|
|
191
195
|
process.exit(1);
|
|
@@ -199,9 +203,7 @@ function parseArgs(): TunnelArgs {
|
|
|
199
203
|
|
|
200
204
|
if (provider === null) {
|
|
201
205
|
provider = DEFAULT_TUNNEL_PROVIDER;
|
|
202
|
-
console.log(
|
|
203
|
-
`Using the default provider: ${provider} (pass --provider to choose another).`,
|
|
204
|
-
);
|
|
206
|
+
console.log(`Using the default provider: ${provider}.`);
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
if (domain && !providerSupportsReservedDomain(provider)) {
|
|
@@ -225,7 +227,7 @@ function parseArgs(): TunnelArgs {
|
|
|
225
227
|
process.exit(1);
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
return { assistantName, provider, domain, clearDomain,
|
|
230
|
+
return { assistantName, provider, domain, clearDomain, foreground };
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
/** A tunnelable assistant plus the gateway port and workspace the edge fronts. */
|
|
@@ -343,31 +345,31 @@ const TUNNEL_BACKGROUND_START_TIMEOUT_MS = 35_000;
|
|
|
343
345
|
const TUNNEL_ESTABLISHED_RE = /Tunnel established: (\S+)/;
|
|
344
346
|
|
|
345
347
|
/**
|
|
346
|
-
* Re-invoke `cadet tunnel
|
|
347
|
-
*
|
|
348
|
-
* "Tunnel established" or adopted-tunnel line (readiness) or
|
|
349
|
-
*
|
|
350
|
-
*
|
|
348
|
+
* Re-invoke `cadet tunnel --foreground` as a detached background process via
|
|
349
|
+
* `relaunchDetached`, waiting for the child to either print its
|
|
350
|
+
* "Tunnel established" or adopted-tunnel line (readiness) or exit early
|
|
351
|
+
* (failure), so a misconfigured provider still fails loudly in the terminal
|
|
352
|
+
* that ran `cadet tunnel`.
|
|
351
353
|
*
|
|
352
|
-
* The log
|
|
353
|
-
*
|
|
354
|
-
* truncate and read each other's readiness output.
|
|
354
|
+
* The log file is the fixed `tunnel.log` so there is one stable path to tail;
|
|
355
|
+
* concurrent tunnel runs share (and truncate) it.
|
|
355
356
|
*/
|
|
356
357
|
async function spawnDetachedTunnel(): Promise<void> {
|
|
357
358
|
const childArgs: string[] = ["tunnel"];
|
|
358
359
|
for (const arg of process.argv.slice(3)) {
|
|
359
|
-
if (arg === "
|
|
360
|
+
if (arg === "--foreground") {
|
|
360
361
|
continue;
|
|
361
362
|
}
|
|
362
363
|
childArgs.push(arg);
|
|
363
364
|
}
|
|
365
|
+
childArgs.push("--foreground");
|
|
364
366
|
|
|
365
367
|
let publicUrl: string | undefined;
|
|
366
368
|
let adopted = false;
|
|
367
369
|
|
|
368
370
|
const { child, logPath, ready, exitCode } = await relaunchDetached({
|
|
369
371
|
args: childArgs,
|
|
370
|
-
logFile:
|
|
372
|
+
logFile: "tunnel.log",
|
|
371
373
|
timeoutMs: TUNNEL_BACKGROUND_START_TIMEOUT_MS,
|
|
372
374
|
isReady: (logPath) => {
|
|
373
375
|
let logText = "";
|
|
@@ -415,57 +417,78 @@ async function spawnDetachedTunnel(): Promise<void> {
|
|
|
415
417
|
|
|
416
418
|
export const tunnel = Object.assign(
|
|
417
419
|
async function tunnel(): Promise<void> {
|
|
418
|
-
|
|
420
|
+
const { assistantName, provider, domain, clearDomain, foreground } =
|
|
421
|
+
parseArgs();
|
|
419
422
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
423
|
+
if (!foreground) {
|
|
424
|
+
await spawnDetachedTunnel();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
424
427
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
+
if (PROVIDER_META[provider].comingSoon) {
|
|
429
|
+
throw cadetTunnelComingSoonError(provider);
|
|
430
|
+
}
|
|
428
431
|
|
|
429
|
-
|
|
430
|
-
|
|
432
|
+
const { entry, gatewayPort, workspaceDir } =
|
|
433
|
+
resolveLocalTunnelTarget(assistantName);
|
|
431
434
|
|
|
432
|
-
|
|
433
|
-
|
|
435
|
+
const preserveIngressUrl =
|
|
436
|
+
provider === "tailscale" && guardTailnetOnlyWebhookIngress(workspaceDir);
|
|
434
437
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
438
|
+
if (clearDomain) {
|
|
439
|
+
clearReservedDomain();
|
|
440
|
+
console.log("Cleared the saved reserved domain.");
|
|
441
|
+
}
|
|
439
442
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
443
|
+
const nginxVersion = await ensureBinaryInstalled({
|
|
444
|
+
binary: "nginx",
|
|
445
|
+
purpose: "the tunnel edge",
|
|
446
|
+
packages: {
|
|
447
|
+
brew: ["brew", "install", "nginx"],
|
|
448
|
+
"apt-get": ["sudo", "apt-get", "install", "-y", "nginx"],
|
|
449
|
+
dnf: ["sudo", "dnf", "install", "-y", "nginx"],
|
|
450
|
+
pacman: ["sudo", "pacman", "-S", "--noconfirm", "nginx"],
|
|
451
|
+
zypper: ["sudo", "zypper", "install", "-y", "nginx"],
|
|
452
|
+
choco: ["choco", "install", "-y", "nginx"],
|
|
453
|
+
},
|
|
454
|
+
manualHint:
|
|
455
|
+
"See https://nginx.org/en/docs/install.html for how to install, or point NGINX_BIN at an existing binary.",
|
|
456
|
+
probe: getNginxVersion,
|
|
446
457
|
});
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
458
|
+
if (!nginxVersion) {
|
|
459
|
+
console.error("Error: nginx is required to run the tunnel edge.");
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
451
462
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
463
|
+
let edge: TunnelEdge;
|
|
464
|
+
try {
|
|
465
|
+
edge = await ensureTunnelEdge({
|
|
466
|
+
assistantId: entry.assistantId,
|
|
467
|
+
workspaceDir,
|
|
468
|
+
gatewayPort,
|
|
469
|
+
});
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.error(
|
|
472
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
473
|
+
);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
console.log(
|
|
478
|
+
`${edge.started ? "Started" : "Reusing"} the nginx edge on 127.0.0.1:${edge.port}.`,
|
|
479
|
+
);
|
|
456
480
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
481
|
+
const baseTunnelOpts = {
|
|
482
|
+
port: edge.port,
|
|
483
|
+
assistantId: entry.assistantId,
|
|
484
|
+
workspaceDir,
|
|
485
|
+
};
|
|
462
486
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
},
|
|
487
|
+
await runProviderTunnel(provider, {
|
|
488
|
+
...baseTunnelOpts,
|
|
489
|
+
...(domain ? { domain } : {}),
|
|
490
|
+
...(preserveIngressUrl ? { preserveIngressUrl } : {}),
|
|
491
|
+
});
|
|
492
|
+
},
|
|
469
493
|
{ description: "Create a tunnel for a locally hosted assistant" },
|
|
470
494
|
);
|
|
471
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, renameSync, rmSync, unlinkSync } from "fs";
|
|
2
|
-
import { basename, dirname, join } from "path";
|
|
2
|
+
import { basename, dirname, join, sep } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
import { retireDocker } from "../lib/docker.js";
|
|
15
15
|
import {
|
|
16
16
|
getConfigDirs,
|
|
17
|
-
getInputHistoryPath,
|
|
18
17
|
getLogDir,
|
|
18
|
+
getSharedStateDir,
|
|
19
19
|
getStateDir,
|
|
20
20
|
} from "../lib/environments/paths.js";
|
|
21
21
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
@@ -98,62 +98,84 @@ function findInstalledPackageRoot(startDir: string): string | null {
|
|
|
98
98
|
return null;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** The CLI's own install location, when it can be deleted. */
|
|
102
|
+
interface SelfDeleteTarget {
|
|
103
|
+
path: string;
|
|
104
|
+
description: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
108
|
+
* Where this CLI is running from, when that is an INSTALL we can remove:
|
|
109
|
+
* the compiled `cadet` binary (process.execPath IS the install) or a global
|
|
110
|
+
* package install (bun install -g mycadet / npm install -g mycadet). Under
|
|
111
|
+
* a source checkout or a bare runtime (bun/node), null — the runtime binary
|
|
112
|
+
* must never be a deletion target. The uninstall prompt lists the same
|
|
113
|
+
* target it later deletes, so both share this resolver.
|
|
107
114
|
*/
|
|
108
|
-
function
|
|
115
|
+
function resolveSelfDeleteTarget(): SelfDeleteTarget | null {
|
|
109
116
|
const execPath = process.execPath;
|
|
110
117
|
const execBase = basename(execPath);
|
|
111
118
|
if (execBase === "cadet" || execBase === "cadet.exe") {
|
|
119
|
+
return { path: execPath, description: "the Cadet executable" };
|
|
120
|
+
}
|
|
121
|
+
const runningFile = fileURLToPath(import.meta.url);
|
|
122
|
+
const pkgRoot = findInstalledPackageRoot(dirname(runningFile));
|
|
123
|
+
if (pkgRoot) {
|
|
124
|
+
return { path: pkgRoot, description: "the installed Cadet CLI package" };
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Delete the install {@link resolveSelfDeleteTarget} found. The two shapes
|
|
131
|
+
* delete differently — the running binary needs the win32 rename dance, a
|
|
132
|
+
* package dir is a plain recursive delete — and the bin symlinks the
|
|
133
|
+
* package manager planted (e.g. ~/.bun/bin/cadet, the npm global bin) are
|
|
134
|
+
* left dangling — say so.
|
|
135
|
+
*/
|
|
136
|
+
function selfDelete(): void {
|
|
137
|
+
const target = resolveSelfDeleteTarget();
|
|
138
|
+
if (!target) {
|
|
139
|
+
console.log(
|
|
140
|
+
"Cadet is running from a source checkout — skipping self-removal.",
|
|
141
|
+
);
|
|
142
|
+
console.log(
|
|
143
|
+
"Remove the installed copy with `bun remove -g mycadet` or `npm uninstall -g mycadet`.",
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const execBase = basename(target.path);
|
|
148
|
+
const isCompiledBinary = execBase === "cadet" || execBase === "cadet.exe";
|
|
149
|
+
if (isCompiledBinary) {
|
|
112
150
|
try {
|
|
113
151
|
if (process.platform === "win32") {
|
|
114
152
|
// Windows refuses to unlink a running executable — stage it under a
|
|
115
153
|
// junk name and delete that.
|
|
116
|
-
const stale = `${
|
|
117
|
-
renameSync(
|
|
154
|
+
const stale = `${target.path}.uninstalled`;
|
|
155
|
+
renameSync(target.path, stale);
|
|
118
156
|
rmSync(stale, { force: true, recursive: true });
|
|
119
157
|
} else {
|
|
120
|
-
unlinkSync(
|
|
158
|
+
unlinkSync(target.path);
|
|
121
159
|
}
|
|
122
|
-
console.log(`Removed ${
|
|
123
|
-
return;
|
|
160
|
+
console.log(`Removed ${target.path}.`);
|
|
124
161
|
} catch (err) {
|
|
125
162
|
console.warn(
|
|
126
|
-
`⚠️ Could not remove ${
|
|
163
|
+
`⚠️ Could not remove ${target.path}: ${
|
|
127
164
|
err instanceof Error ? err.message : String(err)
|
|
128
165
|
} — remove it by hand.`,
|
|
129
166
|
);
|
|
130
|
-
return;
|
|
131
167
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Package-manager install: remove the package directory we are running
|
|
135
|
-
// from, best-effort. Bin symlinks the package manager planted (e.g.
|
|
136
|
-
// ~/.bun/bin/cadet, the npm global bin) are left dangling — say so.
|
|
137
|
-
const runningFile = fileURLToPath(import.meta.url);
|
|
138
|
-
const pkgRoot = findInstalledPackageRoot(dirname(runningFile));
|
|
139
|
-
if (!pkgRoot) {
|
|
140
|
-
console.log(
|
|
141
|
-
"Cadet is running from a source checkout — skipping self-removal.",
|
|
142
|
-
);
|
|
143
|
-
console.log(
|
|
144
|
-
"Remove the installed copy with `bun remove -g mycadet` (or `npm uninstall -g mycadet`).",
|
|
145
|
-
);
|
|
146
168
|
return;
|
|
147
169
|
}
|
|
148
170
|
try {
|
|
149
|
-
rmSync(
|
|
150
|
-
console.log(`Removed ${
|
|
171
|
+
rmSync(target.path, { recursive: true, force: true });
|
|
172
|
+
console.log(`Removed ${target.path}.`);
|
|
151
173
|
console.log(
|
|
152
174
|
"The `cadet` command on your PATH is now stale — remove it if your package manager didn't (`bun remove -g mycadet` or `npm uninstall -g mycadet`).",
|
|
153
175
|
);
|
|
154
176
|
} catch (err) {
|
|
155
177
|
console.warn(
|
|
156
|
-
`⚠️ Could not remove ${
|
|
178
|
+
`⚠️ Could not remove ${target.path}: ${
|
|
157
179
|
err instanceof Error ? err.message : String(err)
|
|
158
180
|
} — remove it with \`bun remove -g mycadet\` (or \`npm uninstall -g mycadet\`).`,
|
|
159
181
|
);
|
|
@@ -173,10 +195,24 @@ function collectDeleteTargets(env = getCurrentEnvironment()): DeleteTarget[] {
|
|
|
173
195
|
{ path: getLogDir(env), description: "logs" },
|
|
174
196
|
// Not environment-scoped: deleting it affects every cadet CLI on the
|
|
175
197
|
// machine — called out in the prompt, not hidden.
|
|
176
|
-
{
|
|
198
|
+
{
|
|
199
|
+
path: getSharedStateDir(),
|
|
200
|
+
description: "shared state, including the terminal input history",
|
|
201
|
+
},
|
|
177
202
|
];
|
|
178
203
|
// Only targets that exist — the prompt lists what will actually go.
|
|
179
|
-
|
|
204
|
+
const existing = targets.filter((t) => existsSync(t.path));
|
|
205
|
+
// The log dir lives INSIDE a listed target (the config dir on
|
|
206
|
+
// macOS/Linux, the state dir on Windows) and goes with its parent's
|
|
207
|
+
// recursive delete — never list a path twice.
|
|
208
|
+
const deduped: DeleteTarget[] = [];
|
|
209
|
+
for (const t of existing) {
|
|
210
|
+
const covered = deduped.some(
|
|
211
|
+
(k) => t.path === k.path || t.path.startsWith(k.path + sep),
|
|
212
|
+
);
|
|
213
|
+
if (!covered) deduped.push(t);
|
|
214
|
+
}
|
|
215
|
+
return deduped;
|
|
180
216
|
}
|
|
181
217
|
|
|
182
218
|
function rmrf(target: string): void {
|
|
@@ -190,63 +226,48 @@ export const uninstall = Object.assign(
|
|
|
190
226
|
console.log("Usage: cadet uninstall [--yes]");
|
|
191
227
|
console.log("");
|
|
192
228
|
console.log(
|
|
193
|
-
"Remove Cadet entirely: delete every location it uses for this environment (assistant state, retired archives, config, logs) and
|
|
229
|
+
"Remove Cadet entirely: retire any remaining assistants, delete every location it uses for this environment (assistant state, retired archives, config, logs), and remove the Cadet installation itself.",
|
|
194
230
|
);
|
|
195
231
|
process.exit(0);
|
|
196
232
|
}
|
|
197
233
|
const yes = args.includes("--yes");
|
|
198
234
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// INCLUDING the retired archives, so retiring first (which archives)
|
|
202
|
-
// only matters for the teardown it performs: docker assistants keep
|
|
203
|
-
// containers, volumes, and a network in docker that the filesystem
|
|
204
|
-
// sweep below never reaches. A warning, not a refusal (v0.0.10 run
|
|
205
|
-
// feedback: the hard error just blocked the uninstall).
|
|
235
|
+
const env = getCurrentEnvironment();
|
|
236
|
+
const envLabel = env.name ?? "production";
|
|
206
237
|
const existing = loadAllAssistants();
|
|
238
|
+
const targets = collectDeleteTargets(env);
|
|
239
|
+
const selfTarget = resolveSelfDeleteTarget();
|
|
240
|
+
|
|
241
|
+
console.log(
|
|
242
|
+
`This removes Cadet entirely for the '${envLabel}' environment and cannot be undone.`,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Assistants still on the rack are retired — with their data deleted —
|
|
246
|
+
// as part of the uninstall, and retiring happens only AFTER the
|
|
247
|
+
// confirmation below: it is part of what the prompt describes, never a
|
|
248
|
+
// side effect of running the command. Uninstall deletes every location
|
|
249
|
+
// cadet uses INCLUDING the retired archives, so retiring first (which
|
|
250
|
+
// archives) only matters for the teardown it performs: docker
|
|
251
|
+
// assistants keep containers, volumes, and a network in docker that
|
|
252
|
+
// the filesystem sweep below never reaches. A warning, not a refusal
|
|
253
|
+
// (v0.0.10 run feedback: the hard error just blocked the uninstall).
|
|
207
254
|
if (existing.length > 0) {
|
|
208
255
|
console.warn(
|
|
209
|
-
`\n⚠️ ${existing.length} assistant
|
|
256
|
+
`\n⚠️ ${existing.length} ${existing.length === 1 ? "assistant still exists" : "assistants still exist"}: ${existing.map((a) => a.assistantId).join(", ")}`,
|
|
210
257
|
);
|
|
211
258
|
console.warn(
|
|
212
|
-
" They will be retired
|
|
259
|
+
" They will be retired as part of this uninstall — their data (and any retired archives) will be permanently deleted.",
|
|
213
260
|
);
|
|
214
|
-
for (const entry of existing) {
|
|
215
|
-
if (entry.cloud === "apple-container" || entry.cloud === "paired") {
|
|
216
|
-
console.log(` Skipping '${entry.assistantId}' (${entry.cloud}) — its lifecycle is managed elsewhere.`);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
try {
|
|
220
|
-
await retireAssistantData(entry);
|
|
221
|
-
} catch (err) {
|
|
222
|
-
console.error(
|
|
223
|
-
` Warning: could not fully retire '${entry.assistantId}': ${err instanceof Error ? err.message : String(err)}`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
261
|
}
|
|
228
262
|
|
|
229
|
-
|
|
230
|
-
const envLabel = env.name ?? "production";
|
|
231
|
-
const targets = collectDeleteTargets(env);
|
|
232
|
-
const execPath = process.execPath;
|
|
233
|
-
|
|
234
|
-
console.log(
|
|
235
|
-
`This removes Cadet entirely for the '${envLabel}' environment and cannot be undone.`,
|
|
236
|
-
);
|
|
237
|
-
if (targets.length === 0) {
|
|
263
|
+
if (targets.length === 0 && selfTarget === null) {
|
|
238
264
|
console.log("No Cadet data directories found to delete.");
|
|
239
265
|
} else {
|
|
240
266
|
console.log("It will permanently delete:");
|
|
241
|
-
for (const t of targets) {
|
|
242
|
-
console.log(` - ${t.path}
|
|
267
|
+
for (const t of selfTarget ? [...targets, selfTarget] : targets) {
|
|
268
|
+
console.log(` - ${t.path} — ${t.description}`);
|
|
243
269
|
}
|
|
244
270
|
}
|
|
245
|
-
const execBase = basename(execPath);
|
|
246
|
-
const isCompiledBinary = execBase === "cadet" || execBase === "cadet.exe";
|
|
247
|
-
if (isCompiledBinary) {
|
|
248
|
-
console.log(`It will then remove the Cadet executable at ${execPath}.`);
|
|
249
|
-
}
|
|
250
271
|
|
|
251
272
|
if (!yes) {
|
|
252
273
|
if (!canPromptForConfirmation()) {
|
|
@@ -265,6 +286,20 @@ export const uninstall = Object.assign(
|
|
|
265
286
|
}
|
|
266
287
|
}
|
|
267
288
|
|
|
289
|
+
for (const entry of existing) {
|
|
290
|
+
if (entry.cloud === "apple-container" || entry.cloud === "paired") {
|
|
291
|
+
console.log(` Skipping '${entry.assistantId}' on ${entry.cloud} — its lifecycle is managed elsewhere.`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
await retireAssistantData(entry);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error(
|
|
298
|
+
` Warning: could not fully retire '${entry.assistantId}': ${err instanceof Error ? err.message : String(err)}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
268
303
|
for (const t of targets) {
|
|
269
304
|
rmrf(t.path);
|
|
270
305
|
console.log(`Deleted ${t.path}.`);
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
GATEWAY_INTERNAL_PORT,
|
|
20
20
|
ASSISTANT_INTERNAL_PORT,
|
|
21
21
|
dockerResourceNames,
|
|
22
|
+
pullImage,
|
|
22
23
|
startContainers,
|
|
23
24
|
stopContainers,
|
|
24
25
|
} from "../lib/docker";
|
|
@@ -46,7 +47,6 @@ import {
|
|
|
46
47
|
} from "../lib/backup-ops.js";
|
|
47
48
|
import { emitCliError, categorizeUpgradeError } from "../lib/cli-error.js";
|
|
48
49
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
49
|
-
import { exec } from "../lib/step-runner.js";
|
|
50
50
|
import {
|
|
51
51
|
broadcastUpgradeEvent,
|
|
52
52
|
attemptFailedStateRestore,
|
|
@@ -118,12 +118,12 @@ function parseArgs(): UpgradeArgs {
|
|
|
118
118
|
console.log("");
|
|
119
119
|
console.log("Arguments:");
|
|
120
120
|
console.log(
|
|
121
|
-
" <name> Name of the assistant to upgrade
|
|
121
|
+
" <name> Name of the assistant to upgrade, defaults to active or only assistant",
|
|
122
122
|
);
|
|
123
123
|
console.log("");
|
|
124
124
|
console.log("Options:");
|
|
125
125
|
console.log(
|
|
126
|
-
" --version <version> Target version to upgrade to
|
|
126
|
+
" --version <version> Target version to upgrade to, defaults to the CLI version",
|
|
127
127
|
);
|
|
128
128
|
console.log(
|
|
129
129
|
" --latest Upgrade to the latest stable release, updating the CLI first if needed",
|
|
@@ -316,7 +316,7 @@ async function upgradeDocker(
|
|
|
316
316
|
);
|
|
317
317
|
return;
|
|
318
318
|
}
|
|
319
|
-
console.log(`🔁 Reinstalling ${versionTag}
|
|
319
|
+
console.log(`🔁 Reinstalling ${versionTag} with --force...\n`);
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
console.log("🔍 Resolving image references...");
|
|
@@ -406,7 +406,7 @@ async function upgradeDocker(
|
|
|
406
406
|
try {
|
|
407
407
|
for (const [service, image] of pullImages) {
|
|
408
408
|
console.log(` Pulling ${service}: ${image}`);
|
|
409
|
-
await
|
|
409
|
+
await pullImage(image);
|
|
410
410
|
}
|
|
411
411
|
} catch (pullErr) {
|
|
412
412
|
const detail = pullErr instanceof Error ? pullErr.message : String(pullErr);
|
|
@@ -453,7 +453,7 @@ async function upgradeDocker(
|
|
|
453
453
|
// Clean up old pre-upgrade backups, keep last 3
|
|
454
454
|
pruneOldBackups(entry.assistantId, 3);
|
|
455
455
|
} else {
|
|
456
|
-
console.warn("⚠️ Pre-upgrade backup failed
|
|
456
|
+
console.warn("⚠️ Pre-upgrade backup failed, continuing with upgrade\n");
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
// Persist the backup path so `cadet rollback` can restore the exact backup
|
|
@@ -891,7 +891,7 @@ async function upgradeLocal(
|
|
|
891
891
|
);
|
|
892
892
|
return;
|
|
893
893
|
}
|
|
894
|
-
console.log(`🔁 Reinstalling ${targetVersion}
|
|
894
|
+
console.log(`🔁 Reinstalling ${targetVersion} with --force...\n`);
|
|
895
895
|
}
|
|
896
896
|
|
|
897
897
|
console.log(
|
|
@@ -933,7 +933,7 @@ async function upgradeLocal(
|
|
|
933
933
|
console.log(` Backup saved: ${backupPath}\n`);
|
|
934
934
|
pruneOldBackups(entry.assistantId, 3);
|
|
935
935
|
} else {
|
|
936
|
-
console.warn("⚠️ Pre-upgrade backup failed
|
|
936
|
+
console.warn("⚠️ Pre-upgrade backup failed, continuing with upgrade\n");
|
|
937
937
|
}
|
|
938
938
|
|
|
939
939
|
let bootstrapSecret = entry.guardianBootstrapSecret;
|
package/src/commands/use.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const use = Object.assign(
|
|
|
38
38
|
`Active assistant: ${formatAssistantReference(result.entry)}`,
|
|
39
39
|
);
|
|
40
40
|
} else {
|
|
41
|
-
console.log(`Active assistant: ${active}
|
|
41
|
+
console.log(`Active assistant: ${active} — not found in lockfile`);
|
|
42
42
|
}
|
|
43
43
|
} else {
|
|
44
44
|
console.log("No active assistant set.");
|