@newlandnpt/pep-cli 0.4.1 → 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 +49 -3
  2. package/dist/pep.cjs +142 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -137,9 +137,55 @@ 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
- Deleting a skill folder by hand is fine — `pep skills update` notices it is missing and writes it
141
- again. It checks the disk, not just its own ledger, so it cannot report "already up to date" about
142
- files that are no longer there.
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
+
172
+ ### Deleting a skill
173
+
174
+ Delete a skill folder by hand and it **stays deleted**. `update` refreshes what is still on disk
175
+ and leaves the rest alone, naming them so the run is not silent:
176
+
177
+ ```
178
+ group/sub/project: updated b
179
+ deleted locally, left alone: a
180
+ (`pep skills add group/sub/project` puts them back)
181
+ ```
182
+
183
+ `add` is the command that installs, so that is what brings a deleted skill back. This split is
184
+ deliberate: `update` means "bring what I have up to date", not "refill everything the repository
185
+ offers".
186
+
187
+ Skills that are **new upstream** are still installed by `update` — the test is "was mine and is now
188
+ gone", not "is absent".
143
189
 
144
190
  `pep auth token` writes only the access token to stdout, so agents can use it without parsing status
145
191
  text. Refresh occurs automatically shortly before expiry and the rotated refresh token replaces the old
package/dist/pep.cjs CHANGED
@@ -1195,14 +1195,43 @@ function hashSkill(files) {
1195
1195
  }
1196
1196
  return hash.digest("hex").slice(0, 32);
1197
1197
  }
1198
- async function allPresent(directory, skills, linkedInto) {
1199
- for (const skill of skills) {
1200
- if (!await (0, import_promises4.stat)((0, import_node_path4.join)(directory, skill)).catch(() => null)) return false;
1201
- if (linkedInto !== void 0 && !await (0, import_promises4.stat)((0, import_node_path4.join)(linkedInto, skill)).catch(() => null)) {
1202
- return false;
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 });
1203
1217
  }
1218
+ deleted.push(skill);
1204
1219
  }
1205
- return true;
1220
+ const { [name]: _dropped, ...rest } = state.packages;
1221
+ await store.write({ version: 4, packages: rest });
1222
+ return { name, deleted, keptForOthers, directory: pkg.directory };
1223
+ }
1224
+ function pickSkipped(previous, skipped) {
1225
+ return Object.fromEntries(
1226
+ skipped.map((skill) => [skill, previous?.skills[skill] ?? ""])
1227
+ );
1228
+ }
1229
+ async function missingSkills(directory, skills) {
1230
+ const gone = /* @__PURE__ */ new Set();
1231
+ for (const skill of skills) {
1232
+ if (!await (0, import_promises4.stat)((0, import_node_path4.join)(directory, skill)).catch(() => null)) gone.add(skill);
1233
+ }
1234
+ return gone;
1206
1235
  }
