@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.
Files changed (3) hide show
  1. package/README.md +38 -0
  2. package/dist/pep.cjs +114 -13
  3. 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 skills = inArchive.filter((skill) => !skipped.includes(skill));
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 (skipped.includes(file.skill)) continue;
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
- skills: { ...pickSkipped(previous, skipped), ...hashes }
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.5.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 explicitDirectory = command === "list" ? void 0 : option(args, "--dir");
1463
- const project = command === "list" ? false : flag(args, "--project", "-p");
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 requested = command === "add" ? args.shift() : void 0;
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 authorization = await auth.currentAuthorization();
1579
- const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
1654
+ const stateStore = fileSkillsStateStore();
1580
1655
  if (command === "list") {
1581
- const listed = await fileSkillsStateStore().read();
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
- const stateStore = fileSkillsStateStore();
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newlandnpt/pep-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",