@newlandnpt/pep-cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +32 -0
  2. package/dist/pep.cjs +103 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -137,6 +137,38 @@ 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
+ Adding a second such repository is refused, and the error names the one already there. If you
157
+ already have a colliding pair from an older version, nothing breaks — every command reports it
158
+ until you `remove` one:
159
+
160
+ ```
161
+ ⚠ 3 repositories all install "semi-integration" here:
162
+ https://git.example.com/team-a/skill
163
+ https://git.example.com/team-b/skill-testing
164
+ They overwrite each other. `pep skills remove <repo>` keeps just one.
165
+ ```
166
+
167
+ Installing them into **different** directories is fine — that is what `-p` is for.
168
+
169
+ `remove` leaves a shared skill's files on disk (which copy it is can no longer be told) and says
170
+ so, rather than deleting what might be the other repository's content.
171
+
140
172
  ### Deleting a skill
141
173
 
142
174
  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)));
@@ -1251,6 +1284,18 @@ async function updateSkills(dependencies) {
1251
1284
  else if (before !== void 0 && before !== "" && before === hash) unchanged.push(skill);
1252
1285
  else updated.push(skill);
1253
1286
  }
1287
+ const conflicts = [];
1288
+ for (const skill of skills) {
1289
+ const owner = claimedElsewhere(skill);
1290
+ if (owner === void 0) continue;
1291
+ if (previous !== void 0 && skill in previous.skills) {
1292
+ conflicts.push({ skill, owner });
1293
+ continue;
1294
+ }
1295
+ throw new Error(
1296
+ `${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.`
1297
+ );
1298
+ }
1254
1299
  const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !inArchive.includes(skill));
