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/README.md +44 -8
- package/dist/index.js +1732 -433
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
7
|
+
import path25 from "path";
|
|
8
8
|
import crossSpawn from "cross-spawn";
|
|
9
|
-
import
|
|
10
|
-
import * as
|
|
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
|
|
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
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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, --
|
|
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((
|
|
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((
|
|
418
|
+
return [...text2.matchAll(KEY_SHAPE_PATTERN)].map((match29) => toSpan(match29, maskKey(match29[0])));
|
|
400
419
|
}
|
|
401
|
-
function toSpan(
|
|
402
|
-
return { start:
|
|
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)
|
|
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/
|
|
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 +
|
|
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
|
|
1043
|
-
return
|
|
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) =>
|
|
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
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
|
1392
|
-
const entry = { path:
|
|
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,
|
|
1529
|
+
return [entry, ...listInBody(keyValue.value.body, path26)];
|
|
1395
1530
|
});
|
|
1396
1531
|
}
|
|
1397
|
-
function isSamePath(
|
|
1398
|
-
return
|
|
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) =>
|
|
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({
|
|
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) =>
|
|
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({
|
|
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({
|
|
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
|
|
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),
|
|
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
|
|
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 =
|
|
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(
|
|
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-
|
|
2827
|
-
function
|
|
2828
|
-
|
|
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,
|
|
2844
|
-
const
|
|
2845
|
-
const usable =
|
|
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
|
-
|
|
2910
|
-
|
|
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
|
|
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 {
|
|
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
|
|
3000
|
-
(signal) =>
|
|
3323
|
+
const probes = await spinner.waitFor(
|
|
3324
|
+
(signal) => Promise.all(protocols.map((protocol) => probeProtocol({ ...params, protocol, signal })))
|
|
3001
3325
|
);
|
|
3002
|
-
spinner.stop(
|
|
3003
|
-
return
|
|
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
|
|
3031
|
-
|
|
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
|
|
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
|
|
3468
|
+
({ model }) => !ownModels.has(model) && !isPredefinedModel(model) && isServed({ model, models })
|
|
3121
3469
|
);
|
|
3122
3470
|
}
|
|
3123
|
-
function isServed({ model, models
|
|
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 &&
|
|
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,
|
|
3214
|
-
const replaced = await replaceRejectedKey({ input: keyStep.input, rejected, probe
|
|
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,
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
3272
|
-
|
|
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
|
-
|
|
3312
|
-
|
|
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
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
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
|
|
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
|
|
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
|
|
3788
|
+
import { match as match20 } from "ts-pattern";
|
|
3407
3789
|
var CONFIG_FORMATS = ["jsonc", "toml"];
|
|
3408
3790
|
function renderConfig(current, edits) {
|
|
3409
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
3809
|
+
import { match as match21 } from "ts-pattern";
|
|
3428
3810
|
var OWNER_ONLY_MODE = 384;
|
|
3429
3811
|
function resolveOwnerOnlyMode(platform) {
|
|
3430
|
-
return
|
|
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 =
|
|
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:
|
|
3510
|
-
if (Object.keys(
|
|
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(
|
|
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
|
|
3897
|
+
import { match as match22 } from "ts-pattern";
|
|
3523
3898
|
var RESTORE_RECORD_FILE = "restore-record.json";
|
|
3524
3899
|
function resolveRestoreRecordLocation(ctx) {
|
|
3525
|
-
return
|
|
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
|
|
3931
|
+
// src/ui/report/preview/format-restore-preview.ts
|
|
3932
|
+
import pc6 from "picocolors";
|
|
3558
3933
|
|
|
3559
|
-
// src/ui/report/format-
|
|
3560
|
-
import
|
|
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
|
|
3622
|
-
import { match as
|
|
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 [
|
|
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(
|
|
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
|
|
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({
|
|
3661
|
-
|
|
3662
|
-
|
|
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, ` ${
|
|
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 ["", ` ${
|
|
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 ${
|
|
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
|
-
|
|
3684
|
-
|
|
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 ["", ` ${
|
|
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
|
|
3701
|
-
import { P as
|
|
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:
|
|
3726
|
-
unchanged: { glyph: GLYPHS.unchanged, colour:
|
|
3727
|
-
"nothing-to-restore": { glyph: GLYPHS.absent, colour:
|
|
3728
|
-
failed: { glyph: GLYPHS.failure, colour:
|
|
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 =
|
|
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}${
|
|
3740
|
-
...outcome.warnings.map((warning) => `${INDENT}${
|
|
3741
|
-
...outcome.kind === "restored" ? [`${INDENT}${
|
|
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"
|
|
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
|
-
` ${
|
|
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 ${
|
|
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(
|
|
3835
|
-
const harnesses2 = Object.fromEntries(Object.entries(
|
|
3836
|
-
return { ...
|
|
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(
|
|
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
|
|
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
|
|
4012
|
-
if (
|
|
4013
|
-
writeRestoreRecord({ filePath, record: removeHarnessRecords(
|
|
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
|
|
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:
|
|
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
|
|
4047
|
-
|
|
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:
|
|
4578
|
+
function listNamesRecordedElsewhere({ candidates, record: record9 }) {
|
|
4050
4579
|
const selectedIds = candidates.map(({ target }) => target.id);
|
|
4051
|
-
const others = Object.entries(
|
|
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
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
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(
|
|
4632
|
+
const { files, warnings } = planRecordedFiles(record9.files, params.ctx);
|
|
4063
4633
|
const hasEnvChange = hasOwnEnvChange({ envFileRead, envVariables, shell: params.shell });
|
|
4064
|
-
const
|
|
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 =
|
|
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:
|
|
4117
|
-
const recorded = Object.values(
|
|
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 [
|
|
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:
|
|
4165
|
-
|
|
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:
|
|
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:
|
|
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({
|
|
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
|
|
4210
|
-
return { kind: "read", record:
|
|
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:
|
|
4221
|
-
if (namedHarnesses.length > 0) return namedHarnesses.map((harness) => toCandidate(harness.id,
|
|
4222
|
-
const recordedIds = Object.keys(
|
|
4223
|
-
if (
|
|
4224
|
-
|
|
4225
|
-
|
|
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,
|
|
4228
|
-
return { target: { id, label: labelOf(id) }, record:
|
|
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:
|
|
4232
|
-
const harness =
|
|
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:
|
|
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:
|
|
4243
|
-
const plan = planRestore({ candidates, record:
|
|
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({
|
|
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(
|
|
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/
|
|
4274
|
-
|
|
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:
|
|
4284
|
-
"not-found": { glyph: GLYPHS.absent, colour:
|
|
4285
|
-
failed: { glyph: GLYPHS.failure, colour:
|
|
4286
|
-
"not-configurable": { glyph: GLYPHS.failure, colour:
|
|
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}${
|
|
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
|
|
4302
|
-
import { P as
|
|
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
|
|
4967
|
+
import pc9 from "picocolors";
|
|
4321
4968
|
var OUTCOME_STYLE = {
|
|
4322
|
-
updated: { glyph: GLYPHS.success, colour:
|
|
4323
|
-
unchanged: { glyph: GLYPHS.unchanged, colour:
|
|
4324
|
-
"not-found": { glyph: GLYPHS.absent, colour:
|
|
4325
|
-
unsupported: { glyph: GLYPHS.failure, colour:
|
|
4326
|
-
failed: { glyph: GLYPHS.failure, colour:
|
|
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 =
|
|
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}${
|
|
4350
|
-
...outcome.warnings.map((warning) => `${INDENT}${
|
|
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}${
|
|
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}${
|
|
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
|
-
|
|
4375
|
-
return (
|
|
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"
|
|
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}${
|
|
4387
|
-
` ${
|
|
4388
|
-
...
|
|
4389
|
-
` ${
|
|
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 ${
|
|
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
|
|
4409
|
-
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
|
4541
|
-
|
|
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
|
|
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:
|
|
4648
|
-
const harness = readHarnessRecord(
|
|
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(
|
|
5352
|
+
return withHarnessRecord(record9, harnessId, { ...harness, files: [...others, mergeFile(existing, file)] });
|
|
4652
5353
|
}
|
|
4653
|
-
function addEnvOriginals({ record:
|
|
4654
|
-
const harness = readHarnessRecord(
|
|
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(
|
|
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(
|
|
4661
|
-
return
|
|
5366
|
+
function readHarnessRecord(record9, harnessId) {
|
|
5367
|
+
return record9.harnesses[harnessId] ?? EMPTY_HARNESS_RECORD;
|
|
4662
5368
|
}
|
|
4663
|
-
function withHarnessRecord(
|
|
4664
|
-
return { ...
|
|
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(
|
|
4672
|
-
return
|
|
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, (
|
|
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
|
-
(
|
|
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
|
-
},
|
|
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
|
|
4707
|
-
writeRestoreRecord({ filePath: location.filePath, record: update(
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4795
|
-
|
|
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({
|
|
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(
|
|
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({
|
|
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({
|
|
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(
|
|
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
|
|
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/
|
|
4946
|
-
import
|
|
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
|
|
4961
|
-
if (
|
|
4962
|
-
const directory =
|
|
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(
|
|
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(
|
|
4984
|
-
|
|
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
|
|
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(
|
|
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
|
|
5437
|
-
|
|
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
|
|
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(`${
|
|
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(`${
|
|
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}${
|
|
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: () =>
|
|
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({
|
|
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(
|
|
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:
|
|
5704
|
-
project: `.${
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
7087
|
+
console.error(pc14.red(masker.maskText(describeError(error))));
|
|
5789
7088
|
finish(EXIT_CODE.failed);
|
|
5790
7089
|
});
|
|
5791
7090
|
//# sourceMappingURL=index.js.map
|