@mycadet/cli 0.12.9 → 0.12.10

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.
@@ -39,9 +39,11 @@ 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;
@@ -58,7 +60,12 @@ const dev: EnvironmentDefinition = {
58
60
  webUrl: "https://dev.mycadet.ai",
59
61
  };
60
62
 
61
- const XDG_ENV_VARS = ["XDG_DATA_HOME", "XDG_CONFIG_HOME", "APPDATA"] as const;
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);
@@ -82,18 +82,26 @@ export function getLogDir(env: EnvironmentDefinition): string {
82
82
  }
83
83
 
84
84
  /**
85
- * Path to the interactive CLI's input history file.
85
+ * The interactive CLI's shared state directory — `$XDG_STATE_HOME/cadet`.
86
86
  *
87
- * Follows the XDG Base Directory spec: history files are state data
88
- * (persistent across runs but not portable / user-owned content), so they
89
- * belong under `$XDG_STATE_HOME`, mirroring `bash`, `zsh`, `psql`, and `gh`.
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: terminal input history is per-user, not per-assistant,
93
- * so dev and prod CLIs share the same history file.
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(xdgStateHome(), "cadet", "input-history");
104
+ return join(getSharedStateDir(), "input-history");
97
105
  }
98
106
 
99
107
  /**
@@ -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, to match the silent-fallback
71
- // behavior in assistant/src/util/platform.ts:getXdgCadetConfigDirName,
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`,
@@ -21,6 +21,7 @@ import {
21
21
  setActiveAssistant,
22
22
  } from "./assistant-config.js";
23
23
  import type { AssistantEntry } from "./assistant-config.js";
24
+ import { green, yellow } from "./color.js";
24
25
  import { buildHatchConfigValues, writeInitialConfig } from "./config-utils.js";
25
26
  import {
26
27
  generateLocalSigningKey,
@@ -379,14 +380,18 @@ export async function hatchLocal(
379
380
  ) {
380
381
  const delayMs = 2000 * 2 ** (attempt - 1);
381
382
  reporter.error(
382
- `⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
383
+ yellow(
384
+ `⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
385
+ ),
383
386
  );
384
387
  await new Promise((r) => setTimeout(r, delayMs));
385
388
  } else {
386
389
  reporter.error(
387
- `⚠️ Guardian token lease failed${err instanceof GuardianLeasePermanentError ? ", not retrying — a malformed 200 will not fix itself" : ` after ${maxLeaseAttempts} attempts`}: ${err}\n` +
388
- ` The assistant is running but guardian-token.json was not written.\n` +
389
- ` Run \`cadet restart ${JSON.stringify(instanceName)} --repair-guardian\` to re-lease a token.`,
390
+ yellow(
391
+ `⚠️ Guardian token lease failed${err instanceof GuardianLeasePermanentError ? ", not retrying — a malformed 200 will not fix itself" : ` after ${maxLeaseAttempts} attempts`}: ${err}\n` +
392
+ ` The assistant is running but guardian-token.json was not written.\n` +
393
+ ` Run \`cadet restart ${JSON.stringify(instanceName)} --repair-guardian\` to re-lease a token.`,
394
+ ),
390
395
  );
391
396
  }
392
397
  }
@@ -398,8 +403,10 @@ export async function hatchLocal(
398
403
 
399
404
  if (provider !== undefined && provider !== null && !guardianAccessToken) {
400
405
  reporter.error(
401
- `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
402
- ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
406
+ yellow(
407
+ `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
408
+ ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
409
+ ),
403
410
  );
404
411
  } else if (provider !== undefined) {
405
412
  await configureHatchProviderApiKey({
@@ -429,7 +436,7 @@ export async function hatchLocal(
429
436
  }
430
437
 
431
438
  reporter.log("");
432
- reporter.log(`✅ Local assistant hatched!`);
439
+ reporter.log(green(`✅ Local assistant hatched!`, process.stdout));
433
440
  printInstanceDetails(
434
441
  {
435
442
  name: instanceName,
@@ -438,7 +445,7 @@ export async function hatchLocal(
438
445
  },
439
446
  (message) => reporter.log(message),
440
447
  );
441
- logHatchNextSteps((message) => reporter.log(message), verbose);
448
+ logHatchNextSteps((message) => reporter.log(message));
442
449
 
443
450
  const result: HatchLocalResult = {
444
451
  assistantId: instanceName,
@@ -1,12 +1,8 @@
1
- export function logHatchNextSteps(
2
- log: (message: string) => void,
3
- verbose = false,
4
- ): void {
1
+ export function logHatchNextSteps(log: (message: string) => void): void {
5
2
  log("Next steps:");
6
- log(" cadet client");
7
- if (verbose) {
8
- log(' cadet message "hello"');
9
- log(" cadet events");
10
- }
3
+ log(" - Run `cadet client` to interact with your cadet via the terminal");
4
+ log(
5
+ " - Run `cadet tunnel` to make your cadet publicly accessible and interact with it via the browser",
6
+ );
11
7
  log("");
12
8
  }
package/src/lib/local.ts CHANGED
@@ -984,6 +984,41 @@ function resolveGatewayDir(resources?: LocalInstanceResources): string {
984
984
  }
985
985
  }
986
986
 
987
+ /**
988
+ * How to invoke a `gateway <subcommand>` CLI process so it resolves the same
989
+ * workspace AND gateway security dir as the locally spawned gateway server:
990
+ * run the gateway package's CLI entry through bun with the spawn state root
991
+ * as cwd and the instance-pinned CADET_WORKSPACE_DIR / GATEWAY_SECURITY_DIR
992
+ * the server was given (the file-backed pairing store must resolve
993
+ * identically in both processes). Callers pass the resolved assistant
994
+ * entry's `resources` (undefined for the default instance, where both
995
+ * processes fall back to the same cwd-relative paths).
996
+ */
997
+ export function gatewayCliInvocation(resources?: LocalInstanceResources): {
998
+ command: string;
999
+ args: string[];
1000
+ cwd: string;
1001
+ env: Record<string, string | undefined>;
1002
+ } {
1003
+ return {
1004
+ command: resolveBunExecutable(),
1005
+ args: ["run", join(resolveGatewayDir(resources), "src/cli/index.ts")],
1006
+ cwd: spawnStateRoot(),
1007
+ env: {
1008
+ ...process.env,
1009
+ ...(resources
1010
+ ? {
1011
+ CADET_WORKSPACE_DIR: join(resources.instanceDir, "workspace"),
1012
+ GATEWAY_SECURITY_DIR: join(
1013
+ resources.instanceDir,
1014
+ "gateway-security",
1015
+ ),
1016
+ }
1017
+ : {}),
1018
+ },
1019
+ };
1020
+ }
1021
+
987
1022
  function resolveCesDir(resources?: LocalInstanceResources): string {
988
1023
  const runtimeCesDir = localRuntimeCesDir(resources);
989
1024
  if (runtimeCesDir) return runtimeCesDir;
@@ -1118,9 +1153,7 @@ export async function startCes(
1118
1153
  // Source tree / bunx: run the CES entry point via bun.
1119
1154
  const cesDir = runtimeCesDir ?? resolveCesDir(resources);
1120
1155
  const cesEntry = join(cesDir, "src/main.ts");
1121
- const bunArgs = watch
1122
- ? ["--watch", "run", cesEntry]
1123
- : ["run", cesEntry];
1156
+ const bunArgs = watch ? ["--watch", "run", cesEntry] : ["run", cesEntry];
1124
1157
  const cesLogFd = openLogFile("hatch.log");
1125
1158
  ces = spawn(resolveBunExecutable(), bunArgs, {
1126
1159
  cwd: spawnStateRoot(),
@@ -177,8 +177,6 @@ function remoteWebIngressConfig(
177
177
  return {
178
178
  remote: EDGE_SERVED_REMOTE,
179
179
  apiBaseUrl: "/v1",
180
- cloudDisabled: true,
181
- disableCloud: true,
182
180
  ...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
183
181
  ...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
184
182
  ...(opts.hubUrl ? { hubUrl: opts.hubUrl } : {}),
@@ -191,7 +189,7 @@ function remoteWebIngressConfig(
191
189
  * fingerprint matches, so this must change whenever the generated index or
192
190
  * nginx template does.
193
191
  */
194
- export const EDGE_TEMPLATE_VERSION = 9;
192
+ export const EDGE_TEMPLATE_VERSION = 10;
195
193
 
196
194
  /**
197
195
  * Stable fingerprint of the SPA config injected into the served index and
@@ -112,11 +112,16 @@ export function execWithStdin(
112
112
  export function execOutput(
113
113
  command: string,
114
114
  args: string[],
115
- options: { cwd?: string; timeoutMs?: number } = {},
115
+ options: {
116
+ cwd?: string;
117
+ timeoutMs?: number;
118
+ env?: Record<string, string | undefined>;
119
+ } = {},
116
120
  ): Promise<string> {
117
121
  return new Promise((resolve, reject) => {
118
122
  const child = spawn(command, args, {
119
123
  cwd: options.cwd,
124
+ env: options.env,
120
125
  stdio: ["pipe", "pipe", "pipe"],
121
126
  windowsHide: true,
122
127
  });