@newlandnpt/pep-cli 0.3.0 → 0.4.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 +64 -5
  2. package/dist/pep.cjs +143 -56
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -72,11 +72,70 @@ The issuer is fixed at build time:
72
72
  `--issuer` remains available only as a temporary override — it is **not** remembered, so every
73
73
  `auth login` needs it again. That is why the built-in default decides where most users land.
74
74
 
75
- ⚠ **The npm release is built from `view`** (see `prepublishOnly`). As of 2026-09-08
76
- `https://pep.newlandnpt.us/.well-known/openid-configuration` returns **404** — the production
77
- domain does not serve the authorization server yet, so a `production` build would fail at
78
- discovery on the user's very first `auth login`. Switch `prepublishOnly` back to `production`
79
- once that endpoint is live.
75
+ ⚠ **The npm release is currently built from `development`** — it ships
76
+ `https://pep-webapp-dev.onrender.com` (see `prepack` in `package.json`). This is a deliberate
77
+ temporary state: production is not going live in the near term and this build is for other
78
+ departments to use now.
79
+
80
+ `https://pep.newlandnpt.us/.well-known/openid-configuration` still returns **404** (last checked
81
+ 2026-09-09) — the production domain does not serve the authorization server yet, so a
82
+ `production` build fails at discovery on the user's very first `auth login`. **Switch `prepack`
83
+ back to `production` and publish immediately once that endpoint is live**: the issuer is baked in
84
+ at build time, so releases already out in the wild cannot be repointed.
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.
80
139
 
81
140
  `pep auth token` writes only the access token to stdout, so agents can use it without parsing status
82
141
  text. Refresh occurs automatically shortly before expiry and the rotated refresh token replaces the old
package/dist/pep.cjs CHANGED
@@ -27,7 +27,7 @@ function issuerForEnvironment(environment) {
27
27
  if (environment === "view") return "https://pep-webapp-view.onrender.com";
28
28
  return "https://pep-webapp-dev.onrender.com";
29
29
  }
30
- var BUILD_ENVIRONMENT = false ? "development" : "production";
30
+ var BUILD_ENVIRONMENT = false ? "development" : "development";
31
31
  var DEFAULT_ISSUER = issuerForEnvironment(BUILD_ENVIRONMENT);
