@mycadet/cli 0.12.9 → 0.12.10
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/package.json +4 -3
- package/src/__tests__/client-web-config.test.ts +0 -1
- package/src/__tests__/nginx-ingress.test.ts +3 -9
- package/src/__tests__/pair.test.ts +149 -78
- package/src/__tests__/ps-cloud-status.test.ts +4 -3
- package/src/__tests__/teleport.test.ts +0 -15
- package/src/__tests__/tunnel.test.ts +253 -68
- package/src/__tests__/uninstall.test.ts +140 -8
- package/src/commands/client.ts +4 -6
- package/src/commands/hatch.ts +4 -1
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +6 -18
- package/src/commands/teleport.ts +2 -21
- package/src/commands/tunnel.ts +25 -25
- package/src/commands/uninstall.ts +106 -71
- 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/color.ts +4 -0
- package/src/lib/docker.ts +40 -58
- 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 +15 -8
- package/src/lib/hatch-next-steps.ts +5 -9
- package/src/lib/local.ts +36 -3
- package/src/lib/nginx-ingress.ts +1 -3
- package/src/lib/step-runner.ts +6 -1
package/src/commands/tunnel.ts
CHANGED
|
@@ -47,7 +47,7 @@ interface TunnelArgs {
|
|
|
47
47
|
provider: TunnelProviderName;
|
|
48
48
|
domain: string | null;
|
|
49
49
|
clearDomain: boolean;
|
|
50
|
-
|
|
50
|
+
foreground: boolean;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const FLAGS_WITH_VALUES = ["--provider", "--domain"] as const;
|
|
@@ -57,7 +57,7 @@ function parseArgs(): TunnelArgs {
|
|
|
57
57
|
let provider: TunnelProviderName | null = null;
|
|
58
58
|
let domain: string | null = null;
|
|
59
59
|
let clearDomain = false;
|
|
60
|
-
let
|
|
60
|
+
let foreground = false;
|
|
61
61
|
|
|
62
62
|
for (let i = 0; i < args.length; i++) {
|
|
63
63
|
const arg = args[i];
|
|
@@ -112,19 +112,22 @@ function parseArgs(): TunnelArgs {
|
|
|
112
112
|
" tunnel without one. Cannot be combined with --domain.",
|
|
113
113
|
);
|
|
114
114
|
console.log(
|
|
115
|
-
`
|
|
115
|
+
` --foreground Stream the tunnel's logs to this terminal. By default the tunnel`,
|
|
116
116
|
);
|
|
117
117
|
console.log(
|
|
118
|
-
"
|
|
118
|
+
" runs in the background: the command returns after printing the public",
|
|
119
119
|
);
|
|
120
120
|
console.log(
|
|
121
|
-
|
|
121
|
+
` URL, and logs stream to tunnel.log in the CLI log directory. A background`,
|
|
122
122
|
);
|
|
123
123
|
console.log(
|
|
124
|
-
" adopted an already-running
|
|
124
|
+
" tunnel stops with `kill <pid>`, except when it adopted an already-running",
|
|
125
125
|
);
|
|
126
126
|
console.log(
|
|
127
|
-
" command): killing the supervisor then
|
|
127
|
+
" tunnel (started outside this command): killing the supervisor then",
|
|
128
|
+
);
|
|
129
|
+
console.log(
|
|
130
|
+
" leaves that tunnel running.",
|
|
128
131
|
);
|
|
129
132
|
console.log("");
|
|
130
133
|
console.log("Providers:");
|
|
@@ -151,9 +154,6 @@ function parseArgs(): TunnelArgs {
|
|
|
151
154
|
}
|
|
152
155
|
console.log(" $ cadet tunnel --provider cloudflare");
|
|
153
156
|
console.log(" $ cadet tunnel my-assistant --provider tailscale");
|
|
154
|
-
console.log(
|
|
155
|
-
` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --detach`,
|
|
156
|
-
);
|
|
157
157
|
process.exit(0);
|
|
158
158
|
} else if (arg === "--provider") {
|
|
159
159
|
const next = args[i + 1];
|
|
@@ -188,8 +188,8 @@ function parseArgs(): TunnelArgs {
|
|
|
188
188
|
i++;
|
|
189
189
|
} else if (arg === "--clear-domain") {
|
|
190
190
|
clearDomain = true;
|
|
191
|
-
} else if (arg === "
|
|
192
|
-
|
|
191
|
+
} else if (arg === "--foreground") {
|
|
192
|
+
foreground = true;
|
|
193
193
|
} else if (arg.startsWith("-")) {
|
|
194
194
|
console.error(`Error: Unknown option '${arg}'.`);
|
|
195
195
|
process.exit(1);
|
|
@@ -227,7 +227,7 @@ function parseArgs(): TunnelArgs {
|
|
|
227
227
|
process.exit(1);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
return { assistantName, provider, domain, clearDomain,
|
|
230
|
+
return { assistantName, provider, domain, clearDomain, foreground };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/** A tunnelable assistant plus the gateway port and workspace the edge fronts. */
|
|
@@ -345,31 +345,31 @@ const TUNNEL_BACKGROUND_START_TIMEOUT_MS = 35_000;
|
|
|
345
345
|
const TUNNEL_ESTABLISHED_RE = /Tunnel established: (\S+)/;
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
* Re-invoke `cadet tunnel
|
|
349
|
-
*
|
|
350
|
-
* "Tunnel established" or adopted-tunnel line (readiness) or
|
|
351
|
-
*
|
|
352
|
-
*
|
|
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`.
|
|
353
353
|
*
|
|
354
|
-
* The log
|
|
355
|
-
*
|
|
356
|
-
* 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.
|
|
357
356
|
*/
|
|
358
357
|
async function spawnDetachedTunnel(): Promise<void> {
|
|
359
358
|
const childArgs: string[] = ["tunnel"];
|
|
360
359
|
for (const arg of process.argv.slice(3)) {
|
|
361
|
-
if (arg === "
|
|
360
|
+
if (arg === "--foreground") {
|
|
362
361
|
continue;
|
|
363
362
|
}
|
|
364
363
|
childArgs.push(arg);
|
|
365
364
|
}
|
|
365
|
+
childArgs.push("--foreground");
|
|
366
366
|
|
|
367
367
|
let publicUrl: string | undefined;
|
|
368
368
|
let adopted = false;
|
|
369
369
|
|
|
370
370
|
const { child, logPath, ready, exitCode } = await relaunchDetached({
|
|
371
371
|
args: childArgs,
|
|
372
|
-
logFile:
|
|
372
|
+
logFile: "tunnel.log",
|
|
373
373
|
timeoutMs: TUNNEL_BACKGROUND_START_TIMEOUT_MS,
|
|
374
374
|
isReady: (logPath) => {
|
|
375
375
|
let logText = "";
|
|
@@ -417,10 +417,10 @@ async function spawnDetachedTunnel(): Promise<void> {
|
|
|
417
417
|
|
|
418
418
|
export const tunnel = Object.assign(
|
|
419
419
|
async function tunnel(): Promise<void> {
|
|
420
|
-
const { assistantName, provider, domain, clearDomain,
|
|
420
|
+
const { assistantName, provider, domain, clearDomain, foreground } =
|
|
421
421
|
parseArgs();
|
|
422
422
|
|
|
423
|
-
if (
|
|
423
|
+
if (!foreground) {
|
|
424
424
|
await spawnDetachedTunnel();
|
|
425
425
|
return;
|
|
426
426
|
}
|
|
@@ -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
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}' on ${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) {
|
|
267
|
+
for (const t of selfTarget ? [...targets, selfTarget] : targets) {
|
|
242
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}.`);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { LocalInstanceResources } from "../assistant-config.js";
|
|
4
|
+
import { gatewayCliInvocation } from "../local.js";
|
|
5
|
+
|
|
6
|
+
function fakeResources(instanceDir: string): LocalInstanceResources {
|
|
7
|
+
// gatewayCliInvocation only reads instanceDir; the rest of the resources
|
|
8
|
+
// shape is irrelevant here.
|
|
9
|
+
return { instanceDir } as unknown as LocalInstanceResources;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("gatewayCliInvocation", () => {
|
|
13
|
+
test("runs the gateway package's CLI entry through bun", () => {
|
|
14
|
+
const invocation = gatewayCliInvocation();
|
|
15
|
+
expect(invocation.args[0]).toBe("run");
|
|
16
|
+
expect(invocation.args[1]).toMatch(/gateway\/src\/cli\/index\.ts$/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("pins CADET_WORKSPACE_DIR to the instance dir for named instances", () => {
|
|
20
|
+
const invocation = gatewayCliInvocation(
|
|
21
|
+
fakeResources("/data/assistants/testy"),
|
|
22
|
+
);
|
|
23
|
+
expect(invocation.env.CADET_WORKSPACE_DIR).toBe(
|
|
24
|
+
"/data/assistants/testy/workspace",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("pins GATEWAY_SECURITY_DIR to the instance dir for named instances", () => {
|
|
29
|
+
const invocation = gatewayCliInvocation(
|
|
30
|
+
fakeResources("/data/assistants/testy"),
|
|
31
|
+
);
|
|
32
|
+
expect(invocation.env.GATEWAY_SECURITY_DIR).toBe(
|
|
33
|
+
"/data/assistants/testy/gateway-security",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("leaves the workspace resolution untouched for the default instance", () => {
|
|
38
|
+
delete process.env.CADET_WORKSPACE_DIR;
|
|
39
|
+
const invocation = gatewayCliInvocation();
|
|
40
|
+
expect(invocation.env.CADET_WORKSPACE_DIR).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/lib/cloud-client.ts
CHANGED
|
@@ -604,7 +604,6 @@ export interface CredentialInjectionParams {
|
|
|
604
604
|
bearerToken?: string;
|
|
605
605
|
assistantApiKey?: string | null;
|
|
606
606
|
cloudAssistantId: string;
|
|
607
|
-
cloudBaseUrl: string;
|
|
608
607
|
organizationId: string;
|
|
609
608
|
userId?: string;
|
|
610
609
|
webhookSecret?: string | null;
|
|
@@ -631,8 +630,6 @@ export async function injectCredentialsIntoAssistant(
|
|
|
631
630
|
promises.push(inject("cadet:assistant_api_key", params.assistantApiKey));
|
|
632
631
|
}
|
|
633
632
|
|
|
634
|
-
promises.push(inject("cadet:platform_base_url", params.cloudBaseUrl));
|
|
635
|
-
|
|
636
633
|
if (params.webhookSecret) {
|
|
637
634
|
promises.push(inject("cadet:webhook_secret", params.webhookSecret));
|
|
638
635
|
}
|
package/src/lib/cloud-login.ts
CHANGED
|
@@ -409,7 +409,6 @@ export async function registerLocalAssistantWithPlatform(
|
|
|
409
409
|
bearerToken: options.bearerToken ?? entry.bearerToken,
|
|
410
410
|
assistantApiKey,
|
|
411
411
|
cloudAssistantId: registration.assistant.id,
|
|
412
|
-
cloudBaseUrl: cloudUrl,
|
|
413
412
|
organizationId: orgId,
|
|
414
413
|
userId: user.id,
|
|
415
414
|
webhookSecret: registration.webhook_secret,
|
package/src/lib/color.ts
CHANGED
|
@@ -14,3 +14,7 @@ export function red(text: string, stream?: NodeJS.WriteStream): string {
|
|
|
14
14
|
export function yellow(text: string, stream?: NodeJS.WriteStream): string {
|
|
15
15
|
return colorize("\x1b[33m", text, stream);
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
export function green(text: string, stream?: NodeJS.WriteStream): string {
|
|
19
|
+
return colorize("\x1b[32m", text, stream);
|
|
20
|
+
}
|
package/src/lib/docker.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
ASSISTANT_INTERNAL_PORT,
|
|
28
28
|
getDefaultPorts,
|
|
29
29
|
} from "./environments/paths.js";
|
|
30
|
+
import { green, yellow } from "./color.js";
|
|
30
31
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
31
32
|
import { leaseGuardianToken } from "./guardian-token";
|
|
32
33
|
import { logHatchNextSteps } from "./hatch-next-steps.js";
|
|
@@ -42,7 +43,6 @@ import { isDevVersion } from "./version-compat.js";
|
|
|
42
43
|
import {
|
|
43
44
|
collectProviderApiKey,
|
|
44
45
|
configureHatchProviderApiKey,
|
|
45
|
-
formatProviderName,
|
|
46
46
|
promptProviderChoice,
|
|
47
47
|
resolveHatchProvider,
|
|
48
48
|
shouldPromptForHatchProvider,
|
|
@@ -1544,7 +1544,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1544
1544
|
}
|
|
1545
1545
|
}
|
|
1546
1546
|
}
|
|
1547
|
-
log("✅ Docker images acquired");
|
|
1547
|
+
log(green("✅ Docker images acquired", process.stdout));
|
|
1548
1548
|
}
|
|
1549
1549
|
|
|
1550
1550
|
const res = dockerResourceNames(instanceName);
|
|
@@ -1745,18 +1745,16 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1745
1745
|
);
|
|
1746
1746
|
} else if (providerSetupAction === "missing-token" && provider !== null) {
|
|
1747
1747
|
log(
|
|
1748
|
-
|
|
1749
|
-
|
|
1748
|
+
yellow(
|
|
1749
|
+
`⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
|
|
1750
|
+
` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
|
|
1751
|
+
process.stdout,
|
|
1752
|
+
),
|
|
1750
1753
|
);
|
|
1751
1754
|
} else if (
|
|
1752
1755
|
providerSetupAction === "configure" &&
|
|
1753
1756
|
provider !== undefined
|
|
1754
1757
|
) {
|
|
1755
|
-
log(
|
|
1756
|
-
provider === null
|
|
1757
|
-
? "Checking provider credentials..."
|
|
1758
|
-
: `Checking ${formatProviderName(provider)} credentials...`,
|
|
1759
|
-
);
|
|
1760
1758
|
await configureHatchProviderApiKey({
|
|
1761
1759
|
gatewayUrl: runtimeUrl,
|
|
1762
1760
|
provider,
|
|
@@ -1855,25 +1853,12 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1855
1853
|
};
|
|
1856
1854
|
|
|
1857
1855
|
if (detached) {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
container: containerName,
|
|
1865
|
-
},
|
|
1866
|
-
log,
|
|
1867
|
-
);
|
|
1868
|
-
log("");
|
|
1869
|
-
log(`Stop with: cadet retire ${instanceName}`);
|
|
1870
|
-
|
|
1871
|
-
// Lease a guardian token even in detached mode so that `cadet ps`,
|
|
1872
|
-
// `cadet exec`, and other CLI commands can authenticate to the
|
|
1873
|
-
// gateway. Skip the /readyz readiness poll (the caller asked to detach
|
|
1874
|
-
// and not block) but retry the lease itself since the gateway may need
|
|
1875
|
-
// a moment to accept connections after the container starts.
|
|
1876
|
-
const leaseStart = Date.now();
|
|
1856
|
+
// Lease a guardian token BEFORE declaring the assistant hatched so the
|
|
1857
|
+
// banner means `cadet ps`, `cadet exec`, and other CLI commands can
|
|
1858
|
+
// already authenticate. Skip the /readyz readiness poll (the caller
|
|
1859
|
+
// asked to detach and not block) but retry the lease silently since
|
|
1860
|
+
// the gateway may need a moment to accept connections after the
|
|
1861
|
+
// container starts; only a final failure surfaces to the user.
|
|
1877
1862
|
const leaseDeadline = containersUpAt + DOCKER_READY_TIMEOUT_MS;
|
|
1878
1863
|
let guardianAccessToken: string | undefined;
|
|
1879
1864
|
while (Date.now() < leaseDeadline) {
|
|
@@ -1884,27 +1869,33 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1884
1869
|
bootstrapSecret,
|
|
1885
1870
|
);
|
|
1886
1871
|
guardianAccessToken = tokenData.accessToken;
|
|
1887
|
-
const leaseElapsed = ((Date.now() - leaseStart) / 1000).toFixed(1);
|
|
1888
|
-
log(
|
|
1889
|
-
`Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId})`,
|
|
1890
|
-
);
|
|
1891
1872
|
break;
|
|
1892
|
-
} catch
|
|
1893
|
-
const elapsed = ((Date.now() - leaseStart) / 1000).toFixed(0);
|
|
1894
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1895
|
-
log(
|
|
1896
|
-
`Guardian token lease: attempt failed after ${elapsed}s (${msg.split("\n")[0]}), retrying...`,
|
|
1897
|
-
);
|
|
1873
|
+
} catch {
|
|
1898
1874
|
await new Promise((r) => setTimeout(r, 2000));
|
|
1899
1875
|
}
|
|
1900
1876
|
}
|
|
1901
1877
|
if (!guardianAccessToken) {
|
|
1902
1878
|
log(
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1879
|
+
yellow(
|
|
1880
|
+
`⚠️ Guardian token lease failed after ${DOCKER_READY_TIMEOUT_MS / 1000}s.\n` +
|
|
1881
|
+
` The assistant is running but CLI commands (cadet ps, cadet exec) will not authenticate.\n` +
|
|
1882
|
+
` Re-hatch or run \`cadet setup\` to recover.`,
|
|
1883
|
+
process.stdout,
|
|
1884
|
+
),
|
|
1906
1885
|
);
|
|
1907
1886
|
}
|
|
1887
|
+
|
|
1888
|
+
log(`\n${green("✅ Docker assistant hatched!", process.stdout)}\n`);
|
|
1889
|
+
log("Instance details:");
|
|
1890
|
+
printInstanceDetails(
|
|
1891
|
+
{
|
|
1892
|
+
name: instanceName,
|
|
1893
|
+
runtime: runtimeUrl,
|
|
1894
|
+
container: containerName,
|
|
1895
|
+
},
|
|
1896
|
+
log,
|
|
1897
|
+
);
|
|
1898
|
+
log("");
|
|
1908
1899
|
return { ready: true, guardianAccessToken };
|
|
1909
1900
|
}
|
|
1910
1901
|
|
|
@@ -1985,7 +1976,6 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1985
1976
|
// Lease guardian token. The /readyz check confirms both gateway and
|
|
1986
1977
|
// assistant are reachable. Retry with backoff in case there is a brief
|
|
1987
1978
|
// window where readiness passes but the guardian endpoint is not yet ready.
|
|
1988
|
-
log(`Guardian token lease: starting for ${instanceName} at ${runtimeUrl}`);
|
|
1989
1979
|
const leaseStart = Date.now();
|
|
1990
1980
|
const leaseDeadline = start + DOCKER_READY_TIMEOUT_MS;
|
|
1991
1981
|
let leaseSuccess = false;
|
|
@@ -1999,38 +1989,30 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1999
1989
|
instanceName,
|
|
2000
1990
|
bootstrapSecret,
|
|
2001
1991
|
);
|
|
2002
|
-
const leaseElapsed = ((Date.now() - leaseStart) / 1000).toFixed(1);
|
|
2003
|
-
log(
|
|
2004
|
-
`Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId}, expiresAt=${tokenData.accessTokenExpiresAt})`,
|
|
2005
|
-
);
|
|
2006
1992
|
guardianAccessToken = tokenData.accessToken;
|
|
2007
1993
|
leaseSuccess = true;
|
|
2008
1994
|
break;
|
|
2009
1995
|
} catch (err) {
|
|
2010
1996
|
lastLeaseError =
|
|
2011
1997
|
err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
2012
|
-
// Log periodically so the user knows we're still trying
|
|
2013
|
-
const elapsed = ((Date.now() - leaseStart) / 1000).toFixed(0);
|
|
2014
|
-
log(
|
|
2015
|
-
`Guardian token lease: attempt failed after ${elapsed}s (${
|
|
2016
|
-
lastLeaseError.split("\n")[0]
|
|
2017
|
-
}), retrying...`,
|
|
2018
|
-
);
|
|
2019
1998
|
}
|
|
2020
1999
|
await new Promise((r) => setTimeout(r, 2000));
|
|
2021
2000
|
}
|
|
2022
2001
|
|
|
2023
2002
|
if (!leaseSuccess) {
|
|
2024
2003
|
log(
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2004
|
+
yellow(
|
|
2005
|
+
`\u26a0\ufe0f Guardian token lease: FAILED after ${(
|
|
2006
|
+
(Date.now() - leaseStart) /
|
|
2007
|
+
1000
|
|
2008
|
+
).toFixed(1)}s — ${lastLeaseError ?? "unknown error"}`,
|
|
2009
|
+
process.stdout,
|
|
2010
|
+
),
|
|
2029
2011
|
);
|
|
2030
2012
|
}
|
|
2031
2013
|
|
|
2032
2014
|
log("");
|
|
2033
|
-
log(`\u2705 Docker containers are up and running
|
|
2015
|
+
log(green(`\u2705 Docker containers are up and running!`, process.stdout));
|
|
2034
2016
|
log(` Name: ${instanceName}`);
|
|
2035
2017
|
log(` Runtime: ${runtimeUrl}`);
|
|
2036
2018
|
log("");
|