@pify/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/LICENSE +21 -0
- package/README.md +117 -0
- package/catalog.json +85 -0
- package/dist/cli.js +1419 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.js +628 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/version.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
var require2 = createRequire(import.meta.url);
|
|
6
|
+
var VERSION = require2("../package.json").version;
|
|
7
|
+
|
|
8
|
+
// src/catalog.ts
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { dirname, join as join2 } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
|
|
14
|
+
// src/exec.ts
|
|
15
|
+
import { spawn, spawnSync } from "child_process";
|
|
16
|
+
import { accessSync, constants, statSync } from "fs";
|
|
17
|
+
import { delimiter, join } from "path";
|
|
18
|
+
|
|
19
|
+
// src/errors.ts
|
|
20
|
+
var ExitCode = {
|
|
21
|
+
OK: 0,
|
|
22
|
+
/** Generic / unexpected failure. */
|
|
23
|
+
FAILURE: 1,
|
|
24
|
+
/** Bad arguments, unknown command, malformed package name. */
|
|
25
|
+
USAGE: 2,
|
|
26
|
+
/** A required external program (pi, npm, node) is missing or too old. */
|
|
27
|
+
ENVIRONMENT: 3,
|
|
28
|
+
/** The requested package is not in the catalog or not yet published. */
|
|
29
|
+
NOT_FOUND: 4,
|
|
30
|
+
/** A delegated `pi` or `npm` subprocess exited non-zero. */
|
|
31
|
+
SUBPROCESS: 5
|
|
32
|
+
};
|
|
33
|
+
var PifyError = class extends Error {
|
|
34
|
+
code;
|
|
35
|
+
/** Optional follow-up shown after the message, e.g. a command to run. */
|
|
36
|
+
hint;
|
|
37
|
+
constructor(message, code = ExitCode.FAILURE, hint) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "PifyError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.hint = hint;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function usageError(message, hint) {
|
|
45
|
+
return new PifyError(message, ExitCode.USAGE, hint);
|
|
46
|
+
}
|
|
47
|
+
function environmentError(message, hint) {
|
|
48
|
+
return new PifyError(message, ExitCode.ENVIRONMENT, hint);
|
|
49
|
+
}
|
|
50
|
+
function notFoundError(message, hint) {
|
|
51
|
+
return new PifyError(message, ExitCode.NOT_FOUND, hint);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/exec.ts
|
|
55
|
+
var isWindows = process.platform === "win32";
|
|
56
|
+
function isOffline() {
|
|
57
|
+
for (const name of ["PIFY_OFFLINE", "PI_OFFLINE"]) {
|
|
58
|
+
const v = process.env[name];
|
|
59
|
+
if (v && /^(1|true|yes)$/i.test(v)) return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
var SAFE_ARG = /^[A-Za-z0-9@._:/\\+=-]+$/;
|
|
64
|
+
function assertSafeArg(arg, what = "argument") {
|
|
65
|
+
if (!SAFE_ARG.test(arg)) {
|
|
66
|
+
throw new PifyError(
|
|
67
|
+
`Refusing to pass unsafe ${what} to a subprocess: ${JSON.stringify(arg)}`,
|
|
68
|
+
ExitCode.USAGE,
|
|
69
|
+
"Package names may contain letters, digits and @ . _ : / \\ + = - only."
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function spawnPlan(command, args) {
|
|
74
|
+
assertSafeArg(command, "command");
|
|
75
|
+
args.forEach((a) => assertSafeArg(a));
|
|
76
|
+
return isWindows ? [`${command} ${args.join(" ")}`.trim(), [], { shell: true }] : [command, args, { shell: false }];
|
|
77
|
+
}
|
|
78
|
+
function runCapture(command, args) {
|
|
79
|
+
const [cmd, argv, opts] = spawnPlan(command, args);
|
|
80
|
+
const res = spawnSync(cmd, argv, {
|
|
81
|
+
...opts,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
windowsHide: true
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
status: res.status ?? (res.error ? 127 : 1),
|
|
87
|
+
stdout: res.stdout ?? "",
|
|
88
|
+
stderr: res.stderr ?? ""
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function runInherit(command, args) {
|
|
92
|
+
const [cmd, argv, opts] = spawnPlan(command, args);
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const child = spawn(cmd, argv, {
|
|
95
|
+
...opts,
|
|
96
|
+
stdio: "inherit",
|
|
97
|
+
windowsHide: true
|
|
98
|
+
});
|
|
99
|
+
child.on("error", reject);
|
|
100
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function which(command) {
|
|
104
|
+
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
105
|
+
if (isWindows) {
|
|
106
|
+
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
107
|
+
for (const dir of dirs) {
|
|
108
|
+
const candidates = command.includes(".") ? [join(dir, command)] : exts.map((ext) => join(dir, command + ext));
|
|
109
|
+
for (const candidate of candidates) {
|
|
110
|
+
try {
|
|
111
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
for (const dir of dirs) {
|
|
119
|
+
const candidate = join(dir, command);
|
|
120
|
+
try {
|
|
121
|
+
if (!statSync(candidate).isFile()) continue;
|
|
122
|
+
accessSync(candidate, constants.X_OK);
|
|
123
|
+
return candidate;
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/catalog.ts
|
|
131
|
+
function bundledCatalogPath() {
|
|
132
|
+
return join2(dirname(fileURLToPath(import.meta.url)), "..", "catalog.json");
|
|
133
|
+
}
|
|
134
|
+
function cachePath() {
|
|
135
|
+
return join2(homedir(), ".pify", "catalog.json");
|
|
136
|
+
}
|
|
137
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
138
|
+
function loadBundledCatalog() {
|
|
139
|
+
return JSON.parse(readFileSync(bundledCatalogPath(), "utf8"));
|
|
140
|
+
}
|
|
141
|
+
function validateCatalog(data) {
|
|
142
|
+
if (typeof data !== "object" || data === null) return false;
|
|
143
|
+
const c = data;
|
|
144
|
+
if (!Number.isInteger(c.version)) return false;
|
|
145
|
+
if (!Array.isArray(c.packages)) return false;
|
|
146
|
+
for (const entry of c.packages) {
|
|
147
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
148
|
+
const p = entry;
|
|
149
|
+
if (typeof p.name !== "string" || !/^[a-z0-9-]+$/.test(p.name)) return false;
|
|
150
|
+
if (typeof p.npm !== "string" || !p.npm.startsWith("@pify/")) return false;
|
|
151
|
+
if (p.status !== "published" && p.status !== "planned") return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
async function loadCatalog(opts = {}) {
|
|
156
|
+
const bundled = loadBundledCatalog();
|
|
157
|
+
const cached = readCache();
|
|
158
|
+
let remote = cached?.catalog ?? null;
|
|
159
|
+
const stale = !cached || Date.now() - cached.fetchedAt > CACHE_TTL_MS;
|
|
160
|
+
if (opts.refresh || stale) {
|
|
161
|
+
const fetched = await fetchRemoteCatalog(bundled.remote, opts.refresh ? 1e4 : 3e3);
|
|
162
|
+
if (fetched) {
|
|
163
|
+
remote = fetched;
|
|
164
|
+
writeCache(fetched);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!remote || !validateCatalog(remote)) return bundled;
|
|
168
|
+
return remote.version >= bundled.version ? remote : bundled;
|
|
169
|
+
}
|
|
170
|
+
async function refreshCatalog() {
|
|
171
|
+
const bundled = loadBundledCatalog();
|
|
172
|
+
const fetched = await fetchRemoteCatalog(bundled.remote, 1e4);
|
|
173
|
+
if (fetched) {
|
|
174
|
+
writeCache(fetched);
|
|
175
|
+
return { ok: true, catalog: fetched };
|
|
176
|
+
}
|
|
177
|
+
const cached = readCache();
|
|
178
|
+
const fallback = cached && validateCatalog(cached.catalog) && cached.catalog.version >= bundled.version ? cached.catalog : bundled;
|
|
179
|
+
return { ok: false, catalog: fallback };
|
|
180
|
+
}
|
|
181
|
+
async function fetchRemoteCatalog(defaultUrl, timeoutMs) {
|
|
182
|
+
if (isOffline()) return null;
|
|
183
|
+
const url = process.env.PIFY_CATALOG_URL || defaultUrl;
|
|
184
|
+
try {
|
|
185
|
+
const controller = new AbortController();
|
|
186
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
187
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
if (!res.ok) return null;
|
|
190
|
+
const data = await res.json();
|
|
191
|
+
return validateCatalog(data) ? data : null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function readCache() {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(readFileSync(cachePath(), "utf8"));
|
|
199
|
+
if (typeof parsed.fetchedAt !== "number" || !validateCatalog(parsed.catalog)) return null;
|
|
200
|
+
return parsed;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function writeCache(catalog) {
|
|
206
|
+
try {
|
|
207
|
+
mkdirSync(dirname(cachePath()), { recursive: true });
|
|
208
|
+
writeFileSync(cachePath(), JSON.stringify({ fetchedAt: Date.now(), catalog }, null, 2));
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
213
|
+
var DIST_TAG = /^[a-z][a-z0-9._-]*$/i;
|
|
214
|
+
function parseInstallSpec(input) {
|
|
215
|
+
const raw = input.startsWith("npm:") ? input.slice(4) : input;
|
|
216
|
+
const at = raw.indexOf("@", raw.startsWith("@") ? 1 : 0);
|
|
217
|
+
const base = at > 0 ? raw.slice(0, at) : raw;
|
|
218
|
+
const pin = at > 0 ? raw.slice(at + 1) : null;
|
|
219
|
+
if (pin !== null && !EXACT_VERSION.test(pin) && !(DIST_TAG.test(pin) && !/^\d/.test(pin))) {
|
|
220
|
+
throw usageError(
|
|
221
|
+
"Version pins must be exact versions or dist-tags.",
|
|
222
|
+
`Got ${JSON.stringify(pin)} - ranges (^, ~, >, <, *, x) are not supported.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return { base, pin };
|
|
226
|
+
}
|
|
227
|
+
function resolveInstallTarget(catalog, input) {
|
|
228
|
+
const { base, pin } = parseInstallSpec(input);
|
|
229
|
+
if (base.startsWith("@")) {
|
|
230
|
+
if (!base.startsWith("@pify/")) {
|
|
231
|
+
throw usageError(
|
|
232
|
+
"pify installs @pify packages only.",
|
|
233
|
+
`For other sources use: pi install ${input}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const shortName = base.slice("@pify/".length).toLowerCase();
|
|
237
|
+
if (!/^[a-z0-9._-]+$/.test(shortName)) {
|
|
238
|
+
throw usageError(`Invalid package name: ${JSON.stringify(base)}`);
|
|
239
|
+
}
|
|
240
|
+
const entry2 = catalog.packages.find((p) => p.name === shortName) ?? null;
|
|
241
|
+
return {
|
|
242
|
+
shortName,
|
|
243
|
+
npm: `@pify/${shortName}`,
|
|
244
|
+
pin,
|
|
245
|
+
explicit: true,
|
|
246
|
+
inCatalog: entry2 !== null,
|
|
247
|
+
status: entry2?.status ?? null,
|
|
248
|
+
repo: entry2?.repo ?? null
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (/[:/\\]/.test(base)) {
|
|
252
|
+
throw usageError(
|
|
253
|
+
"pify installs @pify packages only.",
|
|
254
|
+
`For other sources use: pi install ${input}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const needle = base.toLowerCase();
|
|
258
|
+
const entry = catalog.packages.find((p) => p.name === needle || `pify-${p.name}` === needle) ?? null;
|
|
259
|
+
if (!entry) {
|
|
260
|
+
const names = catalog.packages.map((p) => p.name).join(", ");
|
|
261
|
+
throw notFoundError(
|
|
262
|
+
`Unknown pify package: ${JSON.stringify(input)}`,
|
|
263
|
+
`Available: ${names}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (entry.status !== "published") {
|
|
267
|
+
throw notFoundError(
|
|
268
|
+
`@pify/${entry.name} is not published yet.`,
|
|
269
|
+
`Track it at ${entry.repo}. Run "pify update --catalog" to refresh availability.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
shortName: entry.name,
|
|
274
|
+
npm: entry.npm,
|
|
275
|
+
pin,
|
|
276
|
+
explicit: false,
|
|
277
|
+
inCatalog: true,
|
|
278
|
+
status: entry.status,
|
|
279
|
+
repo: entry.repo
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function resolvePifyName(input) {
|
|
283
|
+
const { base } = parseInstallSpec(input);
|
|
284
|
+
if (base.startsWith("@")) {
|
|
285
|
+
if (!base.startsWith("@pify/")) {
|
|
286
|
+
throw usageError(
|
|
287
|
+
"pify manages @pify packages only.",
|
|
288
|
+
`For other sources use the pi CLI directly.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const shortName = base.slice("@pify/".length).toLowerCase();
|
|
292
|
+
if (!/^[a-z0-9._-]+$/.test(shortName)) {
|
|
293
|
+
throw usageError(`Invalid package name: ${JSON.stringify(base)}`);
|
|
294
|
+
}
|
|
295
|
+
return shortName;
|
|
296
|
+
}
|
|
297
|
+
if (/[:/\\]/.test(base)) {
|
|
298
|
+
throw usageError(
|
|
299
|
+
"pify manages @pify packages only.",
|
|
300
|
+
`For other sources use the pi CLI directly.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const name = base.toLowerCase();
|
|
304
|
+
return name.startsWith("pify-") ? name.slice("pify-".length) : name;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/pi.ts
|
|
308
|
+
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
309
|
+
import { homedir as homedir2 } from "os";
|
|
310
|
+
import { join as join3 } from "path";
|
|
311
|
+
|
|
312
|
+
// src/ui.ts
|
|
313
|
+
function detectColor(stream) {
|
|
314
|
+
const { NO_COLOR, FORCE_COLOR, TERM } = process.env;
|
|
315
|
+
if (FORCE_COLOR !== void 0 && FORCE_COLOR !== "0") return true;
|
|
316
|
+
if (NO_COLOR !== void 0 && NO_COLOR !== "") return false;
|
|
317
|
+
if (TERM === "dumb") return false;
|
|
318
|
+
return Boolean(stream.isTTY);
|
|
319
|
+
}
|
|
320
|
+
var stdoutColor = detectColor(process.stdout);
|
|
321
|
+
var stderrColor = detectColor(process.stderr);
|
|
322
|
+
function palette(enabled) {
|
|
323
|
+
const wrap = (open, close) => (s) => enabled() ? `[${open}m${s}[${close}m` : s;
|
|
324
|
+
return {
|
|
325
|
+
bold: wrap("1", "22"),
|
|
326
|
+
dim: wrap("2", "22"),
|
|
327
|
+
red: wrap("31", "39"),
|
|
328
|
+
green: wrap("32", "39"),
|
|
329
|
+
yellow: wrap("33", "39"),
|
|
330
|
+
cyan: wrap("36", "39")
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
var style = palette(() => stdoutColor);
|
|
334
|
+
var estyle = palette(() => stderrColor);
|
|
335
|
+
function step(command) {
|
|
336
|
+
process.stdout.write(`${style.dim(`$ ${command}`)}
|
|
337
|
+
`);
|
|
338
|
+
}
|
|
339
|
+
function success(msg) {
|
|
340
|
+
process.stdout.write(`${style.green(msg)}
|
|
341
|
+
`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/pi.ts
|
|
345
|
+
var PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
346
|
+
var NODE_FLOOR = "22.19.0";
|
|
347
|
+
var TESTED_PI_VERSION = "0.84.4";
|
|
348
|
+
var LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
|
|
349
|
+
function piStatus() {
|
|
350
|
+
const binPath = which("pi");
|
|
351
|
+
if (!binPath) return { installed: false, binPath: null, version: null };
|
|
352
|
+
const res = runCapture("pi", ["--version"]);
|
|
353
|
+
const version = res.status === 0 ? res.stdout.trim() || null : null;
|
|
354
|
+
return { installed: true, binPath, version };
|
|
355
|
+
}
|
|
356
|
+
async function fetchLatestPiVersion() {
|
|
357
|
+
if (isOffline()) return null;
|
|
358
|
+
try {
|
|
359
|
+
const controller = new AbortController();
|
|
360
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
361
|
+
const res = await fetch(LATEST_VERSION_URL, {
|
|
362
|
+
signal: controller.signal,
|
|
363
|
+
headers: { "user-agent": `pify/${VERSION}` }
|
|
364
|
+
});
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
if (!res.ok) return null;
|
|
367
|
+
const data = await res.json();
|
|
368
|
+
return typeof data.version === "string" ? data.version : null;
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function compareSemver(a, b) {
|
|
374
|
+
const pa = a.split(".").map((n) => Number.parseInt(n, 10));
|
|
375
|
+
const pb = b.split(".").map((n) => Number.parseInt(n, 10));
|
|
376
|
+
for (let i = 0; i < 3; i++) {
|
|
377
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
378
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
379
|
+
}
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
function assertNodeVersion() {
|
|
383
|
+
const current = process.versions.node;
|
|
384
|
+
if (compareSemver(current, NODE_FLOOR) < 0) {
|
|
385
|
+
throw environmentError(
|
|
386
|
+
`The pi coding agent requires Node >= ${NODE_FLOOR}; you are running ${current}.`,
|
|
387
|
+
"Upgrade Node (e.g. via nvm / nvm-windows) and try again."
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function installPi(version) {
|
|
392
|
+
assertNodeVersion();
|
|
393
|
+
if (!which("npm")) {
|
|
394
|
+
throw environmentError(
|
|
395
|
+
"npm was not found on PATH.",
|
|
396
|
+
"Install Node.js (which bundles npm) from https://nodejs.org and retry."
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (process.env.PI_MANAGED_INSTALL_ROOT && !piStatus().installed) {
|
|
400
|
+
throw environmentError(
|
|
401
|
+
"PI_MANAGED_INSTALL_ROOT is set but pi is not installed.",
|
|
402
|
+
"Use the managed installer: curl -fsSL https://pi.dev/install.sh | sh (Linux/macOS)."
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const spec = version ? `${PI_PACKAGE}@${version}` : PI_PACKAGE;
|
|
406
|
+
step(`npm install -g --ignore-scripts ${spec}`);
|
|
407
|
+
const code = await runInherit("npm", ["install", "-g", "--ignore-scripts", spec]);
|
|
408
|
+
if (code !== 0) {
|
|
409
|
+
throw new PifyError(
|
|
410
|
+
`npm install -g ${spec} exited with code ${code}.`,
|
|
411
|
+
ExitCode.SUBPROCESS,
|
|
412
|
+
isWindows ? "If this is a permissions or file-lock error, close running pi sessions and retry from an elevated terminal." : "If this is an EACCES error, configure a user-writable npm prefix (https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally) or use a Node version manager."
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const st = piStatus();
|
|
416
|
+
if (!st.installed) {
|
|
417
|
+
throw environmentError(
|
|
418
|
+
"pi was installed but is not resolvable on PATH.",
|
|
419
|
+
"Add npm's global prefix to PATH (npm prefix -g) and open a new terminal."
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
success(`pi ${st.version ?? ""} installed.`.replace(" ", " "));
|
|
423
|
+
}
|
|
424
|
+
async function updatePi() {
|
|
425
|
+
const st = piStatus();
|
|
426
|
+
if (!st.installed) {
|
|
427
|
+
await installPi();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
step("pi update");
|
|
431
|
+
const code = await runInherit("pi", ["update"]);
|
|
432
|
+
if (code !== 0) {
|
|
433
|
+
throw new PifyError(`pi update exited with code ${code}.`, ExitCode.SUBPROCESS);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function agentDir() {
|
|
437
|
+
const override = process.env.PI_CODING_AGENT_DIR;
|
|
438
|
+
if (override) {
|
|
439
|
+
return override.startsWith("~") ? join3(homedir2(), override.slice(1).replace(/^[/\\]/, "")) : override;
|
|
440
|
+
}
|
|
441
|
+
return join3(homedir2(), ".pi", "agent");
|
|
442
|
+
}
|
|
443
|
+
function settingsPath(scope) {
|
|
444
|
+
return scope === "user" ? join3(agentDir(), "settings.json") : join3(process.cwd(), ".pi", "settings.json");
|
|
445
|
+
}
|
|
446
|
+
function readSettings(scope) {
|
|
447
|
+
try {
|
|
448
|
+
return JSON.parse(readFileSync2(settingsPath(scope), "utf8"));
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
var PIFY_SOURCE = /^npm:@pify\/([A-Za-z0-9._-]+?)(?:@(.+))?$/;
|
|
454
|
+
function installedPifyPackages() {
|
|
455
|
+
const result = /* @__PURE__ */ new Map();
|
|
456
|
+
for (const scope of ["user", "project"]) {
|
|
457
|
+
const settings = readSettings(scope);
|
|
458
|
+
for (const entry of settings?.packages ?? []) {
|
|
459
|
+
const source = typeof entry === "string" ? entry : entry.source;
|
|
460
|
+
if (typeof source !== "string") continue;
|
|
461
|
+
const match = PIFY_SOURCE.exec(source);
|
|
462
|
+
const name = match?.[1];
|
|
463
|
+
if (!name) continue;
|
|
464
|
+
result.set(name, { name, scope, source, pin: match?.[2] ?? null });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
function installedVersionOnDisk(name, scope) {
|
|
470
|
+
const root = scope === "user" ? join3(agentDir(), "npm") : join3(process.cwd(), ".pi", "npm");
|
|
471
|
+
try {
|
|
472
|
+
const pkg = JSON.parse(
|
|
473
|
+
readFileSync2(join3(root, "node_modules", "@pify", name, "package.json"), "utf8")
|
|
474
|
+
);
|
|
475
|
+
return { present: true, version: typeof pkg.version === "string" ? pkg.version : null };
|
|
476
|
+
} catch {
|
|
477
|
+
return { present: false, version: null };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/templates.ts
|
|
482
|
+
function interpolate(template, vars) {
|
|
483
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
484
|
+
const value = vars[key];
|
|
485
|
+
if (value === void 0) throw new Error(`Unknown template variable: ${key}`);
|
|
486
|
+
return value;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function packageJsonTemplate(scoped) {
|
|
490
|
+
const publishConfig = scoped ? `
|
|
491
|
+
"publishConfig": { "access": "public" },` : "";
|
|
492
|
+
return `{
|
|
493
|
+
"name": "{{name}}",
|
|
494
|
+
"version": "0.1.0",
|
|
495
|
+
"description": "{{description}}",
|
|
496
|
+
"type": "module",
|
|
497
|
+
"keywords": ["pi-package", "pi-extension", "pi", "pify"],
|
|
498
|
+
"license": "MIT",${publishConfig}
|
|
499
|
+
"files": ["index.ts", "src"],
|
|
500
|
+
"pi": {
|
|
501
|
+
"extensions": ["./index.ts"]
|
|
502
|
+
},
|
|
503
|
+
"engines": { "node": ">=22.19.0" },
|
|
504
|
+
"scripts": { "typecheck": "tsc --noEmit" },
|
|
505
|
+
"peerDependencies": {
|
|
506
|
+
"@earendil-works/pi-ai": "*",
|
|
507
|
+
"@earendil-works/pi-agent-core": "*",
|
|
508
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
509
|
+
"@earendil-works/pi-tui": "*",
|
|
510
|
+
"typebox": "*"
|
|
511
|
+
},
|
|
512
|
+
"peerDependenciesMeta": {
|
|
513
|
+
"@earendil-works/pi-ai": { "optional": true },
|
|
514
|
+
"@earendil-works/pi-agent-core": { "optional": true },
|
|
515
|
+
"@earendil-works/pi-coding-agent": { "optional": true },
|
|
516
|
+
"@earendil-works/pi-tui": { "optional": true },
|
|
517
|
+
"typebox": { "optional": true }
|
|
518
|
+
},
|
|
519
|
+
"devDependencies": {
|
|
520
|
+
"@earendil-works/pi-ai": "*",
|
|
521
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
522
|
+
"typescript": "^5.7.2",
|
|
523
|
+
"@types/node": "^22.10.2"
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
var INDEX_TS = `import { Type } from "@earendil-works/pi-ai";
|
|
529
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
530
|
+
|
|
531
|
+
// Rules this template already follows:
|
|
532
|
+
// - Signal tool errors by THROWING from execute(); returned values never set isError.
|
|
533
|
+
// - Truncate large output with truncateHead/truncateTail from
|
|
534
|
+
// @earendil-works/pi-coding-agent (built-in limits: 50 KB / 2000 lines).
|
|
535
|
+
// - Use StringEnum from @earendil-works/pi-ai for enum parameters;
|
|
536
|
+
// Type.Union/Type.Literal breaks on Google's API.
|
|
537
|
+
// - Use the CONFIG_DIR_NAME export instead of hardcoding ".pi" for
|
|
538
|
+
// project-local config paths.
|
|
539
|
+
const exampleTool = defineTool({
|
|
540
|
+
name: "{{snakeName}}_hello",
|
|
541
|
+
label: "{{shortName}} hello",
|
|
542
|
+
description: "Example tool for {{name}}. Replace with your own.",
|
|
543
|
+
parameters: Type.Object({
|
|
544
|
+
name: Type.String({ description: "Name to greet" }),
|
|
545
|
+
}),
|
|
546
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
547
|
+
return {
|
|
548
|
+
content: [{ type: "text", text: \`Hello, \${params.name}!\` }],
|
|
549
|
+
details: { greeted: params.name },
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
export default function (pi: ExtensionAPI) {
|
|
555
|
+
pi.registerTool(exampleTool);
|
|
556
|
+
|
|
557
|
+
pi.registerCommand("{{shortName}}-hello", {
|
|
558
|
+
description: "Example command for {{name}}. Replace with your own.",
|
|
559
|
+
handler: async (args, _ctx) => {
|
|
560
|
+
// \`args\` is the single raw string typed after the command name.
|
|
561
|
+
// There is no argv parsing - split it yourself if you need arguments.
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Never start watchers, sockets, timers, or child processes here in the
|
|
566
|
+
// factory: it can run in invocations that never start a session.
|
|
567
|
+
pi.on("session_start", async (_event, _ctx) => {
|
|
568
|
+
// Start background resources here.
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
572
|
+
// Tear them down here. This must be idempotent: it fires on quit,
|
|
573
|
+
// /reload, /new, /resume, and /fork.
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
`;
|
|
577
|
+
var README_MD = `# {{name}}
|
|
578
|
+
|
|
579
|
+
{{description}}
|
|
580
|
+
|
|
581
|
+
A Pi Package for the pi coding agent (https://pi.dev).
|
|
582
|
+
|
|
583
|
+
## Develop
|
|
584
|
+
|
|
585
|
+
npm install # dev dependencies for typechecking
|
|
586
|
+
pi install ./{{dir}} # register locally; hot-reloads with /reload
|
|
587
|
+
pi -e ./index.ts # or load ad hoc for a single run
|
|
588
|
+
npm run typecheck
|
|
589
|
+
|
|
590
|
+
## Publish
|
|
591
|
+
|
|
592
|
+
npm publish --access public
|
|
593
|
+
|
|
594
|
+
Then install anywhere with: pi install npm:{{name}}
|
|
595
|
+
|
|
596
|
+
Tested with pi {{piVersion}}.
|
|
597
|
+
`;
|
|
598
|
+
export {
|
|
599
|
+
ExitCode,
|
|
600
|
+
INDEX_TS,
|
|
601
|
+
NODE_FLOOR,
|
|
602
|
+
PI_PACKAGE,
|
|
603
|
+
PifyError,
|
|
604
|
+
README_MD,
|
|
605
|
+
TESTED_PI_VERSION,
|
|
606
|
+
VERSION,
|
|
607
|
+
agentDir,
|
|
608
|
+
assertSafeArg,
|
|
609
|
+
compareSemver,
|
|
610
|
+
fetchLatestPiVersion,
|
|
611
|
+
installPi,
|
|
612
|
+
installedPifyPackages,
|
|
613
|
+
installedVersionOnDisk,
|
|
614
|
+
interpolate,
|
|
615
|
+
isOffline,
|
|
616
|
+
loadBundledCatalog,
|
|
617
|
+
loadCatalog,
|
|
618
|
+
packageJsonTemplate,
|
|
619
|
+
parseInstallSpec,
|
|
620
|
+
piStatus,
|
|
621
|
+
readSettings,
|
|
622
|
+
refreshCatalog,
|
|
623
|
+
resolveInstallTarget,
|
|
624
|
+
resolvePifyName,
|
|
625
|
+
updatePi,
|
|
626
|
+
validateCatalog,
|
|
627
|
+
which
|
|
628
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pify/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Front door for the Pify suite: install and update the pi coding agent, manage @pify packages, and scaffold Pi Packages",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pify",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-coding-agent",
|
|
9
|
+
"cli",
|
|
10
|
+
"coding-agent",
|
|
11
|
+
"extensions",
|
|
12
|
+
"scaffolding"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/pifydev/cli#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/pifydev/cli/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/pifydev/cli.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Pify maintainers",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"pify": "dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"catalog.json",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.19.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"pretest": "npm run build",
|
|
48
|
+
"test": "node --test test/*.test.js",
|
|
49
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.2",
|
|
53
|
+
"tsup": "^8.3.5",
|
|
54
|
+
"typescript": "^5.7.2"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|