1207
1236
  async function updateSkills(dependencies) {
1208
1237
  const doFetch = dependencies.fetch ?? globalThis.fetch;
@@ -1220,10 +1249,18 @@ async function updateSkills(dependencies) {
1220
1249
  const state = await dependencies.stateStore.read();
1221
1250
  const previous = state?.packages[name];
1222
1251
  const sameTarget = previous?.directory === dependencies.directory && previous?.linkedInto === dependencies.linkInto;
1223
- const intact = sameTarget && previous !== void 0 && await allPresent(previous.directory, Object.keys(previous.skills), previous.linkedInto);
1224
- if (commit !== void 0 && commit === previous?.commit && intact) {
1252
+ const claimedElsewhere = (skill) => Object.entries(state?.packages ?? {}).find(
1253
+ ([other, pkg]) => other !== name && pkg.directory === dependencies.directory && skill in pkg.skills
1254
+ )?.[0];
1255
+ const gone = previous !== void 0 && sameTarget ? await missingSkills(previous.directory, Object.keys(previous.skills)) : /* @__PURE__ */ new Set();
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
+ });
1261
+ if (commit !== void 0 && commit === previous?.commit && sameTarget && nothingToRestore) {
1225
1262
  await response.body?.cancel();
1226
- return { status: "unchanged", name, commit };
1263
+ return { status: "unchanged", name, commit, conflicts: standing };
1227
1264
  }
1228
1265
  const gzipped = new Uint8Array(await response.arrayBuffer());
1229
1266
  const files = planSkillFiles(readTar((0, import_node_zlib.gunzipSync)(gzipped)));
@@ -1233,7 +1270,9 @@ async function updateSkills(dependencies) {
1233
1270
  if (group) group.push(file);
1234
1271
  else bySkill.set(file.skill, [file]);
1235
1272
  }
1236
- const skills = [...bySkill.keys()].sort();
1273
+ const inArchive = [...bySkill.keys()].sort();
1274
+ const skipped = dependencies.restoreMissing ? [] : inArchive.filter((skill) => gone.has(skill));
1275
+ const skills = inArchive.filter((skill) => !skipped.includes(skill));
1237
1276
  const hashes = {};
1238
1277
  const updated = [];
1239
1278
  const unchanged = [];
@@ -1241,10 +1280,23 @@ async function updateSkills(dependencies) {
1241
1280
  const hash = hashSkill(bySkill.get(skill));
1242
1281
  hashes[skill] = hash;
1243
1282
  const before = previous?.skills[skill];
1244
- if (before !== void 0 && before !== "" && before === hash) unchanged.push(skill);
1283
+ if (gone.has(skill)) updated.push(skill);
1284
+ else if (before !== void 0 && before !== "" && before === hash) unchanged.push(skill);
1245
1285
  else updated.push(skill);
1246
1286
  }
1247
- const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !skills.includes(skill));
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
+ }
1299
+ const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !inArchive.includes(skill));
1248
1300
  if (previous !== void 0) {
1249
1301
  const stale = sameTarget ? removed : Object.keys(previous.skills);
1250
1302
  for (const skill of stale) {
@@ -1258,6 +1310,7 @@ async function updateSkills(dependencies) {
1258
1310
  await (0, import_promises4.rm)((0, import_node_path4.join)(dependencies.directory, skill), { recursive: true, force: true });
1259
1311
  }
1260
1312
  for (const file of files) {
1313
+ if (skipped.includes(file.skill)) continue;
1261
1314
  const target = (0, import_node_path4.join)(dependencies.directory, file.skill, ...file.path.split("/"));
1262
1315
  await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(target), { recursive: true });
1263
1316
  await (0, import_promises4.writeFile)(target, file.data);
@@ -1282,7 +1335,9 @@ async function updateSkills(dependencies) {
1282
1335
  ...commit ? { commit } : {},
1283
1336
  directory: dependencies.directory,
1284
1337
  ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
1285
- skills: hashes
1338
+ // 跳过的那些**原样留着上次的哈希** —— 账上仍然认它是这个包的(将来 add 能补回来,
1339
+ // 上游删掉时也还清得动),只是这次没去碰它。
1340
+ skills: { ...pickSkipped(previous, skipped), ...hashes }
1286
1341
  }
1287
1342
  }
1288
1343
  });
@@ -1292,8 +1347,10 @@ async function updateSkills(dependencies) {
1292
1347
  updated,
1293
1348
  unchanged,
1294
1349
  skills,
1295
- fileCount: files.length,
1350
+ fileCount: files.filter((one) => !skipped.includes(one.skill)).length,
1296
1351
  removed,
1352
+ skipped,
1353
+ conflicts,
1297
1354
  directory: dependencies.directory,
1298
1355
  ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
1299
1356
  ...copiedCount > 0 ? { copiedCount } : {},
@@ -1302,7 +1359,7 @@ async function updateSkills(dependencies) {
1302
1359
  }
1303
1360
 
1304
1361
  // src/cli.ts
1305
- var VERSION = false ? "0.0.0-dev" : "0.4.1";
1362
+ var VERSION = false ? "0.0.0-dev" : "0.6.0";
1306
1363
  function usage() {
1307
1364
  return `PEP CLI ${VERSION}
1308
1365
 
@@ -1314,6 +1371,7 @@ Usage:
1314
1371
  pep skills list
1315
1372
  pep skills add <repo-url | group/project[@ref]> [-p | --dir <path>]
1316
1373
  pep skills update [<repo>...] [-p | --dir <path>]
1374
+ pep skills remove <repo>
1317
1375
  pep docs list [--docs-url <url>]
1318
1376
  pep docs get <path> [--docs-url <url>]
1319
1377
  pep nexus setup
@@ -1358,8 +1416,18 @@ it already lives and never moves anything. On update, -p and --dir instead NARRO
1358
1416
  repositories installed there: \`pep skills update -p\` refreshes only this project's, and reports
1359
1417
  nothing to do when the project has none. To move a repository, add it again with the new flag \u2014
1360
1418
  add is where the location is decided, and the copy in the old location is then removed.
1361
- Deleting a skill folder by hand is fine: update notices it is gone and writes it again.
1362
- Either way update only touches skills it wrote itself; anything you put there by hand is left alone.
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
+
1427
+ Delete a skill folder by hand and update LEAVES IT DELETED \u2014 it refreshes what is still there and
1428
+ reports the ones it left alone. Run \`pep skills add <repo>\` to put them back; add is the command
1429
+ that installs. Either way update only touches skills it wrote itself; anything you put there by
1430
+ hand is left alone.
1363
1431
 
1364
1432
  \`pep docs list\` prints the documents this account can read (path + description); feed a path
1365
1433
  straight to \`pep docs get\` to print that document as markdown on stdout. The documentation
@@ -1376,8 +1444,15 @@ The password is shown by PEP once and never stored, so a second run reports 409
1376
1444
  handing it out again.`;
1377
1445
  }
1378
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
+ };
1379
1453
  if (result.status === "unchanged") {
1380
1454
  console.log(`${result.name}: already at ${result.commit}. Nothing to do.`);
1455
+ conflicts();
1381
1456
  return;
1382
1457
  }
