nexos-connect 0.5.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 path24 from "path";
7
+ import path25 from "path";
8
8
  import crossSpawn from "cross-spawn";
9
- import pc12 from "picocolors";
10
- import * as v15 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((match27) => toSpan(match27, masksByForm.get(match27[0]) ?? match27[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((match27) => toSpan(match27, maskKey(match27[0])));
418
+ return [...text2.matchAll(KEY_SHAPE_PATTERN)].map((match29) => toSpan(match29, maskKey(match29[0])));
400
419
  }
401
- function toSpan(match27, mask) {
402
- return { start: match27.index, end: match27.index + match27[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,6 +770,48 @@ 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
816
  import * as v9 from "valibot";
720
817
 
@@ -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 path25 = [...tablePath, ...keyValue.key.keys.map(readKeyName)];
1392
- const entry = { path: path25, 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, path25)];
1529
+ return [entry, ...listInBody(keyValue.value.body, path26)];
1395
1530
  });
1396
1531
  }
1397
- function isSamePath(path25, other) {
1398
- return path25.length === other.length && path25.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",
@@ -1756,22 +1917,38 @@ function isConfigSubstitution(value) {
1756
1917
  return value.startsWith(COMMAND_PREFIX) || value.includes(VARIABLE_MARKER);
1757
1918
  }
1758
1919
 
1920
+ // src/harnesses/launch/launch-recipe.ts
1921
+ var LAUNCH_DIRECTORY_PLACEHOLDER = "{launchDirectory}";
1922
+
1759
1923
  // src/harnesses/crush/crush.ts
1760
1924
  var BINARY4 = "crush";
1761
1925
  var PROVIDER_ID2 = "nexos";
1762
1926
  var PROVIDER_NAME2 = "nexos.ai";
1763
1927
  var PROVIDER_TYPE2 = "openai-compat";
1764
1928
  var CONFIG_SCHEMA_URL = "https://charm.land/crush.json";
1929
+ var GLOBAL_CONFIG_VARIABLE = "CRUSH_GLOBAL_CONFIG";
1765
1930
  var crush = {
1766
1931
  kind: "configurable",
1767
1932
  id: "crush",
1768
1933
  label: "Crush",
1769
1934
  binary: BINARY4,
1935
+ installPackage: "@charmland/crush",
1770
1936
  protocols: ["openai-chat"],
1771
1937
  projectSupport: { kind: "supported" },
1772
1938
  detect: (ctx) => detectCrush(ctx),
1773
1939
  configPath: (ctx) => resolveConfigPath2(ctx),
1774
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
+ ],
1775
1952
  readKey: (ctx) => readProviderKey(ctx),
1776
1953
  readSecrets: (ctx) => readProviderSecrets(ctx),
1777
1954
  postSetupNotes: [
@@ -1819,9 +1996,23 @@ function listUpdates(desired) {
1819
1996
  }
1820
1997
  var ConfigSchema = v5.looseObject({
1821
1998
  providers: v5.optional(
1822
- v5.looseObject({ [PROVIDER_ID2]: v5.optional(v5.looseObject({ api_key: v5.optional(v5.string()) })) })
1823
- )
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()) })) }))
1824
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
+ }
1825
2016
  function readProviderKey(ctx) {
1826
2017
  const config = readJsoncFile(resolveConfigPath2(ctx), ConfigSchema);
1827
2018
  const apiKey = config.kind === "valid" ? config.value.providers?.[PROVIDER_ID2]?.api_key : void 0;
@@ -1874,11 +2065,13 @@ var opencode = {
1874
2065
  id: "opencode",
1875
2066
  label: "OpenCode",
1876
2067
  binary: BINARY5,
2068
+ installPackage: "opencode-ai",
1877
2069
  protocols: ["openai-chat"],
1878
2070
  projectSupport: { kind: "supported" },
1879
- detect: (ctx) => detectOpenCode(ctx),
2071
+ detect: (ctx) => detectConfigOrBinary({ candidates: listConfigCandidates(ctx), binary: BINARY5, ctx }),
1880
2072
  configPath: (ctx) => resolveConfigPath3(ctx),
1881
2073
  plan: (desired, ctx) => [planConfig3(desired, ctx)],
2074
+ inspect: (ctx) => inspectConfig3(ctx),
1882
2075
  launch: (desired) => {
1883
2076
  const config = renderJsonc(null, [
1884
2077
  ...providerUpdates({ providerId: RUN_PROVIDER_ID2, apiKey: `{env:${RUN_KEY_VARIABLE}}`, desired }),
@@ -1904,11 +2097,6 @@ var opencode = {
1904
2097
  "OpenCode keeps its own credentials in its auth.json; this key goes in the provider block instead, so no /connect step is needed."
1905
2098
  ]
1906
2099
  };
1907
- function detectOpenCode(ctx) {
1908
- const detected = detectConfigFile(listConfigCandidates(ctx), ctx.home);
1909
- if (detected.isFound || ctx.scope === "project" || !ctx.hasBinary(BINARY5)) return detected;
1910
- return { isFound: true, detail: `${BINARY5} found on PATH` };
1911
- }
1912
2100
  var USER_CONFIG_DIRECTORY = path7.join(".config", "opencode");
1913
2101
  var JSON_CONFIG_FILE = "opencode.json";
1914
2102
  var JSONC_CONFIG_FILE = "opencode.jsonc";
@@ -1989,14 +2177,33 @@ function readProviderSecrets2(ctx) {
1989
2177
  }
1990
2178
  var ConfigSchema2 = v6.looseObject({
1991
2179
  $schema: v6.optional(v6.unknown()),
2180
+ model: v6.optional(v6.string()),
1992
2181
  provider: v6.optional(
1993
2182
  v6.looseObject({
1994
2183
  [PROVIDER_ID3]: v6.optional(
1995
- v6.looseObject({ options: v6.optional(v6.looseObject({ apiKey: v6.optional(v6.string()) })) })
2184
+ v6.looseObject({
2185
+ options: v6.optional(v6.looseObject({ apiKey: v6.optional(v6.string()), baseURL: v6.optional(v6.string()) }))
2186
+ })
1996
2187
  )
1997
2188
  })
1998
2189
  )
1999
2190
  });
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
+ };
2201
+ }
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);
2206
+ }
2000
2207
 
2001
2208
  // src/harnesses/pi/pi.ts
2002
2209
  import path8 from "path";
@@ -2005,11 +2212,14 @@ var BINARY6 = "pi";
2005
2212
  var PROVIDER_ID4 = "nexos";
2006
2213
  var PROVIDER_NAME4 = "nexos.ai";
