@newlandnpt/pep-cli 0.6.1 → 0.7.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 (2) hide show
  1. package/dist/pep.cjs +80 -16
  2. package/package.json +1 -1
package/dist/pep.cjs CHANGED
@@ -51,6 +51,9 @@ 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 samePlace(a, b) {
55
+ return a.toLowerCase() === b.toLowerCase();
56
+ }
54
57
  function userSkillsTarget() {
55
58
  return { directory: defaultSkillsDirectory(), linkInto: claudeSkillsDirectory() };
56
59
  }
@@ -1102,6 +1105,14 @@ function readTar(bytes) {
1102
1105
 
1103
1106
  // src/skills-service.ts
1104
1107
  var ARCHIVE_PATH = "/api/skills/archive";
1108
+ var MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
1109
+ var MAX_UNPACKED_BYTES = 256 * 1024 * 1024;
1110
+ var MAX_ENTRIES = 1e4;
1111
+ function tooBig(what, limit) {
1112
+ return new Error(
1113
+ `The skills archive is too large (${what} exceeds ${Math.round(limit / 1024 / 1024)} MiB). Refusing to unpack it.`
1114
+ );
1115
+ }
1105
1116
  var SKILL_MANIFEST = "SKILL.md";
1106
1117
  function planSkillFiles(entries) {
1107
1118
  const roots = [];
@@ -1116,13 +1127,13 @@ function planSkillFiles(entries) {
1116
1127
  for (const root of roots) {
1117
1128
  const name = root[root.length - 1];
1118
1129
  const where = root.join("/");
1119
- const first = seen.get(name);
1130
+ const first = seen.get(name.toLowerCase());
1120
1131
  if (first !== void 0) {
1121
1132
  throw new Error(
1122
1133
  `Two skills in this package would both be called "${name}": ${first} and ${where}. Skill names come from the directory name, so they must be unique within a package.`
1123
1134
  );
1124
1135
  }
1125
- seen.set(name, where);
1136
+ seen.set(name.toLowerCase(), where);
1126
1137
  }
1127
1138
  const files = [];
1128
1139
  for (const entry of entries) {
@@ -1137,9 +1148,38 @@ function planSkillFiles(entries) {
1137
1148
  }
1138
1149
  return files;
1139
1150
  }
1151
+ async function readCapped(response, limit) {
1152
+ const reader = response.body?.getReader();
1153
+ if (!reader) return new Uint8Array(await response.arrayBuffer());
1154
+ const chunks = [];
1155
+ let total = 0;
1156
+ for (; ; ) {
1157
+ const { done, value } = await reader.read();
1158
+ if (done) break;
1159
+ total += value.byteLength;
1160
+ if (total > limit) {
1161
+ await reader.cancel();
1162
+ throw tooBig("the download", limit);
1163
+ }
1164
+ chunks.push(value);
1165
+ }
1166
+ const out = new Uint8Array(total);
1167
+ let at = 0;
1168
+ for (const chunk of chunks) {
1169
+ out.set(chunk, at);
1170
+ at += chunk.byteLength;
1171
+ }
1172
+ return out;
1173
+ }
1174
+ function assertInside(base, target) {
1175
+ const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(base), (0, import_node_path4.resolve)(target));
1176
+ if (rel === "" || rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel)) {
1177
+ throw new Error(`Refusing an archive entry that escapes its directory: ${target}`);
1178
+ }
1179
+ }
1140
1180
  function segmentsOf(path) {
1141
- const segments = path.split("/").filter((one) => one !== "" && one !== ".");
1142
- if (segments.some((one) => one === "..")) {
1181
+ const segments = path.split(/[\\/]+/).filter((one) => one !== "" && one !== ".");
1182
+ if (segments.some((one) => one === ".." || /^[a-z]:$/i.test(one))) {
1143
1183
  throw new Error(`Refusing an archive entry that escapes its directory: ${path}`);
1144
1184
  }
1145
1185
  return segments;
@@ -1195,6 +1235,12 @@ function hashSkill(files) {
1195
1235
  }
1196
1236
  return hash.digest("hex").slice(0, 32);
1197
1237
  }
1238
+ function otherClaimant(packages, self, directory, skill) {
1239
+ const wanted = skill.toLowerCase();
1240
+ return Object.entries(packages).find(
1241
+ ([other, pkg]) => other !== self && samePlace(pkg.directory, directory) && Object.keys(pkg.skills).some((one) => one.toLowerCase() === wanted)
1242
+ )?.[0];
1243
+ }
1198
1244
  async function removeSkills(name, store) {
1199
1245
  const state = await store.read();
1200
1246
  const pkg = state?.packages[name];
@@ -1204,9 +1250,7 @@ async function removeSkills(name, store) {
1204
1250
  const deleted = [];
1205
1251
  const keptForOthers = [];
1206
1252
  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];
1253
+ const owner = otherClaimant(state.packages, name, pkg.directory, skill);
1210
1254
  if (owner !== void 0) {
1211
1255
  keptForOthers.push({ skill, owner });
1212
1256
  continue;
@@ -1248,10 +1292,8 @@ async function updateSkills(dependencies) {
1248
1292
  const commit = response.headers.get("x-skills-commit") ?? void 0;
1249
1293
  const state = await dependencies.stateStore.read();
1250
1294
  const previous = state?.packages[name];
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];
1295
+ const sameTarget = previous !== void 0 && samePlace(previous.directory, dependencies.directory) && (previous.linkedInto === void 0 ? dependencies.linkInto === void 0 : dependencies.linkInto !== void 0 && samePlace(previous.linkedInto, dependencies.linkInto));
1296
+ const claimedElsewhere = (skill) => otherClaimant(state?.packages ?? {}, name, dependencies.directory, skill);
1255
1297
  const gone = previous !== void 0 && sameTarget ? await missingSkills(previous.directory, Object.keys(previous.skills)) : /* @__PURE__ */ new Set();
1256
1298
  const nothingToRestore = !dependencies.restoreMissing || gone.size === 0;
1257
1299
  const standing = Object.keys(previous?.skills ?? {}).flatMap((skill) => {
@@ -1262,8 +1304,23 @@ async function updateSkills(dependencies) {
1262
1304
  await response.body?.cancel();
1263
1305
  return { status: "unchanged", name, commit, conflicts: standing };
1264
1306
  }
1265
- const gzipped = new Uint8Array(await response.arrayBuffer());
1266
- const files = planSkillFiles(readTar((0, import_node_zlib.gunzipSync)(gzipped)));
1307
+ const gzipped = await readCapped(response, dependencies.maxArchiveBytes ?? MAX_ARCHIVE_BYTES);
1308
+ let unpacked;
1309
+ try {
1310
+ unpacked = (0, import_node_zlib.gunzipSync)(gzipped, { maxOutputLength: MAX_UNPACKED_BYTES });
1311
+ } catch (error) {
1312
+ if (error.code === "ERR_BUFFER_TOO_LARGE") {
1313
+ throw tooBig("the unpacked archive", MAX_UNPACKED_BYTES);
1314
+ }
1315
+ throw error;
1316
+ }
1317
+ const entries = readTar(unpacked);
1318
+ if (entries.length > MAX_ENTRIES) {
1319
+ throw new Error(
1320
+ `The skills archive holds ${entries.length} files (limit ${MAX_ENTRIES}). Refusing to unpack it.`
1321
+ );
1322
+ }
1323
+ const files = planSkillFiles(entries);
1267
1324
  const bySkill = /* @__PURE__ */ new Map();
1268
1325
  for (const file of files) {
1269
1326
  const group = bySkill.get(file.skill);
@@ -1300,6 +1357,7 @@ async function updateSkills(dependencies) {
1300
1357
  if (previous !== void 0) {
1301
1358
  const stale = sameTarget ? removed : Object.keys(previous.skills);
1302
1359
  for (const skill of stale) {
1360
+ if (otherClaimant(state?.packages ?? {}, name, previous.directory, skill)) continue;
1303
1361
  await (0, import_promises4.rm)((0, import_node_path4.join)(previous.directory, skill), { recursive: true, force: true });
1304
1362
  if (previous.linkedInto !== void 0) {
1305
1363
  await (0, import_promises4.rm)((0, import_node_path4.join)(previous.linkedInto, skill), { recursive: true, force: true });
@@ -1313,6 +1371,7 @@ async function updateSkills(dependencies) {
1313
1371
  for (const file of files) {
1314
1372
  if (untouched.has(file.skill)) continue;
1315
1373
  const target = (0, import_node_path4.join)(dependencies.directory, file.skill, ...file.path.split("/"));
1374
+ assertInside(dependencies.directory, target);
1316
1375
  await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(target), { recursive: true });
1317
1376
  await (0, import_promises4.writeFile)(target, file.data);
1318
1377
  }
@@ -1353,7 +1412,9 @@ async function updateSkills(dependencies) {
1353
1412
  updated,
1354
1413
  unchanged,
1355
1414
  skills,
1356
- fileCount: files.filter((one) => !skipped.includes(one.skill)).length,
1415
+ // ⚠ 扣掉的是 `untouched` 全部(用户删的 + 让给别的包的),不只是 skipped ——
1416
+ // 让开没写的那些文件算进去,「N file(s)」就是句假话。
1417
+ fileCount: files.filter((one) => !untouched.has(one.skill)).length,
1357
1418
  removed,
1358
1419
  skipped,
1359
1420
  conflicts,
@@ -1365,7 +1426,7 @@ async function updateSkills(dependencies) {
1365
1426
  }
1366
1427
 
1367
1428
  // src/cli.ts
1368
- var VERSION = false ? "0.0.0-dev" : "0.6.1";
1429
+ var VERSION = false ? "0.0.0-dev" : "0.7.0";
1369
1430
  function usage() {
1370
1431
  return `PEP CLI ${VERSION}
1371
1432
 
@@ -1723,7 +1784,10 @@ ${usage()}`);
1723
1784
  );
1724
1785
  }
1725
1786
  const wanted = named.length > 0 ? named : installed;
1726
- const targets = chosenTarget ? wanted.filter((one) => state?.packages[one]?.directory === chosenTarget.directory) : wanted;
1787
+ const targets = chosenTarget ? wanted.filter((one) => {
1788
+ const at = state?.packages[one]?.directory;
1789
+ return at !== void 0 && samePlace(at, chosenTarget.directory);
1790
+ }) : wanted;
1727
1791
  if (targets.length === 0) {
1728
1792
  console.error(`No skills are installed in ${chosenTarget?.directory}.`);
1729
1793
  console.error("`pep skills update` (no flag) refreshes every repository where it lives;");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newlandnpt/pep-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",