@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.
- package/README.md +49 -3
- package/dist/pep.cjs +142 -27
- 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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
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
|
|
1224
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1362
|
-
|
|
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
|
|
1446
|
-
const
|
|
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
|
|
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
|
|
1562
|
-
const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
|
|
1646
|
+
const stateStore = fileSkillsStateStore();
|
|
1563
1647
|
if (command === "list") {
|
|
1564
|
-
const listed = await
|
|
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
|
-
|
|
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(
|
|
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) {
|