@newlandnpt/pep-cli 0.3.1 → 0.4.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 +58 -0
  2. package/dist/pep.cjs +153 -55
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -83,6 +83,64 @@ departments to use now.
83
83
  back to `production` and publish immediately once that endpoint is live**: the issuer is baked in
84
84
  at build time, so releases already out in the wild cannot be repointed.
85
85
 
86
+ ## Skills
87
+
88
+ ```powershell
89
+ pep skills add https://git.newlandpayment.com/group/sub/project # or group/sub/project[@ref]
90
+ pep skills list
91
+ pep skills update # all of them
92
+ pep skills update group/sub/project # just this one
93
+ pep skills add group/sub/project -p # into THIS project
94
+ ```
95
+
96
+ PEP fetches the repository with its own read-only service account, so no GitLab credential ever
97
+ reaches the machine. Only repositories on the platform's own GitLab host are accepted. One
98
+ repository may hold several skills: every directory containing a `SKILL.md` becomes one, and
99
+ `update` reports which of them actually changed rather than just that the repository moved.
100
+
101
+ ### Where skills are installed
102
+
103
+ | Scope | Flag | Canonical copy | Linked into |
104
+ | ----------- | ----------- | ------------------------- | -------------------------- |
105
+ | **Personal**| (default) | `~/.agents/skills/` | `~/.claude/skills/` |
106
+ | **Project** | `-p`, `--project` | `./.agents/skills/` | `./.claude/skills/` |
107
+ | **Raw** | `--dir <p>` | `<p>/` | nothing — linking skipped |
108
+
109
+ `~/.agents/skills` is the cross-agent convention that Codex, Cursor, Amp and ~20 others read
110
+ directly; Claude Code keeps its own directory, so each skill is also linked there — one copy on
111
+ disk, updated in one place. The project paths are the same two names rooted at the current
112
+ directory, matching what `npx skills` installs at project scope.
113
+
114
+ Personal is the default because the project directories get committed: whether a synced skill
115
+ counts as a change, and whether to gitignore it, is a question every repository would otherwise
116
+ have to answer. Use `-p` when the skills genuinely belong to one repository.
117
+
118
+ **The location is remembered per repository.** `pep skills update` refreshes each repository where
119
+ it already lives, and never moves anything.
120
+
121
+ On `update`, `-p` and `--dir` **narrow the run** rather than relocate — the same meaning they have
122
+ in `npx skills`:
123
+
124
+ ```powershell
125
+ pep skills update # every repository, each where it lives
126
+ pep skills update -p # only the ones installed in this project
127
+ ```
128
+
129
+ If the current project has none, `update -p` says so and does nothing. **To move a repository, add
130
+ it again with the new flag** — `add` is where the location is decided, and the copy in the old
131
+ location is then removed:
132
+
133
+ ```powershell
134
+ pep skills add group/sub/project -p # move it into this project
135
+ ```
136
+
137
+ `pep skills list` prints each repository with the directory it lives in, which is what `-p` matches
138
+ against.
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.
143
+
86
144
  `pep auth token` writes only the access token to stdout, so agents can use it without parsing status
87
145
  text. Refresh occurs automatically shortly before expiry and the rotated refresh token replaces the old
88
146
  credential atomically.
