@newlandnpt/pep-cli 0.9.0-rc.4 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pep.cjs +311 -93
- package/package.json +1 -1
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
|
|
|
@@ -1310,13 +1388,18 @@ async function errorCodeOf(response) {
|
|
|
1310
1388
|
}
|
|
1311
1389
|
async function fetchNexusCredential(dependencies) {
|
|
1312
1390
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
1313
|
-
const response = await
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
+
);
|
|
1320
1403
|
if (!response.ok) {
|
|
1321
1404
|
throw new Error(describeFailure2(response.status, await errorCodeOf(response)));
|
|
1322
1405
|
}
|
|
@@ -1350,7 +1433,7 @@ async function setupNexusCredential(dependencies) {
|
|
|
1350
1433
|
var import_promises4 = require("node:fs/promises");
|
|
1351
1434
|
var import_node_os3 = require("node:os");
|
|
1352
1435
|
var import_node_path4 = require("node:path");
|
|
1353
|
-
var
|
|
1436
|
+
var import_node_crypto3 = require("node:crypto");
|
|
1354
1437
|
var import_node_zlib = require("node:zlib");
|
|
1355
1438
|
|
|
1356
1439
|
// src/tar.ts
|
|
@@ -1401,6 +1484,9 @@ function readTar(bytes) {
|
|
|
1401
1484
|
const size = readOctal(header, SIZE_OFFSET, SIZE_LENGTH);
|
|
1402
1485
|
const type = String.fromCharCode(header[TYPE_OFFSET]);
|
|
1403
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
|
+
}
|
|
1404
1490
|
const data = bytes.subarray(dataStart, dataStart + size);
|
|
1405
1491
|
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
|
|
1406
1492
|
if (type === "L") {
|
|
@@ -1438,6 +1524,16 @@ function tooBig(what, limit) {
|
|
|
1438
1524
|
);
|
|
1439
1525
|
}
|
|
1440
1526
|
var SKILL_MANIFEST = "SKILL.md";
|
|
1527
|
+
function occupiedByOutsider(skill, path) {
|
|
1528
|
+
return new Error(
|
|
1529
|
+
`${path} already exists and pep did not install it. Installing "${skill}" would delete it, so this repository was left alone. Move that directory aside (or delete it yourself), then run this again \u2014 pep never overwrites what it does not track.`
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
function nameClash(owner, skill, directory) {
|
|
1533
|
+
return new Error(
|
|
1534
|
+
`${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.`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1441
1537
|
function planSkillFiles(entries) {
|
|
1442
1538
|
const roots = [];
|
|
1443
1539
|
for (const entry of entries) {
|
|
@@ -1560,9 +1656,12 @@ async function listGroupRepos(dependencies) {
|
|
|
1560
1656
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
1561
1657
|
const url = new URL(`${dependencies.issuer.replace(/\/+$/, "")}${GROUP_PATH}`);
|
|
1562
1658
|
url.searchParams.set("path", dependencies.group);
|
|
1563
|
-
const response = await
|
|
1564
|
-
|
|
1565
|
-
|
|
1659
|
+
const response = await fetchOrExplain(
|
|
1660
|
+
doFetch,
|
|
1661
|
+
url,
|
|
1662
|
+
{ headers: { Authorization: `Bearer ${dependencies.accessToken}` } },
|
|
1663
|
+
"Could not list the skills group"
|
|
1664
|
+
);
|
|
1566
1665
|
if (!response.ok) {
|
|
1567
1666
|
await response.body?.cancel();
|
|
1568
1667
|
throw new Error(describeGroupFailure(response.status));
|
|
@@ -1570,8 +1669,8 @@ async function listGroupRepos(dependencies) {
|
|
|
1570
1669
|
const raw = await readCapped(response, dependencies.maxListBytes ?? MAX_GROUP_LIST_BYTES);
|
|
1571
1670
|
return parseGroupList(new TextDecoder().decode(raw));
|
|
1572
1671
|
}
|
|
1573
|
-
function
|
|
1574
|
-
let path =
|
|
1672
|
+
function repoIdentity(address) {
|
|
1673
|
+
let path = address.trim();
|
|
1575
1674
|
if (/^https?:\/\//i.test(path)) {
|
|
1576
1675
|
try {
|
|
1577
1676
|
path = new URL(path).pathname;
|
|
@@ -1579,13 +1678,22 @@ function groupMemberPath(ledgerKey, group) {
|
|
|
1579
1678
|
return void 0;
|
|
1580
1679
|
}
|
|
1581
1680
|
}
|
|
1582
|
-
path =
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
|
|
1681
|
+
path = path.split("@")[0] ?? "";
|
|
1682
|
+
const webRoute = path.indexOf("/-/");
|
|
1683
|
+
if (webRoute !== -1) path = path.slice(0, webRoute);
|
|
1684
|
+
path = path.replace(/\.git$/i, "");
|
|
1685
|
+
const segments = path.split("/").filter((one) => one !== "").map((one) => one.toLowerCase());
|
|
1686
|
+
return segments.length === 0 ? void 0 : segments.join("/");
|
|
1687
|
+
}
|
|
1688
|
+
function groupMemberPath(ledgerKey, group) {
|
|
1689
|
+
const path = repoIdentity(ledgerKey);
|
|
1690
|
+
const groupPath = repoIdentity(group);
|
|
1691
|
+
if (path === void 0 || groupPath === void 0) return void 0;
|
|
1692
|
+
const segments = path.split("/");
|
|
1693
|
+
const groupSegments = groupPath.split("/");
|
|
1694
|
+
if (segments.length <= groupSegments.length) return void 0;
|
|
1695
|
+
if (!groupSegments.every((one, index) => one === segments[index])) return void 0;
|
|
1696
|
+
return path;
|
|
1589
1697
|
}
|
|
1590
1698
|
async function physicalDir(path) {
|
|
1591
1699
|
const absolute = (0, import_node_path4.resolve)(path);
|
|
@@ -1613,7 +1721,7 @@ async function linkSkill(canonicalSkillDir, linkPath) {
|
|
|
1613
1721
|
}
|
|
1614
1722
|
}
|
|
1615
1723
|
function hashSkill(files) {
|
|
1616
|
-
const hash = (0,
|
|
1724
|
+
const hash = (0, import_node_crypto3.createHash)("sha256");
|
|
1617
1725
|
for (const file of [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
|
|
1618
1726
|
hash.update(file.path);
|
|
1619
1727
|
hash.update("\0");
|
|
@@ -1628,12 +1736,20 @@ function otherClaimant(packages, self, directory, skill) {
|
|
|
1628
1736
|
([other, pkg]) => other !== self && samePlace(pkg.directory, directory) && Object.keys(pkg.skills).some((one) => one.toLowerCase() === wanted)
|
|
1629
1737
|
)?.[0];
|
|
1630
1738
|
}
|
|
1739
|
+
function deletableLedgerDirectory(directory) {
|
|
1740
|
+
return isStableDirectory(directory) ? directory : void 0;
|
|
1741
|
+
}
|
|
1631
1742
|
async function removeSkills(name, store) {
|
|
1632
1743
|
const state = await store.read();
|
|
1633
1744
|
const pkg = state?.packages[name];
|
|
1634
1745
|
if (!state || !pkg) {
|
|
1635
1746
|
throw new Error(`Not added: ${name}. Run \`pep skills list\` to see what is.`);
|
|
1636
1747
|
}
|
|
1748
|
+
if (deletableLedgerDirectory(pkg.directory) === void 0) {
|
|
1749
|
+
throw new Error(
|
|
1750
|
+
`${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.`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1637
1753
|
const deleted = [];
|
|
1638
1754
|
const keptForOthers = [];
|
|
1639
1755
|
for (const skill of Object.keys(pkg.skills).sort()) {
|
|
@@ -1657,6 +1773,15 @@ function pickSkipped(previous, skipped) {
|
|
|
1657
1773
|
skipped.map((skill) => [skill, previous?.skills[skill] ?? ""])
|
|
1658
1774
|
);
|
|
1659
1775
|
}
|
|
1776
|
+
async function outsidersAt(directory, skills, ours) {
|
|
1777
|
+
const blocked = [];
|
|
1778
|
+
for (const skill of skills) {
|
|
1779
|
+
if (ours.has(skill)) continue;
|
|
1780
|
+
const path = (0, import_node_path4.join)(directory, skill);
|
|
1781
|
+
if (await (0, import_promises4.stat)(path).catch(() => null)) blocked.push({ skill, path });
|
|
1782
|
+
}
|
|
1783
|
+
return blocked;
|
|
1784
|
+
}
|
|
1660
1785
|
async function missingSkills(directory, skills) {
|
|
1661
1786
|
const gone = /* @__PURE__ */ new Set();
|
|
1662
1787
|
for (const skill of skills) {
|
|
@@ -1669,9 +1794,12 @@ async function updateSkills(dependencies) {
|
|
|
1669
1794
|
const name = dependencies.source;
|
|
1670
1795
|
const url = new URL(`${dependencies.issuer.replace(/\/+$/, "")}${ARCHIVE_PATH}`);
|
|
1671
1796
|
url.searchParams.set("source", name);
|
|
1672
|
-
const response = await
|
|
1673
|
-
|
|
1674
|
-
|
|
1797
|
+
const response = await fetchOrExplain(
|
|
1798
|
+
doFetch,
|
|
1799
|
+
url,
|
|
1800
|
+
{ headers: { Authorization: `Bearer ${dependencies.accessToken}` } },
|
|
1801
|
+
"Could not download the skills archive"
|
|
1802
|
+
);
|
|
1675
1803
|
if (!response.ok) {
|
|
1676
1804
|
await response.body?.cancel();
|
|
1677
1805
|
throw new Error(describeFailure3(response.status));
|
|
@@ -1681,15 +1809,28 @@ async function updateSkills(dependencies) {
|
|
|
1681
1809
|
const previous = state?.packages[name];
|
|
1682
1810
|
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));
|
|
1683
1811
|
const claimedElsewhere = (skill) => otherClaimant(state?.packages ?? {}, name, dependencies.directory, skill);
|
|
1684
|
-
const
|
|
1685
|
-
const
|
|
1812
|
+
const previousSkills = Object.keys(previous?.skills ?? {});
|
|
1813
|
+
const gone = previous !== void 0 && sameTarget ? await missingSkills(previous.directory, previousSkills) : /* @__PURE__ */ new Set();
|
|
1814
|
+
const linksGone = dependencies.restoreMissing && previous !== void 0 && sameTarget && previous.linkedInto !== void 0 && dependencies.linkInto !== void 0 ? await missingSkills(previous.linkedInto, previousSkills) : /* @__PURE__ */ new Set();
|
|
1815
|
+
const nothingToRestore = !dependencies.restoreMissing || gone.size === 0 && linksGone.size === 0;
|
|
1686
1816
|
const standing = Object.keys(previous?.skills ?? {}).flatMap((skill) => {
|
|
1687
1817
|
const owner = claimedElsewhere(skill);
|
|
1688
1818
|
return owner ? [{ skill, owner }] : [];
|
|
1689
1819
|
});
|
|
1820
|
+
if (dependencies.restoreMissing && standing.length > 0) {
|
|
1821
|
+
const [first] = standing;
|
|
1822
|
+
throw nameClash(first.owner, first.skill, dependencies.directory);
|
|
1823
|
+
}
|
|
1690
1824
|
if (commit !== void 0 && commit === previous?.commit && sameTarget && nothingToRestore) {
|
|
1691
1825
|
await response.body?.cancel();
|
|
1692
|
-
return {
|
|
1826
|
+
return {
|
|
1827
|
+
status: "unchanged",
|
|
1828
|
+
name,
|
|
1829
|
+
commit,
|
|
1830
|
+
conflicts: standing,
|
|
1831
|
+
skipped: [...gone],
|
|
1832
|
+
blocked: []
|
|
1833
|
+
};
|
|
1693
1834
|
}
|
|
1694
1835
|
const gzipped = await readCapped(response, dependencies.maxArchiveBytes ?? MAX_ARCHIVE_BYTES);
|
|
1695
1836
|
let unpacked;
|
|
@@ -1721,14 +1862,25 @@ async function updateSkills(dependencies) {
|
|
|
1721
1862
|
for (const skill of candidates) {
|
|
1722
1863
|
const owner = claimedElsewhere(skill);
|
|
1723
1864
|
if (owner === void 0) continue;
|
|
1724
|
-
if (dependencies.restoreMissing)
|
|
1725
|
-
throw new Error(
|
|
1726
|
-
`${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.`
|
|
1727
|
-
);
|
|
1728
|
-
}
|
|
1865
|
+
if (dependencies.restoreMissing) throw nameClash(owner, skill, dependencies.directory);
|
|
1729
1866
|
conflicts.push({ skill, owner });
|
|
1730
1867
|
}
|
|
1731
|
-
const
|
|
1868
|
+
const unclashed = candidates.filter((skill) => !conflicts.some((one) => one.skill === skill));
|
|
1869
|
+
const ours = new Set(
|
|
1870
|
+
sameTarget ? Object.keys(previous?.skills ?? {}) : []
|
|
1871
|
+
);
|
|
1872
|
+
const blocked = [];
|
|
1873
|
+
for (const one of [
|
|
1874
|
+
...await outsidersAt(dependencies.directory, unclashed, ours),
|
|
1875
|
+
...dependencies.linkInto !== void 0 ? await outsidersAt(dependencies.linkInto, unclashed, ours) : []
|
|
1876
|
+
]) {
|
|
1877
|
+
if (!blocked.some((seen) => seen.skill === one.skill)) blocked.push(one);
|
|
1878
|
+
}
|
|
1879
|
+
if (dependencies.restoreMissing && blocked.length > 0) {
|
|
1880
|
+
const [first] = blocked;
|
|
1881
|
+
throw occupiedByOutsider(first.skill, first.path);
|
|
1882
|
+
}
|
|
1883
|
+
const skills = unclashed.filter((skill) => !blocked.some((one) => one.skill === skill));
|
|
1732
1884
|
const hashes = {};
|
|
1733
1885
|
const updated = [];
|
|
1734
1886
|
const unchanged = [];
|
|
@@ -1745,7 +1897,9 @@ async function updateSkills(dependencies) {
|
|
|
1745
1897
|
const stale = sameTarget ? removed : Object.keys(previous.skills);
|
|
1746
1898
|
for (const skill of stale) {
|
|
1747
1899
|
if (otherClaimant(state?.packages ?? {}, name, previous.directory, skill)) continue;
|
|
1748
|
-
|
|
1900
|
+
const stalePlace = deletableLedgerDirectory(previous.directory);
|
|
1901
|
+
if (stalePlace === void 0) continue;
|
|
1902
|
+
await (0, import_promises4.rm)((0, import_node_path4.join)(stalePlace, skill), { recursive: true, force: true });
|
|
1749
1903
|
if (previous.linkedInto !== void 0) {
|
|
1750
1904
|
await (0, import_promises4.rm)((0, import_node_path4.join)(previous.linkedInto, skill), { recursive: true, force: true });
|
|
1751
1905
|
}
|
|
@@ -1754,7 +1908,11 @@ async function updateSkills(dependencies) {
|
|
|
1754
1908
|
for (const skill of skills) {
|
|
1755
1909
|
await (0, import_promises4.rm)((0, import_node_path4.join)(dependencies.directory, skill), { recursive: true, force: true });
|
|
1756
1910
|
}
|
|
1757
|
-
const untouched = /* @__PURE__ */ new Set([
|
|
1911
|
+
const untouched = /* @__PURE__ */ new Set([
|
|
1912
|
+
...skipped,
|
|
1913
|
+
...conflicts.map((one) => one.skill),
|
|
1914
|
+
...blocked.map((one) => one.skill)
|
|
1915
|
+
]);
|
|
1758
1916
|
for (const file of files) {
|
|
1759
1917
|
if (untouched.has(file.skill)) continue;
|
|
1760
1918
|
const target = (0, import_node_path4.join)(dependencies.directory, file.skill, ...file.path.split("/"));
|
|
@@ -1805,6 +1963,7 @@ async function updateSkills(dependencies) {
|
|
|
1805
1963
|
removed,
|
|
1806
1964
|
skipped,
|
|
1807
1965
|
conflicts,
|
|
1966
|
+
blocked,
|
|
1808
1967
|
directory: dependencies.directory,
|
|
1809
1968
|
...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
|
|
1810
1969
|
...copiedCount > 0 ? { copiedCount } : {},
|
|
@@ -1830,11 +1989,11 @@ function driftedFromGroup(installedSources, groupRepos, group) {
|
|
|
1830
1989
|
return member !== void 0 && !present.has(member);
|
|
1831
1990
|
}).sort();
|
|
1832
1991
|
}
|
|
1833
|
-
async function installGroup(dependencies,
|
|
1992
|
+
async function installGroup(dependencies, remoteNow) {
|
|
1834
1993
|
const group = dependencies.group;
|
|
1835
1994
|
let repos;
|
|
1836
1995
|
try {
|
|
1837
|
-
repos = await dependencies.listGroupRepos(
|
|
1996
|
+
repos = await dependencies.listGroupRepos(await remoteNow());
|
|
1838
1997
|
} catch (error) {
|
|
1839
1998
|
const message = messageOf(error);
|
|
1840
1999
|
dependencies.onProgress(` \u2717 ${message}`);
|
|
@@ -1859,7 +2018,7 @@ async function installGroup(dependencies, remote) {
|
|
|
1859
2018
|
for (const [index, source] of repos.entries()) {
|
|
1860
2019
|
dependencies.onProgress(` [${index + 1}/${repos.length}] ${source}`);
|
|
1861
2020
|
try {
|
|
1862
|
-
installed.push({ source, ...await dependencies.installSkills(
|
|
2021
|
+
installed.push({ source, ...await dependencies.installSkills(await remoteNow(), source) });
|
|
1863
2022
|
} catch (error) {
|
|
1864
2023
|
const message = messageOf(error);
|
|
1865
2024
|
failed.push({ source, message });
|
|
@@ -1899,15 +2058,17 @@ async function runSetup(dependencies) {
|
|
|
1899
2058
|
login = { alreadySignedIn: false, account };
|
|
1900
2059
|
dependencies.onProgress(` Signed in as ${account}.`);
|
|
1901
2060
|
}
|
|
1902
|
-
const
|
|
1903
|
-
|
|
2061
|
+
const remoteNow = async () => {
|
|
2062
|
+
const authorization = await dependencies.auth.currentAuthorization();
|
|
2063
|
+
return { issuer: authorization.issuer, accessToken: authorization.accessToken };
|
|
2064
|
+
};
|
|
1904
2065
|
let skills;
|
|
1905
2066
|
if (dependencies.skipSkills) {
|
|
1906
2067
|
skills = { status: "skipped", reason: "--skip-skills" };
|
|
1907
2068
|
dependencies.onProgress("\u2461 Installing skills\u2026 skipped (--skip-skills).");
|
|
1908
2069
|
} else {
|
|
1909
2070
|
dependencies.onProgress(`\u2461 Listing skills repositories in ${dependencies.group}\u2026`);
|
|
1910
|
-
skills = await installGroup(dependencies,
|
|
2071
|
+
skills = await installGroup(dependencies, remoteNow);
|
|
1911
2072
|
}
|
|
1912
2073
|
let nexus;
|
|
1913
2074
|
if (dependencies.skipNexus) {
|
|
@@ -1916,7 +2077,7 @@ async function runSetup(dependencies) {
|
|
|
1916
2077
|
} else {
|
|
1917
2078
|
dependencies.onProgress("\u2462 Setting up Maven credentials\u2026");
|
|
1918
2079
|
try {
|
|
1919
|
-
nexus = { status: "done", result: await dependencies.setupNexus(
|
|
2080
|
+
nexus = { status: "done", result: await dependencies.setupNexus(await remoteNow()) };
|
|
1920
2081
|
} catch (error) {
|
|
1921
2082
|
nexus = { status: "failed", message: messageOf(error) };
|
|
1922
2083
|
dependencies.onProgress(` \u2717 ${nexus.message}`);
|
|
@@ -1939,7 +2100,6 @@ function setupFailureSummary(report) {
|
|
|
1939
2100
|
async function planSetup(dependencies) {
|
|
1940
2101
|
const group = dependencies.group;
|
|
1941
2102
|
const nexus = dependencies.skipNexus ? "skipped" : "planned";
|
|
1942
|
-
const bySource = new Map(dependencies.installed.map((one) => [one.source, one]));
|
|
1943
2103
|
let current;
|
|
1944
2104
|
try {
|
|
1945
2105
|
current = await dependencies.auth.status();
|
|
@@ -1988,22 +2148,28 @@ async function planSetup(dependencies) {
|
|
|
1988
2148
|
} catch (error) {
|
|
1989
2149
|
return { ...plan, skills: { status: "unavailable", reason: messageOf(error) } };
|
|
1990
2150
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
path
|
|
1995
|
-
|
|
1996
|
-
directory:
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2151
|
+
let repositories;
|
|
2152
|
+
try {
|
|
2153
|
+
repositories = repos.map((path) => {
|
|
2154
|
+
const known = dependencies.recordFor(path);
|
|
2155
|
+
const directory = dependencies.directoryFor(path);
|
|
2156
|
+
return known === void 0 ? { path, state: "new", directory } : {
|
|
2157
|
+
path,
|
|
2158
|
+
state: "installed",
|
|
2159
|
+
directory,
|
|
2160
|
+
...known.commit !== void 0 ? { commit: known.commit } : {}
|
|
2161
|
+
};
|
|
2162
|
+
});
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
return { ...plan, skills: { status: "unavailable", reason: messageOf(error) } };
|
|
2165
|
+
}
|
|
2000
2166
|
const gone = new Set(driftedFromGroup(dependencies.installed.map((one) => one.source), repos, group));
|
|
2001
2167
|
const drifted = dependencies.installed.filter((one) => gone.has(one.source));
|
|
2002
2168
|
return { ...plan, skills: { status: "planned", repositories, drifted } };
|
|
2003
2169
|
}
|
|
2004
2170
|
|
|
2005
2171
|
// src/cli.ts
|
|
2006
|
-
var VERSION = false ? "0.0.0-dev" : "0.9.
|
|
2172
|
+
var VERSION = false ? "0.0.0-dev" : "0.9.1";
|
|
2007
2173
|
var SKILLS_ADD_DEPRECATION = "\u26A0 `pep skills add` will be removed in a future release.";
|
|
2008
2174
|
function reportUpdate(result) {
|
|
2009
2175
|
const conflicts = () => {
|
|
@@ -2012,9 +2178,22 @@ function reportUpdate(result) {
|
|
|
2012
2178
|
console.error(` each other. \`pep skills remove\` one of them to settle it.`);
|
|
2013
2179
|
}
|
|
2014
2180
|
};
|
|
2181
|
+
const blocked = () => {
|
|
2182
|
+
for (const { skill, path } of result.blocked) {
|
|
2183
|
+
console.error(` \u26A0 "${skill}" was not installed \u2014 ${path} already exists`);
|
|
2184
|
+
console.error(` and pep did not put it there. Move it aside, then run this again.`);
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
const skipped = (list, name) => {
|
|
2188
|
+
if (list.length === 0) return;
|
|
2189
|
+
console.log(` deleted locally, left alone: ${list.join(" ")}`);
|
|
2190
|
+
console.log(` (\`pep skills add ${name}\` puts them back)`);
|
|
2191
|
+
};
|
|
2015
2192
|
if (result.status === "unchanged") {
|
|
2016
2193
|
console.log(`${result.name}: already at ${result.commit}. Nothing to do.`);
|
|
2194
|
+
skipped(result.skipped, result.name);
|
|
2017
2195
|
conflicts();
|
|
2196
|
+
blocked();
|
|
2018
2197
|
return;
|
|
2019
2198
|
}
|
|
2020
2199
|
console.log(
|
|
@@ -2023,11 +2202,9 @@ function reportUpdate(result) {
|
|
|
2023
2202
|
if (result.unchanged.length > 0) {
|
|
2024
2203
|
console.log(` unchanged: ${result.unchanged.join(" ")}`);
|
|
2025
2204
|
}
|
|
2026
|
-
|
|
2027
|
-
console.log(` deleted locally, left alone: ${result.skipped.join(" ")}`);
|
|
2028
|
-
console.log(` (\`pep skills add ${result.name}\` puts them back)`);
|
|
2029
|
-
}
|
|
2205
|
+
skipped(result.skipped, result.name);
|
|
2030
2206
|
conflicts();
|
|
2207
|
+
blocked();
|
|
2031
2208
|
console.log(` ${result.fileCount} file(s) in ${result.directory}`);
|
|
2032
2209
|
if (result.linkedInto !== void 0) {
|
|
2033
2210
|
console.log(
|
|
@@ -2117,17 +2294,52 @@ function parseSkillsArgs(command, args, cwd) {
|
|
|
2117
2294
|
if (command !== "update" && args.length > 0) {
|
|
2118
2295
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
2119
2296
|
}
|
|
2120
|
-
const chosenTarget = explicitDirectory !== void 0 ?
|
|
2297
|
+
const chosenTarget = explicitDirectory !== void 0 ? (
|
|
2298
|
+
// ⚠ **立刻转成绝对路径**,相对路径绝不能进账本。
|
|
2299
|
+
// 账上记的落点会在**另一个工作目录**下被重新使用(`targetForSource` 的中间那一档):
|
|
2300
|
+
// A 项目里 `--dir ./skills` 存下字面量 `./skills`,到 B 项目再跑,它就指向 B 的
|
|
2301
|
+
// `./skills` —— 写进去是覆盖别人的文件,换落点清理和 `remove` 那两条路更是直接
|
|
2302
|
+
// `rm -rf` 到 B 头上。`--project` 没有这个问题,因为它走 `join(cwd, …)`。
|
|
2303
|
+
// 基准用 `cwd` 而不是 `process.cwd()`:与同一处的 `projectSkillsTarget(cwd)` 同源,
|
|
2304
|
+
// 也让这条分支可测。
|
|
2305
|
+
{ directory: (0, import_node_path5.resolve)(cwd ?? process.cwd(), explicitDirectory) }
|
|
2306
|
+
) : project ? projectSkillsTarget(cwd) : void 0;
|
|
2121
2307
|
return { ...requested !== void 0 ? { requested } : {}, named, ...chosenTarget ? { chosenTarget } : {} };
|
|
2122
2308
|
}
|
|
2123
|
-
function
|
|
2309
|
+
async function readLedger(store, skipSkills, warn) {
|
|
2310
|
+
try {
|
|
2311
|
+
return await store.read();
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
if (!skipSkills) throw error;
|
|
2314
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2315
|
+
warn(` \u26A0 Could not read the skills ledger (${detail}). Continuing because --skip-skills was given.`);
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
function targetForSource(chosen, recorded, source = "this repository") {
|
|
2124
2320
|
if (chosen) return chosen;
|
|
2125
2321
|
if (!recorded) return userSkillsTarget();
|
|
2322
|
+
if (!isStableDirectory(recorded.directory)) {
|
|
2323
|
+
throw new Error(
|
|
2324
|
+
`${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.`
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2126
2327
|
return {
|
|
2127
2328
|
directory: recorded.directory,
|
|
2128
2329
|
...recorded.linkedInto !== void 0 ? { linkInto: recorded.linkedInto } : {}
|
|
2129
2330
|
};
|
|
2130
2331
|
}
|
|
2332
|
+
function recordedFor(packages, source) {
|
|
2333
|
+
if (!packages) return void 0;
|
|
2334
|
+
const exact = packages[source];
|
|
2335
|
+
if (exact !== void 0) return { key: source, pkg: exact };
|
|
2336
|
+
const wanted = repoIdentity(source);
|
|
2337
|
+
if (wanted === void 0) return void 0;
|
|
2338
|
+
for (const [key, pkg] of Object.entries(packages)) {
|
|
2339
|
+
if (repoIdentity(key) === wanted) return { key, pkg };
|
|
2340
|
+
}
|
|
2341
|
+
return void 0;
|
|
2342
|
+
}
|
|
2131
2343
|
function nexusReport(result, printEnv) {
|
|
2132
2344
|
const stdout = printEnv ? [result.lines] : [];
|
|
2133
2345
|
const stderr = [];
|
|
@@ -2227,7 +2439,7 @@ async function main() {
|
|
|
2227
2439
|
const skillsGroup = requested ?? DEFAULT_SKILLS_GROUP;
|
|
2228
2440
|
const { configStore: configStore2, auth: auth2 } = createCliServices();
|
|
2229
2441
|
const stateStore = fileSkillsStateStore();
|
|
2230
|
-
const state = await stateStore.
|
|
2442
|
+
const state = await readLedger(stateStore, skipSkills, (line) => console.error(line));
|
|
2231
2443
|
const installed = Object.entries(state?.packages ?? {}).map(([source, pkg]) => ({
|
|
2232
2444
|
source,
|
|
2233
2445
|
directory: pkg.directory,
|
|
@@ -2245,7 +2457,11 @@ async function main() {
|
|
|
2245
2457
|
installed,
|
|
2246
2458
|
// ⚠ 与真跑**同一条推导**(`targetForSource`):干跑报的落点要是和真跑落的地方
|
|
2247
2459
|
// 不一样,那它比没有还坏。
|
|
2248
|
-
directoryFor: (source) => targetForSource(chosenTarget, state?.packages
|
|
2460
|
+
directoryFor: (source) => targetForSource(chosenTarget, recordedFor(state?.packages, source)?.pkg, source).directory,
|
|
2461
|
+
// ⚠ 与真跑**共用同一个查账函数**。干跑自己再建一份索引的话,两边的重复键策略会分叉
|
|
2462
|
+
// (Map 是后者覆盖,`recordedFor` 是精确优先),于是预览的落点与 commit 可能来自
|
|
2463
|
+
// 账上两条不同的记录。
|
|
2464
|
+
recordFor: (source) => recordedFor(state?.packages, source)?.pkg
|
|
2249
2465
|
});
|
|
2250
2466
|
console.log(JSON.stringify(plan, null, 2));
|
|
2251
2467
|
if (plan.skills.status === "unavailable") {
|
|
@@ -2264,10 +2480,12 @@ async function main() {
|
|
|
2264
2480
|
skipSkills,
|
|
2265
2481
|
listGroupRepos: (remote) => listGroupRepos({ ...remote, group: skillsGroup }),
|
|
2266
2482
|
installSkills: async (remote, source) => {
|
|
2267
|
-
const
|
|
2483
|
+
const recorded = recordedFor(state?.packages, source);
|
|
2484
|
+
const target = targetForSource(chosenTarget, recorded?.pkg, source);
|
|
2268
2485
|
const result = await updateSkills({
|
|
2269
2486
|
...remote,
|
|
2270
|
-
|
|
2487
|
+
// 原键还回去 —— 更新那一行,而不是按裸路径另起一条。
|
|
2488
|
+
source: recorded?.key ?? source,
|
|
2271
2489
|
directory: target.directory,
|
|
2272
2490
|
...target.linkInto !== void 0 ? { linkInto: target.linkInto } : {},
|
|
2273
2491
|
// `add` 那一档的语义:盘上被删掉的那些由它补回来(`update` 不管)。
|
|
@@ -2423,7 +2641,7 @@ ${usage(VERSION, ["skills"])}`);
|
|
|
2423
2641
|
console.error(SKILLS_ADD_DEPRECATION);
|
|
2424
2642
|
await run2(
|
|
2425
2643
|
requested,
|
|
2426
|
-
targetForSource(chosenTarget, state?.packages[requested]),
|
|
2644
|
+
targetForSource(chosenTarget, state?.packages[requested], requested),
|
|
2427
2645
|
true
|
|
2428
2646
|
);
|
|
2429
2647
|
return;
|
|
@@ -2454,7 +2672,7 @@ ${usage(VERSION, ["skills"])}`);
|
|
|
2454
2672
|
console.error(`Not installed in ${chosenTarget?.directory}, skipped: ${skipped.join(" ")}`);
|
|
2455
2673
|
}
|
|
2456
2674
|
for (const source of targets) {
|
|
2457
|
-
await run2(source, targetForSource(void 0, state?.packages[source]));
|
|
2675
|
+
await run2(source, targetForSource(void 0, state?.packages[source], source));
|
|
2458
2676
|
}
|
|
2459
2677
|
return;
|
|
2460
2678
|
}
|