@modootoday/pmcp 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/LICENSE +93 -0
- package/NOTICE +49 -0
- package/README.md +69 -0
- package/SECURITY.md +27 -0
- package/dist/chunk-UTABV2C4.js +33 -0
- package/dist/chunk-WIVQKKE4.js +363 -0
- package/dist/chunk-X3KWFZBP.js +134 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1885 -0
- package/dist/index.d.ts +263 -0
- package/dist/index.js +43 -0
- package/dist/stdio.d.ts +14 -0
- package/dist/stdio.js +13 -0
- package/package.json +96 -0
- package/skills/package-skill-catalog/SKILL.md +237 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1885 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openDatabase
|
|
4
|
+
} from "./chunk-X3KWFZBP.js";
|
|
5
|
+
import {
|
|
6
|
+
serve
|
|
7
|
+
} from "./chunk-UTABV2C4.js";
|
|
8
|
+
import {
|
|
9
|
+
readCatalog
|
|
10
|
+
} from "./chunk-WIVQKKE4.js";
|
|
11
|
+
|
|
12
|
+
// src/cli/command.ts
|
|
13
|
+
var ArgumentError = class extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "ArgumentError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function parseArgs(argv, specs = []) {
|
|
20
|
+
const byName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
21
|
+
const positional = [];
|
|
22
|
+
const options = /* @__PURE__ */ new Map();
|
|
23
|
+
const flags = /* @__PURE__ */ new Set();
|
|
24
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
25
|
+
const arg = argv[i];
|
|
26
|
+
if (arg === "--") {
|
|
27
|
+
positional.push(...argv.slice(i + 1));
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
if (!arg.startsWith("--")) {
|
|
31
|
+
positional.push(arg);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const eq = arg.indexOf("=");
|
|
35
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
36
|
+
const spec = byName.get(name);
|
|
37
|
+
if (spec === void 0) {
|
|
38
|
+
throw new ArgumentError(`unknown option --${name}`);
|
|
39
|
+
}
|
|
40
|
+
if (spec.boolean === true) {
|
|
41
|
+
if (eq !== -1) {
|
|
42
|
+
throw new ArgumentError(`--${name} does not take a value`);
|
|
43
|
+
}
|
|
44
|
+
flags.add(name);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const value = eq === -1 ? argv[++i] : arg.slice(eq + 1);
|
|
48
|
+
if (value === void 0 || value === "" || value.startsWith("--")) {
|
|
49
|
+
throw new ArgumentError(`--${name} needs a value`);
|
|
50
|
+
}
|
|
51
|
+
const existing = options.get(name);
|
|
52
|
+
if (existing !== void 0 && spec.repeat !== true) {
|
|
53
|
+
throw new ArgumentError(`--${name} was given more than once`);
|
|
54
|
+
}
|
|
55
|
+
options.set(name, [...existing ?? [], value]);
|
|
56
|
+
}
|
|
57
|
+
return { positional, options, flags };
|
|
58
|
+
}
|
|
59
|
+
function one(args, name) {
|
|
60
|
+
return args.options.get(name)?.[0];
|
|
61
|
+
}
|
|
62
|
+
function many(args, name) {
|
|
63
|
+
return args.options.get(name) ?? [];
|
|
64
|
+
}
|
|
65
|
+
function printCommandHelp(ui, command) {
|
|
66
|
+
ui.heading(`pmcp ${command.name}`);
|
|
67
|
+
ui.line(` ${command.describe}`);
|
|
68
|
+
ui.line();
|
|
69
|
+
ui.line(` ${ui.paint(command.usage, "dim")}`);
|
|
70
|
+
if (command.options && command.options.length > 0) {
|
|
71
|
+
ui.line();
|
|
72
|
+
ui.table(
|
|
73
|
+
command.options.map((option) => [
|
|
74
|
+
`--${option.name}${option.placeholder ? ` ${option.placeholder}` : ""}`,
|
|
75
|
+
option.describe
|
|
76
|
+
])
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
ui.line();
|
|
80
|
+
}
|
|
81
|
+
function printHelp(ui, commands, notImplemented) {
|
|
82
|
+
ui.heading("pmcp \u2014 the skills your installed packages ship");
|
|
83
|
+
ui.line();
|
|
84
|
+
ui.table(commands.map((command) => [command.name, command.describe]));
|
|
85
|
+
ui.line();
|
|
86
|
+
ui.line(" With no command, pmcp runs the MCP server on stdio.");
|
|
87
|
+
ui.line();
|
|
88
|
+
ui.table([
|
|
89
|
+
["-q, --quiet", "only errors"],
|
|
90
|
+
["--no-color", "never colour output; NO_COLOR is honoured too"],
|
|
91
|
+
["--version", "print the version"],
|
|
92
|
+
["-h, --help", "this, or help for one command"]
|
|
93
|
+
]);
|
|
94
|
+
ui.line();
|
|
95
|
+
if (notImplemented.length > 0) {
|
|
96
|
+
ui.line(
|
|
97
|
+
` ${ui.paint(`not implemented yet: ${notImplemented.join(", ")}`, "dim")}`
|
|
98
|
+
);
|
|
99
|
+
ui.line();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/cli/ui.ts
|
|
104
|
+
var ESC = String.fromCharCode(27);
|
|
105
|
+
var CODES = {
|
|
106
|
+
reset: `${ESC}[0m`,
|
|
107
|
+
bold: `${ESC}[1m`,
|
|
108
|
+
dim: `${ESC}[2m`,
|
|
109
|
+
red: `${ESC}[31m`,
|
|
110
|
+
green: `${ESC}[32m`,
|
|
111
|
+
yellow: `${ESC}[33m`,
|
|
112
|
+
cyan: `${ESC}[36m`
|
|
113
|
+
};
|
|
114
|
+
var SYMBOL = {
|
|
115
|
+
success: "+",
|
|
116
|
+
error: "x",
|
|
117
|
+
warn: "!",
|
|
118
|
+
info: "-",
|
|
119
|
+
prompt: ">"
|
|
120
|
+
};
|
|
121
|
+
var KIND_COLOUR = {
|
|
122
|
+
success: "green",
|
|
123
|
+
error: "red",
|
|
124
|
+
warn: "yellow",
|
|
125
|
+
info: "cyan",
|
|
126
|
+
prompt: "cyan"
|
|
127
|
+
};
|
|
128
|
+
var Ui = class {
|
|
129
|
+
out;
|
|
130
|
+
err;
|
|
131
|
+
quiet;
|
|
132
|
+
colour;
|
|
133
|
+
constructor(options = {}) {
|
|
134
|
+
this.out = options.stdout ?? process.stdout;
|
|
135
|
+
this.err = options.stderr ?? process.stderr;
|
|
136
|
+
this.quiet = options.quiet ?? false;
|
|
137
|
+
const env = options.env ?? process.env;
|
|
138
|
+
this.colour = options.color ?? (this.err.isTTY === true && (env["NO_COLOR"] ?? "") === "" && env["TERM"] !== "dumb");
|
|
139
|
+
}
|
|
140
|
+
paint(text, ...colours) {
|
|
141
|
+
if (!this.colour || colours.length === 0) return text;
|
|
142
|
+
return `${colours.map((c) => CODES[c]).join("")}${text}${CODES.reset}`;
|
|
143
|
+
}
|
|
144
|
+
/** Data goes to stdout so it can be piped; everything else to stderr. */
|
|
145
|
+
data(text) {
|
|
146
|
+
this.out.write(text);
|
|
147
|
+
}
|
|
148
|
+
line(text = "") {
|
|
149
|
+
if (this.quiet) return;
|
|
150
|
+
this.err.write(`${text}
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
message(kind, text, detail) {
|
|
154
|
+
const mark = this.paint(SYMBOL[kind], KIND_COLOUR[kind], "bold");
|
|
155
|
+
const tail = detail === void 0 ? "" : ` ${this.paint(detail, "dim")}`;
|
|
156
|
+
if (kind === "error" && this.quiet) {
|
|
157
|
+
this.err.write(`${mark} ${text}${tail}
|
|
158
|
+
`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.line(`${mark} ${text}${tail}`);
|
|
162
|
+
}
|
|
163
|
+
success(text, detail) {
|
|
164
|
+
this.message("success", text, detail);
|
|
165
|
+
}
|
|
166
|
+
error(text, detail) {
|
|
167
|
+
this.message("error", text, detail);
|
|
168
|
+
}
|
|
169
|
+
warn(text, detail) {
|
|
170
|
+
this.message("warn", text, detail);
|
|
171
|
+
}
|
|
172
|
+
info(text, detail) {
|
|
173
|
+
this.message("info", text, detail);
|
|
174
|
+
}
|
|
175
|
+
/** Two aligned columns, as a help screen or a summary. */
|
|
176
|
+
table(rows, indent = " ") {
|
|
177
|
+
const width = rows.reduce((n, [left]) => Math.max(n, left.length), 0);
|
|
178
|
+
for (const [left, right] of rows) {
|
|
179
|
+
this.line(
|
|
180
|
+
`${indent}${this.paint(left.padEnd(width), "cyan")} ${this.paint(right, "dim")}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
heading(text) {
|
|
185
|
+
this.line(this.paint(text, "bold"));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// src/embed.ts
|
|
190
|
+
var DEFAULT_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
|
|
191
|
+
var ENTRY_SCHEMA = `
|
|
192
|
+
CREATE TABLE IF NOT EXISTS entry_vector (
|
|
193
|
+
name TEXT NOT NULL,
|
|
194
|
+
model_id TEXT NOT NULL,
|
|
195
|
+
dims INTEGER NOT NULL,
|
|
196
|
+
description_hash TEXT NOT NULL,
|
|
197
|
+
vector BLOB NOT NULL,
|
|
198
|
+
PRIMARY KEY (name, model_id)
|
|
199
|
+
);
|
|
200
|
+
`;
|
|
201
|
+
var TRANSFORMERS = "@huggingface/transformers";
|
|
202
|
+
async function loadEmbedder(modelId = DEFAULT_MODEL_ID) {
|
|
203
|
+
let module;
|
|
204
|
+
try {
|
|
205
|
+
module = await import(TRANSFORMERS);
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const factory = module.pipeline;
|
|
210
|
+
if (typeof factory !== "function") return null;
|
|
211
|
+
const encode = await factory("feature-extraction", modelId);
|
|
212
|
+
let dims = 0;
|
|
213
|
+
return {
|
|
214
|
+
modelId,
|
|
215
|
+
get dims() {
|
|
216
|
+
return dims;
|
|
217
|
+
},
|
|
218
|
+
async embed(text) {
|
|
219
|
+
const output = await encode(text, { pooling: "mean", normalize: true });
|
|
220
|
+
const vector = Float32Array.from(output.data);
|
|
221
|
+
dims = vector.length;
|
|
222
|
+
return vector;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function describeHash(description) {
|
|
227
|
+
let hash = 2166136261;
|
|
228
|
+
for (let i = 0; i < description.length; i++) {
|
|
229
|
+
hash ^= description.charCodeAt(i);
|
|
230
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
231
|
+
}
|
|
232
|
+
return hash.toString(16).padStart(8, "0");
|
|
233
|
+
}
|
|
234
|
+
async function openEntryVectors(path) {
|
|
235
|
+
const opened = await openDatabase(path);
|
|
236
|
+
if (opened === null) return null;
|
|
237
|
+
const db = opened.db;
|
|
238
|
+
db.exec(ENTRY_SCHEMA);
|
|
239
|
+
return {
|
|
240
|
+
read(modelId) {
|
|
241
|
+
const out = /* @__PURE__ */ new Map();
|
|
242
|
+
const rows = db.prepare(
|
|
243
|
+
"SELECT name, dims, vector FROM entry_vector WHERE model_id = ?"
|
|
244
|
+
).all(modelId);
|
|
245
|
+
for (const row of rows) {
|
|
246
|
+
const bytes = row.vector;
|
|
247
|
+
const vector = new Float32Array(
|
|
248
|
+
bytes.buffer.slice(
|
|
249
|
+
bytes.byteOffset,
|
|
250
|
+
bytes.byteOffset + bytes.byteLength
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
if (vector.length === row.dims) out.set(row.name, vector);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
},
|
|
257
|
+
stale(modelId, entries) {
|
|
258
|
+
const rows = db.prepare(
|
|
259
|
+
"SELECT name, description_hash FROM entry_vector WHERE model_id = ?"
|
|
260
|
+
).all(modelId);
|
|
261
|
+
const stored = new Map(
|
|
262
|
+
rows.map((row) => [row.name, row.description_hash])
|
|
263
|
+
);
|
|
264
|
+
const out = [];
|
|
265
|
+
for (const [name, description] of entries) {
|
|
266
|
+
if (stored.get(name) !== describeHash(description)) out.push(name);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
},
|
|
270
|
+
write(modelId, name, description, vector) {
|
|
271
|
+
db.prepare(
|
|
272
|
+
`INSERT INTO entry_vector (name, model_id, dims, description_hash, vector)
|
|
273
|
+
VALUES (?, ?, ?, ?, ?)
|
|
274
|
+
ON CONFLICT(name, model_id) DO UPDATE SET
|
|
275
|
+
dims = excluded.dims,
|
|
276
|
+
description_hash = excluded.description_hash,
|
|
277
|
+
vector = excluded.vector`
|
|
278
|
+
).run(
|
|
279
|
+
name,
|
|
280
|
+
modelId,
|
|
281
|
+
vector.length,
|
|
282
|
+
describeHash(description),
|
|
283
|
+
new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength)
|
|
284
|
+
);
|
|
285
|
+
},
|
|
286
|
+
close() {
|
|
287
|
+
db.close();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/commands/roots.ts
|
|
293
|
+
var CATALOG_OPTIONS = [
|
|
294
|
+
{
|
|
295
|
+
name: "root",
|
|
296
|
+
describe: "A node_modules directory to walk. Repeatable.",
|
|
297
|
+
repeat: true,
|
|
298
|
+
placeholder: "<dir>"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "scope",
|
|
302
|
+
describe: "Only packages whose names start with this. Repeatable.",
|
|
303
|
+
repeat: true,
|
|
304
|
+
placeholder: "<prefix>"
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
function rootsFrom(context) {
|
|
308
|
+
const given = many(context.args, "root");
|
|
309
|
+
return given.length > 0 ? given : [`${context.cwd}/node_modules`];
|
|
310
|
+
}
|
|
311
|
+
function catalogFrom(context) {
|
|
312
|
+
const scopes = many(context.args, "scope");
|
|
313
|
+
const roots = rootsFrom(context);
|
|
314
|
+
return readCatalog(scopes.length > 0 ? { roots, scopes } : { roots });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/commands/reindex.ts
|
|
318
|
+
function indexPath(root) {
|
|
319
|
+
return `${root}/.cache/pmcp/vectors.sqlite`;
|
|
320
|
+
}
|
|
321
|
+
var indexCommand = {
|
|
322
|
+
name: "index",
|
|
323
|
+
describe: "Encode the catalog so ranking can be semantic",
|
|
324
|
+
usage: "pmcp index [--root <dir>] [--model <id>]",
|
|
325
|
+
options: [
|
|
326
|
+
...CATALOG_OPTIONS,
|
|
327
|
+
{
|
|
328
|
+
name: "model",
|
|
329
|
+
describe: "Encoder to use. Rows from another model are never scored.",
|
|
330
|
+
placeholder: "<id>"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "json",
|
|
334
|
+
describe: "Emit JSON rather than a summary.",
|
|
335
|
+
boolean: true
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "dry-run",
|
|
339
|
+
describe: "Report what would be encoded and write nothing.",
|
|
340
|
+
boolean: true
|
|
341
|
+
}
|
|
342
|
+
],
|
|
343
|
+
async run(context) {
|
|
344
|
+
const entries = catalogFrom(context);
|
|
345
|
+
if (entries.length === 0) {
|
|
346
|
+
context.ui.error("no skills to index", rootsFrom(context).join(" "));
|
|
347
|
+
return 1;
|
|
348
|
+
}
|
|
349
|
+
const dryRun = context.args.flags.has("dry-run");
|
|
350
|
+
const model = one(context.args, "model");
|
|
351
|
+
const embedder = await loadEmbedder(model);
|
|
352
|
+
if (embedder === null) {
|
|
353
|
+
context.ui.error(
|
|
354
|
+
"semantic ranking needs an encoder that is not installed"
|
|
355
|
+
);
|
|
356
|
+
context.ui.info("npm install --save-dev @huggingface/transformers");
|
|
357
|
+
context.ui.info("ranking stays lexical until then, which is the default");
|
|
358
|
+
return 3;
|
|
359
|
+
}
|
|
360
|
+
const store = await openEntryVectors(
|
|
361
|
+
indexPath(rootsFrom(context)[0] ?? ".")
|
|
362
|
+
);
|
|
363
|
+
if (store === null) {
|
|
364
|
+
context.ui.error(
|
|
365
|
+
"no sqlite in this runtime, so vectors cannot be stored"
|
|
366
|
+
);
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const descriptions = new Map(entries.map((e) => [e.name, e.description]));
|
|
371
|
+
const stale = new Set(store.stale(embedder.modelId, descriptions));
|
|
372
|
+
const todo = entries.filter((entry2) => stale.has(entry2.name));
|
|
373
|
+
context.ui.info(
|
|
374
|
+
`${todo.length} of ${entries.length} to encode`,
|
|
375
|
+
embedder.modelId
|
|
376
|
+
);
|
|
377
|
+
if (dryRun) {
|
|
378
|
+
for (const entry2 of todo)
|
|
379
|
+
context.ui.line(` would encode ${entry2.name}`);
|
|
380
|
+
context.ui.info("dry run, nothing written");
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
for (const entry2 of todo) {
|
|
384
|
+
const vector = await embedder.embed(entry2.description);
|
|
385
|
+
store.write(embedder.modelId, entry2.name, entry2.description, vector);
|
|
386
|
+
}
|
|
387
|
+
const indexed = store.read(embedder.modelId).size;
|
|
388
|
+
if (context.args.flags.has("json")) {
|
|
389
|
+
context.ui.data(
|
|
390
|
+
`${JSON.stringify(
|
|
391
|
+
{
|
|
392
|
+
model: embedder.modelId,
|
|
393
|
+
encoded: todo.length,
|
|
394
|
+
indexed,
|
|
395
|
+
catalog: entries.length
|
|
396
|
+
},
|
|
397
|
+
null,
|
|
398
|
+
2
|
|
399
|
+
)}
|
|
400
|
+
`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (indexed < entries.length) {
|
|
404
|
+
context.ui.warn(
|
|
405
|
+
`indexed ${indexed} of ${entries.length}`,
|
|
406
|
+
"ranking stays lexical"
|
|
407
|
+
);
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
context.ui.success(`indexed ${indexed} skills`, embedder.modelId);
|
|
411
|
+
return 0;
|
|
412
|
+
} finally {
|
|
413
|
+
store.close();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// src/commands/list.ts
|
|
419
|
+
var listCommand = {
|
|
420
|
+
name: "list",
|
|
421
|
+
describe: "List the skills discovered in node_modules",
|
|
422
|
+
usage: "pmcp list [--root <dir>] [--scope <prefix>] [--json]",
|
|
423
|
+
options: [
|
|
424
|
+
...CATALOG_OPTIONS,
|
|
425
|
+
{ name: "json", describe: "Emit JSON rather than a table.", boolean: true }
|
|
426
|
+
],
|
|
427
|
+
run(context) {
|
|
428
|
+
const entries = catalogFrom(context);
|
|
429
|
+
if (context.args.flags.has("json")) {
|
|
430
|
+
context.ui.data(`${JSON.stringify(entries, null, 2)}
|
|
431
|
+
`);
|
|
432
|
+
return entries.length > 0 ? 0 : 1;
|
|
433
|
+
}
|
|
434
|
+
if (entries.length === 0) {
|
|
435
|
+
context.ui.error("no skills found", rootsFrom(context).join(" "));
|
|
436
|
+
return 1;
|
|
437
|
+
}
|
|
438
|
+
context.ui.table(entries.map((entry2) => [entry2.name, entry2.description]));
|
|
439
|
+
context.ui.success(`${entries.length} skills`);
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/commands/serve.ts
|
|
445
|
+
var serveCommand = {
|
|
446
|
+
name: "serve",
|
|
447
|
+
describe: "Run the MCP server on stdio (the default with no command)",
|
|
448
|
+
usage: "pmcp serve [--root <dir>] [--scope <prefix>]",
|
|
449
|
+
options: [...CATALOG_OPTIONS],
|
|
450
|
+
run(context) {
|
|
451
|
+
serve(rootsFrom(context), many(context.args, "scope"));
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/remote/session.ts
|
|
457
|
+
import {
|
|
458
|
+
chmodSync,
|
|
459
|
+
closeSync,
|
|
460
|
+
fchmodSync,
|
|
461
|
+
mkdirSync,
|
|
462
|
+
openSync,
|
|
463
|
+
readFileSync,
|
|
464
|
+
rmSync,
|
|
465
|
+
writeSync
|
|
466
|
+
} from "fs";
|
|
467
|
+
import { homedir } from "os";
|
|
468
|
+
import { join } from "path";
|
|
469
|
+
var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
470
|
+
function globalDir(home = homedir(), env = process.env) {
|
|
471
|
+
const xdg = env["XDG_CONFIG_HOME"];
|
|
472
|
+
return xdg !== void 0 && xdg !== "" ? join(xdg, "pmcp") : join(home, ".pmcp");
|
|
473
|
+
}
|
|
474
|
+
var form = (fields) => new URLSearchParams(fields).toString();
|
|
475
|
+
var FORM_HEADERS = { "content-type": "application/x-www-form-urlencoded" };
|
|
476
|
+
function validateIssuerUrl(value, issuer) {
|
|
477
|
+
const url = new URL(value);
|
|
478
|
+
if (url.protocol !== "https:" || url.origin !== new URL(issuer).origin || url.username || url.password || url.hash) {
|
|
479
|
+
throw new Error("OAuth endpoint must stay on the issuer origin");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function discover(issuer, fetcher2) {
|
|
483
|
+
const base = issuer.replace(/\/+$/, "");
|
|
484
|
+
const source = new URL(base);
|
|
485
|
+
if (source.protocol !== "https:" || source.username || source.password || source.search || source.hash) {
|
|
486
|
+
throw new Error("the issuer must be a plain HTTPS origin");
|
|
487
|
+
}
|
|
488
|
+
const res = await fetcher2(`${base}/.well-known/oauth-authorization-server`);
|
|
489
|
+
if (!res.ok)
|
|
490
|
+
throw new Error(`issuer did not answer discovery (${res.status})`);
|
|
491
|
+
const body = await res.json();
|
|
492
|
+
if (!body.token_endpoint || !body.device_authorization_endpoint) {
|
|
493
|
+
throw new Error("issuer advertises no device grant");
|
|
494
|
+
}
|
|
495
|
+
for (const endpoint of [
|
|
496
|
+
body.token_endpoint,
|
|
497
|
+
body.device_authorization_endpoint,
|
|
498
|
+
body.revocation_endpoint
|
|
499
|
+
]) {
|
|
500
|
+
if (endpoint !== void 0) validateIssuerUrl(endpoint, base);
|
|
501
|
+
}
|
|
502
|
+
const grants = body.grant_types_supported ?? [];
|
|
503
|
+
if (grants.length > 0 && !grants.includes(DEVICE_GRANT)) {
|
|
504
|
+
throw new Error("issuer does not support the device grant");
|
|
505
|
+
}
|
|
506
|
+
return body;
|
|
507
|
+
}
|
|
508
|
+
async function startDevice(metadata, request, fetcher2, now = Date.now) {
|
|
509
|
+
const res = await fetcher2(metadata.device_authorization_endpoint, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: FORM_HEADERS,
|
|
512
|
+
body: form({
|
|
513
|
+
client_id: request.clientId,
|
|
514
|
+
resource: request.resource,
|
|
515
|
+
scope: request.scope
|
|
516
|
+
})
|
|
517
|
+
});
|
|
518
|
+
const body = await res.json();
|
|
519
|
+
if (!res.ok) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
String(body["error"] ?? `device request failed (${res.status})`)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const deviceCode = body["device_code"];
|
|
525
|
+
const userCode = body["user_code"];
|
|
526
|
+
const verificationUri = body["verification_uri"];
|
|
527
|
+
if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string") {
|
|
528
|
+
throw new Error("device response is missing a field");
|
|
529
|
+
}
|
|
530
|
+
const interval = Number(body["interval"] ?? 5);
|
|
531
|
+
const expires = Number(body["expires_in"] ?? 600);
|
|
532
|
+
const complete = body["verification_uri_complete"];
|
|
533
|
+
return {
|
|
534
|
+
deviceCode,
|
|
535
|
+
userCode,
|
|
536
|
+
verificationUri,
|
|
537
|
+
...typeof complete === "string" ? { verificationUriComplete: complete } : {},
|
|
538
|
+
intervalMs: (Number.isFinite(interval) ? interval : 5) * 1e3,
|
|
539
|
+
expiresAt: now() + (Number.isFinite(expires) ? expires : 600) * 1e3
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async function pollOnce(metadata, request, fetcher2, now = Date.now) {
|
|
543
|
+
const res = await fetcher2(metadata.token_endpoint, {
|
|
544
|
+
method: "POST",
|
|
545
|
+
headers: FORM_HEADERS,
|
|
546
|
+
body: form({
|
|
547
|
+
grant_type: DEVICE_GRANT,
|
|
548
|
+
device_code: request.deviceCode,
|
|
549
|
+
client_id: request.clientId
|
|
550
|
+
})
|
|
551
|
+
});
|
|
552
|
+
const body = await res.json();
|
|
553
|
+
if (res.ok) {
|
|
554
|
+
const token = body["access_token"];
|
|
555
|
+
if (typeof token !== "string")
|
|
556
|
+
throw new Error("token response has no token");
|
|
557
|
+
const expiresIn = Number(body["expires_in"]);
|
|
558
|
+
const refresh = body["refresh_token"];
|
|
559
|
+
const scope = body["scope"];
|
|
560
|
+
return {
|
|
561
|
+
kind: "granted",
|
|
562
|
+
session: {
|
|
563
|
+
issuer: request.issuer,
|
|
564
|
+
accessToken: token,
|
|
565
|
+
...typeof refresh === "string" ? { refreshToken: refresh } : {},
|
|
566
|
+
...Number.isFinite(expiresIn) ? { expiresAt: now() + expiresIn * 1e3 } : {},
|
|
567
|
+
...typeof scope === "string" ? { scope } : {}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
switch (body["error"]) {
|
|
572
|
+
case "authorization_pending":
|
|
573
|
+
return { kind: "pending" };
|
|
574
|
+
case "slow_down":
|
|
575
|
+
return { kind: "slow_down" };
|
|
576
|
+
case "expired_token":
|
|
577
|
+
return { kind: "expired" };
|
|
578
|
+
default:
|
|
579
|
+
return { kind: "denied" };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
var sessionPath = (home) => join(globalDir(home), "session.json");
|
|
583
|
+
function readSession(home) {
|
|
584
|
+
try {
|
|
585
|
+
return JSON.parse(readFileSync(sessionPath(home), "utf8"));
|
|
586
|
+
} catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function writeSession(session, home) {
|
|
591
|
+
const path = sessionPath(home);
|
|
592
|
+
const directory = globalDir(home);
|
|
593
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
594
|
+
chmodSync(directory, 448);
|
|
595
|
+
const fd = openSync(path, "w", 384);
|
|
596
|
+
try {
|
|
597
|
+
fchmodSync(fd, 384);
|
|
598
|
+
writeSync(fd, `${JSON.stringify(session, null, 2)}
|
|
599
|
+
`);
|
|
600
|
+
} finally {
|
|
601
|
+
closeSync(fd);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function clearSession(home) {
|
|
605
|
+
try {
|
|
606
|
+
rmSync(sessionPath(home));
|
|
607
|
+
} catch {
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function revoke(metadata, session, clientId, fetcher2) {
|
|
611
|
+
if (!metadata.revocation_endpoint) return false;
|
|
612
|
+
validateIssuerUrl(metadata.revocation_endpoint, session.issuer);
|
|
613
|
+
let revoked = true;
|
|
614
|
+
const tokens = [session.refreshToken, session.accessToken].filter(
|
|
615
|
+
(token) => typeof token === "string" && token !== ""
|
|
616
|
+
);
|
|
617
|
+
for (const token of tokens) {
|
|
618
|
+
try {
|
|
619
|
+
const res = await fetcher2(metadata.revocation_endpoint, {
|
|
620
|
+
method: "POST",
|
|
621
|
+
headers: FORM_HEADERS,
|
|
622
|
+
body: form({ token, client_id: clientId })
|
|
623
|
+
});
|
|
624
|
+
if (!res.ok) revoked = false;
|
|
625
|
+
} catch {
|
|
626
|
+
revoked = false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return revoked;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/commands/session.ts
|
|
633
|
+
var DEFAULT_ISSUER = "https://auth.pmcp.build";
|
|
634
|
+
var DEFAULT_CLIENT = "https://auth.pmcp.build/oauth-client.json";
|
|
635
|
+
var DEFAULT_RESOURCE = "https://api.pmcp.build";
|
|
636
|
+
var DEFAULT_SCOPE = "pmcp:catalog:read pmcp:distribution:read";
|
|
637
|
+
var fetcher = (url, init) => fetch(url, init);
|
|
638
|
+
var setting = (env, name, fallback) => env[name] ?? fallback;
|
|
639
|
+
var wait = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
640
|
+
var loginCommand = {
|
|
641
|
+
name: "login",
|
|
642
|
+
describe: "Sign in on this machine, to obtain skills this project lacks",
|
|
643
|
+
usage: "pmcp login [--issuer <url>]",
|
|
644
|
+
options: [
|
|
645
|
+
{
|
|
646
|
+
name: "issuer",
|
|
647
|
+
describe: "override the auth issuer",
|
|
648
|
+
placeholder: "<url>"
|
|
649
|
+
}
|
|
650
|
+
],
|
|
651
|
+
async run({ ui, args, env }) {
|
|
652
|
+
const issuer = args.options.get("issuer")?.[0] ?? setting(env, "PMCP_AUTH_ISSUER", DEFAULT_ISSUER);
|
|
653
|
+
const clientId = setting(env, "PMCP_OAUTH_CLIENT_ID", DEFAULT_CLIENT);
|
|
654
|
+
const resource = setting(env, "PMCP_OAUTH_RESOURCE", DEFAULT_RESOURCE);
|
|
655
|
+
const scope = setting(env, "PMCP_OAUTH_SCOPE", DEFAULT_SCOPE);
|
|
656
|
+
let metadata;
|
|
657
|
+
try {
|
|
658
|
+
metadata = await discover(issuer, fetcher);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
ui.error("cannot sign in", error.message);
|
|
661
|
+
return 1;
|
|
662
|
+
}
|
|
663
|
+
let start;
|
|
664
|
+
try {
|
|
665
|
+
start = await startDevice(
|
|
666
|
+
metadata,
|
|
667
|
+
{ clientId, resource, scope },
|
|
668
|
+
fetcher
|
|
669
|
+
);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
ui.error("cannot start sign-in", error.message);
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
ui.info("open", start.verificationUriComplete ?? start.verificationUri);
|
|
675
|
+
ui.info("code", start.userCode);
|
|
676
|
+
ui.line();
|
|
677
|
+
ui.info("waiting", "approve it and this finishes on its own");
|
|
678
|
+
let interval = start.intervalMs;
|
|
679
|
+
for (; ; ) {
|
|
680
|
+
if (Date.now() >= start.expiresAt) {
|
|
681
|
+
ui.error("expired", "the code timed out; run pmcp login again");
|
|
682
|
+
return 1;
|
|
683
|
+
}
|
|
684
|
+
await wait(interval);
|
|
685
|
+
const outcome = await pollOnce(
|
|
686
|
+
metadata,
|
|
687
|
+
{ clientId, deviceCode: start.deviceCode, issuer },
|
|
688
|
+
fetcher
|
|
689
|
+
);
|
|
690
|
+
if (outcome.kind === "granted" && outcome.session) {
|
|
691
|
+
writeSession(outcome.session);
|
|
692
|
+
ui.success("signed in", issuer);
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
if (outcome.kind === "slow_down") interval += 5e3;
|
|
696
|
+
if (outcome.kind === "denied") {
|
|
697
|
+
ui.error("declined", "the request was not approved");
|
|
698
|
+
return 1;
|
|
699
|
+
}
|
|
700
|
+
if (outcome.kind === "expired") {
|
|
701
|
+
ui.error("expired", "the code timed out; run pmcp login again");
|
|
702
|
+
return 1;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
var logoutCommand = {
|
|
708
|
+
name: "logout",
|
|
709
|
+
describe: "Forget the sign-in on this machine",
|
|
710
|
+
usage: "pmcp logout",
|
|
711
|
+
async run({ ui, env }) {
|
|
712
|
+
const session = readSession();
|
|
713
|
+
if (!session) {
|
|
714
|
+
ui.info("not signed in", "nothing to forget");
|
|
715
|
+
return 0;
|
|
716
|
+
}
|
|
717
|
+
const clientId = setting(env, "PMCP_OAUTH_CLIENT_ID", DEFAULT_CLIENT);
|
|
718
|
+
clearSession();
|
|
719
|
+
try {
|
|
720
|
+
const metadata = await discover(session.issuer, fetcher);
|
|
721
|
+
const revoked = await revoke(metadata, session, clientId, fetcher);
|
|
722
|
+
ui.success(
|
|
723
|
+
"signed out",
|
|
724
|
+
revoked ? "token revoked" : "local copy removed"
|
|
725
|
+
);
|
|
726
|
+
} catch {
|
|
727
|
+
ui.success("signed out", "local copy removed");
|
|
728
|
+
}
|
|
729
|
+
return 0;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var whoamiCommand = {
|
|
733
|
+
name: "whoami",
|
|
734
|
+
describe: "Whether this machine is signed in, and to what",
|
|
735
|
+
usage: "pmcp whoami [--json]",
|
|
736
|
+
options: [
|
|
737
|
+
{ name: "json", describe: "Print the answer as JSON.", boolean: true }
|
|
738
|
+
],
|
|
739
|
+
run({ ui, args }) {
|
|
740
|
+
const session = readSession();
|
|
741
|
+
if (!session) {
|
|
742
|
+
if (args.flags.has("json")) {
|
|
743
|
+
ui.data(`${JSON.stringify({ signedIn: false }, null, 2)}
|
|
744
|
+
`);
|
|
745
|
+
} else {
|
|
746
|
+
ui.data("not signed in\n");
|
|
747
|
+
ui.info("sign in", "run pmcp login");
|
|
748
|
+
}
|
|
749
|
+
return 1;
|
|
750
|
+
}
|
|
751
|
+
const left = session.expiresAt === void 0 ? void 0 : session.expiresAt - Date.now();
|
|
752
|
+
const answer = {
|
|
753
|
+
signedIn: true,
|
|
754
|
+
issuer: session.issuer,
|
|
755
|
+
...session.scope === void 0 ? {} : { scope: session.scope },
|
|
756
|
+
...left === void 0 ? {} : { expiresInMinutes: Math.max(0, Math.floor(left / 6e4)) }
|
|
757
|
+
};
|
|
758
|
+
if (args.flags.has("json")) {
|
|
759
|
+
ui.data(`${JSON.stringify(answer, null, 2)}
|
|
760
|
+
`);
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
ui.data(`issuer ${answer.issuer}
|
|
764
|
+
`);
|
|
765
|
+
if (answer.scope !== void 0) ui.data(`scope ${answer.scope}
|
|
766
|
+
`);
|
|
767
|
+
if (left !== void 0) {
|
|
768
|
+
ui.data(
|
|
769
|
+
left > 0 ? `token valid for ${String(answer.expiresInMinutes)} min
|
|
770
|
+
` : "token expired, run pmcp login\n"
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
return 0;
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// src/installed.ts
|
|
778
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
779
|
+
import { dirname, join as join2, resolve } from "path";
|
|
780
|
+
function object(value) {
|
|
781
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
782
|
+
}
|
|
783
|
+
function absent(error) {
|
|
784
|
+
return object(error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
785
|
+
}
|
|
786
|
+
function packageName(name) {
|
|
787
|
+
return /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/u.test(name);
|
|
788
|
+
}
|
|
789
|
+
function locate(name, project) {
|
|
790
|
+
let directory = project;
|
|
791
|
+
for (; ; ) {
|
|
792
|
+
let source;
|
|
793
|
+
try {
|
|
794
|
+
source = readFileSync2(
|
|
795
|
+
join2(directory, "node_modules", name, "package.json"),
|
|
796
|
+
"utf8"
|
|
797
|
+
);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (!absent(error)) return { name, reason: "unreadable_manifest" };
|
|
800
|
+
const parent = dirname(directory);
|
|
801
|
+
if (parent === directory) return { name, reason: "not_installed" };
|
|
802
|
+
directory = parent;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
let manifest;
|
|
806
|
+
try {
|
|
807
|
+
manifest = JSON.parse(source);
|
|
808
|
+
} catch {
|
|
809
|
+
return { name, reason: "invalid_manifest" };
|
|
810
|
+
}
|
|
811
|
+
if (!object(manifest) || typeof manifest.name !== "string" || !packageName(manifest.name) || typeof manifest.version !== "string" || manifest.version.trim() === "") {
|
|
812
|
+
return { name, reason: "invalid_manifest" };
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
name: manifest.name,
|
|
816
|
+
requestedAs: name,
|
|
817
|
+
version: manifest.version
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function readInstalledDependencies(projectDirectory) {
|
|
822
|
+
const project = resolve(projectDirectory);
|
|
823
|
+
const manifest = JSON.parse(
|
|
824
|
+
readFileSync2(join2(project, "package.json"), "utf8")
|
|
825
|
+
);
|
|
826
|
+
if (!object(manifest))
|
|
827
|
+
throw new Error("project package.json must contain an object");
|
|
828
|
+
const names = /* @__PURE__ */ new Set();
|
|
829
|
+
for (const field of [
|
|
830
|
+
"dependencies",
|
|
831
|
+
"devDependencies",
|
|
832
|
+
"optionalDependencies",
|
|
833
|
+
"peerDependencies"
|
|
834
|
+
]) {
|
|
835
|
+
const declared = manifest[field];
|
|
836
|
+
if (declared === void 0) continue;
|
|
837
|
+
if (!object(declared)) throw new Error(`${field} must contain an object`);
|
|
838
|
+
for (const [name, range] of Object.entries(declared)) {
|
|
839
|
+
if (!packageName(name) || typeof range !== "string") {
|
|
840
|
+
throw new Error(`invalid dependency declaration in ${field}`);
|
|
841
|
+
}
|
|
842
|
+
names.add(name);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const dependencies = [];
|
|
846
|
+
const issues = [];
|
|
847
|
+
for (const name of [...names].sort()) {
|
|
848
|
+
const result = locate(name, project);
|
|
849
|
+
if ("reason" in result) issues.push(result);
|
|
850
|
+
else dependencies.push(result);
|
|
851
|
+
}
|
|
852
|
+
return { dependencies, issues };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/install/plan.ts
|
|
856
|
+
import { createHash } from "crypto";
|
|
857
|
+
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
858
|
+
import { dirname as dirname2, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
859
|
+
import semver from "semver";
|
|
860
|
+
function matchingSkills(catalog, inventory) {
|
|
861
|
+
const matches = [];
|
|
862
|
+
for (const dependency of inventory.dependencies) {
|
|
863
|
+
for (const entry2 of catalog.entries) {
|
|
864
|
+
const target2 = entry2.targets.find(
|
|
865
|
+
(target3) => target3.packageName === dependency.name
|
|
866
|
+
);
|
|
867
|
+
if (target2 && semver.satisfies(dependency.version, target2.range)) {
|
|
868
|
+
matches.push({
|
|
869
|
+
targetName: dependency.name,
|
|
870
|
+
targetVersion: dependency.version,
|
|
871
|
+
verified: target2.verifiedVersions.includes(dependency.version),
|
|
872
|
+
entry: entry2
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return matches;
|
|
878
|
+
}
|
|
879
|
+
var LOCKS = [
|
|
880
|
+
"package-lock.json",
|
|
881
|
+
"npm-shrinkwrap.json",
|
|
882
|
+
"bun.lock",
|
|
883
|
+
"bun.lockb",
|
|
884
|
+
"yarn.lock",
|
|
885
|
+
"pnpm-lock.yaml"
|
|
886
|
+
];
|
|
887
|
+
function fingerprint(files) {
|
|
888
|
+
const hash = createHash("sha256");
|
|
889
|
+
for (const file of files) {
|
|
890
|
+
hash.update(file).update("\0");
|
|
891
|
+
try {
|
|
892
|
+
hash.update(readFileSync3(file));
|
|
893
|
+
} catch (error) {
|
|
894
|
+
if (error.code !== "ENOENT") throw error;
|
|
895
|
+
hash.update("absent");
|
|
896
|
+
}
|
|
897
|
+
hash.update("\0");
|
|
898
|
+
}
|
|
899
|
+
return hash.digest("hex");
|
|
900
|
+
}
|
|
901
|
+
function manifestAt(directory) {
|
|
902
|
+
const value = JSON.parse(
|
|
903
|
+
readFileSync3(join3(directory, "package.json"), "utf8")
|
|
904
|
+
);
|
|
905
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
906
|
+
throw new Error("invalid project manifest");
|
|
907
|
+
return value;
|
|
908
|
+
}
|
|
909
|
+
function installContext(directory) {
|
|
910
|
+
const project = resolve2(directory);
|
|
911
|
+
const manifest = manifestAt(project);
|
|
912
|
+
let root = project;
|
|
913
|
+
let ancestor = project;
|
|
914
|
+
for (; ; ) {
|
|
915
|
+
if (ancestor !== project && existsSync(join3(ancestor, "package.json"))) {
|
|
916
|
+
const candidate = manifestAt(ancestor);
|
|
917
|
+
if (candidate.workspaces !== void 0) {
|
|
918
|
+
root = ancestor;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
if (LOCKS.some((file) => existsSync(join3(ancestor, file)))) break;
|
|
922
|
+
}
|
|
923
|
+
if (existsSync(join3(ancestor, ".git")) || dirname2(ancestor) === ancestor)
|
|
924
|
+
break;
|
|
925
|
+
ancestor = dirname2(ancestor);
|
|
926
|
+
}
|
|
927
|
+
const owner = manifestAt(root);
|
|
928
|
+
const lockNames = LOCKS.filter((file) => existsSync(join3(root, file)));
|
|
929
|
+
const lockManagers = new Set(
|
|
930
|
+
lockNames.map((file) => {
|
|
931
|
+
if (file.startsWith("bun.")) return "bun";
|
|
932
|
+
if (file === "package-lock.json" || file === "npm-shrinkwrap.json")
|
|
933
|
+
return "npm";
|
|
934
|
+
return "unsupported";
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
if (lockManagers.size > 1 || lockManagers.has("unsupported"))
|
|
938
|
+
throw new Error("conflicting or unsupported package manager lockfiles");
|
|
939
|
+
const declared = owner.packageManager?.match(
|
|
940
|
+
/^(npm|bun)@\d+\.\d+\.\d+(?:[-+].+)?$/u
|
|
941
|
+
)?.[1];
|
|
942
|
+
if (owner.packageManager !== void 0 && !declared)
|
|
943
|
+
throw new Error("only npm and Bun projects are supported");
|
|
944
|
+
const locked = [...lockManagers][0];
|
|
945
|
+
if (declared && locked && declared !== locked)
|
|
946
|
+
throw new Error("packageManager and lockfile disagree");
|
|
947
|
+
const manager = declared ?? locked;
|
|
948
|
+
if (manager !== "npm" && manager !== "bun")
|
|
949
|
+
throw new Error(
|
|
950
|
+
"declare packageManager or install project dependencies first"
|
|
951
|
+
);
|
|
952
|
+
if (root !== project && LOCKS.some((file) => existsSync(join3(project, file)))) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
"nested project has its own lockfile; resolve the workspace boundary first"
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const files = [
|
|
958
|
+
.../* @__PURE__ */ new Set([
|
|
959
|
+
join3(root, "package.json"),
|
|
960
|
+
join3(project, "package.json"),
|
|
961
|
+
...LOCKS.map((file) => join3(root, file))
|
|
962
|
+
])
|
|
963
|
+
];
|
|
964
|
+
return {
|
|
965
|
+
project,
|
|
966
|
+
root,
|
|
967
|
+
manager,
|
|
968
|
+
manifest,
|
|
969
|
+
files,
|
|
970
|
+
fingerprint: fingerprint(files)
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function planInstall(options) {
|
|
974
|
+
if (options.inventory.issues.length)
|
|
975
|
+
throw new Error(
|
|
976
|
+
"resolve missing or invalid installed dependencies before changing skills"
|
|
977
|
+
);
|
|
978
|
+
const { context, catalog } = options;
|
|
979
|
+
const matched = matchingSkills(catalog, options.inventory);
|
|
980
|
+
const available = new Set(
|
|
981
|
+
matched.map((match) => match.entry.delivery.packageName)
|
|
982
|
+
);
|
|
983
|
+
if (options.sync && (options.selected.length || options.all))
|
|
984
|
+
throw new Error("sync accepts neither package arguments nor --all");
|
|
985
|
+
if (!options.sync && !options.all && options.selected.length === 0)
|
|
986
|
+
throw new Error("select a skill package or pass --all");
|
|
987
|
+
for (const name of options.selected)
|
|
988
|
+
if (!available.has(name))
|
|
989
|
+
throw new Error(
|
|
990
|
+
"a selected skill package does not match the installed dependencies"
|
|
991
|
+
);
|
|
992
|
+
const wanted = new Set(options.selected);
|
|
993
|
+
const entries = /* @__PURE__ */ new Map();
|
|
994
|
+
const published = /* @__PURE__ */ new Map();
|
|
995
|
+
for (const match of matched) {
|
|
996
|
+
const entry2 = match.entry;
|
|
997
|
+
const name = entry2.delivery.packageName;
|
|
998
|
+
const declared = Object.hasOwn(
|
|
999
|
+
context.manifest.devDependencies ?? {},
|
|
1000
|
+
name
|
|
1001
|
+
);
|
|
1002
|
+
if (options.sync ? !declared : !options.all && !wanted.has(name)) continue;
|
|
1003
|
+
if (Object.hasOwn(context.manifest.dependencies ?? {}, name) || Object.hasOwn(context.manifest.optionalDependencies ?? {}, name)) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
"a skill package is already declared outside devDependencies"
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const identity = `${name}@${entry2.delivery.version}`;
|
|
1009
|
+
const hash = published.get(identity);
|
|
1010
|
+
if (hash && hash !== entry2.delivery.integrity)
|
|
1011
|
+
throw new Error("conflicting integrity for one package version");
|
|
1012
|
+
published.set(identity, entry2.delivery.integrity);
|
|
1013
|
+
const current = entries.get(name);
|
|
1014
|
+
if (!current || semver.gt(entry2.delivery.version, current.delivery.version))
|
|
1015
|
+
entries.set(name, entry2);
|
|
1016
|
+
}
|
|
1017
|
+
const changes = [...entries.values()].filter((entry2) => {
|
|
1018
|
+
const installed = options.inventory.dependencies.find(
|
|
1019
|
+
(dependency) => dependency.requestedAs === entry2.delivery.packageName
|
|
1020
|
+
);
|
|
1021
|
+
if (installed && installed.name !== entry2.delivery.packageName)
|
|
1022
|
+
throw new Error("skill package aliases require explicit migration");
|
|
1023
|
+
if (installed && semver.gt(installed.version, entry2.delivery.version))
|
|
1024
|
+
throw new Error("catalog would downgrade an installed skill package");
|
|
1025
|
+
return !installed || installed.version !== entry2.delivery.version;
|
|
1026
|
+
}).map((entry2) => entry2.delivery).sort((a, b) => a.packageName.localeCompare(b.packageName));
|
|
1027
|
+
const specs = changes.map((entry2) => `${entry2.packageName}@${entry2.version}`);
|
|
1028
|
+
let args;
|
|
1029
|
+
if (context.manager === "npm") {
|
|
1030
|
+
args = [
|
|
1031
|
+
"install",
|
|
1032
|
+
"--save-dev",
|
|
1033
|
+
"--save-exact",
|
|
1034
|
+
"--ignore-scripts",
|
|
1035
|
+
"--no-audit",
|
|
1036
|
+
"--no-fund"
|
|
1037
|
+
];
|
|
1038
|
+
if (context.project !== context.root)
|
|
1039
|
+
args.push(
|
|
1040
|
+
`--workspace=${relative(context.root, context.project).split(sep).join("/")}`
|
|
1041
|
+
);
|
|
1042
|
+
} else {
|
|
1043
|
+
args = ["add", "--dev", "--exact", "--ignore-scripts"];
|
|
1044
|
+
}
|
|
1045
|
+
args.push(...specs);
|
|
1046
|
+
return {
|
|
1047
|
+
catalogRevision: catalog.revision,
|
|
1048
|
+
context,
|
|
1049
|
+
changes,
|
|
1050
|
+
command: {
|
|
1051
|
+
executable: context.manager,
|
|
1052
|
+
args,
|
|
1053
|
+
cwd: context.manager === "npm" ? context.root : context.project
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/commands/remote-options.ts
|
|
1059
|
+
import { resolve as resolve3 } from "path";
|
|
1060
|
+
|
|
1061
|
+
// src/remote/catalog.ts
|
|
1062
|
+
import { readFileSync as readFileSync4, statSync } from "fs";
|
|
1063
|
+
import semver2 from "semver";
|
|
1064
|
+
import { z } from "zod";
|
|
1065
|
+
var npmName = z.string().max(214).regex(/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u);
|
|
1066
|
+
var version = z.string().refine(
|
|
1067
|
+
(value) => semver2.valid(value) !== null && semver2.valid(value) === value.split("+")[0]
|
|
1068
|
+
);
|
|
1069
|
+
var target = z.object({
|
|
1070
|
+
packageName: npmName,
|
|
1071
|
+
range: z.string().min(1).refine((value) => semver2.validRange(value) !== null),
|
|
1072
|
+
verifiedVersions: z.array(version).min(1)
|
|
1073
|
+
}).refine(
|
|
1074
|
+
(value) => value.verifiedVersions.every((item) => semver2.satisfies(item, value.range))
|
|
1075
|
+
);
|
|
1076
|
+
var deliveryName = npmName.refine(
|
|
1077
|
+
(value) => value.startsWith("@modootoday/pmcp-")
|
|
1078
|
+
);
|
|
1079
|
+
var entry = z.object({
|
|
1080
|
+
productId: z.string().min(1),
|
|
1081
|
+
title: z.string().min(1),
|
|
1082
|
+
summary: z.string().min(1),
|
|
1083
|
+
delivery: z.object({
|
|
1084
|
+
packageName: deliveryName,
|
|
1085
|
+
version,
|
|
1086
|
+
integrity: z.string().regex(/^sha512-[A-Za-z0-9+/]{86}==$/u)
|
|
1087
|
+
}),
|
|
1088
|
+
skillRevision: z.number().int().positive(),
|
|
1089
|
+
contentDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
1090
|
+
evidence: z.object({
|
|
1091
|
+
verifiedOn: z.string().regex(/^\d{4}-\d{2}-\d{2}$/u).optional(),
|
|
1092
|
+
examplesExecuted: z.number().int().positive()
|
|
1093
|
+
}),
|
|
1094
|
+
// Optional for the same reason the service makes it optional: a snapshot
|
|
1095
|
+
// published before previews existed cannot grow one, and a client that
|
|
1096
|
+
// demands it rejects the whole catalog rather than one older entry.
|
|
1097
|
+
preview: z.object({
|
|
1098
|
+
headings: z.array(z.string().min(1)),
|
|
1099
|
+
example: z.string().min(1)
|
|
1100
|
+
}).optional(),
|
|
1101
|
+
targets: z.array(target).min(1)
|
|
1102
|
+
});
|
|
1103
|
+
var responseSchema = z.object({
|
|
1104
|
+
schemaVersion: z.literal(1),
|
|
1105
|
+
requestId: z.string().min(1),
|
|
1106
|
+
catalog: z.object({
|
|
1107
|
+
revision: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u),
|
|
1108
|
+
publishedAt: z.string().datetime(),
|
|
1109
|
+
entries: z.array(entry)
|
|
1110
|
+
})
|
|
1111
|
+
});
|
|
1112
|
+
var MAX_CATALOG_BYTES = 5 * 1024 * 1024;
|
|
1113
|
+
var DEFAULT_API_ORIGIN = "https://api.pmcp.build";
|
|
1114
|
+
function parseCatalog(value) {
|
|
1115
|
+
const parsed = responseSchema.safeParse(value);
|
|
1116
|
+
if (!parsed.success)
|
|
1117
|
+
throw new Error("the service returned an invalid catalog");
|
|
1118
|
+
const identities = /* @__PURE__ */ new Set();
|
|
1119
|
+
for (const entry2 of parsed.data.catalog.entries) {
|
|
1120
|
+
const identity = JSON.stringify([entry2.productId, entry2.skillRevision]);
|
|
1121
|
+
if (identities.has(identity))
|
|
1122
|
+
throw new Error("the catalog contains duplicate revisions");
|
|
1123
|
+
identities.add(identity);
|
|
1124
|
+
}
|
|
1125
|
+
return parsed.data.catalog;
|
|
1126
|
+
}
|
|
1127
|
+
function catalogEndpoint(origin) {
|
|
1128
|
+
const url = new URL(origin);
|
|
1129
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
"the API origin must be an HTTPS origin without credentials or a path"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
return new URL("/v1/catalog", url).href;
|
|
1135
|
+
}
|
|
1136
|
+
function readCatalogFile(path) {
|
|
1137
|
+
const stat = statSync(path);
|
|
1138
|
+
if (!stat.isFile() || stat.size > MAX_CATALOG_BYTES)
|
|
1139
|
+
throw new Error("catalog file exceeds the supported size");
|
|
1140
|
+
const bytes = readFileSync4(path);
|
|
1141
|
+
if (bytes.byteLength > MAX_CATALOG_BYTES)
|
|
1142
|
+
throw new Error("catalog file exceeds the supported size");
|
|
1143
|
+
return parseCatalog(JSON.parse(bytes.toString("utf8")));
|
|
1144
|
+
}
|
|
1145
|
+
async function fetchCatalog(origin, fetcher2 = fetch) {
|
|
1146
|
+
const response = await fetcher2(catalogEndpoint(origin), {
|
|
1147
|
+
method: "GET",
|
|
1148
|
+
headers: { accept: "application/json" },
|
|
1149
|
+
redirect: "error",
|
|
1150
|
+
signal: AbortSignal.timeout(15e3)
|
|
1151
|
+
});
|
|
1152
|
+
if (!response.ok)
|
|
1153
|
+
throw new Error(`catalog request failed (${response.status})`);
|
|
1154
|
+
if (!response.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
1155
|
+
throw new Error("catalog response is not JSON");
|
|
1156
|
+
}
|
|
1157
|
+
if (!response.body) throw new Error("catalog response has no body");
|
|
1158
|
+
const reader = response.body.getReader();
|
|
1159
|
+
const chunks = [];
|
|
1160
|
+
let size = 0;
|
|
1161
|
+
try {
|
|
1162
|
+
for (; ; ) {
|
|
1163
|
+
const { done, value } = await reader.read();
|
|
1164
|
+
if (done) break;
|
|
1165
|
+
size += value.byteLength;
|
|
1166
|
+
if (size > MAX_CATALOG_BYTES)
|
|
1167
|
+
throw new Error("catalog response exceeds the supported size");
|
|
1168
|
+
chunks.push(value);
|
|
1169
|
+
}
|
|
1170
|
+
} finally {
|
|
1171
|
+
await reader.cancel().catch(() => void 0);
|
|
1172
|
+
reader.releaseLock();
|
|
1173
|
+
}
|
|
1174
|
+
const bytes = new Uint8Array(size);
|
|
1175
|
+
let offset = 0;
|
|
1176
|
+
for (const chunk of chunks) {
|
|
1177
|
+
bytes.set(chunk, offset);
|
|
1178
|
+
offset += chunk.byteLength;
|
|
1179
|
+
}
|
|
1180
|
+
return parseCatalog(JSON.parse(new TextDecoder().decode(bytes)));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/commands/remote-options.ts
|
|
1184
|
+
var REMOTE_OPTIONS = [
|
|
1185
|
+
{
|
|
1186
|
+
name: "project",
|
|
1187
|
+
describe: "Project directory containing package.json.",
|
|
1188
|
+
placeholder: "<dir>"
|
|
1189
|
+
},
|
|
1190
|
+
{ name: "api", describe: "Catalog API origin.", placeholder: "<origin>" },
|
|
1191
|
+
{
|
|
1192
|
+
name: "catalog",
|
|
1193
|
+
describe: "Use a saved catalog response without network access.",
|
|
1194
|
+
placeholder: "<file>"
|
|
1195
|
+
},
|
|
1196
|
+
{ name: "json", describe: "Print machine-readable results.", boolean: true }
|
|
1197
|
+
];
|
|
1198
|
+
function projectFrom(context) {
|
|
1199
|
+
return resolve3(context.cwd, one(context.args, "project") ?? ".");
|
|
1200
|
+
}
|
|
1201
|
+
function apiOrigin(context) {
|
|
1202
|
+
return one(context.args, "api") ?? context.env["PMCP_API_ORIGIN"] ?? DEFAULT_API_ORIGIN;
|
|
1203
|
+
}
|
|
1204
|
+
async function remoteCatalog(context) {
|
|
1205
|
+
const file = one(context.args, "catalog");
|
|
1206
|
+
if (file) {
|
|
1207
|
+
if (one(context.args, "api"))
|
|
1208
|
+
throw new Error("choose --catalog or --api, not both");
|
|
1209
|
+
return readCatalogFile(resolve3(context.cwd, file));
|
|
1210
|
+
}
|
|
1211
|
+
return fetchCatalog(apiOrigin(context));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/commands/available.ts
|
|
1215
|
+
var availableCommand = {
|
|
1216
|
+
name: "available",
|
|
1217
|
+
describe: "Find provided skills matching this project's installed dependencies",
|
|
1218
|
+
usage: "pmcp available [--project <dir>] [--catalog <file>] [--json]",
|
|
1219
|
+
options: REMOTE_OPTIONS,
|
|
1220
|
+
async run(context) {
|
|
1221
|
+
if (context.args.positional.length)
|
|
1222
|
+
throw new ArgumentError("available takes no package arguments");
|
|
1223
|
+
const inventory = readInstalledDependencies(projectFrom(context));
|
|
1224
|
+
const catalog = await remoteCatalog(context);
|
|
1225
|
+
const matches = matchingSkills(catalog, inventory);
|
|
1226
|
+
if (context.args.flags.has("json")) {
|
|
1227
|
+
context.ui.data(
|
|
1228
|
+
`${JSON.stringify({ catalogRevision: catalog.revision, matches, issues: inventory.issues }, null, 2)}
|
|
1229
|
+
`
|
|
1230
|
+
);
|
|
1231
|
+
} else {
|
|
1232
|
+
for (const match of matches) {
|
|
1233
|
+
const status = match.verified ? "verified" : "compatible, not verified at this exact version";
|
|
1234
|
+
context.ui.data(
|
|
1235
|
+
`${match.targetName}@${match.targetVersion} ${match.entry.delivery.packageName}@${match.entry.delivery.version} ${status}
|
|
1236
|
+
`
|
|
1237
|
+
);
|
|
1238
|
+
const { verifiedOn, examplesExecuted } = match.entry.evidence;
|
|
1239
|
+
context.ui.line(
|
|
1240
|
+
` ${examplesExecuted} examples executed${verifiedOn ? ` on ${verifiedOn}` : ""}`
|
|
1241
|
+
);
|
|
1242
|
+
const headings = match.entry.preview?.headings ?? [];
|
|
1243
|
+
if (headings.length > 0) {
|
|
1244
|
+
context.ui.line(` ${headings.join(" \xB7 ")}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
context.ui.info(`${matches.length} matching skills`, catalog.revision);
|
|
1248
|
+
if (matches.length > 0) {
|
|
1249
|
+
context.ui.info(
|
|
1250
|
+
"preview",
|
|
1251
|
+
"pmcp preview <package> shows a full example before you subscribe"
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
for (const issue of inventory.issues)
|
|
1255
|
+
context.ui.warn(issue.reason, issue.name);
|
|
1256
|
+
}
|
|
1257
|
+
return inventory.issues.length ? 1 : 0;
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
// src/commands/preview.ts
|
|
1262
|
+
var previewCommand = {
|
|
1263
|
+
name: "preview",
|
|
1264
|
+
describe: "Show what a skill contains before subscribing",
|
|
1265
|
+
usage: "pmcp preview <skill-package> [--json]",
|
|
1266
|
+
options: REMOTE_OPTIONS,
|
|
1267
|
+
async run(context) {
|
|
1268
|
+
const [requested, ...extra] = context.args.positional;
|
|
1269
|
+
if (!requested || extra.length) {
|
|
1270
|
+
throw new ArgumentError("name one skill package to preview");
|
|
1271
|
+
}
|
|
1272
|
+
const catalog = await remoteCatalog(context);
|
|
1273
|
+
const entry2 = catalog.entries.find(
|
|
1274
|
+
(candidate) => candidate.delivery.packageName === requested || candidate.productId === requested
|
|
1275
|
+
);
|
|
1276
|
+
if (!entry2) {
|
|
1277
|
+
context.ui.error("no such skill", requested);
|
|
1278
|
+
return 1;
|
|
1279
|
+
}
|
|
1280
|
+
if (context.args.flags.has("json")) {
|
|
1281
|
+
context.ui.data(
|
|
1282
|
+
`${JSON.stringify(
|
|
1283
|
+
{
|
|
1284
|
+
productId: entry2.productId,
|
|
1285
|
+
title: entry2.title,
|
|
1286
|
+
summary: entry2.summary,
|
|
1287
|
+
package: `${entry2.delivery.packageName}@${entry2.delivery.version}`,
|
|
1288
|
+
evidence: entry2.evidence,
|
|
1289
|
+
targets: entry2.targets,
|
|
1290
|
+
preview: entry2.preview
|
|
1291
|
+
},
|
|
1292
|
+
null,
|
|
1293
|
+
2
|
|
1294
|
+
)}
|
|
1295
|
+
`
|
|
1296
|
+
);
|
|
1297
|
+
return 0;
|
|
1298
|
+
}
|
|
1299
|
+
context.ui.data(`${entry2.title}
|
|
1300
|
+
|
|
1301
|
+
${entry2.summary}
|
|
1302
|
+
|
|
1303
|
+
`);
|
|
1304
|
+
context.ui.data(
|
|
1305
|
+
`${entry2.evidence.examplesExecuted} examples executed${entry2.evidence.verifiedOn ? ` on ${entry2.evidence.verifiedOn}` : ""}, against ${entry2.targets.map(
|
|
1306
|
+
(target2) => `${target2.packageName}@${target2.verifiedVersions.join(", ")}`
|
|
1307
|
+
).join("; ")}
|
|
1308
|
+
|
|
1309
|
+
`
|
|
1310
|
+
);
|
|
1311
|
+
if (!entry2.preview) {
|
|
1312
|
+
context.ui.data(
|
|
1313
|
+
"This entry was published before previews were recorded, so there is no sample to show.\n"
|
|
1314
|
+
);
|
|
1315
|
+
return 0;
|
|
1316
|
+
}
|
|
1317
|
+
if (entry2.preview.headings.length > 0) {
|
|
1318
|
+
context.ui.data("Contents\n");
|
|
1319
|
+
for (const heading of entry2.preview.headings) {
|
|
1320
|
+
context.ui.data(` ${heading}
|
|
1321
|
+
`);
|
|
1322
|
+
}
|
|
1323
|
+
context.ui.data("\n");
|
|
1324
|
+
}
|
|
1325
|
+
context.ui.data(
|
|
1326
|
+
`One example from this skill
|
|
1327
|
+
|
|
1328
|
+
${entry2.preview.example}
|
|
1329
|
+
`
|
|
1330
|
+
);
|
|
1331
|
+
return 0;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
// src/commands/install.ts
|
|
1336
|
+
import { createInterface } from "readline/promises";
|
|
1337
|
+
|
|
1338
|
+
// src/install/apply.ts
|
|
1339
|
+
import { spawnSync } from "child_process";
|
|
1340
|
+
function environment(userConfig, base) {
|
|
1341
|
+
return userConfig ? {
|
|
1342
|
+
...base,
|
|
1343
|
+
NPM_CONFIG_USERCONFIG: userConfig,
|
|
1344
|
+
npm_config_userconfig: userConfig
|
|
1345
|
+
} : base;
|
|
1346
|
+
}
|
|
1347
|
+
function createPackageManagerRunner(userConfig, baseEnv = process.env, verifiedIntegrity) {
|
|
1348
|
+
return {
|
|
1349
|
+
integrity(packageSpec, plan) {
|
|
1350
|
+
const alreadyVerified = verifiedIntegrity?.get(packageSpec);
|
|
1351
|
+
if (alreadyVerified) return alreadyVerified;
|
|
1352
|
+
const { manager, project } = plan.context;
|
|
1353
|
+
const args = manager === "npm" ? ["view", packageSpec, "dist.integrity", "--json"] : ["info", packageSpec, "dist.integrity", "--json"];
|
|
1354
|
+
const result = spawnSync(manager, args, {
|
|
1355
|
+
cwd: project,
|
|
1356
|
+
env: environment(userConfig, baseEnv),
|
|
1357
|
+
encoding: "utf8",
|
|
1358
|
+
shell: false,
|
|
1359
|
+
timeout: 3e4,
|
|
1360
|
+
maxBuffer: 1024 * 1024
|
|
1361
|
+
});
|
|
1362
|
+
if (result.error || result.status !== 0) {
|
|
1363
|
+
const detail = `${result.error?.message ?? ""} ${result.stderr ?? ""}`.replace(/pmcp_[A-Za-z0-9_-]{43}/gu, "[redacted]").trim().slice(0, 300);
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
`package manager could not verify registry integrity${detail ? `: ${detail}` : ""}`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
const value = JSON.parse(result.stdout.trim());
|
|
1369
|
+
if (typeof value !== "string")
|
|
1370
|
+
throw new Error("registry returned no package integrity");
|
|
1371
|
+
return value;
|
|
1372
|
+
},
|
|
1373
|
+
install(plan) {
|
|
1374
|
+
const args = [...plan.command.args];
|
|
1375
|
+
const result = spawnSync(plan.command.executable, args, {
|
|
1376
|
+
cwd: plan.command.cwd,
|
|
1377
|
+
env: environment(userConfig, baseEnv),
|
|
1378
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1379
|
+
encoding: "utf8",
|
|
1380
|
+
shell: false,
|
|
1381
|
+
maxBuffer: 8 * 1024 * 1024
|
|
1382
|
+
});
|
|
1383
|
+
if (result.error)
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
"package manager execution failed; inspect the project before retrying"
|
|
1386
|
+
);
|
|
1387
|
+
if (result.stdout) process.stderr.write(result.stdout);
|
|
1388
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
1389
|
+
return result.status ?? 1;
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
var packageManagerRunner = createPackageManagerRunner();
|
|
1394
|
+
function applyInstallPlan(plan, runner = packageManagerRunner) {
|
|
1395
|
+
if (!plan.changes.length) return 0;
|
|
1396
|
+
if (fingerprint(plan.context.files) !== plan.context.fingerprint)
|
|
1397
|
+
throw new Error("project changed after planning; create a new plan");
|
|
1398
|
+
for (const entry2 of plan.changes) {
|
|
1399
|
+
const integrity = runner.integrity(
|
|
1400
|
+
`${entry2.packageName}@${entry2.version}`,
|
|
1401
|
+
plan
|
|
1402
|
+
);
|
|
1403
|
+
if (integrity !== entry2.integrity)
|
|
1404
|
+
throw new Error(
|
|
1405
|
+
"registry integrity differs from the catalog; nothing was installed"
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
if (fingerprint(plan.context.files) !== plan.context.fingerprint)
|
|
1409
|
+
throw new Error("project changed during registry verification");
|
|
1410
|
+
const code = runner.install(plan);
|
|
1411
|
+
if (code !== 0) return code;
|
|
1412
|
+
const after = readInstalledDependencies(plan.context.project);
|
|
1413
|
+
for (const entry2 of plan.changes) {
|
|
1414
|
+
const found = after.dependencies.find(
|
|
1415
|
+
(dependency) => dependency.requestedAs === entry2.packageName
|
|
1416
|
+
);
|
|
1417
|
+
if (!found || found.name !== entry2.packageName || found.version !== entry2.version) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
"package manager exited successfully but the planned version is not installed"
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return 0;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/install/verify-content.ts
|
|
1427
|
+
import { createHash as createHash2 } from "crypto";
|
|
1428
|
+
import { readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
1429
|
+
import { join as join4, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
|
|
1430
|
+
var SKILL = "SKILL.md";
|
|
1431
|
+
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".cache"]);
|
|
1432
|
+
function skillFiles(root) {
|
|
1433
|
+
const found = /* @__PURE__ */ new Map();
|
|
1434
|
+
const walk = (dir) => {
|
|
1435
|
+
let entries;
|
|
1436
|
+
try {
|
|
1437
|
+
entries = readdirSync(dir);
|
|
1438
|
+
} catch {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
for (const name of entries) {
|
|
1442
|
+
if (SKIP.has(name)) continue;
|
|
1443
|
+
const path = join4(dir, name);
|
|
1444
|
+
let stats;
|
|
1445
|
+
try {
|
|
1446
|
+
stats = statSync2(path);
|
|
1447
|
+
} catch {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (stats.isDirectory()) walk(path);
|
|
1451
|
+
else if (name === SKILL)
|
|
1452
|
+
found.set(
|
|
1453
|
+
relative2(root, path).split(sep2).join("/"),
|
|
1454
|
+
readFileSync5(path, "utf8")
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
walk(root);
|
|
1459
|
+
return found;
|
|
1460
|
+
}
|
|
1461
|
+
function contentDigest(files) {
|
|
1462
|
+
const hash = createHash2("sha256");
|
|
1463
|
+
for (const path of [...files.keys()].sort()) {
|
|
1464
|
+
const body = files.get(path);
|
|
1465
|
+
hash.update(`${path.length}:${path}:${body.length}:${body}`);
|
|
1466
|
+
}
|
|
1467
|
+
return `sha256:${hash.digest("hex")}`;
|
|
1468
|
+
}
|
|
1469
|
+
function verifyInstalledContent(project, packageName2, expected) {
|
|
1470
|
+
const root = resolve4(project, "node_modules", ...packageName2.split("/"));
|
|
1471
|
+
const files = skillFiles(root);
|
|
1472
|
+
const actual = contentDigest(files);
|
|
1473
|
+
return {
|
|
1474
|
+
packageName: packageName2,
|
|
1475
|
+
expected,
|
|
1476
|
+
actual,
|
|
1477
|
+
matched: files.size > 0 && actual === expected,
|
|
1478
|
+
skillCount: files.size
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/remote/distribution.ts
|
|
1483
|
+
import { chmodSync as chmodSync2, mkdtempSync, rmSync as rmSync2, writeFileSync } from "fs";
|
|
1484
|
+
import { tmpdir } from "os";
|
|
1485
|
+
import { join as join5 } from "path";
|
|
1486
|
+
function api(origin, path) {
|
|
1487
|
+
const base = new URL(catalogEndpoint(origin));
|
|
1488
|
+
base.pathname = path;
|
|
1489
|
+
return base.href;
|
|
1490
|
+
}
|
|
1491
|
+
function credential(value) {
|
|
1492
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1493
|
+
throw new Error("invalid distribution credential response");
|
|
1494
|
+
const row = value;
|
|
1495
|
+
if (!row["credential"] || typeof row["credential"] !== "object" || Array.isArray(row["credential"])) {
|
|
1496
|
+
throw new Error("invalid distribution credential response");
|
|
1497
|
+
}
|
|
1498
|
+
const item = row["credential"];
|
|
1499
|
+
for (const key of ["id", "token", "registry", "scope", "expiresAt"]) {
|
|
1500
|
+
if (typeof item[key] !== "string" || item[key] === "")
|
|
1501
|
+
throw new Error("invalid distribution credential response");
|
|
1502
|
+
}
|
|
1503
|
+
const registry = new URL(item["registry"]);
|
|
1504
|
+
if (registry.href !== "https://api.pmcp.build/npm/" || item["scope"] !== "@modootoday" || !/^pmcp_[A-Za-z0-9_-]{43}$/u.test(item["token"]) || !Number.isFinite(Date.parse(item["expiresAt"]))) {
|
|
1505
|
+
throw new Error("invalid distribution credential response");
|
|
1506
|
+
}
|
|
1507
|
+
return item;
|
|
1508
|
+
}
|
|
1509
|
+
var REFUSALS = {
|
|
1510
|
+
401: "sign in again before installing skills",
|
|
1511
|
+
402: "your subscription does not include skill downloads",
|
|
1512
|
+
403: "sign in again to approve skill downloads"
|
|
1513
|
+
};
|
|
1514
|
+
async function refusal(response, fallback) {
|
|
1515
|
+
let requestId;
|
|
1516
|
+
try {
|
|
1517
|
+
const body = await response.json();
|
|
1518
|
+
const value = body && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
1519
|
+
if (typeof value["requestId"] === "string" && value["requestId"] !== "") {
|
|
1520
|
+
requestId = value["requestId"];
|
|
1521
|
+
}
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
const said = REFUSALS[response.status] ?? `${fallback} (${response.status})`;
|
|
1525
|
+
return new Error(requestId ? `${said} [request ${requestId}]` : said);
|
|
1526
|
+
}
|
|
1527
|
+
async function issueDistributionCredential(accessToken, origin = DEFAULT_API_ORIGIN, fetcher2 = fetch) {
|
|
1528
|
+
const response = await fetcher2(api(origin, "/v1/distribution/credentials"), {
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
redirect: "error",
|
|
1531
|
+
signal: AbortSignal.timeout(15e3),
|
|
1532
|
+
headers: {
|
|
1533
|
+
authorization: `Bearer ${accessToken}`,
|
|
1534
|
+
accept: "application/json"
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
if (!response.ok)
|
|
1538
|
+
throw await refusal(response, "distribution credential request failed");
|
|
1539
|
+
return credential(await response.json());
|
|
1540
|
+
}
|
|
1541
|
+
async function verifyRegistryIntegrity(entries, value, fetcher2 = fetch) {
|
|
1542
|
+
const verified = /* @__PURE__ */ new Map();
|
|
1543
|
+
for (const entry2 of entries) {
|
|
1544
|
+
if (!entry2.packageName.startsWith(`${value.scope}/pmcp-`))
|
|
1545
|
+
throw new Error(
|
|
1546
|
+
"catalog delivery name is not a skill package for this credential"
|
|
1547
|
+
);
|
|
1548
|
+
const endpoint = new URL(
|
|
1549
|
+
encodeURIComponent(entry2.packageName),
|
|
1550
|
+
value.registry
|
|
1551
|
+
);
|
|
1552
|
+
const response = await fetcher2(endpoint, {
|
|
1553
|
+
method: "GET",
|
|
1554
|
+
redirect: "error",
|
|
1555
|
+
signal: AbortSignal.timeout(15e3),
|
|
1556
|
+
headers: {
|
|
1557
|
+
authorization: `Bearer ${value.token}`,
|
|
1558
|
+
accept: "application/json"
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
if (!response.ok)
|
|
1562
|
+
throw await refusal(response, "registry metadata request failed");
|
|
1563
|
+
const raw = await response.text();
|
|
1564
|
+
if (raw.length > 2 * 1024 * 1024)
|
|
1565
|
+
throw new Error("registry metadata exceeds the supported size");
|
|
1566
|
+
const body = JSON.parse(raw);
|
|
1567
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
1568
|
+
throw new Error("registry returned invalid metadata");
|
|
1569
|
+
const versions = body["versions"];
|
|
1570
|
+
if (!versions || typeof versions !== "object" || Array.isArray(versions))
|
|
1571
|
+
throw new Error("registry metadata contains no versions");
|
|
1572
|
+
const release = versions[entry2.version];
|
|
1573
|
+
if (!release || typeof release !== "object" || Array.isArray(release))
|
|
1574
|
+
throw new Error("registry does not contain the catalog version");
|
|
1575
|
+
const dist = release["dist"];
|
|
1576
|
+
if (!dist || typeof dist !== "object" || Array.isArray(dist) || dist["integrity"] !== entry2.integrity) {
|
|
1577
|
+
throw new Error("registry integrity differs from the catalog");
|
|
1578
|
+
}
|
|
1579
|
+
verified.set(`${entry2.packageName}@${entry2.version}`, entry2.integrity);
|
|
1580
|
+
}
|
|
1581
|
+
return verified;
|
|
1582
|
+
}
|
|
1583
|
+
async function revokeDistributionCredential(accessToken, id, origin = DEFAULT_API_ORIGIN, fetcher2 = fetch) {
|
|
1584
|
+
try {
|
|
1585
|
+
const response = await fetcher2(api(origin, "/v1/distribution/revoke"), {
|
|
1586
|
+
method: "POST",
|
|
1587
|
+
redirect: "error",
|
|
1588
|
+
signal: AbortSignal.timeout(15e3),
|
|
1589
|
+
headers: {
|
|
1590
|
+
authorization: `Bearer ${accessToken}`,
|
|
1591
|
+
"content-type": "application/json"
|
|
1592
|
+
},
|
|
1593
|
+
body: JSON.stringify({ id })
|
|
1594
|
+
});
|
|
1595
|
+
return response.ok || response.status === 404;
|
|
1596
|
+
} catch {
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function temporaryRegistryConfig(value) {
|
|
1601
|
+
const directory = mkdtempSync(join5(tmpdir(), "pmcp-registry-"));
|
|
1602
|
+
chmodSync2(directory, 448);
|
|
1603
|
+
const path = join5(directory, ".npmrc");
|
|
1604
|
+
const registry = new URL(value.registry);
|
|
1605
|
+
const auth = `//${registry.host}${registry.pathname}:_authToken=${value.token}`;
|
|
1606
|
+
writeFileSync(path, `${value.scope}:registry=${registry.href}
|
|
1607
|
+
${auth}
|
|
1608
|
+
`, {
|
|
1609
|
+
mode: 384
|
|
1610
|
+
});
|
|
1611
|
+
chmodSync2(path, 384);
|
|
1612
|
+
return {
|
|
1613
|
+
path,
|
|
1614
|
+
environment: { HOME: directory },
|
|
1615
|
+
close() {
|
|
1616
|
+
rmSync2(directory, { recursive: true, force: true });
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/commands/install.ts
|
|
1622
|
+
async function manage(context, sync) {
|
|
1623
|
+
const dryRun = context.args.flags.has("dry-run");
|
|
1624
|
+
const yes = context.args.flags.has("yes");
|
|
1625
|
+
if (dryRun && yes)
|
|
1626
|
+
throw new ArgumentError("choose --dry-run or --yes, not both");
|
|
1627
|
+
if (sync && context.args.positional.length)
|
|
1628
|
+
throw new ArgumentError("sync takes no package arguments");
|
|
1629
|
+
if (!sync && !context.args.positional.length && !context.args.flags.has("all")) {
|
|
1630
|
+
throw new ArgumentError("select a skill package or pass --all");
|
|
1631
|
+
}
|
|
1632
|
+
const project = projectFrom(context);
|
|
1633
|
+
const install = installContext(project);
|
|
1634
|
+
const inventory = readInstalledDependencies(project);
|
|
1635
|
+
const catalog = await remoteCatalog(context);
|
|
1636
|
+
const plan = planInstall({
|
|
1637
|
+
context: install,
|
|
1638
|
+
inventory,
|
|
1639
|
+
catalog,
|
|
1640
|
+
selected: context.args.positional,
|
|
1641
|
+
all: context.args.flags.has("all"),
|
|
1642
|
+
sync
|
|
1643
|
+
});
|
|
1644
|
+
const digests = new Map(
|
|
1645
|
+
catalog.entries.map((entry2) => [
|
|
1646
|
+
entry2.delivery.packageName,
|
|
1647
|
+
entry2.contentDigest
|
|
1648
|
+
])
|
|
1649
|
+
);
|
|
1650
|
+
const mismatched = (packages) => packages.map(
|
|
1651
|
+
(name) => verifyInstalledContent(project, name, digests.get(name) ?? "")
|
|
1652
|
+
).filter((check) => !check.matched);
|
|
1653
|
+
const reportMismatch = (checks) => {
|
|
1654
|
+
for (const check of checks) {
|
|
1655
|
+
context.ui.error(
|
|
1656
|
+
"installed content does not match the catalog",
|
|
1657
|
+
`${check.packageName}: expected ${check.expected}, found ${check.actual} across ${check.skillCount} skill files`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
const report = {
|
|
1662
|
+
catalogRevision: plan.catalogRevision,
|
|
1663
|
+
project,
|
|
1664
|
+
changes: plan.changes,
|
|
1665
|
+
command: plan.command
|
|
1666
|
+
};
|
|
1667
|
+
if (!context.args.flags.has("json")) {
|
|
1668
|
+
context.ui.info("project", project);
|
|
1669
|
+
for (const entry2 of plan.changes)
|
|
1670
|
+
context.ui.line(` ${entry2.packageName}@${entry2.version}`);
|
|
1671
|
+
if (plan.changes.length)
|
|
1672
|
+
context.ui.line(
|
|
1673
|
+
` ${plan.command.executable} ${plan.command.args.join(" ")}`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
if (!dryRun && plan.changes.length === 0) {
|
|
1677
|
+
const drifted = mismatched(
|
|
1678
|
+
inventory.dependencies.filter((dependency) => digests.has(dependency.name)).map((dependency) => dependency.name)
|
|
1679
|
+
);
|
|
1680
|
+
if (drifted.length > 0) {
|
|
1681
|
+
reportMismatch(drifted);
|
|
1682
|
+
return 1;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (dryRun || plan.changes.length === 0) {
|
|
1686
|
+
const status = dryRun ? "planned" : "unchanged";
|
|
1687
|
+
if (context.args.flags.has("json"))
|
|
1688
|
+
context.ui.data(`${JSON.stringify({ status, ...report }, null, 2)}
|
|
1689
|
+
`);
|
|
1690
|
+
else
|
|
1691
|
+
context.ui.info(
|
|
1692
|
+
status,
|
|
1693
|
+
`${plan.changes.length} changes; nothing written`
|
|
1694
|
+
);
|
|
1695
|
+
return 0;
|
|
1696
|
+
}
|
|
1697
|
+
if (!yes) {
|
|
1698
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY || context.args.flags.has("json")) {
|
|
1699
|
+
throw new ArgumentError(
|
|
1700
|
+
"use --yes to apply non-interactively or --dry-run to inspect the plan"
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const prompt = createInterface({
|
|
1704
|
+
input: process.stdin,
|
|
1705
|
+
output: process.stderr
|
|
1706
|
+
});
|
|
1707
|
+
try {
|
|
1708
|
+
const answer = await prompt.question(
|
|
1709
|
+
"Update this project's dependencies and lockfile? [y/N] "
|
|
1710
|
+
);
|
|
1711
|
+
if (!/^y(?:es)?$/iu.test(answer.trim())) {
|
|
1712
|
+
context.ui.info("cancelled", "nothing written");
|
|
1713
|
+
return 1;
|
|
1714
|
+
}
|
|
1715
|
+
} finally {
|
|
1716
|
+
prompt.close();
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
const session = readSession();
|
|
1720
|
+
if (!session || session.expiresAt !== void 0 && session.expiresAt <= Date.now()) {
|
|
1721
|
+
throw new Error("run pmcp login before installing paid skill packages");
|
|
1722
|
+
}
|
|
1723
|
+
const origin = apiOrigin(context);
|
|
1724
|
+
const credential2 = await issueDistributionCredential(
|
|
1725
|
+
session.accessToken,
|
|
1726
|
+
origin
|
|
1727
|
+
);
|
|
1728
|
+
const registry = temporaryRegistryConfig(credential2);
|
|
1729
|
+
let revoked = false;
|
|
1730
|
+
try {
|
|
1731
|
+
const verified = await verifyRegistryIntegrity(plan.changes, credential2);
|
|
1732
|
+
const code = applyInstallPlan(
|
|
1733
|
+
plan,
|
|
1734
|
+
createPackageManagerRunner(
|
|
1735
|
+
registry.path,
|
|
1736
|
+
{ ...process.env, ...registry.environment },
|
|
1737
|
+
verified
|
|
1738
|
+
)
|
|
1739
|
+
);
|
|
1740
|
+
if (code !== 0) {
|
|
1741
|
+
context.ui.error(
|
|
1742
|
+
"package manager failed",
|
|
1743
|
+
`exit ${code}; inspect the project before retrying`
|
|
1744
|
+
);
|
|
1745
|
+
return code;
|
|
1746
|
+
}
|
|
1747
|
+
const wrong = mismatched(plan.changes.map((change) => change.packageName));
|
|
1748
|
+
if (wrong.length > 0) {
|
|
1749
|
+
reportMismatch(wrong);
|
|
1750
|
+
return 1;
|
|
1751
|
+
}
|
|
1752
|
+
if (context.args.flags.has("json"))
|
|
1753
|
+
context.ui.data(
|
|
1754
|
+
`${JSON.stringify({ status: "applied", ...report }, null, 2)}
|
|
1755
|
+
`
|
|
1756
|
+
);
|
|
1757
|
+
else context.ui.success(`${plan.changes.length} skill packages installed`);
|
|
1758
|
+
return 0;
|
|
1759
|
+
} finally {
|
|
1760
|
+
registry.close();
|
|
1761
|
+
revoked = await revokeDistributionCredential(
|
|
1762
|
+
session.accessToken,
|
|
1763
|
+
credential2.id,
|
|
1764
|
+
origin
|
|
1765
|
+
);
|
|
1766
|
+
if (!revoked)
|
|
1767
|
+
context.ui.warn(
|
|
1768
|
+
"temporary registry credential expires automatically",
|
|
1769
|
+
credential2.expiresAt
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
var OPTIONS = [
|
|
1774
|
+
...REMOTE_OPTIONS,
|
|
1775
|
+
{
|
|
1776
|
+
name: "dry-run",
|
|
1777
|
+
describe: "Show the plan without installing or changing files.",
|
|
1778
|
+
boolean: true
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
name: "yes",
|
|
1782
|
+
describe: "Approve the displayed project and lockfile changes.",
|
|
1783
|
+
boolean: true
|
|
1784
|
+
}
|
|
1785
|
+
];
|
|
1786
|
+
var installCommand = {
|
|
1787
|
+
name: "install",
|
|
1788
|
+
describe: "Install matching skill npm packages with the project's package manager",
|
|
1789
|
+
usage: "pmcp install <skill-package>... | --all [--dry-run | --yes]",
|
|
1790
|
+
options: [
|
|
1791
|
+
...OPTIONS,
|
|
1792
|
+
{
|
|
1793
|
+
name: "all",
|
|
1794
|
+
describe: "Select all matching skill packages.",
|
|
1795
|
+
boolean: true
|
|
1796
|
+
}
|
|
1797
|
+
],
|
|
1798
|
+
run: (context) => manage(context, false)
|
|
1799
|
+
};
|
|
1800
|
+
var syncCommand = {
|
|
1801
|
+
name: "sync",
|
|
1802
|
+
describe: "Update declared skill packages without removing other dependencies",
|
|
1803
|
+
usage: "pmcp sync [--project <dir>] [--dry-run | --yes]",
|
|
1804
|
+
options: OPTIONS,
|
|
1805
|
+
run: (context) => manage(context, true)
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
// src/commands/index.ts
|
|
1809
|
+
var COMMANDS = [
|
|
1810
|
+
serveCommand,
|
|
1811
|
+
listCommand,
|
|
1812
|
+
indexCommand,
|
|
1813
|
+
loginCommand,
|
|
1814
|
+
logoutCommand,
|
|
1815
|
+
whoamiCommand,
|
|
1816
|
+
availableCommand,
|
|
1817
|
+
previewCommand,
|
|
1818
|
+
installCommand,
|
|
1819
|
+
syncCommand
|
|
1820
|
+
];
|
|
1821
|
+
var PLANNED = [];
|
|
1822
|
+
var GLOBAL = /* @__PURE__ */ new Set(["-q", "--quiet", "--no-color", "--color"]);
|
|
1823
|
+
function dispatch(argv, options = {}) {
|
|
1824
|
+
const global = argv.filter((arg) => GLOBAL.has(arg));
|
|
1825
|
+
const rest0 = argv.filter((arg) => !GLOBAL.has(arg));
|
|
1826
|
+
const env = options.env ?? process.env;
|
|
1827
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1828
|
+
const ui = options.ui ?? new Ui({
|
|
1829
|
+
env,
|
|
1830
|
+
quiet: global.includes("-q") || global.includes("--quiet"),
|
|
1831
|
+
...global.includes("--no-color") ? { color: false } : global.includes("--color") ? { color: true } : {}
|
|
1832
|
+
});
|
|
1833
|
+
const [verb, ...rest] = rest0;
|
|
1834
|
+
if (verb === void 0) {
|
|
1835
|
+
return run(serveCommand, [], { ui, env, cwd });
|
|
1836
|
+
}
|
|
1837
|
+
if (verb === "--help" || verb === "-h" || verb === "help") {
|
|
1838
|
+
const named = COMMANDS.find((command2) => command2.name === rest[0]);
|
|
1839
|
+
if (named) printCommandHelp(ui, named);
|
|
1840
|
+
else printHelp(ui, COMMANDS, PLANNED);
|
|
1841
|
+
return 0;
|
|
1842
|
+
}
|
|
1843
|
+
if (verb === "--version") {
|
|
1844
|
+
ui.data(`${VERSION}
|
|
1845
|
+
`);
|
|
1846
|
+
return 0;
|
|
1847
|
+
}
|
|
1848
|
+
if (verb.startsWith("-")) {
|
|
1849
|
+
return run(serveCommand, rest0, { ui, env, cwd });
|
|
1850
|
+
}
|
|
1851
|
+
const command = COMMANDS.find((candidate) => candidate.name === verb);
|
|
1852
|
+
if (command === void 0) {
|
|
1853
|
+
if (PLANNED.includes(verb))
|
|
1854
|
+
ui.error(`"${verb}" is designed but not implemented yet`);
|
|
1855
|
+
else ui.error(`unknown command "${verb}"`);
|
|
1856
|
+
printHelp(ui, COMMANDS, PLANNED);
|
|
1857
|
+
return 2;
|
|
1858
|
+
}
|
|
1859
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1860
|
+
printCommandHelp(ui, command);
|
|
1861
|
+
return 0;
|
|
1862
|
+
}
|
|
1863
|
+
return run(command, rest, { ui, env, cwd });
|
|
1864
|
+
}
|
|
1865
|
+
var VERSION = "0.1.0";
|
|
1866
|
+
async function run(command, argv, context) {
|
|
1867
|
+
try {
|
|
1868
|
+
return await command.run({
|
|
1869
|
+
...context,
|
|
1870
|
+
args: parseArgs(argv, command.options)
|
|
1871
|
+
});
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
if (error instanceof ArgumentError) {
|
|
1874
|
+
context.ui.error(error.message);
|
|
1875
|
+
printCommandHelp(context.ui, command);
|
|
1876
|
+
return 2;
|
|
1877
|
+
}
|
|
1878
|
+
context.ui.error(error instanceof Error ? error.message : "command failed");
|
|
1879
|
+
return 1;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/cli.ts
|
|
1884
|
+
process.exitCode = await dispatch(process.argv.slice(2));
|
|
1885
|
+
//# sourceMappingURL=cli.js.map
|