32
32
  function configuredIssuer(explicitIssuer) {
33
33
  return explicitIssuer ?? DEFAULT_ISSUER;
@@ -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;
@@ -1185,7 +1210,8 @@ async function updateSkills(dependencies) {
1185
1210
  const commit = response.headers.get("x-skills-commit") ?? void 0;
1186
1211
  const state = await dependencies.stateStore.read();
1187
1212
  const previous = state?.packages[name];
1188
- if (commit !== void 0 && commit === previous?.commit && state?.directory === dependencies.directory) {
1213
+ const sameTarget = previous?.directory === dependencies.directory && previous?.linkedInto === dependencies.linkInto;
1214
+ if (commit !== void 0 && commit === previous?.commit && sameTarget) {
1189
1215
  await response.body?.cancel();
1190
1216
  return { status: "unchanged", name, commit };
1191
1217
  }
@@ -1208,6 +1234,16 @@ async function updateSkills(dependencies) {
1208
1234
  if (before !== void 0 && before !== "" && before === hash) unchanged.push(skill);
1209
1235
  else updated.push(skill);
1210
1236
  }
1237
+ const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !skills.includes(skill));
1238
+ if (previous !== void 0) {
1239
+ const stale = sameTarget ? removed : Object.keys(previous.skills);
1240
+ for (const skill of stale) {
1241
+ await (0, import_promises4.rm)((0, import_node_path4.join)(previous.directory, skill), { recursive: true, force: true });
1242
+ if (previous.linkedInto !== void 0) {
1243
+ await (0, import_promises4.rm)((0, import_node_path4.join)(previous.linkedInto, skill), { recursive: true, force: true });
1244
+ }
1245
+ }
1246
+ }
1211
1247
  for (const skill of skills) {
1212
1248
  await (0, import_promises4.rm)((0, import_node_path4.join)(dependencies.directory, skill), { recursive: true, force: true });
1213
1249
  }
@@ -1226,24 +1262,18 @@ async function updateSkills(dependencies) {
1226
1262
  if (how === "copied") copiedCount += 1;
1227
1263
  }
1228
1264
  }
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
1265
  await dependencies.stateStore.write({
1240
- version: 3,
1241
- directory: dependencies.directory,
1242
- ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
1266
+ version: 4,
1243
1267
  packages: {
1244
1268
  // 别的包的账原样留着 —— 装 B 不该把 A 的记录抹掉,那会让 A 铺的目录从此没人清理。
1269
+ // v4 起每个包各记各的落点,所以这一份合并不会再把别人的落点带歪。
1245
1270
  ...state?.packages ?? {},
1246
- [name]: { ...commit ? { commit } : {}, skills: hashes }
1271
+ [name]: {
1272
+ ...commit ? { commit } : {},
1273
+ directory: dependencies.directory,
1274
+ ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
1275
+ skills: hashes
1276
+ }
1247
1277
  }
1248
1278
  });
1249
1279
  return {
@@ -1260,13 +1290,9 @@ async function updateSkills(dependencies) {
1260
1290
  ...commit ? { commit } : {}
1261
1291
  };
1262
1292
  }
1263
- async function installedPackages(store) {
1264
- const state = await store.read();
1265
- return Object.keys(state?.packages ?? {}).sort();
1266
- }
1267
1293
 
1268
1294
  // src/cli.ts
1269
- var VERSION = "0.3.0";
1295
+ var VERSION = false ? "0.0.0-dev" : "0.4.0";
1270
1296
  function usage() {
1271
1297
  return `PEP CLI ${VERSION}
1272
1298
 
@@ -1276,8 +1302,8 @@ Usage:
1276
1302
  pep auth token
1277
1303
  pep auth logout
1278
1304
  pep skills list
1279
- pep skills add <repo-url | group/project[@ref]> [--dir <path>]
1280
- pep skills update [<repo>...] [--dir <path>]
1305
+ pep skills add <repo-url | group/project[@ref]> [-p | --dir <path>]
1306
+ pep skills update [<repo>...] [-p | --dir <path>]
1281
1307
  pep docs list [--docs-url <url>]
1282
1308
  pep docs get <path> [--docs-url <url>]
1283
1309
  pep nexus setup
@@ -1309,9 +1335,20 @@ directly. Claude Code keeps its own directory, so each skill is also linked into
1309
1335
  ${claudeSkillsDirectory()} \u2014 one copy on disk, updated in one place. Where links are not
1310
1336
  available the skill is copied instead and the run says so.
1311
1337
 
1338
+ -p, --project installs into the CURRENT PROJECT instead: ./.agents/skills plus the same link into
1339
+ ./.claude/skills. Use it when the skills belong to one repository and should be committed with
1340
+ it; note both directories then show up in git status, which is why the personal location is the
1341
+ default.
1342
+
1312
1343
  --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.
1344
+ directory.
1345
+
1346
+ Where a repository was installed is remembered, so \`pep skills update\` refreshes each one where
1347
+ it already lives and never moves anything. On update, -p and --dir instead NARROW the run to the
1348
+ repositories installed there: \`pep skills update -p\` refreshes only this project's, and reports
1349
+ nothing to do when the project has none. To move a repository, add it again with the new flag \u2014
1350
+ add is where the location is decided, and the copy in the old location is then removed.
1351
+ Either way update only touches skills it wrote itself; anything you put there by hand is left alone.
1315
1352
 
1316
1353
  \`pep docs list\` prints the documents this account can read (path + description); feed a path
1317
1354
  straight to \`pep docs get\` to print that document as markdown on stdout. The documentation
@@ -1359,6 +1396,16 @@ function repeatedOption(args, name) {
1359
1396
  args.splice(index, 2);
1360
1397
  }
1361
1398
  }
1399
+ function flag(args, ...names) {
1400
+ let found = false;
1401
+ for (const name of names) {
1402
+ for (let index = args.indexOf(name); index !== -1; index = args.indexOf(name)) {
1403
+ args.splice(index, 1);
1404
+ found = true;
1405
+ }
1406
+ }
1407
+ return found;
1408
+ }
1362
1409
  function option(args, name) {
1363
1410
  const index = args.indexOf(name);
1364
1411
  if (index === -1) return void 0;
@@ -1383,6 +1430,33 @@ async function loginConfig(args, store = fileConfigStore()) {
1383
1430
  ...saved?.docsUrl !== void 0 ? { docsUrl: saved.docsUrl } : {}
1384
1431
  };
1385
1432
  }
1433
+ function parseSkillsArgs(command, args, cwd) {
1434
+ const explicitDirectory = command === "list" ? void 0 : option(args, "--dir");
1435
+ const project = command === "list" ? false : flag(args, "--project", "-p");
1436
+ if (explicitDirectory !== void 0 && project) {
1437
+ throw new Error("--dir and --project/-p cannot be used together: they name different places.");
1438
+ }
1439
+ const unknownOption = args.find((one) => one.startsWith("-"));
1440
+ if (unknownOption) throw new Error(`Unknown option: ${unknownOption}`);
1441
+ const requested = command === "add" ? args.shift() : void 0;
1442
+ if (command === "add" && !requested) {
1443
+ throw new Error("pep skills add needs a repository: a full https URL, or <group>/<project>.");
1444
+ }
1445
+ const named = command === "update" ? [...args] : [];
1446
+ if (command !== "update" && args.length > 0) {
1447
+ throw new Error(`Unexpected argument: ${args[0]}`);
1448
+ }
1449
+ const chosenTarget = explicitDirectory !== void 0 ? { directory: explicitDirectory } : project ? projectSkillsTarget(cwd) : void 0;
1450
+ return { ...requested !== void 0 ? { requested } : {}, named, ...chosenTarget ? { chosenTarget } : {} };
1451
+ }
1452
+ function targetForSource(chosen, recorded) {
1453
+ if (chosen) return chosen;
1454
+ if (!recorded) return userSkillsTarget();
1455
+ return {
1456
+ directory: recorded.directory,
1457
+ ...recorded.linkedInto !== void 0 ? { linkInto: recorded.linkedInto } : {}
1458
+ };
1459
+ }
1386
1460
  async function main() {
1387
1461
  const args = process.argv.slice(2);
1388
1462
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -1472,31 +1546,39 @@ ${usage()}`);
1472
1546
 
1473
1547
  ${usage()}`);
1474
1548
  }
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]}`);
1549
+ const { requested, named, chosenTarget } = parseSkillsArgs(command, args);
1484
1550
  const authorization = await auth.currentAuthorization();
