nexos-connect 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -1,66 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/main.ts
4
- import { spawnSync } from "child_process";
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
5
  import { createRequire } from "module";
6
6
  import os from "os";
7
- import path21 from "path";
7
+ import path25 from "path";
8
8
  import crossSpawn from "cross-spawn";
9
- import pc12 from "picocolors";
10
- import * as v12 from "valibot";
11
-
12
- // src/cli/classify-failure.ts
13
- import { match } from "ts-pattern";
14
-
15
- // src/shared/text/describe-error.ts
16
- function describeError(error) {
17
- return error instanceof Error ? error.message : String(error);
18
- }
19
-
20
- // src/ui/cancelled-error.ts
21
- import * as clack from "@clack/prompts";
22
- var CancelledError = class extends Error {
23
- constructor() {
24
- super("cancelled by the user");
25
- this.name = "CancelledError";
26
- }
27
- };
28
- async function throwIfCancelled(pending) {
29
- const value = await pending;
30
- if (clack.isCancel(value)) throw new CancelledError();
31
- return value;
32
- }
33
-
34
- // src/cli/exit-code.ts
35
- var EXIT_CODE = { ok: 0, failed: 1, usage: 2, cancelled: 130 };
36
-
37
- // src/cli/usage-error.ts
38
- var UsageError = class extends Error {
39
- name = "UsageError";
40
- };
41
-
42
- // src/cli/classify-failure.ts
43
- var UNEXPECTED_NEXT_STEP = "Run again with --dry-run to see what would change. If this keeps happening, it is a bug \u2014 please report it.";
44
- function classifyFailure(error, masker2) {
45
- if (error instanceof CancelledError) return { kind: "cancelled" };
46
- const message = masker2.maskText(describeError(error));
47
- if (error instanceof UsageError) return { kind: "usage", message };
48
- return { kind: "unexpected", message: `${message}
49
- ${UNEXPECTED_NEXT_STEP}` };
50
- }
51
- function exitCodeForFailure(failure) {
52
- return match(failure.kind).with("usage", () => EXIT_CODE.usage).with("cancelled", () => EXIT_CODE.cancelled).with("unexpected", () => EXIT_CODE.failed).exhaustive();
53
- }
9
+ import pc14 from "picocolors";
10
+ import * as v16 from "valibot";
54
11
 
55
12
  // src/cli/environment.ts
56
13
  import * as v from "valibot";
57
14
 
58
- // src/product/gateway/gateway-errors.ts
59
- import { match as match2 } from "ts-pattern";
60
-
61
15
  // src/product/gateway/desired-config.ts
62
16
  var GATEWAY_HOST = "https://api.nexos.ai";
17
+ var GATEWAY_ENV_VARIABLE = "NEXOS_BASE_URL";
63
18
  var API_KEYS_URL = "https://workspace.nexos.ai/gateway/api-keys";
19
+ var TRAILING_SLASHES = /\/+$/;
20
+ var LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
21
+ function isAllowedGatewayHost(host) {
22
+ if (!URL.canParse(host)) return false;
23
+ const { protocol, hostname, pathname } = new URL(host);
24
+ if (pathname !== "/" && pathname !== "") return false;
25
+ return protocol === "https:" || protocol === "http:" && LOOPBACK_HOSTNAMES.includes(hostname);
26
+ }
27
+ function resolveGatewayHost(env) {
28
+ const configured = env[GATEWAY_ENV_VARIABLE]?.trim().replace(TRAILING_SLASHES, "") ?? "";
29
+ return isAllowedGatewayHost(configured) ? configured : GATEWAY_HOST;
30
+ }
64
31
  var OPENAI_PATH = "/v1";
