@mycadet/cli 0.12.11 → 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.
Files changed (86) hide show
  1. package/README.md +13 -0
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/package.json +3 -2
  9. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  10. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  11. package/src/__tests__/backup-ops.test.ts +105 -0
  12. package/src/__tests__/backup.test.ts +114 -673
  13. package/src/__tests__/cloud-releases.test.ts +82 -0
  14. package/src/__tests__/commands-mock.ts +101 -0
  15. package/src/__tests__/debug.test.ts +17 -91
  16. package/src/__tests__/devices.test.ts +2 -2
  17. package/src/__tests__/gateway-token.test.ts +267 -0
  18. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  19. package/src/__tests__/host-teleport.test.ts +405 -0
  20. package/src/__tests__/nginx-ingress.test.ts +182 -23
  21. package/src/__tests__/orphan-detection.test.ts +1 -1
  22. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  23. package/src/__tests__/ps-cloud-status.test.ts +6 -6
  24. package/src/__tests__/root-backup-docker.test.ts +294 -0
  25. package/src/__tests__/root-backup.test.ts +527 -0
  26. package/src/__tests__/root-restore-command.test.ts +147 -0
  27. package/src/__tests__/root-signing-key.test.ts +50 -0
  28. package/src/__tests__/setup.test.ts +3 -3
  29. package/src/__tests__/sleep.test.ts +11 -0
  30. package/src/__tests__/teleport.test.ts +68 -2996
  31. package/src/__tests__/tunnel.test.ts +1 -1
  32. package/src/__tests__/upgrade-local.test.ts +21 -11
  33. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  34. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  35. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  36. package/src/commands/backup.ts +44 -399
  37. package/src/commands/client.ts +3 -3
  38. package/src/commands/completions.ts +0 -2
  39. package/src/commands/debug.ts +75 -131
  40. package/src/commands/devices.ts +51 -17
  41. package/src/commands/gateway/token.ts +77 -23
  42. package/src/commands/pair.ts +2 -2
  43. package/src/commands/restore.ts +77 -444
  44. package/src/commands/retire.ts +2 -3
  45. package/src/commands/sleep.ts +15 -2
  46. package/src/commands/teleport.ts +47 -1559
  47. package/src/commands/upgrade.ts +78 -156
  48. package/src/commands/wake.ts +1 -1
  49. package/src/index.ts +0 -4
  50. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  51. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  52. package/src/lib/__tests__/web-dist.test.ts +27 -1
  53. package/src/lib/assistant-client.ts +1 -1
  54. package/src/lib/assistant-config.ts +0 -18
  55. package/src/lib/backup-ops.ts +37 -251
  56. package/src/lib/cloud-client.ts +10 -13
  57. package/src/lib/cloud-login.ts +2 -1
  58. package/src/lib/cloud-releases.ts +54 -10
  59. package/src/lib/cloud-values.ts +1 -1
  60. package/src/lib/confirm-action.ts +2 -2
  61. package/src/lib/docker.ts +59 -22
  62. package/src/lib/github-repository.ts +2 -0
  63. package/src/lib/guardian-token.ts +1 -23
  64. package/src/lib/hatch-local.ts +17 -1
  65. package/src/lib/host-teleport.ts +506 -0
  66. package/src/lib/http-client.ts +25 -0
  67. package/src/lib/live-voice/tunnel.test.ts +1 -1
  68. package/src/lib/live-voice/tunnel.ts +1 -1
  69. package/src/lib/local.ts +4 -2
  70. package/src/lib/nginx-ingress.ts +134 -18
  71. package/src/lib/process.ts +13 -0
  72. package/src/lib/root-backup.ts +917 -0
  73. package/src/lib/root-signing-key.ts +31 -0
  74. package/src/lib/runtime-url.ts +0 -29
  75. package/src/lib/teleport-backup.ts +3 -16
  76. package/src/lib/upgrade-lifecycle.ts +63 -141
  77. package/src/lib/web-dist.ts +23 -1
  78. package/src/__tests__/bundle-staging.test.ts +0 -268
  79. package/src/__tests__/unpair.test.ts +0 -160
  80. package/src/commands/rollback.ts +0 -479
  81. package/src/commands/unpair.ts +0 -122
  82. package/src/lib/__tests__/job-polling.test.ts +0 -278
  83. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  84. package/src/lib/bundle-staging.ts +0 -220
  85. package/src/lib/job-polling.ts +0 -195
  86. package/src/lib/local-runtime-client.ts +0 -374