package/dist/pep.cjs CHANGED
@@ -51,6 +51,15 @@ function defaultSkillsDirectory() {
51
51
  function claudeSkillsDirectory() {
52
52
  return (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "skills");
53
53
  }
54
+ function userSkillsTarget() {
55
+ return { directory: defaultSkillsDirectory(), linkInto: claudeSkillsDirectory() };
56
+ }
57
+ function projectSkillsTarget(cwd = process.cwd()) {
58
+ return {
59
+ directory: (0, import_node_path.join)(cwd, ".agents", "skills"),
60
+ linkInto: (0, import_node_path.join)(cwd, ".claude", "skills")
61
+ };
62
+ }
54
63
  function isCliConfig(value) {
55
64
  if (typeof value !== "object" || value === null) return false;
56
65
  const candidate = value;
@@ -82,6 +91,16 @@ function fileConfigStore(path = configPath()) {
82
91
  };
83
92
  }
84
93
  function isSkillsState(value) {
94
+ if (typeof value !== "object" || value === null) return false;
95
+ const candidate = value;
96
+ if (candidate.version !== 4) return false;
97
+ return isPackageMap(candidate.packages, (skills, pkg) => {
98
+ if (typeof pkg.directory !== "string") return false;
99
+ if (pkg.linkedInto !== void 0 && typeof pkg.linkedInto !== "string") return false;
100
+ return isStringMap(skills);
101
+ });
102
+ }
103
+ function isSkillsStateV3(value) {
85
104
  if (typeof value !== "object" || value === null) return false;
86
105
  const candidate = value;
87
106
  if (candidate.version !== 3) return false;
@@ -110,7 +129,7 @@ function isPackageMap(value, skillsOk) {
110
129
  return Object.values(value).every((one) => {
111
130
  if (typeof one !== "object" || one === null || Array.isArray(one)) return false;
112
131
  const pkg = one;
113
- return skillsOk(pkg.skills) && (pkg.commit === void 0 || typeof pkg.commit === "string");
132
+ return skillsOk(pkg.skills, pkg) && (pkg.commit === void 0 || typeof pkg.commit === "string");
114
133
  });
115
134
  }
116
135
  function isStringMap(value) {
@@ -118,29 +137,34 @@ function isStringMap(value) {
118
137
  return Object.values(value).every((one) => typeof one === "string");
119
138
  }
120
139
  function migrateSkillsState(old) {
121
- const base = {
122
- version: 3,
140
+ const where = {
123
141
  directory: old.directory,
124
142
  ...old.linkedInto !== void 0 ? { linkedInto: old.linkedInto } : {}
125
143
  };
126
144
  const unknownHashes = (names) => Object.fromEntries(names.map((name) => [name, ""]));
127
145
  if (old.version === 1) {
128
146
  return {
129
- ...base,
147
+ version: 4,
130
148
  packages: {
131
149
  [LEGACY_PACKAGE]: {
132
150
  ...old.commit !== void 0 ? { commit: old.commit } : {},
151
+ ...where,
133
152
  skills: unknownHashes(old.skills)
134
153
  }
135
154
  }
136
155
  };
137
156
  }
138
157
  return {
139
- ...base,
158
+ version: 4,
140
159
  packages: Object.fromEntries(
141
160
  Object.entries(old.packages).map(([source, pkg]) => [
142
161
  source,
143
- { ...pkg.commit !== void 0 ? { commit: pkg.commit } : {}, skills: unknownHashes(pkg.skills) }
162
+ {
163
+ ...pkg.commit !== void 0 ? { commit: pkg.commit } : {},
164
+ ...where,
165
+ // v2 的 `skills` 是名字数组(哈希未知),v3 的已经是「名字 → 真哈希」。
166
+ skills: Array.isArray(pkg.skills) ? unknownHashes(pkg.skills) : pkg.skills
167
+ }
144
168
  ])
145
169
  )
146
170
  };
@@ -151,7 +175,8 @@ function fileSkillsStateStore(path = skillsStatePath()) {
151
175
  try {
152
176
  const parsed = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
153
177
  if (isSkillsState(parsed)) return parsed;
154
- if (isSkillsStateV2(parsed) || isSkillsStateV1(parsed)) return migrateSkillsState(parsed);
178
+ if (isSkillsStateV3(parsed) || isSkillsStateV2(parsed) || isSkillsStateV1(parsed))
179
+ return migrateSkillsState(parsed);
155
180
  return null;
156
181
  } catch (error) {
157
182
  if (error.code === "ENOENT") return null;
@@ -1170,6 +1195,15 @@ function hashSkill(files) {
1170
1195
  }
1171
1196
  return hash.digest("hex").slice(0, 32);
1172
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;
1203
+ }
1204
+ }
1205
+ return true;
1206
+ }
1173
1207
  async function updateSkills(dependencies) {
1174
1208
  const doFetch = dependencies.fetch ?? globalThis.fetch;
1175
1209
  const name = dependencies.source;
@@ -1185,7 +1219,9 @@ async function updateSkills(dependencies) {
1185
1219
  const commit = response.headers.get("x-skills-commit") ?? void 0;
1186
1220
  const state = await dependencies.stateStore.read();
1187
1221
  const previous = state?.packages[name];
1188
- if (commit !== void 0 && commit === previous?.commit && state?.directory === dependencies.directory) {
1222
+ 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) {
1189
1225
  await response.body?.cancel();
1190
1226
  return { status: "unchanged", name, commit };
1191
1227
  }
@@ -1208,6 +1244,16 @@ async function updateSkills(dependencies) {
1208
1244
  if (before !== void 0 && before !== "" && before === hash) unchanged.push(skill);
1209
1245
  else updated.push(skill);
1210
1246
  }
1247
+ const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !skills.includes(skill));
1248
+ if (previous !== void 0) {
1249
+ const stale = sameTarget ? removed : Object.keys(previous.skills);
1250
+ for (const skill of stale) {
1251
+ await (0, import_promises4.rm)((0, import_node_path4.join)(previous.directory, skill), { recursive: true, force: true });
1252
+ if (previous.linkedInto !== void 0) {
1253
+ await (0, import_promises4.rm)((0, import_node_path4.join)(previous.linkedInto, skill), { recursive: true, force: true });
1254
+ }
1255
+ }
1256
+ }
1211
1257
  for (const skill of skills) {
1212
1258
  await (0, import_promises4.rm)((0, import_node_path4.join)(dependencies.directory, skill), { recursive: true, force: true });
1213
1259
  }
@@ -1226,24 +1272,18 @@ async function updateSkills(dependencies) {
1226
1272
  if (how === "copied") copiedCount += 1;
1227
1273
  }
1228
1274
  }
1229
- const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !skills.includes(skill));
1230
- for (const skill of removed) {
1231
- await (0, import_promises4.rm)((0, import_node_path4.join)(state?.directory ?? dependencies.directory, skill), {
1232
- recursive: true,
1233
- force: true
1234
- });
1235
- if (state?.linkedInto !== void 0) {
1236
- await (0, import_promises4.rm)((0, import_node_path4.join)(state.linkedInto, skill), { recursive: true, force: true });
1237
- }
1238
- }
1239
1275
  await dependencies.stateStore.write({
1240
- version: 3,
1241
- directory: dependencies.directory,
1242
- ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
1276
+ version: 4,
1243
1277
  packages: {
1244
1278
  // 别的包的账原样留着 —— 装 B 不该把 A 的记录抹掉,那会让 A 铺的目录从此没人清理。
1279
+ // v4 起每个包各记各的落点,所以这一份合并不会再把别人的落点带歪。
1245
1280
  ...state?.packages ?? {},
1246
- [name]: { ...commit ? { commit } : {}, skills: hashes }
1281
+ [name]: {
1282
+ ...commit ? { commit } : {},
1283
+ directory: dependencies.directory,
1284
+ ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
1285
+ skills: hashes
1286
+ }
1247
1287
  }
1248
1288
  });
1249
1289
  return {
@@ -1260,13 +1300,9 @@ async function updateSkills(dependencies) {
1260
1300
  ...commit ? { commit } : {}
1261
1301
  };
1262
1302
  }
1263
- async function installedPackages(store) {
1264
- const state = await store.read();
1265
- return Object.keys(state?.packages ?? {}).sort();
1266
- }
1267
1303
 
1268
1304
  // src/cli.ts
1269
- var VERSION = "0.3.0";
1305
+ var VERSION = false ? "0.0.0-dev" : "0.4.1";
1270
1306
  function usage() {
1271
1307
  return `PEP CLI ${VERSION}
1272
1308
 
@@ -1276,8 +1312,8 @@ Usage:
1276
1312
  pep auth token
1277
1313
  pep auth logout
1278
1314
  pep skills list
1279
- pep skills add <repo-url | group/project[@ref]> [--dir <path>]
1280
- pep skills update [<repo>...] [--dir <path>]
1315
+ pep skills add <repo-url | group/project[@ref]> [-p | --dir <path>]
1316
+ pep skills update [<repo>...] [-p | --dir <path>]
1281
1317
  pep docs list [--docs-url <url>]
1282
1318
  pep docs get <path> [--docs-url <url>]
1283
1319
  pep nexus setup
@@ -1309,9 +1345,21 @@ directly. Claude Code keeps its own directory, so each skill is also linked into
1309
1345
  ${claudeSkillsDirectory()} \u2014 one copy on disk, updated in one place. Where links are not
1310
1346
  available the skill is copied instead and the run says so.
1311
1347
 
1348
+ -p, --project installs into the CURRENT PROJECT instead: ./.agents/skills plus the same link into
1349
+ ./.claude/skills. Use it when the skills belong to one repository and should be committed with
1350
+ it; note both directories then show up in git status, which is why the personal location is the
1351
+ default.
1352
+
1312
1353
  --dir <path> writes to that path ONLY and skips the linking, for an agent that reads neither
1313
- directory. Either way sync only touches skills it wrote itself; anything you put there by
1314
- hand is left alone.
1354
+ directory.
1355
+
1356
+ Where a repository was installed is remembered, so \`pep skills update\` refreshes each one where
1357
+ it already lives and never moves anything. On update, -p and --dir instead NARROW the run to the
1358
+ repositories installed there: \`pep skills update -p\` refreshes only this project's, and reports
1359
+ nothing to do when the project has none. To move a repository, add it again with the new flag \u2014
1360
+ 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.
1315
1363
 
1316
1364
  \`pep docs list\` prints the documents this account can read (path + description); feed a path
1317
1365
  straight to \`pep docs get\` to print that document as markdown on stdout. The documentation
@@ -1359,6 +1407,16 @@ function repeatedOption(args, name) {
1359
1407
  args.splice(index, 2);
1360
1408
  }
1361
1409
  }
1410
+ function flag(args, ...names) {
1411
+ let found = false;
1412
+ for (const name of names) {
1413
+ for (let index = args.indexOf(name); index !== -1; index = args.indexOf(name)) {
1414
+ args.splice(index, 1);
1415
+ found = true;
1416
+ }
1417
+ }
1418
+ return found;
1419
+ }
1362
1420
  function option(args, name) {
1363
1421
  const index = args.indexOf(name);
1364
1422
  if (index === -1) return void 0;
@@ -1383,6 +1441,33 @@ async function loginConfig(args, store = fileConfigStore()) {
1383
1441
  ...saved?.docsUrl !== void 0 ? { docsUrl: saved.docsUrl } : {}
1384
1442
  };
1385
1443
  }
1444
+ 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");
1447
+ if (explicitDirectory !== void 0 && project) {
1448
+ throw new Error("--dir and --project/-p cannot be used together: they name different places.");
1449
+ }
1450
+ const unknownOption = args.find((one) => one.startsWith("-"));
1451
+ if (unknownOption) throw new Error(`Unknown option: ${unknownOption}`);
1452
+ const requested = command === "add" ? args.shift() : void 0;
1453
+ if (command === "add" && !requested) {
1454
+ throw new Error("pep skills add needs a repository: a full https URL, or <group>/<project>.");
1455
+ }
1456
+ const named = command === "update" ? [...args] : [];
1457
+ if (command !== "update" && args.length > 0) {
1458
+ throw new Error(`Unexpected argument: ${args[0]}`);
1459
+ }
1460
+ const chosenTarget = explicitDirectory !== void 0 ? { directory: explicitDirectory } : project ? projectSkillsTarget(cwd) : void 0;
1461
+ return { ...requested !== void 0 ? { requested } : {}, named, ...chosenTarget ? { chosenTarget } : {} };
1462
+ }
1463
+ function targetForSource(chosen, recorded) {
1464
+ if (chosen) return chosen;
1465
+ if (!recorded) return userSkillsTarget();
1466
+ return {
1467
+ directory: recorded.directory,
1468
+ ...recorded.linkedInto !== void 0 ? { linkInto: recorded.linkedInto } : {}
1469
+ };
1470
+ }
1386
1471
  async function main() {
1387
1472
  const args = process.argv.slice(2);
1388
1473
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -1472,31 +1557,39 @@ ${usage()}`);
1472
1557
 
1473
1558
  ${usage()}`);
1474
1559
  }
1475
- const requested = command === "add" ? args.shift() : void 0;
1476
- const named = command === "update" ? args.filter((one) => !one.startsWith("--")) : [];
1477
- for (const one of named) args.splice(args.indexOf(one), 1);
1478
- if (command === "add" && !requested) {
1479
- throw new Error("pep skills add needs a repository: a full https URL, or <group>/<project>.");
1480
- }
1481
- const explicitDirectory = command === "list" ? void 0 : option(args, "--dir");
1482
- const directory = explicitDirectory ?? defaultSkillsDirectory();
1483
- if (args.length > 0) throw new Error(`Unknown option: ${args[0]}`);
1560
+ const { requested, named, chosenTarget } = parseSkillsArgs(command, args);
1484
1561
  const authorization = await auth.currentAuthorization();
1485
1562
  const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
1486
1563
  if (command === "list") {
1487
- const installed2 = await installedPackages(fileSkillsStateStore());
1488
- if (installed2.length === 0) {
1564
+ const listed = await fileSkillsStateStore().read();
1565
+ const packages = Object.entries(listed?.packages ?? {}).sort(
1566
+ ([a], [b]) => a.localeCompare(b)
1567
+ );
1568
+ if (packages.length === 0) {
1489
1569
  console.error("Nothing added yet. Run `pep skills add <repo>`.");
1490
1570
  return;
1491
1571
  }
1492
- for (const one of installed2) console.log(one);
1493
- console.error("\n`pep skills update` refreshes all of them.");
1572
+ for (const [source, pkg] of packages) console.log(`${source} ${pkg.directory}`);
1573
+ console.error("\n`pep skills update` refreshes all of them, each where it lives.");
1494
1574
  return;
1495
1575
  }
1496
1576
  const stateStore = fileSkillsStateStore();
1497
- const installed = await installedPackages(stateStore);
1498
- const targets = command === "add" ? [requested] : named.length > 0 ? named : installed;
1499
- if (targets.length === 0) {
1577
+ const state = await stateStore.read();
1578
+ const installed = Object.keys(state?.packages ?? {}).sort();
1579
+ const run2 = async (source, target) => reportUpdate(
1580
+ await updateSkills({
1581
+ ...remote,
1582
+ source,
1583
+ directory: target.directory,
1584
+ ...target.linkInto !== void 0 ? { linkInto: target.linkInto } : {},
1585
+ stateStore
1586
+ })
1587
+ );
1588
+ if (command === "add") {
1589
+ await run2(requested, targetForSource(chosenTarget, state?.packages[requested]));
1590
+ return;
1591
+ }
1592
+ if (installed.length === 0) {
1500
1593
  console.error("Nothing added yet. Run `pep skills add <repo>`.");
1501
1594
  return;
1502
1595
  }
@@ -1506,15 +1599,20 @@ ${usage()}`);
1506
1599
  `Not added yet: ${unknown.join(" ")}. Run \`pep skills add <repo>\` first, or \`pep skills list\` to see what is.`
1507
1600
  );
1508
1601
  }
