@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
|
@@ -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/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";
|
|
@@ -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/pair.ts
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Print the pairing link for another device, and the same link rendered as a
|
|
5
5
|
* QR code. Runs on the machine hosting the assistant: it mints a remote-web
|
|
6
|
-
* pairing challenge
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* pairing challenge through the gateway CLI (`gateway pairing mint <url>` —
|
|
7
|
+
* locally, or docker-exec'd into the gateway container) which approves it
|
|
8
|
+
* immediately, so running this command IS the local-presence proof and one
|
|
9
|
+
* scan (or one paste into `cadet connect import`) completes the pairing.
|
|
10
|
+
* local and docker assistants mint without any HTTP; other topologies keep
|
|
11
|
+
* the loopback HTTP mint.
|
|
9
12
|
*
|
|
10
13
|
* `--web-approve` is the reverse direction: approve a code a device already
|
|
11
14
|
* shows, for pairings that device started on its own.
|
|
@@ -38,8 +41,11 @@ import {
|
|
|
38
41
|
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
39
42
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
40
43
|
import { isLoopbackUrl, loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
44
|
+
import { dockerResourceNames } from "../lib/docker.js";
|
|
45
|
+
import { gatewayCliInvocation } from "../lib/local.js";
|
|
41
46
|
import { formatWebApproveFailure, parseGatewayErrorCode } from "../lib/pair.js";
|
|
42
47
|
import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
|
|
48
|
+
import { execOutput } from "../lib/step-runner.js";
|
|
43
49
|
|
|
44
50
|
function assistantDisplayName(entry: AssistantEntry): string {
|
|
45
51
|
return entry.name || entry.assistantName || entry.assistantId;
|
|
@@ -89,9 +95,10 @@ function printUsage(): void {
|
|
|
89
95
|
USAGE:
|
|
90
96
|
cadet pair [assistant] [options]
|
|
91
97
|
|
|
92
|
-
Mints a pairing challenge on the assistant's gateway
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
Mints a pairing challenge on the assistant's gateway (via the gateway CLI
|
|
99
|
+
for local and docker assistants, over loopback HTTP otherwise) and approves
|
|
100
|
+
it on the spot (running this command on the host machine is the approval),
|
|
101
|
+
then prints the pairing link and the same link as a QR code.
|
|
95
102
|
Scanning the QR, opening the link, or passing it to 'cadet connect import'
|
|
96
103
|
on the other device completes the pairing in one step.
|
|
97
104
|
|
|
@@ -214,26 +221,122 @@ async function exitOnHttpError(response: Response): Promise<Response> {
|
|
|
214
221
|
return response;
|
|
215
222
|
}
|
|
216
223
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Mint (and approve) a remote-web pairing challenge. `local` and `docker`
|
|
226
|
+
* remotes mint through the gateway CLI — `gateway pairing mint <url>` run on
|
|
227
|
+
* the assistant's host (locally, or docker-exec'd into the gateway container)
|
|
228
|
+
* — replacing the old loopback HTTP mint whose loopback-peer gate
|
|
229
|
+
* containerized gateways could never satisfy (published-port NAT rewrites
|
|
230
|
+
* host-originated peers to the docker bridge address). The mint executes the
|
|
231
|
+
* pairing logic directly against the file-backed challenge store, so the
|
|
232
|
+
* running gateway's verification routes see it with no IPC round-trip.
|
|
233
|
+
* Process execution is the local-presence proof; the mint command approves
|
|
234
|
+
* on the spot, so the returned challenge is already paired-ready. Other
|
|
235
|
+
* topologies keep the loopback HTTP mint they used before.
|
|
236
|
+
*/
|
|
237
|
+
async function mintPairingChallengeOrExit(
|
|
238
|
+
entry: AssistantEntry,
|
|
239
|
+
publicBaseUrl: string,
|
|
240
|
+
): Promise<RemoteWebPairingChallengeResponse> {
|
|
241
|
+
if (entry.cloud !== "local" && entry.cloud !== "docker") {
|
|
242
|
+
return mintPairingChallengeOverLoopbackOrExit(entry, publicBaseUrl);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let stdout: string;
|
|
246
|
+
try {
|
|
247
|
+
if (entry.cloud === "docker") {
|
|
248
|
+
stdout = await execOutput(
|
|
249
|
+
"docker",
|
|
250
|
+
[
|
|
251
|
+
"exec",
|
|
252
|
+
dockerResourceNames(entry.assistantId).gatewayContainer,
|
|
253
|
+
"bun",
|
|
254
|
+
"run",
|
|
255
|
+
"src/cli/index.ts",
|
|
256
|
+
"pairing",
|
|
257
|
+
"mint",
|
|
258
|
+
publicBaseUrl,
|
|
259
|
+
],
|
|
260
|
+
{ timeoutMs: 30_000 },
|
|
261
|
+
);
|
|
262
|
+
} else {
|
|
263
|
+
const invocation = gatewayCliInvocation(entry.resources);
|
|
264
|
+
stdout = await execOutput(
|
|
265
|
+
invocation.command,
|
|
266
|
+
[...invocation.args, "pairing", "mint", publicBaseUrl],
|
|
267
|
+
{
|
|
268
|
+
cwd: invocation.cwd,
|
|
269
|
+
env: invocation.env,
|
|
270
|
+
timeoutMs: 30_000,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.error(
|
|
276
|
+
`Error: could not mint a pairing challenge on the gateway ` +
|
|
277
|
+
`(${err instanceof Error ? err.message : String(err)}).`,
|
|
278
|
+
);
|
|
279
|
+
console.error("Is the assistant running? Try `cadet wake`.");
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const lastLine = stdout.split("\n").filter(Boolean).pop() ?? "";
|
|
284
|
+
let challenge: RemoteWebPairingChallengeResponse;
|
|
285
|
+
try {
|
|
286
|
+
challenge = JSON.parse(lastLine) as RemoteWebPairingChallengeResponse;
|
|
287
|
+
} catch {
|
|
288
|
+
console.error(
|
|
289
|
+
"Error: could not parse the gateway's pairing mint output.",
|
|
290
|
+
);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
if (!challenge.deviceCode || !challenge.userCode) {
|
|
294
|
+
console.error(
|
|
295
|
+
"Error: the gateway's pairing mint output is missing the challenge codes.",
|
|
296
|
+
);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
return challenge;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Mint a challenge over the gateway's loopback HTTP route — the path every
|
|
304
|
+
* topology took before the gateway-CLI mint existed. Kept for remotes that
|
|
305
|
+
* are neither `local` nor `docker` (apple containers, SSH-managed hosts,
|
|
306
|
+
* cloud-managed), where no gateway-CLI execution surface is defined.
|
|
307
|
+
*/
|
|
308
|
+
async function mintPairingChallengeOverLoopbackOrExit(
|
|
309
|
+
entry: AssistantEntry,
|
|
220
310
|
publicBaseUrl: string,
|
|
221
311
|
): Promise<RemoteWebPairingChallengeResponse> {
|
|
312
|
+
// Mint over loopback (localUrl avoids mDNS for same-machine calls), then
|
|
313
|
+
// approve immediately: running this CLI on the host IS the local-presence
|
|
314
|
+
// proof, so the other device completes pairing in one step from the link.
|
|
315
|
+
const mintUrl = (
|
|
316
|
+
entry.localUrl ||
|
|
317
|
+
entry.runtimeUrl ||
|
|
318
|
+
`http://127.0.0.1:${GATEWAY_PORT}`
|
|
319
|
+
).replace(/\/+$/, "");
|
|
222
320
|
const response = await gatewayPostOrExit(
|
|
223
|
-
|
|
321
|
+
mintUrl,
|
|
224
322
|
"/v1/remote-web/pairing-challenge",
|
|
225
323
|
{ publicBaseUrl } satisfies RemoteWebPairingChallengeRequest,
|
|
226
324
|
);
|
|
227
|
-
|
|
325
|
+
const challenge: RemoteWebPairingChallengeResponse =
|
|
326
|
+
await response.json();
|
|
327
|
+
await gatewayPostOrExit(mintUrl, "/v1/remote-web/pairing-verification", {
|
|
328
|
+
userCode: challenge.userCode,
|
|
329
|
+
} satisfies RemoteWebPairingVerificationRequest);
|
|
330
|
+
return challenge;
|
|
228
331
|
}
|
|
229
332
|
|
|
230
333
|
/**
|
|
231
334
|
* Approve a pending pairing challenge by its user code, the local-presence
|
|
232
335
|
* proof for the device-code flow. Single owner of the pairing-verification
|
|
233
|
-
* route and request body:
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
336
|
+
* route and request body: `--web-approve` calls this directly to inspect
|
|
337
|
+
* rejections and print mismatch diagnostics (see
|
|
338
|
+
* {@link formatWebApproveFailure}); the link flow needs no separate
|
|
339
|
+
* verification call — the gateway-CLI mint approves on the spot.
|
|
237
340
|
*/
|
|
238
341
|
async function postPairingVerification(
|
|
239
342
|
gatewayUrl: string,
|
|
@@ -244,20 +347,6 @@ async function postPairingVerification(
|
|
|
244
347
|
} satisfies RemoteWebPairingVerificationRequest);
|
|
245
348
|
}
|
|
246
349
|
|
|
247
|
-
/**
|
|
248
|
-
* Approve the challenge this command just minted so one scan completes
|
|
249
|
-
* pairing, exiting with a generic message on non-2xx.
|
|
250
|
-
*/
|
|
251
|
-
async function approveRemoteWebPairing(
|
|
252
|
-
gatewayUrl: string,
|
|
253
|
-
userCode: string,
|
|
254
|
-
): Promise<RemoteWebPairingVerificationResponse> {
|
|
255
|
-
const response = await exitOnHttpError(
|
|
256
|
-
await postPairingVerification(gatewayUrl, userCode),
|
|
257
|
-
);
|
|
258
|
-
return (await response.json()) as RemoteWebPairingVerificationResponse;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
350
|
export const pair = Object.assign(
|
|
262
351
|
async function pair(): Promise<void> {
|
|
263
352
|
const rawArgs = process.argv.slice(3);
|
|
@@ -355,6 +444,24 @@ export const pair = Object.assign(
|
|
|
355
444
|
}
|
|
356
445
|
}
|
|
357
446
|
|
|
447
|
+
// Minting requires host presence on this machine. Native and docker
|
|
448
|
+
// topologies qualify (the latter via docker exec); an assistant this
|
|
449
|
+
// machine only knows through `connect import` has no host presence here
|
|
450
|
+
// at all, and `cadet pair` never worked against it (its loopback gate
|
|
451
|
+
// denied remote mints). Refuse clearly instead of a confusing mint
|
|
452
|
+
// failure.
|
|
453
|
+
if (entry.cloud === "paired") {
|
|
454
|
+
console.error(
|
|
455
|
+
`Error: ${formatAssistantReference(entry)} was paired as a remote ` +
|
|
456
|
+
"assistant (cadet connect import), so this machine does not host it.",
|
|
457
|
+
);
|
|
458
|
+
console.error(
|
|
459
|
+
"Run `cadet pair` on the machine hosting the assistant, or approve " +
|
|
460
|
+
"a code it shows with `cadet pair --web-approve <code>`.",
|
|
461
|
+
);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
|
|
358
465
|
// Mint over loopback (localUrl avoids mDNS for same-machine calls), but
|
|
359
466
|
// advertise a REACHABLE url in the link, since the loopback url would point
|
|
360
467
|
// the other device at its own localhost. Prefer an explicit --url, then the
|
|
@@ -437,14 +544,10 @@ export const pair = Object.assign(
|
|
|
437
544
|
}
|
|
438
545
|
const publicBaseUrl = resolved.url;
|
|
439
546
|
|
|
440
|
-
// Mint a challenge
|
|
441
|
-
// IS the local-presence proof, so the other
|
|
442
|
-
// step from the link alone.
|
|
443
|
-
const challenge = await
|
|
444
|
-
mintUrl,
|
|
445
|
-
publicBaseUrl,
|
|
446
|
-
);
|
|
447
|
-
await approveRemoteWebPairing(mintUrl, challenge.userCode);
|
|
547
|
+
// Mint a challenge (approved on the spot by the gateway-CLI invocation):
|
|
548
|
+
// running this CLI on the host IS the local-presence proof, so the other
|
|
549
|
+
// device completes pairing in one step from the link alone.
|
|
550
|
+
const challenge = await mintPairingChallengeOrExit(entry, publicBaseUrl);
|
|
448
551
|
const pairUrl = buildRemoteWebPairingUrl(challenge);
|
|
449
552
|
const displayName = assistantDisplayName(entry);
|
|
450
553
|
const appUrl = appVariant
|
package/src/commands/ps.ts
CHANGED
|
@@ -660,35 +660,23 @@ async function getRootManagedRows(): Promise<RootManagedRow[]> {
|
|
|
660
660
|
}
|
|
661
661
|
|
|
662
662
|
/**
|
|
663
|
-
* Fold the CLI-managed process rows into ONE table with the assistant rows
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
663
|
+
* Fold the CLI-managed process rows into ONE table with the assistant rows.
|
|
664
|
+
* Tunnel agents and the nginx edge are root resources of the machine, not
|
|
665
|
+
* children of whichever assistant they front — especially as the web bundle
|
|
666
|
+
* splits into /app and /ui — so every CLI-managed row renders machine-level.
|
|
667
667
|
*/
|
|
668
668
|
async function assistantRowsWithRootProcesses(
|
|
669
669
|
assistants: AssistantEntry[],
|
|
670
670
|
healths: AssistantHealth[],
|
|
671
671
|
activeAssistantId: string | null,
|
|
672
672
|
): Promise<TableRow[]> {
|
|
673
|
-
const
|
|
674
|
-
const unattributed: TableRow[] = [];
|
|
675
|
-
const knownIds = new Set(assistants.map((entry) => entry.assistantId));
|
|
676
|
-
for (const managed of await getRootManagedRows()) {
|
|
677
|
-
if (managed.assistantId && knownIds.has(managed.assistantId)) {
|
|
678
|
-
const nested = nestedByAssistant.get(managed.assistantId) ?? [];
|
|
679
|
-
nested.push({ ...managed.row, name: ` └ ${managed.row.name}` });
|
|
680
|
-
nestedByAssistant.set(managed.assistantId, nested);
|
|
681
|
-
} else {
|
|
682
|
-
unattributed.push(managed.row);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
673
|
+
const rootRows = (await getRootManagedRows()).map((managed) => managed.row);
|
|
685
674
|
|
|
686
675
|
const rows: TableRow[] = [];
|
|
687
676
|
assistants.forEach((entry, i) => {
|
|
688
677
|
rows.push(formatAssistantListRow(entry, activeAssistantId, healths[i]!));
|
|
689
|
-
rows.push(...(nestedByAssistant.get(entry.assistantId) ?? []));
|
|
690
678
|
});
|
|
691
|
-
rows.push(...
|
|
679
|
+
rows.push(...rootRows);
|
|
692
680
|
return rows;
|
|
693
681
|
}
|
|
694
682
|
|
package/src/commands/teleport.ts
CHANGED
|
@@ -185,7 +185,6 @@ function printHelp(): void {
|
|
|
185
185
|
|
|
186
186
|
export function parseArgs(argv: string[]): {
|
|
187
187
|
from: string | undefined;
|
|
188
|
-
to: string | undefined;
|
|
189
188
|
targetEnv: "local" | "docker" | "platform" | undefined;
|
|
190
189
|
targetName: string | undefined;
|
|
191
190
|
keepSource: boolean;
|
|
@@ -193,7 +192,6 @@ export function parseArgs(argv: string[]): {
|
|
|
193
192
|
help: boolean;
|
|
194
193
|
} {
|
|
195
194
|
let from: string | undefined;
|
|
196
|
-
let to: string | undefined;
|
|
197
195
|
let targetEnv: "local" | "docker" | "platform" | undefined;
|
|
198
196
|
let targetName: string | undefined;
|
|
199
197
|
let keepSource = false;
|
|
@@ -209,11 +207,6 @@ export function parseArgs(argv: string[]): {
|
|
|
209
207
|
continue;
|
|
210
208
|
}
|
|
211
209
|
from = argv[++i];
|
|
212
|
-
} else if (arg === "--to" && i + 1 < argv.length) {
|
|
213
|
-
if (argv[i + 1].startsWith("--")) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
to = argv[++i];
|
|
217
210
|
} else if (
|
|
218
211
|
arg === "--local" ||
|
|
219
212
|
arg === "--docker" ||
|
|
@@ -242,7 +235,7 @@ export function parseArgs(argv: string[]): {
|
|
|
242
235
|
process.exit(1);
|
|
243
236
|
}
|
|
244
237
|
|
|
245
|
-
return { from,
|
|
238
|
+
return { from, targetEnv, targetName, keepSource, dryRun, help };
|
|
246
239
|
}
|
|
247
240
|
|
|
248
241
|
// ---------------------------------------------------------------------------
|
|
@@ -1253,7 +1246,6 @@ async function tryInjectCloudCredentials(
|
|
|
1253
1246
|
bearerToken: entry.bearerToken,
|
|
1254
1247
|
assistantApiKey,
|
|
1255
1248
|
cloudAssistantId: registration.assistant.id,
|
|
1256
|
-
cloudBaseUrl: cloudUrl,
|
|
1257
1249
|
organizationId: orgId,
|
|
1258
1250
|
userId: user.id,
|
|
1259
1251
|
webhookSecret: registration.webhook_secret,
|
|
@@ -1277,7 +1269,7 @@ async function tryInjectCloudCredentials(
|
|
|
1277
1269
|
export const teleport = Object.assign(
|
|
1278
1270
|
async function teleport(): Promise<void> {
|
|
1279
1271
|
const args = process.argv.slice(3);
|
|
1280
|
-
const { from,
|
|
1272
|
+
const { from, targetEnv, targetName, keepSource, dryRun, help } =
|
|
1281
1273
|
parseArgs(args);
|
|
1282
1274
|
|
|
1283
1275
|
if (help) {
|
|
@@ -1285,17 +1277,6 @@ export const teleport = Object.assign(
|
|
|
1285
1277
|
process.exit(0);
|
|
1286
1278
|
}
|
|
1287
1279
|
|
|
1288
|
-
// Legacy --to flag deprecation
|
|
1289
|
-
if (to) {
|
|
1290
|
-
console.error("Error: --to is deprecated. Use environment flags instead:");
|
|
1291
|
-
console.error(
|
|
1292
|
-
" cadet teleport --from <source> --local|--docker|--platform [name]",
|
|
1293
|
-
);
|
|
1294
|
-
console.error("");
|
|
1295
|
-
console.error("Run 'cadet teleport --help' for details.");
|
|
1296
|
-
process.exit(1);
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
1280
|
if (!from) {
|
|
1300
1281
|
printHelp();
|
|
1301
1282
|
process.exit(1);
|