package/README.md CHANGED
@@ -14,6 +14,19 @@ bun run ./src/index.ts <command> [options]
14
14
 
15
15
  ## Commands
16
16
 
17
+ ### Version changes: `upgrade`, `restore`
18
+
19
+ Use `cadet upgrade` to move to a newer release. Rollbacks must restore a backup
20
+ through `cadet restore`; version changes and backup data belong to the same
21
+ restore operation. The CLI has no standalone `rollback` command.
22
+
23
+ ```bash
24
+ cadet restore my-assistant --from backup.tar.gz --version v1.2.3
25
+ ```
26
+
27
+ Automatic failure recovery during an upgrade or restore uses the images and
28
+ migration checkpoints captured in memory for that operation.
29
+
17
30
  ### Lifecycle: `ps`, `sleep`, `wake`
18
31
 
19
32
  Day-to-day process management for the assistant and gateway.
@@ -28,10 +28,10 @@ describe("tunnelHostForCloudHost", () => {
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
30
  expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
33
  expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
@@ -11,7 +11,7 @@ export function tunnelHostForCloudHost(host: string): string | null {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
@@ -28,10 +28,10 @@ describe("tunnelHostForCloudHost", () => {
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
30
  expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
33
  expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
@@ -11,7 +11,7 @@ export function tunnelHostForCloudHost(host: string): string | null {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
@@ -28,10 +28,10 @@ describe("tunnelHostForCloudHost", () => {
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
30
  expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
33
  expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
@@ -11,7 +11,7 @@ export function tunnelHostForCloudHost(host: string): string | null {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/cli",
3
- "version": "0.12.11",
3
+ "version": "0.13.0",
4
4
  "description": "CLI tools for mycadet",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "format": "prettier --write .",
22
22
  "format:check": "prettier --check .",
23
- "lint": "eslint --max-warnings 323",
23
+ "lint": "eslint --max-warnings 319",
24
24
  "lint:unused": "knip --include files,dependencies,unlisted",
25
25
  "compile:check": "bun build --compile src/index.ts --outfile /dev/null",
26
26
  "test": "bun run scripts/run-tests.ts",
@@ -41,6 +41,7 @@
41
41
  "qrcode-terminal": "0.12.0",
42
42
  "react": "19.2.6",
43
43
  "react-devtools-core": "7.0.1",
44
+ "tar": "7.5.22",
44
45
  "yaml": "2.8.2",
45
46
  "zod": "4.3.6"
46
47
  },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for AssistantClient's cloud-managed (`cloud: "cadet"`) auth path.
2
+ * Tests for AssistantClient's cloud-managed (`cloud: "cloud"`) auth path.
3
3
  * These entries have no guardian token on disk, so the client must authenticate
4
4
  * with the stored platform credential instead of sending an anonymous request
5
5
  * (which the cloud answers with a bare 403).
@@ -38,7 +38,7 @@ function seedCloud(token?: string): void {
38
38
  assistantId: ASSISTANT_ID,
39
39
  name: "Credence",
40
40
  runtimeUrl: PLATFORM_URL,
41
- cloud: "cadet",
41
+ cloud: "cloud",
42
42
  });
43
43
  if (token) {
44
44
  saveCloudToken(token);
@@ -7,42 +7,44 @@ const STAMP = "2026-09-15T14-27-28-123Z";
7
7
  describe("assistantBackupFilenamePattern", () => {
8
8
  test("matches this CLI's backups for exactly the given assistant", () => {
9
9
  const pattern = assistantBackupFilenamePattern("alpha");
10
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(true);
11
- expect(pattern.test(`alpha-pre-teleport-${STAMP}.vbundle`)).toBe(true);
10
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz`)).toBe(true);
11
+ expect(pattern.test(`alpha-pre-teleport-${STAMP}.tar.gz`)).toBe(true);
12
+ expect(pattern.test(`alpha-pre-rollback-${STAMP}.tar.gz`)).toBe(true);
13
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
12
14
  });
13
15
 
14
16
  test("does not match an assistant whose id extends this one", () => {
15
17
  const pattern = assistantBackupFilenamePattern("alpha");
16
- expect(pattern.test(`alpha-prod-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
17
- expect(pattern.test(`alpha-prod-pre-teleport-${STAMP}.vbundle`)).toBe(
18
+ expect(pattern.test(`alpha-prod-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
19
+ expect(pattern.test(`alpha-prod-pre-teleport-${STAMP}.tar.gz`)).toBe(
18
20
  false,
19
21
  );
20
22
  });
21
23
 
22
24
  test("does not match a shorter id's backups from a longer id", () => {
23
25
  const pattern = assistantBackupFilenamePattern("alpha-prod");
24
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
26
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
25
27
  });
26
28
 
27
29
  test("ignores user-named cadet backup files and other kinds", () => {
28
30
  const pattern = assistantBackupFilenamePattern("alpha");
29
- expect(pattern.test(`alpha-${STAMP}.vbundle`)).toBe(false);
30
- expect(pattern.test(`alpha-manual-${STAMP}.vbundle`)).toBe(false);
31
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle.enc`)).toBe(false);
31
+ expect(pattern.test(`alpha-${STAMP}.tar.gz`)).toBe(false);
32
+ expect(pattern.test(`alpha-manual-${STAMP}.tar.gz`)).toBe(false);
33
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz.enc`)).toBe(false);
32
34
  });
33
35
 
34
36
  test("restricts to the requested kinds", () => {
35
37
  const pattern = assistantBackupFilenamePattern("alpha", ["pre-teleport"]);
36
- expect(pattern.test(`alpha-pre-teleport-${STAMP}.vbundle`)).toBe(true);
37
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
38
+ expect(pattern.test(`alpha-pre-teleport-${STAMP}.tar.gz`)).toBe(true);
39
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
38
40
  expect(
39
- pattern.test(`alpha-pre-teleport-prod-pre-teleport-${STAMP}.vbundle`),
41
+ pattern.test(`alpha-pre-teleport-prod-pre-teleport-${STAMP}.tar.gz`),
40
42
  ).toBe(false);
41
43
  });
42
44
 
43
45
  test("escapes regex metacharacters in ids", () => {
44
46
  const pattern = assistantBackupFilenamePattern("a.b");
45
- expect(pattern.test(`a.b-pre-upgrade-${STAMP}.vbundle`)).toBe(true);
46
- expect(pattern.test(`axb-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
47
+ expect(pattern.test(`a.b-pre-upgrade-${STAMP}.tar.gz`)).toBe(true);
48
+ expect(pattern.test(`axb-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
47
49
  });
48
50
  });
@@ -0,0 +1,105 @@
1
+ import { afterAll, beforeEach, expect, spyOn, test } from "bun:test";
2
+ import {
3
+ mkdir,
4
+ mkdtemp,
5
+ readFile,
6
+ readdir,
7
+ rm,
8
+ writeFile,
9
+ } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import * as config from "../lib/assistant-config.js";
13
+ import * as backups from "../lib/backup-ops.js";
14
+ import * as guardian from "../lib/guardian-token.js";
15
+ import { stageRootArchive, STATE_ROOTS } from "../lib/root-backup.js";
16
+
17
+ const directory = await mkdtemp(join(tmpdir(), "safety-root-backup-"));
18
+ const root = join(directory, "instance");
19
+ const backupDir = join(directory, "backups");
20
+ const entry: config.AssistantEntry = {
21
+ assistantId: "alpha",
22
+ cloud: "local",
23
+ runtimeUrl: "http://127.0.0.1:1",
24
+ resources: {
25
+ instanceDir: root,
26
+ daemonPort: 1,
27
+ gatewayPort: 2,
28
+ cesPort: 3,
29
+ qdrantPort: 4,
30
+ signingKey: "07".repeat(32),
31
+ },
32
+ };
33
+ const lookup = spyOn(config, "findAssistantByName").mockReturnValue(entry);
34
+ const location = spyOn(backups, "getBackupsDir").mockReturnValue(backupDir);
35
+ const save = spyOn(config, "saveAssistantEntry").mockImplementation(() => {});
36
+ const tokens = spyOn(guardian, "loadGuardianToken").mockImplementation(() => {
37
+ throw new Error("no guardian token access allowed");
38
+ });
39
+ beforeEach(async () => {
40
+ for (const name of STATE_ROOTS)
41
+ await mkdir(join(root, name), { recursive: true });
42
+ await writeFile(join(root, "workspace", "notes"), "original");
43
+ await writeFile(join(root, "gateway-security", "trust"), "gateway trust");
44
+ await writeFile(join(root, "ces-security", "keys.enc"), "opaque ciphertext", {
45
+ mode: 0o600,
46
+ });
47
+ await writeFile(join(root, "ces-data", "grants"), "CES policy");
48
+ lookup.mockReturnValue(entry);
49
+ tokens.mockClear();
50
+ });
51
+ afterAll(async () => {
52
+ for (const spy of [lookup, location, save, tokens]) spy.mockRestore();
53
+ await rm(directory, { recursive: true, force: true });
54
+ });
55
+
56
+ test("pre-upgrade safety backup captures all roots in XDG without a guardian token", async () => {
57
+ const path = await backups.createBackup(entry.runtimeUrl, entry.assistantId, {
58
+ prefix: "alpha-pre-upgrade",
59
+ });
60
+ expect(path).toMatch(
61
+ new RegExp(`^${backupDir}/alpha-pre-upgrade-.*\\.tar\\.gz$`),
62
+ );
63
+ const stage = await stageRootArchive(path!, directory);
64
+ for (const name of STATE_ROOTS)
65
+ expect(await readdir(join(stage, "state", name))).not.toHaveLength(0);
66
+ expect(
67
+ await readFile(join(stage, "state", "ces-security", "keys.enc"), "utf8"),
68
+ ).toBe("opaque ciphertext");
69
+ expect(tokens).not.toHaveBeenCalled();
70
+ });
71
+
72
+ test("rollback restores gateway and CES state as well as workspace", async () => {
73
+ const path = await backups.createBackup(entry.runtimeUrl, entry.assistantId, {
74
+ prefix: "alpha-pre-rollback",
75
+ });
76
+ for (const name of STATE_ROOTS)
77
+ await writeFile(join(root, name, "after-upgrade"), "stale");
78
+ await writeFile(join(root, "workspace", "notes"), "modified");
79
+ expect(
80
+ await backups.restoreBackup(entry.runtimeUrl, entry.assistantId, path!, {
81
+ resume: false,
82
+ }),
83
+ ).toBe(true);
84
+ expect(await readFile(join(root, "workspace", "notes"), "utf8")).toBe(
85
+ "original",
86
+ );
87
+ expect(await readFile(join(root, "gateway-security", "trust"), "utf8")).toBe(
88
+ "gateway trust",
89
+ );
90
+ expect(await readFile(join(root, "ces-data", "grants"), "utf8")).toBe(
91
+ "CES policy",
92
+ );
93
+ for (const name of STATE_ROOTS)
94
+ expect(await readdir(join(root, name))).not.toContain("after-upgrade");
95
+ expect(tokens).not.toHaveBeenCalled();
96
+ });
97
+
98
+ test("missing registry identity fails before capture instead of trying runtime export", async () => {
99
+ lookup.mockReturnValue(null);
100
+ expect(await backups.createBackup(entry.runtimeUrl, "missing")).toBeNull();
101
+ expect(
102
+ await backups.restoreBackup(entry.runtimeUrl, "missing", "missing.tar.gz"),
103
+ ).toBe(false);
104
+ expect(tokens).not.toHaveBeenCalled();
105
+ });