1255
1300
  if (previous !== void 0) {
1256
1301
  const stale = sameTarget ? removed : Object.keys(previous.skills);
@@ -1305,6 +1350,7 @@ async function updateSkills(dependencies) {
1305
1350
  fileCount: files.filter((one) => !skipped.includes(one.skill)).length,
1306
1351
  removed,
1307
1352
  skipped,
1353
+ conflicts,
1308
1354
  directory: dependencies.directory,
1309
1355
  ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
1310
1356
  ...copiedCount > 0 ? { copiedCount } : {},
@@ -1313,7 +1359,7 @@ async function updateSkills(dependencies) {
1313
1359
  }
1314
1360
 
1315
1361
  // src/cli.ts
1316
- var VERSION = false ? "0.0.0-dev" : "0.5.0";
1362
+ var VERSION = false ? "0.0.0-dev" : "0.6.0";
1317
1363
  function usage() {
1318
1364
  return `PEP CLI ${VERSION}
1319
1365
 
@@ -1325,6 +1371,7 @@ Usage:
1325
1371
  pep skills list
1326
1372
  pep skills add <repo-url | group/project[@ref]> [-p | --dir <path>]
1327
1373
  pep skills update [<repo>...] [-p | --dir <path>]
1374
+ pep skills remove <repo>
1328
1375
  pep docs list [--docs-url <url>]
1329
1376
  pep docs get <path> [--docs-url <url>]
1330
1377
  pep nexus setup
@@ -1369,6 +1416,14 @@ it already lives and never moves anything. On update, -p and --dir instead NARRO
1369
1416
  repositories installed there: \`pep skills update -p\` refreshes only this project's, and reports
1370
1417
  nothing to do when the project has none. To move a repository, add it again with the new flag \u2014
1371
1418
  add is where the location is decided, and the copy in the old location is then removed.
1419
+ \`pep skills remove <repo>\` deletes the skills that repository installed and drops it from the
1420
+ list. It only removes what it installed itself, and it needs no login. A skill that another
1421
+ repository also installs is left on disk (which copy it is can no longer be told) and reported.
1422
+
1423
+ Two repositories cannot both provide a skill with the SAME NAME in one directory \u2014 the skill name
1424
+ is the folder name inside the repository, so they would overwrite each other. Adding a second one
1425
+ is refused; if you already have such a pair, every command says so until you remove one.
1426
+
1372
1427
  Delete a skill folder by hand and update LEAVES IT DELETED \u2014 it refreshes what is still there and
1373
1428
  reports the ones it left alone. Run \`pep skills add <repo>\` to put them back; add is the command
1374
1429
  that installs. Either way update only touches skills it wrote itself; anything you put there by
@@ -1389,8 +1444,15 @@ The password is shown by PEP once and never stored, so a second run reports 409
1389
1444
  handing it out again.`;
1390
1445
  }
1391
1446
  function reportUpdate(result) {
1447
+ const conflicts = () => {
1448
+ for (const { skill, owner } of result.conflicts) {
1449
+ console.error(` \u26A0 "${skill}" is also installed here by ${owner} \u2014 they overwrite`);
1450
+ console.error(` each other. \`pep skills remove\` one of them to settle it.`);
1451
+ }
1452
+ };
1392
1453
  if (result.status === "unchanged") {
1393
1454
  console.log(`${result.name}: already at ${result.commit}. Nothing to do.`);
1455
+ conflicts();
1394
1456
  return;
1395
1457
  }
1396
1458
  console.log(
@@ -1403,6 +1465,7 @@ function reportUpdate(result) {
1403
1465
  console.log(` deleted locally, left alone: ${result.skipped.join(" ")}`);
1404
1466
  console.log(` (\`pep skills add ${result.name}\` puts them back)`);
1405
1467
  }
1468
+ conflicts();
1406
1469
  console.log(` ${result.fileCount} file(s) in ${result.directory}`);
1407
1470
  if (result.linkedInto !== void 0) {
1408
1471
  console.log(
@@ -1459,17 +1522,22 @@ async function loginConfig(args, store = fileConfigStore()) {
1459
1522
  };
1460
1523
  }
1461
1524
  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");
1525
+ const local = command === "list" || command === "remove";
1526
+ const explicitDirectory = local ? void 0 : option(args, "--dir");
1527
+ const project = local ? false : flag(args, "--project", "-p");
1464
1528
  if (explicitDirectory !== void 0 && project) {
1465
1529
  throw new Error("--dir and --project/-p cannot be used together: they name different places.");
1466
1530
  }
1467
1531
  const unknownOption = args.find((one) => one.startsWith("-"));
1468
1532
  if (unknownOption) throw new Error(`Unknown option: ${unknownOption}`);
1469
- const requested = command === "add" ? args.shift() : void 0;
1533
+ const takesOne = command === "add" || command === "remove";
1534
+ const requested = takesOne ? args.shift() : void 0;
1470
1535
  if (command === "add" && !requested) {
1471
1536
  throw new Error("pep skills add needs a repository: a full https URL, or <group>/<project>.");
1472
1537
  }
1538
+ if (command === "remove" && !requested) {
1539
+ throw new Error("pep skills remove needs a repository. Run `pep skills list` to see them.");
1540
+ }
1473
1541
  const named = command === "update" ? [...args] : [];
1474
1542
  if (command !== "update" && args.length > 0) {
1475
1543
  throw new Error(`Unexpected argument: ${args[0]}`);
@@ -1569,16 +1637,15 @@ ${usage()}`);
1569
1637
  return;
1570
1638
  }
1571
1639
  if (group === "skills") {
1572
- if (command !== "update" && command !== "add" && command !== "list") {
1640
+ if (command !== "update" && command !== "add" && command !== "list" && command !== "remove") {
1573
1641
  throw new Error(`Unknown skills command.
1574
1642
 
1575
1643
  ${usage()}`);
1576
1644
  }
1577
1645
  const { requested, named, chosenTarget } = parseSkillsArgs(command, args);
1578
- const authorization = await auth.currentAuthorization();
1579
- const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
1646
+ const stateStore = fileSkillsStateStore();
1580
1647
  if (command === "list") {
1581
- const listed = await fileSkillsStateStore().read();
1648
+ const listed = await stateStore.read();
1582
1649
  const packages = Object.entries(listed?.packages ?? {}).sort(
1583
1650
  ([a], [b]) => a.localeCompare(b)
1584
1651
  );
@@ -1587,10 +1654,36 @@ ${usage()}`);
1587
1654
  return;
1588
1655
  }
1589
1656
  for (const [source, pkg] of packages) console.log(`${source} ${pkg.directory}`);
1657
+ const byPlace = /* @__PURE__ */ new Map();
1658
+ for (const [source, pkg] of packages) {
1659
+ for (const skill of Object.keys(pkg.skills)) {
1660
+ const key = `${pkg.directory}\0${skill}`;
1661
+ byPlace.set(key, [...byPlace.get(key) ?? [], source]);
1662
+ }
1663
+ }
1664
+ for (const [key, owners] of byPlace) {
1665
+ if (owners.length < 2) continue;
1666
+ const skill = key.split("\0")[1];
1667
+ console.error(`
1668
+ \u26A0 ${owners.length} repositories all install "${skill}" here:`);
1669
+ for (const one of owners) console.error(` ${one}`);
1670
+ console.error(" They overwrite each other. `pep skills remove <repo>` keeps just one.");
1671
+ }
1590
1672
  console.error("\n`pep skills update` refreshes all of them, each where it lives.");
1591
1673
  return;
1592
1674
  }
1593
- const stateStore = fileSkillsStateStore();
1675
+ if (command === "remove") {
1676
+ const result = await removeSkills(requested, stateStore);
1677
+ console.log(
1678
+ 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`
1679
+ );
1680
+ for (const { skill, owner } of result.keptForOthers) {
1681
+ console.error(` kept "${skill}" \u2014 ${owner} also installs it here`);
1682
+ }
1683
+ return;
1684
+ }
1685
+ const authorization = await auth.currentAuthorization();
1686
+ const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
1594
1687
  const state = await stateStore.read();
1595
1688
  const installed = Object.keys(state?.packages ?? {}).sort();
1596
1689
  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.0",
4
4
  "description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",