65
32
  function buildGatewayUrls(host) {
66
33
  return { anthropicBaseUrl: host, openaiBaseUrl: `${host}${OPENAI_PATH}` };
@@ -70,6 +37,7 @@ function buildDesiredConfig({ gatewayUrls, apiKey, model }) {
70
37
  }
71
38
 
72
39
  // src/product/gateway/gateway-errors.ts
40
+ import { match } from "ts-pattern";
73
41
  var GatewayUnreachableError = class extends Error {
74
42
  kind = "unreachable";
75
43
  endpoint;
@@ -97,7 +65,7 @@ function isGatewayFailure(error) {
97
65
  }
98
66
  var NETWORK_NEXT_STEP = "check your network or proxy, or rerun with --no-verify";
99
67
  function describeGatewayFailure(failure, masker2) {
100
- return match2(failure).with({ kind: "unreachable" }, ({ reason }) => describeUnreachable(reason, masker2)).with(
68
+ return match(failure).with({ kind: "unreachable" }, ({ reason }) => describeUnreachable(reason, masker2)).with(
101
69
  { kind: "timed-out" },
102
70
  ({ timeoutMs }) => `no response within ${formatSeconds(timeoutMs)} \u2014 ${NETWORK_NEXT_STEP}`
103
71
  ).exhaustive();
@@ -110,7 +78,7 @@ function formatHttpFailure(status, explanation, masker2) {
110
78
  var REDIRECT_DESCRIPTION = "the gateway answered with a redirect, which is refused \u2014 check for a proxy or captive portal, or rerun with --no-verify";
111
79
  var INVALID_HEADER_DESCRIPTION = `the key holds a character an HTTP header cannot carry, so it was not sent \u2014 copy it again from ${API_KEYS_URL}`;
112
80
  function describeUnreachable(reason, masker2) {
113
- return match2(reason).with(
81
+ return match(reason).with(
114
82
  { kind: "network" },
115
83
  ({ causeCode, causeMessage }) => masker2.maskText(`could not connect (${causeCode ?? causeMessage}) \u2014 ${NETWORK_NEXT_STEP}`)
116
84
  ).with({ kind: "redirect" }, () => REDIRECT_DESCRIPTION).with({ kind: "invalid-header" }, () => INVALID_HEADER_DESCRIPTION).exhaustive();
@@ -224,10 +192,14 @@ function readNonBlank(value) {
224
192
  return trimmed === "" ? void 0 : trimmed;
225
193
  }
226
194
 
195
+ // src/cli/errors/usage-error.ts
196
+ var UsageError = class extends Error {
197
+ name = "UsageError";
198
+ };
199
+
227
200
  // src/cli/environment.ts
228
201
  var KEY_ENV_VARIABLE = "NEXOS_API_KEY";
229
202
  var MODEL_ENV_VARIABLE = "NEXOS_MODEL";
230
- var GATEWAY_ENV_VARIABLE = "NEXOS_BASE_URL";
231
203
  var BlankAsAbsentSchema = v.pipe(v.optional(v.string()), v.transform(readNonBlank));
232
204
  var ModelSchema = v.pipe(
233
205
  BlankAsAbsentSchema,
@@ -254,21 +226,58 @@ function parseEnvironmentKey(key) {
254
226
  return key;
255
227
  }
256
228
  var UNSET_HINT = "Unset it to use the nexos.ai gateway.";
257
- var LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
258
229
  function parseGatewayHost(value) {
259
230
  if (value === void 0) return void 0;
260
231
  const host = trimTrailingSlashes(value);
261
232
  if (!URL.canParse(host)) throw new UsageError(`$${GATEWAY_ENV_VARIABLE} is not a URL. ${UNSET_HINT}`);
262
- const { protocol, hostname } = new URL(host);
263
- const isEncrypted = protocol === "https:";
264
- const isLocalPlainHttp = protocol === "http:" && LOOPBACK_HOSTNAMES.includes(hostname);
265
- if (isEncrypted || isLocalPlainHttp) return host;
266
- throw new UsageError(`$${GATEWAY_ENV_VARIABLE} must be an https:// URL, or http:// on this machine. ${UNSET_HINT}`);
233
+ if (!isAllowedGatewayHost(host)) {
234
+ throw new UsageError(`$${GATEWAY_ENV_VARIABLE} must be an https:// URL, or http:// on this machine. ${UNSET_HINT}`);
235
+ }
236
+ return host;
237
+ }
238
+
239
+ // src/cli/errors/classify-failure.ts
240
+ import { match as match2 } from "ts-pattern";
241
+
242
+ // src/shared/text/describe-error.ts
243
+ function describeError(error) {
244
+ return error instanceof Error ? error.message : String(error);
245
+ }
246
+
247
+ // src/ui/terminal/cancelled-error.ts
248
+ import * as clack from "@clack/prompts";
249
+ var CancelledError = class extends Error {
250
+ constructor() {
251
+ super("cancelled by the user");
252
+ this.name = "CancelledError";
253
+ }
254
+ };
255
+ async function throwIfCancelled(pending) {
256
+ const value = await pending;
257
+ if (clack.isCancel(value)) throw new CancelledError();
258
+ return value;
259
+ }
260
+
261
+ // src/cli/exit-code.ts
262
+ var EXIT_CODE = { ok: 0, failed: 1, usage: 2, cancelled: 130 };
263
+
264
+ // src/cli/errors/classify-failure.ts
265
+ var UNEXPECTED_NEXT_STEP = "Run again with --dry-run to see what would change. If this keeps happening, it is a bug \u2014 please report it.";
266
+ function classifyFailure(error, masker2) {
267
+ if (error instanceof CancelledError) return { kind: "cancelled" };
268
+ const message = masker2.maskText(describeError(error));
269
+ if (error instanceof UsageError) return { kind: "usage", message };
270
+ return { kind: "unexpected", message: `${message}
271
+ ${UNEXPECTED_NEXT_STEP}` };
272
+ }
273
+ function exitCodeForFailure(failure) {
274
+ return match2(failure.kind).with("usage", () => EXIT_CODE.usage).with("cancelled", () => EXIT_CODE.cancelled).with("unexpected", () => EXIT_CODE.failed).exhaustive();
267
275
  }
268
276
 
269
277
  // src/cli/help.ts
270
278
  var COMMAND_NAME = "nexos-connect";
271
279
  var RESTORE_COMMAND = "restore";
280
+ var STATUS_COMMAND = "status";
272
281
  var TAGLINE = `${COMMAND_NAME} \u2014 point your coding agents at the nexos.ai Gateway`;
273
282
  function renderHelp({
274
283
  harnessIds,
@@ -285,6 +294,7 @@ Usage
285
294
  npx ${COMMAND_NAME} <agent> [options] [-- <agent arguments>]
286
295
  npx ${COMMAND_NAME} forget-key
287
296
  npx ${COMMAND_NAME} ${RESTORE_COMMAND} [options] put back what it changed in each agent's config
297
+ npx ${COMMAND_NAME} ${STATUS_COMMAND} [options] report what is configured, and whether it still matches
288
298
 
289
299
  <agent> starts one agent once, pointed at the gateway, without changing its config.
290
300
  One of: ${launchBinaries.join(", ")}.
@@ -296,6 +306,9 @@ ${RESTORE_COMMAND} puts every key ${COMMAND_NAME} set back the way it was before
296
306
  setup, and keeps everything added since. It takes --harnesses, --scope, --yes,
297
307
  --dry-run and --backup-dir; --yes restores every agent it has a record of.
298
308
 
309
+ ${STATUS_COMMAND} is read-only: it reports the key, the env file, the backup folders and every
310
+ agent, at user and project scope. It takes --harnesses, --verify and --json.
311
+
299
312
  Options
300
313
  --harnesses <ids> Comma-separated agent ids; skips the picker.
301
314
  One of: ${harnessIds.join(", ")}
@@ -308,6 +321,9 @@ Options
308
321
  the picker: ${predefinedModels}, or any id from GET /v1/models.
309
322
  --save-model Save the model as the default of each agent it is used for,
310
323
  without asking. Never on a dry run.
324
+ --install Install a not-installed agent with npm. Setup installs the
325
+ ones you pick in the list; starting an agent installs the
326
+ named one first. --yes never installs.
311
327
  --scope <scope> Where to write: user (the default \u2014 your home directory,
312
328
  applying everywhere) or project (this directory only).
313
329
  -y, --yes Answer every question with its default: user scope, the
@@ -321,16 +337,19 @@ Options
321
337
  then shows no model list; ${predefinedModels} is not offered.
322
338
  --backup-dir <path> Where originals are copied, in a <timestamp> folder.
323
339
  Default: ${backupDirectories.user} (user scope), ${backupDirectories.project} (project).
340
+ --verify With ${STATUS_COMMAND}: check the resolved key against the gateway once.
341
+ --json With ${STATUS_COMMAND}: print the report as one JSON object.
324
342
  -h, --help Show this help.
325
343
  -v, --version Show the version.
326
344
 
327
- Starting an agent takes --key, --model, --save-key, --save-model, --no-verify
328
- and --yes only. Its model is --model, else $${MODEL_ENV_VARIABLE}, else the agent's saved default, else
345
+ Starting an agent takes --key, --model, --save-key, --save-model, --install,
346
+ --no-verify and --yes only. Its model is --model, else $${MODEL_ENV_VARIABLE}, else the agent's saved default, else
329
347
  the picker (--yes: ${defaultModel}).
330
348
 
331
349
  Exit codes
332
350
  ${EXIT_CODE.ok} done
333
- ${EXIT_CODE.failed} an agent failed, while planning or while writing; or an agent could not start
351
+ ${EXIT_CODE.failed} an agent failed, while planning or while writing; or an agent could not start.
352
+ With ${STATUS_COMMAND}: an agent drifted, a config could not be read, or --verify rejected the key.
334
353
  ${EXIT_CODE.usage} usage error, or a question asked with no terminal to answer it
335
354
  ${EXIT_CODE.cancelled} cancelled, by Ctrl-C or by declining a confirmation
336
355
  Once an agent has started, its own exit code passes through unchanged.
@@ -393,13 +412,13 @@ function buildIdentityMatching(secrets) {
393
412
  if (masksByForm.size === 0) return () => [];
394
413
  const alternatives = [...masksByForm.keys()].sort((first, second) => second.length - first.length);
395
414
  const pattern = new RegExp(alternatives.map(buildFormPattern).join("|"), "g");
396
- return (text2) => [...text2.matchAll(pattern)].map((match25) => toSpan(match25, masksByForm.get(match25[0]) ?? match25[0]));
415
+ return (text2) => [...text2.matchAll(pattern)].map((match29) => toSpan(match29, masksByForm.get(match29[0]) ?? match29[0]));
397
416
  }
398
417
  function findShapeSpans(text2) {
399
- return [...text2.matchAll(KEY_SHAPE_PATTERN)].map((match25) => toSpan(match25, maskKey(match25[0])));
418
+ return [...text2.matchAll(KEY_SHAPE_PATTERN)].map((match29) => toSpan(match29, maskKey(match29[0])));
400
419
  }
401
- function toSpan(match25, mask) {
402
- return { start: match25.index, end: match25.index + match25[0].length, mask };
420
+ function toSpan(match29, mask) {
421
+ return { start: match29.index, end: match29.index + match29[0].length, mask };
403
422
  }
404
423
  function pickOuterSpans(spans) {
405
424
  const picked = [];
@@ -451,7 +470,7 @@ import { match as match3 } from "ts-pattern";
451
470
  // src/shared/file-system/is-file.ts
452
471
  import fs from "fs";
453
472
 
454
- // src/shared/file-system/is-missing-file-error.ts
473
+ // src/shared/file-system/errors/is-missing-file-error.ts
455
474
  var MISSING_FILE_CODES = ["ENOENT", "ENOTDIR"];
456
475
  function isMissingFileError(error) {
457
476
  return error instanceof Error && "code" in error && MISSING_FILE_CODES.includes(error.code);
@@ -520,15 +539,18 @@ function parseCliArguments(argv) {
520
539
  const { values, tokens } = parseFlags(argv);
521
540
  return {
522
541
  command: commandFrom(tokens, values),
523
- harnessIds: parseHarnessIds(values.harnesses),
542
+ harnessIds: parseHarnessIds(values.harnesses ?? values.harness),
524
543
  key: values.key === void 0 ? void 0 : parseKeyFlag(values.key),
525
544
  model: parseModelFlag(values.model),
526
545
  scope: parseScope(values.scope),
527
546
  shouldAssumeYes: values.yes === true,
528
547
  shouldSaveKey: values["save-key"] === true,
529
548
  shouldSaveModel: values["save-model"] === true,
549
+ shouldInstall: values.install === true,
530
550
  isDryRun: values["dry-run"] === true,
531
551
  shouldVerify: values["no-verify"] !== true,
552
+ shouldVerifyStatus: values.verify === true,
553
+ isJson: values.json === true,
532
554
  backupDirectory: readNonBlank(values["backup-dir"]),
533
555
  shouldShowHelp: values.help === true,
534
556
  shouldShowVersion: values.version === true
@@ -536,14 +558,18 @@ function parseCliArguments(argv) {
536
558
  }
537
559
  var FLAGS = {
538
560
  harnesses: { type: "string" },
561
+ harness: { type: "string" },
539
562
  key: { type: "string" },
540
563
  model: { type: "string" },
541
564
  scope: { type: "string" },
542
565
  yes: { type: "boolean", short: "y" },
543
566
  "save-key": { type: "boolean" },
544
567
  "save-model": { type: "boolean" },
568
+ install: { type: "boolean" },
545
569
  "dry-run": { type: "boolean" },
546
570
  "no-verify": { type: "boolean" },
571
+ verify: { type: "boolean" },
572
+ json: { type: "boolean" },
547
573
  "backup-dir": { type: "string" },
548
574
  help: { type: "boolean", short: "h" },
549
575
  version: { type: "boolean", short: "v" }
@@ -589,6 +615,7 @@ function commandFrom(tokens, values) {
589
615
  if (positionals.length > 1) throw new UsageError(describeExtraCommands(positionals));
590
616
  const [name] = positionals;
591
617
  if (name === void 0 || name === FORGET_KEY_COMMAND) return commandWithoutAgent({ name, passthrough, values });
618
+ if (name === STATUS_COMMAND) return statusCommand({ passthrough, values });
592
619
  if (name === RESTORE_COMMAND) return restoreCommand({ name, passthrough, values });
593
620
  return launchCommand({ name, passthrough, values });
594
621
  }
@@ -599,29 +626,57 @@ function describeExtraCommands(positionals) {
599
626
  }
600
627
  function commandWithoutAgent({ name, passthrough, values }) {
601
628
  if (passthrough !== null) throw new UsageError(PASSTHROUGH_MISUSE);
602
- if (name === void 0) return { kind: "setup" };
629
+ if (name === void 0) {
630
+ assertNotStatusFlags(values);
631
+ return { kind: "setup" };
632
+ }
603
633
  const flags = Object.keys(values).filter((flag) => flag !== "help" && flag !== "version");
604
634
  if (flags.length > 0) throw new UsageError(`${FORGET_KEY_COMMAND} takes no options (got --${flags[0]}).`);
605
635
  return { kind: "forget-key" };
606
636
  }
637
+ var STATUS_UNSUPPORTED_FLAGS = [
638
+ "key",
639
+ "model",
640
+ "scope",
641
+ "yes",
642
+ "save-key",
643
+ "save-model",
644
+ "install",
645
+ "dry-run",
646
+ "no-verify",
647
+ "backup-dir"
648
+ ];
649
+ function statusCommand({ passthrough, values }) {
650
+ if (passthrough !== null) throw new UsageError(PASSTHROUGH_MISUSE);
651
+ const unsupported = STATUS_UNSUPPORTED_FLAGS.find((flag) => flag in values);
652
+ if (unsupported !== void 0) throw new UsageError(`--${unsupported} does not apply to ${STATUS_COMMAND}.`);
653
+ return { kind: "status" };
654
+ }
655
+ var STATUS_ONLY_FLAGS = ["verify", "json"];
656
+ function assertNotStatusFlags(values) {
657
+ const flag = STATUS_ONLY_FLAGS.find((name) => values[name] === true);
658
+ if (flag !== void 0) throw new UsageError(`--${flag} is for ${STATUS_COMMAND} only.`);
659
+ }
607
660
  function restoreCommand({ passthrough, values }) {
608
661
  if (passthrough !== null) throw new UsageError(PASSTHROUGH_MISUSE);
609
662
  assertRestoreFlags(values);
610
663
  return { kind: "restore" };
611
664
  }
612
- var LAUNCH_UNSUPPORTED_FLAGS = ["harnesses", "scope", "dry-run", "backup-dir"];
665
+ var LAUNCH_UNSUPPORTED_FLAGS = ["harnesses", "harness", "scope", "dry-run", "backup-dir"];
613
666
  function launchCommand({ name, passthrough, values }) {
667
+ assertNotStatusFlags(values);
614
668
  const unsupported = LAUNCH_UNSUPPORTED_FLAGS.filter((flag) => flag in values);
615
669
  if (unsupported.length > 0) {
616
670
  const shown = isAgentName(name) ? name : "<agent>";
617
671
  throw new UsageError(
618
- `--${unsupported[0]} is for setup only. npx ${COMMAND_NAME} ${shown} takes --key, --model, --save-key, --save-model, --no-verify and --yes.`
672
+ `--${unsupported[0]} is for setup only. npx ${COMMAND_NAME} ${shown} takes --key, --model, --save-key, --save-model, --install, --no-verify and --yes.`
619
673
  );
620
674
  }
621
675
  return { kind: "launch", binary: name, passthrough: passthrough ?? [] };
622
676
  }
623
- var RESTORE_UNSUPPORTED_FLAGS = ["key", "model", "save-model", "no-verify"];
677
+ var RESTORE_UNSUPPORTED_FLAGS = ["key", "model", "save-model", "no-verify", "install"];
624
678
  function assertRestoreFlags(flags) {
679
+ assertNotStatusFlags(flags);
625
680
  const unsupported = RESTORE_UNSUPPORTED_FLAGS.find((name) => flags[name] !== void 0);
626
681
  if (unsupported !== void 0) throw new UsageError(`--${unsupported} does not apply to ${RESTORE_COMMAND}.`);
627
682
  }
@@ -715,8 +770,50 @@ function refuse({ question, answers }) {
715
770
  };
716
771
  }
717
772
 
773
+ // src/flows/install/install-harness.ts
774
+ var NPM = "npm";
775
+ var NPM_INSTALL_ARGUMENTS = ["install", "--global", "--no-audit", "--no-fund"];
776
+ async function installHarness(params) {
777
+ const { label, binary, packageName, shouldInstall, shouldAssumeYes, ui, ctx, runNpm: runNpm2 } = params;
778
+ if (!shouldInstall && !shouldAssumeYes && !ctx.isInteractive) {
779
+ throw new UsageError(`Cannot install ${label} without a terminal. Pass --install.`);
780
+ }
781
+ if (!shouldInstall) {
782
+ if (shouldAssumeYes) return { kind: "skipped" };
783
+ if (!await ui.confirm(askToInstall({ label, packageName }), false)) return { kind: "declined" };
784
+ }
785
+ if (!ctx.hasBinary(NPM)) return { kind: "failed", message: describeNpmMissing({ label, packageName }) };
786
+ const result = await runNpm2([...NPM_INSTALL_ARGUMENTS, packageName]);
787
+ if (result.kind === "could-not-start") {
788
+ return { kind: "failed", message: describeNpmNotStarted({ label, packageName }) };
789
+ }
790
+ if (result.code !== 0) {
791
+ return { kind: "failed", message: describeNpmFailure({ label, packageName, exitCode: result.code }) };
792
+ }
793
+ if (!ctx.hasBinary(binary)) return { kind: "not-on-path", packageName };
794
+ return { kind: "installed" };
795
+ }
796
+ function describeMissingBinary({ label, binary }) {
797
+ return `${binary} not found on PATH. Install ${label} first.`;
798
+ }
799
+ function describeNotOnPath({ label, binary }) {
800
+ return `Installed ${label}, but ${binary} is still not on PATH. Check npm's global bin is on PATH (npm prefix -g).`;
801
+ }
802
+ function askToInstall({ label, packageName }) {
803
+ return `${label} isn't on your PATH. Install it now? (npm install -g ${packageName})`;
804
+ }
805
+ function describeNpmMissing({ label, packageName }) {
806
+ return `Couldn't install ${label}: npm was not found on PATH. Install it yourself: npm install -g ${packageName}.`;
807
+ }
808
+ function describeNpmNotStarted({ label, packageName }) {
809
+ return `Couldn't install ${label}: npm could not be started. Install it yourself: npm install -g ${packageName}.`;
810
+ }
811
+ function describeNpmFailure({ label, packageName, exitCode }) {
812
+ return `Couldn't install ${label} (npm exited ${exitCode}). Install it yourself: npm install -g ${packageName}.`;
813
+ }
814
+
718
815
  // src/flows/key/find-existing-keys.ts
719
- import * as v6 from "valibot";
816
+ import * as v9 from "valibot";
720
817
 
721
818
  // src/harnesses/claude-code/claude-code.ts
722
819
  import path3 from "path";
@@ -740,7 +837,7 @@ function findFirstExistingFile(candidates) {
740
837
  return candidates.find((candidate) => isFile(candidate)) ?? null;
741
838
  }
742
839
 
743
- // src/harnesses/detect-config-file.ts
840
+ // src/harnesses/config/detect-config-file.ts
744
841
  function detectConfigFile(candidates, home) {
745
842
  const existing = findFirstExistingFile(candidates);
746
843
  if (existing === null) {
@@ -750,7 +847,29 @@ function detectConfigFile(candidates, home) {
750
847
  return { isFound: true, detail: displayPath(existing, home) };
751
848
  }
752
849
 
753
- // src/harnesses/read-jsonc-file.ts
850
+ // src/harnesses/config/detect-config-or-binary.ts
851
+ function detectConfigOrBinary({ candidates, binary, ctx }) {
852
+ const detected = detectConfigFile(candidates, ctx.home);
853
+ if (detected.isFound || ctx.scope === "project" || !ctx.hasBinary(binary)) return detected;
854
+ return { isFound: true, detail: `${binary} found on PATH` };
855
+ }
856
+
857
+ // src/harnesses/config/is-gateway-url.ts
858
+ function isGatewayUrl(url, gatewayHost) {
859
+ const parsed = parseUrl(url);
860
+ const gateway = parseUrl(gatewayHost);
861
+ if (parsed === void 0 || gateway === void 0) return false;
862
+ return parsed.origin === gateway.origin;
863
+ }
864
+ function parseUrl(value) {
865
+ if (value === void 0 || !URL.canParse(value)) return void 0;
866
+ return new URL(value);
867
+ }
868
+ function gatewayHostOf(ctx) {
869
+ return resolveGatewayHost(ctx.env);
870
+ }
871
+
872
+ // src/harnesses/config/read-jsonc-file.ts
754
873
  import * as v3 from "valibot";
755
874
 
756
875
  // src/shared/file-formats/config-errors.ts
@@ -821,7 +940,7 @@ function describeLocation(keyPath) {
821
940
  return `"${keyPath.join(".")}"`;
822
941
  }
823
942
 
824
- // src/shared/file-formats/jsonc.ts
943
+ // src/shared/file-formats/json/jsonc.ts
825
944
  import {
826
945
  applyEdits,
827
946
  findNodeAtLocation,
@@ -940,7 +1059,7 @@ function splitByteOrderMark(text2) {
940
1059
  return { byteOrderMark: BYTE_ORDER_MARK, body: text2.slice(BYTE_ORDER_MARK.length) };
941
1060
  }
942
1061
 
943
- // src/shared/file-formats/jsonc.ts
1062
+ // src/shared/file-formats/json/jsonc.ts
944
1063
  var PARSE_OPTIONS = { allowTrailingComma: true, disallowComments: false };
945
1064
  function parseJsonc(text2) {
946
1065
  const { body } = splitByteOrderMark(text2 ?? "");
@@ -973,7 +1092,7 @@ function renderJsonc(current, updates) {
973
1092
  (text2, [keyPath, value]) => setValue({ text: text2, keyPath, value, formattingOptions }),
974
1093
  body.trim() === "" ? EMPTY_DOCUMENT : body
975
1094
  );
976
- return byteOrderMark + endWithLineEnding(edited, formattingOptions.eol);
1095
+ return byteOrderMark + matchFinalLineEnding(edited, formattingOptions.eol, isNewOrEndsWithLineEnding(body));
977
1096
  }
978
1097
  function readJsoncOriginals(current, keyPaths) {
979
1098
  const { body } = splitByteOrderMark(current ?? "");
@@ -1039,8 +1158,12 @@ function detectFormatting(text2) {
1039
1158
  eol: detectLineEnding(text2) ?? DEFAULT_LINE_ENDING
1040
1159
  };
1041
1160
  }
1042
- function endWithLineEnding(text2, lineEnding) {
1043
- return text2.endsWith("\n") ? text2 : text2 + lineEnding;
1161
+ function isNewOrEndsWithLineEnding(body) {
1162
+ return body.trim() === "" || body.endsWith("\n");
1163
+ }
1164
+ function matchFinalLineEnding(text2, lineEnding, shouldEndWithLineEnding) {
1165
+ if (shouldEndWithLineEnding) return text2.endsWith("\n") ? text2 : text2 + lineEnding;
1166
+ return text2.replace(/\r?\n$/, "");
1044
1167
  }
1045
1168
  function findNode(text2, keyPath) {
1046
1169
  const root = parseTree(text2, [], PARSE_OPTIONS);
@@ -1072,7 +1195,7 @@ function setValue({ text: text2, keyPath, value, formattingOptions }) {
1072
1195
  // src/shared/file-system/read-file-if-exists.ts
1073
1196
  import fs2 from "fs";
1074
1197
 
1075
- // src/shared/file-system/utf16-file-error.ts
1198
+ // src/shared/file-system/errors/utf16-file-error.ts
1076
1199
  var Utf16FileError = class extends Error {
1077
1200
  filePath;
1078
1201
  constructor(filePath) {
@@ -1106,7 +1229,7 @@ function hasUtf16ByteOrderMark(bytes) {
1106
1229
  return UTF16_BYTE_ORDER_MARKS.some((byteOrderMark) => bytes.subarray(0, byteOrderMark.length).equals(byteOrderMark));
1107
1230
  }
1108
1231
 
1109
- // src/harnesses/read-jsonc-file.ts
1232
+ // src/harnesses/config/read-jsonc-file.ts
1110
1233
  function readJsoncFile(filePath, schema) {
1111
1234
  return parseJsoncConfig(readFileIfExists(filePath), schema);
1112
1235
  }
@@ -1128,11 +1251,13 @@ var claudeCode = {
1128
1251
  id: "claude-code",
1129
1252
  label: "Claude Code",
1130
1253
  binary: BINARY,
1254
+ installPackage: "@anthropic-ai/claude-code",
1131
1255
  protocols: ["anthropic"],
1132
1256
  projectSupport: { kind: "supported" },
1133
- detect: (ctx) => detectConfigFile(listSettingsCandidates(ctx), ctx.home),
1257
+ detect: (ctx) => detectConfigOrBinary({ candidates: listSettingsCandidates(ctx), binary: BINARY, ctx }),
1134
1258
  configPath: (ctx) => resolveSettingsPath(ctx),
1135
1259
  plan: (desired, ctx) => [planSettings(desired, ctx)],
1260
+ inspect: (ctx) => inspectSettings(ctx),
1136
1261
  launch: (desired) => ({
1137
1262
  binary: BINARY,
1138
1263
  arguments: ["--settings", SETTINGS_PLACEHOLDER],
@@ -1225,7 +1350,7 @@ function planSettings(desired, ctx) {
1225
1350
  }
1226
1351
  function readGatewayToken(ctx) {
1227
1352
  const env = readSettingsEnv(ctx);
1228
- if (!isGatewayUrl(env?.[BASE_URL_VARIABLE])) return void 0;
1353
+ if (!isGatewayUrl(env?.[BASE_URL_VARIABLE], gatewayHostOf(ctx))) return void 0;
1229
1354
  return env?.[AUTH_TOKEN_VARIABLE];
1230
1355
  }
1231
1356
  function readCredentials(ctx) {
@@ -1246,19 +1371,29 @@ var SettingsSchema = v4.looseObject({
1246
1371
  v4.looseObject({
1247
1372
  [BASE_URL_VARIABLE]: OptionalTextSchema,
1248
1373
  [AUTH_TOKEN_VARIABLE]: OptionalTextSchema,
1249
- [API_KEY_VARIABLE]: OptionalTextSchema
1374
+ [API_KEY_VARIABLE]: OptionalTextSchema,
1375
+ [MODEL_VARIABLE]: OptionalTextSchema
1250
1376
  })
1251
1377
  ),
1252
1378
  void 0
1253
- )
1379
+ ),
1380
+ [MODEL_SETTING]: OptionalTextSchema
1254
1381
  });
1255
1382
  function readSettingsEnv(ctx) {
1256
1383
  const settings2 = readJsoncFile(resolveSettingsPath(ctx), SettingsSchema);
1257
1384
  return settings2.kind === "valid" ? settings2.value[ENV_BLOCK] : void 0;
1258
1385
  }
1259
- var GATEWAY_HOSTNAME = new URL(GATEWAY_HOST).hostname;
1260
- function isGatewayUrl(url) {
1261
- return url !== void 0 && URL.canParse(url) && new URL(url).hostname === GATEWAY_HOSTNAME;
1386
+ function inspectSettings(ctx) {
1387
+ const settings2 = readJsoncFile(resolveSettingsPath(ctx), SettingsSchema);
1388
+ if (settings2.kind === "absent") return { isConfigured: false, model: void 0, isUnreadable: false };
1389
+ if (settings2.kind === "invalid") return { isConfigured: false, model: void 0, isUnreadable: true };
1390
+ const env = settings2.value[ENV_BLOCK];
1391
+ const model = settings2.value[MODEL_SETTING] ?? env?.[MODEL_VARIABLE];
1392
+ return {
1393
+ isConfigured: isGatewayUrl(env?.[BASE_URL_VARIABLE], gatewayHostOf(ctx)),
1394
+ model: model === void 0 ? void 0 : normalizeModel(model),
1395
+ isUnreadable: false
1396
+ };
1262
1397
  }
1263
1398
 
1264
1399
  // src/harnesses/codex/codex.ts
@@ -1388,14 +1523,14 @@ function listKeyValues(program) {
1388
1523
  }
1389
1524
  function listInBody(keyValues, tablePath) {
1390
1525
  return keyValues.flatMap((keyValue) => {
1391
- const path22 = [...tablePath, ...keyValue.key.keys.map(readKeyName)];
1392
- const entry = { path: path22, keyValue, tablePath };
1526
+ const path26 = [...tablePath, ...keyValue.key.keys.map(readKeyName)];
1527
+ const entry = { path: path26, keyValue, tablePath };
1393
1528
  if (keyValue.value.type !== "TOMLInlineTable") return [entry];
1394
- return [entry, ...listInBody(keyValue.value.body, path22)];
1529
+ return [entry, ...listInBody(keyValue.value.body, path26)];
1395
1530
  });
1396
1531
  }
1397
- function isSamePath(path22, other) {
1398
- return path22.length === other.length && path22.every((key, index) => key === other[index]);
1532
+ function isSamePath(path26, other) {
1533
+ return path26.length === other.length && path26.every((key, index) => key === other[index]);
1399
1534
  }
1400
1535
  function replaceValue(source, node, value) {
1401
1536
  if (node.type === "TOMLValue" && node.value === value) return source.text;
@@ -1597,15 +1732,17 @@ var codex = {
1597
1732
  id: "codex",
1598
1733
  label: "OpenAI Codex",
1599
1734
  binary: BINARY2,
1735
+ installPackage: "@openai/codex",
1600
1736
  protocols: ["openai-responses"],
1601
1737
  projectSupport: { kind: "unsupported", reason: "a repo-level config.toml cannot set the provider or its base_url" },
1602
1738
  keyEnvVariable: API_KEY_VARIABLE2,
1603
- detect: (ctx) => detectConfigFile([resolveConfigPath(ctx)], ctx.home),
1739
+ detect: (ctx) => detectConfigOrBinary({ candidates: [resolveConfigPath(ctx)], binary: BINARY2, ctx }),
1604
1740
  configPath: (ctx) => resolveConfigPath(ctx),
1605
1741
  plan: (desired, ctx) => [
1606
1742
  planConfig(desired, ctx),
1607
1743
  { kind: "env", variables: { [API_KEY_VARIABLE2]: desired.apiKey } }
1608
1744
  ],
1745
+ inspect: (ctx) => inspectConfig(ctx),
1609
1746
  launch: (desired) => {
1610
1747
  const overrides = [
1611
1748
  `model=${JSON.stringify(desired.model)}`,
@@ -1628,6 +1765,7 @@ var codex = {
1628
1765
  "Running /model saves that model to ~/.codex/config.toml.",
1629
1766
  'A later plain `codex resume` of this session points at a "nexos-run" provider that no longer exists.'
1630
1767
  ],
1768
+ postSetupNote: (model) => `If Codex reports \`Missing environment variable: ${API_KEY_VARIABLE2}\`, try \`${BINARY2} -c '${escapePosixSingleQuoted(`model=${JSON.stringify(model)}`)}'\`. This is a reported workaround, not a required launch command or a guaranteed fix.`,
1631
1769
  postSetupNotes: [
1632
1770
  `OpenAI Codex requires wire_api = "${WIRE_API}": if the gateway does not serve /v1/responses, Codex fails even though its config is correct.`
1633
1771
  ]
@@ -1679,6 +1817,28 @@ function listLeftoverRemovals({ desired, current }) {
1679
1817
  const isRouterLeftover = readPath(parseToml(current ?? ""), WEB_SEARCH_KEY_PATH) === WEB_SEARCH_DISABLED;
1680
1818
  return isRouterLeftover ? [[WEB_SEARCH_KEY_PATH, void 0]] : [];
1681
1819
  }
1820
+ function inspectConfig(ctx) {
1821
+ const filePath = resolveConfigPath(ctx);
1822
+ const text2 = readFileIfExists(filePath);
1823
+ if (text2 === null) return { isConfigured: false, model: void 0, isUnreadable: false };
1824
+ let document;
1825
+ try {
1826
+ document = parseToml(text2);
1827
+ } catch (error) {
1828
+ if (!(error instanceof ConfigFormatError)) throw error;
1829
+ return { isConfigured: false, model: void 0, isUnreadable: true };
1830
+ }
1831
+ const baseUrl = readString(document, ["model_providers", PROVIDER_ID, "base_url"]);
1832
+ return {
1833
+ isConfigured: readString(document, ["model_provider"]) === PROVIDER_ID && isGatewayUrl(baseUrl, gatewayHostOf(ctx)),
1834
+ model: readString(document, ["model"]),
1835
+ isUnreadable: false
1836
+ };
1837
+ }
1838
+ function readString(document, keyPath) {
1839
+ const value = readPath(document, keyPath);
1840
+ return typeof value === "string" ? value : void 0;
1841
+ }
1682
1842
 
1683
1843
  // src/harnesses/copilot-cli/copilot-cli.ts
1684
1844
  var BINARY3 = "copilot";
@@ -1706,6 +1866,7 @@ var copilotCli = {
1706
1866
  id: "copilot-cli",
1707
1867
  label: "GitHub Copilot CLI",
1708
1868
  binary: BINARY3,
1869
+ installPackage: "@github/copilot",
1709
1870
  protocols: ["openai-chat"],
1710
1871
  projectSupport: {
1711
1872
  kind: "unsupported",
@@ -1740,9 +1901,137 @@ function detectBinary(ctx) {
1740
1901
  return { isFound, detail: isFound ? `${BINARY3} found on PATH` : `${BINARY3} not found on PATH` };
1741
1902
  }
1742
1903
 
1904
+ // src/harnesses/crush/crush.ts
1905
+ import path5 from "path";
1906
+ import { match as match8 } from "ts-pattern";
1907
+ import * as v5 from "valibot";
1908
+
1909
+ // src/shared/text/read-literal-key.ts
1910
+ var COMMAND_PREFIX = "!";
1911
+ var VARIABLE_MARKER = "$";
1912
+ function readLiteralKey(value) {
1913
+ const key = readNonBlank(value);
1914
+ return key === void 0 || isConfigSubstitution(key) ? void 0 : key;
1915
+ }
1916
+ function isConfigSubstitution(value) {
1917
+ return value.startsWith(COMMAND_PREFIX) || value.includes(VARIABLE_MARKER);
1918
+ }
1919
+
1920
+ // src/harnesses/launch/launch-recipe.ts
1921
+ var LAUNCH_DIRECTORY_PLACEHOLDER = "{launchDirectory}";
1922
+
1923
+ // src/harnesses/crush/crush.ts
1924
+ var BINARY4 = "crush";
1925
+ var PROVIDER_ID2 = "nexos";
1926
+ var PROVIDER_NAME2 = "nexos.ai";
1927
+ var PROVIDER_TYPE2 = "openai-compat";
1928
+ var CONFIG_SCHEMA_URL = "https://charm.land/crush.json";
1929
+ var GLOBAL_CONFIG_VARIABLE = "CRUSH_GLOBAL_CONFIG";
1930
+ var crush = {
1931
+ kind: "configurable",
1932
+ id: "crush",
1933
+ label: "Crush",
1934
+ binary: BINARY4,
1935
+ installPackage: "@charmland/crush",
1936
+ protocols: ["openai-chat"],
1937
+ projectSupport: { kind: "supported" },
1938
+ detect: (ctx) => detectCrush(ctx),
1939
+ configPath: (ctx) => resolveConfigPath2(ctx),
1940
+ plan: (desired, ctx) => [planConfig2(desired, ctx)],
1941
+ inspect: (ctx) => inspectConfig2(ctx),
1942
+ launch: (desired) => ({
1943
+ binary: BINARY4,
1944
+ arguments: [],
1945
+ env: { [GLOBAL_CONFIG_VARIABLE]: LAUNCH_DIRECTORY_PLACEHOLDER },
1946
+ files: [{ name: USER_FILE, content: renderJsonc(null, listUpdates(desired)) }]
1947
+ }),
1948
+ launchNotes: [
1949
+ "Crush reads crush.json from a temporary config directory for this run; your own Crush config is untouched.",
1950
+ "This session is kept in your Crush history like any other."
1951
+ ],
1952
+ readKey: (ctx) => readProviderKey(ctx),
1953
+ readSecrets: (ctx) => readProviderSecrets(ctx),
1954
+ postSetupNotes: [
1955
+ "Crush starts on the model setup chose. For another, run its `model large` command.",
1956
+ "Crush ranks its config, highest first: the project's crushrc, the project's .crush.json or crush.json, the user's crushrc, then the user's crush.json. A higher one that sets the same values overrides this file."
1957
+ ]
1958
+ };
1959
+ function detectCrush(ctx) {
1960
+ const detected = detectConfigFile([resolveConfigPath2(ctx)], ctx.home);
1961
+ if (detected.isFound || ctx.scope === "project" || !ctx.hasBinary(BINARY4)) return detected;
1962
+ return { isFound: true, detail: `${BINARY4} found on PATH` };
1963
+ }
1964
+ var USER_DIRECTORY = path5.join(".config", "crush");
1965
+ var USER_FILE = "crush.json";
1966
+ var PROJECT_FILE = ".crush.json";
1967
+ function resolveConfigPath2(ctx) {
1968
+ return match8(ctx.scope).with("user", () => path5.join(ctx.home, USER_DIRECTORY, USER_FILE)).with("project", () => path5.join(ctx.cwd, PROJECT_FILE)).exhaustive();
1969
+ }
1970
+ function planConfig2(desired, ctx) {
1971
+ return {
1972
+ kind: "file",
1973
+ path: resolveConfigPath2(ctx),
1974
+ hasSecret: true,
1975
+ edits: () => ({ format: "jsonc", updates: listUpdates(desired) })
1976
+ };
1977
+ }
1978
+ function listUpdates(desired) {
1979
+ const contextWindow = findContextWindow(desired.model);
1980
+ const modelEntry = {
1981
+ id: desired.model,
1982
+ name: desired.model,
1983
+ ...contextWindow === void 0 ? {} : { context_window: contextWindow }
1984
+ };
1985
+ const selection = { provider: PROVIDER_ID2, model: desired.model };
1986
+ return [
1987
+ [["$schema"], CONFIG_SCHEMA_URL],
1988
+ [["providers", PROVIDER_ID2, "type"], PROVIDER_TYPE2],
1989
+ [["providers", PROVIDER_ID2, "name"], PROVIDER_NAME2],
1990
+ [["providers", PROVIDER_ID2, "base_url"], desired.openaiBaseUrl],
1991
+ [["providers", PROVIDER_ID2, "api_key"], desired.apiKey],
1992
+ [["providers", PROVIDER_ID2, "models"], [modelEntry]],
1993
+ [["models", "large"], selection],
1994
+ [["models", "small"], selection]
1995
+ ];
1996
+ }
1997
+ var ConfigSchema = v5.looseObject({
1998
+ providers: v5.optional(
1999
+ v5.looseObject({
2000
+ [PROVIDER_ID2]: v5.optional(v5.looseObject({ api_key: v5.optional(v5.string()), base_url: v5.optional(v5.string()) }))
2001
+ })
2002
+ ),
2003
+ models: v5.optional(v5.looseObject({ large: v5.optional(v5.looseObject({ model: v5.optional(v5.string()) })) }))
2004
+ });
2005
+ function inspectConfig2(ctx) {
2006
+ const config = readJsoncFile(resolveConfigPath2(ctx), ConfigSchema);
2007
+ if (config.kind === "absent") return { isConfigured: false, model: void 0, isUnreadable: false };
2008
+ if (config.kind === "invalid") return { isConfigured: false, model: void 0, isUnreadable: true };
2009
+ const provider = config.value.providers?.[PROVIDER_ID2];
2010
+ return {
2011
+ isConfigured: isGatewayUrl(provider?.base_url, gatewayHostOf(ctx)),
2012
+ model: config.value.models?.large?.model,
2013
+ isUnreadable: false
2014
+ };
2015
+ }
2016
+ function readProviderKey(ctx) {
2017
+ const config = readJsoncFile(resolveConfigPath2(ctx), ConfigSchema);
2018
+ const apiKey = config.kind === "valid" ? config.value.providers?.[PROVIDER_ID2]?.api_key : void 0;
2019
+ return readLiteralKey(apiKey);
2020
+ }
2021
+ var ProvidersSchema = v5.looseObject({ providers: v5.optional(v5.record(v5.string(), v5.unknown())) });
2022
+ var ProviderKeySchema = v5.looseObject({ api_key: v5.string() });
2023
+ function readProviderSecrets(ctx) {
2024
+ const config = readJsoncFile(resolveConfigPath2(ctx), ProvidersSchema);
2025
+ if (config.kind !== "valid") return [];
2026
+ return Object.values(config.value.providers ?? {}).flatMap((provider) => {
2027
+ const parsed = v5.safeParse(ProviderKeySchema, provider);
2028
+ return parsed.success ? readLiteralKey(parsed.output.api_key) ?? [] : [];
2029
+ });
2030
+ }
2031
+
1743
2032
  // src/harnesses/gemini-cli/gemini-cli.ts
1744
2033
  import fs3 from "fs";
1745
- import path5 from "path";
2034
+ import path6 from "path";
1746
2035
  var geminiCli = {
1747
2036
  kind: "unsupported",
1748
2037
  id: "gemini-cli",
@@ -1753,7 +2042,7 @@ var geminiCli = {
1753
2042
  };
1754
2043
  var CONFIG_DIRECTORY3 = ".gemini";
1755
2044
  function detectConfigDirectory(ctx) {
1756
- const directory = path5.join(ctx.home, CONFIG_DIRECTORY3);
2045
+ const directory = path6.join(ctx.home, CONFIG_DIRECTORY3);
1757
2046
  if (!isDirectory(directory)) return { isFound: false, detail: `no ${displayPath(directory, ctx.home)}` };
1758
2047
  return { isFound: true, detail: "installed, but not configurable through the nexos.ai Gateway" };
1759
2048
  }
@@ -1767,20 +2056,22 @@ function isDirectory(directoryPath) {
1767
2056
  }
1768
2057
 
1769
2058
  // src/harnesses/opencode/opencode.ts
1770
- import path6 from "path";
1771
- import { match as match8 } from "ts-pattern";
1772
- import * as v5 from "valibot";
1773
- var BINARY4 = "opencode";
2059
+ import path7 from "path";
2060
+ import { match as match9 } from "ts-pattern";
2061
+ import * as v6 from "valibot";
2062
+ var BINARY5 = "opencode";
1774
2063
  var opencode = {
1775
2064
  kind: "configurable",
1776
2065
  id: "opencode",
1777
2066
  label: "OpenCode",
1778
- binary: BINARY4,
2067
+ binary: BINARY5,
2068
+ installPackage: "opencode-ai",
1779
2069
  protocols: ["openai-chat"],
1780
2070
  projectSupport: { kind: "supported" },
1781
- detect: (ctx) => detectOpenCode(ctx),
1782
- configPath: (ctx) => resolveConfigPath2(ctx),
1783
- plan: (desired, ctx) => [planConfig2(desired, ctx)],
2071
+ detect: (ctx) => detectConfigOrBinary({ candidates: listConfigCandidates(ctx), binary: BINARY5, ctx }),
2072
+ configPath: (ctx) => resolveConfigPath3(ctx),
2073
+ plan: (desired, ctx) => [planConfig3(desired, ctx)],
2074
+ inspect: (ctx) => inspectConfig3(ctx),
1784
2075
  launch: (desired) => {
1785
2076
  const config = renderJsonc(null, [
1786
2077
  ...providerUpdates({ providerId: RUN_PROVIDER_ID2, apiKey: `{env:${RUN_KEY_VARIABLE}}`, desired }),
@@ -1788,7 +2079,7 @@ var opencode = {
1788
2079
  [["enabled_providers"], [RUN_PROVIDER_ID2]]
1789
2080
  ]);
1790
2081
  return {
1791
- binary: BINARY4,
2082
+ binary: BINARY5,
1792
2083
  arguments: [],
1793
2084
  env: {
1794
2085
  [CONFIG_CONTENT_VARIABLE]: config,
@@ -1800,49 +2091,44 @@ var opencode = {
1800
2091
  launchNotes: [
1801
2092
  'OpenCode adds the "nexos-run" model to its recent models, where it shows as unavailable in later sessions.'
1802
2093
  ],
1803
- readKey: (ctx) => readProviderKey(ctx),
1804
- readSecrets: (ctx) => readProviderSecrets(ctx),
2094
+ readKey: (ctx) => readProviderKey2(ctx),
2095
+ readSecrets: (ctx) => readProviderSecrets2(ctx),
1805
2096
  postSetupNotes: [
1806
2097
  "OpenCode keeps its own credentials in its auth.json; this key goes in the provider block instead, so no /connect step is needed."
1807
2098
  ]
1808
2099
  };
1809
- function detectOpenCode(ctx) {
1810
- const detected = detectConfigFile(listConfigCandidates(ctx), ctx.home);
1811
- if (detected.isFound || ctx.scope === "project" || !ctx.hasBinary(BINARY4)) return detected;
1812
- return { isFound: true, detail: `${BINARY4} found on PATH` };
1813
- }
1814
- var USER_CONFIG_DIRECTORY = path6.join(".config", "opencode");
2100
+ var USER_CONFIG_DIRECTORY = path7.join(".config", "opencode");
1815
2101
  var JSON_CONFIG_FILE = "opencode.json";
1816
2102
  var JSONC_CONFIG_FILE = "opencode.jsonc";
1817
2103
  function listConfigCandidates(ctx) {
1818
- const directory = match8(ctx.scope).with("user", () => path6.join(ctx.home, USER_CONFIG_DIRECTORY)).with("project", () => ctx.cwd).exhaustive();
1819
- return [path6.join(directory, JSON_CONFIG_FILE), path6.join(directory, JSONC_CONFIG_FILE)];
2104
+ const directory = match9(ctx.scope).with("user", () => path7.join(ctx.home, USER_CONFIG_DIRECTORY)).with("project", () => ctx.cwd).exhaustive();
2105
+ return [path7.join(directory, JSON_CONFIG_FILE), path7.join(directory, JSONC_CONFIG_FILE)];
1820
2106
  }
1821
- function resolveConfigPath2(ctx) {
2107
+ function resolveConfigPath3(ctx) {
1822
2108
  const candidates = listConfigCandidates(ctx);
1823
2109
  return findFirstExistingFile(candidates) ?? candidates[0];
1824
2110
  }
1825
- function planConfig2(desired, ctx) {
2111
+ function planConfig3(desired, ctx) {
1826
2112
  return {
1827
2113
  kind: "file",
1828
- path: resolveConfigPath2(ctx),
2114
+ path: resolveConfigPath3(ctx),
1829
2115
  hasSecret: true,
1830
- edits: (current) => ({ format: "jsonc", updates: listUpdates(desired, parseJsoncConfig(current, ConfigSchema)) })
2116
+ edits: (current) => ({ format: "jsonc", updates: listUpdates2(desired, parseJsoncConfig(current, ConfigSchema2)) })
1831
2117
  };
1832
2118
  }
1833
- var PROVIDER_ID2 = "nexos";
1834
- var PROVIDER_NAME2 = "nexos.ai";
2119
+ var PROVIDER_ID3 = "nexos";
2120
+ var PROVIDER_NAME3 = "nexos.ai";
1835
2121
  var PROVIDER_PACKAGE = "@ai-sdk/openai-compatible";
1836
2122
  var RUN_PROVIDER_ID2 = "nexos-run";
1837
2123
  var RUN_KEY_VARIABLE = "NEXOS_API_KEY";
1838
2124
  var CONFIG_CONTENT_VARIABLE = "OPENCODE_CONFIG_CONTENT";
1839
2125
  var MODELS_FETCH_SWITCH = "OPENCODE_DISABLE_MODELS_FETCH";
1840
2126
  var SWITCH_ON = "1";
1841
- var CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
2127
+ var CONFIG_SCHEMA_URL2 = "https://opencode.ai/config.json";
1842
2128
  function providerUpdates({ providerId, apiKey, desired }) {
1843
2129
  return [
1844
2130
  [["provider", providerId, "npm"], PROVIDER_PACKAGE],
1845
- [["provider", providerId, "name"], PROVIDER_NAME2],
2131
+ [["provider", providerId, "name"], PROVIDER_NAME3],
1846
2132
  [["provider", providerId, "options", "baseURL"], desired.openaiBaseUrl],
1847
2133
  [["provider", providerId, "options", "apiKey"], apiKey],
1848
2134
  [["provider", providerId, "models", desired.model, "name"], desired.model],
@@ -1859,69 +2145,366 @@ function listLimitUpdates({ providerId, model }) {
1859
2145
  [["provider", providerId, "models", model, "limit", "output"], OUTPUT_TOKENS]
1860
2146
  ];
1861
2147
  }
1862
- function listUpdates(desired, config) {
1863
- const updates = providerUpdates({ providerId: PROVIDER_ID2, apiKey: desired.apiKey, desired });
2148
+ function listUpdates2(desired, config) {
2149
+ const updates = providerUpdates({ providerId: PROVIDER_ID3, apiKey: desired.apiKey, desired });
1864
2150
  if (!isMissingSchemaUrl(config)) return updates;
1865
- return [[["$schema"], CONFIG_SCHEMA_URL], ...updates];
2151
+ return [[["$schema"], CONFIG_SCHEMA_URL2], ...updates];
1866
2152
  }
1867
2153
  function isMissingSchemaUrl(config) {
1868
- return match8(config).with({ kind: "absent" }, () => true).with({ kind: "invalid" }, () => false).with({ kind: "valid" }, ({ value }) => value.$schema === void 0).exhaustive();
2154
+ return match9(config).with({ kind: "absent" }, () => true).with({ kind: "invalid" }, () => false).with({ kind: "valid" }, ({ value }) => value.$schema === void 0).exhaustive();
1869
2155
  }
1870
- function readProviderKey(ctx) {
1871
- const config = readJsoncFile(resolveConfigPath2(ctx), ConfigSchema);
1872
- const apiKey = config.kind === "valid" ? config.value.provider?.[PROVIDER_ID2]?.options?.apiKey : void 0;
2156
+ function readProviderKey2(ctx) {
2157
+ const config = readJsoncFile(resolveConfigPath3(ctx), ConfigSchema2);
2158
+ const apiKey = config.kind === "valid" ? config.value.provider?.[PROVIDER_ID3]?.options?.apiKey : void 0;
1873
2159
  return apiKey === void 0 || isSubstitution(apiKey) ? void 0 : apiKey;
1874
2160
  }
1875
2161
  var SUBSTITUTION_MARKERS = ["{env:", "{file:"];
1876
2162
  function isSubstitution(value) {
1877
2163
  return SUBSTITUTION_MARKERS.some((marker) => value.includes(marker));
1878
2164
  }
1879
- var ProvidersSchema = v5.looseObject({
1880
- provider: v5.fallback(v5.optional(v5.record(v5.string(), v5.unknown())), void 0)
2165
+ var ProvidersSchema2 = v6.looseObject({
2166
+ provider: v6.fallback(v6.optional(v6.record(v6.string(), v6.unknown())), void 0)
1881
2167
  });
1882
- var ProviderKeySchema = v5.looseObject({ options: v5.looseObject({ apiKey: v5.string() }) });
1883
- function readProviderSecrets(ctx) {
1884
- const config = readJsoncFile(resolveConfigPath2(ctx), ProvidersSchema);
2168
+ var ProviderKeySchema2 = v6.looseObject({ options: v6.looseObject({ apiKey: v6.string() }) });
2169
+ function readProviderSecrets2(ctx) {
2170
+ const config = readJsoncFile(resolveConfigPath3(ctx), ProvidersSchema2);
1885
2171
  if (config.kind !== "valid") return [];
1886
2172
  return Object.values(config.value.provider ?? {}).flatMap((provider) => {
1887
- const parsed = v5.safeParse(ProviderKeySchema, provider);
2173
+ const parsed = v6.safeParse(ProviderKeySchema2, provider);
1888
2174
  if (!parsed.success || isSubstitution(parsed.output.options.apiKey)) return [];
1889
2175
  return readNonBlank(parsed.output.options.apiKey) ?? [];
1890
2176
  });
1891
2177
  }
1892
- var ConfigSchema = v5.looseObject({
1893
- $schema: v5.optional(v5.unknown()),
1894
- provider: v5.optional(
1895
- v5.looseObject({
1896
- [PROVIDER_ID2]: v5.optional(
1897
- v5.looseObject({ options: v5.optional(v5.looseObject({ apiKey: v5.optional(v5.string()) })) })
2178
+ var ConfigSchema2 = v6.looseObject({
2179
+ $schema: v6.optional(v6.unknown()),
2180
+ model: v6.optional(v6.string()),
2181
+ provider: v6.optional(
2182
+ v6.looseObject({
2183
+ [PROVIDER_ID3]: v6.optional(
2184
+ v6.looseObject({
2185
+ options: v6.optional(v6.looseObject({ apiKey: v6.optional(v6.string()), baseURL: v6.optional(v6.string()) }))
2186
+ })
1898
2187
  )
1899
2188
  })
1900
2189
  )
1901
2190
  });
1902
-
1903
- // src/harnesses/registry.ts
1904
- var harnesses = [claudeCode, codex, opencode, copilotCli, geminiCli];
1905
- function findHarness(id) {
1906
- return harnesses.find((harness) => harness.id === id);
2191
+ function inspectConfig3(ctx) {
2192
+ const config = readJsoncFile(resolveConfigPath3(ctx), ConfigSchema2);
2193
+ if (config.kind === "absent") return { isConfigured: false, model: void 0, isUnreadable: false };
2194
+ if (config.kind === "invalid") return { isConfigured: false, model: void 0, isUnreadable: true };
2195
+ const options = config.value.provider?.[PROVIDER_ID3]?.options;
2196
+ return {
2197
+ isConfigured: isGatewayUrl(options?.baseURL, gatewayHostOf(ctx)),
2198
+ model: readOpenCodeModel(config.value.model),
2199
+ isUnreadable: false
2200
+ };
1907
2201
  }
1908
- function findHarnessByBinary(binary) {
1909
- return harnesses.find((harness) => harness.binary === binary);
2202
+ function readOpenCodeModel(model) {
2203
+ if (model === void 0) return void 0;
2204
+ const separatorIndex = model.indexOf("/");
2205
+ return separatorIndex === -1 ? model : model.slice(separatorIndex + 1);
1910
2206
  }
1911
2207
 
1912
- // src/product/storage/env-file/split-lines.ts
1913
- var LINE_BREAK = /\r?\n/;
1914
- function splitLines(text2) {
1915
- return text2.split(LINE_BREAK);
2208
+ // src/harnesses/pi/pi.ts
2209
+ import path8 from "path";
2210
+ import * as v7 from "valibot";
2211
+ var BINARY6 = "pi";
2212
+ var PROVIDER_ID4 = "nexos";
2213
+ var PROVIDER_NAME4 = "nexos.ai";
2214
+ var API_TYPE = "openai-completions";
2215
+ var AGENT_DIRECTORY_VARIABLE = "PI_CODING_AGENT_DIR";
2216
+ var SESSION_DIRECTORY_VARIABLE = "PI_CODING_AGENT_SESSION_DIR";
2217
+ var pi = {
2218
+ kind: "configurable",
2219
+ id: "pi",
2220
+ label: "Pi",
2221
+ binary: BINARY6,
2222
+ installPackage: "@earendil-works/pi-coding-agent",
2223
+ protocols: ["openai-chat"],
2224
+ projectSupport: {
2225
+ kind: "unsupported",
2226
+ reason: "it reads models.json only from its agent directory (~/.pi/agent), never from the project"
2227
+ },
2228
+ detect: (ctx) => detectBinary2(ctx),
2229
+ configPath: (ctx) => resolveModelsPath(ctx),
2230
+ plan: (desired, ctx) => [planModels(desired, ctx), planSettings2(desired, ctx)],
2231
+ inspect: (ctx) => inspectConfig4(ctx),
2232
+ launch: (desired) => ({
2233
+ binary: BINARY6,
2234
+ arguments: ["--provider", PROVIDER_ID4, "--model", desired.model],
2235
+ env: {
2236
+ [AGENT_DIRECTORY_VARIABLE]: LAUNCH_DIRECTORY_PLACEHOLDER,
2237
+ [SESSION_DIRECTORY_VARIABLE]: SESSION_DIRECTORY
2238
+ },
2239
+ files: [{ name: MODELS_FILE, content: renderJsonc(null, listModelUpdates(desired)) }]
2240
+ }),
2241
+ launchNotes: [
2242
+ "Pi reads models.json from a temporary agent directory for this run; your ~/.pi/agent files are untouched.",
2243
+ "This session is kept in your Pi history like any other."
2244
+ ],
2245
+ readKey: (ctx) => readProviderKey3(ctx),
2246
+ readSecrets: (ctx) => readProviderSecrets3(ctx),
2247
+ postSetupNotes: [
2248
+ "Pi starts on the model setup chose. Change it with /model, and press Ctrl+S to make that the default."
2249
+ ]
2250
+ };
2251
+ function detectBinary2(ctx) {
2252
+ const isFound = ctx.hasBinary(BINARY6);
2253
+ return { isFound, detail: isFound ? `${BINARY6} found on PATH` : `${BINARY6} not found on PATH` };
1916
2254
  }
1917
-
1918
- // src/product/storage/env-file/syntax/env-syntaxes.ts
1919
- import { match as match9 } from "ts-pattern";
1920
-
1921
- // src/product/storage/env-file/syntax/posix-env-syntax.ts
1922
- import path7 from "path";
1923
-
1924
- // src/product/storage/env-file/syntax/env-syntax.ts
2255
+ var DEFAULT_AGENT_DIRECTORY = path8.join(".pi", "agent");
2256
+ var SESSION_DIRECTORY = path8.join("~", DEFAULT_AGENT_DIRECTORY, "sessions");
2257
+ var MODELS_FILE = "models.json";
2258
+ var SETTINGS_FILE = "settings.json";
2259
+ var HOME_PREFIX = "~";
2260
+ function resolveAgentDirectory(ctx) {
2261
+ const configured = readNonBlank(ctx.env.PI_CODING_AGENT_DIR);
2262
+ return configured === void 0 ? path8.join(ctx.home, DEFAULT_AGENT_DIRECTORY) : resolveConfiguredPath(configured, ctx);
2263
+ }
2264
+ function resolveConfiguredPath(configured, ctx) {
2265
+ if (configured === HOME_PREFIX) return ctx.home;
2266
+ if (configured.startsWith(`${HOME_PREFIX}${path8.sep}`)) {
2267
+ return path8.join(ctx.home, configured.slice(HOME_PREFIX.length + 1));
2268
+ }
2269
+ return path8.resolve(ctx.cwd, configured);
2270
+ }
2271
+ function resolveModelsPath(ctx) {
2272
+ return path8.join(resolveAgentDirectory(ctx), MODELS_FILE);
2273
+ }
2274
+ function resolveSettingsPath2(ctx) {
2275
+ return path8.join(resolveAgentDirectory(ctx), SETTINGS_FILE);
2276
+ }
2277
+ function planModels(desired, ctx) {
2278
+ return {
2279
+ kind: "file",
2280
+ path: resolveModelsPath(ctx),
2281
+ hasSecret: true,
2282
+ edits: () => ({ format: "jsonc", updates: listModelUpdates(desired) })
2283
+ };
2284
+ }
2285
+ function planSettings2(desired, ctx) {
2286
+ return {
2287
+ kind: "file",
2288
+ path: resolveSettingsPath2(ctx),
2289
+ hasSecret: false,
2290
+ edits: () => ({
2291
+ format: "jsonc",
2292
+ updates: [
2293
+ [["defaultProvider"], PROVIDER_ID4],
2294
+ [["defaultModel"], desired.model]
2295
+ ]
2296
+ })
2297
+ };
2298
+ }
2299
+ function listModelUpdates(desired) {
2300
+ const contextWindow = findContextWindow(desired.model);
2301
+ const modelEntry = {
2302
+ id: desired.model,
2303
+ name: desired.model,
2304
+ ...contextWindow === void 0 ? {} : { contextWindow }
2305
+ };
2306
+ return [
2307
+ [["providers", PROVIDER_ID4, "baseUrl"], desired.openaiBaseUrl],
2308
+ [["providers", PROVIDER_ID4, "api"], API_TYPE],
2309
+ [["providers", PROVIDER_ID4, "apiKey"], desired.apiKey],
2310
+ [["providers", PROVIDER_ID4, "name"], PROVIDER_NAME4],
2311
+ [["providers", PROVIDER_ID4, "models"], [modelEntry]]
2312
+ ];
2313
+ }
2314
+ var ModelsSchema = v7.looseObject({
2315
+ providers: v7.optional(
2316
+ v7.looseObject({
2317
+ [PROVIDER_ID4]: v7.optional(v7.looseObject({ apiKey: v7.optional(v7.string()), baseUrl: v7.optional(v7.string()) }))
2318
+ })
2319
+ )
2320
+ });
2321
+ var SettingsSchema2 = v7.looseObject({
2322
+ defaultProvider: v7.optional(v7.string()),
2323
+ defaultModel: v7.optional(v7.string())
2324
+ });
2325
+ function inspectConfig4(ctx) {
2326
+ const models = readJsoncFile(resolveModelsPath(ctx), ModelsSchema);
2327
+ const settings2 = readJsoncFile(resolveSettingsPath2(ctx), SettingsSchema2);
2328
+ const provider = models.kind === "valid" ? models.value.providers?.[PROVIDER_ID4] : void 0;
2329
+ return {
2330
+ isConfigured: isGatewayUrl(provider?.baseUrl, gatewayHostOf(ctx)),
2331
+ model: settings2.kind === "valid" ? settings2.value.defaultModel : void 0,
2332
+ isUnreadable: models.kind === "invalid" || settings2.kind === "invalid"
2333
+ };
2334
+ }
2335
+ function readProviderKey3(ctx) {
2336
+ const models = readJsoncFile(resolveModelsPath(ctx), ModelsSchema);
2337
+ const apiKey = models.kind === "valid" ? models.value.providers?.[PROVIDER_ID4]?.apiKey : void 0;
2338
+ return readLiteralKey(apiKey);
2339
+ }
2340
+ var ProvidersSchema3 = v7.looseObject({ providers: v7.optional(v7.record(v7.string(), v7.unknown())) });
2341
+ var ProviderKeySchema3 = v7.looseObject({ apiKey: v7.string() });
2342
+ function readProviderSecrets3(ctx) {
2343
+ const models = readJsoncFile(resolveModelsPath(ctx), ProvidersSchema3);
2344
+ if (models.kind !== "valid") return [];
2345
+ return Object.values(models.value.providers ?? {}).flatMap((provider) => {
2346
+ const parsed = v7.safeParse(ProviderKeySchema3, provider);
2347
+ return parsed.success ? readLiteralKey(parsed.output.apiKey) ?? [] : [];
2348
+ });
2349
+ }
2350
+
2351
+ // src/harnesses/qwen-code/qwen.ts
2352
+ import path9 from "path";
2353
+ import { match as match10 } from "ts-pattern";
2354
+ import * as v8 from "valibot";
2355
+ var BINARY7 = "qwen";
2356
+ var KEY_VARIABLE = "NEXOS_API_KEY";
2357
+ var OPENAI_KEY_VARIABLE = "OPENAI_API_KEY";
2358
+ var AUTH_TYPE = "openai";
2359
+ var qwen = {
2360
+ kind: "configurable",
2361
+ id: "qwen-code",
2362
+ label: "Qwen Code",
2363
+ binary: BINARY7,
2364
+ installPackage: "@qwen-code/qwen-code",
2365
+ protocols: ["openai-chat"],
2366
+ projectSupport: { kind: "supported" },
2367
+ detect: (ctx) => detectQwen(ctx),
2368
+ configPath: (ctx) => resolveSettingsPath3(ctx),
2369
+ plan: (desired, ctx) => [planSettings3(desired, ctx)],
2370
+ inspect: (ctx) => inspectSettings2(ctx),
2371
+ launch: (desired) => ({
2372
+ binary: BINARY7,
2373
+ arguments: ["--auth-type", AUTH_TYPE, "--openai-base-url", desired.openaiBaseUrl, "--model", desired.model],
2374
+ env: { [OPENAI_KEY_VARIABLE]: desired.apiKey }
2375
+ }),
2376
+ launchNotes: ["This session is kept in your Qwen Code history like any other."],
2377
+ readKey: (ctx) => readProviderKey4(ctx),
2378
+ readSecrets: (ctx) => readProviderSecrets4(ctx),
2379
+ postSetupNotes: [
2380
+ "Qwen Code keeps the key in its settings file, so no /auth step is needed.",
2381
+ "Qwen Code ranks its settings, highest first: the system settings file, the project's .qwen/settings.json, then the user's ~/.qwen/settings.json. A higher one that sets the same values overrides this file."
2382
+ ]
2383
+ };
2384
+ function detectQwen(ctx) {
2385
+ const detected = detectConfigFile([resolveSettingsPath3(ctx)], ctx.home);
2386
+ if (detected.isFound || ctx.scope === "project" || !ctx.hasBinary(BINARY7)) return detected;
2387
+ return { isFound: true, detail: `${BINARY7} found on PATH` };
2388
+ }
2389
+ var USER_DIRECTORY2 = ".qwen";
2390
+ var PROJECT_DIRECTORY = ".qwen";
2391
+ var SETTINGS_FILE2 = "settings.json";
2392
+ function resolveSettingsPath3(ctx) {
2393
+ return match10(ctx.scope).with("user", () => path9.join(ctx.home, USER_DIRECTORY2, SETTINGS_FILE2)).with("project", () => path9.join(ctx.cwd, PROJECT_DIRECTORY, SETTINGS_FILE2)).exhaustive();
2394
+ }
2395
+ function planSettings3(desired, ctx) {
2396
+ return {
2397
+ kind: "file",
2398
+ path: resolveSettingsPath3(ctx),
2399
+ hasSecret: true,
2400
+ edits: (current) => ({ format: "jsonc", updates: listUpdates3(desired, current) })
2401
+ };
2402
+ }
2403
+ function listUpdates3(desired, current) {
2404
+ const provider = {
2405
+ id: desired.model,
2406
+ name: desired.model,
2407
+ baseUrl: desired.openaiBaseUrl,
2408
+ envKey: KEY_VARIABLE
2409
+ };
2410
+ return [
2411
+ [
2412
+ ["modelProviders", "openai"],
2413
+ [...listOtherOpenAiProviders(current), provider]
2414
+ ],
2415
+ [["env", KEY_VARIABLE], desired.apiKey],
2416
+ [["security", "auth", "selectedType"], AUTH_TYPE],
2417
+ [["model", "name"], desired.model]
2418
+ ];
2419
+ }
2420
+ var ModelProvidersSchema = v8.looseObject({
2421
+ modelProviders: v8.optional(v8.looseObject({ openai: v8.optional(v8.unknown()) }))
2422
+ });
2423
+ var NexosEntrySchema = v8.looseObject({ envKey: v8.literal(KEY_VARIABLE), baseUrl: v8.optional(v8.string()) });
2424
+ function listOtherOpenAiProviders(current) {
2425
+ const parsed = parseJsoncConfig(current, ModelProvidersSchema);
2426
+ if (parsed.kind !== "valid") return [];
2427
+ const openai = parsed.value.modelProviders?.openai;
2428
+ if (openai === void 0) return [];
2429
+ if (!Array.isArray(openai)) throw new MergeConflictError();
2430
+ return openai.filter((entry) => !v8.is(NexosEntrySchema, entry));
2431
+ }
2432
+ var SettingsSchema3 = v8.looseObject({
2433
+ env: v8.optional(v8.looseObject({ [KEY_VARIABLE]: v8.optional(v8.string()) })),
2434
+ model: v8.optional(v8.looseObject({ name: v8.optional(v8.string()) })),
2435
+ modelProviders: v8.optional(v8.looseObject({ openai: v8.optional(v8.unknown()) }))
2436
+ });
2437
+ function inspectSettings2(ctx) {
2438
+ const settings2 = readJsoncFile(resolveSettingsPath3(ctx), SettingsSchema3);
2439
+ if (settings2.kind === "absent") return { isConfigured: false, model: void 0, isUnreadable: false };
2440
+ if (settings2.kind === "invalid") return { isConfigured: false, model: void 0, isUnreadable: true };
2441
+ const openai = settings2.value.modelProviders?.openai;
2442
+ const entry = Array.isArray(openai) ? openai.find((candidate) => v8.is(NexosEntrySchema, candidate)) : void 0;
2443
+ const baseUrl = v8.is(NexosEntrySchema, entry) ? entry.baseUrl : void 0;
2444
+ return {
2445
+ isConfigured: isGatewayUrl(baseUrl, gatewayHostOf(ctx)),
2446
+ model: settings2.value.model?.name,
2447
+ isUnreadable: false
2448
+ };
2449
+ }
2450
+ function readProviderKey4(ctx) {
2451
+ const settings2 = readJsoncFile(resolveSettingsPath3(ctx), SettingsSchema3);
2452
+ return readLiteralKey(settings2.kind === "valid" ? settings2.value.env?.[KEY_VARIABLE] : void 0);
2453
+ }
2454
+ var SecretsSchema = v8.looseObject({
2455
+ env: v8.optional(v8.record(v8.string(), v8.unknown())),
2456
+ security: v8.optional(v8.looseObject({ auth: v8.optional(v8.looseObject({ apiKey: v8.optional(v8.string()) })) })),
2457
+ modelProviders: v8.optional(v8.record(v8.string(), v8.unknown()))
2458
+ });
2459
+ var ProviderEntrySchema = v8.looseObject({ envKey: v8.string() });
2460
+ function readProviderSecrets4(ctx) {
2461
+ const settings2 = readJsoncFile(resolveSettingsPath3(ctx), SecretsSchema);
2462
+ if (settings2.kind !== "valid") return [];
2463
+ const { env = {}, security, modelProviders = {} } = settings2.value;
2464
+ const fromEnv = listProviderEnvKeys(modelProviders).flatMap((name) => readEnvSecret(env, name));
2465
+ const fromAuth = readOptionalKey(security?.auth?.apiKey);
2466
+ return [...fromEnv, ...fromAuth];
2467
+ }
2468
+ function listProviderEnvKeys(modelProviders) {
2469
+ return Object.values(modelProviders).flatMap((providers) => {
2470
+ if (!Array.isArray(providers)) return [];
2471
+ return providers.flatMap((entry) => {
2472
+ const parsed = v8.safeParse(ProviderEntrySchema, entry);
2473
+ return parsed.success ? [parsed.output.envKey] : [];
2474
+ });
2475
+ });
2476
+ }
2477
+ function readEnvSecret(env, name) {
2478
+ const value = env[name];
2479
+ return typeof value === "string" ? readOptionalKey(value) : [];
2480
+ }
2481
+ function readOptionalKey(value) {
2482
+ const key = readLiteralKey(value);
2483
+ return key === void 0 ? [] : [key];
2484
+ }
2485
+
2486
+ // src/harnesses/registry.ts
2487
+ var harnesses = [claudeCode, codex, opencode, copilotCli, pi, crush, qwen, geminiCli];
2488
+ function findHarness(id) {
2489
+ return harnesses.find((harness) => harness.id === id);
2490
+ }
2491
+ function findHarnessByBinary(binary) {
2492
+ return harnesses.find((harness) => harness.binary === binary);
2493
+ }
2494
+
2495
+ // src/product/storage/env-file/split-lines.ts
2496
+ var LINE_BREAK = /\r?\n/;
2497
+ function splitLines(text2) {
2498
+ return text2.split(LINE_BREAK);
2499
+ }
2500
+
2501
+ // src/product/storage/env-file/syntax/env-syntaxes.ts
2502
+ import { match as match11 } from "ts-pattern";
2503
+
2504
+ // src/product/storage/env-file/syntax/posix-env-syntax.ts
2505
+ import path10 from "path";
2506
+
2507
+ // src/product/storage/env-file/syntax/env-syntax.ts
1925
2508
  var SHELLS = ["posix", "powershell"];
1926
2509
  var ENV_DIRECTORY_NAME = ".nexos";
1927
2510
  var MANAGED_HEADER_LINE = "# nexos.ai Gateway \u2014 managed by nexos-connect.";
@@ -1943,7 +2526,7 @@ function createAssignmentMatcher(pattern) {
1943
2526
  // src/product/storage/env-file/syntax/posix-env-syntax.ts
1944
2527
  var POSIX_FILE_NAME = "env";
1945
2528
  var POSIX_ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
1946
- var POSIX_LOAD_COMMAND = `source ${path7.posix.join("~", ENV_DIRECTORY_NAME, POSIX_FILE_NAME)}`;
2529
+ var POSIX_LOAD_COMMAND = `source ${path10.posix.join("~", ENV_DIRECTORY_NAME, POSIX_FILE_NAME)}`;
1947
2530
  var POSIX_ENV_SYNTAX = {
1948
2531
  fileName: POSIX_FILE_NAME,
1949
2532
  defaultLineEnding: "\n",
@@ -1974,10 +2557,10 @@ function decodePosixSegment([, singleQuoted, doubleQuoted, escaped, bare]) {
1974
2557
  }
1975
2558
 
1976
2559
  // src/product/storage/env-file/syntax/powershell-env-syntax.ts
1977
- import path8 from "path";
2560
+ import path11 from "path";
1978
2561
  var POWERSHELL_FILE_NAME = "env.ps1";
1979
2562
  var POWERSHELL_ASSIGNMENT = /^\s*\$\{?[Ee][Nn][Vv]:([A-Za-z_][A-Za-z0-9_]*)\}?\s*=\s*(.*)$/;
1980
- var POWERSHELL_LOAD_COMMAND = `. "${path8.win32.join("$HOME", ENV_DIRECTORY_NAME, POWERSHELL_FILE_NAME)}"`;
2563
+ var POWERSHELL_LOAD_COMMAND = `. "${path11.win32.join("$HOME", ENV_DIRECTORY_NAME, POWERSHELL_FILE_NAME)}"`;
1981
2564
  var POWERSHELL_ENV_SYNTAX = {
1982
2565
  fileName: POWERSHELL_FILE_NAME,
1983
2566
  defaultLineEnding: "\r\n",
@@ -2009,7 +2592,7 @@ var ENV_SYNTAXES = {
2009
2592
  powershell: POWERSHELL_ENV_SYNTAX
2010
2593
  };
2011
2594
  function resolveShell(platform) {
2012
- return match9(platform).returnType().with("win32", () => "powershell").otherwise(() => "posix");
2595
+ return match11(platform).returnType().with("win32", () => "powershell").otherwise(() => "posix");
2013
2596
  }
2014
2597
 
2015
2598
  // src/product/storage/env-file/parse-env-file.ts
@@ -2028,9 +2611,9 @@ function parseEnvFile(text2, shell) {
2028
2611
  }
2029
2612
 
2030
2613
  // src/flows/key/find-existing-keys.ts
2031
- var OfferableKeySchema = v6.pipe(
2032
- v6.string(),
2033
- v6.check((key) => !hasLineBreak(key))
2614
+ var OfferableKeySchema = v9.pipe(
2615
+ v9.string(),
2616
+ v9.check((key) => !hasLineBreak(key))
2034
2617
  );
2035
2618
  function findExistingKeys({ ctx, envPath, shell }) {
2036
2619
  const userContext = { ...ctx, scope: "user" };
@@ -2041,7 +2624,7 @@ function findExistingKeys({ ctx, envPath, shell }) {
2041
2624
  ...readFromEachHarness((harness) => harness.readSecrets?.(userContext) ?? []),
2042
2625
  ...readFromEachHarness((harness) => harness.readSecrets?.(projectContext) ?? [])
2043
2626
  ];
2044
- const offered = machineKeys.find((key) => v6.is(OfferableKeySchema, key));
2627
+ const offered = machineKeys.find((key) => v9.is(OfferableKeySchema, key));
2045
2628
  return { offered, all: [...machineKeys, ...otherSecrets] };
2046
2629
  }
2047
2630
  function readEnvFileKeys(envPath, shell) {
@@ -2076,7 +2659,7 @@ function readSafely(read) {
2076
2659
  }
2077
2660
 
2078
2661
  // src/flows/key/key-step.ts
2079
- import * as v7 from "valibot";
2662
+ import * as v10 from "valibot";
2080
2663
  async function resolveKey(input, deps) {
2081
2664
  const { keystore: keystore2, ui } = deps;
2082
2665
  if (input.shouldSaveKey) {
@@ -2101,11 +2684,11 @@ async function resolveKey(input, deps) {
2101
2684
  }
2102
2685
  return await promptForKey({ input, deps, availability, existing: existing.offered });
2103
2686
  }
2104
- var SavedKeySchema = v7.pipe(
2105
- v7.string(),
2106
- v7.trim(),
2107
- v7.nonEmpty(),
2108
- v7.check((key) => !hasLineBreak(key))
2687
+ var SavedKeySchema = v10.pipe(
2688
+ v10.string(),
2689
+ v10.trim(),
2690
+ v10.nonEmpty(),
2691
+ v10.check((key) => !hasLineBreak(key))
2109
2692
  );
2110
2693
  var FORGET_KEY_HINT = `npx ${COMMAND_NAME} ${FORGET_KEY_COMMAND}`;
2111
2694
  async function loadSaved({ keystore: keystore2, ui, masker: masker2 }) {
@@ -2121,7 +2704,7 @@ async function loadSaved({ keystore: keystore2, ui, masker: masker2 }) {
2121
2704
  return void 0;
2122
2705
  }
2123
2706
  if (saved === void 0) return void 0;
2124
- const parsed = v7.safeParse(SavedKeySchema, saved);
2707
+ const parsed = v10.safeParse(SavedKeySchema, saved);
2125
2708
  if (!parsed.success) {
2126
2709
  ui.dim(`The saved key is not usable. Run \`${FORGET_KEY_HINT}\` and save it again.`);
2127
2710
  return void 0;
@@ -2234,13 +2817,9 @@ async function replaceRejectedKey({
2234
2817
 
2235
2818
  // src/product/gateway/models/protocol.ts
2236
2819
  var PROTOCOLS = ["anthropic", "openai-chat", "openai-responses"];
2237
- var PROTOCOL_ENDPOINTS = {
2238
- anthropic: "messages",
2239
- "openai-chat": "chat_completion",
2240
- "openai-responses": "responses"
2241
- };
2820
+ var CONVERSATIONAL_ENDPOINTS = ["chat_completion", "messages", "responses"];
2242
2821
 
2243
- // src/ui/report/format-probes.ts
2822
+ // src/ui/report/detection/format-probes.ts
2244
2823
  import pc2 from "picocolors";
2245
2824
 
2246
2825
  // src/ui/glyphs.ts
@@ -2268,11 +2847,11 @@ function indentLines({ text: text2, indent }) {
2268
2847
  return lines.map((line) => line === "" ? "" : indent + line);
2269
2848
  }
2270
2849
 
2271
- // src/ui/report/format-probes.ts
2850
+ // src/ui/report/detection/format-probes.ts
2272
2851
  function formatProbes(results) {
2273
2852
  return [...formatHeading("Key check"), ...results.flatMap(formatProbe)];
2274
2853
  }
2275
- var SURFACE_COLUMN_WIDTH = 10;
2854
+ var SURFACE_COLUMN_WIDTH = 16;
2276
2855
  function formatProbe(result) {
2277
2856
  const colour = result.kind === "passed" ? pc2.dim : pc2.yellow;
2278
2857
  const glyph = result.kind === "passed" ? pc2.green(GLYPHS.success) : pc2.yellow(GLYPHS.warning);
@@ -2283,31 +2862,36 @@ function formatProbe(result) {
2283
2862
  }
2284
2863
 
2285
2864
  // src/flows/key/key-check.ts
2286
- import { match as match12 } from "ts-pattern";
2865
+ import { match as match14 } from "ts-pattern";
2287
2866
 
2288
2867
  // src/product/gateway/list-models.ts
2289
- import { P as P2, match as match10 } from "ts-pattern";
2868
+ import { P as P2, match as match12 } from "ts-pattern";
2290
2869
 
2291
2870
  // src/shared/text/format-count.ts
2292
2871
  function formatCount(count, noun) {
2293
2872
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
2294
2873
  }
2295
2874
 
2875
+ // src/product/gateway/error-codes.ts
2876
+ var INVALID_API_KEY_ERROR_CODE = "invalid_api_key";
2877
+ var MODEL_NOT_FOUND_ERROR_CODE = 100110;
2878
+ var MODEL_NOT_SUPPORTED_ERROR_CODE = 100800;
2879
+
2296
2880
  // src/product/gateway/model-schema.ts
2297
- import * as v8 from "valibot";
2298
- var OptionalTextSchema2 = v8.fallback(v8.optional(v8.pipe(v8.string(), v8.nonEmpty())), void 0);
2299
- var OptionalCountSchema = v8.fallback(v8.optional(v8.pipe(v8.number(), v8.finite())), void 0);
2300
- var EndpointsSchema = v8.fallback(v8.pipe(v8.array(v8.unknown()), v8.transform(keepStrings)), []);
2301
- var ModelSchema2 = v8.pipe(
2302
- v8.object({
2303
- id: v8.pipe(v8.string(), v8.nonEmpty()),
2881
+ import * as v11 from "valibot";
2882
+ var OptionalTextSchema2 = v11.fallback(v11.optional(v11.pipe(v11.string(), v11.nonEmpty())), void 0);
2883
+ var OptionalCountSchema = v11.fallback(v11.optional(v11.pipe(v11.number(), v11.finite())), void 0);
2884
+ var EndpointsSchema = v11.fallback(v11.pipe(v11.array(v11.unknown()), v11.transform(keepStrings)), []);
2885
+ var ModelSchema2 = v11.pipe(
2886
+ v11.object({
2887
+ id: v11.pipe(v11.string(), v11.nonEmpty()),
2304
2888
  developer: OptionalTextSchema2,
2305
2889
  owned_by: OptionalTextSchema2,
2306
2890
  context_length: OptionalCountSchema,
2307
2891
  region: OptionalTextSchema2,
2308
2892
  endpoints: EndpointsSchema
2309
2893
  }),
2310
- v8.transform((entry) => ({
2894
+ v11.transform((entry) => ({
2311
2895
  id: entry.id,
2312
2896
  developer: entry.developer ?? entry.owned_by ?? null,
2313
2897
  contextLength: entry.context_length ?? null,
@@ -2315,23 +2899,23 @@ var ModelSchema2 = v8.pipe(
2315
2899
  endpoints: entry.endpoints
2316
2900
  }))
2317
2901
  );
2318
- var ModelListSchema = v8.pipe(
2319
- v8.object({ data: v8.array(v8.unknown()) }),
2320
- v8.transform(({ data }) => keepUsableModels(data))
2902
+ var ModelListSchema = v11.pipe(
2903
+ v11.object({ data: v11.array(v11.unknown()) }),
2904
+ v11.transform(({ data }) => keepUsableModels(data))
2321
2905
  );
2322
- var OpenAiErrorSchema = v8.object({
2323
- error: v8.object({
2324
- message: v8.string(),
2325
- type: v8.optional(v8.string()),
2326
- code: v8.optional(v8.nullable(v8.string()))
2906
+ var OpenAiErrorSchema = v11.object({
2907
+ error: v11.object({
2908
+ message: v11.string(),
2909
+ type: v11.optional(v11.string()),
2910
+ code: v11.optional(v11.nullable(v11.union([v11.string(), v11.number()])))
2327
2911
  })
2328
2912
  });
2329
- var AnthropicErrorSchema = v8.object({
2330
- type: v8.literal("error"),
2331
- error: v8.object({ type: v8.string(), message: v8.string() })
2913
+ var AnthropicErrorSchema = v11.object({
2914
+ type: v11.literal("error"),
2915
+ error: v11.object({ type: v11.string(), message: v11.string() })
2332
2916
  });
2333
2917
  function parseJsonBody(schema, text2) {
2334
- const result = v8.safeParse(schema, parseJson(text2));
2918
+ const result = v11.safeParse(schema, parseJson(text2));
2335
2919
  return result.success ? result.output : void 0;
2336
2920
  }
2337
2921
  function keepStrings(values) {
@@ -2340,7 +2924,7 @@ function keepStrings(values) {
2340
2924
  function keepUsableModels(entries) {
2341
2925
  const modelsById = /* @__PURE__ */ new Map();
2342
2926
  for (const entry of entries) {
2343
- const model = v8.safeParse(ModelSchema2, entry);
2927
+ const model = v11.safeParse(ModelSchema2, entry);
2344
2928
  if (model.success && !modelsById.has(model.output.id)) modelsById.set(model.output.id, model.output);
2345
2929
  }
2346
2930
  return [...modelsById.values()];
@@ -2400,15 +2984,18 @@ function readListing({ status, text: text2 }, masker2) {
2400
2984
  const detail = `HTTP ${status} \u2014 authenticated, ${formatCount(models.length, "model")} available`;
2401
2985
  return { kind: "passed", detail, models, status };
2402
2986
  }
2403
- var INVALID_KEY_ERROR_CODE = "invalid_api_key";
2404
2987
  function classifyRejection(status, errorCode) {
2405
- return match10({ status, errorCode }).returnType().with({ status: P2.union(...KEY_REJECTED_STATUSES) }, { errorCode: INVALID_KEY_ERROR_CODE }, () => "key-rejected").otherwise(() => "bad-status");
2988
+ return match12({ status, errorCode }).returnType().with(
2989
+ { status: P2.union(...KEY_REJECTED_STATUSES) },
2990
+ { errorCode: INVALID_API_KEY_ERROR_CODE },
2991
+ () => "key-rejected"
2992
+ ).otherwise(() => "bad-status");
2406
2993
  }
2407
2994
 
2408
- // src/product/gateway/probe-result.ts
2409
- import { match as match11 } from "ts-pattern";
2995
+ // src/product/gateway/probes/probe-result.ts
2996
+ import { match as match13 } from "ts-pattern";
2410
2997
  function isKeyAccepted(probe) {
2411
- return match11(probe.kind).with("passed", "bad-response", () => true).with("key-rejected", "model-rejected", "unreachable", "timed-out", "bad-status", () => false).exhaustive();
2998
+ return match13(probe.kind).with("passed", "bad-response", () => true).with("key-rejected", "model-rejected", "unreachable", "timed-out", "bad-status", () => false).exhaustive();
2412
2999
  }
2413
3000
 
2414
3001
  // src/flows/key/probe-summaries.ts
@@ -2476,7 +3063,7 @@ async function attemptKeyCheck(params) {
2476
3063
  }
2477
3064
  ui.print(formatProbes([probe]));
2478
3065
  const choice = await ui.chooseOnFetchFailure();
2479
- return await match12(choice).with("retry", async () => {
3066
+ return await match14(choice).with("retry", async () => {
2480
3067
  spinner.start("Checking again\u2026");
2481
3068
  return await attemptKeyCheck(params);
2482
3069
  }).with("manual", async () => ({
@@ -2496,7 +3083,7 @@ async function attemptKeyCheck(params) {
2496
3083
  })).exhaustive();
2497
3084
  }
2498
3085
  function summarizeKeyCheck(probe, modelCount) {
2499
- return match12(probe.kind).with("passed", () => `Key verified \u2014 ${formatCount(modelCount, "model")} available.`).with("bad-response", () => "Key verified, but the model list could not be read.").otherwise((kind) => PROBE_SUMMARIES[kind]);
3086
+ return match14(probe.kind).with("passed", () => `Key verified \u2014 ${formatCount(modelCount, "model")} available.`).with("bad-response", () => "Key verified, but the model list could not be read.").otherwise((kind) => PROBE_SUMMARIES[kind]);
2500
3087
  }
2501
3088
  var ASSUMED_YES_CONTINUATION = "Continuing despite the failed check (--yes).";
2502
3089
  function findContinuation({ shouldAssumeYes, requestedModel, masker: masker2 }) {
@@ -2505,16 +3092,13 @@ function findContinuation({ shouldAssumeYes, requestedModel, masker: masker2 })
2505
3092
  return masker2.maskText(`Continuing despite the failed check \u2014 "${requestedModel}" was named explicitly.`);
2506
3093
  }
2507
3094
 
2508
- // src/product/gateway/models/model-compatibility.ts
2509
- function isModelCompatible(model, protocols) {
2510
- return findUnservedProtocols(model, protocols).length === 0;
2511
- }
2512
- function findUnservedProtocols(model, protocols) {
2513
- if (model.endpoints.length === 0) return [];
2514
- return protocols.filter((protocol) => !model.endpoints.includes(PROTOCOL_ENDPOINTS[protocol]));
3095
+ // src/product/gateway/models/model-capability.ts
3096
+ function isConversationalModel(model) {
3097
+ if (model.endpoints.length === 0) return true;
3098
+ return CONVERSATIONAL_ENDPOINTS.some((endpoint) => model.endpoints.includes(endpoint));
2515
3099
  }
2516
3100
 
2517
- // src/ui/model-choices.ts
3101
+ // src/ui/prompts/model/model-choices.ts
2518
3102
  var BROWSE_VALUE = "\0browse";
2519
3103
  var MANUAL_VALUE = "\0manual";
2520
3104
  var MANUAL_ROW = {
@@ -2522,9 +3106,9 @@ var MANUAL_ROW = {
2522
3106
  label: "Type a model id\u2026",
2523
3107
  hint: "no model list available"
2524
3108
  };
2525
- function buildModelChoices(models, protocols, isRouterOffered) {
2526
- const compatible = models.filter((model) => isModelCompatible(model, protocols));
2527
- const usable = compatible.length > 0 ? compatible : models;
3109
+ function buildModelChoices(models, isRouterOffered) {
3110
+ const conversational = models.filter(isConversationalModel);
3111
+ const usable = conversational.length > 0 ? conversational : models;
2528
3112
  const browsable = [...usable].sort((first, second) => first.id.localeCompare(second.id));
2529
3113
  const hiddenCount = models.length - usable.length;
2530
3114
  const routerRows = isRouterOffered ? PREDEFINED_MODELS.map(({ id, hint }) => ({ value: id, label: id, hint })) : [];
@@ -2560,7 +3144,6 @@ async function chooseModel({
2560
3144
  requestedModel,
2561
3145
  routerAvailability,
2562
3146
  shouldAssumeYes,
2563
- protocols,
2564
3147
  suggestions,
2565
3148
  ui
2566
3149
  }) {
@@ -2574,43 +3157,28 @@ async function chooseModel({
2574
3157
  }
2575
3158
  return DEFAULT_MODEL;
2576
3159
  }
2577
- return await ui.selectModel(
2578
- addSuggestionRows(buildModelChoices(keyCheck.models, protocols, isRouterOffered), suggestions)
2579
- );
3160
+ return await ui.selectModel(addSuggestionRows(buildModelChoices(keyCheck.models, isRouterOffered), suggestions));
2580
3161
  }
2581
- function findModelWarnings({
2582
- model,
2583
- models,
2584
- configurable,
2585
- routerAvailability
2586
- }) {
3162
+ function findModelWarnings({ model, models, routerAvailability }) {
2587
3163
  if (isPredefinedModel(model)) {
2588
3164
  return routerAvailability === "unavailable" ? [`"${model}" is not available for this key. Continuing because you named it.`] : [];
2589
3165
  }
2590
3166
  if (models.length === 0) return [];
2591
- const listed = models.find((candidate) => candidate.id === model);
2592
- if (listed === void 0) {
2593
- return [`"${model}" is not in the list this key can see. Continuing \u2014 the gateway may still accept it.`];
2594
- }
2595
- return configurable.flatMap((harness) => {
2596
- const unserved = findUnservedProtocols(listed, harness.protocols);
2597
- if (unserved.length === 0) return [];
2598
- const endpoints = unserved.map((protocol) => PROTOCOL_ENDPOINTS[protocol]).join(", ");
2599
- return [`${harness.label}: "${model}" does not advertise ${endpoints}. It may still work.`];
2600
- });
3167
+ if (models.some((candidate) => candidate.id === model)) return [];
3168
+ return [`"${model}" is not in the list this key can see. Continuing \u2014 the gateway may still accept it.`];
2601
3169
  }
2602
3170
 
2603
3171
  // src/flows/model/model-probe.ts
2604
- import { match as match15 } from "ts-pattern";
3172
+ import { match as match18 } from "ts-pattern";
2605
3173
 
2606
3174
  // src/product/gateway/models/router-availability.ts
2607
- import { match as match13 } from "ts-pattern";
3175
+ import { match as match15 } from "ts-pattern";
2608
3176
  function classifyRouterAvailability(probe) {
2609
- return match13(probe.kind).returnType().with("passed", () => "available").with("model-rejected", () => "unavailable").otherwise(() => "unknown");
3177
+ return match15(probe.kind).returnType().with("passed", () => "available").with("model-rejected", () => "unavailable").otherwise(() => "unknown");
2610
3178
  }
2611
3179
 
2612
- // src/product/gateway/probe-anthropic.ts
2613
- import { P as P3, match as match14 } from "ts-pattern";
3180
+ // src/product/gateway/probes/probe-anthropic.ts
3181
+ import { P as P3, match as match16 } from "ts-pattern";
2614
3182
  var MESSAGES_PATH = "/v1/messages";
2615
3183
  var ANTHROPIC_VERSION = "2023-06-01";
2616
3184
  var PING_MAX_TOKENS = 1;
@@ -2666,23 +3234,116 @@ var MODEL_NOT_FOUND_STATUS = 404;
2666
3234
  var KEY_REJECTED_ERROR_TYPES = ["authentication_error", "permission_error"];
2667
3235
  var MODEL_NOT_FOUND_ERROR_TYPE = "not_found_error";
2668
3236
  function classifyRejection2(status, errorType) {
2669
- return match14({ status, errorType }).returnType().with(
3237
+ return match16({ status, errorType }).returnType().with(
2670
3238
  { status: P3.union(...KEY_REJECTED_STATUSES) },
2671
3239
  { errorType: P3.union(...KEY_REJECTED_ERROR_TYPES) },
2672
3240
  () => "key-rejected"
2673
3241
  ).with({ status: MODEL_NOT_FOUND_STATUS }, { errorType: MODEL_NOT_FOUND_ERROR_TYPE }, () => "model-rejected").otherwise(() => "bad-status");
2674
3242
  }
2675
3243
 
3244
+ // src/product/gateway/probes/probe-openai.ts
3245
+ import { P as P4, match as match17 } from "ts-pattern";
3246
+ var CHAT_COMPLETIONS_PATH = "/chat/completions";
3247
+ var RESPONSES_PATH = "/responses";
3248
+ var BEARER_PREFIX2 = "Bearer ";
3249
+ var PING_TEXT2 = "ping";
3250
+ var CHAT_MAX_TOKENS = 1;
3251
+ var RESPONSES_MAX_OUTPUT_TOKENS = 16;
3252
+ async function probeOpenAi({
3253
+ openaiBaseUrl,
3254
+ apiKey,
3255
+ model,
3256
+ protocol,
3257
+ masker: masker2,
3258
+ fetch: fetch2,
3259
+ signal
3260
+ }) {
3261
+ const endpoint = `${trimTrailingSlashes(openaiBaseUrl)}${endpointPath(protocol)}`;
3262
+ const probe = (kind, detail, status) => ({
3263
+ kind,
3264
+ surface: protocol,
3265
+ endpoint,
3266
+ detail,
3267
+ ...status === void 0 ? {} : { status }
3268
+ });
3269
+ try {
3270
+ const response = await requestGateway({
3271
+ url: endpoint,
3272
+ init: {
3273
+ method: "POST",
3274
+ headers: { "content-type": "application/json", authorization: `${BEARER_PREFIX2}${apiKey}` },
3275
+ body: JSON.stringify(buildBody(protocol, model))
3276
+ },
3277
+ fetch: fetch2,
3278
+ timeoutMs: GATEWAY_TIMEOUT_MS,
3279
+ signal
3280
+ });
3281
+ if (isSuccessStatus(response.status)) {
3282
+ return probe("passed", `HTTP ${response.status} \u2014 authenticated`, response.status);
3283
+ }
3284
+ const rejection = parseJsonBody(OpenAiErrorSchema, response.text)?.error;
3285
+ const detail = formatHttpFailure(response.status, rejection?.message ?? response.text, masker2);
3286
+ return probe(classifyRejection3(response.status, rejection?.code ?? void 0), detail, response.status);
3287
+ } catch (error) {
3288
+ if (!isGatewayFailure(error)) throw error;
3289
+ return probe(error.kind, describeGatewayFailure(error, masker2), void 0);
3290
+ }
3291
+ }
3292
+ function endpointPath(protocol) {
3293
+ return match17(protocol).with("openai-chat", () => CHAT_COMPLETIONS_PATH).with("openai-responses", () => RESPONSES_PATH).exhaustive();
3294
+ }
3295
+ function buildBody(protocol, model) {
3296
+ return match17(protocol).with("openai-chat", () => ({
3297
+ model,
3298
+ max_tokens: CHAT_MAX_TOKENS,
3299
+ messages: [{ role: "user", content: PING_TEXT2 }]
3300
+ })).with("openai-responses", () => ({
3301
+ model,
3302
+ input: PING_TEXT2,
3303
+ max_output_tokens: RESPONSES_MAX_OUTPUT_TOKENS
3304
+ })).exhaustive();
3305
+ }
3306
+ function classifyRejection3(status, errorCode) {
3307
+ return match17({ status, errorCode }).returnType().with(
3308
+ { status: P4.union(...KEY_REJECTED_STATUSES) },
3309
+ { errorCode: INVALID_API_KEY_ERROR_CODE },
3310
+ () => "key-rejected"
3311
+ ).with(
3312
+ { errorCode: MODEL_NOT_FOUND_ERROR_CODE },
3313
+ { errorCode: MODEL_NOT_SUPPORTED_ERROR_CODE },
3314
+ () => "model-rejected"
3315
+ ).otherwise(() => "bad-status");
3316
+ }
3317
+
2676
3318
  // src/flows/model/model-probe.ts
2677
3319
  async function probeModel(params) {
2678
- const { anthropicBaseUrl, key, model, ui, masker: masker2, fetch: fetch2 } = params;
3320
+ const { model, protocols, ui, masker: masker2 } = params;
2679
3321
  const spinner = ui.spinner();
2680
3322
  spinner.start(masker2.maskText(`Checking "${model}" against the gateway\u2026`));
2681
- const probe = await spinner.waitFor(
2682
- (signal) => probeAnthropic({ anthropicBaseUrl, apiKey: key, model, masker: masker2, fetch: fetch2, signal })
3323
+ const probes = await spinner.waitFor(
3324
+ (signal) => Promise.all(protocols.map((protocol) => probeProtocol({ ...params, protocol, signal })))
2683
3325
  );
2684
- spinner.stop(summarizeModelProbe(probe));
2685
- return probe;
3326
+ spinner.stop(summarizeModelProbes(probes));
3327
+ return probes;
3328
+ }
3329
+ function probeProtocol({
3330
+ anthropicBaseUrl,
3331
+ openaiBaseUrl,
3332
+ key,
3333
+ model,
3334
+ protocol,
3335
+ reuse,
3336
+ masker: masker2,
3337
+ fetch: fetch2,
3338
+ signal
3339
+ }) {
3340
+ const reused = reuse?.[protocol];
3341
+ if (reused !== void 0) return Promise.resolve(reused);
3342
+ return match18(protocol).with("anthropic", () => probeAnthropic({ anthropicBaseUrl, apiKey: key, model, masker: masker2, fetch: fetch2, signal })).with(
3343
+ "openai-chat",
3344
+ "openai-responses",
3345
+ (openAiProtocol) => probeOpenAi({ openaiBaseUrl, apiKey: key, model, protocol: openAiProtocol, masker: masker2, fetch: fetch2, signal })
3346
+ ).exhaustive();
2686
3347
  }
2687
3348
  async function probeRouter({
2688
3349
  anthropicBaseUrl,
@@ -2709,22 +3370,35 @@ async function shouldContinueAfterProbe({
2709
3370
  }
2710
3371
  return await ui.confirm(`Continue anyway? ${consequence}`, false);
2711
3372
  }
2712
- function summarizeModelProbe(probe) {
2713
- return match15(probe.kind).with("passed", () => "Model verified.").otherwise((kind) => PROBE_SUMMARIES[kind]);
3373
+ function summarizeModelProbes(probes) {
3374
+ const failed = probes.find((probe) => probe.kind !== "passed");
3375
+ return failed === void 0 ? "Model verified." : PROBE_SUMMARIES[failed.kind];
2714
3376
  }
2715
3377
 
2716
3378
  // src/product/storage/preferences/preferences.ts
2717
3379
  import { randomUUID } from "crypto";
2718
3380
  import fs4 from "fs";
2719
- import path9 from "path";
2720
- import * as v9 from "valibot";
3381
+ import path12 from "path";
3382
+ import * as v12 from "valibot";
3383
+
3384
+ // src/shared/file-formats/json/parse-json.ts
3385
+ function parseJson2(text2) {
3386
+ try {
3387
+ return JSON.parse(text2);
3388
+ } catch (error) {
3389
+ if (error instanceof SyntaxError) return void 0;
3390
+ throw error;
3391
+ }
3392
+ }
3393
+
3394
+ // src/product/storage/preferences/preferences.ts
2721
3395
  var PREFERENCES_FILE = "preferences.json";
2722
3396
  function resolvePreferencesPath(home) {
2723
- return path9.join(home, ENV_DIRECTORY_NAME, PREFERENCES_FILE);
3397
+ return path12.join(home, ENV_DIRECTORY_NAME, PREFERENCES_FILE);
2724
3398
  }
2725
- var ModelIdSchema = v9.pipe(v9.string(), v9.trim(), v9.nonEmpty(), v9.transform(normalizeModel));
2726
- var PreferencesSchema = v9.looseObject({
2727
- defaultModels: v9.optional(v9.record(v9.string(), ModelIdSchema), {})
3399
+ var ModelIdSchema = v12.pipe(v12.string(), v12.trim(), v12.nonEmpty(), v12.transform(normalizeModel));
3400
+ var PreferencesSchema = v12.looseObject({
3401
+ defaultModels: v12.optional(v12.record(v12.string(), ModelIdSchema), {})
2728
3402
  });
2729
3403
  var PreferencesError = class extends Error {
2730
3404
  name = "PreferencesError";
@@ -2743,7 +3417,7 @@ function readPreferences(home) {
2743
3417
  const filePath = resolvePreferencesPath(home);
2744
3418
  const text2 = readFileIfExists(filePath);
2745
3419
  if (text2 === null) return { defaultModels: {} };
2746
- const parsed = v9.safeParse(PreferencesSchema, parseJson2(text2));
3420
+ const parsed = v12.safeParse(PreferencesSchema, parseJson2(text2));
2747
3421
  if (!parsed.success) {
2748
3422
  throw new PreferencesError(
2749
3423
  `${displayPath(filePath, home)} is not a preferences file nexos-connect can read. Fix it or delete it.`
@@ -2751,16 +3425,8 @@ function readPreferences(home) {
2751
3425
  }
2752
3426
  return parsed.output;
2753
3427
  }
2754
- function parseJson2(text2) {
2755
- try {
2756
- return JSON.parse(text2);
2757
- } catch (error) {
2758
- if (error instanceof SyntaxError) return void 0;
2759
- throw error;
2760
- }
2761
- }
2762
3428
  function writeAtomically(filePath, content) {
2763
- fs4.mkdirSync(path9.dirname(filePath), { recursive: true });
3429
+ fs4.mkdirSync(path12.dirname(filePath), { recursive: true });
2764
3430
  const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
2765
3431
  try {
2766
3432
  fs4.writeFileSync(temporaryPath, content);
@@ -2771,20 +3437,20 @@ function writeAtomically(filePath, content) {
2771
3437
  }
2772
3438
  }
2773
3439
 
2774
- // src/shared/file-system/describe-file-error.ts
2775
- import * as v10 from "valibot";
3440
+ // src/shared/file-system/errors/describe-file-error.ts
3441
+ import * as v13 from "valibot";
2776
3442
  function describeFileError({ error, action, home }) {
2777
3443
  const failure = readFileFailure(error);
2778
3444
  if (failure === void 0) return describeError(error);
2779
3445
  return `${displayPath(failure.path, home)}: cannot ${action} it (${failure.reason}). ${failure.nextStep}, then run again.`;
2780
3446
  }
2781
- var FileSystemErrorSchema = v10.object({ code: v10.string(), path: v10.string() });
3447
+ var FileSystemErrorSchema = v13.object({ code: v13.string(), path: v13.string() });
2782
3448
  var PERMISSION_CODES = ["EACCES", "EPERM", "EROFS"];
2783
3449
  function readFileFailure(error) {
2784
3450
  if (error instanceof Utf16FileError) {
2785
3451
  return { path: error.filePath, reason: "saved as UTF-16", nextStep: "Re-save it as UTF-8" };
2786
3452
  }
2787
- if (!v10.is(FileSystemErrorSchema, error)) return void 0;
3453
+ if (!v13.is(FileSystemErrorSchema, error)) return void 0;
2788
3454
  const nextStep = PERMISSION_CODES.includes(error.code) ? "Check its permissions" : "Check that path";
2789
3455
  return { path: error.path, reason: error.code, nextStep };
2790
3456
  }
@@ -2794,18 +3460,18 @@ function findOwnDefaults(defaultModels, targets) {
2794
3460
  return groupByModel(defaultModels, targets);
2795
3461
  }
2796
3462
  function findBorrowedDefaults(params) {
2797
- const { defaultModels, targets, models, protocols } = params;
3463
+ const { defaultModels, targets, models } = params;
2798
3464
  const targetIds = new Set(targets.map((harness) => harness.id));
2799
3465
  const ownModels = new Set(targets.flatMap((harness) => defaultModels[harness.id] ?? []));
2800
3466
  const others = params.harnesses.filter((harness) => !targetIds.has(harness.id));
2801
3467
  return groupByModel(defaultModels, others).filter(
2802
- ({ model }) => !ownModels.has(model) && !isPredefinedModel(model) && isServed({ model, models, protocols })
3468
+ ({ model }) => !ownModels.has(model) && !isPredefinedModel(model) && isServed({ model, models })
2803
3469
  );
2804
3470
  }
2805
- function isServed({ model, models, protocols }) {
3471
+ function isServed({ model, models }) {
2806
3472
  if (models.length === 0) return true;
2807
3473
  const listed = models.find((candidate) => candidate.id === model);
2808
- return listed !== void 0 && isModelCompatible(listed, protocols);
3474
+ return listed !== void 0 && isConversationalModel(listed);
2809
3475
  }
2810
3476
  function groupByModel(defaultModels, harnesses2) {
2811
3477
  const labelsByModel = /* @__PURE__ */ new Map();
@@ -2892,8 +3558,8 @@ async function chooseCheckedModel(params) {
2892
3558
  requestedModel,
2893
3559
  shouldAssumeYes: options.shouldAssumeYes,
2894
3560
  resolved: keyStep.resolved,
2895
- onSavedKeyRejected: async (rejected, probe2) => {
2896
- const replaced = await replaceRejectedKey({ input: keyStep.input, rejected, probe: probe2, deps: keyStep.deps });
3561
+ onSavedKeyRejected: async (rejected, probe) => {
3562
+ const replaced = await replaceRejectedKey({ input: keyStep.input, rejected, probe, deps: keyStep.deps });
2897
3563
  if (replaced === null) return null;
2898
3564
  if (!isKeyUsable(replaced.key)) return null;
2899
3565
  adoptKey(replaced.key, { ui, masker: masker2 });
@@ -2915,8 +3581,7 @@ async function chooseCheckedModel(params) {
2915
3581
  defaultModels: preferences.defaultModels,
2916
3582
  targets: configurable,
2917
3583
  harnesses,
2918
- models: keyCheck.models,
2919
- protocols
3584
+ models: keyCheck.models
2920
3585
  });
2921
3586
  const { shouldAssumeYes } = options;
2922
3587
  const model = await chooseModel({
@@ -2924,11 +3589,10 @@ async function chooseCheckedModel(params) {
2924
3589
  requestedModel,
2925
3590
  routerAvailability,
2926
3591
  shouldAssumeYes,
2927
- protocols,
2928
3592
  suggestions,
2929
3593
  ui
2930
3594
  });
2931
- for (const warning of findModelWarnings({ model, models: keyCheck.models, configurable, routerAvailability })) {
3595
+ for (const warning of findModelWarnings({ model, models: keyCheck.models, routerAvailability })) {
2932
3596
  ui.log.warn(masker2.maskText(warning));
2933
3597
  }
2934
3598
  const source = requested?.source ?? "picked";
@@ -2940,21 +3604,26 @@ async function chooseCheckedModel(params) {
2940
3604
  check: keyCheck.outcome,
2941
3605
  modelSave
2942
3606
  };
2943
- if (!options.shouldVerify || !protocols.includes("anthropic")) {
3607
+ if (!options.shouldVerify || protocols.length === 0) {
2944
3608
  printProbes(keyCheck.probes, ui);
2945
3609
  return chosen;
2946
3610
  }
2947
3611
  const reusedRouterProbe = model === DEFAULT_MODEL ? routerProbe : void 0;
2948
- const probe = reusedRouterProbe === void 0 ? await probeModel({
3612
+ const probes = await probeModel({
2949
3613
  ...gatewayParams,
2950
3614
  anthropicBaseUrl: gatewayUrls.anthropicBaseUrl,
3615
+ openaiBaseUrl: gatewayUrls.openaiBaseUrl,
2951
3616
  model,
2952
- key: keyCheck.resolvedKey.key
2953
- }) : reusedRouterProbe.probe;
2954
- printProbes([...keyCheck.probes, probe], ui);
3617
+ key: keyCheck.resolvedKey.key,
3618
+ protocols,
3619
+ ...reusedRouterProbe === void 0 ? {} : { reuse: { anthropic: reusedRouterProbe.probe } }
3620
+ });
3621
+ printProbes([...keyCheck.probes, ...probes], ui);
2955
3622
  if (reusedRouterProbe !== void 0 && routerAvailability === "unknown") return chosen;
3623
+ const failed = probes.find((probe) => probe.kind !== "passed");
3624
+ if (failed === void 0) return chosen;
2956
3625
  const shouldContinue = await shouldContinueAfterProbe({
2957
- probe,
3626
+ probe: failed,
2958
3627
  shouldAssumeYes,
2959
3628
  consequence: params.probeConsequence,
2960
3629
  ui
@@ -2978,9 +3647,9 @@ async function probeRouterIfOffered({
2978
3647
  }
2979
3648
 
2980
3649
  // src/product/storage/env-file/env-file-path.ts
2981
- import path10 from "path";
3650
+ import path13 from "path";
2982
3651
  function resolveEnvFilePath(home, shell) {
2983
- return path10.join(home, ENV_DIRECTORY_NAME, ENV_SYNTAXES[shell].fileName);
3652
+ return path13.join(home, ENV_DIRECTORY_NAME, ENV_SYNTAXES[shell].fileName);
2984
3653
  }
2985
3654
 
2986
3655
  // src/commands/launch/launch.ts
@@ -2990,8 +3659,8 @@ async function launch(input, rawDeps) {
2990
3659
  const { masker: masker2, ctx } = rawDeps;
2991
3660
  const ui = ctx.isInteractive ? rawDeps.ui : refusePrompts(rawDeps.ui);
2992
3661
  if (!ctx.hasBinary(harness.binary)) {
2993
- ui.log.error(`${harness.binary} not found on PATH. Install ${harness.label} first.`);
2994
- return { code: EXIT_CODE.failed };
3662
+ const stopped = await offerInstall({ harness, options, ui, ctx, runNpm: rawDeps.runNpm });
3663
+ if (stopped !== null) return stopped;
2995
3664
  }
2996
3665
  const environment = readEnvironmentInputs(input.env);
2997
3666
  const gatewayUrls = buildGatewayUrls(environment.gatewayHost ?? GATEWAY_HOST);
@@ -3032,6 +3701,37 @@ async function launch(input, rawDeps) {
3032
3701
  }
3033
3702
  return await rawDeps.runLaunch(recipe, input.passthrough);
3034
3703
  }
3704
+ async function offerInstall({ harness, options, ui, ctx, runNpm: runNpm2 }) {
3705
+ if (harness.installPackage === void 0) {
3706
+ ui.log.error(describeMissingBinary(harness));
3707
+ return { code: EXIT_CODE.failed };
3708
+ }
3709
+ const outcome = await installHarness({
3710
+ label: harness.label,
3711
+ binary: harness.binary,
3712
+ packageName: harness.installPackage,
3713
+ shouldInstall: options.shouldInstall,
3714
+ shouldAssumeYes: options.shouldAssumeYes,
3715
+ ui,
3716
+ ctx,
3717
+ runNpm: runNpm2
3718
+ });
3719
+ if (outcome.kind === "installed") return null;
3720
+ if (outcome.kind === "declined") {
3721
+ ui.cancel("Aborted \u2014 the agent was not started.");
3722
+ return { code: EXIT_CODE.cancelled };
3723
+ }
3724
+ if (outcome.kind === "skipped") {
3725
+ ui.log.error(describeMissingBinary(harness));
3726
+ return { code: EXIT_CODE.failed };
3727
+ }
3728
+ if (outcome.kind === "not-on-path") {
3729
+ ui.log.error(describeNotOnPath(harness));
3730
+ return { code: EXIT_CODE.failed };
3731
+ }
3732
+ ui.log.error(outcome.message);
3733
+ return { code: EXIT_CODE.failed };
3734
+ }
3035
3735
  function aKeyStepInput({ ctx, options, environmentKey, masker: masker2 }) {
3036
3736
  const shell = resolveShell(ctx.platform);
3037
3737
  const existingKeys = findExistingKeys({ ctx, envPath: resolveEnvFilePath(ctx.home, shell), shell });
@@ -3048,7 +3748,7 @@ function aKeyStepInput({ ctx, options, environmentKey, masker: masker2 }) {
3048
3748
  }
3049
3749
 
3050
3750
  // src/commands/restore/run-restore.ts
3051
- import path18 from "path";
3751
+ import path21 from "path";
3052
3752
 
3053
3753
  // src/cli/resolve-named-harnesses.ts
3054
3754
  function resolveNamedHarnesses(ids) {
@@ -3062,61 +3762,61 @@ function resolveNamedHarnesses(ids) {
3062
3762
  }
3063
3763
 
3064
3764
  // src/product/storage/backups/resolve-backup-root.ts
3065
- import path11 from "path";
3066
- import { match as match16 } from "ts-pattern";
3765
+ import path14 from "path";
3766
+ import { match as match19 } from "ts-pattern";
3067
3767
  var PROJECT_BACKUP_DIRECTORY = ".nexos-backup";
3068
- var USER_BACKUP_DIRECTORY = path11.join(".nexos", "backups");
3768
+ var USER_BACKUP_DIRECTORY = path14.join(".nexos", "backups");
3069
3769
  function formatBackupTimestamp(now) {
3070
3770
  return now.toISOString().replace(/[:.]/g, "-");
3071
3771
  }
3072
3772
  function resolveBackupRoot({ scope, home, cwd, stamp, backupDirectory }) {
3073
3773
  if (backupDirectory !== void 0) {
3074
- return { path: path11.join(path11.resolve(cwd, backupDirectory), stamp), gitIgnoredDirectory: null };
3774
+ return { path: path14.join(path14.resolve(cwd, backupDirectory), stamp), gitIgnoredDirectory: null };
3075
3775
  }
3076
- return match16(scope).returnType().with("user", () => ({ path: path11.join(home, USER_BACKUP_DIRECTORY, stamp), gitIgnoredDirectory: null })).with("project", () => {
3077
- const directory = path11.join(cwd, PROJECT_BACKUP_DIRECTORY);
3078
- return { path: path11.join(directory, stamp), gitIgnoredDirectory: directory };
3776
+ return match19(scope).returnType().with("user", () => ({ path: path14.join(home, USER_BACKUP_DIRECTORY, stamp), gitIgnoredDirectory: null })).with("project", () => {
3777
+ const directory = path14.join(cwd, PROJECT_BACKUP_DIRECTORY);
3778
+ return { path: path14.join(directory, stamp), gitIgnoredDirectory: directory };
3079
3779
  }).exhaustive();
3080
3780
  }
3081
3781
 
3082
3782
  // src/product/storage/restore-record/restore-record.ts
3083
3783
  import fs6 from "fs";
3084
- import path13 from "path";
3085
- import * as v11 from "valibot";
3784
+ import path16 from "path";
3785
+ import * as v14 from "valibot";
3086
3786
 
3087
3787
  // src/shared/file-formats/config-format.ts
3088
- import { match as match17 } from "ts-pattern";
3788
+ import { match as match20 } from "ts-pattern";
3089
3789
  var CONFIG_FORMATS = ["jsonc", "toml"];
3090
3790
  function renderConfig(current, edits) {
3091
- return match17(edits).with({ format: "jsonc" }, ({ updates }) => renderJsonc(current, updates)).with({ format: "toml" }, ({ updates }) => renderToml(current, updates)).exhaustive();
3791
+ return match20(edits).with({ format: "jsonc" }, ({ updates }) => renderJsonc(current, updates)).with({ format: "toml" }, ({ updates }) => renderToml(current, updates)).exhaustive();
3092
3792
  }
3093
3793
  function readOriginals(current, edits) {
3094
3794
  const keyPaths = edits.updates.map(([keyPath]) => keyPath);
3095
- return match17(edits.format).with("jsonc", () => readJsoncOriginals(current, keyPaths)).with("toml", () => readTomlOriginals(current, keyPaths)).exhaustive();
3795
+ return match20(edits.format).with("jsonc", () => readJsoncOriginals(current, keyPaths)).with("toml", () => readTomlOriginals(current, keyPaths)).exhaustive();
3096
3796
  }
3097
3797
  function restoreConfig({ format, current, originals }) {
3098
- return match17(format).with("jsonc", () => restoreJsonc(current, originals)).with("toml", () => restoreToml(current, originals)).exhaustive();
3798
+ return match20(format).with("jsonc", () => restoreJsonc(current, originals)).with("toml", () => restoreToml(current, originals)).exhaustive();
3099
3799
  }
3100
3800
  function isEmptyConfig(format, text2) {
3101
- const document = match17(format).with("jsonc", () => parseJsonc(text2)).with("toml", () => parseToml(text2)).exhaustive();
3801
+ const document = match20(format).with("jsonc", () => parseJsonc(text2)).with("toml", () => parseToml(text2)).exhaustive();
3102
3802
  return Object.keys(document).length === 0;
3103
3803
  }
3104
3804
  function parseValueSource(format, source) {
3105
- return match17(format).with("jsonc", () => parseJsoncValue(source)).with("toml", () => parseTomlValue(source)).exhaustive();
3805
+ return match20(format).with("jsonc", () => parseJsoncValue(source)).with("toml", () => parseTomlValue(source)).exhaustive();
3106
3806
  }
3107
3807
 
3108
3808
  // src/shared/file-system/resolve-owner-only-mode.ts
3109
- import { match as match18 } from "ts-pattern";
3809
+ import { match as match21 } from "ts-pattern";
3110
3810
  var OWNER_ONLY_MODE = 384;
3111
3811
  function resolveOwnerOnlyMode(platform) {
3112
- return match18(platform).returnType().with("win32", () => null).otherwise(() => OWNER_ONLY_MODE);
3812
+ return match21(platform).returnType().with("win32", () => null).otherwise(() => OWNER_ONLY_MODE);
3113
3813
  }
3114
3814
 
3115
3815
  // src/shared/file-system/write-file-with-mode.ts
3116
3816
  import fs5 from "fs";
3117
- import path12 from "path";
3817
+ import path15 from "path";
3118
3818
  function writeFileWithMode({ filePath, content, mode }) {
3119
- fs5.mkdirSync(path12.dirname(filePath), { recursive: true });
3819
+ fs5.mkdirSync(path15.dirname(filePath), { recursive: true });
3120
3820
  if (mode === null) {
3121
3821
  fs5.writeFileSync(filePath, content);
3122
3822
  return;
@@ -3129,30 +3829,31 @@ function writeFileWithMode({ filePath, content, mode }) {
3129
3829
  // src/product/storage/restore-record/restore-record.ts
3130
3830
  var FILE_ORIGINS = ["absent", "blank", "content"];
3131
3831
  var RECORD_VERSION = 1;
3132
- var SegmentsSchema = v11.pipe(v11.tupleWithRest([v11.string()], v11.string()), v11.readonly());
3133
- var RecordedKeySchema = v11.object({
3832
+ var SegmentsSchema = v14.pipe(v14.tupleWithRest([v14.string()], v14.string()), v14.readonly());
3833
+ var RecordedKeySchema = v14.object({
3134
3834
  keyPath: SegmentsSchema,
3135
- original: v11.nullable(v11.string()),
3136
- createdParents: v11.pipe(v11.array(SegmentsSchema), v11.readonly()),
3137
- holdsSecret: v11.boolean()
3835
+ original: v14.nullable(v14.string()),
3836
+ createdParents: v14.pipe(v14.array(SegmentsSchema), v14.readonly()),
3837
+ holdsSecret: v14.boolean()
3138
3838
  });
3139
- var RecordedFileSchema = v11.object({
3839
+ var RecordedFileSchema = v14.object({
3140
3840
  path: SegmentsSchema,
3141
- format: v11.picklist(CONFIG_FORMATS),
3142
- origin: v11.picklist(FILE_ORIGINS),
3143
- keys: v11.pipe(v11.array(RecordedKeySchema), v11.readonly())
3841
+ format: v14.picklist(CONFIG_FORMATS),
3842
+ origin: v14.picklist(FILE_ORIGINS),
3843
+ keys: v14.pipe(v14.array(RecordedKeySchema), v14.readonly())
3144
3844
  });
3145
- var RecordedEnvVariableSchema = v11.object({ name: v11.string(), original: v11.nullable(v11.string()) });
3146
- var HarnessRecordSchema = v11.object({
3147
- files: v11.pipe(v11.array(RecordedFileSchema), v11.readonly()),
3148
- envVariables: v11.pipe(v11.array(RecordedEnvVariableSchema), v11.readonly())
3845
+ var RecordedEnvVariableSchema = v14.object({ name: v14.string(), original: v14.nullable(v14.string()) });
3846
+ var HarnessRecordSchema = v14.object({
3847
+ files: v14.pipe(v14.array(RecordedFileSchema), v14.readonly()),
3848
+ envVariables: v14.pipe(v14.array(RecordedEnvVariableSchema), v14.readonly())
3149
3849
  });
3150
- var RestoreRecordSchema = v11.object({
3151
- version: v11.literal(RECORD_VERSION),
3152
- harnesses: v11.record(v11.string(), HarnessRecordSchema)
3850
+ var RestoreRecordSchema = v14.object({
3851
+ version: v14.literal(RECORD_VERSION),
3852
+ harnesses: v14.record(v14.string(), HarnessRecordSchema),
3853
+ userEnvironment: v14.optional(v14.pipe(v14.array(RecordedEnvVariableSchema), v14.readonly()), [])
3153
3854
  });
3154
3855
  function createEmptyRestoreRecord() {
3155
- return { version: RECORD_VERSION, harnesses: {} };
3856
+ return { version: RECORD_VERSION, harnesses: {}, userEnvironment: [] };
3156
3857
  }
3157
3858
  var RestoreRecordError = class extends Error {
3158
3859
  name = "RestoreRecordError";
@@ -3161,11 +3862,11 @@ function readRestoreRecord({ filePath, home }) {
3161
3862
  const text2 = readFileIfExists(filePath);
3162
3863
  if (text2 === null) return null;
3163
3864
  const shownPath = displayPath(filePath, home);
3164
- const json = parseJson3(text2);
3865
+ const json = parseJson2(text2);
3165
3866
  if (isNewerRecord(json)) {
3166
3867
  throw new RestoreRecordError(`${shownPath} was written by a newer nexos-connect. Update it, then run again.`);
3167
3868
  }
3168
- const parsed = v11.safeParse(RestoreRecordSchema, json);
3869
+ const parsed = v14.safeParse(RestoreRecordSchema, json);
3169
3870
  if (!parsed.success) {
3170
3871
  throw new RestoreRecordError(
3171
3872
  `${shownPath} is not a restore record nexos-connect can read. Fix it, or move it aside to start a new one, then run again.`
@@ -3173,55 +3874,47 @@ function readRestoreRecord({ filePath, home }) {
3173
3874
  }
3174
3875
  return parsed.output;
3175
3876
  }
3176
- function parseJson3(text2) {
3177
- try {
3178
- return JSON.parse(text2);
3179
- } catch (error) {
3180
- if (error instanceof SyntaxError) return void 0;
3181
- throw error;
3182
- }
3183
- }
3184
- var VersionSchema = v11.object({ version: v11.number() });
3877
+ var VersionSchema = v14.object({ version: v14.number() });
3185
3878
  function isNewerRecord(json) {
3186
- const parsed = v11.safeParse(VersionSchema, json);
3879
+ const parsed = v14.safeParse(VersionSchema, json);
3187
3880
  return parsed.success && parsed.output.version > RECORD_VERSION;
3188
3881
  }
3189
3882
  var RECORD_DIRECTORY_MODE = 448;
3190
3883
  var JSON_INDENT3 = 2;
3191
- function writeRestoreRecord({ filePath, record: record5, platform }) {
3192
- if (Object.keys(record5.harnesses).length === 0) {
3884
+ function writeRestoreRecord({ filePath, record: record9, platform }) {
3885
+ if (Object.keys(record9.harnesses).length === 0 && record9.userEnvironment.length === 0) {
3193
3886
  fs6.rmSync(filePath, { force: true });
3194
3887
  return;
3195
3888
  }
3196
- fs6.mkdirSync(path13.dirname(filePath), { recursive: true, mode: RECORD_DIRECTORY_MODE });
3197
- const content = `${JSON.stringify(record5, null, JSON_INDENT3)}
3889
+ fs6.mkdirSync(path16.dirname(filePath), { recursive: true, mode: RECORD_DIRECTORY_MODE });
3890
+ const content = `${JSON.stringify(record9, null, JSON_INDENT3)}
3198
3891
  `;
3199
3892
  writeFileWithMode({ filePath, content, mode: resolveOwnerOnlyMode(platform) });
3200
3893
  }
3201
3894
 
3202
3895
  // src/product/storage/restore-record/restore-record-location.ts
3203
- import path14 from "path";
3204
- import { match as match19 } from "ts-pattern";
3896
+ import path17 from "path";
3897
+ import { match as match22 } from "ts-pattern";
3205
3898
  var RESTORE_RECORD_FILE = "restore-record.json";
3206
3899
  function resolveRestoreRecordLocation(ctx) {
3207
- return match19(ctx.scope).returnType().with("user", () => ({
3208
- filePath: path14.join(ctx.home, ENV_DIRECTORY_NAME, RESTORE_RECORD_FILE),
3900
+ return match22(ctx.scope).returnType().with("user", () => ({
3901
+ filePath: path17.join(ctx.home, ENV_DIRECTORY_NAME, RESTORE_RECORD_FILE),
3209
3902
  root: ctx.home,
3210
3903
  gitIgnoredDirectory: null
3211
3904
  })).with("project", () => {
3212
- const directory = path14.join(ctx.cwd, PROJECT_BACKUP_DIRECTORY);
3213
- return { filePath: path14.join(directory, RESTORE_RECORD_FILE), root: ctx.cwd, gitIgnoredDirectory: directory };
3905
+ const directory = path17.join(ctx.cwd, PROJECT_BACKUP_DIRECTORY);
3906
+ return { filePath: path17.join(directory, RESTORE_RECORD_FILE), root: ctx.cwd, gitIgnoredDirectory: directory };
3214
3907
  }).exhaustive();
3215
3908
  }
3216
3909
  function toRecordedPath(filePath, root) {
3217
- const [first, ...rest] = path14.relative(root, filePath).split(path14.sep);
3910
+ const [first, ...rest] = path17.relative(root, filePath).split(path17.sep);
3218
3911
  if (first === void 0 || !isInsideRoot(filePath, root)) {
3219
3912
  throw new Error(`${filePath} is outside ${root}, so it cannot be recorded for a restore.`);
3220
3913
  }
3221
3914
  return [first, ...rest];
3222
3915
  }
3223
3916
  function fromRecordedPath(recordedPath, root) {
3224
- const filePath = path14.join(root, ...recordedPath);
3917
+ const filePath = path17.join(root, ...recordedPath);
3225
3918
  if (!isInsideRoot(filePath, root)) {
3226
3919
  throw new Error(
3227
3920
  `${filePath} is outside ${root}, so it cannot be restored. Remove it from the restore record, then run again.`
@@ -3230,16 +3923,29 @@ function fromRecordedPath(recordedPath, root) {
3230
3923
  return filePath;
3231
3924
  }
3232
3925
  function isInsideRoot(filePath, root) {
3233
- const relativePath = path14.relative(root, filePath);
3234
- const [first] = relativePath.split(path14.sep);
3235
- return relativePath !== "" && first !== ".." && !path14.isAbsolute(relativePath);
3926
+ const relativePath = path17.relative(root, filePath);
3927
+ const [first] = relativePath.split(path17.sep);
3928
+ return relativePath !== "" && first !== ".." && !path17.isAbsolute(relativePath);
3236
3929
  }
3237
3930
 
3238
- // src/ui/report/format-restore-preview.ts
3239
- import pc5 from "picocolors";
3931
+ // src/ui/report/preview/format-restore-preview.ts
3932
+ import pc6 from "picocolors";
3240
3933
 
3241
- // src/ui/report/format-preview.ts
3242
- import pc4 from "picocolors";
3934
+ // src/ui/report/format-user-environment.ts
3935
+ import pc3 from "picocolors";
3936
+ var HEADING = "Windows user environment";
3937
+ function formatUserEnvironmentPreview({ userEnvironment: userEnvironment2, masker: masker2 }) {
3938
+ const lines = Object.entries(userEnvironment2.after).flatMap(([name, value]) => {
3939
+ if (userEnvironment2.before[name] === value) return [];
3940
+ if (value === void 0) return [`${INDENT}${pc3.red(`- ${name}`)}`];
3941
+ return [`${INDENT}${pc3.green(`+ ${name}=${masker2.maskText(value)}`)}`];
3942
+ });
3943
+ if (lines.length === 0) return [];
3944
+ return ["", ` ${pc3.bold(HEADING)}`, ...lines];
3945
+ }
3946
+
3947
+ // src/ui/report/preview/format-preview.ts
3948
+ import pc5 from "picocolors";
3243
3949
 
3244
3950
  // src/ui/diff/diff-lines.ts
3245
3951
  function diffLines({ before, after }) {
@@ -3300,15 +4006,15 @@ function buildCommonLengthTable({ beforeLines, afterLines }) {
3300
4006
  }
3301
4007
 
3302
4008
  // src/ui/diff/render-diff.ts
3303
- import pc3 from "picocolors";
3304
- import { match as match20 } from "ts-pattern";
4009
+ import pc4 from "picocolors";
4010
+ import { match as match23 } from "ts-pattern";
3305
4011
  var DIFF_CONTEXT_LINES = 2;
3306
4012
  var ADDED_PREFIX = " + ";
3307
4013
  var REMOVED_PREFIX = " - ";
3308
4014
  var UNCHANGED_PREFIX = " ";
3309
4015
  function renderDiff(lines) {
3310
4016
  const visibleIndexes = findVisibleIndexes(lines);
3311
- if (visibleIndexes.size === 0) return [pc3.dim(`${UNCHANGED_PREFIX}(no change)`)];
4017
+ if (visibleIndexes.size === 0) return [pc4.dim(`${UNCHANGED_PREFIX}(no change)`)];
3312
4018
  const rendered = [];
3313
4019
  let isSkipping = false;
3314
4020
  for (const [index, line] of lines.entries()) {
@@ -3317,7 +4023,7 @@ function renderDiff(lines) {
3317
4023
  isSkipping = false;
3318
4024
  continue;
3319
4025
  }
3320
- if (!isSkipping) rendered.push(pc3.dim(`${UNCHANGED_PREFIX}${GLYPHS.elided}`));
4026
+ if (!isSkipping) rendered.push(pc4.dim(`${UNCHANGED_PREFIX}${GLYPHS.elided}`));
3321
4027
  isSkipping = true;
3322
4028
  }
3323
4029
  return rendered;
@@ -3333,54 +4039,65 @@ function findVisibleIndexes(lines) {
3333
4039
  return visibleIndexes;
3334
4040
  }
3335
4041
  function renderLine(line) {
3336
- return match20(line.kind).with("added", () => pc3.green(`${ADDED_PREFIX}${line.text}`)).with("removed", () => pc3.red(`${REMOVED_PREFIX}${line.text}`)).with("unchanged", () => pc3.dim(`${UNCHANGED_PREFIX}${line.text}`)).exhaustive();
4042
+ return match23(line.kind).with("added", () => pc4.green(`${ADDED_PREFIX}${line.text}`)).with("removed", () => pc4.red(`${REMOVED_PREFIX}${line.text}`)).with("unchanged", () => pc4.dim(`${UNCHANGED_PREFIX}${line.text}`)).exhaustive();
3337
4043
  }
3338
4044
 
3339
- // src/ui/report/format-preview.ts
4045
+ // src/ui/report/preview/format-preview.ts
3340
4046
  var NOTHING_TO_CHANGE = "Nothing to change \u2014 every selected agent is already configured or was skipped.";
3341
4047
  var NOTHING_TO_CHANGE_AFTER_FAILURE = "Nothing to change \u2014 an agent failed while planning; the report below says why.";
3342
- function formatPreview({ changes, model, scope, hasFailedAgent, home, masker: masker2 }) {
3343
- const heading = formatHeading(`Planned changes ${pc4.dim(`(scope: ${scope}, model: ${masker2.maskText(model)})`)}`);
3344
- if (changes.length === 0) {
4048
+ function formatPreview({
4049
+ changes,
4050
+ userEnvironment: userEnvironment2,
4051
+ model,
4052
+ scope,
4053
+ hasFailedAgent,
4054
+ home,
4055
+ masker: masker2
4056
+ }) {
4057
+ const heading = formatHeading(`Planned changes ${pc5.dim(`(scope: ${scope}, model: ${masker2.maskText(model)})`)}`);
4058
+ const userEnvironmentLines = userEnvironment2 === null ? [] : formatUserEnvironmentPreview({ userEnvironment: userEnvironment2, masker: masker2 });
4059
+ if (changes.length === 0 && userEnvironmentLines.length === 0) {
3345
4060
  const note4 = hasFailedAgent ? NOTHING_TO_CHANGE_AFTER_FAILURE : NOTHING_TO_CHANGE;
3346
- return [...heading, ` ${pc4.dim(note4)}`];
4061
+ return [...heading, ` ${pc5.dim(note4)}`];
3347
4062
  }
3348
- return [...heading, ...changes.flatMap((change) => formatFileDiff({ change, home, masker: masker2 }))];
4063
+ return [...heading, ...changes.flatMap((change) => formatFileDiff({ change, home, masker: masker2 })), ...userEnvironmentLines];
3349
4064
  }
3350
4065
  function formatFileDiff({ change, home, masker: masker2 }) {
3351
4066
  const diff = diffLines({ before: masker2.maskText(change.before ?? ""), after: masker2.maskText(change.after) });
3352
4067
  const lineCount = formatCount(countChangedLines(diff), "line");
3353
4068
  const lineCountNote = change.before === null ? `new file, ${lineCount}` : lineCount;
3354
- return ["", ` ${pc4.bold(displayPath(change.path, home))} ${pc4.dim(`(${lineCountNote})`)}`, ...renderDiff(diff)];
4069
+ return ["", ` ${pc5.bold(displayPath(change.path, home))} ${pc5.dim(`(${lineCountNote})`)}`, ...renderDiff(diff)];
3355
4070
  }
3356
4071
 
3357
- // src/ui/report/format-restore-preview.ts
4072
+ // src/ui/report/preview/format-restore-preview.ts
3358
4073
  var NOTHING_TO_RESTORE = "Nothing to change \u2014 every selected agent is already as it was.";
3359
4074
  var NOTHING_TO_RESTORE_AFTER_FAILURE = "Nothing to change \u2014 an agent failed while planning; the report below says why.";
3360
4075
  function formatRestorePreview(params) {
3361
- const { files, envFile, scope, hasFailedAgent, home, masker: masker2 } = params;
3362
- const heading = formatHeading(`Planned restore ${pc5.dim(`(scope: ${scope})`)}`);
4076
+ const { files, envFile, userEnvironment: userEnvironment2, scope, hasFailedAgent, home, masker: masker2 } = params;
4077
+ const heading = formatHeading(`Planned restore ${pc6.dim(`(scope: ${scope})`)}`);
3363
4078
  const changedFiles = files.filter((file) => file.isChanged);
3364
4079
  const changedEnvFile = envFile?.isChanged === true ? [envFile] : [];
3365
- if (changedFiles.length === 0 && changedEnvFile.length === 0) {
3366
- return [...heading, ` ${pc5.dim(hasFailedAgent ? NOTHING_TO_RESTORE_AFTER_FAILURE : NOTHING_TO_RESTORE)}`];
4080
+ const userEnvironmentLines = userEnvironment2 === null ? [] : formatUserEnvironmentPreview({ userEnvironment: userEnvironment2, masker: masker2 });
4081
+ if (changedFiles.length === 0 && changedEnvFile.length === 0 && userEnvironmentLines.length === 0) {
4082
+ return [...heading, ` ${pc6.dim(hasFailedAgent ? NOTHING_TO_RESTORE_AFTER_FAILURE : NOTHING_TO_RESTORE)}`];
3367
4083
  }
3368
4084
  return [
3369
4085
  ...heading,
3370
4086
  ...changedFiles.flatMap((file) => formatRestoredFile({ file, home, masker: masker2 })),
3371
- ...changedEnvFile.flatMap((change) => formatFileDiff({ change, home, masker: masker2 }))
4087
+ ...changedEnvFile.flatMap((change) => formatFileDiff({ change, home, masker: masker2 })),
4088
+ ...userEnvironmentLines
3372
4089
  ];
3373
4090
  }
3374
4091
  var DELETION_NOTE = "deleted \u2014 nexos-connect created it";
3375
4092
  function formatRestoredFile({ file, home, masker: masker2 }) {
3376
- if (file.after === null) return ["", ` ${pc5.bold(displayPath(file.path, home))} ${pc5.dim(`(${DELETION_NOTE})`)}`];
4093
+ if (file.after === null) return ["", ` ${pc6.bold(displayPath(file.path, home))} ${pc6.dim(`(${DELETION_NOTE})`)}`];
3377
4094
  const change = { path: file.path, before: file.before, after: file.after, isChanged: file.isChanged };
3378
4095
  return formatFileDiff({ change, home, masker: masker2 });
3379
4096
  }
3380
4097
 
3381
- // src/ui/report/format-restore-result.ts
3382
- import pc6 from "picocolors";
3383
- import { P as P4, match as match21 } from "ts-pattern";
4098
+ // src/ui/report/result/format-restore-result.ts
4099
+ import pc7 from "picocolors";
4100
+ import { P as P5, match as match24 } from "ts-pattern";
3384
4101
 
3385
4102
  // src/commands/restore/restore-result.ts
3386
4103
  var RESTORE_OUTCOME_KINDS = ["restored", "unchanged", "nothing-to-restore", "failed"];
@@ -3392,7 +4109,7 @@ function exitCodeForRestore(outcomes) {
3392
4109
  return isFailed ? EXIT_CODE.failed : EXIT_CODE.ok;
3393
4110
  }
3394
4111
 
3395
- // src/ui/report/format-restore-result.ts
4112
+ // src/ui/report/result/format-restore-result.ts
3396
4113
  function formatRestoreResult(params) {
3397
4114
  const heading = params.mode.kind === "dry-run" ? "Result (dry run)" : "Result";
3398
4115
  return [
@@ -3404,45 +4121,51 @@ function formatRestoreResult(params) {
3404
4121
  ];
3405
4122
  }
3406
4123
  var RESTORE_OUTCOME_STYLE = {
3407
- restored: { glyph: GLYPHS.success, colour: pc6.green, label: "restored" },
3408
- unchanged: { glyph: GLYPHS.unchanged, colour: pc6.green, label: "already as it was" },
3409
- "nothing-to-restore": { glyph: GLYPHS.absent, colour: pc6.yellow, label: "nothing to restore" },
3410
- failed: { glyph: GLYPHS.failure, colour: pc6.red, label: "failed" }
4124
+ restored: { glyph: GLYPHS.success, colour: pc7.green, label: "restored" },
4125
+ unchanged: { glyph: GLYPHS.unchanged, colour: pc7.green, label: "already as it was" },
4126
+ "nothing-to-restore": { glyph: GLYPHS.absent, colour: pc7.yellow, label: "nothing to restore" },
4127
+ failed: { glyph: GLYPHS.failure, colour: pc7.red, label: "failed" }
3411
4128
  };
3412
4129
  var NO_RECORD = "nexos-connect has no record of setting it up at this scope.";
3413
4130
  function formatOutcome(outcome, { home, masker: masker2 }) {
3414
4131
  const style = RESTORE_OUTCOME_STYLE[outcome.kind];
3415
- const body = match21(outcome).with({ kind: P4.union("restored", "unchanged") }, (planned) => formatPlannedRestore(planned, home, masker2)).with({ kind: "nothing-to-restore" }, () => [`${INDENT}${pc6.dim(NO_RECORD)}`]).with({ kind: "failed" }, ({ error }) => [`${INDENT}${pc6.red(masker2.maskText(error))}`]).exhaustive();
4132
+ const body = match24(outcome).with({ kind: P5.union("restored", "unchanged") }, (planned) => formatPlannedRestore(planned, home, masker2)).with({ kind: "nothing-to-restore" }, () => [`${INDENT}${pc7.dim(NO_RECORD)}`]).with({ kind: "failed" }, ({ error }) => [`${INDENT}${pc7.red(masker2.maskText(error))}`]).exhaustive();
3416
4133
  return ["", ` ${style.colour(`${style.glyph} ${outcome.target.label} \u2014 ${style.label}`)}`, ...body];
3417
4134
  }
3418
4135
  function formatPlannedRestore(outcome, home, masker2) {
3419
4136
  const restartNote = `Restart ${outcome.target.label} so it reads the restored config.`;
3420
4137
  return [
3421
- ...outcome.files.filter((file) => file.isChanged).map((file) => `${INDENT}${pc6.dim(describeFile(file, home))}`),
3422
- ...outcome.warnings.map((warning) => `${INDENT}${pc6.yellow(`${GLYPHS.warning} ${masker2.maskText(warning)}`)}`),
3423
- ...outcome.kind === "restored" ? [`${INDENT}${pc6.cyan(`${GLYPHS.next} ${restartNote}`)}`] : []
4138
+ ...outcome.files.filter((file) => file.isChanged).map((file) => `${INDENT}${pc7.dim(describeFile(file, home))}`),
4139
+ ...outcome.warnings.map((warning) => `${INDENT}${pc7.yellow(`${GLYPHS.warning} ${masker2.maskText(warning)}`)}`),
4140
+ ...outcome.kind === "restored" ? [`${INDENT}${pc7.cyan(`${GLYPHS.next} ${restartNote}`)}`] : []
3424
4141
  ];
3425
4142
  }
3426
4143
  function describeFile(file, home) {
3427
4144
  const action = file.after === null ? "deleted" : "put back";
3428
4145
  return `${displayPath(file.path, home)} \u2014 ${action}`;
3429
4146
  }
3430
- function formatEnvFileStep({ envFile, mode, home }) {
3431
- if (mode.kind === "dry-run" || envFile === null || !envFile.isChanged) return [];
4147
+ function formatEnvFileStep({ envFile, userEnvironment: userEnvironment2, mode, home }) {
4148
+ if (mode.kind === "dry-run") return [];
4149
+ const envFileLines = envFile !== null && envFile.isChanged ? describeEnvFile(envFile, home) : [];
4150
+ const userEnvironmentLines = userEnvironment2 === null ? [] : [` ${pc7.dim("The Windows user environment changed; open a new terminal to pick it up.")}`];
4151
+ if (envFileLines.length === 0 && userEnvironmentLines.length === 0) return [];
4152
+ return [...formatHeading("One more step"), ...envFileLines, ...userEnvironmentLines];
4153
+ }
4154
+ function describeEnvFile(envFile, home) {
3432
4155
  const isEmpty = parseEnvFile(envFile.after, envFile.shell).size === 0;
3433
4156
  const emptyNote = `${displayPath(envFile.path, home)} no longer sets anything: you can delete it, and the line that loads it.`;
3434
4157
  return [
3435
- ...formatHeading("One more step"),
3436
4158
  ...isEmpty ? [` ${emptyNote}`] : [],
3437
- ` ${pc6.dim("Shells open now keep the variables it set; a new shell starts without them.")}`
4159
+ ` ${pc7.dim("Shells open now keep the variables it set; a new shell starts without them.")}`
3438
4160
  ];
3439
4161
  }
3440
4162
  function formatBackups({ mode, home, masker: masker2 }) {
3441
4163
  if (mode.kind === "dry-run" || mode.backupRoot === null) return [];
3442
4164
  const backupRoot = masker2.maskText(displayPath(mode.backupRoot, home));
3443
- return [...formatHeading("Backups"), ` Current versions copied to ${pc6.bold(backupRoot)}`];
4165
+ return [...formatHeading("Backups"), ` Current versions copied to ${pc7.bold(backupRoot)}`];
3444
4166
  }
3445
4167
  function formatSummary(outcomes) {
4168
+ if (outcomes.length === 0) return [];
3446
4169
  const countsByKind = /* @__PURE__ */ new Map();
3447
4170
  for (const { kind } of outcomes) countsByKind.set(kind, (countsByKind.get(kind) ?? 0) + 1);
3448
4171
  const counts = RESTORE_OUTCOME_KINDS.map(
@@ -3453,11 +4176,11 @@ function formatSummary(outcomes) {
3453
4176
 
3454
4177
  // src/commands/restore/apply-restore.ts
3455
4178
  import fs9 from "fs";
3456
- import path16 from "path";
4179
+ import path19 from "path";
3457
4180
 
3458
4181
  // src/product/storage/backups/back-up-file.ts
3459
4182
  import fs8 from "fs";
3460
- import path15 from "path";
4183
+ import path18 from "path";
3461
4184
 
3462
4185
  // src/shared/file-system/read-file-mode.ts
3463
4186
  import fs7 from "fs";
@@ -3476,15 +4199,15 @@ function readFileMode(filePath, platform) {
3476
4199
  var BACKUP_DIRECTORY_MODE = 448;
3477
4200
  var OWNER_PERMISSION_BITS = 448;
3478
4201
  function backUpFile({ source, root, relativePath, platform }) {
3479
- const destination = path15.join(root, relativePath);
3480
- fs8.mkdirSync(path15.dirname(destination), { recursive: true, mode: BACKUP_DIRECTORY_MODE });
4202
+ const destination = path18.join(root, relativePath);
4203
+ fs8.mkdirSync(path18.dirname(destination), { recursive: true, mode: BACKUP_DIRECTORY_MODE });
3481
4204
  fs8.copyFileSync(source, destination, fs8.constants.COPYFILE_EXCL);
3482
4205
  const mode = readFileMode(source, platform);
3483
4206
  if (mode !== null) fs8.chmodSync(destination, mode & OWNER_PERMISSION_BITS);
3484
4207
  }
3485
4208
  var IGNORE_EVERYTHING = "*\n";
3486
4209
  function ignoreBackupsInGit(directory) {
3487
- const ignoreFile = path15.join(directory, ".gitignore");
4210
+ const ignoreFile = path18.join(directory, ".gitignore");
3488
4211
  if (fs8.existsSync(ignoreFile)) return;
3489
4212
  fs8.mkdirSync(directory, { recursive: true, mode: BACKUP_DIRECTORY_MODE });
3490
4213
  fs8.writeFileSync(ignoreFile, IGNORE_EVERYTHING);
@@ -3513,9 +4236,102 @@ function writeEnvFile({ filePath, content, platform }) {
3513
4236
  }
3514
4237
 
3515
4238
  // src/product/storage/restore-record/remove-harness-records.ts
3516
- function removeHarnessRecords(record5, harnessIds) {
3517
- const harnesses2 = Object.fromEntries(Object.entries(record5.harnesses).filter(([id]) => !harnessIds.includes(id)));
3518
- return { ...record5, harnesses: harnesses2 };
4239
+ function removeHarnessRecords(record9, harnessIds) {
4240
+ const harnesses2 = Object.fromEntries(Object.entries(record9.harnesses).filter(([id]) => !harnessIds.includes(id)));
4241
+ return { ...record9, harnesses: harnesses2, userEnvironment: pruneUserEnvironment(record9, harnesses2) };
4242
+ }
4243
+ function pruneUserEnvironment(record9, harnesses2) {
4244
+ const referencedNames = new Set(
4245
+ Object.values(harnesses2).flatMap((harness) => harness.envVariables.map(({ name }) => name))
4246
+ );
4247
+ return record9.userEnvironment.filter(({ name }) => referencedNames.has(name));
4248
+ }
4249
+
4250
+ // src/product/storage/windows-environment/describe-user-environment-failure.ts
4251
+ function describeUserEnvironmentFailure({
4252
+ action,
4253
+ envPath,
4254
+ home
4255
+ }) {
4256
+ const fallback4 = displayPath(envPath, home);
4257
+ if (action === "read") {
4258
+ return `Could not read the Windows user environment, so it was left as it is. The key is still in ${fallback4}.`;
4259
+ }
4260
+ return `Could not set the key in the Windows user environment, so a new terminal will not see it yet. The key is still in ${fallback4} \u2014 load it there, or set the variable yourself.`;
4261
+ }
4262
+
4263
+ // src/product/storage/windows-environment/user-environment.ts
4264
+ import * as v15 from "valibot";
4265
+ var UserEnvironmentError = class extends Error {
4266
+ name = "UserEnvironmentError";
4267
+ };
4268
+ var NO_OP_USER_ENVIRONMENT = {
4269
+ read: () => ({}),
4270
+ write: () => {
4271
+ }
4272
+ };
4273
+ function createUserEnvironment({ platform, runPowerShell: runPowerShell2 }) {
4274
+ if (platform !== "win32") return NO_OP_USER_ENVIRONMENT;
4275
+ return {
4276
+ read: (names) => readUserEnvironment({ names, runPowerShell: runPowerShell2 }),
4277
+ write: (values) => writeUserEnvironment({ values, runPowerShell: runPowerShell2 })
4278
+ };
4279
+ }
4280
+ function planUserEnvironment({
4281
+ values,
4282
+ userEnvironment: userEnvironment2
4283
+ }) {
4284
+ const names = Object.keys(values);
4285
+ if (names.length === 0) return null;
4286
+ const before = userEnvironment2.read(names);
4287
+ return { before, after: values, isChanged: names.some((name) => before[name] !== values[name]) };
4288
+ }
4289
+ function replanUserEnvironment({ plan, names }) {
4290
+ if (plan === null) return null;
4291
+ if (names === null) return plan;
4292
+ const after = Object.fromEntries(Object.entries(plan.after).filter(([name]) => names.has(name)));
4293
+ if (Object.keys(after).length === 0) return null;
4294
+ const isChanged = Object.entries(after).some(([name, value]) => plan.before[name] !== value);
4295
+ return { before: plan.before, after, isChanged };
4296
+ }
4297
+ var READ_SCRIPT = [
4298
+ "$ErrorActionPreference = 'Stop'",
4299
+ "$names = ([Console]::In.ReadToEnd() | ConvertFrom-Json).names",
4300
+ "$result = [ordered]@{}",
4301
+ "foreach ($name in $names) { $result[$name] = [Environment]::GetEnvironmentVariable($name, 'User') }",
4302
+ "$result | ConvertTo-Json -Compress"
4303
+ ].join("\n");
4304
+ var WRITE_SCRIPT = [
4305
+ "$ErrorActionPreference = 'Stop'",
4306
+ "$values = ([Console]::In.ReadToEnd() | ConvertFrom-Json).values",
4307
+ "foreach ($property in $values.PSObject.Properties) {",
4308
+ " [Environment]::SetEnvironmentVariable($property.Name, $property.Value, 'User')",
4309
+ "}"
4310
+ ].join("\n");
4311
+ var REPLY_SCHEMA = v15.record(v15.string(), v15.nullable(v15.string()));
4312
+ function readUserEnvironment({ names, runPowerShell: runPowerShell2 }) {
4313
+ const result = runPowerShell2({ script: READ_SCRIPT, input: JSON.stringify({ names }) });
4314
+ if (result.code !== 0) {
4315
+ throw new UserEnvironmentError(`the Windows user environment could not be read (PowerShell exited ${result.code})`);
4316
+ }
4317
+ const reply = parseReply(result.stdout);
4318
+ return Object.fromEntries(names.map((name) => [name, reply[name] ?? void 0]));
4319
+ }
4320
+ function parseReply(stdout) {
4321
+ const parsed = v15.safeParse(REPLY_SCHEMA, parseJson2(stdout));
4322
+ if (!parsed.success) throw new UserEnvironmentError("the Windows user environment replied with something unexpected");
4323
+ return parsed.output;
4324
+ }
4325
+ function writeUserEnvironment({ values, runPowerShell: runPowerShell2 }) {
4326
+ const payload = JSON.stringify({
4327
+ values: Object.fromEntries(Object.entries(values).map(([name, value]) => [name, value ?? null]))
4328
+ });
4329
+ const result = runPowerShell2({ script: WRITE_SCRIPT, input: payload });
4330
+ if (result.code !== 0) {
4331
+ throw new UserEnvironmentError(
4332
+ `the Windows user environment could not be written (PowerShell exited ${result.code})`
4333
+ );
4334
+ }
3519
4335
  }
3520
4336
 
3521
4337
  // src/product/storage/env-file/render-env-file.ts
@@ -3624,10 +4440,12 @@ function applyRestore(params) {
3624
4440
  const outcomes = agentWrites.map(({ outcome }) => outcome);
3625
4441
  const envFile = replanEnvFile(params.plan.envFile, outcomes);
3626
4442
  const envWrite = restoreSharedEnvFile({ outcomes, envFile, params });
4443
+ const userEnvironmentWrite = restoreUserEnvironment({ outcomes: envWrite.outcomes, params });
3627
4444
  const writtenFiles = [...agentWrites.flatMap(({ writtenFiles: writtenFiles2 }) => writtenFiles2), ...envWrite.writtenFiles];
3628
4445
  return {
3629
- outcomes: forgetRestored(envWrite.outcomes, params.ctx),
4446
+ outcomes: forgetRestored(userEnvironmentWrite.outcomes, params.ctx, userEnvironmentWrite.written !== null),
3630
4447
  envFile,
4448
+ userEnvironment: userEnvironmentWrite.written,
3631
4449
  writtenCount: writtenFiles.length,
3632
4450
  backupRoot: writtenFiles.some((file) => file.isBackedUp) ? params.backupRoot.path : null
3633
4451
  };
@@ -3647,7 +4465,7 @@ function restoreAgentFiles(outcome, params) {
3647
4465
  }
3648
4466
  function restoreFile({ file, target, params }) {
3649
4467
  const { ctx, backupRoot } = params;
3650
- const relativePath = path16.join(target.id, path16.basename(file.path));
4468
+ const relativePath = path19.join(target.id, path19.basename(file.path));
3651
4469
  const backup = backUpBeforeWriting({ filePath: file.path, before: file.before, backupRoot, relativePath, ctx });
3652
4470
  if (file.after === null) {
3653
4471
  fs9.rmSync(file.path);
@@ -3666,7 +4484,7 @@ function restoreSharedEnvFile({ outcomes, envFile, params }) {
3666
4484
  if (envFile === null || !envFile.isChanged) return { outcomes, writtenFiles: [] };
3667
4485
  const { ctx, backupRoot } = params;
3668
4486
  try {
3669
- const relativePath = path16.join(ENV_FILE_BACKUP_DIRECTORY, path16.basename(envFile.path));
4487
+ const relativePath = path19.join(ENV_FILE_BACKUP_DIRECTORY, path19.basename(envFile.path));
3670
4488
  const backup = backUpBeforeWriting({
3671
4489
  filePath: envFile.path,
3672
4490
  before: envFile.before,
@@ -3685,14 +4503,36 @@ function failIfRestoringEnvFile(outcome, error) {
3685
4503
  if (!isPlannedRestore(outcome) || outcome.envVariables.length === 0) return outcome;
3686
4504
  return { kind: "failed", target: outcome.target, error };
3687
4505
  }
3688
- function forgetRestored(outcomes, ctx) {
4506
+ function restoreUserEnvironment({ outcomes, params }) {
4507
+ const { userEnvironment: userEnvironment2, envPath, ctx } = params;
4508
+ const plan = replanUserEnvironment({ plan: params.plan.userEnvironment, names: userEnvironmentNames(outcomes) });
4509
+ if (plan === null || !plan.isChanged) return { outcomes: [...outcomes], written: null };
4510
+ try {
4511
+ userEnvironment2.write(plan.after);
4512
+ return { outcomes: [...outcomes], written: plan };
4513
+ } catch {
4514
+ const warning = describeUserEnvironmentFailure({ action: "write", envPath, home: ctx.home });
4515
+ return { outcomes: outcomes.map((outcome) => addUserEnvironmentWarningTo(outcome, warning)), written: null };
4516
+ }
4517
+ }
4518
+ function addUserEnvironmentWarningTo(outcome, warning) {
4519
+ if (!isPlannedRestore(outcome) || outcome.envVariables.length === 0) return outcome;
4520
+ return { ...outcome, warnings: [...outcome.warnings, warning] };
4521
+ }
4522
+ function userEnvironmentNames(outcomes) {
4523
+ if (outcomes.length === 0) return null;
4524
+ return new Set(
4525
+ outcomes.flatMap((outcome) => isPlannedRestore(outcome) ? outcome.envVariables.map(({ name }) => name) : [])
4526
+ );
4527
+ }
4528
+ function forgetRestored(outcomes, ctx, isUserEnvironmentRestored) {
3689
4529
  const restoredIds = outcomes.filter(isPlannedRestore).map(({ target }) => target.id);
3690
- if (restoredIds.length === 0) return [...outcomes];
4530
+ if (restoredIds.length === 0 && !isUserEnvironmentRestored) return [...outcomes];
3691
4531
  const { filePath } = resolveRestoreRecordLocation(ctx);
3692
4532
  try {
3693
- const record5 = readRestoreRecord({ filePath, home: ctx.home });
3694
- if (record5 !== null) {
3695
- writeRestoreRecord({ filePath, record: removeHarnessRecords(record5, restoredIds), platform: ctx.platform });
4533
+ const record9 = readRestoreRecord({ filePath, home: ctx.home });
4534
+ if (record9 !== null) {
4535
+ writeRestoreRecord({ filePath, record: removeHarnessRecords(record9, restoredIds), platform: ctx.platform });
3696
4536
  }
3697
4537
  return [...outcomes];
3698
4538
  } catch (error) {
@@ -3706,8 +4546,8 @@ function failIfPlanned(outcome, error) {
3706
4546
  }
3707
4547
 
3708
4548
  // src/commands/restore/restore-plan.ts
3709
- import path17 from "path";
3710
- import { match as match22 } from "ts-pattern";
4549
+ import path20 from "path";
4550
+ import { match as match25 } from "ts-pattern";
3711
4551
 
3712
4552
  // src/product/storage/env-file/read-env-file.ts
3713
4553
  var NO_ENV_FILE = { kind: "read", text: null };
@@ -3722,28 +4562,79 @@ function readEnvFile({ envPath, home }) {
3722
4562
  // src/commands/restore/restore-plan.ts
3723
4563
  function planRestore(params) {
3724
4564
  const { candidates, ctx, envPath } = params;
3725
- const hasEnvVariables2 = candidates.some(({ record: record5 }) => (record5?.envVariables.length ?? 0) > 0);
4565
+ const hasEnvVariables2 = candidates.some(({ record: record9 }) => (record9?.envVariables.length ?? 0) > 0);
3726
4566
  const envFileRead = hasEnvVariables2 ? readEnvFile({ envPath, home: ctx.home }) : NO_ENV_FILE;
3727
4567
  const namesKept = listNamesRecordedElsewhere(params);
3728
- const outcomes = candidates.map((candidate) => planCandidate({ candidate, envFileRead, namesKept, params }));
3729
- return { outcomes, envFile: planEnvFile({ outcomes, envFileRead, params }) };
4568
+ const userEnvironment2 = planRestoreUserEnvironment({ candidates, record: params.record, namesKept, params });
4569
+ const outcomes = candidates.map(
4570
+ (candidate) => planCandidate({ candidate, envFileRead, namesKept, userEnvironment: userEnvironment2.plan, params })
4571
+ );
4572
+ return {
4573
+ outcomes: addUserEnvironmentWarning({ outcomes, warning: userEnvironment2.warning }),
4574
+ envFile: planEnvFile({ outcomes, envFileRead, params }),
4575
+ userEnvironment: userEnvironment2.plan
4576
+ };
3730
4577
  }
3731
- function listNamesRecordedElsewhere({ candidates, record: record5 }) {
4578
+ function listNamesRecordedElsewhere({ candidates, record: record9 }) {
3732
4579
  const selectedIds = candidates.map(({ target }) => target.id);
3733
- const others = Object.entries(record5.harnesses).filter(([id]) => !selectedIds.includes(id));
4580
+ const others = Object.entries(record9.harnesses).filter(([id]) => !selectedIds.includes(id));
3734
4581
  return new Set(others.flatMap(([, harness]) => harness.envVariables.map(({ name }) => name)));
3735
4582
  }
3736
- function planCandidate({ candidate, envFileRead, namesKept, params }) {
3737
- const { target, record: record5 } = candidate;
3738
- if (record5 === null) return { kind: "nothing-to-restore", target };
3739
- const envVariables = record5.envVariables.filter(({ name }) => !namesKept.has(name));
4583
+ function planRestoreUserEnvironment({
4584
+ candidates,
4585
+ record: record9,
4586
+ namesKept,
4587
+ params
4588
+ }) {
4589
+ if (params.ctx.platform !== "win32") return { plan: null, warning: null };
4590
+ const originals = new Map(record9.userEnvironment.map(({ name, original }) => [name, original]));
4591
+ const names = listRestorableNames({ candidates, record: record9 });
4592
+ const values = Object.fromEntries(
4593
+ names.flatMap(
4594
+ (name) => !namesKept.has(name) && originals.has(name) ? [[name, originals.get(name) ?? void 0]] : []
4595
+ )
4596
+ );
4597
+ if (Object.keys(values).length === 0) return { plan: null, warning: null };
4598
+ try {
4599
+ return { plan: planUserEnvironment({ values, userEnvironment: params.userEnvironment }), warning: null };
4600
+ } catch (error) {
4601
+ if (!(error instanceof UserEnvironmentError)) throw error;
4602
+ return {
4603
+ plan: null,
4604
+ warning: describeUserEnvironmentFailure({ action: "read", envPath: params.envPath, home: params.ctx.home })
4605
+ };
4606
+ }
4607
+ }
4608
+ function listRestorableNames({ candidates, record: record9 }) {
4609
+ if (candidates.length === 0) return record9.userEnvironment.map(({ name }) => name);
4610
+ return candidates.flatMap(({ record: harness }) => (harness?.envVariables ?? []).map(({ name }) => name));
4611
+ }
4612
+ function addUserEnvironmentWarning({ outcomes, warning }) {
4613
+ if (warning === null) return [...outcomes];
4614
+ return outcomes.map(
4615
+ (outcome) => isPlannedRestore(outcome) && outcome.envVariables.length > 0 ? { ...outcome, warnings: [...outcome.warnings, warning] } : outcome
4616
+ );
4617
+ }
4618
+ function planCandidate({
4619
+ candidate,
4620
+ envFileRead,
4621
+ namesKept,
4622
+ userEnvironment: userEnvironment2,
4623
+ params
4624
+ }) {
4625
+ const { target, record: record9 } = candidate;
4626
+ if (record9 === null) return { kind: "nothing-to-restore", target };
4627
+ const envVariables = record9.envVariables.filter(({ name }) => !namesKept.has(name));
3740
4628
  if (envVariables.length > 0 && envFileRead.kind === "failed") {
3741
4629
  return { kind: "failed", target, error: envFileRead.error };
3742
4630
  }
3743
4631
  try {
3744
- const { files, warnings } = planRecordedFiles(record5.files, params.ctx);
4632
+ const { files, warnings } = planRecordedFiles(record9.files, params.ctx);
3745
4633
  const hasEnvChange = hasOwnEnvChange({ envFileRead, envVariables, shell: params.shell });
3746
- const isChanged = hasEnvChange || files.some((file) => file.isChanged);
4634
+ const hasUserEnvironmentChange = userEnvironment2 !== null && envVariables.some(
4635
+ ({ name }) => name in userEnvironment2.after && userEnvironment2.before[name] !== userEnvironment2.after[name]
4636
+ );
4637
+ const isChanged = hasEnvChange || hasUserEnvironmentChange || files.some((file) => file.isChanged);
3747
4638
  return { kind: isChanged ? "restored" : "unchanged", target, files, envVariables, warnings };
3748
4639
  } catch (error) {
3749
4640
  return { kind: "failed", target, error: describeFileError({ error, action: "read", home: params.ctx.home }) };
@@ -3773,7 +4664,7 @@ function planRecordedFile(file, ctx) {
3773
4664
  };
3774
4665
  }
3775
4666
  function describeMissingFile(filePath, ctx) {
3776
- const backups = match22(ctx.scope).with("user", () => path17.join(ctx.home, USER_BACKUP_DIRECTORY)).with("project", () => path17.join(ctx.cwd, PROJECT_BACKUP_DIRECTORY)).exhaustive();
4667
+ const backups = match25(ctx.scope).with("user", () => path20.join(ctx.home, USER_BACKUP_DIRECTORY)).with("project", () => path20.join(ctx.cwd, PROJECT_BACKUP_DIRECTORY)).exhaustive();
3777
4668
  return `${displayPath(filePath, ctx.home)} no longer exists, so it was left as it is. Copies from before each run are under ${displayPath(backups, ctx.home)}, unless --backup-dir put them elsewhere.`;
3778
4669
  }
3779
4670
  function restoreFile2({ file, before, filePath, home }) {
@@ -3795,12 +4686,16 @@ function planEnvFile({ outcomes, envFileRead, params }) {
3795
4686
  }
3796
4687
 
3797
4688
  // src/commands/restore/restore-secrets.ts
3798
- function listRestoreSecrets({ record: record5, ctx, envPath, shell }) {
3799
- const recorded = Object.values(record5.harnesses).flatMap((harness) => [
4689
+ function listRestoreSecrets({ record: record9, ctx, envPath, shell }) {
4690
+ const recorded = Object.values(record9.harnesses).flatMap((harness) => [
3800
4691
  ...harness.files.flatMap(listFileSecrets),
3801
4692
  ...listEnvSecrets(harness)
3802
4693
  ]);
3803
- return [...findExistingKeys({ ctx, envPath, shell }).all, ...recorded];
4694
+ return [
4695
+ ...findExistingKeys({ ctx, envPath, shell }).all,
4696
+ ...recorded,
4697
+ ...listUserEnvironmentSecrets(record9.userEnvironment)
4698
+ ];
3804
4699
  }
3805
4700
  function listFileSecrets(file) {
3806
4701
  return file.keys.flatMap(({ original, holdsSecret }) => {
@@ -3825,6 +4720,11 @@ function listEnvSecrets(harness) {
3825
4720
  ({ name, original }) => KEY_ENV_VARIABLES.includes(name) && original !== null ? [original] : []
3826
4721
  );
3827
4722
  }
4723
+ function listUserEnvironmentSecrets(variables) {
4724
+ return variables.flatMap(
4725
+ ({ name, original }) => KEY_ENV_VARIABLES.includes(name) && original !== null ? [original] : []
4726
+ );
4727
+ }
3828
4728
 
3829
4729
  // src/commands/restore/run-restore.ts
3830
4730
  var FINISHED_WITH_ERRORS = "Finished with errors.";
@@ -3843,27 +4743,28 @@ async function runRestore(params) {
3843
4743
  ui.outro(FINISHED_WITH_ERRORS);
3844
4744
  return EXIT_CODE.failed;
3845
4745
  }
3846
- const { record: record5 } = recordRead;
3847
- if (namedHarnesses.length === 0 && Object.keys(record5.harnesses).length === 0) {
4746
+ const { record: record9 } = recordRead;
4747
+ const hasRecordedHarnesses = Object.keys(record9.harnesses).length > 0;
4748
+ if (namedHarnesses.length === 0 && !hasRecordedHarnesses && record9.userEnvironment.length === 0) {
3848
4749
  const recordPath = displayPath(resolveRestoreRecordLocation(ctx).filePath, ctx.home);
3849
4750
  ui.outro(`Nothing to restore \u2014 nexos-connect has not set up any agent at ${scope} scope (${recordPath}).`);
3850
4751
  return EXIT_CODE.ok;
3851
4752
  }
3852
4753
  const candidates = await selectCandidates({
3853
4754
  namedHarnesses,
3854
- record: record5,
4755
+ record: record9,
3855
4756
  ctx,
3856
4757
  shouldAssumeYes: options.shouldAssumeYes,
3857
4758
  ui
3858
4759
  });
3859
- if (candidates.length === 0) {
4760
+ if (candidates.length === 0 && hasRecordedHarnesses) {
3860
4761
  ui.cancel("No agents selected \u2014 nothing to do.");
3861
4762
  return EXIT_CODE.ok;
3862
4763
  }
3863
4764
  const run = { ...params, ui, ctx };
3864
- const plan = planSelected({ run, candidates, record: record5 });
4765
+ const plan = planSelected({ run, candidates, record: record9 });
3865
4766
  if (options.isDryRun) return report({ run, plan, mode: { kind: "dry-run" }, outro: DRY_RUN_OUTRO });
3866
- const hasChanges = plan.outcomes.some((outcome) => outcome.kind === "restored");
4767
+ const hasChanges = plan.outcomes.some((outcome) => outcome.kind === "restored") || plan.userEnvironment?.isChanged === true;
3867
4768
  if (hasChanges && !options.shouldAssumeYes && !await ui.confirm(CONFIRMATION, true)) {
3868
4769
  ui.cancel("Aborted \u2014 nothing was restored.");
3869
4770
  return EXIT_CODE.cancelled;
@@ -3871,7 +4772,12 @@ async function runRestore(params) {
3871
4772
  const applied = applySelected(run, plan);
3872
4773
  const outro3 = exitCodeForRestore(applied.outcomes) === EXIT_CODE.ok ? "Done." : FINISHED_WITH_ERRORS;
3873
4774
  const mode = { kind: "written", backupRoot: applied.backupRoot };
3874
- return report({ run, plan: { outcomes: applied.outcomes, envFile: applied.envFile }, mode, outro: outro3 });
4775
+ return report({
4776
+ run,
4777
+ plan: { outcomes: applied.outcomes, envFile: applied.envFile, userEnvironment: applied.userEnvironment },
4778
+ mode,
4779
+ outro: outro3
4780
+ });
3875
4781
  }
3876
4782
  async function resolveRestoreScope({ options, baseContext, ui }) {
3877
4783
  if (options.scope !== void 0) return options.scope;
@@ -3888,8 +4794,8 @@ function listRecordedLabels(ctx) {
3888
4794
  }
3889
4795
  function readRecord(ctx) {
3890
4796
  try {
3891
- const record5 = readRestoreRecord({ filePath: resolveRestoreRecordLocation(ctx).filePath, home: ctx.home });
3892
- return { kind: "read", record: record5 ?? createEmptyRestoreRecord() };
4797
+ const record9 = readRestoreRecord({ filePath: resolveRestoreRecordLocation(ctx).filePath, home: ctx.home });
4798
+ return { kind: "read", record: record9 ?? createEmptyRestoreRecord() };
3893
4799
  } catch (error) {
3894
4800
  if (!(error instanceof RestoreRecordError)) throw error;
3895
4801
  return { kind: "failed", error: error.message };
@@ -3899,34 +4805,43 @@ function labelOf(harnessId) {
3899
4805
  return findHarness(harnessId)?.label ?? harnessId;
3900
4806
  }
3901
4807
  async function selectCandidates(params) {
3902
- const { namedHarnesses, record: record5, ctx, ui } = params;
3903
- if (namedHarnesses.length > 0) return namedHarnesses.map((harness) => toCandidate(harness.id, record5));
3904
- const recordedIds = Object.keys(record5.harnesses);
3905
- if (params.shouldAssumeYes) return recordedIds.map((id) => toCandidate(id, record5));
3906
- const chosenIds = await ui.selectRestoreHarnesses(recordedIds.map((id) => describeChoice({ id, record: record5, ctx })));
3907
- return chosenIds.map((id) => toCandidate(id, record5));
4808
+ const { namedHarnesses, record: record9, ctx, ui } = params;
4809
+ if (namedHarnesses.length > 0) return namedHarnesses.map((harness) => toCandidate(harness.id, record9));
4810
+ const recordedIds = Object.keys(record9.harnesses);
4811
+ if (recordedIds.length === 0) return [];
4812
+ if (params.shouldAssumeYes) return recordedIds.map((id) => toCandidate(id, record9));
4813
+ const chosenIds = await ui.selectRestoreHarnesses(recordedIds.map((id) => describeChoice({ id, record: record9, ctx })));
4814
+ return chosenIds.map((id) => toCandidate(id, record9));
3908
4815
  }
3909
- function toCandidate(id, record5) {
3910
- return { target: { id, label: labelOf(id) }, record: record5.harnesses[id] ?? null };
4816
+ function toCandidate(id, record9) {
4817
+ return { target: { id, label: labelOf(id) }, record: record9.harnesses[id] ?? null };
3911
4818
  }
3912
4819
  var ENV_VARIABLES_HINT = "environment variables";
3913
- function describeChoice({ id, record: record5, ctx }) {
3914
- const harness = record5.harnesses[id];
4820
+ function describeChoice({ id, record: record9, ctx }) {
4821
+ const harness = record9.harnesses[id];
3915
4822
  const root = resolveRestoreRecordLocation(ctx).root;
3916
- const files = (harness?.files ?? []).map((file) => displayPath(path18.join(root, ...file.path), ctx.home));
4823
+ const files = (harness?.files ?? []).map((file) => displayPath(path21.join(root, ...file.path), ctx.home));
3917
4824
  const variables = (harness?.envVariables.length ?? 0) > 0 ? [ENV_VARIABLES_HINT] : [];
3918
4825
  return { value: id, label: labelOf(id), hint: [...files, ...variables].join(", ") };
3919
4826
  }
3920
- function planSelected({ run, candidates, record: record5 }) {
4827
+ function planSelected({ run, candidates, record: record9 }) {
3921
4828
  const { ctx, masker: masker2, ui } = run;
3922
4829
  const shell = resolveShell(ctx.platform);
3923
4830
  const envPath = resolveEnvFilePath(ctx.home, shell);
3924
- masker2.registerSecrets(listRestoreSecrets({ record: record5, ctx, envPath, shell }));
3925
- const plan = planRestore({ candidates, record: record5, ctx, envPath, shell });
4831
+ masker2.registerSecrets(listRestoreSecrets({ record: record9, ctx, envPath, shell }));
4832
+ const plan = planRestore({ candidates, record: record9, ctx, envPath, shell, userEnvironment: run.userEnvironment });
3926
4833
  const files = plan.outcomes.flatMap((outcome) => isPlannedRestore(outcome) ? outcome.files : []);
3927
4834
  const hasFailedAgent = plan.outcomes.some((outcome) => outcome.kind === "failed");
3928
4835
  ui.print(
3929
- formatRestorePreview({ files, envFile: plan.envFile, scope: ctx.scope, hasFailedAgent, home: ctx.home, masker: masker2 })
4836
+ formatRestorePreview({
4837
+ files,
4838
+ envFile: plan.envFile,
4839
+ userEnvironment: plan.userEnvironment,
4840
+ scope: ctx.scope,
4841
+ hasFailedAgent,
4842
+ home: ctx.home,
4843
+ masker: masker2
4844
+ })
3930
4845
  );
3931
4846
  return plan;
3932
4847
  }
@@ -3939,21 +4854,71 @@ function applySelected(run, plan) {
3939
4854
  stamp: formatBackupTimestamp(run.now()),
3940
4855
  backupDirectory: options.backupDirectory
3941
4856
  });
4857
+ const shell = resolveShell(ctx.platform);
4858
+ const envPath = resolveEnvFilePath(ctx.home, shell);
3942
4859
  const spinner = ui.spinner();
3943
4860
  spinner.start("Backing up current versions and restoring\u2026");
3944
- const applied = applyRestore({ plan, ctx, backupRoot });
4861
+ const applied = applyRestore({ plan, ctx, backupRoot, userEnvironment: run.userEnvironment, envPath });
3945
4862
  spinner.stop(`Restored ${formatCount(applied.writtenCount, "file")}.`);
3946
4863
  return applied;
3947
4864
  }
3948
4865
  function report({ run, plan, mode, outro: outro3 }) {
3949
4866
  const { ui, ctx, masker: masker2 } = run;
3950
- ui.print(formatRestoreResult({ outcomes: plan.outcomes, envFile: plan.envFile, mode, home: ctx.home, masker: masker2 }));
4867
+ ui.print(
4868
+ formatRestoreResult({
4869
+ outcomes: plan.outcomes,
4870
+ envFile: plan.envFile,
4871
+ userEnvironment: plan.userEnvironment,
4872
+ mode,
4873
+ home: ctx.home,
4874
+ masker: masker2
4875
+ })
4876
+ );
3951
4877
  ui.outro(outro3);
3952
4878
  return exitCodeForRestore(plan.outcomes);
3953
4879
  }
3954
4880
 
3955
- // src/ui/report/format-detection.ts
3956
- import pc7 from "picocolors";
4881
+ // src/flows/install/install-missing-harnesses.ts
4882
+ var NOTHING_INSTALLED = { installedCount: 0, hasFailure: false };
4883
+ async function installMissingHarnesses(params) {
4884
+ const { notInstalled, isPickerDriven, options, ctx, ui, runNpm: runNpm2 } = params;
4885
+ if (ctx.scope !== "user") return NOTHING_INSTALLED;
4886
+ const missing = notInstalled.filter(isInstallable);
4887
+ if (missing.length === 0) return NOTHING_INSTALLED;
4888
+ if (options.isDryRun) {
4889
+ ui.dim("Dry run \u2014 the missing agents were not installed.");
4890
+ return NOTHING_INSTALLED;
4891
+ }
4892
+ let installedCount = 0;
4893
+ let hasFailure = false;
4894
+ for (const harness of missing) {
4895
+ const outcome = await installHarness({
4896
+ label: harness.label,
4897
+ binary: harness.binary,
4898
+ packageName: harness.installPackage,
4899
+ shouldInstall: options.shouldInstall || isPickerDriven,
4900
+ shouldAssumeYes: options.shouldAssumeYes,
4901
+ ui,
4902
+ ctx,
4903
+ runNpm: runNpm2
4904
+ });
4905
+ if (outcome.kind === "installed") installedCount += 1;
4906
+ else if (outcome.kind === "not-on-path") {
4907
+ ui.log.error(describeNotOnPath(harness));
4908
+ hasFailure = true;
4909
+ } else if (outcome.kind === "failed") {
4910
+ ui.log.error(outcome.message);
4911
+ hasFailure = true;
4912
+ }
4913
+ }
4914
+ return { installedCount, hasFailure };
4915
+ }
4916
+ function isInstallable(harness) {
4917
+ return harness.kind === "configurable" && harness.installPackage !== void 0;
4918
+ }
4919
+
4920
+ // src/ui/report/detection/format-detection.ts
4921
+ import pc8 from "picocolors";
3957
4922
  function formatDetection(rows, masker2) {
3958
4923
  return [
3959
4924
  ...formatHeading("Where each agent keeps its settings"),
@@ -3962,16 +4927,16 @@ function formatDetection(rows, masker2) {
3962
4927
  }
3963
4928
  var LABEL_COLUMN_WIDTH = 24;
3964
4929
  var DETECTION_STYLE = {
3965
- found: { glyph: GLYPHS.success, colour: pc7.green, label: "found" },
3966
- "not-found": { glyph: GLYPHS.absent, colour: pc7.dim, label: "not found" },
3967
- failed: { glyph: GLYPHS.failure, colour: pc7.red, label: "could not check" },
3968
- "not-configurable": { glyph: GLYPHS.failure, colour: pc7.yellow, label: "not configurable" }
4930
+ found: { glyph: GLYPHS.success, colour: pc8.green, label: "found" },
4931
+ "not-found": { glyph: GLYPHS.absent, colour: pc8.dim, label: "not found" },
4932
+ failed: { glyph: GLYPHS.failure, colour: pc8.red, label: "could not check" },
4933
+ "not-configurable": { glyph: GLYPHS.failure, colour: pc8.yellow, label: "not configurable" }
3969
4934
  };
3970
4935
  function formatDetectionRow({ harness, detection }, masker2) {
3971
4936
  const style = DETECTION_STYLE[findDetectionStatus(harness, detection)];
3972
4937
  return [
3973
4938
  ` ${style.colour(style.glyph)} ${harness.label.padEnd(LABEL_COLUMN_WIDTH)} ${style.colour(style.label)}`,
3974
- `${INDENT}${pc7.dim(masker2.maskText(detection.detail))}`
4939
+ `${INDENT}${pc8.dim(masker2.maskText(detection.detail))}`
3975
4940
  ];
3976
4941
  }
3977
4942
  function findDetectionStatus(harness, detection) {
@@ -3979,9 +4944,9 @@ function findDetectionStatus(harness, detection) {
3979
4944
  return detection.kind;
3980
4945
  }
3981
4946
 
3982
- // src/ui/report/format-result.ts
3983
- import pc9 from "picocolors";
3984
- import { P as P5, match as match23 } from "ts-pattern";
4947
+ // src/ui/report/result/format-result.ts
4948
+ import pc10 from "picocolors";
4949
+ import { P as P6, match as match26 } from "ts-pattern";
3985
4950
 
3986
4951
  // src/commands/setup/setup-result.ts
3987
4952
  var OUTCOME_KINDS = ["updated", "unchanged", "not-found", "unsupported", "failed"];
@@ -3999,16 +4964,16 @@ function formatList(items) {
3999
4964
  }
4000
4965
 
4001
4966
  // src/ui/report/outcome-style.ts
4002
- import pc8 from "picocolors";
4967
+ import pc9 from "picocolors";
4003
4968
  var OUTCOME_STYLE = {
4004
- updated: { glyph: GLYPHS.success, colour: pc8.green, label: "updated" },
4005
- unchanged: { glyph: GLYPHS.unchanged, colour: pc8.green, label: "already configured" },
4006
- "not-found": { glyph: GLYPHS.absent, colour: pc8.dim, label: "not found" },
4007
- unsupported: { glyph: GLYPHS.failure, colour: pc8.yellow, label: "not configurable" },
4008
- failed: { glyph: GLYPHS.failure, colour: pc8.red, label: "failed" }
4969
+ updated: { glyph: GLYPHS.success, colour: pc9.green, label: "updated" },
4970
+ unchanged: { glyph: GLYPHS.unchanged, colour: pc9.green, label: "already configured" },
4971
+ "not-found": { glyph: GLYPHS.absent, colour: pc9.dim, label: "not found" },
4972
+ unsupported: { glyph: GLYPHS.failure, colour: pc9.yellow, label: "not configurable" },
4973
+ failed: { glyph: GLYPHS.failure, colour: pc9.red, label: "failed" }
4009
4974
  };
4010
4975
 
4011
- // src/ui/report/format-result.ts
4976
+ // src/ui/report/result/format-result.ts
4012
4977
  function formatResult(params) {
4013
4978
  const heading = params.mode.kind === "dry-run" ? "Result (dry run)" : "Result";
4014
4979
  return [
@@ -4023,21 +4988,21 @@ function formatResult(params) {
4023
4988
  function formatOutcome2(outcome, params) {
4024
4989
  const { masker: masker2 } = params;
4025
4990
  const style = OUTCOME_STYLE[outcome.kind];
4026
- const body = match23(outcome).with({ kind: P5.union("updated", "unchanged") }, (planned) => formatPlannedOutcome(planned, params)).with({ kind: "not-found" }, ({ detail }) => [`${INDENT}${pc9.dim(masker2.maskText(detail))}`]).with({ kind: "unsupported" }, ({ harness }) => [`${INDENT}${pc9.dim(harness.reason)}`]).with({ kind: "failed" }, ({ error }) => [`${INDENT}${pc9.red(masker2.maskText(error))}`]).exhaustive();
4991
+ const body = match26(outcome).with({ kind: P6.union("updated", "unchanged") }, (planned) => formatPlannedOutcome(planned, params)).with({ kind: "not-found" }, ({ detail }) => [`${INDENT}${pc10.dim(masker2.maskText(detail))}`]).with({ kind: "unsupported" }, ({ harness }) => [`${INDENT}${pc10.dim(harness.reason)}`]).with({ kind: "failed" }, ({ error }) => [`${INDENT}${pc10.red(masker2.maskText(error))}`]).exhaustive();
4027
4992
  return ["", ` ${style.colour(`${style.glyph} ${outcome.harness.label} \u2014 ${style.label}`)}`, ...body];
4028
4993
  }
4029
- function formatPlannedOutcome(outcome, { envFile, home, masker: masker2 }) {
4994
+ function formatPlannedOutcome(outcome, { envFile, home, masker: masker2, model }) {
4030
4995
  return [
4031
- `${INDENT}${pc9.dim(masker2.maskText(outcome.detail))}`,
4032
- ...outcome.warnings.map((warning) => `${INDENT}${pc9.yellow(`${GLYPHS.warning} ${masker2.maskText(warning)}`)}`),
4996
+ `${INDENT}${pc10.dim(masker2.maskText(outcome.detail))}`,
4997
+ ...outcome.warnings.map((warning) => `${INDENT}${pc10.yellow(`${GLYPHS.warning} ${masker2.maskText(warning)}`)}`),
4033
4998
  ...outcome.files.filter((file) => file.isChanged).flatMap((file) => formatResultingFile(file.after, masker2)),
4034
4999
  ...envFile === null ? [] : formatEnvVariables({ outcome, envFile, home, masker: masker2 }),
4035
- ...formatPostSetupNotes(outcome)
5000
+ ...formatPostSetupNotes(outcome, model, masker2)
4036
5001
  ];
4037
5002
  }
4038
5003
  function formatResultingFile(content, masker2) {
4039
5004
  return [
4040
- `${INDENT}${pc9.dim("resulting file:")}`,
5005
+ `${INDENT}${pc10.dim("resulting file:")}`,
4041
5006
  ...indentLines({ text: masker2.maskText(content), indent: NESTED_INDENT })
4042
5007
  ];
4043
5008
  }
@@ -4048,34 +5013,44 @@ function formatEnvVariables({ outcome, envFile, home, masker: masker2 }) {
4048
5013
  if (variables.length === 0) return [];
4049
5014
  const syntax = ENV_SYNTAXES[envFile.shell];
4050
5015
  return [
4051
- `${INDENT}${pc9.dim(`variables in ${displayPath(envFile.path, home)}:`)}`,
5016
+ `${INDENT}${pc10.dim(`variables in ${displayPath(envFile.path, home)}:`)}`,
4052
5017
  ...variables.map(([name, value]) => `${NESTED_INDENT}${masker2.maskText(syntax.formatAssignment({ name, value }))}`)
4053
5018
  ];
4054
5019
  }
4055
- function formatPostSetupNotes({ kind, harness }) {
4056
- if (kind === "unchanged") return [];
4057
- return (harness.postSetupNotes ?? []).map((note4) => `${INDENT}${pc9.cyan(`${GLYPHS.next} ${note4}`)}`);
5020
+ function formatPostSetupNotes({ kind, harness }, model, masker2) {
5021
+ const notes = kind === "unchanged" ? [harness.postSetupNote?.(model)] : [...harness.postSetupNotes ?? [], harness.postSetupNote?.(model)];
5022
+ return notes.filter((note4) => note4 !== void 0).map((note4) => `${INDENT}${pc10.cyan(`${GLYPHS.next} ${masker2.maskText(note4)}`)}`);
4058
5023
  }
4059
- function formatLoadStep({ outcomes, envFile, mode }) {
4060
- if (mode.kind === "dry-run" || envFile === null || !envFile.isChanged) return [];
5024
+ function formatLoadStep({ outcomes, envFile, userEnvironment: userEnvironment2, mode }) {
5025
+ if (mode.kind === "dry-run") return [];
5026
+ const hasEnvFileChange = envFile !== null && envFile.isChanged;
5027
+ if (userEnvironment2 === null && !hasEnvFileChange) return [];
4061
5028
  const readers = outcomes.filter((outcome) => outcome.kind === "updated" && Object.keys(outcome.envVariables).length > 0).map((outcome) => outcome.harness.label);
4062
- if (readers.length === 0) return [];
5029
+ if (readers.length === 0 || envFile === null) return [];
4063
5030
  const [verb, pronoun] = readers.length === 1 ? ["reads", "it"] : ["read", "they"];
4064
5031
  const { loadCommand } = ENV_SYNTAXES[envFile.shell];
5032
+ if (userEnvironment2 !== null) {
5033
+ return [
5034
+ ...formatHeading("One more step"),
5035
+ ` ${formatList(readers)} ${verb} the key from the environment. Open a new terminal to pick it up.`,
5036
+ ` ${pc10.dim("Or load it now for this session:")}`,
5037
+ `${INDENT}${pc10.bold(loadCommand)}`
5038
+ ];
5039
+ }
4065
5040
  return [
4066
5041
  ...formatHeading("One more step"),
4067
5042
  ` ${formatList(readers)} ${verb} the key from the environment. Load it with:`,
4068
- `${INDENT}${pc9.bold(loadCommand)}`,
4069
- ` ${pc9.dim(`Until then ${pronoun} will not see the key.`)}`,
4070
- ...match23(envFile.shell).with("powershell", () => [
4071
- ` ${pc9.dim("That lasts for this session only. Add the same line to $PROFILE to keep it.")}`
5043
+ `${INDENT}${pc10.bold(loadCommand)}`,
5044
+ ` ${pc10.dim(`Until then ${pronoun} will not see the key.`)}`,
5045
+ ...match26(envFile.shell).with("powershell", () => [
5046
+ ` ${pc10.dim("That lasts for this session only. Add the same line to $PROFILE to keep it.")}`
4072
5047
  ]).with("posix", () => []).exhaustive()
4073
5048
  ];
4074
5049
  }
4075
5050
  function formatBackups2({ mode, home, masker: masker2 }) {
4076
5051
  if (mode.kind === "dry-run" || mode.backupRoot === null) return [];
4077
5052
  const backupRoot = masker2.maskText(displayPath(mode.backupRoot, home));
4078
- return [...formatHeading("Backups"), ` Originals copied to ${pc9.bold(backupRoot)}`];
5053
+ return [...formatHeading("Backups"), ` Originals copied to ${pc10.bold(backupRoot)}`];
4079
5054
  }
4080
5055
  function formatSummary2(outcomes) {
4081
5056
  const countsByKind = /* @__PURE__ */ new Map();
@@ -4083,16 +5058,22 @@ function formatSummary2(outcomes) {
4083
5058
  const counts = OUTCOME_KINDS.map((kind) => `${countsByKind.get(kind) ?? 0} ${OUTCOME_STYLE[kind].label}`);
4084
5059
  return [...formatHeading("Summary"), ` ${counts.join(", ")}`];
4085
5060
  }
4086
- function formatRunHints({ runHints, mode }) {
5061
+ function formatRunHints({ runHints, mode, model, shell }) {
4087
5062
  if (mode.kind !== "written" || runHints.length === 0) return [];
4088
- return ["", "You're all set up.", ...runHints.map(formatRunHint)];
5063
+ return ["", "You're all set up.", ...runHints.map((runHint) => formatRunHint({ runHint, model, shell }))];
4089
5064
  }
4090
- function formatRunHint({ binary, label }) {
4091
- return ` Run \`${binary}\` to use ${label} with the nexos.ai Gateway.`;
5065
+ function formatRunHint({ runHint: { binary, label }, model, shell }) {
5066
+ const command = binary === "codex" ? `${binary} ${formatModelOverride(model, shell)}` : binary;
5067
+ return ` Run \`${command}\` to use ${label} with the nexos.ai Gateway.`;
5068
+ }
5069
+ function formatModelOverride(model, shell) {
5070
+ const quotedValue = JSON.stringify(model);
5071
+ const escapedValue = match26(shell).with("posix", () => escapePosixSingleQuoted(`model=${quotedValue}`)).with("powershell", () => escapePowerShellSingleQuoted(`model=${quotedValue}`)).exhaustive();
5072
+ return `-c '${escapedValue}'`;
4092
5073
  }
4093
5074
 
4094
5075
  // src/commands/setup/answers/describe-key-destination.ts
4095
- import { match as match24 } from "ts-pattern";
5076
+ import { match as match27 } from "ts-pattern";
4096
5077
 
4097
5078
  // src/commands/setup/answers/scope.ts
4098
5079
  async function resolveScope({
@@ -4140,7 +5121,7 @@ function describeKeyDestination({ configurable, ctx, envPath }) {
4140
5121
  return `It will be written to ${formatList(configFiles)}, and to ${envFile}.`;
4141
5122
  }
4142
5123
  function displayScopedPath(filePath, ctx) {
4143
- return match24(ctx.scope).with("user", () => displayPath(filePath, ctx.home)).with("project", () => displayRelativePath(filePath, ctx.cwd)).exhaustive();
5124
+ return match27(ctx.scope).with("user", () => displayPath(filePath, ctx.home)).with("project", () => displayRelativePath(filePath, ctx.cwd)).exhaustive();
4144
5125
  }
4145
5126
 
4146
5127
  // src/commands/setup/answers/harness-selection.ts
@@ -4192,15 +5173,23 @@ var DETECTION_HINTS = {
4192
5173
  "not-found": "not found",
4193
5174
  failed: "could not check"
4194
5175
  };
5176
+ var INSTALL_FIRST_HINT = "not installed \u2014 will be installed first";
4195
5177
  function describeHarnessChoice(candidate, scope) {
4196
5178
  const ineligibility = findIneligibility(candidate.harness, scope);
4197
5179
  return {
4198
5180
  value: candidate.harness.id,
4199
5181
  label: candidate.harness.label,
4200
- hint: ineligibility ?? DETECTION_HINTS[candidate.detection.kind],
5182
+ hint: ineligibility ?? describeDetection(candidate, scope),
4201
5183
  isDisabled: ineligibility !== void 0
4202
5184
  };
4203
5185
  }
5186
+ function describeDetection({ harness, detection }, scope) {
5187
+ if (scope === "user" && detection.kind === "not-found" && isInstallable2(harness)) return INSTALL_FIRST_HINT;
5188
+ return DETECTION_HINTS[detection.kind];
5189
+ }
5190
+ function isInstallable2(harness) {
5191
+ return harness.kind === "configurable" && harness.installPackage !== void 0;
5192
+ }
4204
5193
  function isDefaultChoice({ harness, detection }, scope) {
4205
5194
  return findIneligibility(harness, scope) === void 0 && detection.kind === "found";
4206
5195
  }
@@ -4211,7 +5200,7 @@ function findIneligibility(harness, scope) {
4211
5200
  }
4212
5201
 
4213
5202
  // src/commands/setup/changes/apply-changes.ts
4214
- import path19 from "path";
5203
+ import path22 from "path";
4215
5204
 
4216
5205
  // src/commands/setup/changes/change-plan.ts
4217
5206
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
@@ -4219,8 +5208,15 @@ function planChanges(params) {
4219
5208
  const { envPath, shell } = params;
4220
5209
  const drafts = params.selected.map((candidate) => planHarness(candidate, params));
4221
5210
  const envFileRead = drafts.some(hasEnvVariables) ? readEnvFile({ envPath, home: params.ctx.home }) : NO_ENV_FILE;
4222
- const outcomes = drafts.map((draft) => settleOutcome({ draft, envFileRead, shell }));
4223
- return { outcomes, envFile: planEnvFile2({ outcomes, envFileRead, envPath, shell }) };
5211
+ const userEnvironment2 = planSetupUserEnvironment({ drafts, params });
5212
+ const outcomes = drafts.map(
5213
+ (draft) => settleOutcome({ draft, envFileRead, shell, userEnvironment: userEnvironment2.plan })
5214
+ );
5215
+ return {
5216
+ outcomes: addUserEnvironmentWarning2({ outcomes, warning: userEnvironment2.warning }),
5217
+ envFile: planEnvFile2({ outcomes, envFileRead, envPath, shell }),
5218
+ userEnvironment: userEnvironment2.plan
5219
+ };
4224
5220
  }
4225
5221
  function listChangedFiles({ outcomes, envFile }) {
4226
5222
  const changedFiles = outcomes.flatMap(
@@ -4305,14 +5301,37 @@ function collectEnvVariables(contributions) {
4305
5301
  function hasEnvVariables(draft) {
4306
5302
  return draft.kind === "planned" && Object.keys(draft.envVariables).length > 0;
4307
5303
  }
4308
- function settleOutcome({ draft, envFileRead, shell }) {
5304
+ function settleOutcome({ draft, envFileRead, shell, userEnvironment: userEnvironment2 }) {
4309
5305
  if (draft.kind !== "planned") return draft;
4310
5306
  if (envFileRead.kind === "failed" && hasEnvVariables(draft)) return failWith(draft, envFileRead.error);
4311
5307
  const envText = envFileRead.kind === "read" ? envFileRead.text : null;
4312
5308
  const hasOwnEnvChange2 = hasEnvVariables(draft) && renderEnvFile({ current: envText, variables: draft.envVariables, shell }) !== envText;
4313
- const isChanged = hasOwnEnvChange2 || draft.files.some((file) => file.isChanged);
5309
+ const hasOwnUserEnvironmentChange = userEnvironment2 !== null && Object.keys(draft.envVariables).some((name) => userEnvironment2.before[name] !== userEnvironment2.after[name]);
5310
+ const isChanged = hasOwnEnvChange2 || hasOwnUserEnvironmentChange || draft.files.some((file) => file.isChanged);
4314
5311
  return { ...draft, kind: isChanged ? "updated" : "unchanged" };
4315
5312
  }
5313
+ function planSetupUserEnvironment({ drafts, params }) {
5314
+ if (params.ctx.platform !== "win32") return { plan: null, warning: null };
5315
+ const values = Object.fromEntries(
5316
+ drafts.flatMap((draft) => draft.kind === "planned" ? Object.entries(draft.envVariables) : [])
5317
+ );
5318
+ if (Object.keys(values).length === 0) return { plan: null, warning: null };
5319
+ try {
5320
+ return { plan: planUserEnvironment({ values, userEnvironment: params.userEnvironment }), warning: null };
5321
+ } catch (error) {
5322
+ if (!(error instanceof UserEnvironmentError)) throw error;
5323
+ return {
5324
+ plan: null,
5325
+ warning: describeUserEnvironmentFailure({ action: "read", envPath: params.envPath, home: params.ctx.home })
5326
+ };
5327
+ }
5328
+ }
5329
+ function addUserEnvironmentWarning2({ outcomes, warning }) {
5330
+ if (warning === null) return [...outcomes];
5331
+ return outcomes.map(
5332
+ (outcome) => isPlanned(outcome) && Object.keys(outcome.envVariables).length > 0 ? { ...outcome, warnings: [...outcome.warnings, warning] } : outcome
5333
+ );
5334
+ }
4316
5335
  function failWith({ harness }, error) {
4317
5336
  return { kind: "failed", harness, error };
4318
5337
  }
@@ -4326,32 +5345,37 @@ function classifyFileOrigin(before) {
4326
5345
  if (before === null) return "absent";
4327
5346
  return before.trim() === "" ? "blank" : "content";
4328
5347
  }
4329
- function addFileOriginals({ record: record5, harnessId, file }) {
4330
- const harness = readHarnessRecord(record5, harnessId);
5348
+ function addFileOriginals({ record: record9, harnessId, file }) {
5349
+ const harness = readHarnessRecord(record9, harnessId);
4331
5350
  const existing = harness.files.find((recorded) => isSamePath2(recorded.path, file.path));
4332
5351
  const others = harness.files.filter((recorded) => recorded !== existing);
4333
- return withHarnessRecord(record5, harnessId, { ...harness, files: [...others, mergeFile(existing, file)] });
5352
+ return withHarnessRecord(record9, harnessId, { ...harness, files: [...others, mergeFile(existing, file)] });
4334
5353
  }
4335
- function addEnvOriginals({ record: record5, harnessId, variables, isFileAbsent }) {
4336
- const harness = readHarnessRecord(record5, harnessId);
5354
+ function addEnvOriginals({ record: record9, harnessId, variables, isFileAbsent }) {
5355
+ const harness = readHarnessRecord(record9, harnessId);
4337
5356
  const kept = isFileAbsent ? [] : harness.envVariables;
4338
5357
  const added = variables.filter((variable) => !kept.some(({ name }) => name === variable.name));
4339
- return withHarnessRecord(record5, harnessId, { ...harness, envVariables: [...kept, ...added] });
5358
+ return withHarnessRecord(record9, harnessId, { ...harness, envVariables: [...kept, ...added] });
5359
+ }
5360
+ function addUserEnvironmentOriginals({ record: record9, variables }) {
5361
+ const kept = record9.userEnvironment;
5362
+ const added = variables.filter((variable) => !kept.some(({ name }) => name === variable.name));
5363
+ return { ...record9, userEnvironment: [...kept, ...added] };
4340
5364
  }
4341
5365
  var EMPTY_HARNESS_RECORD = { files: [], envVariables: [] };
4342
- function readHarnessRecord(record5, harnessId) {
4343
- return record5.harnesses[harnessId] ?? EMPTY_HARNESS_RECORD;
5366
+ function readHarnessRecord(record9, harnessId) {
5367
+ return record9.harnesses[harnessId] ?? EMPTY_HARNESS_RECORD;
4344
5368
  }
4345
- function withHarnessRecord(record5, harnessId, harness) {
4346
- return { ...record5, harnesses: { ...record5.harnesses, [harnessId]: harness } };
5369
+ function withHarnessRecord(record9, harnessId, harness) {
5370
+ return { ...record9, harnesses: { ...record9.harnesses, [harnessId]: harness } };
4347
5371
  }
4348
5372
  function mergeFile(existing, file) {
4349
5373
  if (existing === void 0 || file.origin === "absent") return file;
4350
5374
  const added = file.keys.filter((key) => !existing.keys.some(({ keyPath }) => isSamePath2(keyPath, key.keyPath)));
4351
5375
  return { ...existing, keys: [...existing.keys, ...added] };
4352
5376
  }
4353
- function isSamePath2(path22, other) {
4354
- return path22.length === other.length && path22.every((segment, index) => segment === other[index]);
5377
+ function isSamePath2(path26, other) {
5378
+ return path26.length === other.length && path26.every((segment, index) => segment === other[index]);
4355
5379
  }
4356
5380
 
4357
5381
  // src/commands/setup/changes/record-originals.ts
@@ -4363,7 +5387,7 @@ function recordFileOriginals({ harnessId, file, ctx }) {
4363
5387
  origin: classifyFileOrigin(file.before),
4364
5388
  keys: file.originals
4365
5389
  };
4366
- updateRestoreRecord(location, ctx, (record5) => addFileOriginals({ record: record5, harnessId, file: recordedFile }));
5390
+ updateRestoreRecord(location, ctx, (record9) => addFileOriginals({ record: record9, harnessId, file: recordedFile }));
4367
5391
  }
4368
5392
  function recordEnvOriginals({ outcomes, envFile, ctx }) {
4369
5393
  const syntax = ENV_SYNTAXES[envFile.shell];
@@ -4372,7 +5396,7 @@ function recordEnvOriginals({ outcomes, envFile, ctx }) {
4372
5396
  updateRestoreRecord(
4373
5397
  resolveRestoreRecordLocation(ctx),
4374
5398
  ctx,
4375
- (record5) => outcomes.reduce((updated, outcome) => {
5399
+ (record9) => outcomes.reduce((updated, outcome) => {
4376
5400
  if (outcome.kind !== "updated") return updated;
4377
5401
  const variables = Object.keys(outcome.envVariables).map((name) => ({
4378
5402
  name,
@@ -4380,13 +5404,25 @@ function recordEnvOriginals({ outcomes, envFile, ctx }) {
4380
5404
  }));
4381
5405
  if (variables.length === 0) return updated;
4382
5406
  return addEnvOriginals({ record: updated, harnessId: outcome.harness.id, variables, isFileAbsent });
4383
- }, record5)
5407
+ }, record9)
5408
+ );
5409
+ }
5410
+ function recordUserEnvironmentOriginals({ userEnvironment: userEnvironment2, ctx }) {
5411
+ const variables = Object.keys(userEnvironment2.after).map((name) => ({
5412
+ name,
5413
+ original: userEnvironment2.before[name] ?? null
5414
+ }));
5415
+ if (variables.length === 0) return;
5416
+ updateRestoreRecord(
5417
+ resolveRestoreRecordLocation(ctx),
5418
+ ctx,
5419
+ (record9) => addUserEnvironmentOriginals({ record: record9, variables })
4384
5420
  );
4385
5421
  }
4386
5422
  function updateRestoreRecord(location, ctx, update) {
4387
5423
  if (location.gitIgnoredDirectory !== null) ignoreBackupsInGit(location.gitIgnoredDirectory);
4388
- const record5 = readRestoreRecord({ filePath: location.filePath, home: ctx.home }) ?? createEmptyRestoreRecord();
4389
- writeRestoreRecord({ filePath: location.filePath, record: update(record5), platform: ctx.platform });
5424
+ const record9 = readRestoreRecord({ filePath: location.filePath, home: ctx.home }) ?? createEmptyRestoreRecord();
5425
+ writeRestoreRecord({ filePath: location.filePath, record: update(record9), platform: ctx.platform });
4390
5426
  }
4391
5427
 
4392
5428
  // src/commands/setup/changes/apply-changes.ts
@@ -4395,10 +5431,12 @@ function applyChanges(params) {
4395
5431
  const outcomes = agentWrites.map(({ outcome }) => outcome);
4396
5432
  const envFile = replanEnvFile2(params.plan.envFile, outcomes);
4397
5433
  const envWrite = writeSharedEnvFile({ outcomes, envFile, params });
5434
+ const userEnvironmentWrite = writeUserEnvironment2({ outcomes: envWrite.outcomes, params });
4398
5435
  const writtenFiles = [...agentWrites.flatMap(({ writtenFiles: writtenFiles2 }) => writtenFiles2), ...envWrite.writtenFiles];
4399
5436
  return {
4400
- outcomes: envWrite.outcomes,
5437
+ outcomes: userEnvironmentWrite.outcomes,
4401
5438
  envFile,
5439
+ userEnvironment: userEnvironmentWrite.written,
4402
5440
  writtenCount: writtenFiles.length,
4403
5441
  backupRoot: writtenFiles.some((file) => file.isBackedUp) ? params.backupRoot.path : null
4404
5442
  };
@@ -4408,10 +5446,10 @@ function writeAgentFiles(outcome, params) {
4408
5446
  const writtenFiles = [];
4409
5447
  for (const file of outcome.files.filter((plan) => plan.isChanged)) {
4410
5448
  try {
4411
- const relativePath = path19.join(outcome.harness.id, path19.basename(file.path));
4412
- const record5 = () => recordFileOriginals({ harnessId: outcome.harness.id, file, ctx: params.ctx });
5449
+ const relativePath = path22.join(outcome.harness.id, path22.basename(file.path));
5450
+ const record9 = () => recordFileOriginals({ harnessId: outcome.harness.id, file, ctx: params.ctx });
4413
5451
  const write = () => writeFileWithMode({ filePath: file.path, content: file.after, mode: resolveWriteMode(file, params.ctx) });
4414
- writtenFiles.push(backUpThenWrite({ file, relativePath, record: record5, write, params }));
5452
+ writtenFiles.push(backUpThenWrite({ file, relativePath, record: record9, write, params }));
4415
5453
  } catch (error) {
4416
5454
  const reason = describeFileError({ error, action: "write", home: params.ctx.home });
4417
5455
  return { outcome: { kind: "failed", harness: outcome.harness, error: reason }, writtenFiles };
@@ -4426,11 +5464,11 @@ function replanEnvFile2(envFile, outcomes) {
4426
5464
  function writeSharedEnvFile({ outcomes, envFile, params }) {
4427
5465
  if (envFile === null || !envFile.isChanged) return { outcomes, writtenFiles: [] };
4428
5466
  try {
4429
- const relativePath = path19.join(ENV_FILE_BACKUP_DIRECTORY, path19.basename(envFile.path));
5467
+ const relativePath = path22.join(ENV_FILE_BACKUP_DIRECTORY, path22.basename(envFile.path));
4430
5468
  const { ctx } = params;
4431
- const record5 = () => recordEnvOriginals({ outcomes, envFile, ctx });
5469
+ const record9 = () => recordEnvOriginals({ outcomes, envFile, ctx });
4432
5470
  const write = () => writeEnvFile({ filePath: envFile.path, content: envFile.after, platform: ctx.platform });
4433
- return { outcomes, writtenFiles: [backUpThenWrite({ file: envFile, relativePath, record: record5, write, params })] };
5471
+ return { outcomes, writtenFiles: [backUpThenWrite({ file: envFile, relativePath, record: record9, write, params })] };
4434
5472
  } catch (error) {
4435
5473
  const reason = describeFileError({ error, action: "write", home: params.ctx.home });
4436
5474
  return { outcomes: outcomes.map((outcome) => failIfWritingEnvFile(outcome, reason)), writtenFiles: [] };
@@ -4440,10 +5478,31 @@ function failIfWritingEnvFile(outcome, error) {
4440
5478
  if (outcome.kind !== "updated" || Object.keys(outcome.envVariables).length === 0) return outcome;
4441
5479
  return { kind: "failed", harness: outcome.harness, error };
4442
5480
  }
4443
- function backUpThenWrite({ file, relativePath, record: record5, write, params }) {
5481
+ function writeUserEnvironment2({ outcomes, params }) {
5482
+ const { userEnvironment: userEnvironment2, envPath, ctx } = params;
5483
+ const plan = replanUserEnvironment({ plan: params.plan.userEnvironment, names: userEnvironmentNames2(outcomes) });
5484
+ if (plan === null || !plan.isChanged) return { outcomes: [...outcomes], written: null };
5485
+ try {
5486
+ recordUserEnvironmentOriginals({ userEnvironment: plan, ctx });
5487
+ userEnvironment2.write(plan.after);
5488
+ return { outcomes: [...outcomes], written: plan };
5489
+ } catch {
5490
+ const warning = describeUserEnvironmentFailure({ action: "write", envPath, home: ctx.home });
5491
+ return { outcomes: outcomes.map((outcome) => addUserEnvironmentWarningTo2(outcome, warning)), written: null };
5492
+ }
5493
+ }
5494
+ function addUserEnvironmentWarningTo2(outcome, warning) {
5495
+ if (outcome.kind !== "updated" || Object.keys(outcome.envVariables).length === 0) return outcome;
5496
+ return { ...outcome, warnings: [...outcome.warnings, warning] };
5497
+ }
5498
+ function userEnvironmentNames2(outcomes) {
5499
+ if (outcomes.length === 0) return null;
5500
+ return new Set(outcomes.flatMap((outcome) => isPlanned(outcome) ? Object.keys(outcome.envVariables) : []));
5501
+ }
5502
+ function backUpThenWrite({ file, relativePath, record: record9, write, params }) {
4444
5503
  const { ctx, backupRoot } = params;
4445
5504
  const backup = backUpBeforeWriting({ filePath: file.path, before: file.before, backupRoot, relativePath, ctx });
4446
- record5();
5505
+ record9();
4447
5506
  write();
4448
5507
  return backup;
4449
5508
  }
@@ -4473,11 +5532,26 @@ async function runSetup(params) {
4473
5532
  ui
4474
5533
  });
4475
5534
  const ctx = { ...params.baseContext, scope };
4476
- const selected = await selectHarnesses({ namedHarnesses, ctx, shouldAssumeYes: options.shouldAssumeYes, ui });
4477
- assertProjectSupport({ selected, scope, isScopeNamed: options.scope !== void 0 });
5535
+ if (options.shouldInstall && scope !== "user") {
5536
+ throw new UsageError("--install installs globally; it applies to user scope only.");
5537
+ }
5538
+ const detected = await selectHarnesses({ namedHarnesses, ctx, shouldAssumeYes: options.shouldAssumeYes, ui });
5539
+ assertProjectSupport({ selected: detected, scope, isScopeNamed: options.scope !== void 0 });
5540
+ const install = await installMissingHarnesses({
5541
+ notInstalled: detected.flatMap(({ harness, detection }) => detection.kind === "not-found" ? [harness] : []),
5542
+ isPickerDriven: namedHarnesses.length === 0 && !options.shouldAssumeYes,
5543
+ options,
5544
+ ctx,
5545
+ ui,
5546
+ runNpm: params.runNpm
5547
+ });
5548
+ const selected = install.installedCount > 0 ? detectHarnesses(
5549
+ detected.map(({ harness }) => harness),
5550
+ ctx
5551
+ ) : detected;
4478
5552
  if (selected.length === 0) {
4479
5553
  ui.cancel("No agents selected \u2014 nothing to do.");
4480
- return EXIT_CODE.ok;
5554
+ return install.hasFailure ? EXIT_CODE.failed : EXIT_CODE.ok;
4481
5555
  }
4482
5556
  warnAboutUnsupported(selected, ui);
4483
5557
  if (!selected.some(({ harness }) => harness.kind === "configurable")) {
@@ -4485,7 +5559,7 @@ async function runSetup(params) {
4485
5559
  return EXIT_CODE.ok;
4486
5560
  }
4487
5561
  const found = selected.flatMap(listFoundConfigurable);
4488
- if (found.length === 0) return endWithNothingFound(selected, ui, masker2);
5562
+ if (found.length === 0) return endWithNothingFound({ selected, ui, masker: masker2, hasInstallFailure: install.hasFailure });
4489
5563
  const shell = resolveShell(ctx.platform);
4490
5564
  const run = { ...params, ui, ctx, shell, envPath: resolveEnvFilePath(ctx.home, shell), gatewayUrls };
4491
5565
  const keyStep = await establishKey({ run, configurable: found, environment });
@@ -4512,21 +5586,46 @@ async function runSetup(params) {
4512
5586
  const key = resolvedKey.key;
4513
5587
  ui.print(formatDetection(selected, masker2));
4514
5588
  const desired = buildDesiredConfig({ gatewayUrls, apiKey: key, model });
4515
- const plan = planChanges({ selected, desired, ctx, envPath: run.envPath, shell, isDryRun: options.isDryRun });
5589
+ const plan = planChanges({
5590
+ selected,
5591
+ desired,
5592
+ ctx,
5593
+ envPath: run.envPath,
5594
+ shell,
5595
+ isDryRun: options.isDryRun,
5596
+ userEnvironment: run.userEnvironment
5597
+ });
4516
5598
  const changes = listChangedFiles(plan);
4517
5599
  const isPlanFailed = exitCodeFor(plan.outcomes) !== EXIT_CODE.ok;
4518
- ui.print(formatPreview({ changes, model, scope, hasFailedAgent: isPlanFailed, home: ctx.home, masker: masker2 }));
5600
+ ui.print(
5601
+ formatPreview({
5602
+ changes,
5603
+ userEnvironment: plan.userEnvironment,
5604
+ model,
5605
+ scope,
5606
+ hasFailedAgent: isPlanFailed,
5607
+ home: ctx.home,
5608
+ masker: masker2
5609
+ })
5610
+ );
4519
5611
  if (options.isDryRun) {
4520
5612
  if (resolvedKey.shouldSave) ui.dim("Dry run \u2014 the key was not saved.");
4521
5613
  if (modelSave.length > 0) ui.dim("Dry run \u2014 the default model was not saved.");
4522
- return reportResult({ run, plan, mode: { kind: "dry-run" }, outro: "Dry run \u2014 nothing was written." });
5614
+ return reportResult({
5615
+ run,
5616
+ plan,
5617
+ model,
5618
+ mode: { kind: "dry-run" },
5619
+ outro: "Dry run \u2014 nothing was written.",
5620
+ hasInstallFailure: install.hasFailure
5621
+ });
4523
5622
  }
4524
- if (changes.length === 0) {
5623
+ if (changes.length === 0 && plan.userEnvironment?.isChanged !== true) {
4525
5624
  await commitSave(resolvedKey, check4, keyStep.deps);
4526
5625
  saveModelFor(plan.outcomes);
4527
5626
  const mode2 = { kind: "written", backupRoot: null };
4528
5627
  const outro4 = isPlanFailed ? FINISHED_WITH_ERRORS2 : "Nothing to do \u2014 already configured.";
4529
- return reportResult({ run, plan, mode: mode2, outro: outro4 });
5628
+ return reportResult({ run, plan, model, mode: mode2, outro: outro4, hasInstallFailure: install.hasFailure });
4530
5629
  }
4531
5630
  if (!options.shouldAssumeYes && !await ui.confirm("Write these changes? Originals are backed up first.", true)) {
4532
5631
  return abort(ui);
@@ -4536,7 +5635,14 @@ async function runSetup(params) {
4536
5635
  saveModelFor(applied.outcomes);
4537
5636
  const outro3 = exitCodeFor(applied.outcomes) === EXIT_CODE.ok ? "Done." : FINISHED_WITH_ERRORS2;
4538
5637
  const mode = { kind: "written", backupRoot: applied.backupRoot };
4539
- return reportResult({ run, plan: { outcomes: applied.outcomes, envFile: applied.envFile }, mode, outro: outro3 });
5638
+ return reportResult({
5639
+ run,
5640
+ plan: { outcomes: applied.outcomes, envFile: applied.envFile, userEnvironment: applied.userEnvironment },
5641
+ model,
5642
+ mode,
5643
+ outro: outro3,
5644
+ hasInstallFailure: install.hasFailure
5645
+ });
4540
5646
  }
4541
5647
  function warnAboutUnsupported(selected, ui) {
4542
5648
  for (const { harness } of selected) {
@@ -4547,10 +5653,10 @@ function listFoundConfigurable({ harness, detection }) {
4547
5653
  if (harness.kind !== "configurable" || detection.kind !== "found") return [];
4548
5654
  return [harness];
4549
5655
  }
4550
- function endWithNothingFound(selected, ui, masker2) {
5656
+ function endWithNothingFound({ selected, ui, masker: masker2, hasInstallFailure }) {
4551
5657
  ui.print(formatDetection(selected, masker2));
4552
5658
  const hasFailedCheck = selected.some(({ harness, detection }) => isConfigurableUnchecked(harness, detection));
4553
- if (hasFailedCheck) {
5659
+ if (hasFailedCheck || hasInstallFailure) {
4554
5660
  ui.outro(FINISHED_WITH_ERRORS2);
4555
5661
  return EXIT_CODE.failed;
4556
5662
  }
@@ -4597,24 +5703,32 @@ function writeChanges(run, plan) {
4597
5703
  });
4598
5704
  const spinner = ui.spinner();
4599
5705
  spinner.start("Backing up originals and writing configs\u2026");
4600
- const applied = applyChanges({ plan, ctx, backupRoot });
4601
- spinner.stop(`Wrote ${formatCount(applied.writtenCount, "file")}.`);
5706
+ const applied = applyChanges({ plan, ctx, backupRoot, userEnvironment: run.userEnvironment, envPath: run.envPath });
5707
+ spinner.stop(describeWritten(applied));
4602
5708
  return applied;
4603
5709
  }
4604
- function reportResult({ run, plan, mode, outro: outro3 }) {
5710
+ function describeWritten(applied) {
5711
+ if (applied.writtenCount > 0) return `Wrote ${formatCount(applied.writtenCount, "file")}.`;
5712
+ if (applied.userEnvironment !== null) return "Updated the Windows user environment.";
5713
+ return "Wrote 0 files.";
5714
+ }
5715
+ function reportResult({ run, plan, model, mode, outro: outro3, hasInstallFailure }) {
4605
5716
  const { ui, ctx, masker: masker2 } = run;
4606
5717
  ui.print(
4607
5718
  formatResult({
4608
5719
  outcomes: plan.outcomes,
4609
5720
  envFile: plan.envFile,
5721
+ userEnvironment: plan.userEnvironment,
4610
5722
  mode,
5723
+ model,
5724
+ shell: run.shell,
4611
5725
  home: ctx.home,
4612
5726
  masker: masker2,
4613
5727
  runHints: listRunHints(plan.outcomes, ctx)
4614
5728
  })
4615
5729
  );
4616
5730
  ui.outro(outro3);
4617
- return exitCodeFor(plan.outcomes);
5731
+ return hasInstallFailure ? EXIT_CODE.failed : exitCodeFor(plan.outcomes);
4618
5732
  }
4619
5733
  function listRunHints(outcomes, ctx) {
4620
5734
  return outcomes.filter(isPlanned).flatMap(({ harness }) => ctx.hasBinary(harness.binary) ? [{ binary: harness.binary, label: harness.label }] : []);
@@ -4624,9 +5738,473 @@ function abort(ui) {
4624
5738
  return EXIT_CODE.cancelled;
4625
5739
  }
4626
5740
 
4627
- // src/harnesses/run-launch.ts
5741
+ // src/ui/report/result/format-status.ts
5742
+ import pc11 from "picocolors";
5743
+ var TITLE = "nexos-connect status";
5744
+ var HEADER_LABEL_WIDTH = 11;
5745
+ var SCOPE_WIDTH = 9;
5746
+ var STATE_WIDTH = 15;
5747
+ var SCOPE_STATE_COLOUR = {
5748
+ configured: pc11.green,
5749
+ drifted: pc11.yellow,
5750
+ "not configured": pc11.dim,
5751
+ "not installed": pc11.dim,
5752
+ unreadable: pc11.red
5753
+ };
5754
+ function formatStatus({ report: report2, masker: masker2 }) {
5755
+ return [
5756
+ pc11.bold(TITLE),
5757
+ "",
5758
+ ...formatHeader(report2, masker2),
5759
+ ...report2.harnesses.flatMap((harness) => formatHarness(harness, masker2)),
5760
+ ...formatErrors(report2.errors, masker2)
5761
+ ];
5762
+ }
5763
+ function formatHeader(report2, masker2) {
5764
+ return [
5765
+ formatHeaderRow("Key", describeKey(report2.key)),
5766
+ formatHeaderRow("Env file", describeEnvFile2(report2.envFile)),
5767
+ formatHeaderRow("Backups", `${report2.backups.user} \xB7 ${report2.backups.project}`),
5768
+ formatHeaderRow("Defaults", describeDefaults(report2.defaults, masker2))
5769
+ ];
5770
+ }
5771
+ function formatHeaderRow(label, value) {
5772
+ return `${label.padEnd(HEADER_LABEL_WIDTH)}${value}`;
5773
+ }
5774
+ function describeKey({ isSaved, masked, isValid }) {
5775
+ if (masked === null) return "none";
5776
+ const source = isSaved ? "keychain" : "found";
5777
+ if (isValid === null) return `${source} \xB7 ${masked}`;
5778
+ return `${source} \xB7 ${masked} \xB7 ${isValid ? "valid" : "rejected"}`;
5779
+ }
5780
+ function describeEnvFile2({ path: path26, isPresent }) {
5781
+ return `${path26} \xB7 ${isPresent ? "present" : "absent"}`;
5782
+ }
5783
+ function describeDefaults(defaults, masker2) {
5784
+ const entries = Object.entries(defaults);
5785
+ if (entries.length === 0) return "none";
5786
+ return entries.map(([id, model]) => `${id} ${masker2.maskText(model)}`).join(" \xB7 ");
5787
+ }
5788
+ function formatHarness(harness, masker2) {
5789
+ return ["", ` ${pc11.bold(harness.label)} ${pc11.dim(`(${harness.id})`)}`, ...formatHarnessBody(harness, masker2)];
5790
+ }
5791
+ function formatHarnessBody(harness, masker2) {
5792
+ if (!harness.isSupported) return [` ${pc11.yellow(`unsupported \u2014 ${harness.reason ?? ""}`)}`];
5793
+ if (harness.scopes === null) return [];
5794
+ return [
5795
+ ...formatScopeRow("user", harness.scopes.user, masker2),
5796
+ ...formatScopeRow("project", harness.scopes.project, masker2),
5797
+ ` ${"undo".padEnd(SCOPE_WIDTH)}${harness.isUndoable ? "yes" : "no"}`,
5798
+ ...harness.warnings.map((warning) => ` ${pc11.yellow(`${GLYPHS.warning} ${warning}`)}`)
5799
+ ];
5800
+ }
5801
+ function formatScopeRow(scopeName, status, masker2) {
5802
+ const colour = SCOPE_STATE_COLOUR[status.state];
5803
+ const path26 = status.path === null ? "" : status.path;
5804
+ const model = status.model === null ? "" : `${INDENT}model ${masker2.maskText(status.model)}`;
5805
+ const detail = status.detail === null ? [] : [`${NESTED_INDENT}${pc11.dim(masker2.maskText(status.detail))}`];
5806
+ return [` ${scopeName.padEnd(SCOPE_WIDTH)}${colour(status.state.padEnd(STATE_WIDTH))}${path26}${model}`, ...detail];
5807
+ }
5808
+ function formatErrors(errors, masker2) {
5809
+ return errors.flatMap((error) => ["", ` ${pc11.red(`${GLYPHS.failure} ${masker2.maskText(error)}`)}`]);
5810
+ }
5811
+
5812
+ // src/ui/report/result/format-status-json.ts
5813
+ var JSON_INDENT4 = 2;
5814
+ function formatStatusJson(report2, masker2) {
5815
+ return JSON.stringify(toStatusJson(report2, masker2), null, JSON_INDENT4);
5816
+ }
5817
+ function toStatusJson(report2, masker2) {
5818
+ return {
5819
+ version: report2.version,
5820
+ key: { saved: report2.key.isSaved, masked: report2.key.masked, valid: report2.key.isValid },
5821
+ envFile: { path: report2.envFile.path, present: report2.envFile.isPresent },
5822
+ backups: report2.backups,
5823
+ defaults: maskValues(report2.defaults, masker2),
5824
+ harnesses: report2.harnesses.map((harness) => toHarnessJson(harness, masker2))
5825
+ };
5826
+ }
5827
+ function toHarnessJson(harness, masker2) {
5828
+ return {
5829
+ id: harness.id,
5830
+ label: harness.label,
5831
+ supported: harness.isSupported,
5832
+ detected: harness.isDetected,
5833
+ undoable: harness.isUndoable,
5834
+ status: harness.status,
5835
+ reason: harness.reason,
5836
+ scopes: harness.scopes === null ? null : { user: toScopeJson(harness.scopes.user, masker2), project: toScopeJson(harness.scopes.project, masker2) },
5837
+ warnings: harness.warnings
5838
+ };
5839
+ }
5840
+ function toScopeJson(scope, masker2) {
5841
+ return {
5842
+ state: scope.state,
5843
+ path: scope.path,
5844
+ model: maskValue(scope.model, masker2),
5845
+ keyMasked: scope.keyMasked,
5846
+ detail: maskValue(scope.detail, masker2),
5847
+ drifted: scope.isDrifted,
5848
+ changedPaths: scope.changedPaths
5849
+ };
5850
+ }
5851
+ function maskValues(values, masker2) {
5852
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, masker2.maskText(value)]));
5853
+ }
5854
+ function maskValue(value, masker2) {
5855
+ return value === null ? null : masker2.maskText(value);
5856
+ }
5857
+
5858
+ // src/commands/status/inspect-harness.ts
5859
+ import { match as match28 } from "ts-pattern";
5860
+
5861
+ // src/commands/status/display-scope-path.ts
5862
+ function displayScopePath(filePath, ctx) {
5863
+ return ctx.scope === "user" ? displayPath(filePath, ctx.home) : displayRelativePath(filePath, ctx.cwd);
5864
+ }
5865
+
5866
+ // src/commands/status/read-config-key.ts
5867
+ function readConfigKey({ harness, ctx, envPath, shell }) {
5868
+ const fromConfig = harness.readKey?.(ctx);
5869
+ if (fromConfig !== void 0) return fromConfig;
5870
+ if (harness.keyEnvVariable === void 0) return void 0;
5871
+ const envRead = readEnvFile({ envPath, home: ctx.home });
5872
+ if (envRead.kind === "failed" || envRead.text === null) return void 0;
5873
+ return readNonBlank(parseEnvFile(envRead.text, shell).get(harness.keyEnvVariable));
5874
+ }
5875
+
5876
+ // src/commands/status/detect-drift.ts
5877
+ function detectDrift(params) {
5878
+ const { harness, ctx, envPath, shell, model } = params;
5879
+ const desired = buildDesiredConfig({
5880
+ gatewayUrls: buildGatewayUrls(resolveGatewayHost(ctx.env)),
5881
+ apiKey: readConfigKey({ harness, ctx, envPath, shell }) ?? "",
5882
+ model
5883
+ });
5884
+ const contributions = harness.plan(desired, ctx);
5885
+ const changedPaths = [
5886
+ ...contributions.flatMap((contribution) => contribution.kind === "file" ? listFileDrift(contribution, ctx) : []),
5887
+ ...listEnvDrift(collectEnvVariables2(contributions), { ctx, envPath, shell })
5888
+ ];
5889
+ return { isDrifted: changedPaths.length > 0, changedPaths };
5890
+ }
5891
+ function listFileDrift(contribution, ctx) {
5892
+ const current = readFileIfExists(contribution.path);
5893
+ try {
5894
+ return renderConfig(current, contribution.edits(current)) === current ? [] : [displayScopePath(contribution.path, ctx)];
5895
+ } catch (error) {
5896
+ if (!(error instanceof ConfigFormatError)) throw error;
5897
+ return [displayScopePath(contribution.path, ctx)];
5898
+ }
5899
+ }
5900
+ function listEnvDrift(variables, params) {
5901
+ if (Object.keys(variables).length === 0) return [];
5902
+ const { ctx, envPath, shell } = params;
5903
+ const envRead = readEnvFile({ envPath, home: ctx.home });
5904
+ if (envRead.kind === "failed") return [displayScopePath(envPath, ctx)];
5905
+ const after = renderEnvFile({ current: envRead.text, variables, shell });
5906
+ return after === envRead.text ? [] : [displayScopePath(envPath, ctx)];
5907
+ }
5908
+ function collectEnvVariables2(contributions) {
5909
+ return Object.fromEntries(
5910
+ contributions.flatMap(
5911
+ (contribution) => contribution.kind === "env" ? Object.entries(contribution.variables) : []
5912
+ )
5913
+ );
5914
+ }
5915
+
5916
+ // src/commands/status/status-result.ts
5917
+ var WORST_FIRST = ["unreadable", "drifted", "configured", "not configured", "not installed"];
5918
+ function worstScopeState(states) {
5919
+ return WORST_FIRST.find((state) => states.includes(state)) ?? "not installed";
5920
+ }
5921
+ function exitCodeForStatus(report2, namedIds) {
5922
+ if (report2.errors.length > 0) return EXIT_CODE.failed;
5923
+ if (report2.key.isValid === false) return EXIT_CODE.failed;
5924
+ if (report2.harnesses.some(isUnhealthy)) return EXIT_CODE.failed;
5925
+ const isNamedAndUnsetUp = (harness) => namedIds.includes(harness.id) && (harness.status === "not configured" || harness.status === "not installed");
5926
+ return report2.harnesses.some(isNamedAndUnsetUp) ? EXIT_CODE.failed : EXIT_CODE.ok;
5927
+ }
5928
+ function isUnhealthy(harness) {
5929
+ return harness.status === "drifted" || harness.status === "unreadable";
5930
+ }
5931
+
5932
+ // src/commands/status/inspect-harness.ts
5933
+ function inspectHarness(params) {
5934
+ const { harness } = params;
5935
+ if (harness.kind === "unsupported") {
5936
+ return {
5937
+ id: harness.id,
5938
+ label: harness.label,
5939
+ isSupported: false,
5940
+ isDetected: isDetected(harness, params.baseContext),
5941
+ isUndoable: params.undoableIds.has(harness.id),
5942
+ status: "unsupported",
5943
+ reason: harness.reason,
5944
+ scopes: null,
5945
+ warnings: []
5946
+ };
5947
+ }
5948
+ const user = inspectScope({ ...params, harness, ctx: { ...params.baseContext, scope: "user" } });
5949
+ const project = inspectProjectScope({ ...params, harness, ctx: { ...params.baseContext, scope: "project" } });
5950
+ return {
5951
+ id: harness.id,
5952
+ label: harness.label,
5953
+ isSupported: true,
5954
+ isDetected: user.isDetected || project.isDetected,
5955
+ isUndoable: params.undoableIds.has(harness.id),
5956
+ status: worstScopeState([user.scope.state, project.scope.state]),
5957
+ reason: null,
5958
+ scopes: { user: user.scope, project: project.scope },
5959
+ warnings: listWarnings({ harness, masker: params.masker }, user.ctx, project.ctx)
5960
+ };
5961
+ }
5962
+ function isDetected(harness, ctx) {
5963
+ try {
5964
+ return harness.detect({ ...ctx, scope: "user" }).isFound;
5965
+ } catch {
5966
+ return false;
5967
+ }
5968
+ }
5969
+ function inspectScope(params) {
5970
+ const { harness, ctx } = params;
5971
+ const detection = detectAt(harness, ctx);
5972
+ const { inspection, error } = inspectionFor(params);
5973
+ const key = readConfigKey({ harness, ctx, envPath: params.envPath, shell: params.shell });
5974
+ const scopeCase = match28({
5975
+ detectionKind: detection.kind,
5976
+ isUnreadable: inspection.isUnreadable,
5977
+ isConfigured: inspection.isConfigured
5978
+ }).with({ detectionKind: "failed" }, () => unreadableScope(detection.detail)).with({ isUnreadable: true }, () => unreadableScope(error)).with({ isConfigured: true }, () => {
5979
+ try {
5980
+ const drift = detectDrift({
5981
+ harness,
5982
+ ctx,
5983
+ envPath: params.envPath,
5984
+ shell: params.shell,
5985
+ model: params.defaults[harness.id] ?? inspection.model ?? DEFAULT_MODEL
5986
+ });
5987
+ return {
5988
+ state: drift.isDrifted ? "drifted" : "configured",
5989
+ changedPaths: drift.changedPaths,
5990
+ detail: null
5991
+ };
5992
+ } catch (error2) {
5993
+ return unreadableScope(describeFileError({ error: error2, action: "read", home: ctx.home }));
5994
+ }
5995
+ }).with({ detectionKind: "not-found" }, () => notInstalledScope(detection.detail)).otherwise(() => ({ state: "not configured", changedPaths: [], detail: null }));
5996
+ return {
5997
+ scope: {
5998
+ ...scopeCase,
5999
+ path: displayScopePath(harness.configPath?.(ctx) ?? params.envPath, ctx),
6000
+ model: inspection.model ?? null,
6001
+ keyMasked: key === void 0 ? null : params.masker.maskText(key),
6002
+ isDrifted: scopeCase.state === "drifted"
6003
+ },
6004
+ isDetected: detection.kind === "found",
6005
+ ctx
6006
+ };
6007
+ }
6008
+ function inspectProjectScope(params) {
6009
+ if (params.harness.projectSupport.kind === "unsupported") {
6010
+ return {
6011
+ scope: {
6012
+ state: "not installed",
6013
+ path: null,
6014
+ model: null,
6015
+ keyMasked: null,
6016
+ isDrifted: false,
6017
+ changedPaths: [],
6018
+ detail: params.harness.projectSupport.reason
6019
+ },
6020
+ isDetected: false,
6021
+ ctx: params.ctx
6022
+ };
6023
+ }
6024
+ return inspectScope(params);
6025
+ }
6026
+ function detectAt(harness, ctx) {
6027
+ try {
6028
+ const result = harness.detect(ctx);
6029
+ return { kind: result.isFound ? "found" : "not-found", detail: result.detail };
6030
+ } catch (error) {
6031
+ return { kind: "failed", detail: describeFileError({ error, action: "read", home: ctx.home }) };
6032
+ }
6033
+ }
6034
+ function inspectionFor(params) {
6035
+ try {
6036
+ return { inspection: params.harness.inspect?.(params.ctx) ?? fallbackInspection(params), error: null };
6037
+ } catch (error) {
6038
+ return {
6039
+ inspection: { isConfigured: false, model: void 0, isUnreadable: true },
6040
+ error: describeFileError({ error, action: "read", home: params.ctx.home })
6041
+ };
6042
+ }
6043
+ }
6044
+ function fallbackInspection(params) {
6045
+ const key = readConfigKey({
6046
+ harness: params.harness,
6047
+ ctx: params.ctx,
6048
+ envPath: params.envPath,
6049
+ shell: params.shell
6050
+ });
6051
+ return { isConfigured: key !== void 0, model: void 0, isUnreadable: false };
6052
+ }
6053
+ function unreadableScope(detail) {
6054
+ return { state: "unreadable", changedPaths: [], detail };
6055
+ }
6056
+ function notInstalledScope(detail) {
6057
+ return { state: "not installed", changedPaths: [], detail };
6058
+ }
6059
+ function listWarnings({ harness, masker: masker2 }, ...contexts) {
6060
+ const warnings = contexts.flatMap((ctx) => readWarnings(harness, ctx));
6061
+ return [...new Set(warnings.map((warning) => masker2.maskText(warning)))];
6062
+ }
6063
+ function readWarnings(harness, ctx) {
6064
+ try {
6065
+ return harness.warnings?.(ctx) ?? [];
6066
+ } catch {
6067
+ return [];
6068
+ }
6069
+ }
6070
+
6071
+ // src/commands/status/read-global-status.ts
6072
+ import path23 from "path";
6073
+ async function readGlobalStatus(params) {
6074
+ const { ctx, masker: masker2 } = params;
6075
+ const shell = resolveShell(ctx.platform);
6076
+ const envPath = resolveEnvFilePath(ctx.home, shell);
6077
+ const envKey = parseEnvironmentKey(readEnvironmentInputs(ctx.env).key);
6078
+ const existing = findExistingKeys({ ctx, envPath, shell });
6079
+ masker2.registerSecrets(existing.all);
6080
+ const errors = [];
6081
+ const saved = await loadSavedKey(params.keystore, errors);
6082
+ const resolvedKey = envKey ?? saved ?? existing.offered;
6083
+ if (resolvedKey !== void 0) masker2.registerSecrets([resolvedKey]);
6084
+ const defaults = readDefaults(ctx.home, errors);
6085
+ const undoableIds = readUndoableIds(ctx, errors);
6086
+ return {
6087
+ key: {
6088
+ isSaved: saved !== void 0,
6089
+ masked: resolvedKey === void 0 ? null : masker2.maskText(resolvedKey),
6090
+ isValid: null
6091
+ },
6092
+ resolvedKey,
6093
+ envFile: { path: displayPath(envPath, ctx.home), isPresent: isFilePresent(envPath) },
6094
+ backups: {
6095
+ user: `${displayPath(path23.join(ctx.home, USER_BACKUP_DIRECTORY), ctx.home)}${path23.sep}`,
6096
+ project: `${displayRelativePath(path23.join(ctx.cwd, PROJECT_BACKUP_DIRECTORY), ctx.cwd)}${path23.sep}`
6097
+ },
6098
+ defaults,
6099
+ undoableIds,
6100
+ errors
6101
+ };
6102
+ }
6103
+ async function loadSavedKey(keystore2, errors) {
6104
+ const availability = await keystore2.availability();
6105
+ if (availability.kind !== "available") return void 0;
6106
+ try {
6107
+ return readNonBlank(await keystore2.load());
6108
+ } catch (error) {
6109
+ errors.push(
6110
+ `Could not read the saved key from your system keychain: ${describeError(error)}. Run \`npx ${COMMAND_NAME} ${FORGET_KEY_COMMAND}\` if this keeps happening.`
6111
+ );
6112
+ return void 0;
6113
+ }
6114
+ }
6115
+ function isFilePresent(filePath) {
6116
+ try {
6117
+ return readFileIfExists(filePath) !== null;
6118
+ } catch {
6119
+ return false;
6120
+ }
6121
+ }
6122
+ function readDefaults(home, errors) {
6123
+ try {
6124
+ return readDefaultModels(home);
6125
+ } catch (error) {
6126
+ if (!(error instanceof PreferencesError)) throw error;
6127
+ errors.push(error.message);
6128
+ return {};
6129
+ }
6130
+ }
6131
+ function readUndoableIds(ctx, errors) {
6132
+ const ids = /* @__PURE__ */ new Set();
6133
+ for (const scope of CONFIG_SCOPES) {
6134
+ const record9 = readRecord2({ ...ctx, scope }, errors);
6135
+ for (const id of Object.keys(record9?.harnesses ?? {})) ids.add(id);
6136
+ }
6137
+ return ids;
6138
+ }
6139
+ function readRecord2(ctx, errors) {
6140
+ try {
6141
+ const { filePath } = resolveRestoreRecordLocation(ctx);
6142
+ return readRestoreRecord({ filePath, home: ctx.home });
6143
+ } catch (error) {
6144
+ if (!(error instanceof RestoreRecordError)) throw error;
6145
+ errors.push(error.message);
6146
+ return null;
6147
+ }
6148
+ }
6149
+
6150
+ // src/commands/status/run-status.ts
6151
+ async function runStatus(params) {
6152
+ const { options, baseContext, masker: masker2, ui } = params;
6153
+ const named = resolveNamedHarnesses(options.harnessIds);
6154
+ const shell = resolveShell(baseContext.platform);
6155
+ const envPath = resolveEnvFilePath(baseContext.home, shell);
6156
+ const global = await readGlobalStatus({ ctx: baseContext, keystore: params.keystore, masker: masker2 });
6157
+ const report2 = {
6158
+ version: 1,
6159
+ key: await resolveKeyStatus({ global, options, baseContext, masker: masker2, fetch: params.fetch }),
6160
+ envFile: global.envFile,
6161
+ backups: global.backups,
6162
+ defaults: global.defaults,
6163
+ harnesses: (named.length > 0 ? named : [...harnesses]).map(
6164
+ (harness) => inspectFor({ harness, params, global, envPath, shell })
6165
+ ),
6166
+ errors: global.errors
6167
+ };
6168
+ if (options.isJson) {
6169
+ ui.print([formatStatusJson(report2, masker2)]);
6170
+ for (const error of global.errors) ui.writeError(masker2.maskText(error));
6171
+ } else {
6172
+ ui.print(formatStatus({ report: report2, masker: masker2 }));
6173
+ }
6174
+ return exitCodeForStatus(report2, options.harnessIds);
6175
+ }
6176
+ function inspectFor({ harness, params, global, envPath, shell }) {
6177
+ return inspectHarness({
6178
+ harness,
6179
+ baseContext: params.baseContext,
6180
+ envPath,
6181
+ shell,
6182
+ defaults: global.defaults,
6183
+ undoableIds: global.undoableIds,
6184
+ masker: params.masker
6185
+ });
6186
+ }
6187
+ async function resolveKeyStatus({
6188
+ global,
6189
+ options,
6190
+ baseContext,
6191
+ masker: masker2,
6192
+ fetch: fetch2
6193
+ }) {
6194
+ if (!options.shouldVerifyStatus || global.resolvedKey === void 0) return global.key;
6195
+ const gatewayUrls = buildGatewayUrls(resolveGatewayHost(baseContext.env));
6196
+ const { probe } = await listModels({
6197
+ openaiBaseUrl: gatewayUrls.openaiBaseUrl,
6198
+ apiKey: global.resolvedKey,
6199
+ masker: masker2,
6200
+ fetch: fetch2
6201
+ });
6202
+ return { ...global.key, isValid: isKeyAccepted(probe) };
6203
+ }
6204
+
6205
+ // src/harnesses/launch/run-launch.ts
4628
6206
  import fs10 from "fs";
4629
- import path20 from "path";
6207
+ import path24 from "path";
4630
6208
  var LAUNCH_DIRECTORY_PREFIX = "nexos-connect-";
4631
6209
  var STALE_DIRECTORY_PATTERN = new RegExp(`^${LAUNCH_DIRECTORY_PREFIX}(\\d+)-`);
4632
6210
  var OWNER_ONLY_MODE2 = 384;
@@ -4639,11 +6217,11 @@ function sweepStaleDirectories(deps) {
4639
6217
  return;
4640
6218
  }
4641
6219
  for (const name of names) {
4642
- const match25 = STALE_DIRECTORY_PATTERN.exec(name);
4643
- if (match25 === null) continue;
4644
- const directory = path20.join(root, name);
6220
+ const match29 = STALE_DIRECTORY_PATTERN.exec(name);
6221
+ if (match29 === null) continue;
6222
+ const directory = path24.join(root, name);
4645
6223
  try {
4646
- if (deps.isAlive(Number(match25[1]))) continue;
6224
+ if (deps.isAlive(Number(match29[1]))) continue;
4647
6225
  if (deps.uid !== void 0 && fs10.statSync(directory).uid !== deps.uid) continue;
4648
6226
  fs10.rmSync(directory, { recursive: true, force: true });
4649
6227
  } catch {
@@ -4660,15 +6238,11 @@ async function runLaunch(recipe, passthrough, deps) {
4660
6238
  };
4661
6239
  deps.signals.on("exit", cleanup);
4662
6240
  let commandArguments = recipe.arguments;
6241
+ let byPlaceholder = /* @__PURE__ */ new Map();
4663
6242
  if (recipe.files !== void 0 && recipe.files.length > 0) {
4664
6243
  try {
4665
- directory = fs10.mkdtempSync(path20.join(deps.tmpdir(), `${LAUNCH_DIRECTORY_PREFIX}${deps.pid}-`));
4666
- const byPlaceholder = /* @__PURE__ */ new Map();
4667
- for (const file of recipe.files) {
4668
- const target = path20.join(directory, file.name);
4669
- fs10.writeFileSync(target, file.content, { mode: OWNER_ONLY_MODE2, flag: "wx" });
4670
- byPlaceholder.set(file.placeholder, target);
4671
- }
6244
+ directory = fs10.mkdtempSync(path24.join(deps.tmpdir(), `${LAUNCH_DIRECTORY_PREFIX}${deps.pid}-`));
6245
+ byPlaceholder = writeLaunchFiles({ files: recipe.files, directory });
4672
6246
  commandArguments = commandArguments.map((argument) => byPlaceholder.get(argument) ?? argument);
4673
6247
  } catch (error) {
4674
6248
  cleanup();
@@ -4682,7 +6256,7 @@ async function runLaunch(recipe, passthrough, deps) {
4682
6256
  const env = { ...deps.env };
4683
6257
  for (const [name, value] of Object.entries(recipe.env)) {
4684
6258
  if (value === void 0) delete env[name];
4685
- else env[name] = value;
6259
+ else env[name] = byPlaceholder.get(value) ?? value;
4686
6260
  }
4687
6261
  if (deps.stdin.isTTY) deps.stdin.setRawMode(false);
4688
6262
  deps.stdin.pause();
@@ -4726,6 +6300,15 @@ async function runLaunch(recipe, passthrough, deps) {
4726
6300
  child.once("exit", (code, signal) => finish2(signal !== null ? { signal } : { code: code ?? 1 }));
4727
6301
  });
4728
6302
  }
6303
+ function writeLaunchFiles({ files, directory }) {
6304
+ const byPlaceholder = /* @__PURE__ */ new Map([[LAUNCH_DIRECTORY_PLACEHOLDER, directory]]);
6305
+ for (const file of files) {
6306
+ const target = path24.join(directory, file.name);
6307
+ fs10.writeFileSync(target, file.content, { mode: OWNER_ONLY_MODE2, flag: "wx" });
6308
+ if (file.placeholder !== void 0) byPlaceholder.set(file.placeholder, target);
6309
+ }
6310
+ return byPlaceholder;
6311
+ }
4729
6312
  function joinArguments({ recipeArguments, subcommands, passthrough }) {
4730
6313
  const [first, ...rest] = passthrough;
4731
6314
  if (first !== void 0 && subcommands.includes(first)) return [first, ...recipeArguments, ...rest];
@@ -4819,9 +6402,18 @@ function createKeystore(deps) {
4819
6402
  };
4820
6403
  }
4821
6404
 
6405
+ // src/product/storage/windows-environment/powershell-user-environment.ts
6406
+ import { spawnSync } from "child_process";
6407
+ var POWERSHELL_EXECUTABLE = "powershell.exe";
6408
+ var POWERSHELL_ARGUMENTS = ["-NoProfile", "-NonInteractive", "-Command"];
6409
+ function runPowerShell({ script, input }) {
6410
+ const result = spawnSync(POWERSHELL_EXECUTABLE, [...POWERSHELL_ARGUMENTS, script], { input, encoding: "utf8" });
6411
+ return { code: result.status ?? 1, stdout: result.stdout };
6412
+ }
6413
+
4822
6414
  // src/ui/output.ts
4823
6415
  import * as clack2 from "@clack/prompts";
4824
- import pc10 from "picocolors";
6416
+ import pc12 from "picocolors";
4825
6417
  function intro2(message, io) {
4826
6418
  clack2.intro(message, { output: io.output });
4827
6419
  }
@@ -4843,8 +6435,13 @@ function printLines(lines, io) {
4843
6435
  io.output.write(`${lines.join("\n")}
4844
6436
  `);
4845
6437
  }
6438
+ function writeError(message, io) {
6439
+ const stream = io.errorOutput ?? io.output;
6440
+ stream.write(`${message}
6441
+ `);
6442
+ }
4846
6443
  function dim(message, io) {
4847
- clack2.log.message(pc10.dim(message), { output: io.output });
6444
+ clack2.log.message(pc12.dim(message), { output: io.output });
4848
6445
  }
4849
6446
  function note2(message, title, io) {
4850
6447
  clack2.note(message, title, { output: io.output });
@@ -4941,7 +6538,7 @@ async function chooseSavedKey(maskedKey, io) {
4941
6538
  // src/ui/prompts/model/ask-for-model-id.ts
4942
6539
  import * as clack9 from "@clack/prompts";
4943
6540
 
4944
- // src/ui/back-keys.ts
6541
+ // src/ui/terminal/back-keys.ts
4945
6542
  import * as clack8 from "@clack/prompts";
4946
6543
  var ESCAPE_KEY = "escape";
4947
6544
  var EDITING_BACK_KEYS = ["left", "backspace", "delete"];
@@ -5115,8 +6712,9 @@ async function selectModel(choices, io) {
5115
6712
  }
5116
6713
  }
5117
6714
  function formatHiddenModelsNote(hiddenCount) {
5118
- const subject = hiddenCount === 1 ? "it does" : "they do";
5119
- return `${formatCount(hiddenCount, "model")} hidden \u2014 ${subject} not serve the API your selected agents use.`;
6715
+ const subject = hiddenCount === 1 ? "it is" : "they are";
6716
+ const object6 = hiddenCount === 1 ? "a chat model" : "chat models";
6717
+ return `${formatCount(hiddenCount, "model")} hidden \u2014 ${subject} not ${object6}.`;
5120
6718
  }
5121
6719
 
5122
6720
  // src/ui/prompts/select-harnesses.ts
@@ -5187,9 +6785,9 @@ async function selectScope({ userPaths, projectPaths }, io) {
5187
6785
  );
5188
6786
  }
5189
6787
 
5190
- // src/ui/spinner.ts
6788
+ // src/ui/terminal/spinner.ts
5191
6789
  import * as clack18 from "@clack/prompts";
5192
- import pc11 from "picocolors";
6790
+ import pc13 from "picocolors";
5193
6791
  function createSpinner(io) {
5194
6792
  const taskController = new AbortController();
5195
6793
  const cancelled = new Promise((_resolve, reject) => {
@@ -5222,7 +6820,7 @@ function createSpinner(io) {
5222
6820
  stop: (message) => {
5223
6821
  if (stopSpinning === null) return;
5224
6822
  end();
5225
- io.output.write(`${pc11.green(clack18.S_STEP_SUBMIT)} ${message}
6823
+ io.output.write(`${pc13.green(clack18.S_STEP_SUBMIT)} ${message}
5226
6824
  `);
5227
6825
  },
5228
6826
  waitFor,
@@ -5237,7 +6835,7 @@ function createSpinner(io) {
5237
6835
  };
5238
6836
  }
5239
6837
  function spin({ io, message, onCancel }) {
5240
- io.output.write(`${pc11.gray(clack18.S_BAR)}
6838
+ io.output.write(`${pc13.gray(clack18.S_BAR)}
5241
6839
  `);
5242
6840
  const stopAnimating = isTerminalOutput(io.output) ? animate(io.output, message) : () => {
5243
6841
  };
@@ -5257,7 +6855,7 @@ function animate(output, message) {
5257
6855
  const text2 = fitToWidth(message, output.columns);
5258
6856
  let frameIndex = 0;
5259
6857
  const drawFrame = () => {
5260
- output.write(`${CLEAR_LINE}${pc11.magenta(FRAMES[frameIndex % FRAMES.length] ?? "")} ${text2}`);
6858
+ output.write(`${CLEAR_LINE}${pc13.magenta(FRAMES[frameIndex % FRAMES.length] ?? "")} ${text2}`);
5261
6859
  frameIndex += 1;
5262
6860
  };
5263
6861
  output.write(HIDE_CURSOR);
@@ -5309,6 +6907,7 @@ function createTerminalUi(io) {
5309
6907
  cancel: (message) => cancel2(message, io),
5310
6908
  log: createLog(io),
5311
6909
  print: (lines) => printLines(lines, io),
6910
+ writeError: (message) => writeError(message, io),
5312
6911
  dim: (message) => dim(message, io),
5313
6912
  note: (message, title) => note2(message, title, io),
5314
6913
  spinner: () => createSpinner(io),
@@ -5336,11 +6935,12 @@ var keystore = createKeystore({
5336
6935
  importKeyring: () => import("@napi-rs/keyring"),
5337
6936
  platform: process.platform,
5338
6937
  isWsl: () => process.env.WSL_DISTRO_NAME !== void 0 || /microsoft/i.test(os.release()),
5339
- hasMacKeychain: () => spawnSync("security", ["default-keychain"], { stdio: "ignore", timeout: KEYCHAIN_PROBE_TIMEOUT_MS }).status === 0,
6938
+ hasMacKeychain: () => spawnSync2("security", ["default-keychain"], { stdio: "ignore", timeout: KEYCHAIN_PROBE_TIMEOUT_MS }).status === 0,
5340
6939
  isEnabled: process.env[KEYCHAIN_ENV_VARIABLE] !== KEYCHAIN_DISABLED,
5341
6940
  probeTimeoutMs: KEYCHAIN_PROBE_TIMEOUT_MS,
5342
6941
  interactionTimeoutMs: KEYCHAIN_INTERACTION_TIMEOUT_MS
5343
6942
  });
6943
+ var userEnvironment = createUserEnvironment({ platform: process.platform, runPowerShell });
5344
6944
  var runnerDeps = {
5345
6945
  spawn: (command, commandArguments, options) => crossSpawn(command, commandArguments, options),
5346
6946
  tmpdir: os.tmpdir,
@@ -5360,13 +6960,25 @@ function isProcessAlive(pid) {
5360
6960
  return error.code === "EPERM";
5361
6961
  }
5362
6962
  }
6963
+ function runNpm(npmArguments) {
6964
+ return new Promise((resolve) => {
6965
+ const child = crossSpawn("npm", [...npmArguments], { stdio: "inherit" });
6966
+ child.once("error", () => resolve({ kind: "could-not-start" }));
6967
+ child.once("exit", (code) => resolve({ kind: "exited", code: code ?? 1 }));
6968
+ });
6969
+ }
5363
6970
  async function main() {
5364
- const ui = createTerminalUi({ input: process.stdin, output: process.stdout, onInterrupt: listenForSigint });
6971
+ const ui = createTerminalUi({
6972
+ input: process.stdin,
6973
+ output: process.stdout,
6974
+ errorOutput: process.stderr,
6975
+ onInterrupt: listenForSigint
6976
+ });
5365
6977
  try {
5366
6978
  return await runCommand(ui);
5367
6979
  } catch (error) {
5368
6980
  const failure = classifyFailure(error, masker);
5369
- if (failure.kind === "usage") console.error(pc12.red(failure.message));
6981
+ if (failure.kind === "usage") console.error(pc14.red(failure.message));
5370
6982
  if (failure.kind === "cancelled") ui.cancel("Cancelled.");
5371
6983
  if (failure.kind === "unexpected") ui.log.error(failure.message);
5372
6984
  return exitCodeForFailure(failure);
@@ -5382,8 +6994,8 @@ var HELP = renderHelp({
5382
6994
  predefinedModelIds: PREDEFINED_MODELS.map((model) => model.id),
5383
6995
  defaultModel: DEFAULT_MODEL,
5384
6996
  backupDirectories: {
5385
- user: path21.join("~", USER_BACKUP_DIRECTORY),
5386
- project: `.${path21.sep}${PROJECT_BACKUP_DIRECTORY}`
6997
+ user: path25.join("~", USER_BACKUP_DIRECTORY),
6998
+ project: `.${path25.sep}${PROJECT_BACKUP_DIRECTORY}`
5387
6999
  }
5388
7000
  });
5389
7001
  function isLaunchingHarness(harness) {
@@ -5397,7 +7009,7 @@ function describeUnknownAgent(binary) {
5397
7009
  if (isAgentName(binary)) return `Unknown agent "${binary}". ${oneOf}`;
5398
7010
  return `Unknown agent. ${oneOf}. To pass a key, use --key or set $${KEY_ENV_VARIABLE}.`;
5399
7011
  }
5400
- var PackageSchema = v12.object({ version: v12.string() });
7012
+ var PackageSchema = v16.object({ version: v16.string() });
5401
7013
  async function runCommand(ui) {
5402
7014
  const options = parseCliArguments(process.argv.slice(NODE_AND_SCRIPT_ARGUMENT_COUNT));
5403
7015
  if (options.shouldShowHelp) {
@@ -5405,20 +7017,24 @@ async function runCommand(ui) {
5405
7017
  return EXIT_CODE.ok;
5406
7018
  }
5407
7019
  if (options.shouldShowVersion) {
5408
- console.log(v12.parse(PackageSchema, createRequire(import.meta.url)("../package.json")).version);
7020
+ console.log(v16.parse(PackageSchema, createRequire(import.meta.url)("../package.json")).version);
5409
7021
  return EXIT_CODE.ok;
5410
7022
  }
5411
7023
  if (options.command.kind === "forget-key") return await forgetKey({ keystore, ui, masker });
5412
7024
  if (options.command.kind === "launch") return await runLaunchCommand({ command: options.command, options, ui });
7025
+ if (options.command.kind === "status") {
7026
+ return await runStatus({ options, baseContext: createContext(), ui, keystore, masker, fetch });
7027
+ }
5413
7028
  const commandParams = {
5414
7029
  options,
5415
7030
  baseContext: createContext(),
5416
7031
  ui,
5417
7032
  now: () => /* @__PURE__ */ new Date(),
5418
- masker
7033
+ masker,
7034
+ userEnvironment
5419
7035
  };
5420
7036
  if (options.command.kind === "restore") return await runRestore(commandParams);
5421
- return await runSetup({ ...commandParams, fetch, keystore });
7037
+ return await runSetup({ ...commandParams, fetch, keystore, runNpm });
5422
7038
  }
5423
7039
  function createContext() {
5424
7040
  return createPlatformContext({
@@ -5433,14 +7049,14 @@ async function runLaunchCommand({ command, options, ui }) {
5433
7049
  const { binary, passthrough } = command;
5434
7050
  const harness = findHarnessByBinary(binary);
5435
7051
  if (harness === void 0) {
5436
- console.error(pc12.red(masker.maskText(describeUnknownAgent(binary))));
7052
+ console.error(pc14.red(masker.maskText(describeUnknownAgent(binary))));
5437
7053
  console.error(`
5438
7054
  ${HELP}`);
5439
7055
  return EXIT_CODE.usage;
5440
7056
  }
5441
7057
  if (!isLaunchingHarness(harness)) {
5442
7058
  const reason = harness.kind === "unsupported" ? `not configurable \u2014 ${harness.reason}` : "cannot be started this way";
5443
- console.error(pc12.red(`${harness.label}: ${reason}`));
7059
+ console.error(pc14.red(`${harness.label}: ${reason}`));
5444
7060
  return EXIT_CODE.usage;
5445
7061
  }
5446
7062
  const ctx = createContext();
@@ -5453,7 +7069,8 @@ ${HELP}`);
5453
7069
  masker,
5454
7070
  ctx,
5455
7071
  fetch,
5456
- runLaunch: (recipe, recipePassthrough) => runLaunch(recipe, recipePassthrough, runnerDeps)
7072
+ runLaunch: (recipe, recipePassthrough) => runLaunch(recipe, recipePassthrough, runnerDeps),
7073
+ runNpm
5457
7074
  }
5458
7075
  );
5459
7076
  if ("signal" in result) {
@@ -5467,7 +7084,7 @@ function finish(exitCode) {
5467
7084
  process.exitCode = exitCode;
5468
7085
  }
5469
7086
  void main().then(finish).catch((error) => {
5470
- console.error(pc12.red(masker.maskText(describeError(error))));
7087
+ console.error(pc14.red(masker.maskText(describeError(error))));
5471
7088
  finish(EXIT_CODE.failed);
5472
7089
  });
5473
7090
  //# sourceMappingURL=index.js.map