@newlandnpt/pep-cli 0.5.0 → 0.6.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/README.md +38 -0
- package/dist/pep.cjs +114 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,44 @@ pep skills add group/sub/project -p # move it into this project
|
|
|
137
137
|
`pep skills list` prints each repository with the directory it lives in, which is what `-p` matches
|
|
138
138
|
against.
|
|
139
139
|
|
|
140
|
+
### Removing a repository
|
|
141
|
+
|
|
142
|
+
```powershell
|
|
143
|
+
pep skills remove group/sub/project
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Deletes the skills that repository installed (in both the canonical directory and the linked one)
|
|
147
|
+
and drops it from the list. It only removes what it installed itself — anything you put there by
|
|
148
|
+
hand stays — and it needs no login, since nothing leaves the machine.
|
|
149
|
+
|
|
150
|
+
### Two repositories, one skill name
|
|
151
|
+
|
|
152
|
+
A skill's name is the folder name **inside** the repository, not the repository's own name. So two
|
|
153
|
+
different repositories that each ship a `semi-integration/SKILL.md` both want
|
|
154
|
+
`~/.agents/skills/semi-integration/`, and the second would silently overwrite the first.
|
|
155
|
+
|
|
156
|
+
`add` refuses, and the error names the repository already there — it will not pick a winner for
|
|
157
|
+
you. `update` never fails over it: the contested skill is left exactly as it is, everything else in
|
|
158
|
+
that repository still refreshes, and the clash is reported every run until you `remove` one. That
|
|
159
|
+
matters because a clash can appear **later**, when one repository adds a folder the other already
|
|
160
|
+
has — you did nothing, and one upstream mistake must not stop every other repository from
|
|
161
|
+
updating.
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
⚠ 3 repositories all install "semi-integration" here:
|
|
165
|
+
https://git.example.com/team-a/skill
|
|
166
|
+
https://git.example.com/team-b/skill-testing
|
|
167
|
+
They overwrite each other. `pep skills remove <repo>` keeps just one.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Installing them into **different** directories is fine — that is what `-p` is for.
|
|
171
|
+
|
|
172
|
+
A contested skill stays frozen at whichever copy is on disk until you remove one of the claimants.
|
|
173
|
+
That is deliberate: stable and loudly reported beats "whoever runs update last wins".
|
|
174
|
+
|
|
175
|
+
`remove` leaves a shared skill's files on disk (which copy it is can no longer be told) and says
|
|
176
|
+
so, rather than deleting what might be the other repository's content.
|
|
177
|
+
|
|
140
178
|
### Deleting a skill
|
|
141
179
|
|
|
142
180
|
Delete a skill folder by hand and it **stays deleted**. `update` refreshes what is still on disk
|
package/dist/pep.cjs
CHANGED
|
@@ -1195,6 +1195,32 @@ function hashSkill(files) {
|
|
|
1195
1195
|
}
|
|
1196
1196
|
return hash.digest("hex").slice(0, 32);
|
|
1197
1197
|
}
|
|
1198
|
+
async function removeSkills(name, store) {
|
|
1199
|
+
const state = await store.read();
|
|
1200
|
+
const pkg = state?.packages[name];
|
|
1201
|
+
if (!state || !pkg) {
|
|
1202
|
+
throw new Error(`Not added: ${name}. Run \`pep skills list\` to see what is.`);
|
|
1203
|
+
}
|
|
1204
|
+
const deleted = [];
|
|
1205
|
+
const keptForOthers = [];
|
|
1206
|
+
for (const skill of Object.keys(pkg.skills).sort()) {
|
|
1207
|
+
const owner = Object.entries(state.packages).find(
|
|
1208
|
+
([other, one]) => other !== name && one.directory === pkg.directory && skill in one.skills
|
|
1209
|
+
)?.[0];
|
|
1210
|
+
if (owner !== void 0) {
|
|
1211
|
+
keptForOthers.push({ skill, owner });
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
await (0, import_promises4.rm)((0, import_node_path4.join)(pkg.directory, skill), { recursive: true, force: true });
|
|
1215
|
+
if (pkg.linkedInto !== void 0) {
|
|
1216
|
+
await (0, import_promises4.rm)((0, import_node_path4.join)(pkg.linkedInto, skill), { recursive: true, force: true });
|
|
1217
|
+
}
|
|
1218
|
+
deleted.push(skill);
|
|
1219
|
+
}
|
|
1220
|
+
const { [name]: _dropped, ...rest } = state.packages;
|
|
1221
|
+
await store.write({ version: 4, packages: rest });
|
|
1222
|
+
return { name, deleted, keptForOthers, directory: pkg.directory };
|
|
1223
|
+
}
|
|
1198
1224
|
function pickSkipped(previous, skipped) {
|
|
1199
1225
|
return Object.fromEntries(
|
|
1200
1226
|
skipped.map((skill) => [skill, previous?.skills[skill] ?? ""])
|
|
@@ -1223,11 +1249,18 @@ async function updateSkills(dependencies) {
|
|
|
1223
1249
|
const state = await dependencies.stateStore.read();
|
|
1224
1250
|
const previous = state?.packages[name];
|
|
1225
1251
|
const sameTarget = previous?.directory === dependencies.directory && previous?.linkedInto === dependencies.linkInto;
|
|
1252
|
+
const claimedElsewhere = (skill) => Object.entries(state?.packages ?? {}).find(
|
|
1253
|
+
([other, pkg]) => other !== name && pkg.directory === dependencies.directory && skill in pkg.skills
|
|
1254
|
+
)?.[0];
|
|
1226
1255
|
const gone = previous !== void 0 && sameTarget ? await missingSkills(previous.directory, Object.keys(previous.skills)) : /* @__PURE__ */ new Set();
|
|
1227
1256
|
const nothingToRestore = !dependencies.restoreMissing || gone.size === 0;
|
|
1257
|
+
const standing = Object.keys(previous?.skills ?? {}).flatMap((skill) => {
|
|
1258
|
+
const owner = claimedElsewhere(skill);
|
|
1259
|
+
return owner ? [{ skill, owner }] : [];
|
|
1260
|
+
});
|
|
1228
1261
|
if (commit !== void 0 && commit === previous?.commit && sameTarget && nothingToRestore) {
|
|
1229
1262
|
await response.body?.cancel();
|
|
1230
|
-
return { status: "unchanged", name, commit };
|
|
1263
|
+
return { status: "unchanged", name, commit, conflicts: standing };
|
|
1231
1264
|
}
|
|
1232
1265
|
const gzipped = new Uint8Array(await response.arrayBuffer());
|
|
1233
1266
|
const files = planSkillFiles(readTar((0, import_node_zlib.gunzipSync)(gzipped)));
|
|
@@ -1239,7 +1272,19 @@ async function updateSkills(dependencies) {
|
|
|
1239
1272
|
}
|
|
1240
1273
|
const inArchive = [...bySkill.keys()].sort();
|
|
1241
1274
|
const skipped = dependencies.restoreMissing ? [] : inArchive.filter((skill) => gone.has(skill));
|
|
1242
|
-
const
|
|
1275
|
+
const candidates = inArchive.filter((skill) => !skipped.includes(skill));
|
|
1276
|
+
const conflicts = [];
|
|
1277
|
+
for (const skill of candidates) {
|
|
1278
|
+
const owner = claimedElsewhere(skill);
|
|
1279
|
+
if (owner === void 0) continue;
|
|
1280
|
+
if (dependencies.restoreMissing) {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
`${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.`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
conflicts.push({ skill, owner });
|
|
1286
|
+
}
|
|
1287
|
+
const skills = candidates.filter((skill) => !conflicts.some((one) => one.skill === skill));
|
|
1243
1288
|
const hashes = {};
|
|
1244
1289
|
const updated = [];
|
|
1245
1290
|
const unchanged = [];
|
|
@@ -1264,8 +1309,9 @@ async function updateSkills(dependencies) {
|
|
|
1264
1309
|
for (const skill of skills) {
|
|
1265
1310
|
await (0, import_promises4.rm)((0, import_node_path4.join)(dependencies.directory, skill), { recursive: true, force: true });
|
|
1266
1311
|
}
|
|
1312
|
+
const untouched = /* @__PURE__ */ new Set([...skipped, ...conflicts.map((one) => one.skill)]);
|
|
1267
1313
|
for (const file of files) {
|
|
1268
|
-
if (
|
|
1314
|
+
if (untouched.has(file.skill)) continue;
|
|
1269
1315
|
const target = (0, import_node_path4.join)(dependencies.directory, file.skill, ...file.path.split("/"));
|
|
1270
1316
|
await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(target), { recursive: true });
|
|
1271
1317
|
await (0, import_promises4.writeFile)(target, file.data);
|
|
@@ -1292,7 +1338,12 @@ async function updateSkills(dependencies) {
|
|
|
1292
1338
|
...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
|
|
1293
1339
|
// 跳过的那些**原样留着上次的哈希** —— 账上仍然认它是这个包的(将来 add 能补回来,
|
|
1294
1340
|
// 上游删掉时也还清得动),只是这次没去碰它。
|
|
1295
|
-
|
|
1341
|
+
// 跳过的与让开的都**原样留着上次的哈希** —— 账上仍然认它是这个包的(将来 add
|
|
1342
|
+
// 能补回来、`remove` 也还清得动),只是这次没去碰它。
|
|
1343
|
+
skills: {
|
|
1344
|
+
...pickSkipped(previous, [...skipped, ...conflicts.map((one) => one.skill)]),
|
|
1345
|
+
...hashes
|
|
1346
|
+
}
|
|
1296
1347
|
}
|
|
1297
1348
|
}
|
|
1298
1349
|
});
|
|
@@ -1305,6 +1356,7 @@ async function updateSkills(dependencies) {
|
|
|
1305
1356
|
fileCount: files.filter((one) => !skipped.includes(one.skill)).length,
|
|
1306
1357
|
removed,
|
|
1307
1358
|
skipped,
|
|
1359
|
+
conflicts,
|
|
1308
1360
|
directory: dependencies.directory,
|
|
1309
1361
|
...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
|
|
1310
1362
|
...copiedCount > 0 ? { copiedCount } : {},
|
|
@@ -1313,7 +1365,7 @@ async function updateSkills(dependencies) {
|
|
|
1313
1365
|
}
|
|
1314
1366
|
|
|
1315
1367
|
// src/cli.ts
|
|
1316
|
-
var VERSION = false ? "0.0.0-dev" : "0.
|
|
1368
|
+
var VERSION = false ? "0.0.0-dev" : "0.6.1";
|
|
1317
1369
|
function usage() {
|
|
1318
1370
|
return `PEP CLI ${VERSION}
|
|
1319
1371
|
|
|
@@ -1325,6 +1377,7 @@ Usage:
|
|
|
1325
1377
|
pep skills list
|
|
1326
1378
|
pep skills add <repo-url | group/project[@ref]> [-p | --dir <path>]
|
|
1327
1379
|
pep skills update [<repo>...] [-p | --dir <path>]
|
|
1380
|
+
pep skills remove <repo>
|
|
1328
1381
|
pep docs list [--docs-url <url>]
|
|
1329
1382
|
pep docs get <path> [--docs-url <url>]
|
|
1330
1383
|
pep nexus setup
|
|
@@ -1369,6 +1422,16 @@ it already lives and never moves anything. On update, -p and --dir instead NARRO
|
|
|
1369
1422
|
repositories installed there: \`pep skills update -p\` refreshes only this project's, and reports
|
|
1370
1423
|
nothing to do when the project has none. To move a repository, add it again with the new flag \u2014
|
|
1371
1424
|
add is where the location is decided, and the copy in the old location is then removed.
|
|
1425
|
+
\`pep skills remove <repo>\` deletes the skills that repository installed and drops it from the
|
|
1426
|
+
list. It only removes what it installed itself, and it needs no login. A skill that another
|
|
1427
|
+
repository also installs is left on disk (which copy it is can no longer be told) and reported.
|
|
1428
|
+
|
|
1429
|
+
Two repositories cannot both provide a skill with the SAME NAME in one directory \u2014 the skill name
|
|
1430
|
+
is the folder name inside the repository, so they would overwrite each other. add refuses, naming
|
|
1431
|
+
the repository already there. update never fails over it: it leaves that one skill untouched,
|
|
1432
|
+
refreshes everything else, and reports it every run until you remove one. Installing them into
|
|
1433
|
+
different directories (-p, or --dir) is fine.
|
|
1434
|
+
|
|
1372
1435
|
Delete a skill folder by hand and update LEAVES IT DELETED \u2014 it refreshes what is still there and
|
|
1373
1436
|
reports the ones it left alone. Run \`pep skills add <repo>\` to put them back; add is the command
|
|
1374
1437
|
that installs. Either way update only touches skills it wrote itself; anything you put there by
|
|
@@ -1389,8 +1452,15 @@ The password is shown by PEP once and never stored, so a second run reports 409
|
|
|
1389
1452
|
handing it out again.`;
|
|
1390
1453
|
}
|
|
1391
1454
|
function reportUpdate(result) {
|
|
1455
|
+
const conflicts = () => {
|
|
1456
|
+
for (const { skill, owner } of result.conflicts) {
|
|
1457
|
+
console.error(` \u26A0 "${skill}" is also installed here by ${owner} \u2014 they overwrite`);
|
|
1458
|
+
console.error(` each other. \`pep skills remove\` one of them to settle it.`);
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1392
1461
|
if (result.status === "unchanged") {
|
|
1393
1462
|
console.log(`${result.name}: already at ${result.commit}. Nothing to do.`);
|
|
1463
|
+
conflicts();
|
|
1394
1464
|
return;
|
|
1395
1465
|
}
|
|
1396
1466
|
console.log(
|
|
@@ -1403,6 +1473,7 @@ function reportUpdate(result) {
|
|
|
1403
1473
|
console.log(` deleted locally, left alone: ${result.skipped.join(" ")}`);
|
|
1404
1474
|
console.log(` (\`pep skills add ${result.name}\` puts them back)`);
|
|
1405
1475
|
}
|
|
1476
|
+
conflicts();
|
|
1406
1477
|
console.log(` ${result.fileCount} file(s) in ${result.directory}`);
|
|
1407
1478
|
if (result.linkedInto !== void 0) {
|
|
1408
1479
|
console.log(
|
|
@@ -1459,17 +1530,22 @@ async function loginConfig(args, store = fileConfigStore()) {
|
|
|
1459
1530
|
};
|
|
1460
1531
|
}
|
|
1461
1532
|
function parseSkillsArgs(command, args, cwd) {
|
|
1462
|
-
const
|
|
1463
|
-
const
|
|
1533
|
+
const local = command === "list" || command === "remove";
|
|
1534
|
+
const explicitDirectory = local ? void 0 : option(args, "--dir");
|
|
1535
|
+
const project = local ? false : flag(args, "--project", "-p");
|
|
1464
1536
|
if (explicitDirectory !== void 0 && project) {
|
|
1465
1537
|
throw new Error("--dir and --project/-p cannot be used together: they name different places.");
|
|
1466
1538
|
}
|
|
1467
1539
|
const unknownOption = args.find((one) => one.startsWith("-"));
|
|
1468
1540
|
if (unknownOption) throw new Error(`Unknown option: ${unknownOption}`);
|
|
1469
|
-
const
|
|
1541
|
+
const takesOne = command === "add" || command === "remove";
|
|
1542
|
+
const requested = takesOne ? args.shift() : void 0;
|
|
1470
1543
|
if (command === "add" && !requested) {
|
|
1471
1544
|
throw new Error("pep skills add needs a repository: a full https URL, or <group>/<project>.");
|
|
1472
1545
|
}
|
|
1546
|
+
if (command === "remove" && !requested) {
|
|
1547
|
+
throw new Error("pep skills remove needs a repository. Run `pep skills list` to see them.");
|
|
1548
|
+
}
|
|
1473
1549
|
const named = command === "update" ? [...args] : [];
|
|
1474
1550
|
if (command !== "update" && args.length > 0) {
|
|
1475
1551
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
@@ -1569,16 +1645,15 @@ ${usage()}`);
|
|
|
1569
1645
|
return;
|
|
1570
1646
|
}
|
|
1571
1647
|
if (group === "skills") {
|
|
1572
|
-
if (command !== "update" && command !== "add" && command !== "list") {
|
|
1648
|
+
if (command !== "update" && command !== "add" && command !== "list" && command !== "remove") {
|
|
1573
1649
|
throw new Error(`Unknown skills command.
|
|
1574
1650
|
|
|
1575
1651
|
${usage()}`);
|
|
1576
1652
|
}
|
|
1577
1653
|
const { requested, named, chosenTarget } = parseSkillsArgs(command, args);
|
|
1578
|
-
const
|
|
1579
|
-
const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
|
|
1654
|
+
const stateStore = fileSkillsStateStore();
|
|
1580
1655
|
if (command === "list") {
|
|
1581
|
-
const listed = await
|
|
1656
|
+
const listed = await stateStore.read();
|
|
1582
1657
|
const packages = Object.entries(listed?.packages ?? {}).sort(
|
|
1583
1658
|
([a], [b]) => a.localeCompare(b)
|
|
1584
1659
|
);
|
|
@@ -1587,10 +1662,36 @@ ${usage()}`);
|
|
|
1587
1662
|
return;
|
|
1588
1663
|
}
|
|
1589
1664
|
for (const [source, pkg] of packages) console.log(`${source} ${pkg.directory}`);
|
|
1665
|
+
const byPlace = /* @__PURE__ */ new Map();
|
|
1666
|
+
for (const [source, pkg] of packages) {
|
|
1667
|
+
for (const skill of Object.keys(pkg.skills)) {
|
|
1668
|
+
const key = `${pkg.directory}\0${skill}`;
|
|
1669
|
+
byPlace.set(key, [...byPlace.get(key) ?? [], source]);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
for (const [key, owners] of byPlace) {
|
|
1673
|
+
if (owners.length < 2) continue;
|
|
1674
|
+
const skill = key.split("\0")[1];
|
|
1675
|
+
console.error(`
|
|
1676
|
+
\u26A0 ${owners.length} repositories all install "${skill}" here:`);
|
|
1677
|
+
for (const one of owners) console.error(` ${one}`);
|
|
1678
|
+
console.error(" They overwrite each other. `pep skills remove <repo>` keeps just one.");
|
|
1679
|
+
}
|
|
1590
1680
|
console.error("\n`pep skills update` refreshes all of them, each where it lives.");
|
|
1591
1681
|
return;
|
|
1592
1682
|
}
|
|
1593
|
-
|
|
1683
|
+
if (command === "remove") {
|
|
1684
|
+
const result = await removeSkills(requested, stateStore);
|
|
1685
|
+
console.log(
|
|
1686
|
+
result.deleted.length > 0 ? `${result.name}: removed ${result.deleted.join(" ")} from ${result.directory}` : `${result.name}: nothing on disk to delete; dropped it from the list`
|
|
1687
|
+
);
|
|
1688
|
+
for (const { skill, owner } of result.keptForOthers) {
|
|
1689
|
+
console.error(` kept "${skill}" \u2014 ${owner} also installs it here`);
|
|
1690
|
+
}
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const authorization = await auth.currentAuthorization();
|
|
1694
|
+
const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
|
|
1594
1695
|
const state = await stateStore.read();
|
|
1595
1696
|
const installed = Object.keys(state?.packages ?? {}).sort();
|
|
1596
1697
|
const run2 = async (source, target, restoreMissing = false) => reportUpdate(
|