@newlandnpt/pep-cli 0.1.2 → 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.
- package/dist/pep.cjs +480 -89
- 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 = [
|
|
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
|
-
|
|
80
|
-
(
|
|
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
|
-
|
|
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;
|
|
@@ -196,12 +264,12 @@ async function waitForAuthorizationCallback(args) {
|
|
|
196
264
|
throw new Error("CLI redirect URI must be an http://localhost URL with an explicit port.");
|
|
197
265
|
}
|
|
198
266
|
let server;
|
|
199
|
-
return new Promise((
|
|
267
|
+
return new Promise((resolve2, reject) => {
|
|
200
268
|
const finish = (error, code) => {
|
|
201
269
|
clearTimeout(timeout);
|
|
202
270
|
server?.close();
|
|
203
271
|
if (error) reject(error);
|
|
204
|
-
else
|
|
272
|
+
else resolve2(code);
|
|
205
273
|
};
|
|
206
274
|
const timeout = setTimeout(
|
|
207
275
|
() => finish(new Error("Login timed out while waiting for the browser callback.")),
|
|
@@ -244,7 +312,7 @@ async function waitForAuthorizationCallback(args) {
|
|
|
244
312
|
function openBrowser(url) {
|
|
245
313
|
const command = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
246
314
|
const commandArgs = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
247
|
-
return new Promise((
|
|
315
|
+
return new Promise((resolve2, reject) => {
|
|
248
316
|
const child = (0, import_node_child_process.spawn)(command, commandArgs, {
|
|
249
317
|
detached: true,
|
|
250
318
|
stdio: "ignore",
|
|
@@ -253,7 +321,7 @@ function openBrowser(url) {
|
|
|
253
321
|
child.once("error", reject);
|
|
254
322
|
child.once("spawn", () => {
|
|
255
323
|
child.unref();
|
|
256
|
-
|
|
324
|
+
resolve2();
|
|
257
325
|
});
|
|
258
326
|
});
|
|
259
327
|
}
|
|
@@ -364,7 +432,7 @@ var REFRESH_EARLY_MS = 60 * 1e3;
|
|
|
364
432
|
var LOCK_STALE_MS = 30 * 1e3;
|
|
365
433
|
var LOCK_WAIT_MS = 10 * 1e3;
|
|
366
434
|
async function delay(milliseconds) {
|
|
367
|
-
await new Promise((
|
|
435
|
+
await new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
368
436
|
}
|
|
369
437
|
async function withAuthorizationLock(path, action) {
|
|
370
438
|
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
@@ -558,7 +626,7 @@ function encodedPowerShell(script) {
|
|
|
558
626
|
return Buffer.from(script, "utf16le").toString("base64");
|
|
559
627
|
}
|
|
560
628
|
async function runPowerShell(script, input) {
|
|
561
|
-
return new Promise((
|
|
629
|
+
return new Promise((resolve2, reject) => {
|
|
562
630
|
const child = (0, import_node_child_process2.spawn)(
|
|
563
631
|
"powershell.exe",
|
|
564
632
|
[
|
|
@@ -581,7 +649,7 @@ async function runPowerShell(script, input) {
|
|
|
581
649
|
);
|
|
582
650
|
child.once("close", (code) => {
|
|
583
651
|
clearTimeout(timeout);
|
|
584
|
-
if (code === 0)
|
|
652
|
+
if (code === 0) resolve2(stdout.trim());
|
|
585
653
|
else
|
|
586
654
|
reject(
|
|
587
655
|
new Error(
|
|
@@ -631,7 +699,7 @@ $raw = [Console]::In.ReadToEnd()
|
|
|
631
699
|
}
|
|
632
700
|
var SECURITY_NOT_FOUND = 44;
|
|
633
701
|
async function runSecurity(args) {
|
|
634
|
-
return new Promise((
|
|
702
|
+
return new Promise((resolve2, reject) => {
|
|
635
703
|
const child = (0, import_node_child_process2.spawn)("/usr/bin/security", [...args], {
|
|
636
704
|
stdio: ["ignore", "pipe", "pipe"]
|
|
637
705
|
});
|
|
@@ -646,7 +714,7 @@ async function runSecurity(args) {
|
|
|
646
714
|
);
|
|
647
715
|
child.once("close", (code) => {
|
|
648
716
|
clearTimeout(timeout);
|
|
649
|
-
|
|
717
|
+
resolve2({ code: code ?? -1, stdout, stderr });
|
|
650
718
|
});
|
|
651
719
|
});
|
|
652
720
|
}
|
|
@@ -765,10 +833,173 @@ async function fetchDocContent(dependencies, path) {
|
|
|
765
833
|
return response.text();
|
|
766
834
|
}
|
|
767
835
|
|
|
768
|
-
// src/
|
|
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,122 +1077,180 @@ function readTar(bytes) {
|
|
|
846
1077
|
|
|
847
1078
|
// src/skills-service.ts
|
|
848
1079
|
var ARCHIVE_PATH = "/api/skills/archive";
|
|
849
|
-
var
|
|
1080
|
+
var SKILL_MANIFEST = "SKILL.md";
|
|
850
1081
|
function planSkillFiles(entries) {
|
|
851
|
-
const
|
|
1082
|
+
const roots = [];
|
|
852
1083
|
for (const entry of entries) {
|
|
853
|
-
const segments = entry.path
|
|
854
|
-
if (segments.
|
|
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
|
-
`
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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:
|
|
864
|
-
path:
|
|
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
|
|
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 "
|
|
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.";
|
|
881
1135
|
}
|
|
882
1136
|
return `PEP answered ${status}.`;
|
|
883
1137
|
}
|
|
1138
|
+
async function physicalDir(path) {
|
|
1139
|
+
const absolute = (0, import_node_path4.resolve)(path);
|
|
1140
|
+
try {
|
|
1141
|
+
return (0, import_node_path4.join)(await (0, import_promises4.realpath)((0, import_node_path4.dirname)(absolute)), (0, import_node_path4.basename)(absolute));
|
|
1142
|
+
} catch {
|
|
1143
|
+
return absolute;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
884
1146
|
async function linkSkill(canonicalSkillDir, linkPath) {
|
|
885
|
-
await (0,
|
|
886
|
-
|
|
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 });
|
|
887
1150
|
try {
|
|
888
|
-
const junction = (0,
|
|
889
|
-
await (0,
|
|
890
|
-
junction ? canonicalSkillDir : (0,
|
|
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),
|
|
891
1154
|
linkPath,
|
|
892
1155
|
junction ? "junction" : void 0
|
|
893
1156
|
);
|
|
894
1157
|
return "linked";
|
|
895
1158
|
} catch {
|
|
896
|
-
await (0,
|
|
1159
|
+
await (0, import_promises4.cp)(canonicalSkillDir, linkPath, { recursive: true });
|
|
897
1160
|
return "copied";
|
|
898
1161
|
}
|
|
899
1162
|
}
|
|
900
|
-
|
|
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) {
|
|
901
1174
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
902
|
-
const
|
|
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, {
|
|
903
1179
|
headers: { Authorization: `Bearer ${dependencies.accessToken}` }
|
|
904
1180
|
});
|
|
905
1181
|
if (!response.ok) {
|
|
906
1182
|
await response.body?.cancel();
|
|
907
|
-
throw new Error(
|
|
1183
|
+
throw new Error(describeFailure3(response.status));
|
|
908
1184
|
}
|
|
909
1185
|
const commit = response.headers.get("x-skills-commit") ?? void 0;
|
|
910
|
-
const
|
|
911
|
-
|
|
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) {
|
|
912
1189
|
await response.body?.cancel();
|
|
913
|
-
return { status: "unchanged", commit };
|
|
1190
|
+
return { status: "unchanged", name, commit };
|
|
914
1191
|
}
|
|
915
1192
|
const gzipped = new Uint8Array(await response.arrayBuffer());
|
|
916
1193
|
const files = planSkillFiles(readTar((0, import_node_zlib.gunzipSync)(gzipped)));
|
|
917
|
-
const
|
|
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 = [];
|
|
918
1204
|
for (const skill of skills) {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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 });
|
|
923
1213
|
}
|
|
924
1214
|
for (const file of files) {
|
|
925
|
-
const target = (0,
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
...file.path.split("/")
|
|
929
|
-
);
|
|
930
|
-
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(target), { recursive: true });
|
|
931
|
-
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);
|
|
932
1218
|
}
|
|
933
1219
|
let copiedCount = 0;
|
|
934
1220
|
if (dependencies.linkInto !== void 0) {
|
|
935
1221
|
for (const skill of skills) {
|
|
936
1222
|
const how = await linkSkill(
|
|
937
|
-
(0,
|
|
938
|
-
(0,
|
|
1223
|
+
(0, import_node_path4.join)(dependencies.directory, skill),
|
|
1224
|
+
(0, import_node_path4.join)(dependencies.linkInto, skill)
|
|
939
1225
|
);
|
|
940
1226
|
if (how === "copied") copiedCount += 1;
|
|
941
1227
|
}
|
|
942
1228
|
}
|
|
943
|
-
const removed = (previous?.skills ??
|
|
944
|
-
(skill) => !skills.includes(skill)
|
|
945
|
-
);
|
|
1229
|
+
const removed = Object.keys(previous?.skills ?? {}).filter((skill) => !skills.includes(skill));
|
|
946
1230
|
for (const skill of removed) {
|
|
947
|
-
await (0,
|
|
1231
|
+
await (0, import_promises4.rm)((0, import_node_path4.join)(state?.directory ?? dependencies.directory, skill), {
|
|
948
1232
|
recursive: true,
|
|
949
1233
|
force: true
|
|
950
1234
|
});
|
|
951
|
-
if (
|
|
952
|
-
await (0,
|
|
1235
|
+
if (state?.linkedInto !== void 0) {
|
|
1236
|
+
await (0, import_promises4.rm)((0, import_node_path4.join)(state.linkedInto, skill), { recursive: true, force: true });
|
|
953
1237
|
}
|
|
954
1238
|
}
|
|
955
|
-
|
|
956
|
-
version:
|
|
957
|
-
skills,
|
|
1239
|
+
await dependencies.stateStore.write({
|
|
1240
|
+
version: 3,
|
|
958
1241
|
directory: dependencies.directory,
|
|
959
1242
|
...dependencies.linkInto !== void 0 ? { linkedInto: dependencies.linkInto } : {},
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1243
|
+
packages: {
|
|
1244
|
+
// 别的包的账原样留着 —— 装 B 不该把 A 的记录抹掉,那会让 A 铺的目录从此没人清理。
|
|
1245
|
+
...state?.packages ?? {},
|
|
1246
|
+
[name]: { ...commit ? { commit } : {}, skills: hashes }
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
963
1249
|
return {
|
|
964
1250
|
status: "written",
|
|
1251
|
+
name,
|
|
1252
|
+
updated,
|
|
1253
|
+
unchanged,
|
|
965
1254
|
skills,
|
|
966
1255
|
fileCount: files.length,
|
|
967
1256
|
removed,
|
|
@@ -971,9 +1260,13 @@ async function syncSkills(dependencies) {
|
|
|
971
1260
|
...commit ? { commit } : {}
|
|
972
1261
|
};
|
|
973
1262
|
}
|
|
1263
|
+
async function installedPackages(store) {
|
|
1264
|
+
const state = await store.read();
|
|
1265
|
+
return Object.keys(state?.packages ?? {}).sort();
|
|
1266
|
+
}
|
|
974
1267
|
|
|
975
1268
|
// src/cli.ts
|
|
976
|
-
var VERSION = "0.
|
|
1269
|
+
var VERSION = "0.3.0";
|
|
977
1270
|
function usage() {
|
|
978
1271
|
return `PEP CLI ${VERSION}
|
|
979
1272
|
|
|
@@ -982,9 +1275,12 @@ Usage:
|
|
|
982
1275
|
pep auth status
|
|
983
1276
|
pep auth token
|
|
984
1277
|
pep auth logout
|
|
985
|
-
pep skills
|
|
1278
|
+
pep skills list
|
|
1279
|
+
pep skills add <repo-url | group/project[@ref]> [--dir <path>]
|
|
1280
|
+
pep skills update [<repo>...] [--dir <path>]
|
|
986
1281
|
pep docs list [--docs-url <url>]
|
|
987
1282
|
pep docs get <path> [--docs-url <url>]
|
|
1283
|
+
pep nexus setup
|
|
988
1284
|
|
|
989
1285
|
The issuer is built into this executable. --issuer overrides it for that one command and is
|
|
990
1286
|
NOT remembered \u2014 pass it every time you log in against a non-default deployment.
|
|
@@ -993,7 +1289,21 @@ one. It defaults to the docs platform \u2014 a token minted without it is reject
|
|
|
993
1289
|
resource server, and their answer looks exactly like "this token does not exist".
|
|
994
1290
|
Use \`pep auth token\` when another agent needs a fresh bearer token.
|
|
995
1291
|
|
|
996
|
-
\`pep skills
|
|
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
|
|
997
1307
|
directory (${defaultSkillsDirectory()}), which Codex, Cursor, Amp and ~20 other agents read
|
|
998
1308
|
directly. Claude Code keeps its own directory, so each skill is also linked into
|
|
999
1309
|
${claudeSkillsDirectory()} \u2014 one copy on disk, updated in one place. Where links are not
|
|
@@ -1005,7 +1315,38 @@ hand is left alone.
|
|
|
1005
1315
|
|
|
1006
1316
|
\`pep docs list\` prints the documents this account can read (path + description); feed a path
|
|
1007
1317
|
straight to \`pep docs get\` to print that document as markdown on stdout. The documentation
|
|
1008
|
-
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(" ")}`);
|
|
1009
1350
|
}
|
|
1010
1351
|
function repeatedOption(args, name) {
|
|
1011
1352
|
const values = [];
|
|
@@ -1053,7 +1394,7 @@ async function main() {
|
|
|
1053
1394
|
return;
|
|
1054
1395
|
}
|
|
1055
1396
|
const group = args.shift();
|
|
1056
|
-
if (group !== "auth" && group !== "skills" && group !== "docs")
|
|
1397
|
+
if (group !== "auth" && group !== "skills" && group !== "docs" && group !== "nexus")
|
|
1057
1398
|
throw new Error(`Unknown command.
|
|
1058
1399
|
|
|
1059
1400
|
${usage()}`);
|
|
@@ -1095,36 +1436,86 @@ ${usage()}`);
|
|
|
1095
1436
|
process.stdout.write(await fetchDocContent(dependencies, path));
|
|
1096
1437
|
return;
|
|
1097
1438
|
}
|
|
1098
|
-
if (group === "
|
|
1099
|
-
if (command !== "
|
|
1439
|
+
if (group === "nexus") {
|
|
1440
|
+
if (command !== "setup") throw new Error(`Unknown nexus command.
|
|
1100
1441
|
|
|
1101
1442
|
${usage()}`);
|
|
1102
|
-
const explicitDirectory = option(args, "--dir");
|
|
1103
|
-
const directory = explicitDirectory ?? defaultSkillsDirectory();
|
|
1104
1443
|
if (args.length > 0) throw new Error(`Unknown option: ${args[0]}`);
|
|
1105
1444
|
const authorization = await auth.currentAuthorization();
|
|
1106
|
-
const result = await
|
|
1445
|
+
const result = await setupNexusCredential({
|
|
1107
1446
|
issuer: authorization.issuer,
|
|
1108
|
-
accessToken: authorization.accessToken
|
|
1109
|
-
directory,
|
|
1110
|
-
...explicitDirectory === void 0 ? { linkInto: claudeSkillsDirectory() } : {},
|
|
1111
|
-
stateStore: fileSkillsStateStore()
|
|
1447
|
+
accessToken: authorization.accessToken
|
|
1112
1448
|
});
|
|
1113
|
-
|
|
1114
|
-
|
|
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.");
|
|
1115
1494
|
return;
|
|
1116
1495
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (
|
|
1121
|
-
console.
|
|
1122
|
-
|
|
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.`
|
|
1123
1507
|
);
|
|
1124
1508
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
+
}
|
|
1128
1519
|
return;
|
|
1129
1520
|
}
|
|
1130
1521
|
if (command === "login") {
|