@newlandnpt/pep-cli 0.1.3 → 0.3.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 +465 -83
  2. package/package.json +1 -1
package/dist/pep.cjs CHANGED
@@ -11,8 +11,16 @@ var import_node_os = require("node:os");
11
11
  var import_node_path = require("node:path");
12
12
  var DEFAULT_CLIENT_ID = "pep-cli";
13
13
  var DEFAULT_REDIRECT_URI = "http://localhost:53682/callback";
14
- var DEFAULT_SCOPES = ["openid", "profile", "email", "docs:read", "skills:read"];
14
+ var DEFAULT_SCOPES = [
15
+ "openid",
16
+ "profile",
17
+ "email",
18
+ "docs:read",
19
+ "skills:read",
20
+ "nexus-credentials:write"
21
+ ];
15
22
  var DEFAULT_RESOURCES = ["urn:newland:pep:docs"];
23
+ var LEGACY_PACKAGE = "semi-integration";
16
24
  var DEFAULT_DOCS_URL = "https://pep-developer-docs.onrender.com";
17
25
  function issuerForEnvironment(environment) {
18
26
  if (environment === "production") return "https://pep.newlandnpt.us";
@@ -76,15 +84,75 @@ function fileConfigStore(path = configPath()) {
76
84
  function isSkillsState(value) {
77
85
  if (typeof value !== "object" || value === null) return false;
78
86
  const candidate = value;
79
- return candidate.version === 1 && typeof candidate.directory === "string" && Array.isArray(candidate.skills) && candidate.skills.every((one) => typeof one === "string") && (candidate.commit === void 0 || typeof candidate.commit === "string") && // 可选:0.1.1 及更早写下的 skills.json 没有这个键,读它们不该报「状态损坏」。
80
- (candidate.linkedInto === void 0 || typeof candidate.linkedInto === "string");
87
+ if (candidate.version !== 3) return false;
88
+ if (typeof candidate.directory !== "string") return false;
89
+ if (candidate.linkedInto !== void 0 && typeof candidate.linkedInto !== "string") return false;
90
+ return isPackageMap(candidate.packages, (skills) => isStringMap(skills));
91
+ }
92
+ function isSkillsStateV2(value) {
93
+ if (typeof value !== "object" || value === null) return false;
94
+ const candidate = value;
95
+ if (candidate.version !== 2) return false;
96
+ if (typeof candidate.directory !== "string") return false;
97
+ if (candidate.linkedInto !== void 0 && typeof candidate.linkedInto !== "string") return false;
98
+ return isPackageMap(
99
+ candidate.packages,
100
+ (skills) => Array.isArray(skills) && skills.every((one) => typeof one === "string")
101
+ );
102
+ }
103
+ function isSkillsStateV1(value) {
104
+ if (typeof value !== "object" || value === null) return false;
105
+ const candidate = value;
106
+ return candidate.version === 1 && typeof candidate.directory === "string" && Array.isArray(candidate.skills) && candidate.skills.every((one) => typeof one === "string") && (candidate.commit === void 0 || typeof candidate.commit === "string") && (candidate.linkedInto === void 0 || typeof candidate.linkedInto === "string");
107
+ }
108
+ function isPackageMap(value, skillsOk) {
109
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
110
+ return Object.values(value).every((one) => {
111
+ if (typeof one !== "object" || one === null || Array.isArray(one)) return false;
112
+ const pkg = one;
113
+ return skillsOk(pkg.skills) && (pkg.commit === void 0 || typeof pkg.commit === "string");
114
+ });
115
+ }
116
+ function isStringMap(value) {
117
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
118
+ return Object.values(value).every((one) => typeof one === "string");
119
+ }
120
+ function migrateSkillsState(old) {
121
+ const base = {
122
+ version: 3,
123
+ directory: old.directory,
124
+ ...old.linkedInto !== void 0 ? { linkedInto: old.linkedInto } : {}
125
+ };
126
+ const unknownHashes = (names) => Object.fromEntries(names.map((name) => [name, ""]));
127
+ if (old.version === 1) {
128
+ return {
129
+ ...base,
130
+ packages: {
131
+ [LEGACY_PACKAGE]: {
132
+ ...old.commit !== void 0 ? { commit: old.commit } : {},
133
+ skills: unknownHashes(old.skills)
134
+ }
135
+ }
136
+ };
137
+ }
138
+ return {
139
+ ...base,
140
+ packages: Object.fromEntries(
141
+ Object.entries(old.packages).map(([source, pkg]) => [
142
+ source,
143
+ { ...pkg.commit !== void 0 ? { commit: pkg.commit } : {}, skills: unknownHashes(pkg.skills) }
144
+ ])
145
+ )
146
+ };
81
147
  }
82
148
  function fileSkillsStateStore(path = skillsStatePath()) {
83
149
  return {
84
150
  async read() {
85
151
  try {
86
152
  const parsed = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
87
- return isSkillsState(parsed) ? parsed : null;
153
+ if (isSkillsState(parsed)) return parsed;
154
+ if (isSkillsStateV2(parsed) || isSkillsStateV1(parsed)) return migrateSkillsState(parsed);
155
+ return null;
88
156
  } catch (error) {
89
157
  if (error.code === "ENOENT") return null;
90
158
  if (error instanceof SyntaxError) return null;
@@ -765,10 +833,173 @@ async function fetchDocContent(dependencies, path) {
765
833
  return response.text();
766
834
  }
767
835
 
768
- // src/skills-service.ts
836
+ // src/maven-env.ts
769
837
  var import_promises3 = require("node:fs/promises");
770
838
  var import_node_os2 = require("node:os");
771
839
  var import_node_path3 = require("node:path");
840
+ var import_node_child_process3 = require("node:child_process");
841
+ var import_node_util = require("node:util");
842
+ var run = (0, import_node_util.promisify)(import_node_child_process3.execFile);
843
+ var USERNAME_VAR = "NEWLAND_MAVEN_USERNAME";
844
+ var PASSWORD_VAR = "NEWLAND_MAVEN_PASSWORD";
845
+ var BEGIN = "# >>> pep-cli: Newland Maven credentials >>>";
846
+ var END = "# <<< pep-cli: Newland Maven credentials <<<";
847
+ function resolveTarget(os = (0, import_node_os2.platform)(), shell = process.env.SHELL) {
848
+ if (os === "win32") return { kind: "windows" };
849
+ const file = shell?.endsWith("/bash") ? ".bash_profile" : ".zshrc";
850
+ return { kind: "profile", path: (0, import_node_path3.join)((0, import_node_os2.homedir)(), file) };
851
+ }
852
+ function shellQuote(value) {
853
+ return `'${value.replace(/'/g, `'\\''`)}'`;
854
+ }
855
+ function exportLines(credential, os = (0, import_node_os2.platform)()) {
856
+ if (os === "win32") {
857
+ return `set ${USERNAME_VAR}=${credential.username}
858
+ set ${PASSWORD_VAR}=${credential.password}`;
859
+ }
860
+ return [
861
+ `export ${USERNAME_VAR}=${shellQuote(credential.username)}`,
862
+ `export ${PASSWORD_VAR}=${shellQuote(credential.password)}`
863
+ ].join("\n");
864
+ }
865
+ function profileBlock(credential) {
866
+ return [
867
+ BEGIN,
868
+ "# \u7531 `pep nexus setup` \u5199\u5165\u3002\u8FD9\u4E00\u6BB5\u4F1A\u88AB\u4E0B\u4E00\u6B21\u8FD0\u884C\u6574\u4F53\u66FF\u6362\uFF0C\u624B\u6539\u65E0\u6548\u3002",
869
+ `export ${USERNAME_VAR}=${shellQuote(credential.username)}`,
870
+ `export ${PASSWORD_VAR}=${shellQuote(credential.password)}`,
871
+ END
872
+ ].join("\n");
873
+ }
874
+ function planProfile(existing, credential) {
875
+ const block = profileBlock(credential);
876
+ if (existing === null || existing.trim() === "") {
877
+ return { action: "append", content: `${block}
878
+ ` };
879
+ }
880
+ const begin = existing.indexOf(BEGIN);
881
+ const end = existing.indexOf(END);
882
+ if (begin === -1 && end === -1) {
883
+ const separator = existing.endsWith("\n") ? "" : "\n";
884
+ return { action: "append", content: `${existing}${separator}
885
+ ${block}
886
+ ` };
887
+ }
888
+ if (begin === -1 || end === -1 || end < begin) {
889
+ return {
890
+ action: "manual",
891
+ reason: "profile \u91CC pep-cli \u7684\u56F4\u680F\u53EA\u5269\u534A\u6761\uFF08\u88AB\u624B\u6539\u8FC7\uFF0C\u6216\u4E0A\u6B21\u5199\u4E86\u4E00\u534A\uFF09\uFF0C\u4E0D\u731C\u8FB9\u754C"
892
+ };
893
+ }
894
+ if (existing.indexOf(BEGIN, begin + 1) !== -1) {
895
+ return { action: "manual", reason: "profile \u91CC\u6709\u4E0D\u6B62\u4E00\u6BB5 pep-cli \u56F4\u680F\uFF0C\u4E0D\u731C\u8BE5\u6362\u54EA\u4E00\u6BB5" };
896
+ }
897
+ return {
898
+ action: "replace",
899
+ content: existing.slice(0, begin) + block + existing.slice(end + END.length)
900
+ };
901
+ }
902
+ function stamp(at) {
903
+ return at.toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "");
904
+ }
905
+ async function persistMavenEnv(credential, target = resolveTarget(), now = () => /* @__PURE__ */ new Date()) {
906
+ if (target.kind === "windows") {
907
+ await run("setx", [USERNAME_VAR, credential.username]);
908
+ await run("setx", [PASSWORD_VAR, credential.password]);
909
+ return { status: "windows" };
910
+ }
911
+ const existing = await (0, import_promises3.readFile)(target.path, "utf8").catch((error) => {
912
+ if (error.code === "ENOENT") return null;
913
+ throw error;
914
+ });
915
+ const plan = planProfile(existing, credential);
916
+ if (plan.action === "manual") {
917
+ return { status: "manual", reason: plan.reason, path: target.path };
918
+ }
919
+ let backupPath;
920
+ if (existing !== null) {
921
+ backupPath = `${target.path}.bak-${stamp(now())}`;
922
+ try {
923
+ await (0, import_promises3.writeFile)(backupPath, existing, { encoding: "utf8", flag: "wx" });
924
+ } catch (error) {
925
+ return {
926
+ status: "manual",
927
+ reason: `\u5907\u4EFD\u5199\u4E0D\u6210\uFF08${backupPath}\uFF1A${error.message}\uFF09\uFF0C\u56E0\u6B64\u6CA1\u6709\u6539\u52A8 profile`,
928
+ path: target.path
929
+ };
930
+ }
931
+ }
932
+ await (0, import_promises3.writeFile)(target.path, plan.content, "utf8");
933
+ return plan.action === "replace" ? { status: "profile-replaced", path: target.path, backupPath } : { status: "profile-appended", path: target.path, ...backupPath ? { backupPath } : {} };
934
+ }
935
+
936
+ // src/nexus-service.ts
937
+ var CREDENTIAL_PATH = "/api/nexus/credential";
938
+ function describeFailure2(status, code) {
939
+ if (status === 401) {
940
+ return "PEP rejected the access token. Run `pep auth login` again.";
941
+ }
942
+ if (status === 403 && code === "32003") {
943
+ return "Your organisation's contract does not include Maven repository access. Contact your Newland representative \u2014 re-logging in will not change this.";
944
+ }
945
+ if (status === 403) {
946
+ return "This access token carries no `nexus-credentials:write` scope. Ask an operator to add it to this client's allowed_scopes in PEP, then run `pep auth login` again \u2014 existing tokens do not gain new scopes.";
947
+ }
948
+ if (status === 409) {
949
+ return "This organisation already has a Maven credential. PEP does not store the password, so it cannot be shown again \u2014 ask an operator to reset it in Nexus, or reuse the one you saved earlier.";
950
+ }
951
+ if (status === 503) {
952
+ return "PEP could not reach Nexus (not configured, or upstream is down). This is a platform-side problem \u2014 retrying will not help. Report it to a PEP operator.";
953
+ }
954
+ return `PEP answered ${status} when asking for a Maven credential.`;
955
+ }
956
+ async function errorCodeOf(response) {
957
+ try {
958
+ const body = await response.json();
959
+ return typeof body.code === "string" ? body.code : void 0;
960
+ } catch {
961
+ return void 0;
962
+ }
963
+ }
964
+ async function fetchNexusCredential(dependencies) {
965
+ const doFetch = dependencies.fetch ?? globalThis.fetch;
966
+ const response = await doFetch(`${dependencies.issuer.replace(/\/+$/, "")}${CREDENTIAL_PATH}`, {
967
+ method: "POST",
968
+ headers: {
969
+ Authorization: `Bearer ${dependencies.accessToken}`,
970
+ Accept: "application/json"
971
+ }
972
+ });
973
+ if (!response.ok) {
974
+ throw new Error(describeFailure2(response.status, await errorCodeOf(response)));
975
+ }
976
+ const body = await response.json();
977
+ const username = body.data?.username;
978
+ const password = body.data?.password;
979
+ if (typeof username !== "string" || typeof password !== "string") {
980
+ throw new Error("PEP returned a credential without a username or password.");
981
+ }
982
+ return { username, password };
983
+ }
984
+ async function setupNexusCredential(dependencies) {
985
+ const credential = await fetchNexusCredential(dependencies);
986
+ const target = dependencies.target ?? resolveTarget(dependencies.os);
987
+ const lines = exportLines(credential, dependencies.os);
988
+ const persisted = await persistMavenEnv(credential, target, dependencies.now).catch(
989
+ (error) => ({
990
+ status: "manual",
991
+ reason: error.message,
992
+ ...target.kind === "profile" ? { path: target.path } : {}
993
+ })
994
+ );
995
+ return { username: credential.username, lines, persisted };
996
+ }
997
+
998
+ // src/skills-service.ts
999
+ var import_promises4 = require("node:fs/promises");
1000
+ var import_node_os3 = require("node:os");
1001
+ var import_node_path4 = require("node:path");
1002
+ var import_node_crypto2 = require("node:crypto");
772
1003
  var import_node_zlib = require("node:zlib");
773
1004
 
774
1005
  // src/tar.ts
@@ -846,35 +1077,58 @@ function readTar(bytes) {
846
1077
 
847
1078
  // src/skills-service.ts
848
1079
  var ARCHIVE_PATH = "/api/skills/archive";
849
- var SKILLS_SEGMENT = "skills";
1080
+ var SKILL_MANIFEST = "SKILL.md";
850
1081
  function planSkillFiles(entries) {
851
- const files = [];
1082
+ const roots = [];
852
1083
  for (const entry of entries) {
853
- const segments = entry.path.split("/").filter((one) => one !== "" && one !== ".");
854
- if (segments.some((one) => one === "..")) {
1084
+ const segments = segmentsOf(entry.path);
1085
+ if (segments.at(-1) !== SKILL_MANIFEST) continue;
1086
+ const root = segments.slice(0, -1);
1087
+ if (root.length < 2) continue;
1088
+ roots.push(root);
1089
+ }
1090
+ const seen = /* @__PURE__ */ new Map();
1091
+ for (const root of roots) {
1092
+ const name = root[root.length - 1];
1093
+ const where = root.join("/");
1094
+ const first = seen.get(name);
1095
+ if (first !== void 0) {
855
1096
  throw new Error(
856
- `Refusing an archive entry that escapes its directory: ${entry.path}`
1097
+ `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.`
857
1098
  );
858
1099
  }
859
- const withoutRoot = segments.slice(1);
860
- const relative2 = withoutRoot[0] === SKILLS_SEGMENT ? withoutRoot.slice(1) : withoutRoot;
861
- if (relative2.length < 2) continue;
1100
+ seen.set(name, where);
1101
+ }
1102
+ const files = [];
1103
+ for (const entry of entries) {
1104
+ const segments = segmentsOf(entry.path);
1105
+ const root = roots.filter((r) => r.length < segments.length && r.every((seg, i) => segments[i] === seg)).sort((x, y) => y.length - x.length)[0];
1106
+ if (!root) continue;
862
1107
  files.push({
863
- skill: relative2[0],
864
- path: relative2.slice(1).join("/"),
1108
+ skill: root[root.length - 1],
1109
+ path: segments.slice(root.length).join("/"),
865
1110
  data: entry.data
866
1111
  });
867
1112
  }
868
1113
  return files;
869
1114
  }
870
- function describeFailure2(status) {
1115
+ function segmentsOf(path) {
1116
+ const segments = path.split("/").filter((one) => one !== "" && one !== ".");
1117
+ if (segments.some((one) => one === "..")) {
1118
+ throw new Error(`Refusing an archive entry that escapes its directory: ${path}`);
1119
+ }
1120
+ return segments;
1121
+ }
1122
+ function describeFailure3(status) {
871
1123
  if (status === 401)
872
1124
  return "PEP rejected the access token. Run `pep auth login` again.";
873
1125
  if (status === 403) {
874
1126
  return "This access token carries no `skills:read` scope. Ask an operator to add it to this client's allowed_scopes in PEP, then run `pep auth login` again \u2014 existing tokens do not gain new scopes.";
875
1127
  }
1128
+ if (status === 400)
1129
+ return "PEP did not accept that repository address. It must be an https URL on the platform's GitLab host, or a <group>/<project> path on it.";
876
1130
  if (status === 404) {
877
- return "This PEP deployment does not serve skills \u2014 it has no /api/skills/archive endpoint. Ask an operator whether skills are enabled here.";
1131
+ return "No such repository or ref on the platform's GitLab (or this PEP deployment has no /api/skills/archive endpoint at all). Check the address, then ask an operator whether skills are enabled here.";
878
1132
  }
879
1133
  if (status === 503) {
880
1134
  return "PEP could not reach the skills repository. Retry shortly, or ask an operator whether this deployment serves skills.";
@@ -882,95 +1136,121 @@ function describeFailure2(status) {
882
1136
  return `PEP answered ${status}.`;
883
1137
  }
884
1138
  async function physicalDir(path) {
885
- const absolute = (0, import_node_path3.resolve)(path);
1139
+ const absolute = (0, import_node_path4.resolve)(path);
886
1140
  try {
887
- return (0, import_node_path3.join)(await (0, import_promises3.realpath)((0, import_node_path3.dirname)(absolute)), (0, import_node_path3.basename)(absolute));
1141
+ return (0, import_node_path4.join)(await (0, import_promises4.realpath)((0, import_node_path4.dirname)(absolute)), (0, import_node_path4.basename)(absolute));
888
1142
  } catch {
889
1143
  return absolute;
890
1144
  }
891
1145
  }
892
1146
  async function linkSkill(canonicalSkillDir, linkPath) {
893
- await (0, import_promises3.rm)(linkPath, { recursive: true, force: true });
894
- const linkDir = (0, import_node_path3.dirname)(linkPath);
895
- await (0, import_promises3.mkdir)(linkDir, { recursive: true });
1147
+ await (0, import_promises4.rm)(linkPath, { recursive: true, force: true });
1148
+ const linkDir = (0, import_node_path4.dirname)(linkPath);
1149
+ await (0, import_promises4.mkdir)(linkDir, { recursive: true });
896
1150
  try {
897
- const junction = (0, import_node_os2.platform)() === "win32";
898
- await (0, import_promises3.symlink)(
899
- junction ? canonicalSkillDir : (0, import_node_path3.relative)(await physicalDir(linkDir), canonicalSkillDir),
1151
+ const junction = (0, import_node_os3.platform)() === "win32";
1152
+ await (0, import_promises4.symlink)(
1153
+ junction ? canonicalSkillDir : (0, import_node_path4.relative)(await physicalDir(linkDir), canonicalSkillDir),
900
1154
  linkPath,
901
1155
  junction ? "junction" : void 0
902
1156
  );
903
1157
  return "linked";
904
1158
  } catch {
905
- await (0, import_promises3.cp)(canonicalSkillDir, linkPath, { recursive: true });
1159
+ await (0, import_promises4.cp)(canonicalSkillDir, linkPath, { recursive: true });
906
1160
  return "copied";
907
1161
  }
908
1162
  }
909
- async function syncSkills(dependencies) {
1163
+ function hashSkill(files) {
1164
+ const hash = (0, import_node_crypto2.createHash)("sha256");
1165
+ for (const file of [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
1166
+ hash.update(file.path);
1167
+ hash.update("\0");
1168
+ hash.update(file.data);
1169
+ hash.update("\0");
1170
+ }
1171
+ return hash.digest("hex").slice(0, 32);
1172
+ }
1173
+ async function updateSkills(dependencies) {
910
1174
  const doFetch = dependencies.fetch ?? globalThis.fetch;
911
- const response = await doFetch(`${dependencies.issuer}${ARCHIVE_PATH}`, {
1175
+ const name = dependencies.source;
1176
+ const url = new URL(`${dependencies.issuer.replace(/\/+$/, "")}${ARCHIVE_PATH}`);
1177
+ url.searchParams.set("source", name);
1178
+ const response = await doFetch(url, {
912
1179
  headers: { Authorization: `Bearer ${dependencies.accessToken}` }
913
1180
  });
914
1181
  if (!response.ok) {
915
1182
  await response.body?.cancel();
916
- throw new Error(describeFailure2(response.status));
1183
+ throw new Error(describeFailure3(response.status));
917
1184
  }
918
1185
  const commit = response.headers.get("x-skills-commit") ?? void 0;
919
- const previous = await dependencies.stateStore.read();
920
- if (commit !== void 0 && commit === previous?.commit && previous.directory === dependencies.directory) {
1186
+ const state = await dependencies.stateStore.read();
1187
+ const previous = state?.packages[name];
1188
+ if (commit !== void 0 && commit === previous?.commit && state?.directory === dependencies.directory) {
921
1189
  await response.body?.cancel();
922
- return { status: "unchanged", commit };
1190
+ return { status: "unchanged", name, commit };
923
1191
  }
924
1192
  const gzipped = new Uint8Array(await response.arrayBuffer());
925
1193
  const files = planSkillFiles(readTar((0, import_node_zlib.gunzipSync)(gzipped)));
926
- const skills = [...new Set(files.map((file) => file.skill))].sort();
1194
+ const bySkill = /* @__PURE__ */ new Map();
1195
+ for (const file of files) {
1196
+ const group = bySkill.get(file.skill);
1197
+ if (group) group.push(file);
1198
+ else bySkill.set(file.skill, [file]);
1199
+ }
1200
+ const skills = [...bySkill.keys()].sort();
1201
+ const hashes = {};
1202
+ const updated = [];
1203
+ const unchanged = [];
927
1204
  for (const skill of skills) {
928
- await (0, import_promises3.rm)((0, import_node_path3.join)(dependencies.directory, skill), {
929
- recursive: true,
930
- force: true
931
- });
1205
+ const hash = hashSkill(bySkill.get(skill));
1206
+ hashes[skill] = hash;
1207
+ const before = previous?.skills[skill];
1208
+ if (before !== void 0 && before !== "" && before === hash) unchanged.push(skill);
1209
+ else updated.push(skill);
1210
+ }
1211
+ for (const skill of skills) {
1212
+ await (0, import_promises4.rm)((0, import_node_path4.join)(dependencies.directory, skill), { recursive: true, force: true });
932
1213
  }
933
1214
  for (const file of files) {
934
- const target = (0, import_node_path3.join)(
935
- dependencies.directory,
936
- file.skill,
937
- ...file.path.split("/")
938
- );
939
- await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(target), { recursive: true });
940
- await (0, import_promises3.writeFile)(target, file.data);
1215
+ const target = (0, import_node_path4.join)(dependencies.directory, file.skill, ...file.path.split("/"));
1216
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(target), { recursive: true });
1217
+ await (0, import_promises4.writeFile)(target, file.data);
941
1218
  }
942
1219
  let copiedCount = 0;
943
1220
  if (dependencies.linkInto !== void 0) {
944
1221
  for (const skill of skills) {
945
1222
  const how = await linkSkill(
946
- (0, import_node_path3.join)(dependencies.directory, skill),
947
- (0, import_node_path3.join)(dependencies.linkInto, skill)
1223
+ (0, import_node_path4.join)(dependencies.directory, skill),
1224
+ (0, import_node_path4.join)(dependencies.linkInto, skill)
948
1225
  );
949
1226
  if (how === "copied") copiedCount += 1;
950
1227
  }
951
1228
  }
952
- const removed = (previous?.skills ?? []).filter(
953
- (skill) => !skills.includes(skill)
954
- );
1229
+ const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !skills.includes(skill));
955
1230
  for (const skill of removed) {
956
- await (0, import_promises3.rm)((0, import_node_path3.join)(previous?.directory ?? dependencies.directory, skill), {
1231
+ await (0, import_promises4.rm)((0, import_node_path4.join)(state?.directory ?? dependencies.directory, skill), {
957
1232
  recursive: true,
958
1233
  force: true
959
1234
  });
960
- if (previous?.linkedInto !== void 0) {
961
- await (0, import_promises3.rm)((0, import_node_path3.join)(previous.linkedInto, skill), { recursive: true, force: true });
1235
+ if (state?.linkedInto !== void 0) {
1236
+ await (0, import_promises4.rm)((0, import_node_path4.join)(state.linkedInto, skill), { recursive: true, force: true });
962
1237
  }
963
1238
  }
964
- const state = {
965
- version: 1,
966
- skills,
1239
+ await dependencies.stateStore.write({
1240
+ version: 3,
967
1241
  directory: dependencies.directory,
968
1242
  ...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
969
- ...commit ? { commit } : {}
970
- };
971
- await dependencies.stateStore.write(state);
1243
+ packages: {
1244
+ // 别的包的账原样留着 —— 装 B 不该把 A 的记录抹掉,那会让 A 铺的目录从此没人清理。
1245
+ ...state?.packages ?? {},
1246
+ [name]: { ...commit ? { commit } : {}, skills: hashes }
1247
+ }
1248
+ });
972
1249
  return {
973
1250
  status: "written",
1251
+ name,
1252
+ updated,
1253
+ unchanged,
974
1254
  skills,
975
1255
  fileCount: files.length,
976
1256
  removed,
@@ -980,9 +1260,13 @@ async function syncSkills(dependencies) {
980
1260
  ...commit ? { commit } : {}
981
1261
  };
982
1262
  }
1263
+ async function installedPackages(store) {
1264
+ const state = await store.read();
1265
+ return Object.keys(state?.packages ?? {}).sort();
1266
+ }
983
1267
 
984
1268
  // src/cli.ts
985
- var VERSION = "0.1.3";
1269
+ var VERSION = "0.3.0";
986
1270
  function usage() {
987
1271
  return `PEP CLI ${VERSION}
988
1272
 
@@ -991,9 +1275,12 @@ Usage:
991
1275
  pep auth status
992
1276
  pep auth token
993
1277
  pep auth logout
994
- pep skills sync [--dir <path>]
1278
+ pep skills list
1279
+ pep skills add <repo-url | group/project[@ref]> [--dir <path>]
1280
+ pep skills update [<repo>...] [--dir <path>]
995
1281
  pep docs list [--docs-url <url>]
996
1282
  pep docs get <path> [--docs-url <url>]
1283
+ pep nexus setup
997
1284
 
998
1285
  The issuer is built into this executable. --issuer overrides it for that one command and is
999
1286
  NOT remembered \u2014 pass it every time you log in against a non-default deployment.
@@ -1002,7 +1289,21 @@ one. It defaults to the docs platform \u2014 a token minted without it is reject
1002
1289
  resource server, and their answer looks exactly like "this token does not exist".
1003
1290
  Use \`pep auth token\` when another agent needs a fresh bearer token.
1004
1291
 
1005
- \`pep skills sync\` fetches the latest skills from PEP and writes them to the shared agent
1292
+ \`pep skills add\` takes a repository on the platform's own GitLab, either as a full https URL or
1293
+ as the path with the host left off:
1294
+
1295
+ pep skills add https://git.example.com/group/sub/project
1296
+ pep skills add https://git.example.com/group/sub/project/-/tree/some-branch
1297
+ pep skills add group/sub/project
1298
+ pep skills add group/sub/project@some-branch
1299
+
1300
+ Anything on another host is refused. PEP fetches it with its own read-only service account, so no
1301
+ GitLab credential ever reaches this machine. \`pep skills list\` shows what you have added and
1302
+ \`pep skills update\` refreshes it \u2014 all of it, or just the repositories you name. One repository
1303
+ may hold more than one skill: every directory containing a SKILL.md becomes one, wherever it sits,
1304
+ and update reports which of them actually changed rather than just that the repository moved.
1305
+
1306
+ \`pep skills update\` fetches the latest skills from PEP and writes them to the shared agent
1006
1307
  directory (${defaultSkillsDirectory()}), which Codex, Cursor, Amp and ~20 other agents read
1007
1308
  directly. Claude Code keeps its own directory, so each skill is also linked into
1008
1309
  ${claudeSkillsDirectory()} \u2014 one copy on disk, updated in one place. Where links are not
@@ -1014,7 +1315,38 @@ hand is left alone.
1014
1315
 
1015
1316
  \`pep docs list\` prints the documents this account can read (path + description); feed a path
1016
1317
  straight to \`pep docs get\` to print that document as markdown on stdout. The documentation
1017
- site is built in (${DEFAULT_DOCS_URL}); --docs-url overrides it and is then remembered.`;
1318
+ site is built in (${DEFAULT_DOCS_URL}); --docs-url overrides it and is then remembered.
1319
+
1320
+ \`pep nexus setup\` asks PEP for this organisation's Maven repository credential and saves it as
1321
+ the two environment variables the Newland Android SDK reads \u2014 \`${USERNAME_VAR}\` and
1322
+ \`${PASSWORD_VAR}\` \u2014 so you do not copy anything by hand. On Windows it writes them to your user
1323
+ environment (\`setx\`); on macOS it keeps a marked block in your shell profile, backing the file up
1324
+ first and touching nothing else. Either way persistence only affects NEW shells, so the two lines
1325
+ are also printed on stdout: \`eval "$(pep nexus setup)"\` uses them in the current one.
1326
+ Only Android needs this \u2014 the Windows and iOS SDKs are cloned from Git, not pulled from Maven.
1327
+ The password is shown by PEP once and never stored, so a second run reports 409 rather than
1328
+ handing it out again.`;
1329
+ }
1330
+ function reportUpdate(result) {
1331
+ if (result.status === "unchanged") {
1332
+ console.log(`${result.name}: already at ${result.commit}. Nothing to do.`);
1333
+ return;
1334
+ }
1335
+ console.log(
1336
+ result.updated.length > 0 ? `${result.name}: updated ${result.updated.join(" ")}` : `${result.name}: no skill changed (the repository moved, but not these)`
1337
+ );
1338
+ if (result.unchanged.length > 0) {
1339
+ console.log(` unchanged: ${result.unchanged.join(" ")}`);
1340
+ }
1341
+ console.log(` ${result.fileCount} file(s) in ${result.directory}`);
1342
+ if (result.linkedInto !== void 0) {
1343
+ console.log(
1344
+ result.copiedCount === void 0 ? ` linked into ${result.linkedInto} for Claude Code` : ` linked into ${result.linkedInto} for Claude Code (${result.copiedCount} copied instead \u2014 links unavailable here)`
1345
+ );
1346
+ }
1347
+ if (result.commit) console.log(` commit: ${result.commit}`);
1348
+ if (result.removed.length > 0)
1349
+ console.log(` removed (gone upstream): ${result.removed.join(" ")}`);
1018
1350
  }
1019
1351
  function repeatedOption(args, name) {
1020
1352
  const values = [];
@@ -1062,7 +1394,7 @@ async function main() {
1062
1394
  return;
1063
1395
  }
1064
1396
  const group = args.shift();
1065
- if (group !== "auth" && group !== "skills" && group !== "docs")
1397
+ if (group !== "auth" && group !== "skills" && group !== "docs" && group !== "nexus")
1066
1398
  throw new Error(`Unknown command.
1067
1399
 
1068
1400
  ${usage()}`);
@@ -1104,36 +1436,86 @@ ${usage()}`);
1104
1436
  process.stdout.write(await fetchDocContent(dependencies, path));
1105
1437
  return;
1106
1438
  }
1107
- if (group === "skills") {
1108
- if (command !== "sync") throw new Error(`Unknown skills command.
1439
+ if (group === "nexus") {
1440
+ if (command !== "setup") throw new Error(`Unknown nexus command.
1109
1441
 
1110
1442
  ${usage()}`);
1111
- const explicitDirectory = option(args, "--dir");
1112
- const directory = explicitDirectory ?? defaultSkillsDirectory();
1113
1443
  if (args.length > 0) throw new Error(`Unknown option: ${args[0]}`);
1114
1444
  const authorization = await auth.currentAuthorization();
1115
- const result = await syncSkills({
1445
+ const result = await setupNexusCredential({
1116
1446
  issuer: authorization.issuer,
1117
- accessToken: authorization.accessToken,
1118
- directory,
1119
- ...explicitDirectory === void 0 ? { linkInto: claudeSkillsDirectory() } : {},
1120
- stateStore: fileSkillsStateStore()
1447
+ accessToken: authorization.accessToken
1121
1448
  });
1122
- if (result.status === "unchanged") {
1123
- console.log(`Already at ${result.commit}. Nothing to do.`);
1449
+ console.log(result.lines);
1450
+ const persisted = result.persisted;
1451
+ if (persisted.status === "windows") {
1452
+ console.error(`Saved ${USERNAME_VAR} and ${PASSWORD_VAR} to your Windows user environment.`);
1453
+ console.error("Existing terminals do not see them \u2014 open a new one (or use the lines above).");
1454
+ } else if (persisted.status === "manual") {
1455
+ console.error(
1456
+ `Nothing was written${persisted.path ? ` to ${persisted.path}` : ""}: ${persisted.reason}`
1457
+ );
1458
+ console.error("The two lines above are the only copy \u2014 PEP does not store the password.");
1459
+ } else {
1460
+ console.error(
1461
+ persisted.status === "profile-replaced" ? `Replaced the pep-cli block in ${persisted.path}` : `Appended a pep-cli block to ${persisted.path}`
1462
+ );
1463
+ if (persisted.backupPath) console.error(`Backup: ${persisted.backupPath}`);
1464
+ console.error("New shells will have it; this one will not until you re-source the profile.");
1465
+ }
1466
+ console.error(`Nexus user: ${result.username}`);
1467
+ return;
1468
+ }
1469
+ if (group === "skills") {
1470
+ if (command !== "update" && command !== "add" && command !== "list") {
1471
+ throw new Error(`Unknown skills command.
1472
+
1473
+ ${usage()}`);
1474
+ }
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]}`);
1484
+ const authorization = await auth.currentAuthorization();
1485
+ const remote = { issuer: authorization.issuer, accessToken: authorization.accessToken };
1486
+ if (command === "list") {
1487
+ const installed2 = await installedPackages(fileSkillsStateStore());
1488
+ if (installed2.length === 0) {
1489
+ console.error("Nothing added yet. Run `pep skills add <repo>`.");
1490
+ return;
1491
+ }
1492
+ for (const one of installed2) console.log(one);
1493
+ console.error("\n`pep skills update` refreshes all of them.");
1124
1494
  return;
1125
1495
  }
1126
- console.log(
1127
- `Wrote ${result.fileCount} file(s) across ${result.skills.length} skill(s) to ${result.directory}`
1128
- );
1129
- if (result.linkedInto !== void 0) {
1130
- console.log(
1131
- result.copiedCount === void 0 ? `Linked into ${result.linkedInto} for Claude Code` : `Linked into ${result.linkedInto} for Claude Code (${result.copiedCount} copied instead \u2014 links unavailable here)`
1496
+ 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) {
1500
+ console.error("Nothing added yet. Run `pep skills add <repo>`.");
1501
+ return;
1502
+ }
1503
+ const unknown = named.filter((one) => !installed.includes(one));
1504
+ if (unknown.length > 0) {
1505
+ throw new Error(
1506
+ `Not added yet: ${unknown.join(" ")}. Run \`pep skills add <repo>\` first, or \`pep skills list\` to see what is.`
1132
1507
  );
1133
1508
  }
1134
- if (result.commit) console.log(`Commit: ${result.commit}`);
1135
- if (result.removed.length > 0)
1136
- console.log(`Removed (gone upstream): ${result.removed.join(" ")}`);
1509
+ 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);
1518
+ }
1137
1519
  return;
1138
1520
  }
1139
1521
  if (command === "login") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newlandnpt/pep-cli",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",