1383
1458
  console.log(
@@ -1386,6 +1461,11 @@ function reportUpdate(result) {
1386
1461
  if (result.unchanged.length > 0) {
1387
1462
  console.log(` unchanged: ${result.unchanged.join(" ")}`);
1388
1463
  }
1464
+ if (result.skipped.length > 0) {
1465
+ console.log(` deleted locally, left alone: ${result.skipped.join(" ")}`);
1466
+ console.log(` (\`pep skills add ${result.name}\` puts them back)`);
1467
+ }
1468
+ conflicts();
1389
1469
  console.log(` ${result.fileCount} file(s) in ${result.directory}`);
1390
1470
  if (result.linkedInto !== void 0) {
1391
1471
  console.log(
@@ -1442,17 +1522,22 @@ async function loginConfig(args, store = fileConfigStore()) {
1442
1522
  };
1443
1523
  }
1444
1524
  function parseSkillsArgs(command, args, cwd) {
1445
- const explicitDirectory = command === "list" ? void 0 : option(args, "--dir");
1446
- 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");
1447
1528
  if (explicitDirectory !== void 0 && project) {
1448
1529
  throw new Error("--dir and --project/-p cannot be used together: they name different places.");
1449
1530
  }
1450
1531
  const unknownOption = args.find((one) => one.startsWith("-"));
1451
1532
  if (unknownOption) throw new Error(`Unknown option: ${unknownOption}`);
1452
- const requested = command === "add" ? args.shift() : void 0;
1533
+ const takesOne = command === "add" || command === "remove";
1534
+ const requested = takesOne ? args.shift() : void 0;
1453
1535
  if (command === "add" && !requested) {
1454
1536
  throw new Error("pep skills add needs a repository: a full https URL, or <group>/<project>.");
1455
1537
  }
1538
+ if (command === "remove" && !requested) {
1539
+ throw new Error("pep skills remove needs a repository. Run `pep skills list` to see them.");
1540
+ }
1456
1541
  const named = command === "update" ? [...args] : [];
1457
1542
  if (command !== "update" && args.length > 0) {
1458
1543
  throw new Error(`Unexpected argument: ${args[0]}`);
@@ -1552,16 +1637,15 @@ ${usage()}`);
1552
1637
  return;
1553
1638
  }
1554
1639
  if (group === "skills") {
1555
- if (command !== "update" && command !== "add" && command !== "list") {
1640
+ if (command !== "update" && command !== "add" && command !== "list" && command !== "remove") {
1556
1641
  throw new Error(`Unknown skills command.
1557
1642
 
1558
1643
  ${usage()}`);
1559
1644
  }
1560
1645
  const { requested, named, chosenTarget } = parseSkillsArgs(command, args);
1561
- const authorization = await auth.currentAuthorization();
1562
- const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
1646
+ const stateStore = fileSkillsStateStore();
1563
1647
  if (command === "list") {
1564
- const listed = await fileSkillsStateStore().read();
1648
+ const listed = await stateStore.read();
1565
1649
  const packages = Object.entries(listed?.packages ?? {}).sort(
1566
1650
  ([a], [b]) => a.localeCompare(b)
1567
1651
  );
@@ -1570,23 +1654,54 @@ ${usage()}`);
1570
1654
  return;
1571
1655
  }
1572
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
+ }
1573
1672
  console.error("\n`pep skills update` refreshes all of them, each where it lives.");
1574
1673
  return;
1575
1674
  }
1576
- 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 };
1577
1687
  const state = await stateStore.read();
1578
1688
  const installed = Object.keys(state?.packages ?? {}).sort();
1579
- const run2 = async (source, target) => reportUpdate(
1689
+ const run2 = async (source, target, restoreMissing = false) => reportUpdate(
1580
1690
  await updateSkills({
1581
1691
  ...remote,
1582
1692
  source,
1583
1693
  directory: target.directory,
1584
1694
  ...target.linkInto !== void 0 ? { linkInto: target.linkInto } : {},
1695
+ ...restoreMissing ? { restoreMissing: true } : {},
1585
1696
  stateStore
1586
1697
  })
1587
1698
  );
1588
1699
  if (command === "add") {
1589
- await run2(requested, targetForSource(chosenTarget, state?.packages[requested]));
1700
+ await run2(
1701
+ requested,
1702
+ targetForSource(chosenTarget, state?.packages[requested]),
1703
+ true
1704
+ );
1590
1705
  return;
1591
1706
  }
1592
1707
  if (installed.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newlandnpt/pep-cli",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",