@mycadet/cli 0.12.7 → 0.12.9
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/README.md +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +3 -2
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +2 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/ensure-binary.test.ts +182 -0
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +21 -20
- package/src/__tests__/ngrok-spawn.test.ts +49 -0
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +1 -8
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
- package/src/__tests__/ps-table.test.ts +79 -0
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +181 -197
- package/src/__tests__/tunnel.test.ts +257 -222
- package/src/__tests__/uninstall.test.ts +13 -4
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +260 -43
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +24 -24
- package/src/commands/client.ts +76 -76
- package/src/commands/completions.test.ts +104 -0
- package/src/commands/completions.ts +87 -0
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +12 -12
- package/src/commands/flags.ts +2 -2
- package/src/commands/hatch.ts +23 -23
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/ps.ts +244 -86
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +140 -22
- package/src/commands/retire.ts +48 -150
- package/src/commands/rollback.ts +20 -26
- package/src/commands/setup.ts +2 -2
- package/src/commands/sleep.ts +2 -2
- package/src/commands/teleport.ts +109 -109
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +138 -157
- package/src/commands/uninstall.ts +57 -9
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +30 -54
- package/src/commands/use.ts +1 -1
- package/src/commands/wake.ts +166 -50
- package/src/index.ts +18 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/docker.test.ts +168 -20
- package/src/lib/__tests__/job-polling.test.ts +2 -2
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
- package/src/lib/cloud-login.ts +24 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +260 -53
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +2 -2
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/resolve.ts +8 -8
- package/src/lib/flag-args.test.ts +1 -1
- package/src/lib/flag-args.ts +1 -1
- package/src/lib/guardian-token.ts +37 -5
- package/src/lib/hatch-local.ts +19 -23
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +2 -2
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +10 -31
- package/src/lib/nginx-ingress.ts +42 -15
- package/src/lib/pair.ts +1 -1
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/sync-cloud-assistants.ts +19 -19
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
- package/src/lib/upgrade-lifecycle.ts +8 -21
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- package/src/lib/managed-cloud.ts +0 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import type { Readable, Writable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Consolidated missing-dependency flow for interactive commands: probe for a
|
|
6
|
+
* binary, and when it is absent, offer to install it — naming no commands —
|
|
7
|
+
* then run the right package manager for the platform and verify. Callers
|
|
8
|
+
* supply the recipes; everything else (prompt, package-manager discovery,
|
|
9
|
+
* fallback copy) lives here so every command behaves the same.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface EnsureBinaryOptions {
|
|
13
|
+
/** The binary as invoked on PATH, e.g. "nginx". */
|
|
14
|
+
binary: string;
|
|
15
|
+
/** What the running command needs it for, e.g. "the tunnel edge". */
|
|
16
|
+
purpose: string;
|
|
17
|
+
/**
|
|
18
|
+
* Package-manager name → full install argv (sudo included where needed),
|
|
19
|
+
* e.g. `{ brew: ["brew", "install", "nginx"] }`. The first entry whose
|
|
20
|
+
* package manager is available wins. Leave out a platform rather than
|
|
21
|
+
* guessing at an unreliable package ID — the manual hint covers it.
|
|
22
|
+
*/
|
|
23
|
+
packages: Record<string, string[]>;
|
|
24
|
+
/** Shown when the user declines or auto-install fails. */
|
|
25
|
+
manualHint: string;
|
|
26
|
+
/** Probe: a version-ish string when the binary is usable, null otherwise. */
|
|
27
|
+
probe: () => string | null;
|
|
28
|
+
/** Check that a package manager exists. Default: `<pm> --version` probe. */
|
|
29
|
+
pmAvailable?: (pm: string) => boolean;
|
|
30
|
+
/** Run an install argv; true on success. Default: inherit-stdio spawnSync. */
|
|
31
|
+
runInstall?: (argv: string[]) => boolean;
|
|
32
|
+
/** Confirm the install. Default: promptLine "[Y/n]" on Enter-as-yes. */
|
|
33
|
+
confirmInstall?: () => Promise<boolean>;
|
|
34
|
+
streams?: {
|
|
35
|
+
input?: NodeJS.ReadStream | Readable;
|
|
36
|
+
output?: NodeJS.WriteStream | Writable;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Prompt-then-install a missing binary. Returns the probe result — the
|
|
42
|
+
* existing version when the binary was already present, the fresh one after a
|
|
43
|
+
* successful install — or null when the binary is missing and the user
|
|
44
|
+
* declines, no package manager matches, or the install fails. Non-interactive
|
|
45
|
+
* callers (no TTY) never hang: they get the manual hint and null.
|
|
46
|
+
*/
|
|
47
|
+
export async function ensureBinaryInstalled(
|
|
48
|
+
opts: EnsureBinaryOptions,
|
|
49
|
+
): Promise<string | null> {
|
|
50
|
+
const output = opts.streams?.output ?? process.stdout;
|
|
51
|
+
|
|
52
|
+
const existing = opts.probe();
|
|
53
|
+
if (existing) return existing;
|
|
54
|
+
|
|
55
|
+
output.write(
|
|
56
|
+
`\n${opts.binary} is required for ${opts.purpose}, but it isn't installed.\n`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const confirm = opts.confirmInstall ?? defaultConfirm(opts);
|
|
60
|
+
let install = false;
|
|
61
|
+
try {
|
|
62
|
+
install = await confirm();
|
|
63
|
+
} catch {
|
|
64
|
+
install = false;
|
|
65
|
+
}
|
|
66
|
+
if (!install) {
|
|
67
|
+
output.write(`Skipping install. ${opts.manualHint}\n`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pmAvailable = opts.pmAvailable ?? probePackageManager;
|
|
72
|
+
const argv = Object.entries(opts.packages).find(([pm]) =>
|
|
73
|
+
pmAvailable(pm),
|
|
74
|
+
)?.[1];
|
|
75
|
+
if (!argv) {
|
|
76
|
+
output.write(`⚠ No supported package manager found. ${opts.manualHint}\n`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ok = opts.runInstall ? opts.runInstall(argv) : runInstallArgv(argv);
|
|
81
|
+
const version = ok ? opts.probe() : null;
|
|
82
|
+
if (!version) {
|
|
83
|
+
output.write(
|
|
84
|
+
`⚠ Couldn't install ${opts.binary} automatically. ${opts.manualHint}\n`,
|
|
85
|
+
);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
output.write(`✅ Installed ${opts.binary} (${version}).\n`);
|
|
89
|
+
return version;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Default confirm: visible prompt, Enter = yes, non-TTY throws. */
|
|
93
|
+
function defaultConfirm(opts: EnsureBinaryOptions): () => Promise<boolean> {
|
|
94
|
+
return async () => {
|
|
95
|
+
const input = opts.streams?.input ?? process.stdin;
|
|
96
|
+
const output = opts.streams?.output ?? process.stdout;
|
|
97
|
+
// Readable has no isTTY; only a real terminal answers the prompt.
|
|
98
|
+
if (!("isTTY" in input) || !input.isTTY) {
|
|
99
|
+
throw new Error("Install confirm requires an interactive terminal.");
|
|
100
|
+
}
|
|
101
|
+
const answer = (
|
|
102
|
+
await readLineText(`Install ${opts.binary} now? [Y/n] `, {
|
|
103
|
+
input,
|
|
104
|
+
output,
|
|
105
|
+
})
|
|
106
|
+
)
|
|
107
|
+
.trim()
|
|
108
|
+
.toLowerCase();
|
|
109
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Default installer: blocking spawn with inherited stdio (passwords flow). */
|
|
114
|
+
function runInstallArgv(argv: string[]): boolean {
|
|
115
|
+
const [command, ...args] = argv;
|
|
116
|
+
if (!command) return false;
|
|
117
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
118
|
+
return !result.error && result.status === 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Default package-manager check: the manager answers `--version`. */
|
|
122
|
+
function probePackageManager(pm: string): boolean {
|
|
123
|
+
const result = spawnSync(pm, ["--version"], {
|
|
124
|
+
stdio: "ignore",
|
|
125
|
+
shell: process.platform === "win32",
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
});
|
|
128
|
+
return !result.error && result.status === 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read one visible line from stdin (Enter submits). Shared by the binary
|
|
133
|
+
* install confirm and the ngrok authtoken paste-in, so both degrade the same
|
|
134
|
+
* way on a non-interactive terminal (the caller decides what empty means).
|
|
135
|
+
*/
|
|
136
|
+
export async function readLineText(
|
|
137
|
+
prompt: string,
|
|
138
|
+
streams: { input?: Readable; output?: Writable } = {},
|
|
139
|
+
): Promise<string> {
|
|
140
|
+
const input = streams.input ?? process.stdin;
|
|
141
|
+
const output = streams.output ?? process.stdout;
|
|
142
|
+
|
|
143
|
+
output.write(prompt);
|
|
144
|
+
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
let line = "";
|
|
147
|
+
const onData = (chunk: Buffer | string): void => {
|
|
148
|
+
line += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
149
|
+
const newline = line.indexOf("\n");
|
|
150
|
+
if (newline !== -1) {
|
|
151
|
+
done(line.slice(0, newline).replace(/\r$/, ""));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const cleanup = (): void => {
|
|
155
|
+
input.removeListener("data", onData);
|
|
156
|
+
input.removeListener("error", onError);
|
|
157
|
+
input.pause();
|
|
158
|
+
};
|
|
159
|
+
const done = (value: string): void => {
|
|
160
|
+
cleanup();
|
|
161
|
+
resolve(value);
|
|
162
|
+
};
|
|
163
|
+
const onError = (err: Error): void => {
|
|
164
|
+
cleanup();
|
|
165
|
+
reject(err);
|
|
166
|
+
};
|
|
167
|
+
input.resume();
|
|
168
|
+
input.on("data", onData);
|
|
169
|
+
input.on("error", onError);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -48,13 +48,13 @@ type EnvironmentDefinition =
|
|
|
48
48
|
|
|
49
49
|
const prod: EnvironmentDefinition = {
|
|
50
50
|
name: "production",
|
|
51
|
-
|
|
51
|
+
cloudUrl: "https://cloud.mycadet.ai",
|
|
52
52
|
webUrl: "https://mycadet.ai",
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
const dev: EnvironmentDefinition = {
|
|
56
56
|
name: "dev",
|
|
57
|
-
|
|
57
|
+
cloudUrl: "https://dev-cloud.mycadet.ai",
|
|
58
58
|
webUrl: "https://dev.mycadet.ai",
|
|
59
59
|
};
|
|
60
60
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
const ENV_VARS_TO_SAVE = [
|
|
10
10
|
"CADET_ENVIRONMENT",
|
|
11
|
-
"
|
|
11
|
+
"CADET_CLOUD_URL",
|
|
12
12
|
"CADET_ASSISTANT_PLATFORM_URL",
|
|
13
13
|
"CADET_LOCKFILE_DIR",
|
|
14
14
|
] as const;
|
|
@@ -37,33 +37,33 @@ describe("getCurrentEnvironment", () => {
|
|
|
37
37
|
test("returns production seed when no override, no env var", () => {
|
|
38
38
|
const env = getCurrentEnvironment();
|
|
39
39
|
expect(env.name).toBe("production");
|
|
40
|
-
expect(env.
|
|
40
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
test("returns dev seed when CADET_ENVIRONMENT=dev", () => {
|
|
44
44
|
process.env.CADET_ENVIRONMENT = "dev";
|
|
45
45
|
const env = getCurrentEnvironment();
|
|
46
46
|
expect(env.name).toBe("dev");
|
|
47
|
-
expect(env.
|
|
47
|
+
expect(env.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test("returns staging seed when CADET_ENVIRONMENT=staging", () => {
|
|
51
51
|
process.env.CADET_ENVIRONMENT = "staging";
|
|
52
|
-
expect(getCurrentEnvironment().
|
|
52
|
+
expect(getCurrentEnvironment().cloudUrl).toBe(
|
|
53
53
|
"https://staging-cloud.mycadet.ai",
|
|
54
54
|
);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test("returns local seed with localhost URL", () => {
|
|
58
58
|
process.env.CADET_ENVIRONMENT = "local";
|
|
59
|
-
expect(getCurrentEnvironment().
|
|
59
|
+
expect(getCurrentEnvironment().cloudUrl).toBe("http://localhost:8000");
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
test("override argument takes priority over CADET_ENVIRONMENT env var", () => {
|
|
63
63
|
process.env.CADET_ENVIRONMENT = "staging";
|
|
64
64
|
const env = getCurrentEnvironment("dev");
|
|
65
65
|
expect(env.name).toBe("dev");
|
|
66
|
-
expect(env.
|
|
66
|
+
expect(env.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
test("empty override argument falls through to env var", () => {
|
|
@@ -85,7 +85,7 @@ describe("getCurrentEnvironment", () => {
|
|
|
85
85
|
try {
|
|
86
86
|
const env = getCurrentEnvironment("no-such-env");
|
|
87
87
|
expect(env.name).toBe("production");
|
|
88
|
-
expect(env.
|
|
88
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
89
89
|
expect(stderr).toHaveBeenCalledWith(
|
|
90
90
|
expect.stringContaining('unknown environment "no-such-env"'),
|
|
91
91
|
);
|
|
@@ -105,7 +105,7 @@ describe("getCurrentEnvironment", () => {
|
|
|
105
105
|
try {
|
|
106
106
|
const env = getCurrentEnvironment();
|
|
107
107
|
expect(env.name).toBe("production");
|
|
108
|
-
expect(env.
|
|
108
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
109
109
|
expect(stderr).toHaveBeenCalledWith(
|
|
110
110
|
expect.stringContaining('unknown environment "nope"'),
|
|
111
111
|
);
|
|
@@ -128,49 +128,49 @@ describe("getCurrentEnvironment", () => {
|
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
test("
|
|
131
|
+
test("CADET_CLOUD_URL overrides cloudUrl on the resolved definition", () => {
|
|
132
132
|
process.env.CADET_ENVIRONMENT = "dev";
|
|
133
|
-
process.env.
|
|
133
|
+
process.env.CADET_CLOUD_URL = "https://custom.example.com";
|
|
134
134
|
const env = getCurrentEnvironment();
|
|
135
135
|
expect(env.name).toBe("dev");
|
|
136
|
-
expect(env.
|
|
136
|
+
expect(env.cloudUrl).toBe("https://custom.example.com");
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
test("
|
|
140
|
-
process.env.
|
|
139
|
+
test("CADET_CLOUD_URL override does not affect the seed table", () => {
|
|
140
|
+
process.env.CADET_CLOUD_URL = "https://custom.example.com";
|
|
141
141
|
getCurrentEnvironment();
|
|
142
|
-
delete process.env.
|
|
142
|
+
delete process.env.CADET_CLOUD_URL;
|
|
143
143
|
const env = getCurrentEnvironment();
|
|
144
|
-
expect(env.
|
|
144
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
test("CADET_ASSISTANT_PLATFORM_URL overrides
|
|
147
|
+
test("CADET_ASSISTANT_PLATFORM_URL overrides assistantCloudUrl", () => {
|
|
148
148
|
process.env.CADET_ASSISTANT_PLATFORM_URL =
|
|
149
149
|
"http://host.docker.internal:8000";
|
|
150
150
|
const env = getCurrentEnvironment();
|
|
151
|
-
expect(env.
|
|
151
|
+
expect(env.assistantCloudUrl).toBe("http://host.docker.internal:8000");
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test("CADET_ASSISTANT_PLATFORM_URL does not shadow
|
|
154
|
+
test("CADET_ASSISTANT_PLATFORM_URL does not shadow cloudUrl", () => {
|
|
155
155
|
process.env.CADET_ASSISTANT_PLATFORM_URL = "http://override";
|
|
156
156
|
const env = getCurrentEnvironment();
|
|
157
|
-
expect(env.
|
|
157
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test("does not auto-materialize a new environment from
|
|
160
|
+
test("does not auto-materialize a new environment from CADET_CLOUD_URL alone", () => {
|
|
161
161
|
// Unknown env names fall back to production (parity with daemon + Swift).
|
|
162
|
-
// The per-field
|
|
162
|
+
// The per-field CADET_CLOUD_URL override is intentionally dropped on
|
|
163
163
|
// the fallback path — fallback returns a pristine production seed so a
|
|
164
164
|
// typo'd env var can't accidentally stitch together a new environment.
|
|
165
165
|
process.env.CADET_ENVIRONMENT = "my-custom";
|
|
166
|
-
process.env.
|
|
166
|
+
process.env.CADET_CLOUD_URL = "https://my-custom.example.com";
|
|
167
167
|
const stderr = spyOn(process.stderr, "write").mockImplementation(
|
|
168
168
|
() => true,
|
|
169
169
|
);
|
|
170
170
|
try {
|
|
171
171
|
const env = getCurrentEnvironment();
|
|
172
172
|
expect(env.name).toBe("production");
|
|
173
|
-
expect(env.
|
|
173
|
+
expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
|
|
174
174
|
expect(stderr).toHaveBeenCalledWith(
|
|
175
175
|
expect.stringContaining('unknown environment "my-custom"'),
|
|
176
176
|
);
|
|
@@ -232,7 +232,7 @@ describe("getSeed", () => {
|
|
|
232
232
|
const seed = getSeed("dev");
|
|
233
233
|
expect(seed).toBeDefined();
|
|
234
234
|
expect(seed?.name).toBe("dev");
|
|
235
|
-
expect(seed?.
|
|
235
|
+
expect(seed?.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
test("returns undefined for an unknown name", () => {
|
|
@@ -242,10 +242,10 @@ describe("getSeed", () => {
|
|
|
242
242
|
test("returned seed is a copy — mutations do not affect the table", () => {
|
|
243
243
|
const seed = getSeed("dev");
|
|
244
244
|
if (seed) {
|
|
245
|
-
seed.
|
|
245
|
+
seed.cloudUrl = "mutated";
|
|
246
246
|
}
|
|
247
247
|
const second = getSeed("dev");
|
|
248
|
-
expect(second?.
|
|
248
|
+
expect(second?.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
test("all five canonical seeds exist", () => {
|
|
@@ -43,9 +43,9 @@ export function environmentFromVersion(version: string): string {
|
|
|
43
43
|
*
|
|
44
44
|
* Per-field env-var overrides are honored on the resolved definition as
|
|
45
45
|
* ad-hoc escape hatches (they do not materialize new environments):
|
|
46
|
-
* - `
|
|
46
|
+
* - `CADET_CLOUD_URL` overrides `cloudUrl`
|
|
47
47
|
* - `CADET_WEB_URL` overrides `webUrl`
|
|
48
|
-
* - `CADET_ASSISTANT_PLATFORM_URL` overrides `
|
|
48
|
+
* - `CADET_ASSISTANT_PLATFORM_URL` overrides `assistantCloudUrl`
|
|
49
49
|
* - `CADET_LOCKFILE_DIR` overrides `lockfileDirOverride` (legacy e2e
|
|
50
50
|
* test hook)
|
|
51
51
|
*
|
|
@@ -87,9 +87,9 @@ export function getCurrentEnvironment(
|
|
|
87
87
|
|
|
88
88
|
const resolved: EnvironmentDefinition = { ...seed };
|
|
89
89
|
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
resolved.
|
|
90
|
+
const cloudUrlOverride = process.env.CADET_CLOUD_URL?.trim();
|
|
91
|
+
if (cloudUrlOverride) {
|
|
92
|
+
resolved.cloudUrl = cloudUrlOverride;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const webUrlOverride = process.env.CADET_WEB_URL?.trim();
|
|
@@ -97,10 +97,10 @@ export function getCurrentEnvironment(
|
|
|
97
97
|
resolved.webUrl = webUrlOverride;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const
|
|
100
|
+
const assistantCloudUrlOverride =
|
|
101
101
|
process.env.CADET_ASSISTANT_PLATFORM_URL?.trim();
|
|
102
|
-
if (
|
|
103
|
-
resolved.
|
|
102
|
+
if (assistantCloudUrlOverride) {
|
|
103
|
+
resolved.assistantCloudUrl = assistantCloudUrlOverride;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const lockfileDirOverride = process.env.CADET_LOCKFILE_DIR?.trim();
|
|
@@ -75,7 +75,7 @@ describe("parseFeatureFlagArgs", () => {
|
|
|
75
75
|
"process.exit",
|
|
76
76
|
);
|
|
77
77
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
78
|
-
'Error: invalid flag key "UPPER". Keys must be kebab-case
|
|
78
|
+
'Error: invalid flag key "UPPER". Keys must be kebab-case, e.g. "browser"',
|
|
79
79
|
);
|
|
80
80
|
|
|
81
81
|
exitSpy.mockRestore();
|
package/src/lib/flag-args.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function parseFeatureFlagArgs(args: string[]): {
|
|
|
38
38
|
|
|
39
39
|
if (!ALLOWED_KEY_RE.test(key)) {
|
|
40
40
|
console.error(
|
|
41
|
-
`Error: invalid flag key "${key}". Keys must be kebab-case
|
|
41
|
+
`Error: invalid flag key "${key}". Keys must be kebab-case, e.g. "browser"`,
|
|
42
42
|
);
|
|
43
43
|
process.exit(1);
|
|
44
44
|
}
|
|
@@ -49,7 +49,11 @@ const GuardianTokenFieldsSchema = z
|
|
|
49
49
|
.union([z.string(), z.number()])
|
|
50
50
|
.optional()
|
|
51
51
|
.catch(undefined),
|
|
52
|
-
refreshAfter: z
|
|
52
|
+
refreshAfter: z
|
|
53
|
+
.union([z.string(), z.number()])
|
|
54
|
+
.transform((v) => (typeof v === "number" ? new Date(v).toISOString() : v))
|
|
55
|
+
.optional()
|
|
56
|
+
.catch(undefined),
|
|
53
57
|
isNew: z.boolean().optional().catch(undefined),
|
|
54
58
|
deviceId: z.string().optional().catch(undefined),
|
|
55
59
|
})
|
|
@@ -425,7 +429,7 @@ export async function refreshGuardianTokenResult(
|
|
|
425
429
|
console.warn(
|
|
426
430
|
`Refusing to refresh the guardian token over an insecure URL (${gatewayUrl}). ` +
|
|
427
431
|
"The refresh token is only sent over https or a loopback address: " +
|
|
428
|
-
"use an https URL
|
|
432
|
+
"use an https URL, e.g. a tunnel, or connect over loopback.",
|
|
429
433
|
);
|
|
430
434
|
return {
|
|
431
435
|
ok: false,
|
|
@@ -590,6 +594,20 @@ async function waitForGatewayReady(gatewayUrl: string): Promise<void> {
|
|
|
590
594
|
}
|
|
591
595
|
}
|
|
592
596
|
|
|
597
|
+
export class GuardianLeasePermanentError extends Error {
|
|
598
|
+
constructor(message: string) {
|
|
599
|
+
super(message);
|
|
600
|
+
this.name = "GuardianLeasePermanentError";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function describeJsonType(value: unknown): string {
|
|
605
|
+
if (value === undefined) return "absent";
|
|
606
|
+
if (value === null) return "null";
|
|
607
|
+
if (Array.isArray(value)) return "array";
|
|
608
|
+
return typeof value;
|
|
609
|
+
}
|
|
610
|
+
|
|
593
611
|
export async function leaseGuardianToken(
|
|
594
612
|
gatewayUrl: string,
|
|
595
613
|
assistantId: string,
|
|
@@ -621,7 +639,8 @@ export async function leaseGuardianToken(
|
|
|
621
639
|
throw new Error(`guardian/init failed (${response.status}): ${body}`);
|
|
622
640
|
}
|
|
623
641
|
|
|
624
|
-
const
|
|
642
|
+
const rawBody: unknown = await response.json();
|
|
643
|
+
const parsed = parseGuardianTokenFields(rawBody);
|
|
625
644
|
if (
|
|
626
645
|
parsed.guardianPrincipalId == null ||
|
|
627
646
|
parsed.accessToken == null ||
|
|
@@ -631,8 +650,21 @@ export async function leaseGuardianToken(
|
|
|
631
650
|
parsed.refreshAfter == null ||
|
|
632
651
|
parsed.isNew == null
|
|
633
652
|
) {
|
|
634
|
-
|
|
635
|
-
|
|
653
|
+
const raw = z.record(z.string(), z.unknown()).catch({}).parse(rawBody);
|
|
654
|
+
const requiredFields: (keyof typeof parsed)[] = [
|
|
655
|
+
"guardianPrincipalId",
|
|
656
|
+
"accessToken",
|
|
657
|
+
"accessTokenExpiresAt",
|
|
658
|
+
"refreshToken",
|
|
659
|
+
"refreshTokenExpiresAt",
|
|
660
|
+
"refreshAfter",
|
|
661
|
+
"isNew",
|
|
662
|
+
];
|
|
663
|
+
const unusable = requiredFields
|
|
664
|
+
.filter((field) => parsed[field] == null)
|
|
665
|
+
.map((field) => `${field} arrived as ${describeJsonType(raw[field])}`);
|
|
666
|
+
throw new GuardianLeasePermanentError(
|
|
667
|
+
`guardian/init returned a malformed token response (${response.status}) — ${unusable.join(", ")}`,
|
|
636
668
|
);
|
|
637
669
|
}
|
|
638
670
|
const tokenData: GuardianTokenData = {
|
package/src/lib/hatch-local.ts
CHANGED
|
@@ -32,7 +32,10 @@ import {
|
|
|
32
32
|
} from "./local.js";
|
|
33
33
|
|
|
34
34
|
import { generateInstanceName } from "./random-name.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
GuardianLeasePermanentError,
|
|
37
|
+
leaseGuardianToken,
|
|
38
|
+
} from "./guardian-token.js";
|
|
36
39
|
import { archiveLogFile, resetLogFile } from "./xdg-log.js";
|
|
37
40
|
import {
|
|
38
41
|
consoleLifecycleReporter,
|
|
@@ -54,7 +57,7 @@ import {
|
|
|
54
57
|
ensureCloudLoginInteractive,
|
|
55
58
|
registerLocalAssistantWithPlatform,
|
|
56
59
|
} from "./cloud-login.js";
|
|
57
|
-
import { fetchCurrentUser,
|
|
60
|
+
import { fetchCurrentUser, readCloudToken } from "./cloud-client.js";
|
|
58
61
|
import { withLocalHatchLock } from "./local-hatch-lock.js";
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -185,8 +188,6 @@ export interface HatchLocalResult {
|
|
|
185
188
|
guardianAccessToken?: string;
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
|
|
190
191
|
export async function hatchLocal(
|
|
191
192
|
params: HatchLocalParams = {},
|
|
192
193
|
): Promise<HatchLocalResult> {
|
|
@@ -199,8 +200,7 @@ export async function hatchLocal(
|
|
|
199
200
|
} = params;
|
|
200
201
|
const reporter = params.reporter ?? consoleLifecycleReporter;
|
|
201
202
|
const verbose = params.verbose ?? false;
|
|
202
|
-
const setupProviderCredentials =
|
|
203
|
-
params.setupProviderCredentials !== false;
|
|
203
|
+
const setupProviderCredentials = params.setupProviderCredentials !== false;
|
|
204
204
|
setStartupLogging(verbose);
|
|
205
205
|
let provider = setupProviderCredentials
|
|
206
206
|
? resolveHatchProvider(configValues)
|
|
@@ -226,12 +226,7 @@ export async function hatchLocal(
|
|
|
226
226
|
|
|
227
227
|
const resources = await allocateLocalResources(instanceName);
|
|
228
228
|
|
|
229
|
-
const logsDir = join(
|
|
230
|
-
resources.instanceDir,
|
|
231
|
-
"workspace",
|
|
232
|
-
"data",
|
|
233
|
-
"logs",
|
|
234
|
-
);
|
|
229
|
+
const logsDir = join(resources.instanceDir, "workspace", "data", "logs");
|
|
235
230
|
archiveLogFile("hatch.log", logsDir);
|
|
236
231
|
resetLogFile("hatch.log");
|
|
237
232
|
|
|
@@ -263,7 +258,7 @@ export async function hatchLocal(
|
|
|
263
258
|
picked === "cadet" &&
|
|
264
259
|
!process.env.CADET_DESKTOP_APP &&
|
|
265
260
|
process.stdin.isTTY &&
|
|
266
|
-
!
|
|
261
|
+
!readCloudToken()
|
|
267
262
|
) {
|
|
268
263
|
reporter.log("");
|
|
269
264
|
reporter.log(
|
|
@@ -276,9 +271,7 @@ export async function hatchLocal(
|
|
|
276
271
|
reporter.warn(
|
|
277
272
|
`⚠️ Cadet Cloud login failed: ${describeLoginError(error)}`,
|
|
278
273
|
);
|
|
279
|
-
reporter.warn(
|
|
280
|
-
" You can retry later with `cadet login`.",
|
|
281
|
-
);
|
|
274
|
+
reporter.warn(" You can retry later with `cadet login`.");
|
|
282
275
|
}
|
|
283
276
|
}
|
|
284
277
|
}
|
|
@@ -380,7 +373,10 @@ export async function hatchLocal(
|
|
|
380
373
|
guardianAccessToken = tokenData.accessToken;
|
|
381
374
|
break;
|
|
382
375
|
} catch (err) {
|
|
383
|
-
if (
|
|
376
|
+
if (
|
|
377
|
+
attempt < maxLeaseAttempts &&
|
|
378
|
+
!(err instanceof GuardianLeasePermanentError)
|
|
379
|
+
) {
|
|
384
380
|
const delayMs = 2000 * 2 ** (attempt - 1);
|
|
385
381
|
reporter.error(
|
|
386
382
|
`⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
|
|
@@ -388,9 +384,9 @@ export async function hatchLocal(
|
|
|
388
384
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
389
385
|
} else {
|
|
390
386
|
reporter.error(
|
|
391
|
-
`⚠️ Guardian token lease failed after ${maxLeaseAttempts} attempts: ${err}\n` +
|
|
387
|
+
`⚠️ Guardian token lease failed${err instanceof GuardianLeasePermanentError ? ", not retrying — a malformed 200 will not fix itself" : ` after ${maxLeaseAttempts} attempts`}: ${err}\n` +
|
|
392
388
|
` The assistant is running but guardian-token.json was not written.\n` +
|
|
393
|
-
`
|
|
389
|
+
` Run \`cadet restart ${JSON.stringify(instanceName)} --repair-guardian\` to re-lease a token.`,
|
|
394
390
|
);
|
|
395
391
|
}
|
|
396
392
|
}
|
|
@@ -415,13 +411,13 @@ export async function hatchLocal(
|
|
|
415
411
|
});
|
|
416
412
|
}
|
|
417
413
|
|
|
418
|
-
// Picking Cadet Cloud ends with the assistant registered on the
|
|
414
|
+
// Picking Cadet Cloud ends with the assistant registered on the cloud
|
|
419
415
|
// and its cloud credentials injected — the same post-login flow
|
|
420
|
-
// `cadet login` runs — so messages route through the
|
|
416
|
+
// `cadet login` runs — so messages route through the cloud connection
|
|
421
417
|
// immediately after hatch. Non-fatal: the assistant is up either way.
|
|
422
|
-
if (provider === "cadet" &&
|
|
418
|
+
if (provider === "cadet" && readCloudToken()) {
|
|
423
419
|
try {
|
|
424
|
-
const token =
|
|
420
|
+
const token = readCloudToken()!;
|
|
425
421
|
const user = await fetchCurrentUser(token);
|
|
426
422
|
await registerLocalAssistantWithPlatform(token, user, {
|
|
427
423
|
bearerToken: guardianAccessToken,
|
package/src/lib/health-check.ts
CHANGED
|
@@ -19,8 +19,8 @@ export async function checkManagedHealth(
|
|
|
19
19
|
runtimeUrl: string,
|
|
20
20
|
assistantId: string,
|
|
21
21
|
): Promise<HealthCheckResult> {
|
|
22
|
-
const {
|
|
23
|
-
const token =
|
|
22
|
+
const { readCloudToken } = await import("./cloud-client.js");
|
|
23
|
+
const token = readCloudToken();
|
|
24
24
|
if (!token) {
|
|
25
25
|
return {
|
|
26
26
|
status: "error (auth)",
|
|
@@ -30,7 +30,7 @@ export async function checkManagedHealth(
|
|
|
30
30
|
|
|
31
31
|
let headers: Record<string, string>;
|
|
32
32
|
try {
|
|
33
|
-
const { authHeaders } = await import("./
|
|
33
|
+
const { authHeaders } = await import("./cloud-client.js");
|
|
34
34
|
headers = await authHeaders(token, runtimeUrl);
|
|
35
35
|
} catch (err) {
|
|
36
36
|
return {
|
|
@@ -130,9 +130,9 @@ export async function fetchManagedPs(
|
|
|
130
130
|
runtimeUrl: string,
|
|
131
131
|
assistantId: string,
|
|
132
132
|
): Promise<ManagedPsResponse | null> {
|
|
133
|
-
const {
|
|
134
|
-
await import("./
|
|
135
|
-
const token =
|
|
133
|
+
const { readCloudToken, authHeaders } =
|
|
134
|
+
await import("./cloud-client.js");
|
|
135
|
+
const token = readCloudToken();
|
|
136
136
|
if (!token) return null;
|
|
137
137
|
|
|
138
138
|
let headers: Record<string, string>;
|