2007
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";
2008
2217
  var pi = {
2009
2218
  kind: "configurable",
2010
2219
  id: "pi",
2011
2220
  label: "Pi",
2012
2221
  binary: BINARY6,
2222
+ installPackage: "@earendil-works/pi-coding-agent",
2013
2223
  protocols: ["openai-chat"],
2014
2224
  projectSupport: {
2015
2225
  kind: "unsupported",
@@ -2018,6 +2228,20 @@ var pi = {
2018
2228
  detect: (ctx) => detectBinary2(ctx),
2019
2229
  configPath: (ctx) => resolveModelsPath(ctx),
2020
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
+ ],
2021
2245
  readKey: (ctx) => readProviderKey3(ctx),
2022
2246
  readSecrets: (ctx) => readProviderSecrets3(ctx),
2023
2247
  postSetupNotes: [
@@ -2029,6 +2253,7 @@ function detectBinary2(ctx) {
2029
2253
  return { isFound, detail: isFound ? `${BINARY6} found on PATH` : `${BINARY6} not found on PATH` };
2030
2254
  }
2031
2255
  var DEFAULT_AGENT_DIRECTORY = path8.join(".pi", "agent");
2256
+ var SESSION_DIRECTORY = path8.join("~", DEFAULT_AGENT_DIRECTORY, "sessions");
2032
2257
  var MODELS_FILE = "models.json";
2033
2258
  var SETTINGS_FILE = "settings.json";
2034
2259
  var HOME_PREFIX = "~";
@@ -2088,9 +2313,25 @@ function listModelUpdates(desired) {
2088
2313
  }
2089
2314
  var ModelsSchema = v7.looseObject({
2090
2315
  providers: v7.optional(
2091
- v7.looseObject({ [PROVIDER_ID4]: v7.optional(v7.looseObject({ apiKey: v7.optional(v7.string()) })) })
2316
+ v7.looseObject({
2317
+ [PROVIDER_ID4]: v7.optional(v7.looseObject({ apiKey: v7.optional(v7.string()), baseUrl: v7.optional(v7.string()) }))
2318
+ })
2092
2319
  )
2093
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
+ }
2094
2335
  function readProviderKey3(ctx) {
2095
2336
  const models = readJsoncFile(resolveModelsPath(ctx), ModelsSchema);
2096
2337
  const apiKey = models.kind === "valid" ? models.value.providers?.[PROVIDER_ID4]?.apiKey : void 0;
@@ -2113,17 +2354,26 @@ import { match as match10 } from "ts-pattern";
2113
2354
  import * as v8 from "valibot";
2114
2355
  var BINARY7 = "qwen";
2115
2356
  var KEY_VARIABLE = "NEXOS_API_KEY";
2357
+ var OPENAI_KEY_VARIABLE = "OPENAI_API_KEY";
2116
2358
  var AUTH_TYPE = "openai";
2117
2359
  var qwen = {
2118
2360
  kind: "configurable",
2119
2361
  id: "qwen-code",
2120
2362
  label: "Qwen Code",
2121
2363
  binary: BINARY7,
2364
+ installPackage: "@qwen-code/qwen-code",
2122
2365
  protocols: ["openai-chat"],
2123
2366
  projectSupport: { kind: "supported" },
2124
2367
  detect: (ctx) => detectQwen(ctx),
2125
2368
  configPath: (ctx) => resolveSettingsPath3(ctx),
2126
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."],
2127
2377
  readKey: (ctx) => readProviderKey4(ctx),
2128
2378
  readSecrets: (ctx) => readProviderSecrets4(ctx),
2129
2379
  postSetupNotes: [
@@ -2170,7 +2420,7 @@ function listUpdates3(desired, current) {
2170
2420
  var ModelProvidersSchema = v8.looseObject({
2171
2421
  modelProviders: v8.optional(v8.looseObject({ openai: v8.optional(v8.unknown()) }))
2172
2422
  });
2173
- var NexosEntrySchema = v8.looseObject({ envKey: v8.literal(KEY_VARIABLE) });
2423
+ var NexosEntrySchema = v8.looseObject({ envKey: v8.literal(KEY_VARIABLE), baseUrl: v8.optional(v8.string()) });
2174
2424
  function listOtherOpenAiProviders(current) {
2175
2425
  const parsed = parseJsoncConfig(current, ModelProvidersSchema);
2176
2426
  if (parsed.kind !== "valid") return [];
@@ -2179,11 +2429,26 @@ function listOtherOpenAiProviders(current) {
2179
2429
  if (!Array.isArray(openai)) throw new MergeConflictError();
2180
2430
  return openai.filter((entry) => !v8.is(NexosEntrySchema, entry));
2181
2431
  }
2182
- var SettingsSchema2 = v8.looseObject({
2183
- env: v8.optional(v8.looseObject({ [KEY_VARIABLE]: v8.optional(v8.string()) }))
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()) }))
2184
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
+ }
2185
2450
  function readProviderKey4(ctx) {
2186
- const settings2 = readJsoncFile(resolveSettingsPath3(ctx), SettingsSchema2);
2451
+ const settings2 = readJsoncFile(resolveSettingsPath3(ctx), SettingsSchema3);
2187
2452
  return readLiteralKey(settings2.kind === "valid" ? settings2.value.env?.[KEY_VARIABLE] : void 0);
2188
2453
  }
2189
2454
  var SecretsSchema = v8.looseObject({
@@ -2552,13 +2817,9 @@ async function replaceRejectedKey({
2552
2817
 
2553
2818
  // src/product/gateway/models/protocol.ts
2554
2819
  var PROTOCOLS = ["anthropic", "openai-chat", "openai-responses"];
2555
- var PROTOCOL_ENDPOINTS = {
2556
- anthropic: "messages",
2557
- "openai-chat": "chat_completion",
2558
- "openai-responses": "responses"
2559
- };
2820
+ var CONVERSATIONAL_ENDPOINTS = ["chat_completion", "messages", "responses"];
2560
2821
 
2561
- // src/ui/report/format-probes.ts
2822
+ // src/ui/report/detection/format-probes.ts
2562
2823
  import pc2 from "picocolors";
2563
2824
 
2564
2825
  // src/ui/glyphs.ts
@@ -2586,11 +2847,11 @@ function indentLines({ text: text2, indent }) {
2586
2847
  return lines.map((line) => line === "" ? "" : indent + line);
2587
2848
  }
2588
2849
 
2589
- // src/ui/report/format-probes.ts
2850
+ // src/ui/report/detection/format-probes.ts
2590
2851
  function formatProbes(results) {
2591
2852
  return [...formatHeading("Key check"), ...results.flatMap(formatProbe)];
2592
2853
  }
2593
- var SURFACE_COLUMN_WIDTH = 10;
2854
+ var SURFACE_COLUMN_WIDTH = 16;
2594
2855
  function formatProbe(result) {
2595
2856
  const colour = result.kind === "passed" ? pc2.dim : pc2.yellow;
2596
2857
  const glyph = result.kind === "passed" ? pc2.green(GLYPHS.success) : pc2.yellow(GLYPHS.warning);
@@ -2611,6 +2872,11 @@ function formatCount(count, noun) {
2611
2872
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
2612
2873
  }
2613
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
+
2614
2880
  // src/product/gateway/model-schema.ts
2615
2881
  import * as v11 from "valibot";
2616
2882
  var OptionalTextSchema2 = v11.fallback(v11.optional(v11.pipe(v11.string(), v11.nonEmpty())), void 0);
@@ -2641,7 +2907,7 @@ var OpenAiErrorSchema = v11.object({
2641
2907
  error: v11.object({
2642
2908
  message: v11.string(),
2643
2909
  type: v11.optional(v11.string()),
2644
- code: v11.optional(v11.nullable(v11.string()))
2910
+ code: v11.optional(v11.nullable(v11.union([v11.string(), v11.number()])))
2645
2911
  })
2646
2912
  });
2647
2913
  var AnthropicErrorSchema = v11.object({
@@ -2718,12 +2984,15 @@ function readListing({ status, text: text2 }, masker2) {
2718
2984
  const detail = `HTTP ${status} \u2014 authenticated, ${formatCount(models.length, "model")} available`;
2719
2985
  return { kind: "passed", detail, models, status };
2720
2986
  }
2721
- var INVALID_KEY_ERROR_CODE = "invalid_api_key";
2722
2987
  function classifyRejection(status, errorCode) {
2723
- return match12({ 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");
2724
2993
  }
2725
2994
 
2726
- // src/product/gateway/probe-result.ts
2995
+ // src/product/gateway/probes/probe-result.ts
2727
2996
  import { match as match13 } from "ts-pattern";
2728
2997
  function isKeyAccepted(probe) {
2729
2998
  return match13(probe.kind).with("passed", "bad-response", () => true).with("key-rejected", "model-rejected", "unreachable", "timed-out", "bad-status", () => false).exhaustive();
@@ -2823,16 +3092,13 @@ function findContinuation({ shouldAssumeYes, requestedModel, masker: masker2 })
2823
3092
  return masker2.maskText(`Continuing despite the failed check \u2014 "${requestedModel}" was named explicitly.`);
2824
3093
  }
2825
3094
 
2826
- // src/product/gateway/models/model-compatibility.ts
2827
- function isModelCompatible(model, protocols) {
2828
- return findUnservedProtocols(model, protocols).length === 0;
2829
- }
2830
- function findUnservedProtocols(model, protocols) {
2831
- if (model.endpoints.length === 0) return [];
2832
- 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));
2833
3099
  }
2834
3100
 
2835
- // src/ui/model-choices.ts
3101
+ // src/ui/prompts/model/model-choices.ts
2836
3102
  var BROWSE_VALUE = "\0browse";
2837
3103
  var MANUAL_VALUE = "\0manual";
2838
3104
  var MANUAL_ROW = {
@@ -2840,9 +3106,9 @@ var MANUAL_ROW = {
2840
3106
  label: "Type a model id\u2026",
2841
3107
  hint: "no model list available"
2842
3108
  };
2843
- function buildModelChoices(models, protocols, isRouterOffered) {
2844
- const compatible = models.filter((model) => isModelCompatible(model, protocols));
2845
- 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;
2846
3112
  const browsable = [...usable].sort((first, second) => first.id.localeCompare(second.id));
2847
3113
  const hiddenCount = models.length - usable.length;
2848
3114
  const routerRows = isRouterOffered ? PREDEFINED_MODELS.map(({ id, hint }) => ({ value: id, label: id, hint })) : [];
@@ -2878,7 +3144,6 @@ async function chooseModel({
2878
3144
  requestedModel,
2879
3145
  routerAvailability,
2880
3146
  shouldAssumeYes,
2881
- protocols,
2882
3147
  suggestions,
2883
3148
  ui
2884
3149
  }) {
@@ -2892,34 +3157,19 @@ async function chooseModel({
2892
3157
  }
2893
3158
  return DEFAULT_MODEL;
2894
3159
  }
2895
- return await ui.selectModel(
2896
- addSuggestionRows(buildModelChoices(keyCheck.models, protocols, isRouterOffered), suggestions)
2897
- );
3160
+ return await ui.selectModel(addSuggestionRows(buildModelChoices(keyCheck.models, isRouterOffered), suggestions));
2898
3161
  }
2899
- function findModelWarnings({
2900
- model,
2901
- models,
2902
- configurable,
2903
- routerAvailability
2904
- }) {
3162
+ function findModelWarnings({ model, models, routerAvailability }) {
2905
3163
  if (isPredefinedModel(model)) {
2906
3164
  return routerAvailability === "unavailable" ? [`"${model}" is not available for this key. Continuing because you named it.`] : [];
2907
3165
  }
2908
3166
  if (models.length === 0) return [];
2909
- const listed = models.find((candidate) => candidate.id === model);
2910
- if (listed === void 0) {
2911
- return [`"${model}" is not in the list this key can see. Continuing \u2014 the gateway may still accept it.`];
2912
- }
2913
- return configurable.flatMap((harness) => {
2914
- const unserved = findUnservedProtocols(listed, harness.protocols);
2915
- if (unserved.length === 0) return [];
2916
- const endpoints = unserved.map((protocol) => PROTOCOL_ENDPOINTS[protocol]).join(", ");
2917
- return [`${harness.label}: "${model}" does not advertise ${endpoints}. It may still work.`];
2918
- });
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.`];
2919
3169
  }
2920
3170
 
2921
3171
  // src/flows/model/model-probe.ts
2922
- import { match as match17 } from "ts-pattern";
3172
+ import { match as match18 } from "ts-pattern";
2923
3173
 
2924
3174
  // src/product/gateway/models/router-availability.ts
2925
3175
  import { match as match15 } from "ts-pattern";
@@ -2927,7 +3177,7 @@ function classifyRouterAvailability(probe) {
2927
3177
  return match15(probe.kind).returnType().with("passed", () => "available").with("model-rejected", () => "unavailable").otherwise(() => "unknown");
2928
3178
  }
2929
3179
 
2930
- // src/product/gateway/probe-anthropic.ts
3180
+ // src/product/gateway/probes/probe-anthropic.ts
2931
3181
  import { P as P3, match as match16 } from "ts-pattern";
2932
3182
  var MESSAGES_PATH = "/v1/messages";
2933
3183
  var ANTHROPIC_VERSION = "2023-06-01";
@@ -2991,16 +3241,109 @@ function classifyRejection2(status, errorType) {
2991
3241
  ).with({ status: MODEL_NOT_FOUND_STATUS }, { errorType: MODEL_NOT_FOUND_ERROR_TYPE }, () => "model-rejected").otherwise(() => "bad-status");
2992
3242
  }
2993
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
+
2994
3318
  // src/flows/model/model-probe.ts
2995
3319
  async function probeModel(params) {
2996
- const { anthropicBaseUrl, key, model, ui, masker: masker2, fetch: fetch2 } = params;
3320
+ const { model, protocols, ui, masker: masker2 } = params;
2997
3321
  const spinner = ui.spinner();
2998
3322
  spinner.start(masker2.maskText(`Checking "${model}" against the gateway\u2026`));
2999
- const probe = await spinner.waitFor(
3000
- (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 })))
3001
3325
  );
3002
- spinner.stop(summarizeModelProbe(probe));
3003
- 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();
3004
3347
  }
3005
3348
  async function probeRouter({
3006
3349
  anthropicBaseUrl,
@@ -3027,8 +3370,9 @@ async function shouldContinueAfterProbe({
3027
3370
  }
3028
3371
  return await ui.confirm(`Continue anyway? ${consequence}`, false);
3029
3372
  }
3030
- function summarizeModelProbe(probe) {
3031
- return match17(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];
3032
3376
  }
3033
3377
 
3034
3378
  // src/product/storage/preferences/preferences.ts
@@ -3036,6 +3380,18 @@ import { randomUUID } from "crypto";
3036
3380
  import fs4 from "fs";
3037
3381
  import path12 from "path";
3038
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
3039
3395
  var PREFERENCES_FILE = "preferences.json";
3040
3396
  function resolvePreferencesPath(home) {
3041
3397
  return path12.join(home, ENV_DIRECTORY_NAME, PREFERENCES_FILE);
@@ -3069,14 +3425,6 @@ function readPreferences(home) {
3069
3425
  }
3070
3426
  return parsed.output;
3071
3427
  }
3072
- function parseJson2(text2) {
3073
- try {
3074
- return JSON.parse(text2);
3075
- } catch (error) {
3076
- if (error instanceof SyntaxError) return void 0;
3077
- throw error;
3078
- }
3079
- }
3080
3428
  function writeAtomically(filePath, content) {
3081
3429
  fs4.mkdirSync(path12.dirname(filePath), { recursive: true });
3082
3430
  const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
@@ -3089,7 +3437,7 @@ function writeAtomically(filePath, content) {
3089
3437
  }
3090
3438
  }
3091
3439
 
3092
- // src/shared/file-system/describe-file-error.ts
3440
+ // src/shared/file-system/errors/describe-file-error.ts
3093
3441
  import * as v13 from "valibot";
3094
3442
  function describeFileError({ error, action, home }) {
3095
3443
  const failure = readFileFailure(error);
@@ -3112,18 +3460,18 @@ function findOwnDefaults(defaultModels, targets) {
3112
3460
  return groupByModel(defaultModels, targets);
3113
3461
  }
3114
3462
  function findBorrowedDefaults(params) {
3115
- const { defaultModels, targets, models, protocols } = params;
3463
+ const { defaultModels, targets, models } = params;
3116
3464
  const targetIds = new Set(targets.map((harness) => harness.id));
3117
3465
  const ownModels = new Set(targets.flatMap((harness) => defaultModels[harness.id] ?? []));
3118
3466
  const others = params.harnesses.filter((harness) => !targetIds.has(harness.id));
3119
3467
  return groupByModel(defaultModels, others).filter(
3120
- ({ model }) => !ownModels.has(model) && !isPredefinedModel(model) && isServed({ model, models, protocols })
3468
+ ({ model }) => !ownModels.has(model) && !isPredefinedModel(model) && isServed({ model, models })
3121
3469
  );
3122
3470
  }
3123
- function isServed({ model, models, protocols }) {
3471
+ function isServed({ model, models }) {
3124
3472
  if (models.length === 0) return true;
3125
3473
  const listed = models.find((candidate) => candidate.id === model);
3126
- return listed !== void 0 && isModelCompatible(listed, protocols);
3474
+ return listed !== void 0 && isConversationalModel(listed);
3127
3475
  }
3128
3476
  function groupByModel(defaultModels, harnesses2) {
3129
3477
  const labelsByModel = /* @__PURE__ */ new Map();
@@ -3210,8 +3558,8 @@ async function chooseCheckedModel(params) {
3210
3558
  requestedModel,
3211
3559
  shouldAssumeYes: options.shouldAssumeYes,
3212
3560
  resolved: keyStep.resolved,
3213
- onSavedKeyRejected: async (rejected, probe2) => {
3214
- 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 });
3215
3563
  if (replaced === null) return null;
3216
3564
  if (!isKeyUsable(replaced.key)) return null;
3217
3565
  adoptKey(replaced.key, { ui, masker: masker2 });
@@ -3233,8 +3581,7 @@ async function chooseCheckedModel(params) {
3233
3581
  defaultModels: preferences.defaultModels,
3234
3582
  targets: configurable,
3235
3583
  harnesses,
3236
- models: keyCheck.models,
3237
- protocols
3584
+ models: keyCheck.models
3238
3585
  });
3239
3586
  const { shouldAssumeYes } = options;
3240
3587
  const model = await chooseModel({
@@ -3242,11 +3589,10 @@ async function chooseCheckedModel(params) {
3242
3589
  requestedModel,
3243
3590
  routerAvailability,
3244
3591
  shouldAssumeYes,
3245
- protocols,
3246
3592
  suggestions,
3247
3593
  ui
3248
3594
  });
3249
- for (const warning of findModelWarnings({ model, models: keyCheck.models, configurable, routerAvailability })) {
3595
+ for (const warning of findModelWarnings({ model, models: keyCheck.models, routerAvailability })) {
3250
3596
  ui.log.warn(masker2.maskText(warning));
3251
3597
  }
3252
3598
  const source = requested?.source ?? "picked";
@@ -3258,21 +3604,26 @@ async function chooseCheckedModel(params) {
3258
3604
  check: keyCheck.outcome,
3259
3605
  modelSave
3260
3606
  };
3261
- if (!options.shouldVerify || !protocols.includes("anthropic")) {
3607
+ if (!options.shouldVerify || protocols.length === 0) {
3262
3608
  printProbes(keyCheck.probes, ui);
3263
3609
  return chosen;
3264
3610
  }
3265
3611
  const reusedRouterProbe = model === DEFAULT_MODEL ? routerProbe : void 0;
3266
- const probe = reusedRouterProbe === void 0 ? await probeModel({
3612
+ const probes = await probeModel({
3267
3613
  ...gatewayParams,
3268
3614
  anthropicBaseUrl: gatewayUrls.anthropicBaseUrl,
3615
+ openaiBaseUrl: gatewayUrls.openaiBaseUrl,
3269
3616
  model,
3270
- key: keyCheck.resolvedKey.key
3271
- }) : reusedRouterProbe.probe;
3272
- 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);
3273
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;
3274
3625
  const shouldContinue = await shouldContinueAfterProbe({
3275
- probe,
3626
+ probe: failed,
3276
3627
  shouldAssumeYes,
3277
3628
  consequence: params.probeConsequence,
3278
3629
  ui
@@ -3308,8 +3659,8 @@ async function launch(input, rawDeps) {
3308
3659
  const { masker: masker2, ctx } = rawDeps;
3309
3660
  const ui = ctx.isInteractive ? rawDeps.ui : refusePrompts(rawDeps.ui);
3310
3661
  if (!ctx.hasBinary(harness.binary)) {
3311
- ui.log.error(`${harness.binary} not found on PATH. Install ${harness.label} first.`);
3312
- return { code: EXIT_CODE.failed };
3662
+ const stopped = await offerInstall({ harness, options, ui, ctx, runNpm: rawDeps.runNpm });
3663
+ if (stopped !== null) return stopped;
3313
3664
  }
3314
3665
  const environment = readEnvironmentInputs(input.env);
3315
3666
  const gatewayUrls = buildGatewayUrls(environment.gatewayHost ?? GATEWAY_HOST);
@@ -3350,13 +3701,44 @@ async function launch(input, rawDeps) {
3350
3701
  }
3351
3702
  return await rawDeps.runLaunch(recipe, input.passthrough);
3352
3703
  }
3353
- function aKeyStepInput({ ctx, options, environmentKey, masker: masker2 }) {
3354
- const shell = resolveShell(ctx.platform);
3355
- const existingKeys = findExistingKeys({ ctx, envPath: resolveEnvFilePath(ctx.home, shell), shell });
3356
- masker2.registerSecrets(existingKeys.all);
3357
- return {
3358
- flagKey: options.key,
3359
- envKey: options.key === void 0 ? parseEnvironmentKey(environmentKey) : void 0,
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
+ }
3735
+ function aKeyStepInput({ ctx, options, environmentKey, masker: masker2 }) {
3736
+ const shell = resolveShell(ctx.platform);
3737
+ const existingKeys = findExistingKeys({ ctx, envPath: resolveEnvFilePath(ctx.home, shell), shell });
3738
+ masker2.registerSecrets(existingKeys.all);
3739
+ return {
3740
+ flagKey: options.key,
3741
+ envKey: options.key === void 0 ? parseEnvironmentKey(environmentKey) : void 0,
3360
3742
  shouldSaveKey: options.shouldSaveKey,
3361
3743
  shouldAssumeYes: options.shouldAssumeYes,
3362
3744
  isDryRun: false,
@@ -3381,7 +3763,7 @@ function resolveNamedHarnesses(ids) {
3381
3763
 
3382
3764
  // src/product/storage/backups/resolve-backup-root.ts
3383
3765
  import path14 from "path";
3384
- import { match as match18 } from "ts-pattern";
3766
+ import { match as match19 } from "ts-pattern";
3385
3767
  var PROJECT_BACKUP_DIRECTORY = ".nexos-backup";
3386
3768
  var USER_BACKUP_DIRECTORY = path14.join(".nexos", "backups");
3387
3769
  function formatBackupTimestamp(now) {
@@ -3391,7 +3773,7 @@ function resolveBackupRoot({ scope, home, cwd, stamp, backupDirectory }) {
3391
3773
  if (backupDirectory !== void 0) {
3392
3774
  return { path: path14.join(path14.resolve(cwd, backupDirectory), stamp), gitIgnoredDirectory: null };
3393
3775
  }
3394
- return match18(scope).returnType().with("user", () => ({ path: path14.join(home, USER_BACKUP_DIRECTORY, stamp), gitIgnoredDirectory: null })).with("project", () => {
3776
+ return match19(scope).returnType().with("user", () => ({ path: path14.join(home, USER_BACKUP_DIRECTORY, stamp), gitIgnoredDirectory: null })).with("project", () => {
3395
3777
  const directory = path14.join(cwd, PROJECT_BACKUP_DIRECTORY);
3396
3778
  return { path: path14.join(directory, stamp), gitIgnoredDirectory: directory };
3397
3779
  }).exhaustive();
@@ -3403,31 +3785,31 @@ import path16 from "path";
3403
3785
  import * as v14 from "valibot";
3404
3786
 
3405
3787
  // src/shared/file-formats/config-format.ts
3406
- import { match as match19 } from "ts-pattern";
3788
+ import { match as match20 } from "ts-pattern";
3407
3789
  var CONFIG_FORMATS = ["jsonc", "toml"];
3408
3790
  function renderConfig(current, edits) {
3409
- return match19(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();
3410
3792
  }
3411
3793
  function readOriginals(current, edits) {
3412
3794
  const keyPaths = edits.updates.map(([keyPath]) => keyPath);
3413
- return match19(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();
3414
3796
  }
3415
3797
  function restoreConfig({ format, current, originals }) {
3416
- return match19(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();
3417
3799
  }
3418
3800
  function isEmptyConfig(format, text2) {
3419
- const document = match19(format).with("jsonc", () => parseJsonc(text2)).with("toml", () => parseToml(text2)).exhaustive();
3801
+ const document = match20(format).with("jsonc", () => parseJsonc(text2)).with("toml", () => parseToml(text2)).exhaustive();
3420
3802
  return Object.keys(document).length === 0;
3421
3803
  }
3422
3804
  function parseValueSource(format, source) {
3423
- return match19(format).with("jsonc", () => parseJsoncValue(source)).with("toml", () => parseTomlValue(source)).exhaustive();
3805
+ return match20(format).with("jsonc", () => parseJsoncValue(source)).with("toml", () => parseTomlValue(source)).exhaustive();
3424
3806
  }
3425
3807
 
3426
3808
  // src/shared/file-system/resolve-owner-only-mode.ts
3427
- import { match as match20 } from "ts-pattern";
3809
+ import { match as match21 } from "ts-pattern";
3428
3810
  var OWNER_ONLY_MODE = 384;
3429
3811
  function resolveOwnerOnlyMode(platform) {
3430
- return match20(platform).returnType().with("win32", () => null).otherwise(() => OWNER_ONLY_MODE);
3812
+ return match21(platform).returnType().with("win32", () => null).otherwise(() => OWNER_ONLY_MODE);
3431
3813
  }
3432
3814
 
3433
3815
  // src/shared/file-system/write-file-with-mode.ts
@@ -3467,10 +3849,11 @@ var HarnessRecordSchema = v14.object({
3467
3849
  });
3468
3850
  var RestoreRecordSchema = v14.object({
3469
3851
  version: v14.literal(RECORD_VERSION),
3470
- harnesses: v14.record(v14.string(), HarnessRecordSchema)
3852
+ harnesses: v14.record(v14.string(), HarnessRecordSchema),
3853
+ userEnvironment: v14.optional(v14.pipe(v14.array(RecordedEnvVariableSchema), v14.readonly()), [])
3471
3854
  });
3472
3855
  function createEmptyRestoreRecord() {
3473
- return { version: RECORD_VERSION, harnesses: {} };
3856
+ return { version: RECORD_VERSION, harnesses: {}, userEnvironment: [] };
3474
3857
  }
3475
3858
  var RestoreRecordError = class extends Error {
3476
3859
  name = "RestoreRecordError";
@@ -3479,7 +3862,7 @@ function readRestoreRecord({ filePath, home }) {
3479
3862
  const text2 = readFileIfExists(filePath);
3480
3863
  if (text2 === null) return null;
3481
3864
  const shownPath = displayPath(filePath, home);
3482
- const json = parseJson3(text2);
3865
+ const json = parseJson2(text2);
3483
3866
  if (isNewerRecord(json)) {
3484
3867
  throw new RestoreRecordError(`${shownPath} was written by a newer nexos-connect. Update it, then run again.`);
3485
3868
  }
@@ -3491,14 +3874,6 @@ function readRestoreRecord({ filePath, home }) {
3491
3874
  }
3492
3875
  return parsed.output;
3493
3876
  }
3494
- function parseJson3(text2) {
3495
- try {
3496
- return JSON.parse(text2);
3497
- } catch (error) {
3498
- if (error instanceof SyntaxError) return void 0;
3499
- throw error;
3500
- }
3501
- }
3502
3877
  var VersionSchema = v14.object({ version: v14.number() });
3503
3878
  function isNewerRecord(json) {
3504
3879
  const parsed = v14.safeParse(VersionSchema, json);
@@ -3506,23 +3881,23 @@ function isNewerRecord(json) {
3506
3881
  }
3507
3882
  var RECORD_DIRECTORY_MODE = 448;
3508
3883
  var JSON_INDENT3 = 2;
3509
- function writeRestoreRecord({ filePath, record: record8, platform }) {
3510
- if (Object.keys(record8.harnesses).length === 0) {
3884
+ function writeRestoreRecord({ filePath, record: record9, platform }) {
3885
+ if (Object.keys(record9.harnesses).length === 0 && record9.userEnvironment.length === 0) {
3511
3886
  fs6.rmSync(filePath, { force: true });
3512
3887
  return;
3513
3888
  }
3514
3889
  fs6.mkdirSync(path16.dirname(filePath), { recursive: true, mode: RECORD_DIRECTORY_MODE });
3515
- const content = `${JSON.stringify(record8, null, JSON_INDENT3)}
3890
+ const content = `${JSON.stringify(record9, null, JSON_INDENT3)}
3516
3891
  `;
3517
3892
  writeFileWithMode({ filePath, content, mode: resolveOwnerOnlyMode(platform) });
3518
3893
  }
3519
3894
 
3520
3895
  // src/product/storage/restore-record/restore-record-location.ts
3521
3896
  import path17 from "path";
3522
- import { match as match21 } from "ts-pattern";
3897
+ import { match as match22 } from "ts-pattern";
3523
3898
  var RESTORE_RECORD_FILE = "restore-record.json";
3524
3899
  function resolveRestoreRecordLocation(ctx) {
3525
- return match21(ctx.scope).returnType().with("user", () => ({
3900
+ return match22(ctx.scope).returnType().with("user", () => ({
3526
3901
  filePath: path17.join(ctx.home, ENV_DIRECTORY_NAME, RESTORE_RECORD_FILE),
3527
3902
  root: ctx.home,
3528
3903
  gitIgnoredDirectory: null
@@ -3553,11 +3928,24 @@ function isInsideRoot(filePath, root) {
3553
3928
  return relativePath !== "" && first !== ".." && !path17.isAbsolute(relativePath);
3554
3929
  }
3555
3930
 
3556
- // src/ui/report/format-restore-preview.ts
3557
- import pc5 from "picocolors";
3931
+ // src/ui/report/preview/format-restore-preview.ts
3932
+ import pc6 from "picocolors";
3558
3933
 
3559
- // src/ui/report/format-preview.ts
3560
- 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";
3561
3949
 
3562
3950
  // src/ui/diff/diff-lines.ts
3563
3951
  function diffLines({ before, after }) {
@@ -3618,15 +4006,15 @@ function buildCommonLengthTable({ beforeLines, afterLines }) {
3618
4006
  }
3619
4007
 
3620
4008
  // src/ui/diff/render-diff.ts
3621
- import pc3 from "picocolors";
3622
- import { match as match22 } from "ts-pattern";
4009
+ import pc4 from "picocolors";
4010
+ import { match as match23 } from "ts-pattern";
3623
4011
  var DIFF_CONTEXT_LINES = 2;
3624
4012
  var ADDED_PREFIX = " + ";
3625
4013
  var REMOVED_PREFIX = " - ";
3626
4014
  var UNCHANGED_PREFIX = " ";
3627
4015
  function renderDiff(lines) {
3628
4016
  const visibleIndexes = findVisibleIndexes(lines);
3629
- if (visibleIndexes.size === 0) return [pc3.dim(`${UNCHANGED_PREFIX}(no change)`)];
4017
+ if (visibleIndexes.size === 0) return [pc4.dim(`${UNCHANGED_PREFIX}(no change)`)];
3630
4018
  const rendered = [];
3631
4019
  let isSkipping = false;
3632
4020
  for (const [index, line] of lines.entries()) {
@@ -3635,7 +4023,7 @@ function renderDiff(lines) {
3635
4023
  isSkipping = false;
3636
4024
  continue;
3637
4025
  }
3638
- if (!isSkipping) rendered.push(pc3.dim(`${UNCHANGED_PREFIX}${GLYPHS.elided}`));
4026
+ if (!isSkipping) rendered.push(pc4.dim(`${UNCHANGED_PREFIX}${GLYPHS.elided}`));
3639
4027
  isSkipping = true;
3640
4028
  }
3641
4029
  return rendered;
@@ -3651,54 +4039,65 @@ function findVisibleIndexes(lines) {
3651
4039
  return visibleIndexes;
3652
4040
  }
3653
4041
  function renderLine(line) {
3654
- return match22(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();
3655
4043
  }
3656
4044
 
3657
- // src/ui/report/format-preview.ts
4045
+ // src/ui/report/preview/format-preview.ts
3658
4046
  var NOTHING_TO_CHANGE = "Nothing to change \u2014 every selected agent is already configured or was skipped.";
3659
4047
  var NOTHING_TO_CHANGE_AFTER_FAILURE = "Nothing to change \u2014 an agent failed while planning; the report below says why.";
3660
- function formatPreview({ changes, model, scope, hasFailedAgent, home, masker: masker2 }) {
3661
- const heading = formatHeading(`Planned changes ${pc4.dim(`(scope: ${scope}, model: ${masker2.maskText(model)})`)}`);
3662
- 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) {
3663
4060
  const note4 = hasFailedAgent ? NOTHING_TO_CHANGE_AFTER_FAILURE : NOTHING_TO_CHANGE;
3664
- return [...heading, ` ${pc4.dim(note4)}`];
4061
+ return [...heading, ` ${pc5.dim(note4)}`];
3665
4062
  }
3666
- return [...heading, ...changes.flatMap((change) => formatFileDiff({ change, home, masker: masker2 }))];
4063
+ return [...heading, ...changes.flatMap((change) => formatFileDiff({ change, home, masker: masker2 })), ...userEnvironmentLines];
3667
4064
  }
3668
4065
  function formatFileDiff({ change, home, masker: masker2 }) {
3669
4066
  const diff = diffLines({ before: masker2.maskText(change.before ?? ""), after: masker2.maskText(change.after) });
3670
4067
  const lineCount = formatCount(countChangedLines(diff), "line");
3671
4068
  const lineCountNote = change.before === null ? `new file, ${lineCount}` : lineCount;
3672
- 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)];
3673
4070
  }
3674
4071
 
3675
- // src/ui/report/format-restore-preview.ts
4072
+ // src/ui/report/preview/format-restore-preview.ts
3676
4073
  var NOTHING_TO_RESTORE = "Nothing to change \u2014 every selected agent is already as it was.";
3677
4074
  var NOTHING_TO_RESTORE_AFTER_FAILURE = "Nothing to change \u2014 an agent failed while planning; the report below says why.";
3678
4075
  function formatRestorePreview(params) {
3679
- const { files, envFile, scope, hasFailedAgent, home, masker: masker2 } = params;
3680
- 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})`)}`);
3681
4078
  const changedFiles = files.filter((file) => file.isChanged);
3682
4079
  const changedEnvFile = envFile?.isChanged === true ? [envFile] : [];
3683
- if (changedFiles.length === 0 && changedEnvFile.length === 0) {
3684
- 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)}`];
3685
4083
  }
3686
4084
  return [
3687
4085
  ...heading,
3688
4086
  ...changedFiles.flatMap((file) => formatRestoredFile({ file, home, masker: masker2 })),
3689
- ...changedEnvFile.flatMap((change) => formatFileDiff({ change, home, masker: masker2 }))
4087
+ ...changedEnvFile.flatMap((change) => formatFileDiff({ change, home, masker: masker2 })),
4088
+ ...userEnvironmentLines
3690
4089
  ];
3691
4090
  }
3692
4091
  var DELETION_NOTE = "deleted \u2014 nexos-connect created it";
3693
4092
  function formatRestoredFile({ file, home, masker: masker2 }) {
3694
- 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})`)}`];
3695
4094
  const change = { path: file.path, before: file.before, after: file.after, isChanged: file.isChanged };
3696
4095
  return formatFileDiff({ change, home, masker: masker2 });
3697
4096
  }
3698
4097
 
3699
- // src/ui/report/format-restore-result.ts
3700
- import pc6 from "picocolors";
3701
- import { P as P4, match as match23 } 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";
3702
4101
 
3703
4102
  // src/commands/restore/restore-result.ts
3704
4103
  var RESTORE_OUTCOME_KINDS = ["restored", "unchanged", "nothing-to-restore", "failed"];
@@ -3710,7 +4109,7 @@ function exitCodeForRestore(outcomes) {
3710
4109
  return isFailed ? EXIT_CODE.failed : EXIT_CODE.ok;
3711
4110
  }
3712
4111
 
3713
- // src/ui/report/format-restore-result.ts
4112
+ // src/ui/report/result/format-restore-result.ts
3714
4113
  function formatRestoreResult(params) {
3715
4114
  const heading = params.mode.kind === "dry-run" ? "Result (dry run)" : "Result";
3716
4115
  return [
@@ -3722,45 +4121,51 @@ function formatRestoreResult(params) {
3722
4121
  ];
3723
4122
  }
3724
4123
  var RESTORE_OUTCOME_STYLE = {
3725
- restored: { glyph: GLYPHS.success, colour: pc6.green, label: "restored" },
3726
- unchanged: { glyph: GLYPHS.unchanged, colour: pc6.green, label: "already as it was" },
3727
- "nothing-to-restore": { glyph: GLYPHS.absent, colour: pc6.yellow, label: "nothing to restore" },
3728
- 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" }
3729
4128
  };
3730
4129
  var NO_RECORD = "nexos-connect has no record of setting it up at this scope.";
3731
4130
  function formatOutcome(outcome, { home, masker: masker2 }) {
3732
4131
  const style = RESTORE_OUTCOME_STYLE[outcome.kind];
3733
- const body = match23(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();
3734
4133
  return ["", ` ${style.colour(`${style.glyph} ${outcome.target.label} \u2014 ${style.label}`)}`, ...body];
3735
4134
  }
3736
4135
  function formatPlannedRestore(outcome, home, masker2) {
3737
4136
  const restartNote = `Restart ${outcome.target.label} so it reads the restored config.`;
3738
4137
  return [
3739
- ...outcome.files.filter((file) => file.isChanged).map((file) => `${INDENT}${pc6.dim(describeFile(file, home))}`),
3740
- ...outcome.warnings.map((warning) => `${INDENT}${pc6.yellow(`${GLYPHS.warning} ${masker2.maskText(warning)}`)}`),
3741
- ...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}`)}`] : []
3742
4141
  ];
3743
4142
  }
3744
4143
  function describeFile(file, home) {
3745
4144
  const action = file.after === null ? "deleted" : "put back";
3746
4145
  return `${displayPath(file.path, home)} \u2014 ${action}`;
3747
4146
  }
3748
- function formatEnvFileStep({ envFile, mode, home }) {
3749
- 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) {
3750
4155
  const isEmpty = parseEnvFile(envFile.after, envFile.shell).size === 0;
3751
4156
  const emptyNote = `${displayPath(envFile.path, home)} no longer sets anything: you can delete it, and the line that loads it.`;
3752
4157
  return [
3753
- ...formatHeading("One more step"),
3754
4158
  ...isEmpty ? [` ${emptyNote}`] : [],
3755
- ` ${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.")}`
3756
4160
  ];
3757
4161
  }
3758
4162
  function formatBackups({ mode, home, masker: masker2 }) {
3759
4163
  if (mode.kind === "dry-run" || mode.backupRoot === null) return [];
3760
4164
  const backupRoot = masker2.maskText(displayPath(mode.backupRoot, home));
3761
- return [...formatHeading("Backups"), ` Current versions copied to ${pc6.bold(backupRoot)}`];
4165
+ return [...formatHeading("Backups"), ` Current versions copied to ${pc7.bold(backupRoot)}`];
3762
4166
  }
3763
4167
  function formatSummary(outcomes) {
4168
+ if (outcomes.length === 0) return [];
3764
4169
  const countsByKind = /* @__PURE__ */ new Map();
3765
4170
  for (const { kind } of outcomes) countsByKind.set(kind, (countsByKind.get(kind) ?? 0) + 1);
3766
4171
  const counts = RESTORE_OUTCOME_KINDS.map(
@@ -3831,9 +4236,102 @@ function writeEnvFile({ filePath, content, platform }) {
3831
4236
  }
3832
4237
 
3833
4238
  // src/product/storage/restore-record/remove-harness-records.ts
3834
- function removeHarnessRecords(record8, harnessIds) {
3835
- const harnesses2 = Object.fromEntries(Object.entries(record8.harnesses).filter(([id]) => !harnessIds.includes(id)));
3836
- return { ...record8, 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
+ }
3837
4335
  }
3838
4336
 
3839
4337
  // src/product/storage/env-file/render-env-file.ts
@@ -3942,10 +4440,12 @@ function applyRestore(params) {
3942
4440
  const outcomes = agentWrites.map(({ outcome }) => outcome);
3943
4441
  const envFile = replanEnvFile(params.plan.envFile, outcomes);
3944
4442
  const envWrite = restoreSharedEnvFile({ outcomes, envFile, params });
4443
+ const userEnvironmentWrite = restoreUserEnvironment({ outcomes: envWrite.outcomes, params });
3945
4444
  const writtenFiles = [...agentWrites.flatMap(({ writtenFiles: writtenFiles2 }) => writtenFiles2), ...envWrite.writtenFiles];
3946
4445
  return {
3947
- outcomes: forgetRestored(envWrite.outcomes, params.ctx),
4446
+ outcomes: forgetRestored(userEnvironmentWrite.outcomes, params.ctx, userEnvironmentWrite.written !== null),
3948
4447
  envFile,
4448
+ userEnvironment: userEnvironmentWrite.written,
3949
4449
  writtenCount: writtenFiles.length,
3950
4450
  backupRoot: writtenFiles.some((file) => file.isBackedUp) ? params.backupRoot.path : null
3951
4451
  };
@@ -4003,14 +4503,36 @@ function failIfRestoringEnvFile(outcome, error) {
4003
4503
  if (!isPlannedRestore(outcome) || outcome.envVariables.length === 0) return outcome;
4004
4504
  return { kind: "failed", target: outcome.target, error };
4005
4505
  }
4006
- 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) {
4007
4529
  const restoredIds = outcomes.filter(isPlannedRestore).map(({ target }) => target.id);
4008
- if (restoredIds.length === 0) return [...outcomes];
4530
+ if (restoredIds.length === 0 && !isUserEnvironmentRestored) return [...outcomes];
4009
4531
  const { filePath } = resolveRestoreRecordLocation(ctx);
4010
4532
  try {
4011
- const record8 = readRestoreRecord({ filePath, home: ctx.home });
4012
- if (record8 !== null) {
4013
- writeRestoreRecord({ filePath, record: removeHarnessRecords(record8, 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 });
4014
4536
  }
4015
4537
  return [...outcomes];
4016
4538
  } catch (error) {
@@ -4025,7 +4547,7 @@ function failIfPlanned(outcome, error) {
4025
4547
 
4026
4548
  // src/commands/restore/restore-plan.ts
4027
4549
  import path20 from "path";
4028
- import { match as match24 } from "ts-pattern";
4550
+ import { match as match25 } from "ts-pattern";
4029
4551
 
4030
4552
  // src/product/storage/env-file/read-env-file.ts
4031
4553
  var NO_ENV_FILE = { kind: "read", text: null };
@@ -4040,28 +4562,79 @@ function readEnvFile({ envPath, home }) {
4040
4562
  // src/commands/restore/restore-plan.ts
4041
4563
  function planRestore(params) {
4042
4564
  const { candidates, ctx, envPath } = params;
4043
- const hasEnvVariables2 = candidates.some(({ record: record8 }) => (record8?.envVariables.length ?? 0) > 0);
4565
+ const hasEnvVariables2 = candidates.some(({ record: record9 }) => (record9?.envVariables.length ?? 0) > 0);
4044
4566
  const envFileRead = hasEnvVariables2 ? readEnvFile({ envPath, home: ctx.home }) : NO_ENV_FILE;
4045
4567
  const namesKept = listNamesRecordedElsewhere(params);
4046
- const outcomes = candidates.map((candidate) => planCandidate({ candidate, envFileRead, namesKept, params }));
4047
- 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
+ };
4048
4577
  }
4049
- function listNamesRecordedElsewhere({ candidates, record: record8 }) {
4578
+ function listNamesRecordedElsewhere({ candidates, record: record9 }) {
4050
4579
  const selectedIds = candidates.map(({ target }) => target.id);
4051
- const others = Object.entries(record8.harnesses).filter(([id]) => !selectedIds.includes(id));
4580
+ const others = Object.entries(record9.harnesses).filter(([id]) => !selectedIds.includes(id));
4052
4581
  return new Set(others.flatMap(([, harness]) => harness.envVariables.map(({ name }) => name)));
4053
4582
  }
4054
- function planCandidate({ candidate, envFileRead, namesKept, params }) {
4055
- const { target, record: record8 } = candidate;
4056
- if (record8 === null) return { kind: "nothing-to-restore", target };
4057
- const envVariables = record8.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));
4058
4628
  if (envVariables.length > 0 && envFileRead.kind === "failed") {
4059
4629
  return { kind: "failed", target, error: envFileRead.error };
4060
4630
  }
4061
4631
  try {
4062
- const { files, warnings } = planRecordedFiles(record8.files, params.ctx);
4632
+ const { files, warnings } = planRecordedFiles(record9.files, params.ctx);
4063
4633
  const hasEnvChange = hasOwnEnvChange({ envFileRead, envVariables, shell: params.shell });
4064
- 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);
4065
4638
  return { kind: isChanged ? "restored" : "unchanged", target, files, envVariables, warnings };
4066
4639
  } catch (error) {
4067
4640
  return { kind: "failed", target, error: describeFileError({ error, action: "read", home: params.ctx.home }) };
@@ -4091,7 +4664,7 @@ function planRecordedFile(file, ctx) {
4091
4664
  };
4092
4665
  }
4093
4666
  function describeMissingFile(filePath, ctx) {
4094
- const backups = match24(ctx.scope).with("user", () => path20.join(ctx.home, USER_BACKUP_DIRECTORY)).with("project", () => path20.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();
4095
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.`;
4096
4669
  }
4097
4670
  function restoreFile2({ file, before, filePath, home }) {
@@ -4113,12 +4686,16 @@ function planEnvFile({ outcomes, envFileRead, params }) {
4113
4686
  }
4114
4687
 
4115
4688
  // src/commands/restore/restore-secrets.ts
4116
- function listRestoreSecrets({ record: record8, ctx, envPath, shell }) {
4117
- const recorded = Object.values(record8.harnesses).flatMap((harness) => [
4689
+ function listRestoreSecrets({ record: record9, ctx, envPath, shell }) {
4690
+ const recorded = Object.values(record9.harnesses).flatMap((harness) => [
4118
4691
  ...harness.files.flatMap(listFileSecrets),
4119
4692
  ...listEnvSecrets(harness)
4120
4693
  ]);
4121
- return [...findExistingKeys({ ctx, envPath, shell }).all, ...recorded];
4694
+ return [
4695
+ ...findExistingKeys({ ctx, envPath, shell }).all,
4696
+ ...recorded,
4697
+ ...listUserEnvironmentSecrets(record9.userEnvironment)
4698
+ ];
4122
4699
  }
4123
4700
  function listFileSecrets(file) {
4124
4701
  return file.keys.flatMap(({ original, holdsSecret }) => {
@@ -4143,6 +4720,11 @@ function listEnvSecrets(harness) {
4143
4720
  ({ name, original }) => KEY_ENV_VARIABLES.includes(name) && original !== null ? [original] : []
4144
4721
  );
4145
4722
  }
4723
+ function listUserEnvironmentSecrets(variables) {
4724
+ return variables.flatMap(
4725
+ ({ name, original }) => KEY_ENV_VARIABLES.includes(name) && original !== null ? [original] : []
4726
+ );
4727
+ }
4146
4728
 
4147
4729
  // src/commands/restore/run-restore.ts
4148
4730
  var FINISHED_WITH_ERRORS = "Finished with errors.";
@@ -4161,27 +4743,28 @@ async function runRestore(params) {
4161
4743
  ui.outro(FINISHED_WITH_ERRORS);
4162
4744
  return EXIT_CODE.failed;
4163
4745
  }
4164
- const { record: record8 } = recordRead;
4165
- if (namedHarnesses.length === 0 && Object.keys(record8.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) {
4166
4749
  const recordPath = displayPath(resolveRestoreRecordLocation(ctx).filePath, ctx.home);
4167
4750
  ui.outro(`Nothing to restore \u2014 nexos-connect has not set up any agent at ${scope} scope (${recordPath}).`);
4168
4751
  return EXIT_CODE.ok;
4169
4752
  }
4170
4753
  const candidates = await selectCandidates({
4171
4754
  namedHarnesses,
4172
- record: record8,
4755
+ record: record9,
4173
4756
  ctx,
4174
4757
  shouldAssumeYes: options.shouldAssumeYes,
4175
4758
  ui
4176
4759
  });
4177
- if (candidates.length === 0) {
4760
+ if (candidates.length === 0 && hasRecordedHarnesses) {
4178
4761
  ui.cancel("No agents selected \u2014 nothing to do.");
4179
4762
  return EXIT_CODE.ok;
4180
4763
  }
4181
4764
  const run = { ...params, ui, ctx };
4182
- const plan = planSelected({ run, candidates, record: record8 });
4765
+ const plan = planSelected({ run, candidates, record: record9 });
4183
4766
  if (options.isDryRun) return report({ run, plan, mode: { kind: "dry-run" }, outro: DRY_RUN_OUTRO });
4184
- const hasChanges = plan.outcomes.some((outcome) => outcome.kind === "restored");
4767
+ const hasChanges = plan.outcomes.some((outcome) => outcome.kind === "restored") || plan.userEnvironment?.isChanged === true;
4185
4768
  if (hasChanges && !options.shouldAssumeYes && !await ui.confirm(CONFIRMATION, true)) {
4186
4769
  ui.cancel("Aborted \u2014 nothing was restored.");
4187
4770
  return EXIT_CODE.cancelled;
@@ -4189,7 +4772,12 @@ async function runRestore(params) {
4189
4772
  const applied = applySelected(run, plan);
4190
4773
  const outro3 = exitCodeForRestore(applied.outcomes) === EXIT_CODE.ok ? "Done." : FINISHED_WITH_ERRORS;
4191
4774
  const mode = { kind: "written", backupRoot: applied.backupRoot };
4192
- 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
+ });
4193
4781
  }
4194
4782
  async function resolveRestoreScope({ options, baseContext, ui }) {
4195
4783
  if (options.scope !== void 0) return options.scope;
@@ -4206,8 +4794,8 @@ function listRecordedLabels(ctx) {
4206
4794
  }
4207
4795
  function readRecord(ctx) {
4208
4796
  try {
4209
- const record8 = readRestoreRecord({ filePath: resolveRestoreRecordLocation(ctx).filePath, home: ctx.home });
4210
- return { kind: "read", record: record8 ?? createEmptyRestoreRecord() };
4797
+ const record9 = readRestoreRecord({ filePath: resolveRestoreRecordLocation(ctx).filePath, home: ctx.home });
4798
+ return { kind: "read", record: record9 ?? createEmptyRestoreRecord() };
4211
4799
  } catch (error) {
4212
4800
  if (!(error instanceof RestoreRecordError)) throw error;
4213
4801
  return { kind: "failed", error: error.message };
@@ -4217,34 +4805,43 @@ function labelOf(harnessId) {
4217
4805
  return findHarness(harnessId)?.label ?? harnessId;
4218
4806
  }
4219
4807
  async function selectCandidates(params) {
4220
- const { namedHarnesses, record: record8, ctx, ui } = params;
4221
- if (namedHarnesses.length > 0) return namedHarnesses.map((harness) => toCandidate(harness.id, record8));
4222
- const recordedIds = Object.keys(record8.harnesses);
4223
- if (params.shouldAssumeYes) return recordedIds.map((id) => toCandidate(id, record8));
4224
- const chosenIds = await ui.selectRestoreHarnesses(recordedIds.map((id) => describeChoice({ id, record: record8, ctx })));
4225
- return chosenIds.map((id) => toCandidate(id, record8));
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));
4226
4815
  }
4227
- function toCandidate(id, record8) {
4228
- return { target: { id, label: labelOf(id) }, record: record8.harnesses[id] ?? null };
4816
+ function toCandidate(id, record9) {
4817
+ return { target: { id, label: labelOf(id) }, record: record9.harnesses[id] ?? null };
4229
4818
  }
4230
4819
  var ENV_VARIABLES_HINT = "environment variables";
4231
- function describeChoice({ id, record: record8, ctx }) {
4232
- const harness = record8.harnesses[id];
4820
+ function describeChoice({ id, record: record9, ctx }) {
4821
+ const harness = record9.harnesses[id];
4233
4822
  const root = resolveRestoreRecordLocation(ctx).root;
4234
4823
  const files = (harness?.files ?? []).map((file) => displayPath(path21.join(root, ...file.path), ctx.home));
4235
4824
  const variables = (harness?.envVariables.length ?? 0) > 0 ? [ENV_VARIABLES_HINT] : [];
4236
4825
  return { value: id, label: labelOf(id), hint: [...files, ...variables].join(", ") };
4237
4826
  }
4238
- function planSelected({ run, candidates, record: record8 }) {
4827
+ function planSelected({ run, candidates, record: record9 }) {
4239
4828
  const { ctx, masker: masker2, ui } = run;
4240
4829
  const shell = resolveShell(ctx.platform);
4241
4830
  const envPath = resolveEnvFilePath(ctx.home, shell);
4242
- masker2.registerSecrets(listRestoreSecrets({ record: record8, ctx, envPath, shell }));
4243
- const plan = planRestore({ candidates, record: record8, 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 });
4244
4833
  const files = plan.outcomes.flatMap((outcome) => isPlannedRestore(outcome) ? outcome.files : []);
4245
4834
  const hasFailedAgent = plan.outcomes.some((outcome) => outcome.kind === "failed");
4246
4835
  ui.print(
4247
- 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
+ })
4248
4845
  );
4249
4846
  return plan;
4250
4847
  }
@@ -4257,21 +4854,71 @@ function applySelected(run, plan) {
4257
4854
  stamp: formatBackupTimestamp(run.now()),
4258
4855
  backupDirectory: options.backupDirectory
4259
4856
  });
4857
+ const shell = resolveShell(ctx.platform);
4858
+ const envPath = resolveEnvFilePath(ctx.home, shell);
4260
4859
  const spinner = ui.spinner();
4261
4860
  spinner.start("Backing up current versions and restoring\u2026");
4262
- const applied = applyRestore({ plan, ctx, backupRoot });
4861
+ const applied = applyRestore({ plan, ctx, backupRoot, userEnvironment: run.userEnvironment, envPath });
4263
4862
  spinner.stop(`Restored ${formatCount(applied.writtenCount, "file")}.`);
4264
4863
  return applied;
4265
4864
  }
4266
4865
  function report({ run, plan, mode, outro: outro3 }) {
4267
4866
  const { ui, ctx, masker: masker2 } = run;
4268
- 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
+ );
4269
4877
  ui.outro(outro3);
4270
4878
  return exitCodeForRestore(plan.outcomes);
4271
4879
  }
4272
4880
 
4273
- // src/ui/report/format-detection.ts
4274
- 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";
4275
4922
  function formatDetection(rows, masker2) {
4276
4923
  return [
4277
4924
  ...formatHeading("Where each agent keeps its settings"),
@@ -4280,16 +4927,16 @@ function formatDetection(rows, masker2) {
4280
4927
  }
4281
4928
  var LABEL_COLUMN_WIDTH = 24;
4282
4929
  var DETECTION_STYLE = {
4283
- found: { glyph: GLYPHS.success, colour: pc7.green, label: "found" },
4284
- "not-found": { glyph: GLYPHS.absent, colour: pc7.dim, label: "not found" },
4285
- failed: { glyph: GLYPHS.failure, colour: pc7.red, label: "could not check" },
4286
- "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" }
4287
4934
  };
4288
4935
  function formatDetectionRow({ harness, detection }, masker2) {
4289
4936
  const style = DETECTION_STYLE[findDetectionStatus(harness, detection)];
4290
4937
  return [
4291
4938
  ` ${style.colour(style.glyph)} ${harness.label.padEnd(LABEL_COLUMN_WIDTH)} ${style.colour(style.label)}`,
4292
- `${INDENT}${pc7.dim(masker2.maskText(detection.detail))}`
4939
+ `${INDENT}${pc8.dim(masker2.maskText(detection.detail))}`
4293
4940
  ];
4294
4941
  }
4295
4942
  function findDetectionStatus(harness, detection) {
@@ -4297,9 +4944,9 @@ function findDetectionStatus(harness, detection) {
4297
4944
  return detection.kind;
4298
4945
  }
4299
4946
 
4300
- // src/ui/report/format-result.ts
4301
- import pc9 from "picocolors";
4302
- import { P as P5, match as match25 } 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";
4303
4950
 
4304
4951
  // src/commands/setup/setup-result.ts
4305
4952
  var OUTCOME_KINDS = ["updated", "unchanged", "not-found", "unsupported", "failed"];
@@ -4317,16 +4964,16 @@ function formatList(items) {
4317
4964
  }
4318
4965
 
4319
4966
  // src/ui/report/outcome-style.ts
4320
- import pc8 from "picocolors";
4967
+ import pc9 from "picocolors";
4321
4968
  var OUTCOME_STYLE = {
4322
- updated: { glyph: GLYPHS.success, colour: pc8.green, label: "updated" },
4323
- unchanged: { glyph: GLYPHS.unchanged, colour: pc8.green, label: "already configured" },
4324
- "not-found": { glyph: GLYPHS.absent, colour: pc8.dim, label: "not found" },
4325
- unsupported: { glyph: GLYPHS.failure, colour: pc8.yellow, label: "not configurable" },
4326
- 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" }
4327
4974
  };
4328
4975
 
4329
- // src/ui/report/format-result.ts
4976
+ // src/ui/report/result/format-result.ts
4330
4977
  function formatResult(params) {
4331
4978
  const heading = params.mode.kind === "dry-run" ? "Result (dry run)" : "Result";
4332
4979
  return [
@@ -4341,21 +4988,21 @@ function formatResult(params) {
4341
4988
  function formatOutcome2(outcome, params) {
4342
4989
  const { masker: masker2 } = params;
4343
4990
  const style = OUTCOME_STYLE[outcome.kind];
4344
- const body = match25(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();
4345
4992
  return ["", ` ${style.colour(`${style.glyph} ${outcome.harness.label} \u2014 ${style.label}`)}`, ...body];
4346
4993
  }
4347
- function formatPlannedOutcome(outcome, { envFile, home, masker: masker2 }) {
4994
+ function formatPlannedOutcome(outcome, { envFile, home, masker: masker2, model }) {
4348
4995
  return [
4349
- `${INDENT}${pc9.dim(masker2.maskText(outcome.detail))}`,
4350
- ...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)}`)}`),
4351
4998
  ...outcome.files.filter((file) => file.isChanged).flatMap((file) => formatResultingFile(file.after, masker2)),
4352
4999
  ...envFile === null ? [] : formatEnvVariables({ outcome, envFile, home, masker: masker2 }),
4353
- ...formatPostSetupNotes(outcome)
5000
+ ...formatPostSetupNotes(outcome, model, masker2)
4354
5001
  ];
4355
5002
  }
4356
5003
  function formatResultingFile(content, masker2) {
4357
5004
  return [
4358
- `${INDENT}${pc9.dim("resulting file:")}`,
5005
+ `${INDENT}${pc10.dim("resulting file:")}`,
4359
5006
  ...indentLines({ text: masker2.maskText(content), indent: NESTED_INDENT })
4360
5007
  ];
4361
5008
  }
@@ -4366,34 +5013,44 @@ function formatEnvVariables({ outcome, envFile, home, masker: masker2 }) {
4366
5013
  if (variables.length === 0) return [];
4367
5014
  const syntax = ENV_SYNTAXES[envFile.shell];
4368
5015
  return [
4369
- `${INDENT}${pc9.dim(`variables in ${displayPath(envFile.path, home)}:`)}`,
5016
+ `${INDENT}${pc10.dim(`variables in ${displayPath(envFile.path, home)}:`)}`,
4370
5017
  ...variables.map(([name, value]) => `${NESTED_INDENT}${masker2.maskText(syntax.formatAssignment({ name, value }))}`)
4371
5018
  ];
4372
5019
  }
4373
- function formatPostSetupNotes({ kind, harness }) {
4374
- if (kind === "unchanged") return [];
4375
- 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)}`)}`);
4376
5023
  }
4377
- function formatLoadStep({ outcomes, envFile, mode }) {
4378
- 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 [];
4379
5028
  const readers = outcomes.filter((outcome) => outcome.kind === "updated" && Object.keys(outcome.envVariables).length > 0).map((outcome) => outcome.harness.label);
4380
- if (readers.length === 0) return [];
5029
+ if (readers.length === 0 || envFile === null) return [];
4381
5030
  const [verb, pronoun] = readers.length === 1 ? ["reads", "it"] : ["read", "they"];
4382
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
+ }
4383
5040
  return [
4384
5041
  ...formatHeading("One more step"),
4385
5042
  ` ${formatList(readers)} ${verb} the key from the environment. Load it with:`,
4386
- `${INDENT}${pc9.bold(loadCommand)}`,
4387
- ` ${pc9.dim(`Until then ${pronoun} will not see the key.`)}`,
4388
- ...match25(envFile.shell).with("powershell", () => [
4389
- ` ${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.")}`
4390
5047
  ]).with("posix", () => []).exhaustive()
4391
5048
  ];
4392
5049
  }
4393
5050
  function formatBackups2({ mode, home, masker: masker2 }) {
4394
5051
  if (mode.kind === "dry-run" || mode.backupRoot === null) return [];
4395
5052
  const backupRoot = masker2.maskText(displayPath(mode.backupRoot, home));
4396
- return [...formatHeading("Backups"), ` Originals copied to ${pc9.bold(backupRoot)}`];
5053
+ return [...formatHeading("Backups"), ` Originals copied to ${pc10.bold(backupRoot)}`];
4397
5054
  }
4398
5055
  function formatSummary2(outcomes) {
4399
5056
  const countsByKind = /* @__PURE__ */ new Map();
@@ -4401,16 +5058,22 @@ function formatSummary2(outcomes) {
4401
5058
  const counts = OUTCOME_KINDS.map((kind) => `${countsByKind.get(kind) ?? 0} ${OUTCOME_STYLE[kind].label}`);
4402
5059
  return [...formatHeading("Summary"), ` ${counts.join(", ")}`];
4403
5060
  }
4404
- function formatRunHints({ runHints, mode }) {
5061
+ function formatRunHints({ runHints, mode, model, shell }) {
4405
5062
  if (mode.kind !== "written" || runHints.length === 0) return [];
4406
- return ["", "You're all set up.", ...runHints.map(formatRunHint)];
5063
+ return ["", "You're all set up.", ...runHints.map((runHint) => formatRunHint({ runHint, model, shell }))];
5064
+ }
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.`;
4407
5068
  }
4408
- function formatRunHint({ binary, label }) {
4409
- return ` Run \`${binary}\` to use ${label} with the nexos.ai Gateway.`;
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}'`;
4410
5073
  }
4411
5074
 
4412
5075
  // src/commands/setup/answers/describe-key-destination.ts
4413
- import { match as match26 } from "ts-pattern";
5076
+ import { match as match27 } from "ts-pattern";
4414
5077
 
4415
5078
  // src/commands/setup/answers/scope.ts
4416
5079
  async function resolveScope({
@@ -4458,7 +5121,7 @@ function describeKeyDestination({ configurable, ctx, envPath }) {
4458
5121
  return `It will be written to ${formatList(configFiles)}, and to ${envFile}.`;
4459
5122
  }
4460
5123
  function displayScopedPath(filePath, ctx) {
4461
- return match26(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();
4462
5125
  }
4463
5126
 
4464
5127
  // src/commands/setup/answers/harness-selection.ts
@@ -4510,15 +5173,23 @@ var DETECTION_HINTS = {
4510
5173
  "not-found": "not found",
4511
5174
  failed: "could not check"
4512
5175
  };
5176
+ var INSTALL_FIRST_HINT = "not installed \u2014 will be installed first";
4513
5177
  function describeHarnessChoice(candidate, scope) {
4514
5178
  const ineligibility = findIneligibility(candidate.harness, scope);
4515
5179
  return {
4516
5180
  value: candidate.harness.id,
4517
5181
  label: candidate.harness.label,
4518
- hint: ineligibility ?? DETECTION_HINTS[candidate.detection.kind],
5182
+ hint: ineligibility ?? describeDetection(candidate, scope),
4519
5183
  isDisabled: ineligibility !== void 0
4520
5184
  };
4521
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
+ }
4522
5193
  function isDefaultChoice({ harness, detection }, scope) {
4523
5194
  return findIneligibility(harness, scope) === void 0 && detection.kind === "found";
4524
5195
  }
@@ -4537,8 +5208,15 @@ function planChanges(params) {
4537
5208
  const { envPath, shell } = params;
4538
5209
  const drafts = params.selected.map((candidate) => planHarness(candidate, params));
4539
5210
  const envFileRead = drafts.some(hasEnvVariables) ? readEnvFile({ envPath, home: params.ctx.home }) : NO_ENV_FILE;
4540
- const outcomes = drafts.map((draft) => settleOutcome({ draft, envFileRead, shell }));
4541
- 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
+ };
4542
5220
  }
4543
5221
  function listChangedFiles({ outcomes, envFile }) {
4544
5222
  const changedFiles = outcomes.flatMap(
@@ -4623,14 +5301,37 @@ function collectEnvVariables(contributions) {
4623
5301
  function hasEnvVariables(draft) {
4624
5302
  return draft.kind === "planned" && Object.keys(draft.envVariables).length > 0;
4625
5303
  }
4626
- function settleOutcome({ draft, envFileRead, shell }) {
5304
+ function settleOutcome({ draft, envFileRead, shell, userEnvironment: userEnvironment2 }) {
4627
5305
  if (draft.kind !== "planned") return draft;
4628
5306
  if (envFileRead.kind === "failed" && hasEnvVariables(draft)) return failWith(draft, envFileRead.error);
4629
5307
  const envText = envFileRead.kind === "read" ? envFileRead.text : null;
4630
5308
  const hasOwnEnvChange2 = hasEnvVariables(draft) && renderEnvFile({ current: envText, variables: draft.envVariables, shell }) !== envText;
4631
- 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);
4632
5311
  return { ...draft, kind: isChanged ? "updated" : "unchanged" };
4633
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
+ }
4634
5335
  function failWith({ harness }, error) {
4635
5336
  return { kind: "failed", harness, error };
4636
5337
  }
@@ -4644,32 +5345,37 @@ function classifyFileOrigin(before) {
4644
5345
  if (before === null) return "absent";
4645
5346
  return before.trim() === "" ? "blank" : "content";
4646
5347
  }
4647
- function addFileOriginals({ record: record8, harnessId, file }) {
4648
- const harness = readHarnessRecord(record8, harnessId);
5348
+ function addFileOriginals({ record: record9, harnessId, file }) {
5349
+ const harness = readHarnessRecord(record9, harnessId);
4649
5350
  const existing = harness.files.find((recorded) => isSamePath2(recorded.path, file.path));
4650
5351
  const others = harness.files.filter((recorded) => recorded !== existing);
4651
- return withHarnessRecord(record8, harnessId, { ...harness, files: [...others, mergeFile(existing, file)] });
5352
+ return withHarnessRecord(record9, harnessId, { ...harness, files: [...others, mergeFile(existing, file)] });
4652
5353
  }
4653
- function addEnvOriginals({ record: record8, harnessId, variables, isFileAbsent }) {
4654
- const harness = readHarnessRecord(record8, harnessId);
5354
+ function addEnvOriginals({ record: record9, harnessId, variables, isFileAbsent }) {
5355
+ const harness = readHarnessRecord(record9, harnessId);
4655
5356
  const kept = isFileAbsent ? [] : harness.envVariables;
4656
5357
  const added = variables.filter((variable) => !kept.some(({ name }) => name === variable.name));
4657
- return withHarnessRecord(record8, 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] };
4658
5364
  }
4659
5365
  var EMPTY_HARNESS_RECORD = { files: [], envVariables: [] };
4660
- function readHarnessRecord(record8, harnessId) {
4661
- return record8.harnesses[harnessId] ?? EMPTY_HARNESS_RECORD;
5366
+ function readHarnessRecord(record9, harnessId) {
5367
+ return record9.harnesses[harnessId] ?? EMPTY_HARNESS_RECORD;
4662
5368
  }
4663
- function withHarnessRecord(record8, harnessId, harness) {
4664
- return { ...record8, harnesses: { ...record8.harnesses, [harnessId]: harness } };
5369
+ function withHarnessRecord(record9, harnessId, harness) {
5370
+ return { ...record9, harnesses: { ...record9.harnesses, [harnessId]: harness } };
4665
5371
  }
4666
5372
  function mergeFile(existing, file) {
4667
5373
  if (existing === void 0 || file.origin === "absent") return file;
4668
5374
  const added = file.keys.filter((key) => !existing.keys.some(({ keyPath }) => isSamePath2(keyPath, key.keyPath)));
4669
5375
  return { ...existing, keys: [...existing.keys, ...added] };
4670
5376
  }
4671
- function isSamePath2(path25, other) {
4672
- return path25.length === other.length && path25.every((segment, index) => segment === other[index]);
5377
+ function isSamePath2(path26, other) {
5378
+ return path26.length === other.length && path26.every((segment, index) => segment === other[index]);
4673
5379
  }
4674
5380
 
4675
5381
  // src/commands/setup/changes/record-originals.ts
@@ -4681,7 +5387,7 @@ function recordFileOriginals({ harnessId, file, ctx }) {
4681
5387
  origin: classifyFileOrigin(file.before),
4682
5388
  keys: file.originals
4683
5389
  };
4684
- updateRestoreRecord(location, ctx, (record8) => addFileOriginals({ record: record8, harnessId, file: recordedFile }));
5390
+ updateRestoreRecord(location, ctx, (record9) => addFileOriginals({ record: record9, harnessId, file: recordedFile }));
4685
5391
  }
4686
5392
  function recordEnvOriginals({ outcomes, envFile, ctx }) {
4687
5393
  const syntax = ENV_SYNTAXES[envFile.shell];
@@ -4690,7 +5396,7 @@ function recordEnvOriginals({ outcomes, envFile, ctx }) {
4690
5396
  updateRestoreRecord(
4691
5397
  resolveRestoreRecordLocation(ctx),
4692
5398
  ctx,
4693
- (record8) => outcomes.reduce((updated, outcome) => {
5399
+ (record9) => outcomes.reduce((updated, outcome) => {
4694
5400
  if (outcome.kind !== "updated") return updated;
4695
5401
  const variables = Object.keys(outcome.envVariables).map((name) => ({
4696
5402
  name,
@@ -4698,13 +5404,25 @@ function recordEnvOriginals({ outcomes, envFile, ctx }) {
4698
5404
  }));
4699
5405
  if (variables.length === 0) return updated;
4700
5406
  return addEnvOriginals({ record: updated, harnessId: outcome.harness.id, variables, isFileAbsent });
4701
- }, record8)
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 })
4702
5420
  );
4703
5421
  }
4704
5422
  function updateRestoreRecord(location, ctx, update) {
4705
5423
  if (location.gitIgnoredDirectory !== null) ignoreBackupsInGit(location.gitIgnoredDirectory);
4706
- const record8 = readRestoreRecord({ filePath: location.filePath, home: ctx.home }) ?? createEmptyRestoreRecord();
4707
- writeRestoreRecord({ filePath: location.filePath, record: update(record8), 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 });
4708
5426
  }
4709
5427
 
4710
5428
  // src/commands/setup/changes/apply-changes.ts
@@ -4713,10 +5431,12 @@ function applyChanges(params) {
4713
5431
  const outcomes = agentWrites.map(({ outcome }) => outcome);
4714
5432
  const envFile = replanEnvFile2(params.plan.envFile, outcomes);
4715
5433
  const envWrite = writeSharedEnvFile({ outcomes, envFile, params });
5434
+ const userEnvironmentWrite = writeUserEnvironment2({ outcomes: envWrite.outcomes, params });
4716
5435
  const writtenFiles = [...agentWrites.flatMap(({ writtenFiles: writtenFiles2 }) => writtenFiles2), ...envWrite.writtenFiles];
4717
5436
  return {
4718
- outcomes: envWrite.outcomes,
5437
+ outcomes: userEnvironmentWrite.outcomes,
4719
5438
  envFile,
5439
+ userEnvironment: userEnvironmentWrite.written,
4720
5440
  writtenCount: writtenFiles.length,
4721
5441
  backupRoot: writtenFiles.some((file) => file.isBackedUp) ? params.backupRoot.path : null
4722
5442
  };
@@ -4727,9 +5447,9 @@ function writeAgentFiles(outcome, params) {
4727
5447
  for (const file of outcome.files.filter((plan) => plan.isChanged)) {
4728
5448
  try {
4729
5449
  const relativePath = path22.join(outcome.harness.id, path22.basename(file.path));
4730
- const record8 = () => recordFileOriginals({ harnessId: outcome.harness.id, file, ctx: params.ctx });
5450
+ const record9 = () => recordFileOriginals({ harnessId: outcome.harness.id, file, ctx: params.ctx });
4731
5451
  const write = () => writeFileWithMode({ filePath: file.path, content: file.after, mode: resolveWriteMode(file, params.ctx) });
4732
- writtenFiles.push(backUpThenWrite({ file, relativePath, record: record8, write, params }));
5452
+ writtenFiles.push(backUpThenWrite({ file, relativePath, record: record9, write, params }));
4733
5453
  } catch (error) {
4734
5454
  const reason = describeFileError({ error, action: "write", home: params.ctx.home });
4735
5455
  return { outcome: { kind: "failed", harness: outcome.harness, error: reason }, writtenFiles };
@@ -4746,9 +5466,9 @@ function writeSharedEnvFile({ outcomes, envFile, params }) {
4746
5466
  try {
4747
5467
  const relativePath = path22.join(ENV_FILE_BACKUP_DIRECTORY, path22.basename(envFile.path));
4748
5468
  const { ctx } = params;
4749
- const record8 = () => recordEnvOriginals({ outcomes, envFile, ctx });
5469
+ const record9 = () => recordEnvOriginals({ outcomes, envFile, ctx });
4750
5470
  const write = () => writeEnvFile({ filePath: envFile.path, content: envFile.after, platform: ctx.platform });
4751
- return { outcomes, writtenFiles: [backUpThenWrite({ file: envFile, relativePath, record: record8, write, params })] };
5471
+ return { outcomes, writtenFiles: [backUpThenWrite({ file: envFile, relativePath, record: record9, write, params })] };
4752
5472
  } catch (error) {
4753
5473
  const reason = describeFileError({ error, action: "write", home: params.ctx.home });
4754
5474
  return { outcomes: outcomes.map((outcome) => failIfWritingEnvFile(outcome, reason)), writtenFiles: [] };
@@ -4758,10 +5478,31 @@ function failIfWritingEnvFile(outcome, error) {
4758
5478
  if (outcome.kind !== "updated" || Object.keys(outcome.envVariables).length === 0) return outcome;
4759
5479
  return { kind: "failed", harness: outcome.harness, error };
4760
5480
  }
4761
- function backUpThenWrite({ file, relativePath, record: record8, 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 }) {
4762
5503
  const { ctx, backupRoot } = params;
4763
5504
  const backup = backUpBeforeWriting({ filePath: file.path, before: file.before, backupRoot, relativePath, ctx });
4764
- record8();
5505
+ record9();
4765
5506
  write();
4766
5507
  return backup;
4767
5508
  }
@@ -4791,11 +5532,26 @@ async function runSetup(params) {
4791
5532
  ui
4792
5533
  });
4793
5534
  const ctx = { ...params.baseContext, scope };
4794
- const selected = await selectHarnesses({ namedHarnesses, ctx, shouldAssumeYes: options.shouldAssumeYes, ui });
4795
- 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;
4796
5552
  if (selected.length === 0) {
4797
5553
  ui.cancel("No agents selected \u2014 nothing to do.");
4798
- return EXIT_CODE.ok;
5554
+ return install.hasFailure ? EXIT_CODE.failed : EXIT_CODE.ok;
4799
5555
  }
4800
5556
  warnAboutUnsupported(selected, ui);
4801
5557
  if (!selected.some(({ harness }) => harness.kind === "configurable")) {
@@ -4803,7 +5559,7 @@ async function runSetup(params) {
4803
5559
  return EXIT_CODE.ok;
4804
5560
  }
4805
5561
  const found = selected.flatMap(listFoundConfigurable);
4806
- if (found.length === 0) return endWithNothingFound(selected, ui, masker2);
5562
+ if (found.length === 0) return endWithNothingFound({ selected, ui, masker: masker2, hasInstallFailure: install.hasFailure });
4807
5563
  const shell = resolveShell(ctx.platform);
4808
5564
  const run = { ...params, ui, ctx, shell, envPath: resolveEnvFilePath(ctx.home, shell), gatewayUrls };
4809
5565
  const keyStep = await establishKey({ run, configurable: found, environment });
@@ -4830,21 +5586,46 @@ async function runSetup(params) {
4830
5586
  const key = resolvedKey.key;
4831
5587
  ui.print(formatDetection(selected, masker2));
4832
5588
  const desired = buildDesiredConfig({ gatewayUrls, apiKey: key, model });
4833
- 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
+ });
4834
5598
  const changes = listChangedFiles(plan);
4835
5599
  const isPlanFailed = exitCodeFor(plan.outcomes) !== EXIT_CODE.ok;
4836
- 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
+ );
4837
5611
  if (options.isDryRun) {
4838
5612
  if (resolvedKey.shouldSave) ui.dim("Dry run \u2014 the key was not saved.");
4839
5613
  if (modelSave.length > 0) ui.dim("Dry run \u2014 the default model was not saved.");
4840
- 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
+ });
4841
5622
  }
4842
- if (changes.length === 0) {
5623
+ if (changes.length === 0 && plan.userEnvironment?.isChanged !== true) {
4843
5624
  await commitSave(resolvedKey, check4, keyStep.deps);
4844
5625
  saveModelFor(plan.outcomes);
4845
5626
  const mode2 = { kind: "written", backupRoot: null };
4846
5627
  const outro4 = isPlanFailed ? FINISHED_WITH_ERRORS2 : "Nothing to do \u2014 already configured.";
4847
- return reportResult({ run, plan, mode: mode2, outro: outro4 });
5628
+ return reportResult({ run, plan, model, mode: mode2, outro: outro4, hasInstallFailure: install.hasFailure });
4848
5629
  }
4849
5630
  if (!options.shouldAssumeYes && !await ui.confirm("Write these changes? Originals are backed up first.", true)) {
4850
5631
  return abort(ui);
@@ -4854,7 +5635,14 @@ async function runSetup(params) {
4854
5635
  saveModelFor(applied.outcomes);
4855
5636
  const outro3 = exitCodeFor(applied.outcomes) === EXIT_CODE.ok ? "Done." : FINISHED_WITH_ERRORS2;
4856
5637
  const mode = { kind: "written", backupRoot: applied.backupRoot };
4857
- 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
+ });
4858
5646
  }
4859
5647
  function warnAboutUnsupported(selected, ui) {
4860
5648
  for (const { harness } of selected) {
@@ -4865,10 +5653,10 @@ function listFoundConfigurable({ harness, detection }) {
4865
5653
  if (harness.kind !== "configurable" || detection.kind !== "found") return [];
4866
5654
  return [harness];
4867
5655
  }
4868
- function endWithNothingFound(selected, ui, masker2) {
5656
+ function endWithNothingFound({ selected, ui, masker: masker2, hasInstallFailure }) {
4869
5657
  ui.print(formatDetection(selected, masker2));
4870
5658
  const hasFailedCheck = selected.some(({ harness, detection }) => isConfigurableUnchecked(harness, detection));
4871
- if (hasFailedCheck) {
5659
+ if (hasFailedCheck || hasInstallFailure) {
4872
5660
  ui.outro(FINISHED_WITH_ERRORS2);
4873
5661
  return EXIT_CODE.failed;
4874
5662
  }
@@ -4915,24 +5703,32 @@ function writeChanges(run, plan) {
4915
5703
  });
4916
5704
  const spinner = ui.spinner();
4917
5705
  spinner.start("Backing up originals and writing configs\u2026");
4918
- const applied = applyChanges({ plan, ctx, backupRoot });
4919
- 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));
4920
5708
  return applied;
4921
5709
  }
4922
- 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 }) {
4923
5716
  const { ui, ctx, masker: masker2 } = run;
4924
5717
  ui.print(
4925
5718
  formatResult({
4926
5719
  outcomes: plan.outcomes,
4927
5720
  envFile: plan.envFile,
5721
+ userEnvironment: plan.userEnvironment,
4928
5722
  mode,
5723
+ model,
5724
+ shell: run.shell,
4929
5725
  home: ctx.home,
4930
5726
  masker: masker2,
4931
5727
  runHints: listRunHints(plan.outcomes, ctx)
4932
5728
  })
4933
5729
  );
4934
5730
  ui.outro(outro3);
4935
- return exitCodeFor(plan.outcomes);
5731
+ return hasInstallFailure ? EXIT_CODE.failed : exitCodeFor(plan.outcomes);
4936
5732
  }
4937
5733
  function listRunHints(outcomes, ctx) {
4938
5734
  return outcomes.filter(isPlanned).flatMap(({ harness }) => ctx.hasBinary(harness.binary) ? [{ binary: harness.binary, label: harness.label }] : []);
@@ -4942,9 +5738,473 @@ function abort(ui) {
4942
5738
  return EXIT_CODE.cancelled;
4943
5739
  }
4944
5740
 
4945
- // src/harnesses/run-launch.ts
4946
- import fs10 from "fs";
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
4947
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
6206
+ import fs10 from "fs";
6207
+ import path24 from "path";
4948
6208
  var LAUNCH_DIRECTORY_PREFIX = "nexos-connect-";
4949
6209
  var STALE_DIRECTORY_PATTERN = new RegExp(`^${LAUNCH_DIRECTORY_PREFIX}(\\d+)-`);
4950
6210
  var OWNER_ONLY_MODE2 = 384;
@@ -4957,11 +6217,11 @@ function sweepStaleDirectories(deps) {
4957
6217
  return;
4958
6218
  }
4959
6219
  for (const name of names) {
4960
- const match27 = STALE_DIRECTORY_PATTERN.exec(name);
4961
- if (match27 === null) continue;
4962
- const directory = path23.join(root, name);
6220
+ const match29 = STALE_DIRECTORY_PATTERN.exec(name);
6221
+ if (match29 === null) continue;
6222
+ const directory = path24.join(root, name);
4963
6223
  try {
4964
- if (deps.isAlive(Number(match27[1]))) continue;
6224
+ if (deps.isAlive(Number(match29[1]))) continue;
4965
6225
  if (deps.uid !== void 0 && fs10.statSync(directory).uid !== deps.uid) continue;
4966
6226
  fs10.rmSync(directory, { recursive: true, force: true });
4967
6227
  } catch {
@@ -4978,15 +6238,11 @@ async function runLaunch(recipe, passthrough, deps) {
4978
6238
  };
4979
6239
  deps.signals.on("exit", cleanup);
4980
6240
  let commandArguments = recipe.arguments;
6241
+ let byPlaceholder = /* @__PURE__ */ new Map();
4981
6242
  if (recipe.files !== void 0 && recipe.files.length > 0) {
4982
6243
  try {
4983
- directory = fs10.mkdtempSync(path23.join(deps.tmpdir(), `${LAUNCH_DIRECTORY_PREFIX}${deps.pid}-`));
4984
- const byPlaceholder = /* @__PURE__ */ new Map();
4985
- for (const file of recipe.files) {
4986
- const target = path23.join(directory, file.name);
4987
- fs10.writeFileSync(target, file.content, { mode: OWNER_ONLY_MODE2, flag: "wx" });
4988
- byPlaceholder.set(file.placeholder, target);
4989
- }
6244
+ directory = fs10.mkdtempSync(path24.join(deps.tmpdir(), `${LAUNCH_DIRECTORY_PREFIX}${deps.pid}-`));
6245
+ byPlaceholder = writeLaunchFiles({ files: recipe.files, directory });
4990
6246
  commandArguments = commandArguments.map((argument) => byPlaceholder.get(argument) ?? argument);
4991
6247
  } catch (error) {
4992
6248
  cleanup();
@@ -5000,7 +6256,7 @@ async function runLaunch(recipe, passthrough, deps) {
5000
6256
  const env = { ...deps.env };
5001
6257
  for (const [name, value] of Object.entries(recipe.env)) {
5002
6258
  if (value === void 0) delete env[name];
5003
- else env[name] = value;
6259
+ else env[name] = byPlaceholder.get(value) ?? value;
5004
6260
  }
5005
6261
  if (deps.stdin.isTTY) deps.stdin.setRawMode(false);
5006
6262
  deps.stdin.pause();
@@ -5044,6 +6300,15 @@ async function runLaunch(recipe, passthrough, deps) {
5044
6300
  child.once("exit", (code, signal) => finish2(signal !== null ? { signal } : { code: code ?? 1 }));
5045
6301
  });
5046
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
+ }
5047
6312
  function joinArguments({ recipeArguments, subcommands, passthrough }) {
5048
6313
  const [first, ...rest] = passthrough;
5049
6314
  if (first !== void 0 && subcommands.includes(first)) return [first, ...recipeArguments, ...rest];
@@ -5137,9 +6402,18 @@ function createKeystore(deps) {
5137
6402
  };
5138
6403
  }
5139
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
+
5140
6414
  // src/ui/output.ts
5141
6415
  import * as clack2 from "@clack/prompts";
5142
- import pc10 from "picocolors";
6416
+ import pc12 from "picocolors";
5143
6417
  function intro2(message, io) {
5144
6418
  clack2.intro(message, { output: io.output });
5145
6419
  }
@@ -5161,8 +6435,13 @@ function printLines(lines, io) {
5161
6435
  io.output.write(`${lines.join("\n")}
5162
6436
  `);
5163
6437
  }
6438
+ function writeError(message, io) {
6439
+ const stream = io.errorOutput ?? io.output;
6440
+ stream.write(`${message}
6441
+ `);
6442
+ }
5164
6443
  function dim(message, io) {
5165
- clack2.log.message(pc10.dim(message), { output: io.output });
6444
+ clack2.log.message(pc12.dim(message), { output: io.output });
5166
6445
  }
5167
6446
  function note2(message, title, io) {
5168
6447
  clack2.note(message, title, { output: io.output });
@@ -5259,7 +6538,7 @@ async function chooseSavedKey(maskedKey, io) {
5259
6538
  // src/ui/prompts/model/ask-for-model-id.ts
5260
6539
  import * as clack9 from "@clack/prompts";
5261
6540
 
5262
- // src/ui/back-keys.ts
6541
+ // src/ui/terminal/back-keys.ts
5263
6542
  import * as clack8 from "@clack/prompts";
5264
6543
  var ESCAPE_KEY = "escape";
5265
6544
  var EDITING_BACK_KEYS = ["left", "backspace", "delete"];
@@ -5433,8 +6712,9 @@ async function selectModel(choices, io) {
5433
6712
  }
5434
6713
  }
5435
6714
  function formatHiddenModelsNote(hiddenCount) {
5436
- const subject = hiddenCount === 1 ? "it does" : "they do";
5437
- 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}.`;
5438
6718
  }
5439
6719
 
5440
6720
  // src/ui/prompts/select-harnesses.ts
@@ -5505,9 +6785,9 @@ async function selectScope({ userPaths, projectPaths }, io) {
5505
6785
  );
5506
6786
  }
5507
6787
 
5508
- // src/ui/spinner.ts
6788
+ // src/ui/terminal/spinner.ts
5509
6789
  import * as clack18 from "@clack/prompts";
5510
- import pc11 from "picocolors";
6790
+ import pc13 from "picocolors";
5511
6791
  function createSpinner(io) {
5512
6792
  const taskController = new AbortController();
5513
6793
  const cancelled = new Promise((_resolve, reject) => {
@@ -5540,7 +6820,7 @@ function createSpinner(io) {
5540
6820
  stop: (message) => {
5541
6821
  if (stopSpinning === null) return;
5542
6822
  end();
5543
- io.output.write(`${pc11.green(clack18.S_STEP_SUBMIT)} ${message}
6823
+ io.output.write(`${pc13.green(clack18.S_STEP_SUBMIT)} ${message}
5544
6824
  `);
5545
6825
  },
5546
6826
  waitFor,
@@ -5555,7 +6835,7 @@ function createSpinner(io) {
5555
6835
  };
5556
6836
  }
5557
6837
  function spin({ io, message, onCancel }) {
5558
- io.output.write(`${pc11.gray(clack18.S_BAR)}
6838
+ io.output.write(`${pc13.gray(clack18.S_BAR)}
5559
6839
  `);
5560
6840
  const stopAnimating = isTerminalOutput(io.output) ? animate(io.output, message) : () => {
5561
6841
  };
@@ -5575,7 +6855,7 @@ function animate(output, message) {
5575
6855
  const text2 = fitToWidth(message, output.columns);
5576
6856
  let frameIndex = 0;
5577
6857
  const drawFrame = () => {
5578
- output.write(`${CLEAR_LINE}${pc11.magenta(FRAMES[frameIndex % FRAMES.length] ?? "")} ${text2}`);
6858
+ output.write(`${CLEAR_LINE}${pc13.magenta(FRAMES[frameIndex % FRAMES.length] ?? "")} ${text2}`);
5579
6859
  frameIndex += 1;
5580
6860
  };
5581
6861
  output.write(HIDE_CURSOR);
@@ -5627,6 +6907,7 @@ function createTerminalUi(io) {
5627
6907
  cancel: (message) => cancel2(message, io),
5628
6908
  log: createLog(io),
5629
6909
  print: (lines) => printLines(lines, io),
6910
+ writeError: (message) => writeError(message, io),
5630
6911
  dim: (message) => dim(message, io),
5631
6912
  note: (message, title) => note2(message, title, io),
5632
6913
  spinner: () => createSpinner(io),
@@ -5654,11 +6935,12 @@ var keystore = createKeystore({
5654
6935
  importKeyring: () => import("@napi-rs/keyring"),
5655
6936
  platform: process.platform,
5656
6937
  isWsl: () => process.env.WSL_DISTRO_NAME !== void 0 || /microsoft/i.test(os.release()),
5657
- 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,
5658
6939
  isEnabled: process.env[KEYCHAIN_ENV_VARIABLE] !== KEYCHAIN_DISABLED,
5659
6940
  probeTimeoutMs: KEYCHAIN_PROBE_TIMEOUT_MS,
5660
6941
  interactionTimeoutMs: KEYCHAIN_INTERACTION_TIMEOUT_MS
5661
6942
  });
6943
+ var userEnvironment = createUserEnvironment({ platform: process.platform, runPowerShell });
5662
6944
  var runnerDeps = {
5663
6945
  spawn: (command, commandArguments, options) => crossSpawn(command, commandArguments, options),
5664
6946
  tmpdir: os.tmpdir,
@@ -5678,13 +6960,25 @@ function isProcessAlive(pid) {
5678
6960
  return error.code === "EPERM";
5679
6961
  }
5680
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
+ }
5681
6970
  async function main() {
5682
- 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
+ });
5683
6977
  try {
5684
6978
  return await runCommand(ui);
5685
6979
  } catch (error) {
5686
6980
  const failure = classifyFailure(error, masker);
5687
- if (failure.kind === "usage") console.error(pc12.red(failure.message));
6981
+ if (failure.kind === "usage") console.error(pc14.red(failure.message));
5688
6982
  if (failure.kind === "cancelled") ui.cancel("Cancelled.");
5689
6983
  if (failure.kind === "unexpected") ui.log.error(failure.message);
5690
6984
  return exitCodeForFailure(failure);
@@ -5700,8 +6994,8 @@ var HELP = renderHelp({
5700
6994
  predefinedModelIds: PREDEFINED_MODELS.map((model) => model.id),
5701
6995
  defaultModel: DEFAULT_MODEL,
5702
6996
  backupDirectories: {
5703
- user: path24.join("~", USER_BACKUP_DIRECTORY),
5704
- project: `.${path24.sep}${PROJECT_BACKUP_DIRECTORY}`
6997
+ user: path25.join("~", USER_BACKUP_DIRECTORY),
6998
+ project: `.${path25.sep}${PROJECT_BACKUP_DIRECTORY}`
5705
6999
  }
5706
7000
  });
5707
7001
  function isLaunchingHarness(harness) {
@@ -5715,7 +7009,7 @@ function describeUnknownAgent(binary) {
5715
7009
  if (isAgentName(binary)) return `Unknown agent "${binary}". ${oneOf}`;
5716
7010
  return `Unknown agent. ${oneOf}. To pass a key, use --key or set $${KEY_ENV_VARIABLE}.`;
5717
7011
  }
5718
- var PackageSchema = v15.object({ version: v15.string() });
7012
+ var PackageSchema = v16.object({ version: v16.string() });
5719
7013
  async function runCommand(ui) {
5720
7014
  const options = parseCliArguments(process.argv.slice(NODE_AND_SCRIPT_ARGUMENT_COUNT));
5721
7015
  if (options.shouldShowHelp) {
@@ -5723,20 +7017,24 @@ async function runCommand(ui) {
5723
7017
  return EXIT_CODE.ok;
5724
7018
  }
5725
7019
  if (options.shouldShowVersion) {
5726
- console.log(v15.parse(PackageSchema, createRequire(import.meta.url)("../package.json")).version);
7020
+ console.log(v16.parse(PackageSchema, createRequire(import.meta.url)("../package.json")).version);
5727
7021
  return EXIT_CODE.ok;
5728
7022
  }
5729
7023
  if (options.command.kind === "forget-key") return await forgetKey({ keystore, ui, masker });
5730
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
+ }
5731
7028
  const commandParams = {
5732
7029
  options,
5733
7030
  baseContext: createContext(),
5734
7031
  ui,
5735
7032
  now: () => /* @__PURE__ */ new Date(),
5736
- masker
7033
+ masker,
7034
+ userEnvironment
5737
7035
  };
5738
7036
  if (options.command.kind === "restore") return await runRestore(commandParams);
5739
- return await runSetup({ ...commandParams, fetch, keystore });
7037
+ return await runSetup({ ...commandParams, fetch, keystore, runNpm });
5740
7038
  }
5741
7039
  function createContext() {
5742
7040
  return createPlatformContext({
@@ -5751,14 +7049,14 @@ async function runLaunchCommand({ command, options, ui }) {
5751
7049
  const { binary, passthrough } = command;
5752
7050
  const harness = findHarnessByBinary(binary);
5753
7051
  if (harness === void 0) {
5754
- console.error(pc12.red(masker.maskText(describeUnknownAgent(binary))));
7052
+ console.error(pc14.red(masker.maskText(describeUnknownAgent(binary))));
5755
7053
  console.error(`
5756
7054
  ${HELP}`);
5757
7055
  return EXIT_CODE.usage;
5758
7056
  }
5759
7057
  if (!isLaunchingHarness(harness)) {
5760
7058
  const reason = harness.kind === "unsupported" ? `not configurable \u2014 ${harness.reason}` : "cannot be started this way";
5761
- console.error(pc12.red(`${harness.label}: ${reason}`));
7059
+ console.error(pc14.red(`${harness.label}: ${reason}`));
5762
7060
  return EXIT_CODE.usage;
5763
7061
  }
5764
7062
  const ctx = createContext();
@@ -5771,7 +7069,8 @@ ${HELP}`);
5771
7069
  masker,
5772
7070
  ctx,
5773
7071
  fetch,
5774
- runLaunch: (recipe, recipePassthrough) => runLaunch(recipe, recipePassthrough, runnerDeps)
7072
+ runLaunch: (recipe, recipePassthrough) => runLaunch(recipe, recipePassthrough, runnerDeps),
7073
+ runNpm
5775
7074
  }
5776
7075
  );
5777
7076
  if ("signal" in result) {
@@ -5785,7 +7084,7 @@ function finish(exitCode) {
5785
7084
  process.exitCode = exitCode;
5786
7085
  }
5787
7086
  void main().then(finish).catch((error) => {
5788
- console.error(pc12.red(masker.maskText(describeError(error))));
7087
+ console.error(pc14.red(masker.maskText(describeError(error))));
5789
7088
  finish(EXIT_CODE.failed);
5790
7089
  });
5791
7090
  //# sourceMappingURL=index.js.map