@mycadet/cli 0.12.10 → 0.13.0
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 +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/package.json +3 -2
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +64 -2
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +233 -36
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +8 -7
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +17 -7
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +13 -3
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +117 -47
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/hatch.ts +10 -52
- package/src/commands/pair.ts +9 -3
- package/src/commands/ps.ts +4 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/tunnel.ts +43 -4
- package/src/commands/upgrade.ts +84 -157
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +20 -23
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +5 -2
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +65 -34
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +18 -52
- package/src/lib/hatch-next-steps.ts +15 -2
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +3 -3
- package/src/lib/local.ts +10 -8
- package/src/lib/nginx-ingress.ts +254 -36
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/tunnel.ts +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +23 -1
- package/src/lib/xdg-log.ts +11 -0
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { CADET_GITHUB_REPOSITORY } from "./github-repository.js";
|
|
1
4
|
import { getCloudUrl } from "./cloud-client.js";
|
|
2
5
|
import { dockerhubImages } from "./docker.js";
|
|
3
6
|
import type { ServiceName } from "./docker.js";
|
|
@@ -22,12 +25,40 @@ function releasesOptsFor(channel: ReleaseChannel): { channel?: "preview" } {
|
|
|
22
25
|
return channel === "preview" ? { channel: "preview" } : {};
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
version: string
|
|
27
|
-
is_stable
|
|
28
|
-
assistant_image_ref
|
|
29
|
-
gateway_image_ref
|
|
30
|
-
credential_executor_image_ref
|
|
28
|
+
const ReleaseListItemSchema = z.object({
|
|
29
|
+
version: z.string().min(1),
|
|
30
|
+
is_stable: z.boolean().optional(),
|
|
31
|
+
assistant_image_ref: z.string().nullable().optional(),
|
|
32
|
+
gateway_image_ref: z.string().nullable().optional(),
|
|
33
|
+
credential_executor_image_ref: z.string().nullable().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export type ReleaseListItem = z.infer<typeof ReleaseListItemSchema>;
|
|
37
|
+
|
|
38
|
+
const STABLE_VERSION = /^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
39
|
+
const GitHubReleaseSchema = z.object({
|
|
40
|
+
tag_name: z.string().regex(STABLE_VERSION),
|
|
41
|
+
draft: z.literal(false),
|
|
42
|
+
prerelease: z.literal(false),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function fetchLatestGitHubStableVersion(): Promise<string | null> {
|
|
46
|
+
try {
|
|
47
|
+
const response = await loopbackSafeFetch(
|
|
48
|
+
`https://api.github.com/repos/${CADET_GITHUB_REPOSITORY}/releases/latest`,
|
|
49
|
+
{
|
|
50
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
51
|
+
signal: AbortSignal.timeout(10_000),
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const parsed = GitHubReleaseSchema.safeParse(await response.json());
|
|
58
|
+
return parsed.success ? parsed.data.tag_name.replace(/^v/, "") : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
/**
|
|
@@ -56,15 +87,21 @@ export async function fetchReleases(opts?: {
|
|
|
56
87
|
`${cloudUrl}/v1/releases/?${filter}&limit=${RELEASES_FETCH_LIMIT}`,
|
|
57
88
|
{ signal: AbortSignal.timeout(10_000) },
|
|
58
89
|
);
|
|
59
|
-
if (!response.ok)
|
|
60
|
-
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const parsed = z
|
|
94
|
+
.array(ReleaseListItemSchema)
|
|
95
|
+
.safeParse(await response.json());
|
|
96
|
+
return parsed.success ? parsed.data : null;
|
|
61
97
|
} catch {
|
|
62
98
|
return null;
|
|
63
99
|
}
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
/**
|
|
67
|
-
* Fetch the latest release version
|
|
103
|
+
* Fetch the latest release version from Cadet Cloud, falling back to GitHub
|
|
104
|
+
* for stable releases when the cloud has no usable result.
|
|
68
105
|
* Returns the version string (e.g. "0.7.0") or null if unavailable.
|
|
69
106
|
* The releases endpoint returns entries ordered newest-first, so the first
|
|
70
107
|
* entry is the latest for the requested channel.
|
|
@@ -73,7 +110,14 @@ export async function fetchLatestVersion(
|
|
|
73
110
|
channel: ReleaseChannel = "stable",
|
|
74
111
|
): Promise<string | null> {
|
|
75
112
|
const releases = await fetchReleases(releasesOptsFor(channel));
|
|
76
|
-
|
|
113
|
+
if (channel === "preview") {
|
|
114
|
+
return releases?.[0]?.version ?? null;
|
|
115
|
+
}
|
|
116
|
+
const latest = releases?.find(
|
|
117
|
+
(release) =>
|
|
118
|
+
release.is_stable !== false && STABLE_VERSION.test(release.version),
|
|
119
|
+
);
|
|
120
|
+
return latest?.version ?? fetchLatestGitHubStableVersion();
|
|
77
121
|
}
|
|
78
122
|
|
|
79
123
|
/**
|
package/src/lib/cloud-values.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared interactive confirmation for destructive CLI commands (retire,
|
|
3
|
-
* …). Per cli/AGENTS.md, a command that removes assistant state must print the
|
|
2
|
+
* Shared interactive confirmation for destructive CLI commands (retire,
|
|
3
|
+
* devices revoke, …). Per cli/AGENTS.md, a command that removes assistant state must print the
|
|
4
4
|
* resolved identity and require confirmation, with a `--yes` bypass for
|
|
5
5
|
* automation.
|
|
6
6
|
*/
|
package/src/lib/docker.ts
CHANGED
|
@@ -296,6 +296,8 @@ export interface HatchDockerParams {
|
|
|
296
296
|
/** Extra env vars forwarded into the assistant container. */
|
|
297
297
|
flagEnvVars?: Record<string, string>;
|
|
298
298
|
setupProviderCredentials?: boolean;
|
|
299
|
+
/** Whole-installation state installed into stopped containers before startup. */
|
|
300
|
+
initialArchive?: string;
|
|
299
301
|
/**
|
|
300
302
|
* Path to a local source tree to build images from before hatching. When
|
|
301
303
|
* provided, this path is used directly as the repo root and no file
|
|
@@ -305,7 +307,6 @@ export interface HatchDockerParams {
|
|
|
305
307
|
* also enables hot-reload.
|
|
306
308
|
*/
|
|
307
309
|
sourcePath?: string | null;
|
|
308
|
-
analyze?: boolean;
|
|
309
310
|
/**
|
|
310
311
|
* Name of an existing container whose network namespace the assistant,
|
|
311
312
|
* gateway, and credential-executor join (`--network=container:<name>`),
|
|
@@ -813,6 +814,7 @@ export const SERVICE_START_ORDER: ServiceName[] = [
|
|
|
813
814
|
/** Start all three containers in dependency order. */
|
|
814
815
|
export async function startContainers(
|
|
815
816
|
opts: {
|
|
817
|
+
createOnly?: boolean;
|
|
816
818
|
signingKey?: string;
|
|
817
819
|
bootstrapSecret?: string;
|
|
818
820
|
cesServiceToken?: string;
|
|
@@ -833,8 +835,13 @@ export async function startContainers(
|
|
|
833
835
|
avatarDevicePath: resolveAvatarDevicePath(),
|
|
834
836
|
});
|
|
835
837
|
for (const service of SERVICE_START_ORDER) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
+
const args = runArgs[service]();
|
|
839
|
+
if (opts.createOnly) {
|
|
840
|
+
args[0] = "create";
|
|
841
|
+
args.splice(args.indexOf("-d"), 1);
|
|
842
|
+
}
|
|
843
|
+
log(`${opts.createOnly ? "Creating" : "Starting"} ${service} container...`);
|
|
844
|
+
await exec("docker", args);
|
|
838
845
|
}
|
|
839
846
|
}
|
|
840
847
|
|
|
@@ -850,14 +857,23 @@ export async function stopContainers(
|
|
|
850
857
|
/** Stop containers without removing them (preserves state for `docker start`). */
|
|
851
858
|
export async function sleepContainers(
|
|
852
859
|
res: ReturnType<typeof dockerResourceNames>,
|
|
860
|
+
options: { timeoutMs?: number } = {},
|
|
853
861
|
): Promise<void> {
|
|
854
862
|
for (const container of [
|
|
855
|
-
res.cesContainer,
|
|
856
|
-
res.gatewayContainer,
|
|
857
863
|
res.assistantContainer,
|
|
864
|
+
res.gatewayContainer,
|
|
865
|
+
res.cesContainer,
|
|
858
866
|
]) {
|
|
859
867
|
try {
|
|
860
|
-
await
|
|
868
|
+
const status = await execOutput("docker", ["inspect", "--format", "{{.State.Status}}", container]);
|
|
869
|
+
if (status === "exited" || status === "created") continue;
|
|
870
|
+
if (status !== "running") throw new Error(`${container} is ${status}; cannot stop gracefully.`);
|
|
871
|
+
await exec("docker", ["kill", "--signal=SIGTERM", container]);
|
|
872
|
+
const deadline = Date.now() + (options.timeoutMs ?? 60_000);
|
|
873
|
+
while (await execOutput("docker", ["inspect", "--format", "{{.State.Running}}", container]) === "true") {
|
|
874
|
+
if (Date.now() >= deadline) throw new Error(`${container} did not stop gracefully.`);
|
|
875
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
876
|
+
}
|
|
861
877
|
} catch (err) {
|
|
862
878
|
const msg =
|
|
863
879
|
err instanceof Error ? err.message.toLowerCase() : String(err);
|
|
@@ -1631,7 +1647,9 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1631
1647
|
}
|
|
1632
1648
|
|
|
1633
1649
|
const cesServiceToken = randomBytes(32).toString("hex");
|
|
1634
|
-
const signingKey =
|
|
1650
|
+
const signingKey = params.initialArchive
|
|
1651
|
+
? undefined
|
|
1652
|
+
: randomBytes(32).toString("hex");
|
|
1635
1653
|
|
|
1636
1654
|
// When launched by a remote hatch startup script, the env var
|
|
1637
1655
|
// GUARDIAN_BOOTSTRAP_SECRET is already set with the laptop's secret.
|
|
@@ -1670,8 +1688,26 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1670
1688
|
...flagEnvVars,
|
|
1671
1689
|
CADET_DEVICE_ID: hostDeviceId,
|
|
1672
1690
|
};
|
|
1691
|
+
const runtimeUrl = `http://localhost:${gatewayPort}`;
|
|
1692
|
+
const dockerEntry: AssistantEntry = {
|
|
1693
|
+
assistantId: instanceName,
|
|
1694
|
+
runtimeUrl,
|
|
1695
|
+
cloud: "docker",
|
|
1696
|
+
hatchedAt: new Date().toISOString(),
|
|
1697
|
+
guardianBootstrapSecret: ownSecret,
|
|
1698
|
+
containerInfo: {
|
|
1699
|
+
assistantImage: imageTags.assistant,
|
|
1700
|
+
gatewayImage: imageTags.gateway,
|
|
1701
|
+
cesImage: imageTags["credential-executor"],
|
|
1702
|
+
assistantPort,
|
|
1703
|
+
},
|
|
1704
|
+
};
|
|
1705
|
+
if (params.initialArchive) {
|
|
1706
|
+
saveAssistantEntry(dockerEntry);
|
|
1707
|
+
}
|
|
1673
1708
|
await startContainers(
|
|
1674
1709
|
{
|
|
1710
|
+
createOnly: !!params.initialArchive,
|
|
1675
1711
|
signingKey,
|
|
1676
1712
|
bootstrapSecret,
|
|
1677
1713
|
cesServiceToken,
|
|
@@ -1689,29 +1725,29 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1689
1725
|
);
|
|
1690
1726
|
const containersUpAt = Date.now();
|
|
1691
1727
|
|
|
1692
|
-
const runtimeUrl = `http://localhost:${gatewayPort}`;
|
|
1693
|
-
const dockerEntry: AssistantEntry = {
|
|
1694
|
-
assistantId: instanceName,
|
|
1695
|
-
runtimeUrl,
|
|
1696
|
-
cloud: "docker",
|
|
1697
|
-
hatchedAt: new Date().toISOString(),
|
|
1698
|
-
guardianBootstrapSecret: ownSecret,
|
|
1699
|
-
containerInfo: {
|
|
1700
|
-
assistantImage: imageTags.assistant,
|
|
1701
|
-
gatewayImage: imageTags.gateway,
|
|
1702
|
-
cesImage: imageTags["credential-executor"],
|
|
1703
|
-
assistantPort,
|
|
1704
|
-
},
|
|
1705
|
-
};
|
|
1706
1728
|
emitProgress(5, 6, "Saving configuration...");
|
|
1707
1729
|
saveAssistantEntry(dockerEntry);
|
|
1708
1730
|
setActiveAssistant(instanceName);
|
|
1709
1731
|
|
|
1732
|
+
if (params.initialArchive) {
|
|
1733
|
+
const { initializeRootArchive, DockerRootStorage } =
|
|
1734
|
+
await import("./root-backup.js");
|
|
1735
|
+
await initializeRootArchive(dockerEntry, params.initialArchive);
|
|
1736
|
+
const storage = new DockerRootStorage(dockerEntry);
|
|
1737
|
+
storage.initialRunning = new Set(Object.values(storage.containers));
|
|
1738
|
+
await storage.resume();
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1710
1741
|
emitProgress(6, 6, "Waiting for services...");
|
|
1711
1742
|
// Hatch always detaches; only watch mode keeps the log stream attached.
|
|
1712
1743
|
const waitDetached = !watch;
|
|
1713
|
-
const { ready, guardianAccessToken, notReadyReason } =
|
|
1714
|
-
|
|
1744
|
+
const { ready, guardianAccessToken, notReadyReason } = params.initialArchive
|
|
1745
|
+
? {
|
|
1746
|
+
ready: true,
|
|
1747
|
+
guardianAccessToken: undefined,
|
|
1748
|
+
notReadyReason: undefined,
|
|
1749
|
+
}
|
|
1750
|
+
: await waitForGatewayAndLease({
|
|
1715
1751
|
bootstrapSecret: ownSecret,
|
|
1716
1752
|
containerName: res.assistantContainer,
|
|
1717
1753
|
detached: waitDetached,
|
|
@@ -1719,7 +1755,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
|
|
|
1719
1755
|
logFd,
|
|
1720
1756
|
runtimeUrl,
|
|
1721
1757
|
containersUpAt,
|
|
1722
|
-
analyze: params.analyze ?? false,
|
|
1723
1758
|
});
|
|
1724
1759
|
|
|
1725
1760
|
if (!ready && !(watch && repoRoot)) {
|
|
@@ -1828,7 +1863,6 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1828
1863
|
logFd: number | "ignore";
|
|
1829
1864
|
runtimeUrl: string;
|
|
1830
1865
|
containersUpAt: number;
|
|
1831
|
-
analyze: boolean;
|
|
1832
1866
|
}): Promise<{
|
|
1833
1867
|
ready: boolean;
|
|
1834
1868
|
guardianAccessToken?: string;
|
|
@@ -1844,7 +1878,6 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1844
1878
|
logFd,
|
|
1845
1879
|
runtimeUrl,
|
|
1846
1880
|
containersUpAt,
|
|
1847
|
-
analyze,
|
|
1848
1881
|
} = opts;
|
|
1849
1882
|
|
|
1850
1883
|
const log = (msg: string): void => {
|
|
@@ -1964,14 +1997,12 @@ async function waitForGatewayAndLease(opts: {
|
|
|
1964
1997
|
const containersUpToReadyMs = readyAt - start;
|
|
1965
1998
|
const elapsedSec = (containersUpToReadyMs / 1000).toFixed(1);
|
|
1966
1999
|
log(`Assistant ready after ${elapsedSec}s`);
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
);
|
|
1974
|
-
}
|
|
2000
|
+
console.info(
|
|
2001
|
+
`[cadet-hatch-timing] ${JSON.stringify({
|
|
2002
|
+
containers_up_to_ready_ms: containersUpToReadyMs,
|
|
2003
|
+
instance: instanceName,
|
|
2004
|
+
})}`,
|
|
2005
|
+
);
|
|
1975
2006
|
|
|
1976
2007
|
// Lease guardian token. The /readyz check confirms both gateway and
|
|
1977
2008
|
// assistant are reachable. Retry with backoff in case there is a brief
|
package/src/lib/ensure-binary.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface EnsureBinaryOptions {
|
|
|
31
31
|
runInstall?: (argv: string[]) => boolean;
|
|
32
32
|
/** Confirm the install. Default: promptLine "[Y/n]" on Enter-as-yes. */
|
|
33
33
|
confirmInstall?: () => Promise<boolean>;
|
|
34
|
+
platform?: NodeJS.Platform;
|
|
35
|
+
confirmHomebrewInstall?: () => Promise<boolean>;
|
|
34
36
|
streams?: {
|
|
35
37
|
input?: NodeJS.ReadStream | Readable;
|
|
36
38
|
output?: NodeJS.WriteStream | Writable;
|
|
@@ -69,9 +71,60 @@ export async function ensureBinaryInstalled(
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
const pmAvailable = opts.pmAvailable ?? probePackageManager;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
let argv = Object.entries(opts.packages).find(([pm]) => pmAvailable(pm))?.[1];
|
|
75
|
+
if (
|
|
76
|
+
!argv &&
|
|
77
|
+
(opts.platform ?? process.platform) === "darwin" &&
|
|
78
|
+
opts.packages.brew
|
|
79
|
+
) {
|
|
80
|
+
const brew = ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"].find(
|
|
81
|
+
pmAvailable,
|
|
82
|
+
);
|
|
83
|
+
if (brew) {
|
|
84
|
+
const prefix = brew.slice(0, -"/bin/brew".length);
|
|
85
|
+
process.env.PATH = `${prefix}/bin:${prefix}/sbin:${process.env.PATH ?? ""}`;
|
|
86
|
+
argv = [brew, ...opts.packages.brew.slice(1)];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
!argv &&
|
|
91
|
+
(opts.platform ?? process.platform) === "darwin" &&
|
|
92
|
+
opts.packages.brew
|
|
93
|
+
) {
|
|
94
|
+
output.write(
|
|
95
|
+
"Homebrew is required to install this dependency, but it isn't installed.\n",
|
|
96
|
+
);
|
|
97
|
+
const confirmHomebrew =
|
|
98
|
+
opts.confirmHomebrewInstall ?? defaultConfirm(opts, "Homebrew");
|
|
99
|
+
let approved = false;
|
|
100
|
+
try {
|
|
101
|
+
approved = await confirmHomebrew();
|
|
102
|
+
} catch {
|
|
103
|
+
/* No interactive approval. */
|
|
104
|
+
}
|
|
105
|
+
if (approved) {
|
|
106
|
+
const bootstrap = [
|
|
107
|
+
"/bin/bash",
|
|
108
|
+
"-c",
|
|
109
|
+
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/7a3f48c7e498d7df3237fd41c30f4d492857edf4/install.sh)"',
|
|
110
|
+
];
|
|
111
|
+
const installed = opts.runInstall
|
|
112
|
+
? opts.runInstall(bootstrap)
|
|
113
|
+
: runInstallArgv(bootstrap);
|
|
114
|
+
const brew = installed
|
|
115
|
+
? ["brew", "/opt/homebrew/bin/brew", "/usr/local/bin/brew"].find(
|
|
116
|
+
pmAvailable,
|
|
117
|
+
)
|
|
118
|
+
: undefined;
|
|
119
|
+
if (brew) {
|
|
120
|
+
const prefix =
|
|
121
|
+
brew === "brew" ? "" : brew.slice(0, -"/bin/brew".length);
|
|
122
|
+
if (prefix)
|
|
123
|
+
process.env.PATH = `${prefix}/bin:${prefix}/sbin:${process.env.PATH ?? ""}`;
|
|
124
|
+
argv = [brew, ...opts.packages.brew.slice(1)];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
75
128
|
if (!argv) {
|
|
76
129
|
output.write(`⚠ No supported package manager found. ${opts.manualHint}\n`);
|
|
77
130
|
return null;
|
|
@@ -90,7 +143,10 @@ export async function ensureBinaryInstalled(
|
|
|
90
143
|
}
|
|
91
144
|
|
|
92
145
|
/** Default confirm: visible prompt, Enter = yes, non-TTY throws. */
|
|
93
|
-
function defaultConfirm(
|
|
146
|
+
function defaultConfirm(
|
|
147
|
+
opts: EnsureBinaryOptions,
|
|
148
|
+
binary = opts.binary,
|
|
149
|
+
): () => Promise<boolean> {
|
|
94
150
|
return async () => {
|
|
95
151
|
const input = opts.streams?.input ?? process.stdin;
|
|
96
152
|
const output = opts.streams?.output ?? process.stdout;
|
|
@@ -99,7 +155,7 @@ function defaultConfirm(opts: EnsureBinaryOptions): () => Promise<boolean> {
|
|
|
99
155
|
throw new Error("Install confirm requires an interactive terminal.");
|
|
100
156
|
}
|
|
101
157
|
const answer = (
|
|
102
|
-
await readLineText(`Install ${
|
|
158
|
+
await readLineText(`Install ${binary} now? [Y/n] `, {
|
|
103
159
|
input,
|
|
104
160
|
output,
|
|
105
161
|
})
|
|
@@ -114,7 +170,10 @@ function defaultConfirm(opts: EnsureBinaryOptions): () => Promise<boolean> {
|
|
|
114
170
|
function runInstallArgv(argv: string[]): boolean {
|
|
115
171
|
const [command, ...args] = argv;
|
|
116
172
|
if (!command) return false;
|
|
117
|
-
const result = spawnSync(command, args, {
|
|
173
|
+
const result = spawnSync(command, args, {
|
|
174
|
+
stdio: "inherit",
|
|
175
|
+
windowsHide: true,
|
|
176
|
+
});
|
|
118
177
|
return !result.error && result.status === 0;
|
|
119
178
|
}
|
|
120
179
|
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
mkdirSync,
|
|
8
8
|
openSync,
|
|
9
9
|
readFileSync,
|
|
10
|
-
rmdirSync,
|
|
11
10
|
statSync,
|
|
12
11
|
unlinkSync,
|
|
13
12
|
writeFileSync,
|
|
@@ -79,27 +78,6 @@ function getGuardianTokenPath(assistantId: string): string {
|
|
|
79
78
|
return guardianTokenPath(resolveConfigDir(process.env), assistantId);
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
/**
|
|
83
|
-
* Best-effort removal of an assistant's stored guardian token (used by
|
|
84
|
-
* `cadet unpair` to forget a paired connection). Never throws if the token
|
|
85
|
-
* file or its per-assistant directory is already absent.
|
|
86
|
-
*/
|
|
87
|
-
export function deleteGuardianToken(assistantId: string): void {
|
|
88
|
-
const tokenPath = getGuardianTokenPath(assistantId);
|
|
89
|
-
try {
|
|
90
|
-
unlinkSync(tokenPath);
|
|
91
|
-
} catch {
|
|
92
|
-
/* already gone */
|
|
93
|
-
}
|
|
94
|
-
// Clean up the now-empty per-assistant directory; rmdir throws if it still
|
|
95
|
-
// holds other files, in which case we leave it.
|
|
96
|
-
try {
|
|
97
|
-
rmdirSync(dirname(tokenPath));
|
|
98
|
-
} catch {
|
|
99
|
-
/* not empty or absent */
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
81
|
function getPersistedDeviceIdPath(): string {
|
|
104
82
|
return join(getConfigDir(getCurrentEnvironment()), "device-id");
|
|
105
83
|
}
|
|
@@ -454,7 +432,7 @@ export async function refreshGuardianTokenResult(
|
|
|
454
432
|
return {
|
|
455
433
|
ok: false,
|
|
456
434
|
status: 401,
|
|
457
|
-
error: "Guardian token expired
|
|
435
|
+
error: "Guardian refresh token expired",
|
|
458
436
|
};
|
|
459
437
|
}
|
|
460
438
|
|
package/src/lib/hatch-local.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { persistRootSigningKey } from "./root-signing-key.js";
|
|
1
2
|
import {
|
|
2
3
|
existsSync,
|
|
3
4
|
lstatSync,
|
|
@@ -52,7 +53,6 @@ import {
|
|
|
52
53
|
import { logHatchNextSteps } from "./hatch-next-steps.js";
|
|
53
54
|
import { printInstanceDetails } from "./instance-details.js";
|
|
54
55
|
import { checkProviderApiKey } from "./api-key-check.js";
|
|
55
|
-
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
56
56
|
import {
|
|
57
57
|
describeLoginError,
|
|
58
58
|
ensureCloudLoginInteractive,
|
|
@@ -156,13 +156,13 @@ export interface HatchLocalParams {
|
|
|
156
156
|
name?: string | null;
|
|
157
157
|
/** Build from a local source tree and hot-reload on change. */
|
|
158
158
|
watch?: boolean;
|
|
159
|
-
/** Stay attached after hatch and poll the gateway until it stops. */
|
|
160
|
-
keepAlive?: boolean;
|
|
161
159
|
/** Hatch-time config values (key → value). */
|
|
162
160
|
configValues?: Record<string, string>;
|
|
163
161
|
/** Extra env vars forwarded into the assistant process. */
|
|
164
162
|
flagEnvVars?: Record<string, string>;
|
|
165
163
|
setupProviderCredentials?: boolean;
|
|
164
|
+
/** Whole-installation state installed before the first service startup. */
|
|
165
|
+
initialArchive?: string;
|
|
166
166
|
/**
|
|
167
167
|
* Show the per-service startup logs (assistant, credential executor,
|
|
168
168
|
* gateway) and the extended next steps. Off by default — the happy path
|
|
@@ -192,13 +192,7 @@ export interface HatchLocalResult {
|
|
|
192
192
|
export async function hatchLocal(
|
|
193
193
|
params: HatchLocalParams = {},
|
|
194
194
|
): Promise<HatchLocalResult> {
|
|
195
|
-
const {
|
|
196
|
-
name,
|
|
197
|
-
watch = false,
|
|
198
|
-
keepAlive = false,
|
|
199
|
-
configValues = {},
|
|
200
|
-
flagEnvVars = {},
|
|
201
|
-
} = params;
|
|
195
|
+
const { name, watch = false, configValues = {}, flagEnvVars = {} } = params;
|
|
202
196
|
const reporter = params.reporter ?? consoleLifecycleReporter;
|
|
203
197
|
const verbose = params.verbose ?? false;
|
|
204
198
|
const setupProviderCredentials = params.setupProviderCredentials !== false;
|
|
@@ -300,7 +294,20 @@ export async function hatchLocal(
|
|
|
300
294
|
const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
|
|
301
295
|
|
|
302
296
|
reporter.progress(3, 6, "Starting assistant...");
|
|
303
|
-
const signingKey =
|
|
297
|
+
const signingKey = params.initialArchive
|
|
298
|
+
? await (
|
|
299
|
+
await import("./root-backup.js")
|
|
300
|
+
).initializeRootArchive(
|
|
301
|
+
{
|
|
302
|
+
assistantId: instanceName,
|
|
303
|
+
cloud: "local",
|
|
304
|
+
runtimeUrl: "",
|
|
305
|
+
resources,
|
|
306
|
+
},
|
|
307
|
+
params.initialArchive,
|
|
308
|
+
)
|
|
309
|
+
: generateLocalSigningKey();
|
|
310
|
+
persistRootSigningKey(resources.instanceDir, signingKey);
|
|
304
311
|
const bootstrapSecret = generateLocalSigningKey();
|
|
305
312
|
let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
306
313
|
try {
|
|
@@ -454,46 +461,5 @@ export async function hatchLocal(
|
|
|
454
461
|
guardianAccessToken,
|
|
455
462
|
};
|
|
456
463
|
|
|
457
|
-
if (keepAlive) {
|
|
458
|
-
const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
459
|
-
const healthTarget = "Gateway";
|
|
460
|
-
const POLL_INTERVAL_MS = 5000;
|
|
461
|
-
const MAX_FAILURES = 3;
|
|
462
|
-
let consecutiveFailures = 0;
|
|
463
|
-
|
|
464
|
-
const shutdown = async (): Promise<void> => {
|
|
465
|
-
reporter.log("\nShutting down local processes...");
|
|
466
|
-
await stopLocalProcesses(resources);
|
|
467
|
-
process.exit(0);
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
process.on("SIGTERM", () => void shutdown());
|
|
471
|
-
process.on("SIGINT", () => void shutdown());
|
|
472
|
-
|
|
473
|
-
// Poll the health endpoint until it stops responding.
|
|
474
|
-
while (true) {
|
|
475
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
476
|
-
try {
|
|
477
|
-
const res = await loopbackSafeFetch(healthUrl, {
|
|
478
|
-
signal: AbortSignal.timeout(3000),
|
|
479
|
-
});
|
|
480
|
-
if (res.ok) {
|
|
481
|
-
consecutiveFailures = 0;
|
|
482
|
-
} else {
|
|
483
|
-
consecutiveFailures++;
|
|
484
|
-
}
|
|
485
|
-
} catch {
|
|
486
|
-
consecutiveFailures++;
|
|
487
|
-
}
|
|
488
|
-
if (consecutiveFailures >= MAX_FAILURES) {
|
|
489
|
-
reporter.log(
|
|
490
|
-
`\n⚠️ ${healthTarget} stopped responding — shutting down.`,
|
|
491
|
-
);
|
|
492
|
-
await stopLocalProcesses(resources);
|
|
493
|
-
process.exit(1);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
464
|
return result;
|
|
499
465
|
}
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
2
|
+
|
|
3
|
+
function cliCommand(): string {
|
|
4
|
+
// Production keeps the bare `cadet` binary; every non-production
|
|
5
|
+
// environment installs as `cadet-<env>` (the same rule as the config and
|
|
6
|
+
// state dirs — see environments/paths.ts).
|
|
7
|
+
const env = getCurrentEnvironment();
|
|
8
|
+
return env.name === "production" ? "cadet" : `cadet-${env.name}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
export function logHatchNextSteps(log: (message: string) => void): void {
|
|
12
|
+
const command = cliCommand();
|
|
2
13
|
log("Next steps:");
|
|
3
|
-
log(" - Run `cadet client` to interact with your cadet via the terminal");
|
|
4
14
|
log(
|
|
5
|
-
|
|
15
|
+
` - Run \`${command} client\` to interact with your cadet via the terminal`,
|
|
16
|
+
);
|
|
17
|
+
log(
|
|
18
|
+
` - Run \`${command} tunnel\` to make your cadet publicly accessible and interact with it via the browser`,
|
|
6
19
|
);
|
|
7
20
|
log("");
|
|
8
21
|
}
|