1602
+ const wanted = named.length > 0 ? named : installed;
1603
+ const targets = chosenTarget ? wanted.filter((one) => state?.packages[one]?.directory === chosenTarget.directory) : wanted;
1604
+ if (targets.length === 0) {
1605
+ console.error(`No skills are installed in ${chosenTarget?.directory}.`);
1606
+ console.error("`pep skills update` (no flag) refreshes every repository where it lives;");
1607
+ console.error("`pep skills add <repo> -p` installs one into this project.");
1608
+ return;
1609
+ }
1610
+ const skipped = wanted.filter((one) => !targets.includes(one));
1611
+ if (skipped.length > 0) {
1612
+ console.error(`Not installed in ${chosenTarget?.directory}, skipped: ${skipped.join(" ")}`);
1613
+ }
1509
1614
  for (const source of targets) {
1510
- const result = await updateSkills({
1511
- ...remote,
1512
- source,
1513
- directory,
1514
- ...explicitDirectory === void 0 ? { linkInto: claudeSkillsDirectory() } : {},
1515
- stateStore
1516
- });
1517
- reportUpdate(result);
1615
+ await run2(source, targetForSource(void 0, state?.packages[source]));
1518
1616
  }
1519
1617
  return;
1520
1618
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newlandnpt/pep-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",