@mycadet/cli 0.12.9 → 0.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/package.json +4 -3
- package/src/__tests__/client-web-config.test.ts +0 -1
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/devices.test.ts +62 -0
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +58 -26
- package/src/__tests__/pair.test.ts +149 -78
- package/src/__tests__/ps-cloud-status.test.ts +6 -4
- package/src/__tests__/setup.test.ts +14 -4
- package/src/__tests__/teleport.test.ts +0 -15
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +265 -70
- package/src/__tests__/uninstall.test.ts +140 -8
- package/src/commands/client.ts +4 -6
- package/src/commands/devices.ts +66 -30
- package/src/commands/hatch.ts +14 -53
- package/src/commands/pair.ts +147 -38
- package/src/commands/ps.ts +10 -20
- package/src/commands/teleport.ts +2 -21
- package/src/commands/tunnel.ts +67 -28
- package/src/commands/uninstall.ts +106 -71
- package/src/commands/upgrade.ts +6 -1
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/assistant-config.ts +20 -5
- package/src/lib/cloud-client.ts +0 -3
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +3 -2
- package/src/lib/color.ts +4 -0
- package/src/lib/docker.ts +46 -70
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/environments/__tests__/paths.test.ts +27 -1
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +2 -4
- package/src/lib/hatch-local.ts +16 -59
- package/src/lib/hatch-next-steps.ts +18 -9
- package/src/lib/live-voice/tunnel.ts +2 -2
- package/src/lib/local.ts +42 -9
- package/src/lib/nginx-ingress.ts +124 -24
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/tunnel.ts +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/xdg-log.ts +11 -0
|
@@ -1,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}.`);
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -76,7 +76,10 @@ import {
|
|
|
76
76
|
startLocalDaemon,
|
|
77
77
|
stopLocalProcesses,
|
|
78
78
|
} from "../lib/local.js";
|
|
79
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
restoreContainerTunnelEdge,
|
|
81
|
+
restoreTunnelEdgeAndAutoTunnel,
|
|
82
|
+
} from "../lib/tunnel.js";
|
|
80
83
|
import {
|
|
81
84
|
leaseGuardianToken,
|
|
82
85
|
resetGuardianBootstrap,
|
|
@@ -516,6 +519,8 @@ async function upgradeDocker(
|
|
|
516
519
|
preUpgradeBackupPath: backupPath ?? undefined,
|
|
517
520
|
};
|
|
518
521
|
saveAssistantEntry(updatedEntry);
|
|
522
|
+
// Refresh the host-served web bundle before clients reload on completion.
|
|
523
|
+
await restoreContainerTunnelEdge(updatedEntry);
|
|
519
524
|
|
|
520
525
|
// Notify clients on the new service group that the upgrade succeeded.
|
|
521
526
|
await broadcastUpgradeEvent(
|
|
@@ -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
|
+
});
|
|
@@ -170,6 +170,10 @@ export interface RootProcessRecord {
|
|
|
170
170
|
/** The assistant the process fronts, so `cadet ps` can nest the row. */
|
|
171
171
|
assistantId?: string;
|
|
172
172
|
startedAt: string;
|
|
173
|
+
/** Listening port for the nginx edge. */
|
|
174
|
+
port?: number;
|
|
175
|
+
/** Managed process temporarily displaced by a pairing-only tunnel. */
|
|
176
|
+
previous?: RootProcessRecord;
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
export type RootProcessKind = "tunnel" | "app";
|
|
@@ -247,12 +251,21 @@ export function stampRootProcess(
|
|
|
247
251
|
});
|
|
248
252
|
}
|
|
249
253
|
|
|
250
|
-
/**
|
|
251
|
-
export function clearRootProcess(
|
|
254
|
+
/** Release a recorded process, preserving a replacement or restoring a displaced record. */
|
|
255
|
+
export function clearRootProcess(
|
|
256
|
+
kind: RootProcessKind,
|
|
257
|
+
expectedPid?: number,
|
|
258
|
+
): void {
|
|
252
259
|
withCliLockfileLock(() => {
|
|
253
260
|
const data = readLockfile();
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
const record = data[kind];
|
|
262
|
+
if (
|
|
263
|
+
record === undefined ||
|
|
264
|
+
(expectedPid !== undefined && record.pid !== expectedPid)
|
|
265
|
+
)
|
|
266
|
+
return;
|
|
267
|
+
if (record.previous) data[kind] = record.previous;
|
|
268
|
+
else delete data[kind];
|
|
256
269
|
writeLockfileUnlocked(data);
|
|
257
270
|
});
|
|
258
271
|
}
|
|
@@ -442,7 +455,9 @@ export function loadLatestAssistant(): AssistantEntry | null {
|
|
|
442
455
|
}
|
|
443
456
|
|
|
444
457
|
export function findAssistantByName(name: string): AssistantEntry | null {
|
|
445
|
-
return
|
|
458
|
+
return (
|
|
459
|
+
loadAllAssistants().find((entry) => entry.assistantId === name) ?? null
|
|
460
|
+
);
|
|
446
461
|
}
|
|
447
462
|
|
|
448
463
|
export function getAssistantDisplayName(entry: AssistantEntry): string {
|
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
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
3
|
+
|
|
4
|
+
const requestSchema = z.object({
|
|
5
|
+
request_id: z.string().regex(/^[a-f0-9]{64}$/),
|
|
6
|
+
poll_secret: z.string().regex(/^[a-f0-9]{64}$/),
|
|
7
|
+
verification_uri: z.string().url(),
|
|
8
|
+
user_code: z.string().min(1),
|
|
9
|
+
interval: z.number().min(1).max(10),
|
|
10
|
+
});
|
|
11
|
+
const resultSchema = z.object({ session_token: z.string().min(1) });
|
|
12
|
+
|
|
13
|
+
/** Browser approval and polling work when the terminal and browser are on different hosts. */
|
|
14
|
+
export async function pollingCloudLogin(
|
|
15
|
+
cloudUrl: string,
|
|
16
|
+
options: {
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
fetch?: (
|
|
19
|
+
input: string | URL | Request,
|
|
20
|
+
init?: RequestInit,
|
|
21
|
+
) => Promise<Response>;
|
|
22
|
+
log?: (message: string) => void;
|
|
23
|
+
} = {},
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const request = options.fetch ?? loopbackSafeFetch;
|
|
26
|
+
const log = options.log ?? console.log;
|
|
27
|
+
const signal = AbortSignal.timeout(options.timeoutMs ?? 300_000);
|
|
28
|
+
const origin = new URL(cloudUrl).origin;
|
|
29
|
+
try {
|
|
30
|
+
const started = await request(`${origin}/v1/auth/cli/start`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
signal,
|
|
33
|
+
});
|
|
34
|
+
if (!started.ok)
|
|
35
|
+
throw new Error(`Could not start terminal login (${started.status}).`);
|
|
36
|
+
const login = requestSchema.parse(await started.json());
|
|
37
|
+
log(`Open this URL to log in: ${login.verification_uri}`);
|
|
38
|
+
log(
|
|
39
|
+
`Confirm code ${login.user_code} in your browser. Waiting up to 5 minutes...`,
|
|
40
|
+
);
|
|
41
|
+
while (!signal.aborted) {
|
|
42
|
+
const response = await request(`${origin}/v1/auth/cli/poll`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
signal,
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
request_id: login.request_id,
|
|
48
|
+
poll_secret: login.poll_secret,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
if (response.status === 200)
|
|
52
|
+
return resultSchema.parse(await response.json()).session_token;
|
|
53
|
+
if (response.status === 410)
|
|
54
|
+
throw new Error("Login request expired. Run cadet login again.");
|
|
55
|
+
if (response.status !== 202)
|
|
56
|
+
throw new Error(`Login polling failed (${response.status}).`);
|
|
57
|
+
await new Promise<void>((resolve, reject) => {
|
|
58
|
+
const abort = () => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
reject(new Error("Login timed out after 5 minutes."));
|
|
61
|
+
};
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
signal.removeEventListener("abort", abort);
|
|
64
|
+
resolve();
|
|
65
|
+
}, login.interval * 1000);
|
|
66
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
if (signal.aborted) abort();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Login timed out after 5 minutes.");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (signal.aborted) throw new Error("Login timed out after 5 minutes.");
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/lib/cloud-login.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* login when the user picks that provider).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { pollingCloudLogin } from "./cloud-login-poll.js";
|
|
9
10
|
import { randomBytes } from "crypto";
|
|
10
11
|
import { createServer } from "http";
|
|
11
12
|
import type { AddressInfo } from "net";
|
|
@@ -37,7 +38,7 @@ import {
|
|
|
37
38
|
generatePkcePair,
|
|
38
39
|
} from "./workos-pkce.js";
|
|
39
40
|
|
|
40
|
-
const LOGIN_TIMEOUT_MS =
|
|
41
|
+
const LOGIN_TIMEOUT_MS = 300_000;
|
|
41
42
|
|
|
42
43
|
function escapeHtml(s: string): string {
|
|
43
44
|
return s
|
|
@@ -258,6 +259,7 @@ export function startLoopbackListener(
|
|
|
258
259
|
|
|
259
260
|
/** App-held WorkOS PKCE login */
|
|
260
261
|
export async function workosPkceLogin(cloudUrl: string): Promise<string> {
|
|
262
|
+
if (!process.stdin.isTTY) return pollingCloudLogin(cloudUrl);
|
|
261
263
|
const clientId = await fetchWorkosClientId(cloudUrl);
|
|
262
264
|
const { verifier, challenge } = generatePkcePair();
|
|
263
265
|
const state = randomBytes(32).toString("hex");
|
|
@@ -409,7 +411,6 @@ export async function registerLocalAssistantWithPlatform(
|
|
|
409
411
|
bearerToken: options.bearerToken ?? entry.bearerToken,
|
|
410
412
|
assistantApiKey,
|
|
411
413
|
cloudAssistantId: registration.assistant.id,
|
|
412
|
-
cloudBaseUrl: cloudUrl,
|
|
413
414
|
organizationId: orgId,
|
|
414
415
|
userId: user.id,
|
|
415
416
|
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
|
+
}
|