@newlandnpt/pep-cli 0.9.0-rc.3 → 0.9.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 +14 -0
- package/dist/pep.cjs +356 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,6 +77,20 @@ pep setup --skip-skills # only sign in and configure Maven
|
|
|
77
77
|
pep setup group/sub -p # a different group, into THIS project
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
### Seeing what a run would do
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
pep setup --json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Prints the plan and **installs nothing**: which repositories the group holds right now, which of them
|
|
87
|
+
are new, where each would land, and which entries on your list are no longer in the group — together
|
|
88
|
+
with the skill names those occupy, which is what makes one block another. Useful before a first run,
|
|
89
|
+
and when an install fails for a reason you would rather understand than guess at.
|
|
90
|
+
|
|
91
|
+
It never opens a browser: with no valid sign-in it reports `"login": "required"` and exits non-zero.
|
|
92
|
+
It does read the group, so it may refresh your token, exactly as `pep auth status` does.
|
|
93
|
+
|
|
80
94
|
`--issuer`, `--client-id` and `--resource` work here exactly as on `pep auth login`, for pointing a
|
|
81
95
|
run at another deployment:
|
|
82
96
|
|
package/dist/pep.cjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// src/auth-service.ts
|
|
5
5
|
var import_promises2 = require("node:fs/promises");
|
|
6
6
|
var import_node_path2 = require("node:path");
|
|
7
|
+
var import_node_crypto2 = require("node:crypto");
|
|
7
8
|
|
|
8
9
|
// src/config.ts
|
|
9
10
|
var import_promises = require("node:fs/promises");
|
|
@@ -53,8 +54,18 @@ function defaultSkillsDirectory() {
|
|
|
53
54
|
function claudeSkillsDirectory() {
|
|
54
55
|
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "skills");
|
|
55
56
|
}
|
|
57
|
+
function isStableDirectory(directory, api = {
|
|
58
|
+
isAbsolute: import_node_path.isAbsolute,
|
|
59
|
+
parse: import_node_path.parse,
|
|
60
|
+
sep: import_node_path.sep
|
|
61
|
+
}) {
|
|
62
|
+
if (!api.isAbsolute(directory)) return false;
|
|
63
|
+
const root = api.parse(directory).root;
|
|
64
|
+
if (api.sep === "/") return true;
|
|
65
|
+
return root.length > 1;
|
|
66
|
+
}
|
|
56
67
|
function samePlace(a, b) {
|
|
57
|
-
return a.toLowerCase() === b.toLowerCase();
|
|
68
|
+
return (0, import_node_path.resolve)(a).toLowerCase() === (0, import_node_path.resolve)(b).toLowerCase();
|
|
58
69
|
}
|
|
59
70
|
function userSkillsTarget() {
|
|
60
71
|
return { directory: defaultSkillsDirectory(), linkInto: claudeSkillsDirectory() };
|
|
@@ -294,12 +305,12 @@ async function waitForAuthorizationCallback(args) {
|
|
|
294
305
|
throw new Error("CLI redirect URI must be an http://localhost URL with an explicit port.");
|
|
295
306
|
}
|
|
296
307
|
let server;
|
|
297
|
-
return new Promise((
|
|
308
|
+
return new Promise((resolve4, reject) => {
|
|
298
309
|
const finish = (error, code) => {
|
|
299
310
|
clearTimeout(timeout);
|
|
300
311
|
server?.close();
|
|
301
312
|
if (error) reject(error);
|
|
302
|
-
else
|
|
313
|
+
else resolve4(code);
|
|
303
314
|
};
|
|
304
315
|
const timeout = setTimeout(
|
|
305
316
|
() => finish(new Error("Login timed out while waiting for the browser callback.")),
|
|
@@ -342,7 +353,7 @@ async function waitForAuthorizationCallback(args) {
|
|
|
342
353
|
function openBrowser(url) {
|
|
343
354
|
const command = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
344
355
|
const commandArgs = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
345
|
-
return new Promise((
|
|
356
|
+
return new Promise((resolve4, reject) => {
|
|
346
357
|
const child = (0, import_node_child_process.spawn)(command, commandArgs, {
|
|
347
358
|
detached: true,
|
|
348
359
|
stdio: "ignore",
|
|
@@ -351,7 +362,7 @@ function openBrowser(url) {
|
|
|
351
362
|
child.once("error", reject);
|
|
352
363
|
child.once("spawn", () => {
|
|
353
364
|
child.unref();
|
|
354
|
-
|
|
365
|
+
resolve4();
|
|
355
366
|
});
|
|
356
367
|
});
|
|
357
368
|
}
|
|
@@ -462,7 +473,15 @@ var REFRESH_EARLY_MS = 60 * 1e3;
|
|
|
462
473
|
var LOCK_STALE_MS = 30 * 1e3;
|
|
463
474
|
var LOCK_WAIT_MS = 10 * 1e3;
|
|
464
475
|
async function delay(milliseconds) {
|
|
465
|
-
await new Promise((
|
|
476
|
+
await new Promise((resolve4) => setTimeout(resolve4, milliseconds));
|
|
477
|
+
}
|
|
478
|
+
async function releaseIfMine(path, stamp2) {
|
|
479
|
+
try {
|
|
480
|
+
if (await (0, import_promises2.readFile)(path, "utf8") !== stamp2) return;
|
|
481
|
+
} catch {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
await (0, import_promises2.unlink)(path).catch(() => void 0);
|
|
466
485
|
}
|
|
467
486
|
async function withAuthorizationLock(path, action) {
|
|
468
487
|
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
@@ -470,11 +489,13 @@ async function withAuthorizationLock(path, action) {
|
|
|
470
489
|
while (true) {
|
|
471
490
|
try {
|
|
472
491
|
const handle = await (0, import_promises2.open)(path, "wx", 384);
|
|
492
|
+
const stamp2 = `${process.pid}:${(0, import_node_crypto2.randomUUID)()}`;
|
|
493
|
+
await handle.writeFile(stamp2, "utf8");
|
|
473
494
|
try {
|
|
474
495
|
return await action();
|
|
475
496
|
} finally {
|
|
476
497
|
await handle.close();
|
|
477
|
-
await (
|
|
498
|
+
await releaseIfMine(path, stamp2);
|
|
478
499
|
}
|
|
479
500
|
} catch (error) {
|
|
480
501
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -542,17 +563,22 @@ function createAuthService(dependencies) {
|
|
|
542
563
|
code,
|
|
543
564
|
pkce.verifier
|
|
544
565
|
);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
566
|
+
return withAuthorizationLock(
|
|
567
|
+
dependencies.authorizationLockPath ?? lockPath(),
|
|
568
|
+
async () => {
|
|
569
|
+
const previous = await dependencies.credentialStore.read();
|
|
570
|
+
await dependencies.credentialStore.write(authorization);
|
|
571
|
+
await dependencies.configStore.write(normalized);
|
|
572
|
+
if (previous) {
|
|
573
|
+
try {
|
|
574
|
+
const previousDiscovery = await dependencies.oauth.discover(previous.issuer);
|
|
575
|
+
await dependencies.oauth.revoke(previousDiscovery, previous);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return authorization;
|
|
553
580
|
}
|
|
554
|
-
|
|
555
|
-
return authorization;
|
|
581
|
+
);
|
|
556
582
|
},
|
|
557
583
|
currentAuthorization,
|
|
558
584
|
async status() {
|
|
@@ -581,6 +607,9 @@ function createAuthService(dependencies) {
|
|
|
581
607
|
};
|
|
582
608
|
}
|
|
583
609
|
|
|
610
|
+
// src/cli.ts
|
|
611
|
+
var import_node_path5 = require("node:path");
|
|
612
|
+
|
|
584
613
|
// src/credential-store.ts
|
|
585
614
|
var import_node_child_process2 = require("node:child_process");
|
|
586
615
|
var CREDENTIAL_TARGET = "PEP CLI OAuth Tokens";
|
|
@@ -656,7 +685,7 @@ function encodedPowerShell(script) {
|
|
|
656
685
|
return Buffer.from(script, "utf16le").toString("base64");
|
|
657
686
|
}
|
|
658
687
|
async function runPowerShell(script, input) {
|
|
659
|
-
return new Promise((
|
|
688
|
+
return new Promise((resolve4, reject) => {
|
|
660
689
|
const child = (0, import_node_child_process2.spawn)(
|
|
661
690
|
"powershell.exe",
|
|
662
691
|
[
|
|
@@ -679,7 +708,7 @@ async function runPowerShell(script, input) {
|
|
|
679
708
|
);
|
|
680
709
|
child.once("close", (code) => {
|
|
681
710
|
clearTimeout(timeout);
|
|
682
|
-
if (code === 0)
|
|
711
|
+
if (code === 0) resolve4(stdout.trim());
|
|
683
712
|
else
|
|
684
713
|
reject(
|
|
685
714
|
new Error(
|
|
@@ -700,14 +729,18 @@ function parseStoredAuthorization(raw) {
|
|
|
700
729
|
}
|
|
701
730
|
return candidate;
|
|
702
731
|
}
|
|
732
|
+
function psLiteral(value) {
|
|
733
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
734
|
+
}
|
|
703
735
|
function windowsCredentialStore(target = CREDENTIAL_TARGET) {
|
|
736
|
+
const quoted = psLiteral(target);
|
|
704
737
|
const preamble = `Add-Type -TypeDefinition @'
|
|
705
738
|
${WINDOWS_CREDENTIAL_NATIVE}
|
|
706
739
|
'@`;
|
|
707
740
|
return {
|
|
708
741
|
async read() {
|
|
709
742
|
const script = `${preamble}
|
|
710
|
-
$bytes = [PepCredential]::Read(
|
|
743
|
+
$bytes = [PepCredential]::Read(${quoted})
|
|
711
744
|
if ($null -ne $bytes) { [Convert]::ToBase64String($bytes) }`;
|
|
712
745
|
const encoded = await runPowerShell(script);
|
|
713
746
|
if (!encoded) return null;
|
|
@@ -718,18 +751,18 @@ if ($null -ne $bytes) { [Convert]::ToBase64String($bytes) }`;
|
|
|
718
751
|
async write(authorization) {
|
|
719
752
|
const script = `${preamble}
|
|
720
753
|
$raw = [Console]::In.ReadToEnd()
|
|
721
|
-
[PepCredential]::Write(
|
|
754
|
+
[PepCredential]::Write(${quoted}, 'oauth', [Text.Encoding]::UTF8.GetBytes($raw))`;
|
|
722
755
|
await runPowerShell(script, JSON.stringify(authorization));
|
|
723
756
|
},
|
|
724
757
|
async delete() {
|
|
725
758
|
await runPowerShell(`${preamble}
|
|
726
|
-
[PepCredential]::Delete(
|
|
759
|
+
[PepCredential]::Delete(${quoted})`);
|
|
727
760
|
}
|
|
728
761
|
};
|
|
729
762
|
}
|
|
730
763
|
var SECURITY_NOT_FOUND = 44;
|
|
731
764
|
async function runSecurity(args) {
|
|
732
|
-
return new Promise((
|
|
765
|
+
return new Promise((resolve4, reject) => {
|
|
733
766
|
const child = (0, import_node_child_process2.spawn)("/usr/bin/security", [...args], {
|
|
734
767
|
stdio: ["ignore", "pipe", "pipe"]
|
|
735
768
|
});
|
|
@@ -744,7 +777,7 @@ async function runSecurity(args) {
|
|
|
744
777
|
);
|
|
745
778
|
child.once("close", (code) => {
|
|
746
779
|
clearTimeout(timeout);
|
|
747
|
-
|
|
780
|
+
resolve4({ code: code ?? -1, stdout, stderr });
|
|
748
781
|
});
|
|
749
782
|
});
|
|
750
783
|
}
|
|
@@ -800,6 +833,45 @@ function systemCredentialStore() {
|
|
|
800
833
|
);
|
|
801
834
|
}
|
|
802
835
|
|
|
836
|
+
// src/http.ts
|
|
837
|
+
var HEADERS_TIMEOUT_MS = 15e3;
|
|
838
|
+
function explainNetworkFailure(error, what, url) {
|
|
839
|
+
const host = (() => {
|
|
840
|
+
try {
|
|
841
|
+
return new URL(url).host;
|
|
842
|
+
} catch {
|
|
843
|
+
return url;
|
|
844
|
+
}
|
|
845
|
+
})();
|
|
846
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
847
|
+
return new Error(
|
|
848
|
+
`${what}: ${host} did not respond within ${HEADERS_TIMEOUT_MS / 1e3}s. Check the network (VPN / proxy / firewall), then retry.`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const code = error?.cause?.code;
|
|
852
|
+
const hint = code === "ENOTFOUND" || code === "EAI_AGAIN" ? `${host} could not be resolved \u2014 check the address and your DNS.` : code === "ECONNREFUSED" ? `${host} refused the connection \u2014 it may be down, or you may need a VPN.` : code === "ECONNRESET" || code === "EPIPE" ? `the connection to ${host} was reset mid-flight. Retry; if it persists, suspect a proxy.` : typeof code === "string" && code.startsWith("CERT_") ? `${host} presented a certificate this machine does not trust (${code}).` : `could not reach ${host}${typeof code === "string" ? ` (${code})` : ""}.`;
|
|
853
|
+
return new Error(`${what}: ${hint}`);
|
|
854
|
+
}
|
|
855
|
+
async function fetchOrExplain(doFetch, url, init, what, timeoutMs = HEADERS_TIMEOUT_MS) {
|
|
856
|
+
const target = typeof url === "string" ? url : url.toString();
|
|
857
|
+
if (init.signal) {
|
|
858
|
+
try {
|
|
859
|
+
return await doFetch(url, init);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
throw explainNetworkFailure(error, what, target);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const controller = new AbortController();
|
|
865
|
+
const timer = setTimeout(() => controller.abort(new DOMException("timed out", "TimeoutError")), timeoutMs);
|
|
866
|
+
try {
|
|
867
|
+
return await doFetch(url, { ...init, signal: controller.signal });
|
|
868
|
+
} catch (error) {
|
|
869
|
+
throw explainNetworkFailure(error, what, target);
|
|
870
|
+
} finally {
|
|
871
|
+
clearTimeout(timer);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
803
875
|
// src/docs-service.ts
|
|
804
876
|
var ENTRY = /^-\s*\[([^\]]*)\]\(([^)]+)\)(?::\s*(.*))?$/;
|
|
805
877
|
function parseDocsIndex(text) {
|
|
@@ -845,9 +917,12 @@ function describeFailure(status, what) {
|
|
|
845
917
|
}
|
|
846
918
|
async function get(dependencies, path, accept) {
|
|
847
919
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
848
|
-
const response = await
|
|
849
|
-
|
|
850
|
-
|
|
920
|
+
const response = await fetchOrExplain(
|
|
921
|
+
doFetch,
|
|
922
|
+
`${dependencies.docsUrl.replace(/\/+$/, "")}${path}`,
|
|
923
|
+
{ headers: { Authorization: `Bearer ${dependencies.accessToken}`, Accept: accept } },
|
|
924
|
+
"Could not reach the documentation platform"
|
|
925
|
+
);
|
|
851
926
|
if (!response.ok) {
|
|
852
927
|
await response.body?.cancel();
|
|
853
928
|
throw new Error(describeFailure(response.status, path));
|
|
@@ -966,7 +1041,10 @@ async function persistMavenEnv(credential, target = resolveTarget(), now = () =>
|
|
|
966
1041
|
};
|
|
967
1042
|
}
|
|
968
1043
|
}
|
|
969
|
-
await (0, import_promises3.
|
|
1044
|
+
const originalMode = await (0, import_promises3.stat)(target.path).then((info) => info.mode & 511).catch(() => 384);
|
|
1045
|
+
const temporary = `${target.path}.${process.pid}.tmp`;
|
|
1046
|
+
await (0, import_promises3.writeFile)(temporary, plan.content, { encoding: "utf8", mode: originalMode });
|
|
1047
|
+
await (0, import_promises3.rename)(temporary, target.path);
|
|
970
1048
|
return plan.action === "replace" ? { status: "profile-replaced", path: target.path, backupPath } : { status: "profile-appended", path: target.path, ...backupPath ? { backupPath } : {} };
|
|
971
1049
|
}
|
|
972
1050
|
|
|
@@ -1176,6 +1254,13 @@ never deleted. \`pep skills remove <repo>\` drops one for good.
|
|
|
1176
1254
|
The group is baked into this build and is not remembered between runs: pass one
|
|
1177
1255
|
to install from a different group, just for this run.
|
|
1178
1256
|
|
|
1257
|
+
--json prints what this run would do \u2014 which repositories the group holds, which
|
|
1258
|
+
of them are new, where each would land, and which entries on your list are no
|
|
1259
|
+
longer in the group (with the skill names they occupy, which is what makes one
|
|
1260
|
+
block another). It installs nothing, writes nothing and never opens a browser.
|
|
1261
|
+
It does need a valid sign-in to read the group, so it may refresh your token,
|
|
1262
|
+
exactly as \`pep auth status\` does.
|
|
1263
|
+
|
|
1179
1264
|
--issuer, --client-id and --resource are passed to step 1, exactly as on
|
|
1180
1265
|
\`pep auth login\`, and are not remembered either. If you are already signed in to
|
|
1181
1266
|
a different deployment, setup signs in again rather than silently using the one
|
|
@@ -1188,13 +1273,15 @@ replaces it \u2014 run \`pep auth login\` to go back.`,
|
|
|
1188
1273
|
"--dir <path> Install skills into this directory instead",
|
|
1189
1274
|
`--issuer <url> PEP server for this run (default: ${DEFAULT_ISSUER})`,
|
|
1190
1275
|
"--client-id <id> Registered OAuth client for this run",
|
|
1276
|
+
"--json Print what this run would do, install nothing",
|
|
1191
1277
|
"--resource <uri> Token audience; repeat for multiple resources"
|
|
1192
1278
|
],
|
|
1193
1279
|
examples: [
|
|
1194
1280
|
"pep setup",
|
|
1195
1281
|
"pep setup --skip-nexus",
|
|
1196
1282
|
`pep setup ${DEFAULT_SKILLS_GROUP} -p`,
|
|
1197
|
-
"pep setup --issuer https://pep-dev.example --client-id <that deployment's id>"
|
|
1283
|
+
"pep setup --issuer https://pep-dev.example --client-id <that deployment's id>",
|
|
1284
|
+
"pep setup --json"
|
|
1198
1285
|
]
|
|
1199
1286
|
}
|
|
1200
1287
|
};
|
|
@@ -1301,13 +1388,18 @@ async function errorCodeOf(response) {
|
|
|
1301
1388
|
}
|
|
1302
1389
|
async function fetchNexusCredential(dependencies) {
|
|
1303
1390
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
1304
|
-
const response = await
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1391
|
+
const response = await fetchOrExplain(
|
|
1392
|
+
doFetch,
|
|
1393
|
+
`${dependencies.issuer.replace(/\/+$/, "")}${CREDENTIAL_PATH}`,
|
|
1394
|
+
{
|
|
1395
|
+
method: "POST",
|
|
1396
|
+
headers: {
|
|
1397
|
+
Authorization: `Bearer ${dependencies.accessToken}`,
|
|
1398
|
+
Accept: "application/json"
|
|
1399
|
+
}
|
|
1400
|
+
},
|
|
1401
|
+
"Could not reach PEP for Maven credentials"
|
|
1402
|
+
);
|
|
1311
1403
|
if (!response.ok) {
|
|
1312
1404
|
throw new Error(describeFailure2(response.status, await errorCodeOf(response)));
|
|
1313
1405
|
}
|
|
@@ -1341,7 +1433,7 @@ async function setupNexusCredential(dependencies) {
|
|
|
1341
1433
|
var import_promises4 = require("node:fs/promises");
|
|
1342
1434
|
var import_node_os3 = require("node:os");
|
|
1343
1435
|
var import_node_path4 = require("node:path");
|
|
1344
|
-
var
|
|
1436
|
+
var import_node_crypto3 = require("node:crypto");
|
|
1345
1437
|
var import_node_zlib = require("node:zlib");
|
|
1346
1438
|
|
|
1347
1439
|
// src/tar.ts
|
|
@@ -1392,6 +1484,9 @@ function readTar(bytes) {
|
|
|
1392
1484
|
const size = readOctal(header, SIZE_OFFSET, SIZE_LENGTH);
|
|
1393
1485
|
const type = String.fromCharCode(header[TYPE_OFFSET]);
|
|
1394
1486
|
const dataStart = offset + BLOCK;
|
|
1487
|
+
if (dataStart + size > bytes.length) {
|
|
1488
|
+
throw new Error("Malformed archive: an entry extends past the end of the stream.");
|
|
1489
|
+
}
|
|
1395
1490
|
const data = bytes.subarray(dataStart, dataStart + size);
|
|
1396
1491
|
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
|
|
1397
1492
|
if (type === "L") {
|
|
@@ -1429,6 +1524,11 @@ function tooBig(what, limit) {
|
|
|
1429
1524
|
);
|
|
1430
1525
|
}
|
|
1431
1526
|
var SKILL_MANIFEST = "SKILL.md";
|
|
1527
|
+
function nameClash(owner, skill, directory) {
|
|
1528
|
+
return new Error(
|
|
1529
|
+
`${owner} already installs a skill named "${skill}" in ${directory}. Two repositories cannot both provide "${skill}" there \u2014 the second would silently overwrite the first. Install this one elsewhere (-p, or --dir <path>), or run \`pep skills remove ${owner}\` first.`
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1432
1532
|
function planSkillFiles(entries) {
|
|
1433
1533
|
const roots = [];
|
|
1434
1534
|
for (const entry of entries) {
|
|
@@ -1551,9 +1651,12 @@ async function listGroupRepos(dependencies) {
|
|
|
1551
1651
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
1552
1652
|
const url = new URL(`${dependencies.issuer.replace(/\/+$/, "")}${GROUP_PATH}`);
|
|
1553
1653
|
url.searchParams.set("path", dependencies.group);
|
|
1554
|
-
const response = await
|
|
1555
|
-
|
|
1556
|
-
|
|
1654
|
+
const response = await fetchOrExplain(
|
|
1655
|
+
doFetch,
|
|
1656
|
+
url,
|
|
1657
|
+
{ headers: { Authorization: `Bearer ${dependencies.accessToken}` } },
|
|
1658
|
+
"Could not list the skills group"
|
|
1659
|
+
);
|
|
1557
1660
|
if (!response.ok) {
|
|
1558
1661
|
await response.body?.cancel();
|
|
1559
1662
|
throw new Error(describeGroupFailure(response.status));
|
|
@@ -1561,8 +1664,8 @@ async function listGroupRepos(dependencies) {
|
|
|
1561
1664
|
const raw = await readCapped(response, dependencies.maxListBytes ?? MAX_GROUP_LIST_BYTES);
|
|
1562
1665
|
return parseGroupList(new TextDecoder().decode(raw));
|
|
1563
1666
|
}
|
|
1564
|
-
function
|
|
1565
|
-
let path =
|
|
1667
|
+
function repoIdentity(address) {
|
|
1668
|
+
let path = address.trim();
|
|
1566
1669
|
if (/^https?:\/\//i.test(path)) {
|
|
1567
1670
|
try {
|
|
1568
1671
|
path = new URL(path).pathname;
|
|
@@ -1570,13 +1673,22 @@ function groupMemberPath(ledgerKey, group) {
|
|
|
1570
1673
|
return void 0;
|
|
1571
1674
|
}
|
|
1572
1675
|
}
|
|
1573
|
-
path =
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1676
|
+
path = path.split("@")[0] ?? "";
|
|
1677
|
+
const webRoute = path.indexOf("/-/");
|
|
1678
|
+
if (webRoute !== -1) path = path.slice(0, webRoute);
|
|
1679
|
+
path = path.replace(/\.git$/i, "");
|
|
1680
|
+
const segments = path.split("/").filter((one) => one !== "").map((one) => one.toLowerCase());
|
|
1681
|
+
return segments.length === 0 ? void 0 : segments.join("/");
|
|
1682
|
+
}
|
|
1683
|
+
function groupMemberPath(ledgerKey, group) {
|
|
1684
|
+
const path = repoIdentity(ledgerKey);
|
|
1685
|
+
const groupPath = repoIdentity(group);
|
|
1686
|
+
if (path === void 0 || groupPath === void 0) return void 0;
|
|
1687
|
+
const segments = path.split("/");
|
|
1688
|
+
const groupSegments = groupPath.split("/");
|
|
1689
|
+
if (segments.length <= groupSegments.length) return void 0;
|
|
1690
|
+
if (!groupSegments.every((one, index) => one === segments[index])) return void 0;
|
|
1691
|
+
return path;
|
|
1580
1692
|
}
|
|
1581
1693
|
async function physicalDir(path) {
|
|
1582
1694
|
const absolute = (0, import_node_path4.resolve)(path);
|
|
@@ -1604,7 +1716,7 @@ async function linkSkill(canonicalSkillDir, linkPath) {
|
|
|
1604
1716
|
}
|
|
1605
1717
|
}
|
|
1606
1718
|
function hashSkill(files) {
|
|
1607
|
-
const hash = (0,
|
|
1719
|
+
const hash = (0, import_node_crypto3.createHash)("sha256");
|
|
1608
1720
|
for (const file of [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
|
|
1609
1721
|
hash.update(file.path);
|
|
1610
1722
|
hash.update("\0");
|
|
@@ -1619,12 +1731,20 @@ function otherClaimant(packages, self, directory, skill) {
|
|
|
1619
1731
|
([other, pkg]) => other !== self && samePlace(pkg.directory, directory) && Object.keys(pkg.skills).some((one) => one.toLowerCase() === wanted)
|
|
1620
1732
|
)?.[0];
|
|
1621
1733
|
}
|
|
1734
|
+
function deletableLedgerDirectory(directory) {
|
|
1735
|
+
return isStableDirectory(directory) ? directory : void 0;
|
|
1736
|
+
}
|
|
1622
1737
|
async function removeSkills(name, store) {
|
|
1623
1738
|
const state = await store.read();
|
|
1624
1739
|
const pkg = state?.packages[name];
|
|
1625
1740
|
if (!state || !pkg) {
|
|
1626
1741
|
throw new Error(`Not added: ${name}. Run \`pep skills list\` to see what is.`);
|
|
1627
1742
|
}
|
|
1743
|
+
if (deletableLedgerDirectory(pkg.directory) === void 0) {
|
|
1744
|
+
throw new Error(
|
|
1745
|
+
`${name} was recorded with a relative install directory (${pkg.directory}) by an older pep-cli, so its real location is unknown \u2014 removing it here could delete files in the wrong project. Delete that directory yourself, then run \`pep skills add ${name}\` to re-record it with an absolute path.`
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1628
1748
|
const deleted = [];
|
|
1629
1749
|
const keptForOthers = [];
|
|
1630
1750
|
for (const skill of Object.keys(pkg.skills).sort()) {
|
|
@@ -1660,9 +1780,12 @@ async function updateSkills(dependencies) {
|
|
|
1660
1780
|
const name = dependencies.source;
|
|
1661
1781
|
const url = new URL(`${dependencies.issuer.replace(/\/+$/, "")}${ARCHIVE_PATH}`);
|
|
1662
1782
|
url.searchParams.set("source", name);
|
|
1663
|
-
const response = await
|
|
1664
|
-
|
|
1665
|
-
|
|
1783
|
+
const response = await fetchOrExplain(
|
|
1784
|
+
doFetch,
|
|
1785
|
+
url,
|
|
1786
|
+
{ headers: { Authorization: `Bearer ${dependencies.accessToken}` } },
|
|
1787
|
+
"Could not download the skills archive"
|
|
1788
|
+
);
|
|
1666
1789
|
if (!response.ok) {
|
|
1667
1790
|
await response.body?.cancel();
|
|
1668
1791
|
throw new Error(describeFailure3(response.status));
|
|
@@ -1672,15 +1795,21 @@ async function updateSkills(dependencies) {
|
|
|
1672
1795
|
const previous = state?.packages[name];
|
|
1673
1796
|
const sameTarget = previous !== void 0 && samePlace(previous.directory, dependencies.directory) && (previous.linkedInto === void 0 ? dependencies.linkInto === void 0 : dependencies.linkInto !== void 0 && samePlace(previous.linkedInto, dependencies.linkInto));
|
|
1674
1797
|
const claimedElsewhere = (skill) => otherClaimant(state?.packages ?? {}, name, dependencies.directory, skill);
|
|
1675
|
-
const
|
|
1676
|
-
const
|
|
1798
|
+
const previousSkills = Object.keys(previous?.skills ?? {});
|
|
1799
|
+
const gone = previous !== void 0 && sameTarget ? await missingSkills(previous.directory, previousSkills) : /* @__PURE__ */ new Set();
|
|
1800
|
+
const linksGone = dependencies.restoreMissing && previous !== void 0 && sameTarget && previous.linkedInto !== void 0 && dependencies.linkInto !== void 0 ? await missingSkills(previous.linkedInto, previousSkills) : /* @__PURE__ */ new Set();
|
|
1801
|
+
const nothingToRestore = !dependencies.restoreMissing || gone.size === 0 && linksGone.size === 0;
|
|
1677
1802
|
const standing = Object.keys(previous?.skills ?? {}).flatMap((skill) => {
|
|
1678
1803
|
const owner = claimedElsewhere(skill);
|
|
1679
1804
|
return owner ? [{ skill, owner }] : [];
|
|
1680
1805
|
});
|
|
1806
|
+
if (dependencies.restoreMissing && standing.length > 0) {
|
|
1807
|
+
const [first] = standing;
|
|
1808
|
+
throw nameClash(first.owner, first.skill, dependencies.directory);
|
|
1809
|
+
}
|
|
1681
1810
|
if (commit !== void 0 && commit === previous?.commit && sameTarget && nothingToRestore) {
|
|
1682
1811
|
await response.body?.cancel();
|
|
1683
|
-
return { status: "unchanged", name, commit, conflicts: standing };
|
|
1812
|
+
return { status: "unchanged", name, commit, conflicts: standing, skipped: [...gone] };
|
|
1684
1813
|
}
|
|
1685
1814
|
const gzipped = await readCapped(response, dependencies.maxArchiveBytes ?? MAX_ARCHIVE_BYTES);
|
|
1686
1815
|
let unpacked;
|
|
@@ -1712,11 +1841,7 @@ async function updateSkills(dependencies) {
|
|
|
1712
1841
|
for (const skill of candidates) {
|
|
1713
1842
|
const owner = claimedElsewhere(skill);
|
|
1714
1843
|
if (owner === void 0) continue;
|
|
1715
|
-
if (dependencies.restoreMissing)
|
|
1716
|
-
throw new Error(
|
|
1717
|
-
`${owner} already installs a skill named "${skill}" in ${dependencies.directory}. Two repositories cannot both provide "${skill}" there \u2014 the second would silently overwrite the first. Install this one elsewhere (-p, or --dir <path>), or run \`pep skills remove ${owner}\` first.`
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1844
|
+
if (dependencies.restoreMissing) throw nameClash(owner, skill, dependencies.directory);
|
|
1720
1845
|
conflicts.push({ skill, owner });
|
|
1721
1846
|
}
|
|
1722
1847
|
const skills = candidates.filter((skill) => !conflicts.some((one) => one.skill === skill));
|
|
@@ -1736,7 +1861,9 @@ async function updateSkills(dependencies) {
|
|
|
1736
1861
|
const stale = sameTarget ? removed : Object.keys(previous.skills);
|
|
1737
1862
|
for (const skill of stale) {
|
|
1738
1863
|
if (otherClaimant(state?.packages ?? {}, name, previous.directory, skill)) continue;
|
|
1739
|
-
|
|
1864
|
+
const stalePlace = deletableLedgerDirectory(previous.directory);
|
|
1865
|
+
if (stalePlace === void 0) continue;
|
|
1866
|
+
await (0, import_promises4.rm)((0, import_node_path4.join)(stalePlace, skill), { recursive: true, force: true });
|
|
1740
1867
|
if (previous.linkedInto !== void 0) {
|
|
1741
1868
|
await (0, import_promises4.rm)((0, import_node_path4.join)(previous.linkedInto, skill), { recursive: true, force: true });
|
|
1742
1869
|
}
|
|
@@ -1821,11 +1948,11 @@ function driftedFromGroup(installedSources, groupRepos, group) {
|
|
|
1821
1948
|
return member !== void 0 && !present.has(member);
|
|
1822
1949
|
}).sort();
|
|
1823
1950
|
}
|
|
1824
|
-
async function installGroup(dependencies,
|
|
1951
|
+
async function installGroup(dependencies, remoteNow) {
|
|
1825
1952
|
const group = dependencies.group;
|
|
1826
1953
|
let repos;
|
|
1827
1954
|
try {
|
|
1828
|
-
repos = await dependencies.listGroupRepos(
|
|
1955
|
+
repos = await dependencies.listGroupRepos(await remoteNow());
|
|
1829
1956
|
} catch (error) {
|
|
1830
1957
|
const message = messageOf(error);
|
|
1831
1958
|
dependencies.onProgress(` \u2717 ${message}`);
|
|
@@ -1834,7 +1961,11 @@ async function installGroup(dependencies, remote) {
|
|
|
1834
1961
|
if (repos.length === 0) {
|
|
1835
1962
|
dependencies.onProgress(` ${group} lists no repositories \u2014 nothing to install.`);
|
|
1836
1963
|
}
|
|
1837
|
-
const drifted = driftedFromGroup(
|
|
1964
|
+
const drifted = driftedFromGroup(
|
|
1965
|
+
dependencies.installed.map((one) => one.source),
|
|
1966
|
+
repos,
|
|
1967
|
+
group
|
|
1968
|
+
);
|
|
1838
1969
|
if (drifted.length > 0) {
|
|
1839
1970
|
dependencies.onProgress(` \u26A0 On your list but no longer in ${group}:`);
|
|
1840
1971
|
for (const source of drifted) dependencies.onProgress(` ${source}`);
|
|
@@ -1846,7 +1977,7 @@ async function installGroup(dependencies, remote) {
|
|
|
1846
1977
|
for (const [index, source] of repos.entries()) {
|
|
1847
1978
|
dependencies.onProgress(` [${index + 1}/${repos.length}] ${source}`);
|
|
1848
1979
|
try {
|
|
1849
|
-
installed.push({ source, ...await dependencies.installSkills(
|
|
1980
|
+
installed.push({ source, ...await dependencies.installSkills(await remoteNow(), source) });
|
|
1850
1981
|
} catch (error) {
|
|
1851
1982
|
const message = messageOf(error);
|
|
1852
1983
|
failed.push({ source, message });
|
|
@@ -1886,15 +2017,17 @@ async function runSetup(dependencies) {
|
|
|
1886
2017
|
login = { alreadySignedIn: false, account };
|
|
1887
2018
|
dependencies.onProgress(` Signed in as ${account}.`);
|
|
1888
2019
|
}
|
|
1889
|
-
const
|
|
1890
|
-
|
|
2020
|
+
const remoteNow = async () => {
|
|
2021
|
+
const authorization = await dependencies.auth.currentAuthorization();
|
|
2022
|
+
return { issuer: authorization.issuer, accessToken: authorization.accessToken };
|
|
2023
|
+
};
|
|
1891
2024
|
let skills;
|
|
1892
2025
|
if (dependencies.skipSkills) {
|
|
1893
2026
|
skills = { status: "skipped", reason: "--skip-skills" };
|
|
1894
2027
|
dependencies.onProgress("\u2461 Installing skills\u2026 skipped (--skip-skills).");
|
|
1895
2028
|
} else {
|
|
1896
2029
|
dependencies.onProgress(`\u2461 Listing skills repositories in ${dependencies.group}\u2026`);
|
|
1897
|
-
skills = await installGroup(dependencies,
|
|
2030
|
+
skills = await installGroup(dependencies, remoteNow);
|
|
1898
2031
|
}
|
|
1899
2032
|
let nexus;
|
|
1900
2033
|
if (dependencies.skipNexus) {
|
|
@@ -1903,7 +2036,7 @@ async function runSetup(dependencies) {
|
|
|
1903
2036
|
} else {
|
|
1904
2037
|
dependencies.onProgress("\u2462 Setting up Maven credentials\u2026");
|
|
1905
2038
|
try {
|
|
1906
|
-
nexus = { status: "done", result: await dependencies.setupNexus(
|
|
2039
|
+
nexus = { status: "done", result: await dependencies.setupNexus(await remoteNow()) };
|
|
1907
2040
|
} catch (error) {
|
|
1908
2041
|
nexus = { status: "failed", message: messageOf(error) };
|
|
1909
2042
|
dependencies.onProgress(` \u2717 ${nexus.message}`);
|
|
@@ -1923,9 +2056,79 @@ function setupFailureSummary(report) {
|
|
|
1923
2056
|
if (failed.length === 0) return void 0;
|
|
1924
2057
|
return `Setup finished with problems: ${failed.join(", ")} did not complete.`;
|
|
1925
2058
|
}
|
|
2059
|
+
async function planSetup(dependencies) {
|
|
2060
|
+
const group = dependencies.group;
|
|
2061
|
+
const nexus = dependencies.skipNexus ? "skipped" : "planned";
|
|
2062
|
+
let current;
|
|
2063
|
+
try {
|
|
2064
|
+
current = await dependencies.auth.status();
|
|
2065
|
+
} catch {
|
|
2066
|
+
}
|
|
2067
|
+
if (current === void 0) {
|
|
2068
|
+
return {
|
|
2069
|
+
group,
|
|
2070
|
+
issuer: null,
|
|
2071
|
+
account: null,
|
|
2072
|
+
login: "required",
|
|
2073
|
+
skills: { status: "unavailable", reason: "not signed in \u2014 run `pep auth login` first" },
|
|
2074
|
+
nexus
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
const issuer = current.authorization.issuer;
|
|
2078
|
+
if (dependencies.requestedIssuer !== void 0 && !sameIssuer(issuer, dependencies.requestedIssuer)) {
|
|
2079
|
+
return {
|
|
2080
|
+
group,
|
|
2081
|
+
issuer,
|
|
2082
|
+
account: accountOf(current.user),
|
|
2083
|
+
login: "different-deployment",
|
|
2084
|
+
skills: {
|
|
2085
|
+
status: "unavailable",
|
|
2086
|
+
reason: `signed in to ${issuer}, but --issuer names ${dependencies.requestedIssuer}; a real run would sign in again first`
|
|
2087
|
+
},
|
|
2088
|
+
nexus
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
const plan = {
|
|
2092
|
+
group,
|
|
2093
|
+
issuer,
|
|
2094
|
+
account: accountOf(current.user),
|
|
2095
|
+
login: "reuse",
|
|
2096
|
+
nexus
|
|
2097
|
+
};
|
|
2098
|
+
if (dependencies.skipSkills) {
|
|
2099
|
+
return { ...plan, skills: { status: "skipped", reason: "--skip-skills" } };
|
|
2100
|
+
}
|
|
2101
|
+
let repos;
|
|
2102
|
+
try {
|
|
2103
|
+
repos = await dependencies.listGroupRepos({
|
|
2104
|
+
issuer,
|
|
2105
|
+
accessToken: current.authorization.accessToken
|
|
2106
|
+
});
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
return { ...plan, skills: { status: "unavailable", reason: messageOf(error) } };
|
|
2109
|
+
}
|
|
2110
|
+
let repositories;
|
|
2111
|
+
try {
|
|
2112
|
+
repositories = repos.map((path) => {
|
|
2113
|
+
const known = dependencies.recordFor(path);
|
|
2114
|
+
const directory = dependencies.directoryFor(path);
|
|
2115
|
+
return known === void 0 ? { path, state: "new", directory } : {
|
|
2116
|
+
path,
|
|
2117
|
+
state: "installed",
|
|
2118
|
+
directory,
|
|
2119
|
+
...known.commit !== void 0 ? { commit: known.commit } : {}
|
|
2120
|
+
};
|
|
2121
|
+
});
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
return { ...plan, skills: { status: "unavailable", reason: messageOf(error) } };
|
|
2124
|
+
}
|
|
2125
|
+
const gone = new Set(driftedFromGroup(dependencies.installed.map((one) => one.source), repos, group));
|
|
2126
|
+
const drifted = dependencies.installed.filter((one) => gone.has(one.source));
|
|
2127
|
+
return { ...plan, skills: { status: "planned", repositories, drifted } };
|
|
2128
|
+
}
|
|
1926
2129
|
|
|
1927
2130
|
// src/cli.ts
|
|
1928
|
-
var VERSION = false ? "0.0.0-dev" : "0.9.0
|
|
2131
|
+
var VERSION = false ? "0.0.0-dev" : "0.9.0";
|
|
1929
2132
|
var SKILLS_ADD_DEPRECATION = "\u26A0 `pep skills add` will be removed in a future release.";
|
|
1930
2133
|
function reportUpdate(result) {
|
|
1931
2134
|
const conflicts = () => {
|
|
@@ -1934,8 +2137,14 @@ function reportUpdate(result) {
|
|
|
1934
2137
|
console.error(` each other. \`pep skills remove\` one of them to settle it.`);
|
|
1935
2138
|
}
|
|
1936
2139
|
};
|
|
2140
|
+
const skipped = (list, name) => {
|
|
2141
|
+
if (list.length === 0) return;
|
|
2142
|
+
console.log(` deleted locally, left alone: ${list.join(" ")}`);
|
|
2143
|
+
console.log(` (\`pep skills add ${name}\` puts them back)`);
|
|
2144
|
+
};
|
|
1937
2145
|
if (result.status === "unchanged") {
|
|
1938
2146
|
console.log(`${result.name}: already at ${result.commit}. Nothing to do.`);
|
|
2147
|
+
skipped(result.skipped, result.name);
|
|
1939
2148
|
conflicts();
|
|
1940
2149
|
return;
|
|
1941
2150
|
}
|
|
@@ -1945,10 +2154,7 @@ function reportUpdate(result) {
|
|
|
1945
2154
|
if (result.unchanged.length > 0) {
|
|
1946
2155
|
console.log(` unchanged: ${result.unchanged.join(" ")}`);
|
|
1947
2156
|
}
|
|
1948
|
-
|
|
1949
|
-
console.log(` deleted locally, left alone: ${result.skipped.join(" ")}`);
|
|
1950
|
-
console.log(` (\`pep skills add ${result.name}\` puts them back)`);
|
|
1951
|
-
}
|
|
2157
|
+
skipped(result.skipped, result.name);
|
|
1952
2158
|
conflicts();
|
|
1953
2159
|
console.log(` ${result.fileCount} file(s) in ${result.directory}`);
|
|
1954
2160
|
if (result.linkedInto !== void 0) {
|
|
@@ -2039,17 +2245,52 @@ function parseSkillsArgs(command, args, cwd) {
|
|
|
2039
2245
|
if (command !== "update" && args.length > 0) {
|
|
2040
2246
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
2041
2247
|
}
|
|
2042
|
-
const chosenTarget = explicitDirectory !== void 0 ?
|
|
2248
|
+
const chosenTarget = explicitDirectory !== void 0 ? (
|
|
2249
|
+
// ⚠ **立刻转成绝对路径**,相对路径绝不能进账本。
|
|
2250
|
+
// 账上记的落点会在**另一个工作目录**下被重新使用(`targetForSource` 的中间那一档):
|
|
2251
|
+
// A 项目里 `--dir ./skills` 存下字面量 `./skills`,到 B 项目再跑,它就指向 B 的
|
|
2252
|
+
// `./skills` —— 写进去是覆盖别人的文件,换落点清理和 `remove` 那两条路更是直接
|
|
2253
|
+
// `rm -rf` 到 B 头上。`--project` 没有这个问题,因为它走 `join(cwd, …)`。
|
|
2254
|
+
// 基准用 `cwd` 而不是 `process.cwd()`:与同一处的 `projectSkillsTarget(cwd)` 同源,
|
|
2255
|
+
// 也让这条分支可测。
|
|
2256
|
+
{ directory: (0, import_node_path5.resolve)(cwd ?? process.cwd(), explicitDirectory) }
|
|
2257
|
+
) : project ? projectSkillsTarget(cwd) : void 0;
|
|
2043
2258
|
return { ...requested !== void 0 ? { requested } : {}, named, ...chosenTarget ? { chosenTarget } : {} };
|
|
2044
2259
|
}
|
|
2045
|
-
function
|
|
2260
|
+
async function readLedger(store, skipSkills, warn) {
|
|
2261
|
+
try {
|
|
2262
|
+
return await store.read();
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
if (!skipSkills) throw error;
|
|
2265
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2266
|
+
warn(` \u26A0 Could not read the skills ledger (${detail}). Continuing because --skip-skills was given.`);
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
function targetForSource(chosen, recorded, source = "this repository") {
|
|
2046
2271
|
if (chosen) return chosen;
|
|
2047
2272
|
if (!recorded) return userSkillsTarget();
|
|
2273
|
+
if (!isStableDirectory(recorded.directory)) {
|
|
2274
|
+
throw new Error(
|
|
2275
|
+
`${source} was recorded with a relative install directory (${recorded.directory}) by an older pep-cli, so its real location depends on where you run this from \u2014 continuing could overwrite files in the current project. Pass --dir <absolute path> (or --project) to say where it belongs, or run \`pep skills remove\` on it first.`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2048
2278
|
return {
|
|
2049
2279
|
directory: recorded.directory,
|
|
2050
2280
|
...recorded.linkedInto !== void 0 ? { linkInto: recorded.linkedInto } : {}
|
|
2051
2281
|
};
|
|
2052
2282
|
}
|
|
2283
|
+
function recordedFor(packages, source) {
|
|
2284
|
+
if (!packages) return void 0;
|
|
2285
|
+
const exact = packages[source];
|
|
2286
|
+
if (exact !== void 0) return { key: source, pkg: exact };
|
|
2287
|
+
const wanted = repoIdentity(source);
|
|
2288
|
+
if (wanted === void 0) return void 0;
|
|
2289
|
+
for (const [key, pkg] of Object.entries(packages)) {
|
|
2290
|
+
if (repoIdentity(key) === wanted) return { key, pkg };
|
|
2291
|
+
}
|
|
2292
|
+
return void 0;
|
|
2293
|
+
}
|
|
2053
2294
|
function nexusReport(result, printEnv) {
|
|
2054
2295
|
const stdout = printEnv ? [result.lines] : [];
|
|
2055
2296
|
const stderr = [];
|
|
@@ -2144,11 +2385,41 @@ async function main() {
|
|
|
2144
2385
|
const skipNexus = flag(args, "--skip-nexus");
|
|
2145
2386
|
const skipSkills = flag(args, "--skip-skills");
|
|
2146
2387
|
const loginOptions = takeLoginOptions(args);
|
|
2388
|
+
const wantsJson = flag(args, "--json");
|
|
2147
2389
|
const { requested, chosenTarget } = parseSkillsArgs("setup", args);
|
|
2148
2390
|
const skillsGroup = requested ?? DEFAULT_SKILLS_GROUP;
|
|
2149
2391
|
const { configStore: configStore2, auth: auth2 } = createCliServices();
|
|
2150
2392
|
const stateStore = fileSkillsStateStore();
|
|
2151
|
-
const state = await stateStore.
|
|
2393
|
+
const state = await readLedger(stateStore, skipSkills, (line) => console.error(line));
|
|
2394
|
+
const installed = Object.entries(state?.packages ?? {}).map(([source, pkg]) => ({
|
|
2395
|
+
source,
|
|
2396
|
+
directory: pkg.directory,
|
|
2397
|
+
skills: Object.keys(pkg.skills).sort(),
|
|
2398
|
+
...pkg.commit !== void 0 ? { commit: pkg.commit } : {}
|
|
2399
|
+
}));
|
|
2400
|
+
if (wantsJson) {
|
|
2401
|
+
const plan = await planSetup({
|
|
2402
|
+
auth: auth2,
|
|
2403
|
+
...issuerOf(loginOptions) !== void 0 ? { requestedIssuer: issuerOf(loginOptions) } : {},
|
|
2404
|
+
group: skillsGroup,
|
|
2405
|
+
skipSkills,
|
|
2406
|
+
skipNexus,
|
|
2407
|
+
listGroupRepos: (remote) => listGroupRepos({ ...remote, group: skillsGroup }),
|
|
2408
|
+
installed,
|
|
2409
|
+
// ⚠ 与真跑**同一条推导**(`targetForSource`):干跑报的落点要是和真跑落的地方
|
|
2410
|
+
// 不一样,那它比没有还坏。
|
|
2411
|
+
directoryFor: (source) => targetForSource(chosenTarget, recordedFor(state?.packages, source)?.pkg, source).directory,
|
|
2412
|
+
// ⚠ 与真跑**共用同一个查账函数**。干跑自己再建一份索引的话,两边的重复键策略会分叉
|
|
2413
|
+
// (Map 是后者覆盖,`recordedFor` 是精确优先),于是预览的落点与 commit 可能来自
|
|
2414
|
+
// 账上两条不同的记录。
|
|
2415
|
+
recordFor: (source) => recordedFor(state?.packages, source)?.pkg
|
|
2416
|
+
});
|
|
2417
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
2418
|
+
if (plan.skills.status === "unavailable") {
|
|
2419
|
+
throw new Error(`--json could not plan the skills step: ${plan.skills.reason}`);
|
|
2420
|
+
}
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2152
2423
|
const report = await runSetup({
|
|
2153
2424
|
auth: auth2,
|
|
2154
2425
|
// ⚠ 传 `loginOptions` 而不是 `args`:后者到这里**恒为空**(`parseSkillsArgs` 要么抛、
|
|
@@ -2160,10 +2431,12 @@ async function main() {
|
|
|
2160
2431
|
skipSkills,
|
|
2161
2432
|
listGroupRepos: (remote) => listGroupRepos({ ...remote, group: skillsGroup }),
|
|
2162
2433
|
installSkills: async (remote, source) => {
|
|
2163
|
-
const
|
|
2434
|
+
const recorded = recordedFor(state?.packages, source);
|
|
2435
|
+
const target = targetForSource(chosenTarget, recorded?.pkg, source);
|
|
2164
2436
|
const result = await updateSkills({
|
|
2165
2437
|
...remote,
|
|
2166
|
-
|
|
2438
|
+
// 原键还回去 —— 更新那一行,而不是按裸路径另起一条。
|
|
2439
|
+
source: recorded?.key ?? source,
|
|
2167
2440
|
directory: target.directory,
|
|
2168
2441
|
...target.linkInto !== void 0 ? { linkInto: target.linkInto } : {},
|
|
2169
2442
|
// `add` 那一档的语义:盘上被删掉的那些由它补回来(`update` 不管)。
|
|
@@ -2174,7 +2447,7 @@ async function main() {
|
|
|
2174
2447
|
},
|
|
2175
2448
|
// 漂移比对的另一半。取**这一趟开始前**的账 —— 装完再读的话,本轮刚装上的那些也在
|
|
2176
2449
|
// 里头,而它们当然都还在群里,比出来一样,只是白读一遍。
|
|
2177
|
-
|
|
2450
|
+
installed,
|
|
2178
2451
|
skipNexus,
|
|
2179
2452
|
setupNexus: (remote) => setupNexusCredential(remote),
|
|
2180
2453
|
onProgress: (line) => console.error(line),
|
|
@@ -2319,7 +2592,7 @@ ${usage(VERSION, ["skills"])}`);
|
|
|
2319
2592
|
console.error(SKILLS_ADD_DEPRECATION);
|
|
2320
2593
|
await run2(
|
|
2321
2594
|
requested,
|
|
2322
|
-
targetForSource(chosenTarget, state?.packages[requested]),
|
|
2595
|
+
targetForSource(chosenTarget, state?.packages[requested], requested),
|
|
2323
2596
|
true
|
|
2324
2597
|
);
|
|
2325
2598
|
return;
|
|
@@ -2350,7 +2623,7 @@ ${usage(VERSION, ["skills"])}`);
|
|
|
2350
2623
|
console.error(`Not installed in ${chosenTarget?.directory}, skipped: ${skipped.join(" ")}`);
|
|
2351
2624
|
}
|
|
2352
2625
|
for (const source of targets) {
|
|
2353
|
-
await run2(source, targetForSource(void 0, state?.packages[source]));
|
|
2626
|
+
await run2(source, targetForSource(void 0, state?.packages[source], source));
|
|
2354
2627
|
}
|
|
2355
2628
|
return;
|
|
2356
2629
|
}
|