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