@mycadet/cli 0.12.6-dev.202610082233.54244ee → 0.12.7
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/eslint.config.mjs +7 -0
- package/node_modules/@mycadet/local-mode/src/__tests__/environment.test.ts +1 -4
- package/node_modules/@mycadet/local-mode/src/config.ts +14 -19
- package/node_modules/@mycadet/local-mode/src/index.ts +5 -0
- package/node_modules/@mycadet/local-mode/src/local-signing-key.ts +73 -0
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +18 -3
- package/package.json +4 -3
- package/src/__tests__/assistant-config.test.ts +30 -28
- package/src/__tests__/debug.test.ts +8 -8
- package/src/__tests__/default-main-screen-formatting.test.ts +5 -4
- package/src/__tests__/flags.test.ts +2 -2
- package/src/__tests__/hatch-provider-secrets.test.ts +3 -3
- package/src/__tests__/ingress-config.test.ts +2 -2
- package/src/__tests__/login-loopback.test.ts +1 -1
- package/src/__tests__/nginx-ingress.test.ts +68 -76
- package/src/__tests__/pair.test.ts +135 -121
- package/src/__tests__/platform-client.test.ts +2 -2
- package/src/__tests__/recover.test.ts +96 -3
- package/src/__tests__/retire.test.ts +5 -3
- package/src/__tests__/signing-key-continuity.test.ts +261 -0
- package/src/__tests__/sleep.test.ts +189 -6
- package/src/__tests__/tailscale-tunnel.test.ts +1 -1
- package/src/__tests__/uninstall.test.ts +171 -0
- package/src/__tests__/upgrade-local.test.ts +16 -3
- package/src/__tests__/use.test.ts +2 -2
- package/src/__tests__/wake-error.test.ts +76 -0
- package/src/__tests__/wake.test.ts +309 -36
- package/src/commands/backup.ts +6 -2
- package/src/commands/clean.ts +6 -2
- package/src/commands/client.ts +91 -41
- package/src/commands/confirm.ts +6 -2
- package/src/commands/connect.ts +6 -2
- package/src/commands/debug.ts +6 -2
- package/src/commands/devices.ts +6 -2
- package/src/commands/events.ts +6 -2
- package/src/commands/exec.ts +50 -12
- package/src/commands/flags.ts +6 -2
- package/src/commands/gateway.ts +6 -2
- package/src/commands/hatch.ts +6 -2
- package/src/commands/login.ts +26 -356
- package/src/commands/logs.ts +6 -2
- package/src/commands/message.ts +6 -2
- package/src/commands/pair.ts +6 -2
- package/src/commands/ps.ts +7 -2
- package/src/commands/recover.ts +21 -16
- package/src/commands/restore.ts +6 -2
- package/src/commands/retire.ts +6 -2
- package/src/commands/roadmap.ts +6 -2
- package/src/commands/rollback.ts +6 -2
- package/src/commands/setup.ts +76 -9
- package/src/commands/sleep.ts +131 -31
- package/src/commands/ssh.ts +6 -2
- package/src/commands/teleport.ts +6 -2
- package/src/commands/terminal.ts +6 -2
- package/src/commands/tunnel.ts +6 -2
- package/src/commands/uninstall.ts +228 -0
- package/src/commands/unpair.ts +6 -2
- package/src/commands/upgrade.ts +13 -8
- package/src/commands/use.ts +6 -2
- package/src/commands/voice.ts +6 -2
- package/src/commands/wake.ts +69 -37
- package/src/commands/workflows.ts +6 -2
- package/src/components/DefaultMainScreen.tsx +250 -36
- package/src/index.ts +23 -55
- package/src/lib/assistant-config.ts +27 -13
- package/src/lib/backup-ops.ts +3 -4
- package/src/lib/cli-error.ts +5 -0
- package/src/lib/client-identity.ts +8 -10
- package/src/lib/cloud-login.ts +422 -0
- package/src/lib/color.ts +16 -0
- package/src/lib/default-main-screen-formatting.ts +32 -8
- package/src/lib/environments/__tests__/paths.test.ts +9 -11
- package/src/lib/environments/paths.ts +2 -20
- package/src/lib/guardian-token.ts +72 -18
- package/src/lib/hatch-local.ts +52 -1
- package/src/lib/health-check.ts +24 -15
- package/src/lib/lifecycle-reporter.ts +9 -1
- package/src/lib/local-hatch-lock.ts +19 -27
- package/src/lib/local-runtime-client.ts +25 -14
- package/src/lib/local.ts +17 -43
- package/src/lib/ngrok.ts +48 -16
- package/src/lib/platform-client.ts +141 -72
- package/src/lib/retire-archive.ts +4 -4
- package/src/lib/retire-local.ts +2 -25
- package/src/lib/teleport-backup.ts +23 -9
- package/src/lib/terminal-client.ts +21 -9
- package/src/lib/tunnel-edge.ts +11 -13
- package/src/lib/upgrade-lifecycle.ts +16 -4
- package/src/lib/workos-pkce.ts +52 -21
package/src/commands/setup.ts
CHANGED
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
ensureProviderApiKey,
|
|
10
10
|
formatProviderName,
|
|
11
11
|
} from "../lib/provider-secrets.js";
|
|
12
|
+
import { yellow } from "../lib/color.js";
|
|
12
13
|
|
|
13
|
-
function parseSetupArgs(args: string[]): { provider
|
|
14
|
-
let provider
|
|
14
|
+
function parseSetupArgs(args: string[]): { provider?: string } {
|
|
15
|
+
let provider: string | undefined;
|
|
15
16
|
|
|
16
17
|
for (let i = 0; i < args.length; i++) {
|
|
17
18
|
const arg = args[i];
|
|
@@ -32,6 +33,58 @@ function parseSetupArgs(args: string[]): { provider: string } {
|
|
|
32
33
|
return { provider };
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
interface DefaultProviderResponse {
|
|
37
|
+
provider?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Parse the default-provider payload without a type assertion. */
|
|
41
|
+
function parseDefaultProviderResponse(value: unknown): DefaultProviderResponse {
|
|
42
|
+
if (typeof value !== "object" || value === null) return {};
|
|
43
|
+
const provider = "provider" in value ? value.provider : undefined;
|
|
44
|
+
return typeof provider === "string" ? { provider } : {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ask the active assistant which provider it routes by default
|
|
49
|
+
* (`GET /v1/config/llm/default-provider`), so `cadet setup` without
|
|
50
|
+
* `--provider` configures the connection the assistant actually uses —
|
|
51
|
+
* e.g. the keyless Cadet Cloud managed connection picked at hatch —
|
|
52
|
+
* instead of blindly prompting for an API key. Falls back to cadet
|
|
53
|
+
* when the runtime is unreachable or predates the endpoint; the
|
|
54
|
+
* returned warning says why, for the caller to render.
|
|
55
|
+
*/
|
|
56
|
+
async function resolveDefaultProvider(
|
|
57
|
+
gatewayUrl: string,
|
|
58
|
+
bearerToken?: string,
|
|
59
|
+
): Promise<{ provider: string; warning?: string }> {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(
|
|
62
|
+
`${gatewayUrl}/v1/config/llm/default-provider`,
|
|
63
|
+
{
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
67
|
+
},
|
|
68
|
+
signal: AbortSignal.timeout(5000),
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
return {
|
|
73
|
+
provider: "cadet",
|
|
74
|
+
warning: `Default provider lookup failed (gateway responded ${response.status}); falling back to Cadet Cloud.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const data = parseDefaultProviderResponse(await response.json());
|
|
78
|
+
const provider = data.provider?.trim().toLowerCase();
|
|
79
|
+
return { provider: provider || "cadet" };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
provider: "cadet",
|
|
83
|
+
warning: `Default provider lookup failed (${error instanceof Error ? error.message : String(error)}); falling back to Cadet Cloud.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
35
88
|
function isGuardianAccessTokenUsable(
|
|
36
89
|
tokenData: GuardianTokenData | null,
|
|
37
90
|
): tokenData is GuardianTokenData {
|
|
@@ -86,7 +139,8 @@ async function resolveSetupBearerToken(
|
|
|
86
139
|
return entry.bearerToken;
|
|
87
140
|
}
|
|
88
141
|
|
|
89
|
-
export
|
|
142
|
+
export const setup = Object.assign(
|
|
143
|
+
async function setup(): Promise<void> {
|
|
90
144
|
const args = process.argv.slice(3);
|
|
91
145
|
|
|
92
146
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -95,9 +149,7 @@ export async function setup(): Promise<void> {
|
|
|
95
149
|
console.log("Configure a provider API key on the active assistant.");
|
|
96
150
|
console.log("");
|
|
97
151
|
console.log("Options:");
|
|
98
|
-
console.log(
|
|
99
|
-
" --provider <provider> Provider to configure. Defaults to anthropic.",
|
|
100
|
-
);
|
|
152
|
+
console.log(" --provider <provider> Provider to configure. Defaults to Cadet Cloud");
|
|
101
153
|
console.log("");
|
|
102
154
|
console.log("Behavior:");
|
|
103
155
|
console.log(
|
|
@@ -113,7 +165,7 @@ export async function setup(): Promise<void> {
|
|
|
113
165
|
process.exit(0);
|
|
114
166
|
}
|
|
115
167
|
|
|
116
|
-
let parsed: { provider
|
|
168
|
+
let parsed: { provider?: string };
|
|
117
169
|
try {
|
|
118
170
|
parsed = parseSetupArgs(args);
|
|
119
171
|
} catch (error) {
|
|
@@ -130,13 +182,25 @@ export async function setup(): Promise<void> {
|
|
|
130
182
|
const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
|
|
131
183
|
const bearerToken = await resolveSetupBearerToken(entry, gatewayUrl);
|
|
132
184
|
|
|
185
|
+
// Respect the assistant's configured default provider (e.g. Cadet Cloud
|
|
186
|
+
// chosen at hatch) unless the caller named one explicitly — the runtime's
|
|
187
|
+
// keyless managed connection should not be greeted by an API-key prompt.
|
|
188
|
+
let provider = parsed.provider;
|
|
189
|
+
if (!provider) {
|
|
190
|
+
const resolved = await resolveDefaultProvider(gatewayUrl, bearerToken);
|
|
191
|
+
provider = resolved.provider;
|
|
192
|
+
if (resolved.warning) {
|
|
193
|
+
console.error(yellow(resolved.warning));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
133
197
|
console.log("Cadet Setup");
|
|
134
198
|
console.log("============\n");
|
|
135
199
|
|
|
136
200
|
try {
|
|
137
201
|
const result = await ensureProviderApiKey({
|
|
138
202
|
gatewayUrl,
|
|
139
|
-
provider
|
|
203
|
+
provider,
|
|
140
204
|
bearerToken,
|
|
141
205
|
env: process.env,
|
|
142
206
|
});
|
|
@@ -171,4 +235,7 @@ export async function setup(): Promise<void> {
|
|
|
171
235
|
);
|
|
172
236
|
process.exit(1);
|
|
173
237
|
}
|
|
174
|
-
}
|
|
238
|
+
},
|
|
239
|
+
{ description: "Configure API keys interactively" },
|
|
240
|
+
);
|
|
241
|
+
|
package/src/commands/sleep.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
getDaemonPidPath,
|
|
6
|
+
loadAllAssistants,
|
|
6
7
|
resolveTargetAssistant,
|
|
7
8
|
} from "../lib/assistant-config.js";
|
|
8
9
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
@@ -60,10 +61,11 @@ function readActiveCallLeases(cadetDir: string): ActiveCallLease[] {
|
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
export
|
|
64
|
+
export const sleep = Object.assign(
|
|
65
|
+
async function sleep(): Promise<void> {
|
|
64
66
|
const args = process.argv.slice(3);
|
|
65
67
|
if (args.includes("--help") || args.includes("-h")) {
|
|
66
|
-
console.log("Usage: cadet sleep [<name>] [--force] [--wait [<duration>]]");
|
|
68
|
+
console.log("Usage: cadet sleep [<name> | --all] [--force] [--wait [<duration>]]");
|
|
67
69
|
console.log("");
|
|
68
70
|
console.log("Stop the assistant and gateway processes.");
|
|
69
71
|
console.log("");
|
|
@@ -73,6 +75,9 @@ export async function sleep(): Promise<void> {
|
|
|
73
75
|
);
|
|
74
76
|
console.log("");
|
|
75
77
|
console.log("Options:");
|
|
78
|
+
console.log(
|
|
79
|
+
" --all Stop every assistant across all remotes instead of one",
|
|
80
|
+
);
|
|
76
81
|
console.log(
|
|
77
82
|
" --force Stop the assistant even if a phone call keepalive lease is active",
|
|
78
83
|
);
|
|
@@ -128,7 +133,110 @@ export async function sleep(): Promise<void> {
|
|
|
128
133
|
const nameArg = args.find(
|
|
129
134
|
(a) => !a.startsWith("-") && a !== waitDurationToken,
|
|
130
135
|
);
|
|
136
|
+
const all = args.includes("--all");
|
|
137
|
+
|
|
138
|
+
if (all && nameArg) {
|
|
139
|
+
console.error("Error: --all cannot be combined with an assistant name.");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (all) {
|
|
144
|
+
await sleepAll({ force, wait, waitMs });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
131
148
|
const entry = resolveTargetAssistant(nameArg);
|
|
149
|
+
try {
|
|
150
|
+
await sleepAssistant(entry, { force, wait, waitMs });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (err instanceof SleepExit) {
|
|
153
|
+
console.log(err.message);
|
|
154
|
+
process.exit(err.exitCode);
|
|
155
|
+
}
|
|
156
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{ description: "Stop the assistant process" },
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
type SleepOptions = {
|
|
165
|
+
force: boolean;
|
|
166
|
+
wait: boolean;
|
|
167
|
+
waitMs: number | null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Carries a deliberate exit outcome (a cancelled drain) across the
|
|
172
|
+
* per-assistant boundary so the callers can exit with the right code
|
|
173
|
+
* instead of treating it as a stop failure.
|
|
174
|
+
*/
|
|
175
|
+
class SleepExit extends Error {
|
|
176
|
+
constructor(
|
|
177
|
+
readonly exitCode: number,
|
|
178
|
+
message: string,
|
|
179
|
+
) {
|
|
180
|
+
super(message);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Stop every assistant this CLI's environment knows about — `--all`. The
|
|
186
|
+
* CLI only ever reaches one environment, so this is the current env's
|
|
187
|
+
* lockfile. Same per-assistant stop as the single path, but one
|
|
188
|
+
* assistant's failure (or an unmanageable runtime) never blocks the
|
|
189
|
+
* others; a failure only decides the exit code.
|
|
190
|
+
*/
|
|
191
|
+
async function sleepAll(opts: SleepOptions): Promise<void> {
|
|
192
|
+
const entries = loadAllAssistants();
|
|
193
|
+
if (entries.length === 0) {
|
|
194
|
+
console.error("Error: no assistants found. Run 'cadet hatch' first.");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let failures = 0;
|
|
199
|
+
const seen = new Set<string>();
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
if (seen.has(entry.assistantId)) continue;
|
|
202
|
+
seen.add(entry.assistantId);
|
|
203
|
+
if (entry.cloud === "apple-container" || entry.cloud === "paired") {
|
|
204
|
+
console.log(
|
|
205
|
+
`Skipping '${entry.assistantId}' (${entry.cloud}) — its lifecycle is managed elsewhere.`,
|
|
206
|
+
);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await sleepAssistant(entry, opts);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err instanceof SleepExit) {
|
|
213
|
+
// A cancelled drain is the user asking to stop — honor it immediately.
|
|
214
|
+
console.log(err.message);
|
|
215
|
+
process.exit(err.exitCode);
|
|
216
|
+
}
|
|
217
|
+
failures++;
|
|
218
|
+
console.error(
|
|
219
|
+
`Error: failed to stop '${entry.assistantId}': ${
|
|
220
|
+
err instanceof Error ? err.message : String(err)
|
|
221
|
+
}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (failures > 0) {
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Stop one assistant. Throws on hard errors (active call leases, missing
|
|
232
|
+
* resources, unmanageable runtimes) so the caller decides between exiting
|
|
233
|
+
* (single-assistant path) and continuing to the next entry (`--all`).
|
|
234
|
+
*/
|
|
235
|
+
async function sleepAssistant(
|
|
236
|
+
entry: AssistantEntry,
|
|
237
|
+
opts: SleepOptions,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const { force, wait, waitMs } = opts;
|
|
132
240
|
|
|
133
241
|
if (entry.cloud === "docker") {
|
|
134
242
|
if (wait) {
|
|
@@ -146,31 +254,27 @@ export async function sleep(): Promise<void> {
|
|
|
146
254
|
}
|
|
147
255
|
|
|
148
256
|
if (entry.cloud === "apple-container") {
|
|
149
|
-
|
|
150
|
-
`
|
|
257
|
+
throw new Error(
|
|
258
|
+
`'${entry.assistantId}' uses the Apple Containers runtime. Its lifecycle is managed by the macOS app — use the app to stop it.`,
|
|
151
259
|
);
|
|
152
|
-
process.exit(1);
|
|
153
260
|
}
|
|
154
261
|
|
|
155
262
|
if (entry.cloud === "paired") {
|
|
156
|
-
|
|
157
|
-
`
|
|
263
|
+
throw new Error(
|
|
264
|
+
`'${entry.assistantId}' is a remote assistant paired from another machine — its lifecycle is managed on its host machine, not here. Use \`cadet client ${entry.assistantId}\` to chat with it.`,
|
|
158
265
|
);
|
|
159
|
-
process.exit(1);
|
|
160
266
|
}
|
|
161
267
|
|
|
162
268
|
if (entry.cloud && entry.cloud !== "local") {
|
|
163
|
-
|
|
164
|
-
`
|
|
269
|
+
throw new Error(
|
|
270
|
+
`'cadet sleep' only works with local and docker assistants. '${entry.assistantId}' is a ${entry.cloud} instance.`,
|
|
165
271
|
);
|
|
166
|
-
process.exit(1);
|
|
167
272
|
}
|
|
168
273
|
|
|
169
274
|
if (!entry.resources) {
|
|
170
|
-
|
|
171
|
-
`
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Local assistant '${entry.assistantId}' is missing resource configuration. Re-hatch to fix.`,
|
|
172
277
|
);
|
|
173
|
-
process.exit(1);
|
|
174
278
|
}
|
|
175
279
|
const resources = entry.resources;
|
|
176
280
|
const assistantPidFile = getDaemonPidPath(resources);
|
|
@@ -180,26 +284,23 @@ export async function sleep(): Promise<void> {
|
|
|
180
284
|
if (!force) {
|
|
181
285
|
const assistantAlive = isProcessAlive(assistantPidFile).alive;
|
|
182
286
|
if (assistantAlive) {
|
|
287
|
+
let leases: ActiveCallLease[];
|
|
183
288
|
try {
|
|
184
|
-
|
|
185
|
-
if (activeCallLeases.length > 0) {
|
|
186
|
-
const activeIds = activeCallLeases.map(
|
|
187
|
-
(lease) => lease.callSessionId,
|
|
188
|
-
);
|
|
189
|
-
console.error(
|
|
190
|
-
`Error: assistant is staying awake for active phone calls (${activeIds.join(
|
|
191
|
-
", ",
|
|
192
|
-
)}). Use 'cadet sleep --force' to stop it anyway.`,
|
|
193
|
-
);
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
289
|
+
leases = readActiveCallLeases(cadetDir);
|
|
196
290
|
} catch (err) {
|
|
197
|
-
|
|
198
|
-
|
|
291
|
+
throw new Error(
|
|
292
|
+
`${
|
|
199
293
|
err instanceof Error ? err.message : String(err)
|
|
200
294
|
}. Use 'cadet sleep --force' to override if you want to stop the assistant anyway.`,
|
|
201
295
|
);
|
|
202
|
-
|
|
296
|
+
}
|
|
297
|
+
if (leases.length > 0) {
|
|
298
|
+
const activeIds = leases.map((lease) => lease.callSessionId);
|
|
299
|
+
throw new Error(
|
|
300
|
+
`assistant is staying awake for active phone calls (${activeIds.join(
|
|
301
|
+
", ",
|
|
302
|
+
)}). Use 'cadet sleep --force' to stop it anyway.`,
|
|
303
|
+
);
|
|
203
304
|
}
|
|
204
305
|
}
|
|
205
306
|
}
|
|
@@ -207,8 +308,7 @@ export async function sleep(): Promise<void> {
|
|
|
207
308
|
if (wait) {
|
|
208
309
|
const outcome = await runDrainPhase(entry, assistantPidFile, waitMs);
|
|
209
310
|
if (outcome === "cancelled") {
|
|
210
|
-
|
|
211
|
-
process.exit(130);
|
|
311
|
+
throw new SleepExit(130, "Sleep cancelled — the assistant is still running.");
|
|
212
312
|
}
|
|
213
313
|
}
|
|
214
314
|
|
package/src/commands/ssh.ts
CHANGED
|
@@ -18,7 +18,8 @@ const SSH_OPTS = [
|
|
|
18
18
|
"LogLevel=ERROR",
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
export
|
|
21
|
+
export const ssh = Object.assign(
|
|
22
|
+
async function ssh(): Promise<void> {
|
|
22
23
|
const args = process.argv.slice(3);
|
|
23
24
|
if (args.includes("--help") || args.includes("-h")) {
|
|
24
25
|
console.log("Usage: cadet ssh [<name>]");
|
|
@@ -137,4 +138,7 @@ export async function ssh(): Promise<void> {
|
|
|
137
138
|
});
|
|
138
139
|
child.on("error", reject);
|
|
139
140
|
});
|
|
140
|
-
}
|
|
141
|
+
},
|
|
142
|
+
{ description: "SSH into a remote assistant instance" },
|
|
143
|
+
);
|
|
144
|
+
|
package/src/commands/teleport.ts
CHANGED
|
@@ -1274,7 +1274,8 @@ async function tryInjectPlatformCredentials(
|
|
|
1274
1274
|
// Main entry point
|
|
1275
1275
|
// ---------------------------------------------------------------------------
|
|
1276
1276
|
|
|
1277
|
-
export
|
|
1277
|
+
export const teleport = Object.assign(
|
|
1278
|
+
async function teleport(): Promise<void> {
|
|
1278
1279
|
const args = process.argv.slice(3);
|
|
1279
1280
|
const { from, to, targetEnv, targetName, keepSource, dryRun, help } =
|
|
1280
1281
|
parseArgs(args);
|
|
@@ -1664,4 +1665,7 @@ export async function teleport(): Promise<void> {
|
|
|
1664
1665
|
}
|
|
1665
1666
|
|
|
1666
1667
|
console.log(`Teleport complete: ${from} → ${toEntry.assistantId}`);
|
|
1667
|
-
}
|
|
1668
|
+
},
|
|
1669
|
+
{ description: "Transfer assistant data between environments" },
|
|
1670
|
+
);
|
|
1671
|
+
|
package/src/commands/terminal.ts
CHANGED
|
@@ -159,7 +159,8 @@ async function listTmuxSessions(
|
|
|
159
159
|
// Main entry point
|
|
160
160
|
// ---------------------------------------------------------------------------
|
|
161
161
|
|
|
162
|
-
export
|
|
162
|
+
export const terminal = Object.assign(
|
|
163
|
+
async function terminal(): Promise<void> {
|
|
163
164
|
const args = process.argv.slice(3);
|
|
164
165
|
|
|
165
166
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -222,4 +223,7 @@ export async function terminal(): Promise<void> {
|
|
|
222
223
|
|
|
223
224
|
// Default: interactive shell
|
|
224
225
|
await interactiveSession(assistant);
|
|
225
|
-
}
|
|
226
|
+
},
|
|
227
|
+
{ description: "Open a terminal into a managed assistant container" },
|
|
228
|
+
);
|
|
229
|
+
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -440,7 +440,8 @@ async function spawnDetachedTunnel(): Promise<void> {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
export
|
|
443
|
+
export const tunnel = Object.assign(
|
|
444
|
+
async function tunnel(): Promise<void> {
|
|
444
445
|
const { assistantName, provider, domain, clearDomain, detach } = parseArgs();
|
|
445
446
|
|
|
446
447
|
if (detach) {
|
|
@@ -506,4 +507,7 @@ export async function tunnel(): Promise<void> {
|
|
|
506
507
|
...baseTunnelOpts,
|
|
507
508
|
...(preserveIngressUrl ? { preserveIngressUrl } : {}),
|
|
508
509
|
});
|
|
509
|
-
}
|
|
510
|
+
},
|
|
511
|
+
{ description: "Create a tunnel for a locally hosted assistant" },
|
|
512
|
+
);
|
|
513
|
+
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, rmSync, unlinkSync } from "fs";
|
|
2
|
+
import { basename, dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
import { loadAllAssistants } from "../lib/assistant-config.js";
|
|
6
|
+
import {
|
|
7
|
+
canPromptForConfirmation,
|
|
8
|
+
confirmAction,
|
|
9
|
+
} from "../lib/confirm-action.js";
|
|
10
|
+
import {
|
|
11
|
+
getConfigDirs,
|
|
12
|
+
getInputHistoryPath,
|
|
13
|
+
getLogDir,
|
|
14
|
+
getStateDir,
|
|
15
|
+
} from "../lib/environments/paths.js";
|
|
16
|
+
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
17
|
+
|
|
18
|
+
/** A directory (or file) scheduled for deletion, with what it holds. */
|
|
19
|
+
interface DeleteTarget {
|
|
20
|
+
path: string;
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Owning packages a global install of the CLI can live under, in priority
|
|
26
|
+
* order — @mycadet/cli always wins over its meta wrapper.
|
|
27
|
+
*/
|
|
28
|
+
const OWNED_PACKAGE_NAMES: ReadonlyArray<string> = [
|
|
29
|
+
"@mycadet/cli",
|
|
30
|
+
"@mycadet/meta",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find the package root the CLI is running from (…/node_modules/mycadet),
|
|
35
|
+
* or null when running from a source tree. A `.git` ANYWHERE up the tree
|
|
36
|
+
* (repo root or monorepo parent — a checkout's cli/ subpackage has no .git
|
|
37
|
+
* of its own) means this is source, never an install.
|
|
38
|
+
*/
|
|
39
|
+
function findInstalledPackageRoot(startDir: string): string | null {
|
|
40
|
+
// Pass 1: any .git ancestor = a checkout, never an install.
|
|
41
|
+
for (let dir = startDir; ; dir = dirname(dir)) {
|
|
42
|
+
if (existsSync(join(dir, ".git"))) return null;
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir) break; // Reached the filesystem root.
|
|
45
|
+
}
|
|
46
|
+
// Pass 2: walk up looking for the owned package root, priority order —
|
|
47
|
+
// the whole chain is checked for each name in turn, so @mycadet/cli wins
|
|
48
|
+
// over @mycadet/meta wherever both appear.
|
|
49
|
+
for (const owned of OWNED_PACKAGE_NAMES) {
|
|
50
|
+
for (let dir = startDir; ; dir = dirname(dir)) {
|
|
51
|
+
const pkgPath = join(dir, "package.json");
|
|
52
|
+
if (existsSync(pkgPath)) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed: unknown = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
55
|
+
if (
|
|
56
|
+
typeof parsed === "object" &&
|
|
57
|
+
parsed !== null &&
|
|
58
|
+
"name" in parsed &&
|
|
59
|
+
parsed.name === owned
|
|
60
|
+
) {
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Unreadable package.json — keep walking up.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const parent = dirname(dir);
|
|
68
|
+
if (parent === dir) break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Remove the running CLI. Handles the two real install shapes:
|
|
76
|
+
* - the compiled `cadet` binary (process.execPath IS the install)
|
|
77
|
+
* - a global package install (bun install -g mycadet / npm install -g)
|
|
78
|
+
* Under a source checkout or a bare runtime (bun/node), nothing is deleted —
|
|
79
|
+
* the runtime binary must never be a deletion target.
|
|
80
|
+
*/
|
|
81
|
+
function selfDelete(): void {
|
|
82
|
+
const execPath = process.execPath;
|
|
83
|
+
const execBase = basename(execPath);
|
|
84
|
+
if (execBase === "cadet" || execBase === "cadet.exe") {
|
|
85
|
+
try {
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
// Windows refuses to unlink a running executable — stage it under a
|
|
88
|
+
// junk name and delete that.
|
|
89
|
+
const stale = `${execPath}.uninstalled`;
|
|
90
|
+
renameSync(execPath, stale);
|
|
91
|
+
rmSync(stale, { force: true, recursive: true });
|
|
92
|
+
} else {
|
|
93
|
+
unlinkSync(execPath);
|
|
94
|
+
}
|
|
95
|
+
console.log(`Removed ${execPath}.`);
|
|
96
|
+
return;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn(
|
|
99
|
+
`⚠️ Could not remove ${execPath}: ${
|
|
100
|
+
err instanceof Error ? err.message : String(err)
|
|
101
|
+
} — remove it by hand.`,
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Package-manager install: remove the package directory we are running
|
|
108
|
+
// from, best-effort. Bin symlinks the package manager planted (e.g.
|
|
109
|
+
// ~/.bun/bin/cadet, the npm global bin) are left dangling — say so.
|
|
110
|
+
const runningFile = fileURLToPath(import.meta.url);
|
|
111
|
+
const pkgRoot = findInstalledPackageRoot(dirname(runningFile));
|
|
112
|
+
if (!pkgRoot) {
|
|
113
|
+
console.log(
|
|
114
|
+
"Cadet is running from a source checkout — skipping self-removal.",
|
|
115
|
+
);
|
|
116
|
+
console.log(
|
|
117
|
+
"Remove the installed copy with `bun remove -g mycadet` (or `npm uninstall -g mycadet`).",
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
rmSync(pkgRoot, { recursive: true, force: true });
|
|
123
|
+
console.log(`Removed ${pkgRoot}.`);
|
|
124
|
+
console.log(
|
|
125
|
+
"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`).",
|
|
126
|
+
);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.warn(
|
|
129
|
+
`⚠️ Could not remove ${pkgRoot}: ${
|
|
130
|
+
err instanceof Error ? err.message : String(err)
|
|
131
|
+
} — remove it with \`bun remove -g mycadet\` (or \`npm uninstall -g mycadet\`).`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectDeleteTargets(env = getCurrentEnvironment()): DeleteTarget[] {
|
|
137
|
+
const targets: DeleteTarget[] = [
|
|
138
|
+
{
|
|
139
|
+
path: getStateDir(env),
|
|
140
|
+
description: "assistant state, retired archives, and the assistants it holds",
|
|
141
|
+
},
|
|
142
|
+
...getConfigDirs(env).map((dir) => ({
|
|
143
|
+
path: dir,
|
|
144
|
+
description: "config and lockfile",
|
|
145
|
+
})),
|
|
146
|
+
{ path: getLogDir(env), description: "logs" },
|
|
147
|
+
// Not environment-scoped: deleting it affects every cadet CLI on the
|
|
148
|
+
// machine — called out in the prompt, not hidden.
|
|
149
|
+
{ path: getInputHistoryPath(), description: "shared terminal input history" },
|
|
150
|
+
];
|
|
151
|
+
// Only targets that exist — the prompt lists what will actually go.
|
|
152
|
+
return targets.filter((t) => existsSync(t.path));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rmrf(target: string): void {
|
|
156
|
+
rmSync(target, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const uninstall = Object.assign(
|
|
160
|
+
async function uninstall(): Promise<void> {
|
|
161
|
+
const args = process.argv.slice(3);
|
|
162
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
163
|
+
console.log("Usage: cadet uninstall [--yes]");
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(
|
|
166
|
+
"Remove Cadet entirely: delete every location it uses for this environment (assistant state, retired archives, config, logs) and then delete the executable itself.",
|
|
167
|
+
);
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
const yes = args.includes("--yes");
|
|
171
|
+
|
|
172
|
+
// Retire tears down an assistant AND archives its data. Uninstall assumes
|
|
173
|
+
// an empty rack: retiring first is the way to keep an archive.
|
|
174
|
+
if (loadAllAssistants().length > 0) {
|
|
175
|
+
console.error(
|
|
176
|
+
"Error: You still have assistants. Retire them first (`cadet retire <name>`) — retiring archives their data before removing it.",
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const env = getCurrentEnvironment();
|
|
182
|
+
const envLabel = env.name ?? "production";
|
|
183
|
+
const targets = collectDeleteTargets(env);
|
|
184
|
+
const execPath = process.execPath;
|
|
185
|
+
|
|
186
|
+
console.log(
|
|
187
|
+
`This removes Cadet entirely for the '${envLabel}' environment and cannot be undone.`,
|
|
188
|
+
);
|
|
189
|
+
if (targets.length === 0) {
|
|
190
|
+
console.log("No Cadet data directories found to delete.");
|
|
191
|
+
} else {
|
|
192
|
+
console.log("It will permanently delete:");
|
|
193
|
+
for (const t of targets) {
|
|
194
|
+
console.log(` - ${t.path} (${t.description})`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const execBase = basename(execPath);
|
|
198
|
+
const isCompiledBinary = execBase === "cadet" || execBase === "cadet.exe";
|
|
199
|
+
if (isCompiledBinary) {
|
|
200
|
+
console.log(`It will then remove the Cadet executable at ${execPath}.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!yes) {
|
|
204
|
+
if (!canPromptForConfirmation()) {
|
|
205
|
+
console.error(
|
|
206
|
+
"Error: Refusing to uninstall without confirmation in a non-interactive terminal.",
|
|
207
|
+
);
|
|
208
|
+
console.error("Re-run with --yes to confirm from automation.");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const confirmed = await confirmAction(
|
|
212
|
+
"Press Enter to uninstall Cadet, or Esc/q to cancel: ",
|
|
213
|
+
);
|
|
214
|
+
if (!confirmed) {
|
|
215
|
+
console.log("Uninstall cancelled.");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const t of targets) {
|
|
221
|
+
rmrf(t.path);
|
|
222
|
+
console.log(`Deleted ${t.path}.`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
selfDelete();
|
|
226
|
+
},
|
|
227
|
+
{ description: "Remove Cadet entirely" },
|
|
228
|
+
);
|
package/src/commands/unpair.ts
CHANGED
|
@@ -46,7 +46,8 @@ EXAMPLES:
|
|
|
46
46
|
`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export
|
|
49
|
+
export const unpair = Object.assign(
|
|
50
|
+
async function unpair(): Promise<void> {
|
|
50
51
|
const args = process.argv.slice(3);
|
|
51
52
|
|
|
52
53
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -115,4 +116,7 @@ export async function unpair(): Promise<void> {
|
|
|
115
116
|
console.log(
|
|
116
117
|
"Note: this only forgets the connection on this machine. The assistant's host can fully revoke this device from its side.",
|
|
117
118
|
);
|
|
118
|
-
}
|
|
119
|
+
},
|
|
120
|
+
{ description: "Forget a paired assistant imported from another machine [beta]" },
|
|
121
|
+
);
|
|
122
|
+
|