@newlandnpt/pep-cli 0.1.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/README.md +111 -0
- package/dist/pep.cjs +1119 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# PEP CLI
|
|
2
|
+
|
|
3
|
+
Command-line OAuth client for obtaining a PEP access token, reading PEP documentation, and
|
|
4
|
+
syncing agent skills. Login uses the authorization-code flow with PKCE and a localhost callback.
|
|
5
|
+
|
|
6
|
+
Tokens live in the OS keychain — **Windows Credential Manager** or the **macOS login keychain**
|
|
7
|
+
(via the built-in `security` tool). Linux is not implemented yet: `systemCredentialStore()` throws,
|
|
8
|
+
and `package.json` declares `os: ["win32", "darwin"]` so npm refuses to install elsewhere rather
|
|
9
|
+
than letting the install succeed and the first `auth login` fail.
|
|
10
|
+
|
|
11
|
+
The non-secret issuer, client ID, resources and documentation URL are stored in a plain config file
|
|
12
|
+
(`%APPDATA%\\PEP` on Windows, `~/.config/pep` elsewhere). `auth logout` clears both.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
npm i -g @newlandnpt/pep-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The package name is scoped, the command names are not — see below. The scope is not a style
|
|
21
|
+
choice: npm's typo-squatting policy **refuses** the unscoped name `pep-cli` ("too similar to
|
|
22
|
+
existing packages cp-cli, del-cli, open-cli"), and that check applies to every short
|
|
23
|
+
`<prefix>-cli` name. Scoped names skip the check, so `@newlandnpt/…` is what makes this
|
|
24
|
+
publishable at all.
|
|
25
|
+
|
|
26
|
+
## Command name
|
|
27
|
+
|
|
28
|
+
The package installs **two** names for the same executable:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pep # short form, used throughout this document
|
|
32
|
+
pep-cli # alias
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`bin` names are global and independent of the package name — any other package (or any
|
|
36
|
+
non-npm tool) may also claim `pep`, and on a global install the last one wins silently.
|
|
37
|
+
The alias costs nothing (one more symlink to the same file) and gives scripts a name that
|
|
38
|
+
cannot be clobbered by an unrelated `pep`. Prefer `pep-cli` in anything automated.
|
|
39
|
+
|
|
40
|
+
## Register the public client
|
|
41
|
+
|
|
42
|
+
Run this once from the `pep-webapp` repository for the customer tenant (replace the IDs):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node scripts/oauth-bootstrap.mjs --client --public \
|
|
46
|
+
--client-id 1b916aae96f69a535d7a1a30c8f2e1dc --name "PEP CLI" \
|
|
47
|
+
--party <party-id> --admission-user <user-id> \
|
|
48
|
+
--redirect-uri http://localhost:53682/callback \
|
|
49
|
+
--scopes "openid profile email"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If one PEP deployment serves multiple customer tenants, register a distinct client ID for each tenant
|
|
53
|
+
and pass it with `--client-id`.
|
|
54
|
+
|
|
55
|
+
## Use
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
pep auth login
|
|
59
|
+
pep auth status
|
|
60
|
+
$token = pep auth token
|
|
61
|
+
pep auth logout
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The issuer is fixed at build time:
|
|
65
|
+
|
|
66
|
+
| build | issuer |
|
|
67
|
+
| ------------- | -------------------------------------- |
|
|
68
|
+
| `development` | `https://pep-webapp-dev.onrender.com` |
|
|
69
|
+
| `view` | `https://pep-webapp-view.onrender.com` |
|
|
70
|
+
| `production` | `https://pep.newlandnpt.us` |
|
|
71
|
+
|
|
72
|
+
`--issuer` remains available only as a temporary override — it is **not** remembered, so every
|
|
73
|
+
`auth login` needs it again. That is why the built-in default decides where most users land.
|
|
74
|
+
|
|
75
|
+
⚠ **The npm release is built from `view`** (see `prepublishOnly`). As of 2026-09-08
|
|
76
|
+
`https://pep.newlandnpt.us/.well-known/openid-configuration` returns **404** — the production
|
|
77
|
+
domain does not serve the authorization server yet, so a `production` build would fail at
|
|
78
|
+
discovery on the user's very first `auth login`. Switch `prepublishOnly` back to `production`
|
|
79
|
+
once that endpoint is live.
|
|
80
|
+
|
|
81
|
+
`pep auth token` writes only the access token to stdout, so agents can use it without parsing status
|
|
82
|
+
text. Refresh occurs automatically shortly before expiry and the rotated refresh token replaces the old
|
|
83
|
+
credential atomically.
|
|
84
|
+
|
|
85
|
+
## Build the customer executable
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
pnpm build:exe:dev # dist/pep-dev.exe
|
|
89
|
+
pnpm build:exe:view # dist/pep-view.exe
|
|
90
|
+
pnpm build:exe # dist/pep.exe (production)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The unsigned standalone executable is written to `dist/pep.exe` and does not require Node.js
|
|
94
|
+
on the customer machine. Production distribution should sign that file with the organization's Windows
|
|
95
|
+
code-signing certificate after injection.
|
|
96
|
+
|
|
97
|
+
## Build the Windows installer
|
|
98
|
+
|
|
99
|
+
Install [NSIS 3](https://nsis.sourceforge.io/Download), then run:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
pnpm build:installer:dev # dist/pep-dev-setup.exe
|
|
103
|
+
pnpm build:installer:view # dist/pep-view-setup.exe
|
|
104
|
+
pnpm build:installer # dist/pep-setup.exe (production)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Both user-level installers install PEP CLI under `%LOCALAPPDATA%\Programs\PEP`, add that directory
|
|
108
|
+
to the user `PATH`, and register an uninstaller. Installing one environment replaces the other, and
|
|
109
|
+
new terminals can run `pep auth login` from any directory. Run `pep auth login` after switching
|
|
110
|
+
environments so the stored authorization matches the installed environment. Sign both `pep.exe` and
|
|
111
|
+
the final production installer before distribution.
|
package/dist/pep.cjs
ADDED
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/auth-service.ts
|
|
5
|
+
var import_promises2 = require("node:fs/promises");
|
|
6
|
+
var import_node_path2 = require("node:path");
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
var import_promises = require("node:fs/promises");
|
|
10
|
+
var import_node_os = require("node:os");
|
|
11
|
+
var import_node_path = require("node:path");
|
|
12
|
+
var DEFAULT_CLIENT_ID = "pep-cli";
|
|
13
|
+
var DEFAULT_REDIRECT_URI = "http://localhost:53682/callback";
|
|
14
|
+
var DEFAULT_SCOPES = ["openid", "profile", "email", "docs:read", "skills:read"];
|
|
15
|
+
var DEFAULT_RESOURCES = ["urn:newland:pep:docs"];
|
|
16
|
+
var DEFAULT_DOCS_URL = "https://pep-developer-docs.onrender.com";
|
|
17
|
+
function issuerForEnvironment(environment) {
|
|
18
|
+
if (environment === "production") return "https://pep.newlandnpt.us";
|
|
19
|
+
if (environment === "view") return "https://pep-webapp-view.onrender.com";
|
|
20
|
+
return "https://pep-webapp-dev.onrender.com";
|
|
21
|
+
}
|
|
22
|
+
var BUILD_ENVIRONMENT = false ? "development" : "production";
|
|
23
|
+
var DEFAULT_ISSUER = issuerForEnvironment(BUILD_ENVIRONMENT);
|
|
24
|
+
function configuredIssuer(explicitIssuer, savedIssuer) {
|
|
25
|
+
return explicitIssuer ?? savedIssuer ?? DEFAULT_ISSUER;
|
|
26
|
+
}
|
|
27
|
+
function configDirectory() {
|
|
28
|
+
if (process.platform === "win32") return (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Roaming", "PEP");
|
|
29
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "pep");
|
|
30
|
+
}
|
|
31
|
+
function configPath() {
|
|
32
|
+
return (0, import_node_path.join)(configDirectory(), "config.json");
|
|
33
|
+
}
|
|
34
|
+
function lockPath() {
|
|
35
|
+
return (0, import_node_path.join)(configDirectory(), "credentials.lock");
|
|
36
|
+
}
|
|
37
|
+
function skillsStatePath() {
|
|
38
|
+
return (0, import_node_path.join)(configDirectory(), "skills.json");
|
|
39
|
+
}
|
|
40
|
+
function defaultSkillsDirectory() {
|
|
41
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "skills");
|
|
42
|
+
}
|
|
43
|
+
function isCliConfig(value) {
|
|
44
|
+
if (typeof value !== "object" || value === null) return false;
|
|
45
|
+
const candidate = value;
|
|
46
|
+
return candidate.version === 1 && typeof candidate.issuer === "string" && typeof candidate.clientId === "string" && typeof candidate.redirectUri === "string" && // 可选:v0.1.0 写下的配置文件没有这个键,读它们不该报「配置损坏」。
|
|
47
|
+
(candidate.resources === void 0 || Array.isArray(candidate.resources) && candidate.resources.every((one) => typeof one === "string")) && (candidate.docsUrl === void 0 || typeof candidate.docsUrl === "string");
|
|
48
|
+
}
|
|
49
|
+
function fileConfigStore(path = configPath()) {
|
|
50
|
+
return {
|
|
51
|
+
async read() {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
|
|
54
|
+
if (!isCliConfig(parsed)) throw new Error(`PEP CLI config is malformed: ${path}`);
|
|
55
|
+
return parsed;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code === "ENOENT") return null;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async write(config) {
|
|
62
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
63
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
64
|
+
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(config, null, 2)}
|
|
65
|
+
`, { mode: 384 });
|
|
66
|
+
await (0, import_promises.rename)(temporary, path);
|
|
67
|
+
},
|
|
68
|
+
async delete() {
|
|
69
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isSkillsState(value) {
|
|
74
|
+
if (typeof value !== "object" || value === null) return false;
|
|
75
|
+
const candidate = value;
|
|
76
|
+
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");
|
|
77
|
+
}
|
|
78
|
+
function fileSkillsStateStore(path = skillsStatePath()) {
|
|
79
|
+
return {
|
|
80
|
+
async read() {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
|
|
83
|
+
return isSkillsState(parsed) ? parsed : null;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error.code === "ENOENT") return null;
|
|
86
|
+
if (error instanceof SyntaxError) return null;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async write(state) {
|
|
91
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
92
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
93
|
+
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(state, null, 2)}
|
|
94
|
+
`, { mode: 384 });
|
|
95
|
+
await (0, import_promises.rename)(temporary, path);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function normalizeDocsUrl(raw) {
|
|
100
|
+
return normalizeIssuer(raw);
|
|
101
|
+
}
|
|
102
|
+
function normalizeIssuer(raw) {
|
|
103
|
+
const url = new URL(raw);
|
|
104
|
+
const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
105
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
|
|
106
|
+
throw new Error("Issuer must use HTTPS (HTTP is allowed only for a local PEP server).");
|
|
107
|
+
}
|
|
108
|
+
url.search = "";
|
|
109
|
+
url.hash = "";
|
|
110
|
+
url.pathname = url.pathname.replace(/\/$/, "");
|
|
111
|
+
return url.toString().replace(/\/$/, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/oauth-client.ts
|
|
115
|
+
var import_node_crypto = require("node:crypto");
|
|
116
|
+
var import_node_http = require("node:http");
|
|
117
|
+
var import_node_child_process = require("node:child_process");
|
|
118
|
+
var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
119
|
+
var HTTP_TIMEOUT_MS = 15 * 1e3;
|
|
120
|
+
function endpoint(value, name) {
|
|
121
|
+
if (typeof value !== "string") throw new Error(`Discovery document is missing ${name}.`);
|
|
122
|
+
const url = new URL(value);
|
|
123
|
+
const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
124
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
|
|
125
|
+
throw new Error(`Discovery ${name} is not a secure HTTP endpoint.`);
|
|
126
|
+
}
|
|
127
|
+
return url.toString();
|
|
128
|
+
}
|
|
129
|
+
async function responseJson(response, operation) {
|
|
130
|
+
const body = await response.text();
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(body);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`${operation} returned a non-JSON response (HTTP ${response.status}).`);
|
|
136
|
+
}
|
|
137
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
138
|
+
throw new Error(`${operation} returned malformed JSON.`);
|
|
139
|
+
const record = parsed;
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const description = typeof record.error_description === "string" ? record.error_description : void 0;
|
|
142
|
+
const code = typeof record.error === "string" ? record.error : `HTTP ${response.status}`;
|
|
143
|
+
throw new Error(`${operation} failed: ${description ?? code}`);
|
|
144
|
+
}
|
|
145
|
+
return record;
|
|
146
|
+
}
|
|
147
|
+
function createPkce() {
|
|
148
|
+
const verifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
149
|
+
const challenge = (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url");
|
|
150
|
+
return { verifier, challenge };
|
|
151
|
+
}
|
|
152
|
+
async function discover(issuer, fetcher = fetch) {
|
|
153
|
+
const response = await fetcher(`${issuer}/.well-known/openid-configuration`, {
|
|
154
|
+
headers: { Accept: "application/json" },
|
|
155
|
+
redirect: "error",
|
|
156
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
|
157
|
+
});
|
|
158
|
+
const body = await responseJson(response, "OAuth discovery");
|
|
159
|
+
if (body.issuer !== issuer)
|
|
160
|
+
throw new Error("Discovery issuer does not match the configured PEP issuer.");
|
|
161
|
+
return {
|
|
162
|
+
issuer,
|
|
163
|
+
authorizationEndpoint: endpoint(body.authorization_endpoint, "authorization_endpoint"),
|
|
164
|
+
tokenEndpoint: endpoint(body.token_endpoint, "token_endpoint"),
|
|
165
|
+
revocationEndpoint: endpoint(body.revocation_endpoint, "revocation_endpoint"),
|
|
166
|
+
userinfoEndpoint: endpoint(body.userinfo_endpoint, "userinfo_endpoint")
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function authorizationUrl(args) {
|
|
170
|
+
const url = new URL(args.discovery.authorizationEndpoint);
|
|
171
|
+
const params = new URLSearchParams({
|
|
172
|
+
response_type: "code",
|
|
173
|
+
client_id: args.config.clientId,
|
|
174
|
+
redirect_uri: args.config.redirectUri,
|
|
175
|
+
scope: args.scopes.join(" "),
|
|
176
|
+
state: args.state,
|
|
177
|
+
code_challenge: args.challenge,
|
|
178
|
+
code_challenge_method: "S256"
|
|
179
|
+
});
|
|
180
|
+
for (const resource of args.config.resources ?? []) params.append("resource", resource);
|
|
181
|
+
url.search = params.toString();
|
|
182
|
+
return url.toString();
|
|
183
|
+
}
|
|
184
|
+
function callbackHtml(isSuccess) {
|
|
185
|
+
const title = isSuccess ? "PEP login complete" : "PEP login failed";
|
|
186
|
+
const message = isSuccess ? "You can close this window and return to the terminal." : "Return to the terminal for details.";
|
|
187
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><main><h1>${title}</h1><p>${message}</p></main></body></html>`;
|
|
188
|
+
}
|
|
189
|
+
async function waitForAuthorizationCallback(args) {
|
|
190
|
+
const redirect = new URL(args.redirectUri);
|
|
191
|
+
if (redirect.protocol !== "http:" || redirect.hostname !== "localhost" || !redirect.port) {
|
|
192
|
+
throw new Error("CLI redirect URI must be an http://localhost URL with an explicit port.");
|
|
193
|
+
}
|
|
194
|
+
let server;
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const finish = (error, code) => {
|
|
197
|
+
clearTimeout(timeout);
|
|
198
|
+
server?.close();
|
|
199
|
+
if (error) reject(error);
|
|
200
|
+
else resolve(code);
|
|
201
|
+
};
|
|
202
|
+
const timeout = setTimeout(
|
|
203
|
+
() => finish(new Error("Login timed out while waiting for the browser callback.")),
|
|
204
|
+
args.timeoutMs ?? CALLBACK_TIMEOUT_MS
|
|
205
|
+
);
|
|
206
|
+
server = (0, import_node_http.createServer)((request, response) => {
|
|
207
|
+
const current = new URL(request.url ?? "/", args.redirectUri);
|
|
208
|
+
if (request.method !== "GET" || current.pathname !== redirect.pathname) {
|
|
209
|
+
response.writeHead(404).end();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (current.searchParams.get("state") !== args.state) {
|
|
213
|
+
response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(callbackHtml(false));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const oauthError = current.searchParams.get("error");
|
|
217
|
+
if (oauthError) {
|
|
218
|
+
response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(callbackHtml(false));
|
|
219
|
+
finish(
|
|
220
|
+
new Error(
|
|
221
|
+
`Authorization failed: ${current.searchParams.get("error_description") ?? oauthError}`
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const code = current.searchParams.get("code");
|
|
227
|
+
if (!code) {
|
|
228
|
+
response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(callbackHtml(false));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(callbackHtml(true));
|
|
232
|
+
finish(void 0, code);
|
|
233
|
+
});
|
|
234
|
+
server.once("error", (error) => finish(error));
|
|
235
|
+
server.listen(Number(redirect.port), "localhost", () => {
|
|
236
|
+
args.onListening().catch((error) => finish(error));
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function openBrowser(url) {
|
|
241
|
+
const command = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
242
|
+
const commandArgs = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const child = (0, import_node_child_process.spawn)(command, commandArgs, {
|
|
245
|
+
detached: true,
|
|
246
|
+
stdio: "ignore",
|
|
247
|
+
windowsHide: true
|
|
248
|
+
});
|
|
249
|
+
child.once("error", reject);
|
|
250
|
+
child.once("spawn", () => {
|
|
251
|
+
child.unref();
|
|
252
|
+
resolve();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function parseTokens(body, config) {
|
|
257
|
+
if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string" || body.token_type !== "Bearer" || typeof body.expires_in !== "number" || !Number.isFinite(body.expires_in) || body.expires_in <= 0 || typeof body.scope !== "string") {
|
|
258
|
+
throw new Error("Token endpoint returned a malformed token response.");
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
version: 1,
|
|
262
|
+
issuer: config.issuer,
|
|
263
|
+
clientId: config.clientId,
|
|
264
|
+
accessToken: body.access_token,
|
|
265
|
+
refreshToken: body.refresh_token,
|
|
266
|
+
tokenType: "Bearer",
|
|
267
|
+
expiresAt: Date.now() + body.expires_in * 1e3,
|
|
268
|
+
scopes: body.scope.split(/\s+/).filter(Boolean)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function postForm(url, form, operation, fetcher) {
|
|
272
|
+
const body = new URLSearchParams();
|
|
273
|
+
for (const [key, value] of Object.entries(form)) {
|
|
274
|
+
if (typeof value === "string") body.set(key, value);
|
|
275
|
+
else for (const one of value) body.append(key, one);
|
|
276
|
+
}
|
|
277
|
+
const response = await fetcher(url, {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
280
|
+
body,
|
|
281
|
+
redirect: "error",
|
|
282
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
|
283
|
+
});
|
|
284
|
+
return responseJson(response, operation);
|
|
285
|
+
}
|
|
286
|
+
function createOAuthClient(fetcher = fetch) {
|
|
287
|
+
return {
|
|
288
|
+
discover: (issuer) => discover(issuer, fetcher),
|
|
289
|
+
async exchangeCode(discovery, config, code, verifier) {
|
|
290
|
+
const body = await postForm(
|
|
291
|
+
discovery.tokenEndpoint,
|
|
292
|
+
{
|
|
293
|
+
grant_type: "authorization_code",
|
|
294
|
+
client_id: config.clientId,
|
|
295
|
+
code,
|
|
296
|
+
redirect_uri: config.redirectUri,
|
|
297
|
+
code_verifier: verifier,
|
|
298
|
+
// 与 /authorize 报的那批一致。PEP 只允许**收窄**(§2.2),报同一批即原样批准;
|
|
299
|
+
// 不报则照授权时批准的全量落,两者在这里等价 —— 显式带上是为了让线上抓包能看出
|
|
300
|
+
// 这枚令牌的受众是谁,不必回头去翻授权请求。
|
|
301
|
+
resource: config.resources ?? []
|
|
302
|
+
},
|
|
303
|
+
"Authorization code exchange",
|
|
304
|
+
fetcher
|
|
305
|
+
);
|
|
306
|
+
return parseTokens(body, config);
|
|
307
|
+
},
|
|
308
|
+
async refresh(discovery, authorization) {
|
|
309
|
+
const config = {
|
|
310
|
+
version: 1,
|
|
311
|
+
issuer: authorization.issuer,
|
|
312
|
+
clientId: authorization.clientId,
|
|
313
|
+
redirectUri: ""
|
|
314
|
+
};
|
|
315
|
+
const body = await postForm(
|
|
316
|
+
discovery.tokenEndpoint,
|
|
317
|
+
{
|
|
318
|
+
grant_type: "refresh_token",
|
|
319
|
+
client_id: authorization.clientId,
|
|
320
|
+
refresh_token: authorization.refreshToken
|
|
321
|
+
},
|
|
322
|
+
"Token refresh",
|
|
323
|
+
fetcher
|
|
324
|
+
);
|
|
325
|
+
return parseTokens(body, config);
|
|
326
|
+
},
|
|
327
|
+
async userInfo(discovery, accessToken) {
|
|
328
|
+
const response = await fetcher(discovery.userinfoEndpoint, {
|
|
329
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
|
|
330
|
+
redirect: "error",
|
|
331
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
|
332
|
+
});
|
|
333
|
+
const body = await responseJson(response, "User info");
|
|
334
|
+
if (typeof body.sub !== "string") throw new Error("User info response is missing sub.");
|
|
335
|
+
return {
|
|
336
|
+
sub: body.sub,
|
|
337
|
+
...typeof body.email === "string" ? { email: body.email } : {},
|
|
338
|
+
...typeof body.name === "string" ? { name: body.name } : {}
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
async revoke(discovery, authorization) {
|
|
342
|
+
const response = await fetcher(discovery.revocationEndpoint, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
345
|
+
body: new URLSearchParams({
|
|
346
|
+
client_id: authorization.clientId,
|
|
347
|
+
token: authorization.refreshToken,
|
|
348
|
+
token_type_hint: "refresh_token"
|
|
349
|
+
}),
|
|
350
|
+
redirect: "error",
|
|
351
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
|
352
|
+
});
|
|
353
|
+
if (!response.ok) await responseJson(response, "Token revocation");
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/auth-service.ts
|
|
359
|
+
var REFRESH_EARLY_MS = 60 * 1e3;
|
|
360
|
+
var LOCK_STALE_MS = 30 * 1e3;
|
|
361
|
+
var LOCK_WAIT_MS = 10 * 1e3;
|
|
362
|
+
async function delay(milliseconds) {
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
364
|
+
}
|
|
365
|
+
async function withAuthorizationLock(path, action) {
|
|
366
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
367
|
+
const startedAt = Date.now();
|
|
368
|
+
while (true) {
|
|
369
|
+
try {
|
|
370
|
+
const handle = await (0, import_promises2.open)(path, "wx", 384);
|
|
371
|
+
try {
|
|
372
|
+
return await action();
|
|
373
|
+
} finally {
|
|
374
|
+
await handle.close();
|
|
375
|
+
await (0, import_promises2.unlink)(path).catch(() => void 0);
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error.code !== "EEXIST") throw error;
|
|
379
|
+
let age;
|
|
380
|
+
try {
|
|
381
|
+
age = Date.now() - (await (0, import_promises2.stat)(path)).mtimeMs;
|
|
382
|
+
} catch (statError) {
|
|
383
|
+
if (statError.code === "ENOENT") continue;
|
|
384
|
+
throw statError;
|
|
385
|
+
}
|
|
386
|
+
if (age > LOCK_STALE_MS) {
|
|
387
|
+
await (0, import_promises2.unlink)(path).catch(() => void 0);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (Date.now() - startedAt > LOCK_WAIT_MS)
|
|
391
|
+
throw new Error("Another PEP authentication command is still running.");
|
|
392
|
+
await delay(100);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function createAuthService(dependencies) {
|
|
397
|
+
const now = dependencies.now ?? Date.now;
|
|
398
|
+
async function requireAuthorization() {
|
|
399
|
+
const authorization = await dependencies.credentialStore.read();
|
|
400
|
+
if (!authorization) throw new Error("Not logged in. Run `pep auth login` first.");
|
|
401
|
+
return authorization;
|
|
402
|
+
}
|
|
403
|
+
async function currentAuthorization() {
|
|
404
|
+
return withAuthorizationLock(dependencies.authorizationLockPath ?? lockPath(), async () => {
|
|
405
|
+
const authorization = await requireAuthorization();
|
|
406
|
+
if (authorization.expiresAt - now() > REFRESH_EARLY_MS) return authorization;
|
|
407
|
+
const discovery = await dependencies.oauth.discover(authorization.issuer);
|
|
408
|
+
const refreshed = await dependencies.oauth.refresh(discovery, authorization);
|
|
409
|
+
await dependencies.credentialStore.write(refreshed);
|
|
410
|
+
return refreshed;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
async login(config, onAuthorizationUrl) {
|
|
415
|
+
const normalized = { ...config, issuer: normalizeIssuer(config.issuer) };
|
|
416
|
+
const discovery = await dependencies.oauth.discover(normalized.issuer);
|
|
417
|
+
const state = createPkce().verifier;
|
|
418
|
+
const pkce = createPkce();
|
|
419
|
+
const url = authorizationUrl({
|
|
420
|
+
discovery,
|
|
421
|
+
config: normalized,
|
|
422
|
+
state,
|
|
423
|
+
challenge: pkce.challenge,
|
|
424
|
+
scopes: DEFAULT_SCOPES
|
|
425
|
+
});
|
|
426
|
+
const code = await (dependencies.receiveCode ?? waitForAuthorizationCallback)({
|
|
427
|
+
redirectUri: normalized.redirectUri,
|
|
428
|
+
state,
|
|
429
|
+
onListening: async () => {
|
|
430
|
+
onAuthorizationUrl?.(url);
|
|
431
|
+
try {
|
|
432
|
+
await (dependencies.launchBrowser ?? openBrowser)(url);
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
const authorization = await dependencies.oauth.exchangeCode(
|
|
438
|
+
discovery,
|
|
439
|
+
normalized,
|
|
440
|
+
code,
|
|
441
|
+
pkce.verifier
|
|
442
|
+
);
|
|
443
|
+
const previous = await dependencies.credentialStore.read();
|
|
444
|
+
await dependencies.credentialStore.write(authorization);
|
|
445
|
+
await dependencies.configStore.write(normalized);
|
|
446
|
+
if (previous) {
|
|
447
|
+
try {
|
|
448
|
+
const previousDiscovery = await dependencies.oauth.discover(previous.issuer);
|
|
449
|
+
await dependencies.oauth.revoke(previousDiscovery, previous);
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return authorization;
|
|
454
|
+
},
|
|
455
|
+
currentAuthorization,
|
|
456
|
+
async status() {
|
|
457
|
+
const authorization = await currentAuthorization();
|
|
458
|
+
const discovery = await dependencies.oauth.discover(authorization.issuer);
|
|
459
|
+
const user = await dependencies.oauth.userInfo(discovery, authorization.accessToken);
|
|
460
|
+
return { authorization, user };
|
|
461
|
+
},
|
|
462
|
+
async logout() {
|
|
463
|
+
return withAuthorizationLock(dependencies.authorizationLockPath ?? lockPath(), async () => {
|
|
464
|
+
const authorization = await dependencies.credentialStore.read();
|
|
465
|
+
await dependencies.configStore.delete();
|
|
466
|
+
if (!authorization) return { wasLoggedIn: false };
|
|
467
|
+
let revocationError;
|
|
468
|
+
try {
|
|
469
|
+
const discovery = await dependencies.oauth.discover(authorization.issuer);
|
|
470
|
+
await dependencies.oauth.revoke(discovery, authorization);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
revocationError = error;
|
|
473
|
+
} finally {
|
|
474
|
+
await dependencies.credentialStore.delete();
|
|
475
|
+
}
|
|
476
|
+
return { wasLoggedIn: true, ...revocationError ? { revocationError } : {} };
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/credential-store.ts
|
|
483
|
+
var import_node_child_process2 = require("node:child_process");
|
|
484
|
+
var CREDENTIAL_TARGET = "PEP CLI OAuth Tokens";
|
|
485
|
+
var WINDOWS_CREDENTIAL_NATIVE = String.raw`
|
|
486
|
+
using System;
|
|
487
|
+
using System.ComponentModel;
|
|
488
|
+
using System.Runtime.InteropServices;
|
|
489
|
+
|
|
490
|
+
public static class PepCredential {
|
|
491
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
492
|
+
public struct CREDENTIAL {
|
|
493
|
+
public UInt32 Flags;
|
|
494
|
+
public UInt32 Type;
|
|
495
|
+
public string TargetName;
|
|
496
|
+
public string Comment;
|
|
497
|
+
public Int64 LastWritten;
|
|
498
|
+
public UInt32 CredentialBlobSize;
|
|
499
|
+
public IntPtr CredentialBlob;
|
|
500
|
+
public UInt32 Persist;
|
|
501
|
+
public UInt32 AttributeCount;
|
|
502
|
+
public IntPtr Attributes;
|
|
503
|
+
public string TargetAlias;
|
|
504
|
+
public string UserName;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
508
|
+
private static extern bool CredWrite(ref CREDENTIAL credential, UInt32 flags);
|
|
509
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
510
|
+
private static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
|
|
511
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
512
|
+
private static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
|
|
513
|
+
[DllImport("Advapi32.dll", SetLastError = false)]
|
|
514
|
+
private static extern void CredFree(IntPtr credential);
|
|
515
|
+
|
|
516
|
+
public static void Write(string target, string userName, byte[] secret) {
|
|
517
|
+
IntPtr blob = Marshal.AllocCoTaskMem(secret.Length);
|
|
518
|
+
try {
|
|
519
|
+
Marshal.Copy(secret, 0, blob, secret.Length);
|
|
520
|
+
var credential = new CREDENTIAL {
|
|
521
|
+
Type = 1, TargetName = target, UserName = userName, CredentialBlob = blob,
|
|
522
|
+
CredentialBlobSize = (UInt32)secret.Length, Persist = 2
|
|
523
|
+
};
|
|
524
|
+
if (!CredWrite(ref credential, 0)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
525
|
+
} finally {
|
|
526
|
+
for (int i = 0; i < secret.Length; i++) Marshal.WriteByte(blob, i, 0);
|
|
527
|
+
Marshal.FreeCoTaskMem(blob);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
public static byte[] Read(string target) {
|
|
532
|
+
IntPtr pointer;
|
|
533
|
+
if (!CredRead(target, 1, 0, out pointer)) {
|
|
534
|
+
int error = Marshal.GetLastWin32Error();
|
|
535
|
+
if (error == 1168) return null;
|
|
536
|
+
throw new Win32Exception(error);
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
var credential = Marshal.PtrToStructure<CREDENTIAL>(pointer);
|
|
540
|
+
var secret = new byte[credential.CredentialBlobSize];
|
|
541
|
+
Marshal.Copy(credential.CredentialBlob, secret, 0, secret.Length);
|
|
542
|
+
return secret;
|
|
543
|
+
} finally { CredFree(pointer); }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
public static void Delete(string target) {
|
|
547
|
+
if (!CredDelete(target, 1, 0)) {
|
|
548
|
+
int error = Marshal.GetLastWin32Error();
|
|
549
|
+
if (error != 1168) throw new Win32Exception(error);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}`;
|
|
553
|
+
function encodedPowerShell(script) {
|
|
554
|
+
return Buffer.from(script, "utf16le").toString("base64");
|
|
555
|
+
}
|
|
556
|
+
async function runPowerShell(script, input) {
|
|
557
|
+
return new Promise((resolve, reject) => {
|
|
558
|
+
const child = (0, import_node_child_process2.spawn)(
|
|
559
|
+
"powershell.exe",
|
|
560
|
+
[
|
|
561
|
+
"-NoLogo",
|
|
562
|
+
"-NoProfile",
|
|
563
|
+
"-NonInteractive",
|
|
564
|
+
"-EncodedCommand",
|
|
565
|
+
encodedPowerShell(script)
|
|
566
|
+
],
|
|
567
|
+
{ stdio: ["pipe", "pipe", "pipe"], windowsHide: true }
|
|
568
|
+
);
|
|
569
|
+
let stdout = "";
|
|
570
|
+
let stderr = "";
|
|
571
|
+
const timeout = setTimeout(() => child.kill(), 1e4);
|
|
572
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
|
|
573
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
|
|
574
|
+
child.once(
|
|
575
|
+
"error",
|
|
576
|
+
(error) => reject(new Error(`Windows Credential Manager failed: ${error.message}`))
|
|
577
|
+
);
|
|
578
|
+
child.once("close", (code) => {
|
|
579
|
+
clearTimeout(timeout);
|
|
580
|
+
if (code === 0) resolve(stdout.trim());
|
|
581
|
+
else
|
|
582
|
+
reject(
|
|
583
|
+
new Error(
|
|
584
|
+
`Windows Credential Manager failed: ${stderr.trim() || `exit ${code}`}`
|
|
585
|
+
)
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
child.stdin.end(input);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
function parseStoredAuthorization(raw) {
|
|
592
|
+
const value = JSON.parse(raw);
|
|
593
|
+
if (typeof value !== "object" || value === null)
|
|
594
|
+
throw new Error("Stored PEP authorization is malformed.");
|
|
595
|
+
const candidate = value;
|
|
596
|
+
if (candidate.version !== 1 || typeof candidate.issuer !== "string" || typeof candidate.clientId !== "string" || typeof candidate.accessToken !== "string" || typeof candidate.refreshToken !== "string" || candidate.tokenType !== "Bearer" || typeof candidate.expiresAt !== "number" || !Array.isArray(candidate.scopes) || !candidate.scopes.every((scope) => typeof scope === "string")) {
|
|
597
|
+
throw new Error("Stored PEP authorization is malformed.");
|
|
598
|
+
}
|
|
599
|
+
return candidate;
|
|
600
|
+
}
|
|
601
|
+
function windowsCredentialStore(target = CREDENTIAL_TARGET) {
|
|
602
|
+
const preamble = `Add-Type -TypeDefinition @'
|
|
603
|
+
${WINDOWS_CREDENTIAL_NATIVE}
|
|
604
|
+
'@`;
|
|
605
|
+
return {
|
|
606
|
+
async read() {
|
|
607
|
+
const script = `${preamble}
|
|
608
|
+
$bytes = [PepCredential]::Read('${target}')
|
|
609
|
+
if ($null -ne $bytes) { [Convert]::ToBase64String($bytes) }`;
|
|
610
|
+
const encoded = await runPowerShell(script);
|
|
611
|
+
if (!encoded) return null;
|
|
612
|
+
return parseStoredAuthorization(
|
|
613
|
+
Buffer.from(encoded, "base64").toString("utf8")
|
|
614
|
+
);
|
|
615
|
+
},
|
|
616
|
+
async write(authorization) {
|
|
617
|
+
const script = `${preamble}
|
|
618
|
+
$raw = [Console]::In.ReadToEnd()
|
|
619
|
+
[PepCredential]::Write('${target}', 'oauth', [Text.Encoding]::UTF8.GetBytes($raw))`;
|
|
620
|
+
await runPowerShell(script, JSON.stringify(authorization));
|
|
621
|
+
},
|
|
622
|
+
async delete() {
|
|
623
|
+
await runPowerShell(`${preamble}
|
|
624
|
+
[PepCredential]::Delete('${target}')`);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
var SECURITY_NOT_FOUND = 44;
|
|
629
|
+
async function runSecurity(args) {
|
|
630
|
+
return new Promise((resolve, reject) => {
|
|
631
|
+
const child = (0, import_node_child_process2.spawn)("/usr/bin/security", [...args], {
|
|
632
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
633
|
+
});
|
|
634
|
+
let stdout = "";
|
|
635
|
+
let stderr = "";
|
|
636
|
+
const timeout = setTimeout(() => child.kill(), 1e4);
|
|
637
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
|
|
638
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
|
|
639
|
+
child.once(
|
|
640
|
+
"error",
|
|
641
|
+
(error) => reject(new Error(`macOS Keychain failed: ${error.message}`))
|
|
642
|
+
);
|
|
643
|
+
child.once("close", (code) => {
|
|
644
|
+
clearTimeout(timeout);
|
|
645
|
+
resolve({ code: code ?? -1, stdout, stderr });
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function macosCredentialStore(target = CREDENTIAL_TARGET) {
|
|
650
|
+
const selector = ["-s", target, "-a", "oauth"];
|
|
651
|
+
return {
|
|
652
|
+
async read() {
|
|
653
|
+
const { code, stdout, stderr } = await runSecurity([
|
|
654
|
+
"find-generic-password",
|
|
655
|
+
...selector,
|
|
656
|
+
"-w"
|
|
657
|
+
]);
|
|
658
|
+
if (code === SECURITY_NOT_FOUND) return null;
|
|
659
|
+
if (code !== 0)
|
|
660
|
+
throw new Error(
|
|
661
|
+
`macOS Keychain failed: ${stderr.trim() || `exit ${code}`}`
|
|
662
|
+
);
|
|
663
|
+
const raw = stdout.trim();
|
|
664
|
+
if (!raw) return null;
|
|
665
|
+
return parseStoredAuthorization(raw);
|
|
666
|
+
},
|
|
667
|
+
async write(authorization) {
|
|
668
|
+
const { code, stderr } = await runSecurity([
|
|
669
|
+
"add-generic-password",
|
|
670
|
+
...selector,
|
|
671
|
+
"-U",
|
|
672
|
+
"-w",
|
|
673
|
+
JSON.stringify(authorization)
|
|
674
|
+
]);
|
|
675
|
+
if (code !== 0)
|
|
676
|
+
throw new Error(
|
|
677
|
+
`macOS Keychain failed: ${stderr.trim() || `exit ${code}`}`
|
|
678
|
+
);
|
|
679
|
+
},
|
|
680
|
+
async delete() {
|
|
681
|
+
const { code, stderr } = await runSecurity([
|
|
682
|
+
"delete-generic-password",
|
|
683
|
+
...selector
|
|
684
|
+
]);
|
|
685
|
+
if (code !== 0 && code !== SECURITY_NOT_FOUND) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`macOS Keychain failed: ${stderr.trim() || `exit ${code}`}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function systemCredentialStore() {
|
|
694
|
+
if (process.platform === "win32") return windowsCredentialStore();
|
|
695
|
+
if (process.platform === "darwin") return macosCredentialStore();
|
|
696
|
+
throw new Error(
|
|
697
|
+
`PEP CLI stores credentials in the OS keychain and currently supports Windows and macOS only (this is ${process.platform}).`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/docs-service.ts
|
|
702
|
+
var ENTRY = /^-\s*\[([^\]]*)\]\(([^)]+)\)(?::\s*(.*))?$/;
|
|
703
|
+
function parseDocsIndex(text) {
|
|
704
|
+
const entries = [];
|
|
705
|
+
for (const line of text.split("\n")) {
|
|
706
|
+
const match = ENTRY.exec(line.trim());
|
|
707
|
+
if (!match) continue;
|
|
708
|
+
const description = match[3]?.trim();
|
|
709
|
+
entries.push({
|
|
710
|
+
title: match[1],
|
|
711
|
+
path: match[2],
|
|
712
|
+
...description ? { description } : {}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return entries;
|
|
716
|
+
}
|
|
717
|
+
function contentPathFor(path) {
|
|
718
|
+
const clean = `/${path.replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
|
719
|
+
if (clean.includes("/llms.mdx/")) return clean.endsWith(".md") ? clean : `${clean}/content.md`;
|
|
720
|
+
const segments = clean.split("/").filter(Boolean);
|
|
721
|
+
const docsAt = segments.indexOf("docs");
|
|
722
|
+
if (docsAt === -1) {
|
|
723
|
+
throw new Error(`Not a documentation path (expected \u2026/docs/\u2026): ${path}`);
|
|
724
|
+
}
|
|
725
|
+
const before = segments.slice(0, docsAt);
|
|
726
|
+
const after = segments.slice(docsAt);
|
|
727
|
+
return `/${[...before, "llms.mdx", ...after, "content.md"].join("/")}`;
|
|
728
|
+
}
|
|
729
|
+
function describeFailure(status, what) {
|
|
730
|
+
if (status === 401) {
|
|
731
|
+
return `The documentation platform rejected the access token. Run \`pep auth login\` again.`;
|
|
732
|
+
}
|
|
733
|
+
if (status === 403) {
|
|
734
|
+
return "This client is not allowed to read documentation \u2014 it needs the `docs:read` scope.";
|
|
735
|
+
}
|
|
736
|
+
if (status === 404) {
|
|
737
|
+
return `No such document, or your account cannot read it: ${what}`;
|
|
738
|
+
}
|
|
739
|
+
if (status === 502) {
|
|
740
|
+
return "The documentation platform could not reach PEP to verify the token. Retry shortly.";
|
|
741
|
+
}
|
|
742
|
+
return `The documentation platform answered ${status} for ${what}.`;
|
|
743
|
+
}
|
|
744
|
+
async function get(dependencies, path, accept) {
|
|
745
|
+
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
746
|
+
const response = await doFetch(`${dependencies.docsUrl.replace(/\/+$/, "")}${path}`, {
|
|
747
|
+
headers: { Authorization: `Bearer ${dependencies.accessToken}`, Accept: accept }
|
|
748
|
+
});
|
|
749
|
+
if (!response.ok) {
|
|
750
|
+
await response.body?.cancel();
|
|
751
|
+
throw new Error(describeFailure(response.status, path));
|
|
752
|
+
}
|
|
753
|
+
return response;
|
|
754
|
+
}
|
|
755
|
+
async function fetchDocsIndex(dependencies) {
|
|
756
|
+
const response = await get(dependencies, "/llms.txt", "text/plain");
|
|
757
|
+
return parseDocsIndex(await response.text());
|
|
758
|
+
}
|
|
759
|
+
async function fetchDocContent(dependencies, path) {
|
|
760
|
+
const response = await get(dependencies, contentPathFor(path), "text/markdown");
|
|
761
|
+
return response.text();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/skills-service.ts
|
|
765
|
+
var import_promises3 = require("node:fs/promises");
|
|
766
|
+
var import_node_path3 = require("node:path");
|
|
767
|
+
var import_node_zlib = require("node:zlib");
|
|
768
|
+
|
|
769
|
+
// src/tar.ts
|
|
770
|
+
var BLOCK = 512;
|
|
771
|
+
var NAME_OFFSET = 0;
|
|
772
|
+
var NAME_LENGTH = 100;
|
|
773
|
+
var SIZE_OFFSET = 124;
|
|
774
|
+
var SIZE_LENGTH = 12;
|
|
775
|
+
var TYPE_OFFSET = 156;
|
|
776
|
+
var MAGIC_OFFSET = 257;
|
|
777
|
+
var MAGIC_LENGTH = 6;
|
|
778
|
+
var PREFIX_OFFSET = 345;
|
|
779
|
+
var PREFIX_LENGTH = 155;
|
|
780
|
+
var decoder = new TextDecoder();
|
|
781
|
+
function readString(block, offset, length) {
|
|
782
|
+
const raw = block.subarray(offset, offset + length);
|
|
783
|
+
const end = raw.indexOf(0);
|
|
784
|
+
return decoder.decode(end === -1 ? raw : raw.subarray(0, end));
|
|
785
|
+
}
|
|
786
|
+
function readOctal(block, offset, length) {
|
|
787
|
+
const text = readString(block, offset, length).trim();
|
|
788
|
+
if (text === "") return 0;
|
|
789
|
+
const value = Number.parseInt(text, 8);
|
|
790
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
791
|
+
throw new Error("Malformed archive: unreadable size field.");
|
|
792
|
+
}
|
|
793
|
+
return value;
|
|
794
|
+
}
|
|
795
|
+
function isZeroBlock(block) {
|
|
796
|
+
return block.every((byte) => byte === 0);
|
|
797
|
+
}
|
|
798
|
+
function paxPath(text) {
|
|
799
|
+
for (const match of text.matchAll(/\d+ ([^=\n]+)=([^\n]*)\n/g)) {
|
|
800
|
+
if (match[1] === "path") return match[2];
|
|
801
|
+
}
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
804
|
+
function readTar(bytes) {
|
|
805
|
+
const entries = [];
|
|
806
|
+
let overrideName;
|
|
807
|
+
let offset = 0;
|
|
808
|
+
while (offset + BLOCK <= bytes.length) {
|
|
809
|
+
const header = bytes.subarray(offset, offset + BLOCK);
|
|
810
|
+
if (isZeroBlock(header)) break;
|
|
811
|
+
if (!readString(header, MAGIC_OFFSET, MAGIC_LENGTH).startsWith("ustar")) {
|
|
812
|
+
throw new Error("Malformed archive: not a ustar tar stream.");
|
|
813
|
+
}
|
|
814
|
+
const size = readOctal(header, SIZE_OFFSET, SIZE_LENGTH);
|
|
815
|
+
const type = String.fromCharCode(header[TYPE_OFFSET]);
|
|
816
|
+
const dataStart = offset + BLOCK;
|
|
817
|
+
const data = bytes.subarray(dataStart, dataStart + size);
|
|
818
|
+
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
|
|
819
|
+
if (type === "L") {
|
|
820
|
+
overrideName = decoder.decode(data).replace(/\0+$/, "");
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (type === "x") {
|
|
824
|
+
overrideName = paxPath(decoder.decode(data)) ?? overrideName;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (type !== "0" && type !== "\0") {
|
|
828
|
+
overrideName = void 0;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const name = readString(header, NAME_OFFSET, NAME_LENGTH);
|
|
832
|
+
const prefix = readString(header, PREFIX_OFFSET, PREFIX_LENGTH);
|
|
833
|
+
entries.push({
|
|
834
|
+
path: overrideName ?? (prefix === "" ? name : `${prefix}/${name}`),
|
|
835
|
+
data
|
|
836
|
+
});
|
|
837
|
+
overrideName = void 0;
|
|
838
|
+
}
|
|
839
|
+
return entries;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/skills-service.ts
|
|
843
|
+
var ARCHIVE_PATH = "/api/skills/archive";
|
|
844
|
+
var SKILLS_SEGMENT = "skills";
|
|
845
|
+
function planSkillFiles(entries) {
|
|
846
|
+
const files = [];
|
|
847
|
+
for (const entry of entries) {
|
|
848
|
+
const segments = entry.path.split("/").filter((one) => one !== "" && one !== ".");
|
|
849
|
+
if (segments.some((one) => one === "..")) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`Refusing an archive entry that escapes its directory: ${entry.path}`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
const withoutRoot = segments.slice(1);
|
|
855
|
+
const relative = withoutRoot[0] === SKILLS_SEGMENT ? withoutRoot.slice(1) : withoutRoot;
|
|
856
|
+
if (relative.length < 2) continue;
|
|
857
|
+
files.push({
|
|
858
|
+
skill: relative[0],
|
|
859
|
+
path: relative.slice(1).join("/"),
|
|
860
|
+
data: entry.data
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
return files;
|
|
864
|
+
}
|
|
865
|
+
function describeFailure2(status) {
|
|
866
|
+
if (status === 401)
|
|
867
|
+
return "PEP rejected the access token. Run `pep auth login` again.";
|
|
868
|
+
if (status === 403) {
|
|
869
|
+
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.";
|
|
870
|
+
}
|
|
871
|
+
if (status === 404) {
|
|
872
|
+
return "This PEP deployment does not serve skills \u2014 it has no /api/skills/archive endpoint. Ask an operator whether skills are enabled here.";
|
|
873
|
+
}
|
|
874
|
+
if (status === 503) {
|
|
875
|
+
return "PEP could not reach the skills repository. Retry shortly, or ask an operator whether this deployment serves skills.";
|
|
876
|
+
}
|
|
877
|
+
return `PEP answered ${status}.`;
|
|
878
|
+
}
|
|
879
|
+
async function syncSkills(dependencies) {
|
|
880
|
+
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
881
|
+
const response = await doFetch(`${dependencies.issuer}${ARCHIVE_PATH}`, {
|
|
882
|
+
headers: { Authorization: `Bearer ${dependencies.accessToken}` }
|
|
883
|
+
});
|
|
884
|
+
if (!response.ok) {
|
|
885
|
+
await response.body?.cancel();
|
|
886
|
+
throw new Error(describeFailure2(response.status));
|
|
887
|
+
}
|
|
888
|
+
const commit = response.headers.get("x-skills-commit") ?? void 0;
|
|
889
|
+
const previous = await dependencies.stateStore.read();
|
|
890
|
+
if (commit !== void 0 && commit === previous?.commit && previous.directory === dependencies.directory) {
|
|
891
|
+
await response.body?.cancel();
|
|
892
|
+
return { status: "unchanged", commit };
|
|
893
|
+
}
|
|
894
|
+
const gzipped = new Uint8Array(await response.arrayBuffer());
|
|
895
|
+
const files = planSkillFiles(readTar((0, import_node_zlib.gunzipSync)(gzipped)));
|
|
896
|
+
const skills = [...new Set(files.map((file) => file.skill))].sort();
|
|
897
|
+
for (const skill of skills) {
|
|
898
|
+
await (0, import_promises3.rm)((0, import_node_path3.join)(dependencies.directory, skill), {
|
|
899
|
+
recursive: true,
|
|
900
|
+
force: true
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
for (const file of files) {
|
|
904
|
+
const target = (0, import_node_path3.join)(
|
|
905
|
+
dependencies.directory,
|
|
906
|
+
file.skill,
|
|
907
|
+
...file.path.split("/")
|
|
908
|
+
);
|
|
909
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(target), { recursive: true });
|
|
910
|
+
await (0, import_promises3.writeFile)(target, file.data);
|
|
911
|
+
}
|
|
912
|
+
const removed = (previous?.skills ?? []).filter(
|
|
913
|
+
(skill) => !skills.includes(skill)
|
|
914
|
+
);
|
|
915
|
+
for (const skill of removed) {
|
|
916
|
+
await (0, import_promises3.rm)((0, import_node_path3.join)(previous?.directory ?? dependencies.directory, skill), {
|
|
917
|
+
recursive: true,
|
|
918
|
+
force: true
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
const state = {
|
|
922
|
+
version: 1,
|
|
923
|
+
skills,
|
|
924
|
+
directory: dependencies.directory,
|
|
925
|
+
...commit ? { commit } : {}
|
|
926
|
+
};
|
|
927
|
+
await dependencies.stateStore.write(state);
|
|
928
|
+
return {
|
|
929
|
+
status: "written",
|
|
930
|
+
skills,
|
|
931
|
+
fileCount: files.length,
|
|
932
|
+
removed,
|
|
933
|
+
directory: dependencies.directory,
|
|
934
|
+
...commit ? { commit } : {}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/cli.ts
|
|
939
|
+
var VERSION = "0.1.0";
|
|
940
|
+
function usage() {
|
|
941
|
+
return `PEP CLI ${VERSION}
|
|
942
|
+
|
|
943
|
+
Usage:
|
|
944
|
+
pep auth login [--issuer <url>] [--client-id <id>] [--resource <uri> ...]
|
|
945
|
+
pep auth status
|
|
946
|
+
pep auth token
|
|
947
|
+
pep auth logout
|
|
948
|
+
pep skills sync [--dir <path>]
|
|
949
|
+
pep docs list [--docs-url <url>]
|
|
950
|
+
pep docs get <path> [--docs-url <url>]
|
|
951
|
+
|
|
952
|
+
The issuer is built into this executable. Use --issuer only to override it temporarily.
|
|
953
|
+
--resource names which resource server the token is for (RFC 8707); repeat it for more than
|
|
954
|
+
one. It defaults to the docs platform \u2014 a token minted without it is rejected by every
|
|
955
|
+
resource server, and their answer looks exactly like "this token does not exist".
|
|
956
|
+
Use \`pep auth token\` when another agent needs a fresh bearer token.
|
|
957
|
+
|
|
958
|
+
\`pep skills sync\` fetches the latest skills from PEP and writes them where Claude Code
|
|
959
|
+
looks for them (${defaultSkillsDirectory()} unless --dir says otherwise). It only touches
|
|
960
|
+
skills it wrote itself; anything you put there by hand is left alone.
|
|
961
|
+
|
|
962
|
+
\`pep docs list\` prints the documents this account can read (path + description); feed a path
|
|
963
|
+
straight to \`pep docs get\` to print that document as markdown on stdout. The documentation
|
|
964
|
+
site is built in (${DEFAULT_DOCS_URL}); --docs-url overrides it and is then remembered.`;
|
|
965
|
+
}
|
|
966
|
+
function repeatedOption(args, name) {
|
|
967
|
+
const values = [];
|
|
968
|
+
for (; ; ) {
|
|
969
|
+
const index = args.indexOf(name);
|
|
970
|
+
if (index === -1) return values;
|
|
971
|
+
const value = args[index + 1];
|
|
972
|
+
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value.`);
|
|
973
|
+
values.push(value);
|
|
974
|
+
args.splice(index, 2);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function option(args, name) {
|
|
978
|
+
const index = args.indexOf(name);
|
|
979
|
+
if (index === -1) return void 0;
|
|
980
|
+
const value = args[index + 1];
|
|
981
|
+
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value.`);
|
|
982
|
+
args.splice(index, 2);
|
|
983
|
+
return value;
|
|
984
|
+
}
|
|
985
|
+
async function loginConfig(args) {
|
|
986
|
+
const store = fileConfigStore();
|
|
987
|
+
const saved = await store.read();
|
|
988
|
+
const issuer = configuredIssuer(option(args, "--issuer"), saved?.issuer);
|
|
989
|
+
const clientId = option(args, "--client-id") ?? saved?.clientId ?? DEFAULT_CLIENT_ID;
|
|
990
|
+
const explicit = repeatedOption(args, "--resource");
|
|
991
|
+
const resources = explicit.length > 0 ? explicit : saved?.resources ?? [...DEFAULT_RESOURCES];
|
|
992
|
+
if (args.length > 0) throw new Error(`Unknown option: ${args[0]}`);
|
|
993
|
+
return { version: 1, issuer, clientId, redirectUri: DEFAULT_REDIRECT_URI, resources };
|
|
994
|
+
}
|
|
995
|
+
async function main() {
|
|
996
|
+
const args = process.argv.slice(2);
|
|
997
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
998
|
+
console.log(usage());
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
1002
|
+
console.log(VERSION);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const group = args.shift();
|
|
1006
|
+
if (group !== "auth" && group !== "skills" && group !== "docs")
|
|
1007
|
+
throw new Error(`Unknown command.
|
|
1008
|
+
|
|
1009
|
+
${usage()}`);
|
|
1010
|
+
const command = args.shift();
|
|
1011
|
+
const configStore = fileConfigStore(configPath());
|
|
1012
|
+
const auth = createAuthService({
|
|
1013
|
+
configStore,
|
|
1014
|
+
credentialStore: systemCredentialStore(),
|
|
1015
|
+
oauth: createOAuthClient()
|
|
1016
|
+
});
|
|
1017
|
+
if (group === "docs") {
|
|
1018
|
+
if (command !== "list" && command !== "get") {
|
|
1019
|
+
throw new Error(`Unknown docs command.
|
|
1020
|
+
|
|
1021
|
+
${usage()}`);
|
|
1022
|
+
}
|
|
1023
|
+
const explicitDocsUrl = option(args, "--docs-url");
|
|
1024
|
+
const path = command === "get" ? args.shift() : void 0;
|
|
1025
|
+
if (args.length > 0) throw new Error(`Unknown option: ${args[0]}`);
|
|
1026
|
+
if (command === "get" && !path) throw new Error("pep docs get needs a document path.");
|
|
1027
|
+
const saved = await configStore.read();
|
|
1028
|
+
const docsUrl = explicitDocsUrl ? normalizeDocsUrl(explicitDocsUrl) : saved?.docsUrl ?? DEFAULT_DOCS_URL;
|
|
1029
|
+
if (explicitDocsUrl && saved && saved.docsUrl !== docsUrl) {
|
|
1030
|
+
await configStore.write({ ...saved, docsUrl });
|
|
1031
|
+
}
|
|
1032
|
+
const authorization = await auth.currentAuthorization();
|
|
1033
|
+
const dependencies = { docsUrl, accessToken: authorization.accessToken };
|
|
1034
|
+
if (command === "list") {
|
|
1035
|
+
const entries = await fetchDocsIndex(dependencies);
|
|
1036
|
+
if (entries.length === 0) {
|
|
1037
|
+
console.error("No documents are readable with this account.");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
for (const entry of entries) {
|
|
1041
|
+
console.log(entry.description ? `${entry.path} ${entry.description}` : entry.path);
|
|
1042
|
+
}
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
process.stdout.write(await fetchDocContent(dependencies, path));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (group === "skills") {
|
|
1049
|
+
if (command !== "sync") throw new Error(`Unknown skills command.
|
|
1050
|
+
|
|
1051
|
+
${usage()}`);
|
|
1052
|
+
const directory = option(args, "--dir") ?? defaultSkillsDirectory();
|
|
1053
|
+
if (args.length > 0) throw new Error(`Unknown option: ${args[0]}`);
|
|
1054
|
+
const authorization = await auth.currentAuthorization();
|
|
1055
|
+
const result = await syncSkills({
|
|
1056
|
+
issuer: authorization.issuer,
|
|
1057
|
+
accessToken: authorization.accessToken,
|
|
1058
|
+
directory,
|
|
1059
|
+
stateStore: fileSkillsStateStore()
|
|
1060
|
+
});
|
|
1061
|
+
if (result.status === "unchanged") {
|
|
1062
|
+
console.log(`Already at ${result.commit}. Nothing to do.`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
console.log(
|
|
1066
|
+
`Wrote ${result.fileCount} file(s) across ${result.skills.length} skill(s) to ${result.directory}`
|
|
1067
|
+
);
|
|
1068
|
+
if (result.commit) console.log(`Commit: ${result.commit}`);
|
|
1069
|
+
if (result.removed.length > 0)
|
|
1070
|
+
console.log(`Removed (gone upstream): ${result.removed.join(" ")}`);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (command === "login") {
|
|
1074
|
+
const config = await loginConfig(args);
|
|
1075
|
+
console.error("Opening your browser to sign in to PEP\u2026");
|
|
1076
|
+
const authorization = await auth.login(
|
|
1077
|
+
config,
|
|
1078
|
+
(url) => console.error(`If the browser does not open, visit:
|
|
1079
|
+
${url}`)
|
|
1080
|
+
);
|
|
1081
|
+
console.log(`Logged in. Granted scopes: ${authorization.scopes.join(" ")}`);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (args.length > 0) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
1085
|
+
if (command === "status") {
|
|
1086
|
+
const { authorization, user } = await auth.status();
|
|
1087
|
+
console.log("Logged in");
|
|
1088
|
+
console.log(`Issuer: ${authorization.issuer}`);
|
|
1089
|
+
console.log(`Client: ${authorization.clientId}`);
|
|
1090
|
+
console.log(`Account: ${user.email ?? user.name ?? user.sub}`);
|
|
1091
|
+
console.log(`Scopes: ${authorization.scopes.join(" ")}`);
|
|
1092
|
+
console.log(`Access token expires: ${new Date(authorization.expiresAt).toISOString()}`);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (command === "token") {
|
|
1096
|
+
const authorization = await auth.currentAuthorization();
|
|
1097
|
+
process.stdout.write(`${authorization.accessToken}
|
|
1098
|
+
`);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (command === "logout") {
|
|
1102
|
+
const result = await auth.logout();
|
|
1103
|
+
if (!result.wasLoggedIn) {
|
|
1104
|
+
console.log("Already logged out.");
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (result.revocationError)
|
|
1108
|
+
console.error(`Warning: remote revocation failed: ${result.revocationError.message}`);
|
|
1109
|
+
console.log("Logged out. Local credentials were removed.");
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
throw new Error(`Unknown auth command.
|
|
1113
|
+
|
|
1114
|
+
${usage()}`);
|
|
1115
|
+
}
|
|
1116
|
+
main().catch((error) => {
|
|
1117
|
+
console.error(`pep: ${error.message}`);
|
|
1118
|
+
process.exitCode = 1;
|
|
1119
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@newlandnpt/pep-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PEP CLI —— 凭 PEP 身份读取 Newland 开发者文档与 agent skills",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"os": [
|
|
8
|
+
"win32",
|
|
9
|
+
"darwin"
|
|
10
|
+
],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24.0.0"
|
|
13
|
+
},
|
|
14
|
+
"packageManager": "pnpm@10.18.3",
|
|
15
|
+
"bin": {
|
|
16
|
+
"pep": "dist/pep.cjs",
|
|
17
|
+
"pep-cli": "dist/pep.cjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/pep.cjs",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "node scripts/build-js.mjs",
|
|
25
|
+
"build:exe": "node scripts/build-exe.mjs production",
|
|
26
|
+
"build:exe:dev": "node scripts/build-exe.mjs development",
|
|
27
|
+
"build:exe:prod": "node scripts/build-exe.mjs production",
|
|
28
|
+
"build:installer": "node scripts/build-installer.mjs production",
|
|
29
|
+
"build:installer:dev": "node scripts/build-installer.mjs development",
|
|
30
|
+
"build:installer:prod": "node scripts/build-installer.mjs production",
|
|
31
|
+
"prepack": "node scripts/build-js.mjs production",
|
|
32
|
+
"build:exe:view": "node scripts/build-exe.mjs view",
|
|
33
|
+
"build:installer:view": "node scripts/build-installer.mjs view"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.0.0",
|
|
37
|
+
"esbuild": "^0.28.2",
|
|
38
|
+
"postject": "^1.0.0-alpha.6",
|
|
39
|
+
"typescript": "^5.9.2",
|
|
40
|
+
"vitest": "^3.2.4"
|
|
41
|
+
}
|
|
42
|
+
}
|