1485
1551
  const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
1486
1552
  if (command === "list") {
1487
- const installed2 = await installedPackages(fileSkillsStateStore());
1488
- if (installed2.length === 0) {
1553
+ const listed = await fileSkillsStateStore().read();
1554
+ const packages = Object.entries(listed?.packages ?? {}).sort(
1555
+ ([a], [b]) => a.localeCompare(b)
1556
+ );
1557
+ if (packages.length === 0) {
1489
1558
  console.error("Nothing added yet. Run `pep skills add <repo>`.");
1490
1559
  return;
1491
1560
  }
1492
- for (const one of installed2) console.log(one);
1493
- console.error("\n`pep skills update` refreshes all of them.");
1561
+ for (const [source, pkg] of packages) console.log(`${source} ${pkg.directory}`);
1562
+ console.error("\n`pep skills update` refreshes all of them, each where it lives.");
1494
1563
  return;
1495
1564
  }
1496
1565
  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) {
1566
+ const state = await stateStore.read();
1567
+ const installed = Object.keys(state?.packages ?? {}).sort();
1568
+ const run2 = async (source, target) => reportUpdate(
1569
+ await updateSkills({
1570
+ ...remote,
1571
+ source,
1572
+ directory: target.directory,
1573
+ ...target.linkInto !== void 0 ? { linkInto: target.linkInto } : {},
1574
+ stateStore
1575
+ })
1576
+ );
1577
+ if (command === "add") {
1578
+ await run2(requested, targetForSource(chosenTarget, state?.packages[requested]));
1579
+ return;
1580
+ }
1581
+ if (installed.length === 0) {
1500
1582
  console.error("Nothing added yet. Run `pep skills add <repo>`.");
1501
1583
  return;
1502
1584
  }
@@ -1506,15 +1588,20 @@ ${usage()}`);
1506
1588
  `Not added yet: ${unknown.join(" ")}. Run \`pep skills add <repo>\` first, or \`pep skills list\` to see what is.`
1507
1589
  );
1508
1590
  }
1591
+ const wanted = named.length > 0 ? named : installed;
1592
+ const targets = chosenTarget ? wanted.filter((one) => state?.packages[one]?.directory === chosenTarget.directory) : wanted;
1593
+ if (targets.length === 0) {
1594
+ console.error(`No skills are installed in ${chosenTarget?.directory}.`);
1595
+ console.error("`pep skills update` (no flag) refreshes every repository where it lives;");
1596
+ console.error("`pep skills add <repo> -p` installs one into this project.");
1597
+ return;
1598
+ }
1599
+ const skipped = wanted.filter((one) => !targets.includes(one));
1600
+ if (skipped.length > 0) {
1601
+ console.error(`Not installed in ${chosenTarget?.directory}, skipped: ${skipped.join(" ")}`);
1602
+ }
1509
1603
  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);
1604
+ await run2(source, targetForSource(void 0, state?.packages[source]));
1518
1605
  }
1519
1606
  return;
1520
1607
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newlandnpt/pep-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "build:installer": "node scripts/build-installer.mjs production",
29
29
  "build:installer:dev": "node scripts/build-installer.mjs development",
30
30
  "build:installer:prod": "node scripts/build-installer.mjs production",
31
- "prepack": "node scripts/build-js.mjs production",
31
+ "prepack": "node scripts/build-js.mjs development",
32
32
  "build:exe:view": "node scripts/build-exe.mjs view",
33
33
  "build:installer:view": "node scripts/build-installer.mjs view"
34
34
  },