@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64
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 +6 -4
- 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 +1 -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 +15 -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 +149 -85
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -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 +180 -211
- package/src/__tests__/tunnel.test.ts +490 -270
- package/src/__tests__/uninstall.test.ts +150 -9
- 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 -78
- 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 +27 -24
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +232 -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 +110 -129
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +156 -175
- package/src/commands/uninstall.ts +137 -54
- 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-gateway-cli.test.ts +42 -0
- 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} +105 -99
- package/src/lib/cloud-login.ts +23 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/color.ts +4 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +300 -111
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +29 -3
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +10 -12
- 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 +32 -29
- package/src/lib/hatch-next-steps.ts +5 -9
- 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 +46 -34
- package/src/lib/nginx-ingress.ts +40 -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/step-runner.ts +6 -1
- 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
|
+
}
|
|
@@ -39,26 +39,33 @@ const {
|
|
|
39
39
|
getConfigDir,
|
|
40
40
|
getConfigDirs,
|
|
41
41
|
getDefaultPorts,
|
|
42
|
+
getInputHistoryPath,
|
|
42
43
|
getLockfilePath,
|
|
43
44
|
getLockfilePaths,
|
|
44
45
|
getMultiInstanceDir,
|
|
46
|
+
getSharedStateDir,
|
|
45
47
|
} = await import("../paths.js");
|
|
46
48
|
type EnvironmentDefinition =
|
|
47
49
|
import("@mycadet/environments").EnvironmentDefinition;
|
|
48
50
|
|
|
49
51
|
const prod: EnvironmentDefinition = {
|
|
50
52
|
name: "production",
|
|
51
|
-
|
|
53
|
+
cloudUrl: "https://cloud.mycadet.ai",
|
|
52
54
|
webUrl: "https://mycadet.ai",
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
const dev: EnvironmentDefinition = {
|
|
56
58
|
name: "dev",
|
|
57
|
-
|
|
59
|
+
cloudUrl: "https://dev-cloud.mycadet.ai",
|
|
58
60
|
webUrl: "https://dev.mycadet.ai",
|
|
59
61
|
};
|
|
60
62
|
|
|
61
|
-
const XDG_ENV_VARS = [
|
|
63
|
+
const XDG_ENV_VARS = [
|
|
64
|
+
"XDG_DATA_HOME",
|
|
65
|
+
"XDG_CONFIG_HOME",
|
|
66
|
+
"XDG_STATE_HOME",
|
|
67
|
+
"APPDATA",
|
|
68
|
+
] as const;
|
|
62
69
|
|
|
63
70
|
describe("path helpers", () => {
|
|
64
71
|
let savedEnv: Record<string, string | undefined>;
|
|
@@ -224,6 +231,25 @@ describe("path helpers", () => {
|
|
|
224
231
|
});
|
|
225
232
|
});
|
|
226
233
|
|
|
234
|
+
describe("getSharedStateDir", () => {
|
|
235
|
+
test("is not environment-scoped: ~/.local/state/cadet for every env", () => {
|
|
236
|
+
expect(getSharedStateDir()).toBe(
|
|
237
|
+
join(TEST_HOME, ".local", "state", "cadet"),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("respects XDG_STATE_HOME", () => {
|
|
242
|
+
process.env.XDG_STATE_HOME = "/custom/state";
|
|
243
|
+
expect(getSharedStateDir()).toBe("/custom/state/cadet");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("holds the shared terminal input history file", () => {
|
|
247
|
+
expect(getInputHistoryPath()).toBe(
|
|
248
|
+
join(getSharedStateDir(), "input-history"),
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
227
253
|
describe("getDefaultPorts", () => {
|
|
228
254
|
test("returns production defaults for production", () => {
|
|
229
255
|
const ports = getDefaultPorts(prod);
|
|
@@ -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", () => {
|
|
@@ -82,18 +82,26 @@ export function getLogDir(env: EnvironmentDefinition): string {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
85
|
+
* The interactive CLI's shared state directory — `$XDG_STATE_HOME/cadet`.
|
|
86
86
|
*
|
|
87
|
-
* Follows the XDG Base Directory spec:
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* Defaults to `~/.local/state/cadet/input-history`.
|
|
87
|
+
* Follows the XDG Base Directory spec: session state is data that persists
|
|
88
|
+
* across runs but is not portable user-owned content, mirroring `bash`,
|
|
89
|
+
* `zsh`, `psql`, and `gh`. Defaults to `~/.local/state/cadet`.
|
|
91
90
|
*
|
|
92
|
-
* Not environment-scoped:
|
|
93
|
-
*
|
|
91
|
+
* Not environment-scoped: it holds per-user state (the terminal input
|
|
92
|
+
* history, below) that dev and prod CLIs share.
|
|
93
|
+
*/
|
|
94
|
+
export function getSharedStateDir(): string {
|
|
95
|
+
return join(xdgStateHome(), "cadet");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Path to the interactive CLI's input history file, inside
|
|
100
|
+
* {@link getSharedStateDir}. Defaults to
|
|
101
|
+
* `~/.local/state/cadet/input-history`.
|
|
94
102
|
*/
|
|
95
103
|
export function getInputHistoryPath(): string {
|
|
96
|
-
return join(
|
|
104
|
+
return join(getSharedStateDir(), "input-history");
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
/**
|
|
@@ -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
|
*
|
|
@@ -67,10 +67,8 @@ export function getCurrentEnvironment(
|
|
|
67
67
|
const seed = SEEDS[name];
|
|
68
68
|
if (!seed) {
|
|
69
69
|
if (name !== DEFAULT_ENVIRONMENT_NAME) {
|
|
70
|
-
// Warn on stderr instead of throwing
|
|
71
|
-
//
|
|
72
|
-
// which silently falls back to production; the CLI agrees so neither
|
|
73
|
-
// writer ends up in a disjoint state on a typo.
|
|
70
|
+
// Warn on stderr instead of throwing: an unknown environment falls back
|
|
71
|
+
// to production, so a typo never splits writes between two states.
|
|
74
72
|
process.stderr.write(
|
|
75
73
|
`warning: unknown environment "${name}"; falling back to "${DEFAULT_ENVIRONMENT_NAME}". ` +
|
|
76
74
|
`Add it to packages/environments/src/seeds.ts and rebuild if this was intentional.\n`,
|
|
@@ -87,9 +85,9 @@ export function getCurrentEnvironment(
|
|
|
87
85
|
|
|
88
86
|
const resolved: EnvironmentDefinition = { ...seed };
|
|
89
87
|
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
resolved.
|
|
88
|
+
const cloudUrlOverride = process.env.CADET_CLOUD_URL?.trim();
|
|
89
|
+
if (cloudUrlOverride) {
|
|
90
|
+
resolved.cloudUrl = cloudUrlOverride;
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
const webUrlOverride = process.env.CADET_WEB_URL?.trim();
|
|
@@ -97,10 +95,10 @@ export function getCurrentEnvironment(
|
|
|
97
95
|
resolved.webUrl = webUrlOverride;
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
const
|
|
98
|
+
const assistantCloudUrlOverride =
|
|
101
99
|
process.env.CADET_ASSISTANT_PLATFORM_URL?.trim();
|
|
102
|
-
if (
|
|
103
|
-
resolved.
|
|
100
|
+
if (assistantCloudUrlOverride) {
|
|
101
|
+
resolved.assistantCloudUrl = assistantCloudUrlOverride;
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
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 = {
|