@newlandnpt/pep-cli 0.1.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/pep.cjs +466 -84
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,11 +72,16 @@ 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 `
|
|
76
|
-
`https://pep.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
80
85
|
|
|
81
86
|
`pep auth token` writes only the access token to stdout, so agents can use it without parsing status
|
|
82
87
|
text. Refresh occurs automatically shortly before expiry and the rotated refresh token replaces the old
|
package/dist/pep.cjs
CHANGED
|
@@ -11,15 +11,23 @@ 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";
|
|
19
27
|
if (environment === "view") return "https://pep-webapp-view.onrender.com";
|
|
20
28
|
return "https://pep-webapp-dev.onrender.com";
|
|
21
29
|
}
|
|
22
|
-
var BUILD_ENVIRONMENT = false ? "development" : "
|
|
30
|
+
var BUILD_ENVIRONMENT = false ? "development" : "development";
|
|
23
31
|
var DEFAULT_ISSUER = issuerForEnvironment(BUILD_ENVIRONMENT);
|
|
24
32
|
function configuredIssuer(explicitIssuer) {
|
|
25
33
|
return explicitIssuer ?? DEFAULT_ISSUER;
|
|
@@ -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;
|
|
@@ -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,35 +1077,58 @@ 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.";
|
|
@@ -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,
|
|
1139
|
+
const absolute = (0, import_node_path4.resolve)(path);
|
|
886
1140
|
try {
|
|
887
|
-
return (0,
|
|
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,
|
|
894
|
-
const linkDir = (0,
|
|
895
|
-
await (0,
|
|
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,
|
|
898
|
-
await (0,
|
|
899
|
-
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),
|
|
900
1154
|
linkPath,
|
|
901
1155
|
junction ? "junction" : void 0
|
|
902
1156
|
);
|
|
903
1157
|
return "linked";
|
|
904
1158
|
} catch {
|
|
905
|
-
await (0,
|
|
1159
|
+
await (0, import_promises4.cp)(canonicalSkillDir, linkPath, { recursive: true });
|
|
906
1160
|
return "copied";
|
|
907
1161
|
}
|
|
908
1162
|
}
|
|
909
|
-
|
|
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
|
|
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(
|
|
1183
|
+
throw new Error(describeFailure3(response.status));
|
|
917
1184
|
}
|
|
918
1185
|
const commit = response.headers.get("x-skills-commit") ?? void 0;
|
|
919
|
-
const
|
|
920
|
-
|
|
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
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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,
|
|
935
|
-
|
|
936
|
-
|
|
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,
|
|
947
|
-
(0,
|
|
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 ??
|
|
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,
|
|
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 (
|
|
961
|
-
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 });
|
|
962
1237
|
}
|
|
963
1238
|
}
|
|
964
|
-
|
|
965
|
-
version:
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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.
|
|
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
|
|
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
|
|
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 === "
|
|
1108
|
-
if (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
|
|
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
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
if (
|
|
1130
|
-
console.
|
|
1131
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
+
"version": "0.3.1",
|
|
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
|
|
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
|
},
|