@pify/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -38
- package/dist/cli.js +141 -32
- package/dist/index.d.ts +23 -4
- package/dist/index.js +100 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# @pify/cli
|
|
2
2
|
|
|
3
|
-
Front door for the [Pify](https://github.com/pifydev) suite: install and update the
|
|
4
|
-
[pi coding agent](https://github.com/earendil-works/pi), manage the `@pify/*`
|
|
5
|
-
extension packages with short names, and scaffold new Pi Packages.
|
|
3
|
+
Front door for the [Pify](https://github.com/pifydev) suite: install and update the [pi coding agent](https://github.com/earendil-works/pi), manage the `@pify/*` extension packages with short names, and scaffold new Pi Packages.
|
|
6
4
|
|
|
7
|
-
> Not to be confused with the unscoped npm package `pify` (sindresorhus's
|
|
8
|
-
> promisify library) — unrelated.
|
|
5
|
+
> Not to be confused with the unscoped npm package `pify` (sindresorhus's promisify library) — unrelated.
|
|
9
6
|
|
|
10
7
|
## Install
|
|
11
8
|
|
|
@@ -24,6 +21,7 @@ npx @pify/cli setup
|
|
|
24
21
|
```
|
|
25
22
|
pify setup # install the pi coding agent (safe to re-run)
|
|
26
23
|
pify setup --pi-version 0.84.4 # pin the version your team has tested
|
|
24
|
+
pify setup --installer # run pi's official interactive installer (ps1/sh)
|
|
27
25
|
pify list # show the @pify catalog with install state
|
|
28
26
|
pify install goal task # short names resolve to @pify/goal, @pify/task
|
|
29
27
|
pify install goal -l -a # project scope, pre-approved (CI-friendly)
|
|
@@ -34,29 +32,17 @@ pify doctor # diagnose node / npm / pi / settings
|
|
|
34
32
|
pify init my-extension # scaffold a new Pi Package
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
Every command supports `--help`; `install`/`remove`/`update` support `--dry-run`;
|
|
38
|
-
`list`/`doctor` support `--json`.
|
|
35
|
+
Every command supports `--help`; `install`/`remove`/`update` support `--dry-run`; `list`/`doctor` support `--json`.
|
|
39
36
|
|
|
40
37
|
## How it relates to `pi`
|
|
41
38
|
|
|
42
|
-
`pify` deliberately does **not** replace pi's package manager. Every package
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- **Bootstrap** — `pi update` can't run when pi isn't installed yet; `pify setup`
|
|
48
|
-
closes that gap (`npm install -g --ignore-scripts @earendil-works/pi-coding-agent`,
|
|
49
|
-
matching pi's own self-update invocation).
|
|
50
|
-
- **Catalog** — `@pify/*` short names, discovery (including packages that are
|
|
51
|
-
planned but not yet published), and install state at a glance.
|
|
52
|
-
- **One-pass update** — `pify update` chains pi's self-update with per-package
|
|
53
|
-
updates of everything from this org, without touching non-Pify packages you
|
|
54
|
-
manage separately.
|
|
39
|
+
`pify` deliberately does **not** replace pi's package manager. Every package operation delegates to `pi install` / `pi remove` / `pi update`, which own `~/.pi/agent/settings.json` and the extension npm workspace — pify never edits pi's settings. What `pify` adds:
|
|
40
|
+
|
|
41
|
+
- **Bootstrap** — `pi update` can't run when pi isn't installed yet; `pify setup` closes that gap with the same invocation as pi's official installers (`npm install -g --ignore-scripts --min-release-age=0 --no-fund --no-audit`), falling back to `bun add -g --ignore-scripts` when npm is absent and to `--prefix ~/.local` on POSIX when npm's global prefix is not writable (never sudo). `pify setup --installer` hands off to the official interactive installer (`install.ps1` on Windows, `install.sh` on Unix), which can bootstrap Node and Git Bash too.
|
|
42
|
+
- **Catalog** — `@pify/*` short names, discovery (including packages that are planned but not yet published), and install state at a glance.
|
|
43
|
+
- **One-pass update** — `pify update` chains pi's self-update with per-package updates of everything from this org, without touching non-Pify packages you manage separately.
|
|
55
44
|
- **Doctor** — environment sanity checks for support requests.
|
|
56
|
-
- **Scaffolding** — `pify init` emits a correctly-shaped Pi Package: raw
|
|
57
|
-
TypeScript entry (no build step; pi loads `.ts` via jiti), host packages as
|
|
58
|
-
optional `peerDependencies`, `files` allowlist that actually ships the
|
|
59
|
-
sources, and a lifecycle-correct extension factory.
|
|
45
|
+
- **Scaffolding** — `pify init` emits a correctly-shaped Pi Package: raw TypeScript entry (no build step; pi loads `.ts` via jiti), host packages as optional `peerDependencies`, `files` allowlist that actually ships the sources, and a lifecycle-correct extension factory.
|
|
60
46
|
|
|
61
47
|
## Packages
|
|
62
48
|
|
|
@@ -74,16 +60,9 @@ pi's settings. What `pify` adds:
|
|
|
74
60
|
| `@pify/workflow` | Deterministic multi-step agent workflows |
|
|
75
61
|
| `@pify/yolo` | Auto-approve everything, with an undo trail |
|
|
76
62
|
|
|
77
|
-
The catalog ships inside the CLI and refreshes (at most daily) from
|
|
78
|
-
[`catalog.json` on `main`](https://github.com/pifydev/cli/blob/main/catalog.json),
|
|
79
|
-
so newly published packages appear without a CLI update. A fetched catalog is
|
|
80
|
-
validated before use — every entry must stay inside the `@pify` scope — and any
|
|
81
|
-
invalid document is discarded entirely. Overrides: `PIFY_CATALOG_URL` (remote
|
|
82
|
-
URL), `PIFY_OFFLINE=1` or `PI_OFFLINE=1` (skip all network lookups).
|
|
63
|
+
The catalog ships inside the CLI and refreshes (at most daily) from [`catalog.json` on `main`](https://github.com/pifydev/cli/blob/main/catalog.json), so newly published packages appear without a CLI update. A fetched catalog is validated before use — every entry must stay inside the `@pify` scope — and any invalid document is discarded entirely. Overrides: `PIFY_CATALOG_URL` (remote URL), `PIFY_OFFLINE=1` or `PI_OFFLINE=1` (skip all network lookups).
|
|
83
64
|
|
|
84
|
-
Explicit `@pify/<name>` arguments bypass the catalog's published/planned gate
|
|
85
|
-
(with a warning), so a freshly published package is installable before the
|
|
86
|
-
catalog propagates.
|
|
65
|
+
Explicit `@pify/<name>` arguments bypass the catalog's published/planned gate (with a warning), so a freshly published package is installable before the catalog propagates.
|
|
87
66
|
|
|
88
67
|
## Exit codes
|
|
89
68
|
|
|
@@ -96,9 +75,7 @@ catalog propagates.
|
|
|
96
75
|
| 4 | Not found (unknown catalog name; not yet published; not installed) |
|
|
97
76
|
| 5 | A delegated `pi`/`npm` subprocess exited non-zero |
|
|
98
77
|
|
|
99
|
-
Colored output respects `NO_COLOR`, `FORCE_COLOR`, `--no-color`, and non-TTY
|
|
100
|
-
pipes. Results go to stdout; warnings and errors go to stderr. No interactive
|
|
101
|
-
prompts anywhere — behavior is identical in TTY and CI except color.
|
|
78
|
+
Colored output respects `NO_COLOR`, `FORCE_COLOR`, `--no-color`, and non-TTY pipes. Results go to stdout; warnings and errors go to stderr. No interactive prompts anywhere — behavior is identical in TTY and CI except color.
|
|
102
79
|
|
|
103
80
|
## Development
|
|
104
81
|
|
|
@@ -109,8 +86,7 @@ npm test # builds first, then node --test
|
|
|
109
86
|
npm run typecheck
|
|
110
87
|
```
|
|
111
88
|
|
|
112
|
-
Requires Node >= 22.19.0 (same floor as the pi coding agent).
|
|
113
|
-
Tested with pi 0.84.4.
|
|
89
|
+
Requires Node >= 22.19.0 (same floor as the pi coding agent). Tested with pi 0.84.4.
|
|
114
90
|
|
|
115
91
|
## License
|
|
116
92
|
|
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
5
|
|
|
6
6
|
// src/pi.ts
|
|
7
|
-
import { readFileSync, realpathSync } from "fs";
|
|
7
|
+
import { accessSync as accessSync2, constants as constants2, readFileSync, realpathSync, statSync as statSync2 } from "fs";
|
|
8
8
|
import { homedir } from "os";
|
|
9
|
-
import { join as join2 } from "path";
|
|
9
|
+
import { dirname, join as join2 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/exec.ts
|
|
12
12
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -97,6 +97,17 @@ function runInherit(command, args) {
|
|
|
97
97
|
child.on("close", (code) => resolve2(code ?? 1));
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
|
+
function runOfficialInstaller(kind) {
|
|
101
|
+
const [cmd, argv] = kind === "windows" ? [
|
|
102
|
+
"powershell",
|
|
103
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "irm https://pi.dev/install.ps1 | iex"]
|
|
104
|
+
] : ["sh", ["-c", "curl -fsSL https://pi.dev/install.sh | sh"]];
|
|
105
|
+
return new Promise((resolve2, reject) => {
|
|
106
|
+
const child = spawn(cmd, argv, { stdio: "inherit", windowsHide: false });
|
|
107
|
+
child.on("error", reject);
|
|
108
|
+
child.on("close", (code) => resolve2(code ?? 1));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
100
111
|
function which(command) {
|
|
101
112
|
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
102
113
|
if (isWindows) {
|
|
@@ -258,38 +269,107 @@ function guardNpmManaged(binPath) {
|
|
|
258
269
|
);
|
|
259
270
|
}
|
|
260
271
|
}
|
|
272
|
+
function officialInstallerCommand() {
|
|
273
|
+
return isWindows ? 'powershell -c "irm https://pi.dev/install.ps1 | iex"' : "curl -fsSL https://pi.dev/install.sh | sh";
|
|
274
|
+
}
|
|
275
|
+
function writableOrCreatable(path) {
|
|
276
|
+
let current = path;
|
|
277
|
+
for (; ; ) {
|
|
278
|
+
try {
|
|
279
|
+
statSync2(current);
|
|
280
|
+
break;
|
|
281
|
+
} catch {
|
|
282
|
+
const parent = dirname(current);
|
|
283
|
+
if (parent === current) return false;
|
|
284
|
+
current = parent;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
if (!statSync2(current).isDirectory()) return false;
|
|
289
|
+
accessSync2(current, constants2.W_OK);
|
|
290
|
+
return true;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function npmInstallPiArgs(spec, npmMajor, prefix) {
|
|
296
|
+
const args = ["install", "-g", "--ignore-scripts"];
|
|
297
|
+
if (npmMajor >= 11) args.push("--min-release-age=0");
|
|
298
|
+
args.push("--no-fund", "--no-audit");
|
|
299
|
+
if (prefix) args.push("--prefix", prefix);
|
|
300
|
+
args.push(spec);
|
|
301
|
+
return args;
|
|
302
|
+
}
|
|
303
|
+
function npmMajorVersion() {
|
|
304
|
+
const res = runCapture("npm", ["--version"]);
|
|
305
|
+
const major = Number.parseInt(res.stdout.trim().split(".")[0] ?? "", 10);
|
|
306
|
+
return Number.isNaN(major) ? 0 : major;
|
|
307
|
+
}
|
|
261
308
|
async function installPi(version) {
|
|
262
309
|
assertNodeVersion();
|
|
263
|
-
if (!which("npm")) {
|
|
264
|
-
throw environmentError(
|
|
265
|
-
"npm was not found on PATH.",
|
|
266
|
-
"Install Node.js (which bundles npm) from https://nodejs.org and retry."
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
310
|
if (process.env.PI_MANAGED_INSTALL_ROOT && !piStatus().installed) {
|
|
270
311
|
throw environmentError(
|
|
271
312
|
"PI_MANAGED_INSTALL_ROOT is set but pi is not installed.",
|
|
272
|
-
|
|
313
|
+
`Use the official installer: ${officialInstallerCommand()}`
|
|
273
314
|
);
|
|
274
315
|
}
|
|
275
316
|
const spec = version ? `${PI_PACKAGE}@${version}` : PI_PACKAGE;
|
|
276
|
-
|
|
277
|
-
const
|
|
317
|
+
const npmPath = which("npm");
|
|
318
|
+
const bunPath = which("bun");
|
|
319
|
+
if (!npmPath && !bunPath) {
|
|
320
|
+
throw environmentError(
|
|
321
|
+
"Neither npm nor bun was found on PATH.",
|
|
322
|
+
`Run the official installer (it can install Node too): ${officialInstallerCommand()}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
let command;
|
|
326
|
+
let args;
|
|
327
|
+
let localPrefix = null;
|
|
328
|
+
if (npmPath) {
|
|
329
|
+
command = "npm";
|
|
330
|
+
if (!isWindows) {
|
|
331
|
+
const prefixRes = runCapture("npm", ["prefix", "-g"]);
|
|
332
|
+
const prefix = prefixRes.status === 0 ? prefixRes.stdout.trim() : "";
|
|
333
|
+
const prefixWritable = prefix !== "" && writableOrCreatable(join2(prefix, "lib", "node_modules")) && writableOrCreatable(join2(prefix, "bin"));
|
|
334
|
+
if (prefix && !prefixWritable) {
|
|
335
|
+
let staleGlobalPi = false;
|
|
336
|
+
try {
|
|
337
|
+
statSync2(join2(prefix, "bin", "pi"));
|
|
338
|
+
staleGlobalPi = true;
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
if (staleGlobalPi) {
|
|
342
|
+
throw environmentError(
|
|
343
|
+
`npm's global directory is not writable (${prefix}) and pi is already installed there.`,
|
|
344
|
+
`Update or remove it first: sudo npm install -g --ignore-scripts ${PI_PACKAGE}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
localPrefix = join2(homedir(), ".local");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
args = npmInstallPiArgs(spec, npmMajorVersion(), localPrefix ?? void 0);
|
|
351
|
+
} else {
|
|
352
|
+
command = "bun";
|
|
353
|
+
args = ["add", "-g", "--ignore-scripts", spec];
|
|
354
|
+
}
|
|
355
|
+
step(`${command} ${args.join(" ")}`);
|
|
356
|
+
const code = await runInherit(command, args);
|
|
278
357
|
if (code !== 0) {
|
|
279
358
|
throw new PifyError(
|
|
280
|
-
|
|
359
|
+
`${command} ${args[0]} exited with code ${code}.`,
|
|
281
360
|
ExitCode.SUBPROCESS,
|
|
282
|
-
isWindows ? "If this is a permissions or file-lock error, close running pi sessions and retry from an elevated terminal." :
|
|
361
|
+
isWindows ? "If this is a permissions or file-lock error, close running pi sessions and retry from an elevated terminal." : `If the problem persists, try the official installer: ${officialInstallerCommand()}`
|
|
283
362
|
);
|
|
284
363
|
}
|
|
285
364
|
const st = piStatus();
|
|
286
365
|
if (!st.installed) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
"Add npm's global prefix to PATH (npm prefix -g) and open a new terminal."
|
|
290
|
-
);
|
|
366
|
+
const pathHint = localPrefix ? `Add ${join2(localPrefix, "bin")} to PATH and open a new terminal.` : "Add the global bin directory to PATH (npm prefix -g) and open a new terminal.";
|
|
367
|
+
throw environmentError("pi was installed but is not resolvable on PATH.", pathHint);
|
|
291
368
|
}
|
|
292
369
|
success(`pi ${st.version ?? ""} installed.`.replace(" ", " "));
|
|
370
|
+
if (localPrefix) {
|
|
371
|
+
hint(`Installed under ${localPrefix} because npm's global prefix is not writable.`);
|
|
372
|
+
}
|
|
293
373
|
}
|
|
294
374
|
function requirePi() {
|
|
295
375
|
if (!piStatus().installed) {
|
|
@@ -349,12 +429,19 @@ function installedVersionOnDisk(name, scope) {
|
|
|
349
429
|
|
|
350
430
|
// src/commands/setup.ts
|
|
351
431
|
async function setup(opts) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
432
|
+
if (opts.installer) {
|
|
433
|
+
if (!process.stdin.isTTY || process.env.CI) {
|
|
434
|
+
throw environmentError(
|
|
435
|
+
"The official installer is interactive and needs a terminal.",
|
|
436
|
+
"In CI, use plain `pify setup` (non-interactive npm path) instead."
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
step(officialInstallerCommand());
|
|
440
|
+
const code = await runOfficialInstaller(isWindows ? "windows" : "posix");
|
|
441
|
+
if (code !== 0) {
|
|
442
|
+
throw new PifyError(`The official installer exited with code ${code}.`, ExitCode.SUBPROCESS);
|
|
443
|
+
}
|
|
444
|
+
return 0;
|
|
358
445
|
}
|
|
359
446
|
const st = piStatus();
|
|
360
447
|
if (st.installed && !opts.force && !opts.piVersion) {
|
|
@@ -381,10 +468,10 @@ async function setup(opts) {
|
|
|
381
468
|
// src/catalog.ts
|
|
382
469
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
383
470
|
import { homedir as homedir2 } from "os";
|
|
384
|
-
import { dirname, join as join3 } from "path";
|
|
471
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
385
472
|
import { fileURLToPath } from "url";
|
|
386
473
|
function bundledCatalogPath() {
|
|
387
|
-
return join3(
|
|
474
|
+
return join3(dirname2(fileURLToPath(import.meta.url)), "..", "catalog.json");
|
|
388
475
|
}
|
|
389
476
|
function cachePath() {
|
|
390
477
|
return join3(homedir2(), ".pify", "catalog.json");
|
|
@@ -459,7 +546,7 @@ function readCache() {
|
|
|
459
546
|
}
|
|
460
547
|
function writeCache(catalog) {
|
|
461
548
|
try {
|
|
462
|
-
mkdirSync(
|
|
549
|
+
mkdirSync(dirname2(cachePath()), { recursive: true });
|
|
463
550
|
writeFileSync(cachePath(), JSON.stringify({ fetchedAt: Date.now(), catalog }, null, 2));
|
|
464
551
|
} catch {
|
|
465
552
|
}
|
|
@@ -822,6 +909,14 @@ async function doctor(opts) {
|
|
|
822
909
|
gitPath ? "ok" : "warn",
|
|
823
910
|
gitPath ?? "git not found - pi's git: package sources will not work"
|
|
824
911
|
);
|
|
912
|
+
if (isWindows) {
|
|
913
|
+
const bashPath = which("bash");
|
|
914
|
+
push(
|
|
915
|
+
"bash",
|
|
916
|
+
bashPath ? "ok" : "warn",
|
|
917
|
+
bashPath ?? "bash not found - pi needs Git Bash on Windows; run pify setup --installer"
|
|
918
|
+
);
|
|
919
|
+
}
|
|
825
920
|
const pi = piStatus();
|
|
826
921
|
if (!pi.installed) {
|
|
827
922
|
push("pi", "fail", "not installed - run: pify setup");
|
|
@@ -1159,15 +1254,21 @@ settings. Anything beyond these commands is pi's job: run pi --help.
|
|
|
1159
1254
|
}
|
|
1160
1255
|
var COMMAND_HELP = {
|
|
1161
1256
|
setup: `Usage:
|
|
1162
|
-
pify setup [--force] [--pi-version <version>]
|
|
1257
|
+
pify setup [--force] [--pi-version <version>] [--installer]
|
|
1163
1258
|
|
|
1164
|
-
Install the pi coding agent
|
|
1165
|
-
|
|
1166
|
-
|
|
1259
|
+
Install the pi coding agent. Safe to re-run; reports status when pi is
|
|
1260
|
+
already installed. Uses the same npm invocation as pi's official installers
|
|
1261
|
+
(--ignore-scripts --min-release-age=0), falls back to bun when npm is
|
|
1262
|
+
absent, and to --prefix ~/.local on POSIX when npm's global prefix is not
|
|
1263
|
+
writable (never sudo). Updates of an existing pi are pi's own job: run
|
|
1264
|
+
pify update pi.
|
|
1167
1265
|
|
|
1168
1266
|
Options:
|
|
1169
1267
|
--force Reinstall pi even if it is already installed
|
|
1170
1268
|
--pi-version <version> Install an exact pi version (may downgrade)
|
|
1269
|
+
--installer Run pi's official interactive installer for
|
|
1270
|
+
this OS (can bootstrap Node and Git Bash;
|
|
1271
|
+
needs a terminal, refuses in CI)
|
|
1171
1272
|
|
|
1172
1273
|
Examples:
|
|
1173
1274
|
# Bootstrap a bare machine
|
|
@@ -1175,6 +1276,9 @@ Examples:
|
|
|
1175
1276
|
|
|
1176
1277
|
# Pin the version your team has tested
|
|
1177
1278
|
pify setup --pi-version 0.84.4
|
|
1279
|
+
|
|
1280
|
+
# Full native experience (Windows: install.ps1, Unix: install.sh)
|
|
1281
|
+
pify setup --installer
|
|
1178
1282
|
`,
|
|
1179
1283
|
install: `Usage:
|
|
1180
1284
|
pify install <name...> [-l] [-a] [--dry-run]
|
|
@@ -1283,10 +1387,15 @@ var COMMANDS = [
|
|
|
1283
1387
|
aliases: [],
|
|
1284
1388
|
options: {
|
|
1285
1389
|
force: { type: "boolean", default: false },
|
|
1286
|
-
"pi-version": { type: "string" }
|
|
1390
|
+
"pi-version": { type: "string" },
|
|
1391
|
+
installer: { type: "boolean", default: false }
|
|
1287
1392
|
},
|
|
1288
1393
|
allowPositionals: false,
|
|
1289
|
-
run: (_p, v) => setup({
|
|
1394
|
+
run: (_p, v) => setup({
|
|
1395
|
+
force: Boolean(v.force),
|
|
1396
|
+
piVersion: v["pi-version"],
|
|
1397
|
+
installer: Boolean(v.installer)
|
|
1398
|
+
})
|
|
1290
1399
|
},
|
|
1291
1400
|
{
|
|
1292
1401
|
name: "install",
|
package/dist/index.d.ts
CHANGED
|
@@ -92,10 +92,29 @@ declare function piStatus(): PiStatus;
|
|
|
92
92
|
/** Latest pi version from pi's own release endpoint. Null offline/on error. */
|
|
93
93
|
declare function fetchLatestPiVersion(): Promise<string | null>;
|
|
94
94
|
declare function compareSemver(a: string, b: string): number;
|
|
95
|
+
/** The official installer one-liner for this OS, for hints and --installer. */
|
|
96
|
+
declare function officialInstallerCommand(): string;
|
|
95
97
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
98
|
+
* The npm install argv pi's official installers use:
|
|
99
|
+
* `--ignore-scripts` (supply-chain-safe; matches pi's self-update),
|
|
100
|
+
* `--min-release-age=0` (pi ships npm-shrinkwrap.json, so bypassing npm's
|
|
101
|
+
* release-age gate does not reopen transitive ranges; npm >= 11 only),
|
|
102
|
+
* `--no-fund --no-audit` (noise). Exported for tests.
|
|
103
|
+
*/
|
|
104
|
+
declare function npmInstallPiArgs(spec: string, npmMajor: number, prefix?: string): string[];
|
|
105
|
+
/**
|
|
106
|
+
* Install pi with the same decisions as pi's official installers, minus the
|
|
107
|
+
* interactive parts. This is the one operation pi cannot do for itself —
|
|
108
|
+
* `pi update` requires a working pi.
|
|
109
|
+
*
|
|
110
|
+
* Per-OS behavior (ported from install.sh / install.ps1):
|
|
111
|
+
* - POSIX, npm prefix unwritable, no global pi there: fall back to
|
|
112
|
+
* `--prefix ~/.local` — never sudo.
|
|
113
|
+
* - POSIX, npm prefix unwritable but an old global pi lives there: stop
|
|
114
|
+
* (a ~/.local copy would be shadowed by the stale global one).
|
|
115
|
+
* - No npm but bun on PATH: `bun add -g --ignore-scripts` (documented
|
|
116
|
+
* alternative channel).
|
|
117
|
+
* - Neither: point at the official installer, which can bootstrap Node too.
|
|
99
118
|
*/
|
|
100
119
|
declare function installPi(version?: string): Promise<void>;
|
|
101
120
|
/**
|
|
@@ -192,4 +211,4 @@ declare class PifyError extends Error {
|
|
|
192
211
|
constructor(message: string, code?: ExitCodeValue, hint?: string);
|
|
193
212
|
}
|
|
194
213
|
|
|
195
|
-
export { type Catalog, type CatalogPackage, ExitCode, INDEX_TS, type InstallSpec, type InstallTarget, type InstalledPifyPackage, NODE_FLOOR, PI_PACKAGE, type PiSettings, type PiStatus, PifyError, README_MD, TESTED_PI_VERSION, type TemplateVars, VERSION, agentDir, assertSafeArg, compareSemver, fetchLatestPiVersion, installPi, installedPifyPackages, installedVersionOnDisk, interpolate, isOffline, loadBundledCatalog, loadCatalog, packageJsonTemplate, parseInstallSpec, piStatus, readSettings, refreshCatalog, resolveInstallTarget, resolvePifyName, updatePi, validateCatalog, which };
|
|
214
|
+
export { type Catalog, type CatalogPackage, ExitCode, INDEX_TS, type InstallSpec, type InstallTarget, type InstalledPifyPackage, NODE_FLOOR, PI_PACKAGE, type PiSettings, type PiStatus, PifyError, README_MD, TESTED_PI_VERSION, type TemplateVars, VERSION, agentDir, assertSafeArg, compareSemver, fetchLatestPiVersion, installPi, installedPifyPackages, installedVersionOnDisk, interpolate, isOffline, loadBundledCatalog, loadCatalog, npmInstallPiArgs, officialInstallerCommand, packageJsonTemplate, parseInstallSpec, piStatus, readSettings, refreshCatalog, resolveInstallTarget, resolvePifyName, updatePi, validateCatalog, which };
|
package/dist/index.js
CHANGED
|
@@ -34,21 +34,21 @@ var PifyError = class extends Error {
|
|
|
34
34
|
code;
|
|
35
35
|
/** Optional follow-up shown after the message, e.g. a command to run. */
|
|
36
36
|
hint;
|
|
37
|
-
constructor(message, code = ExitCode.FAILURE,
|
|
37
|
+
constructor(message, code = ExitCode.FAILURE, hint2) {
|
|
38
38
|
super(message);
|
|
39
39
|
this.name = "PifyError";
|
|
40
40
|
this.code = code;
|
|
41
|
-
this.hint =
|
|
41
|
+
this.hint = hint2;
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
-
function usageError(message,
|
|
45
|
-
return new PifyError(message, ExitCode.USAGE,
|
|
44
|
+
function usageError(message, hint2) {
|
|
45
|
+
return new PifyError(message, ExitCode.USAGE, hint2);
|
|
46
46
|
}
|
|
47
|
-
function environmentError(message,
|
|
48
|
-
return new PifyError(message, ExitCode.ENVIRONMENT,
|
|
47
|
+
function environmentError(message, hint2) {
|
|
48
|
+
return new PifyError(message, ExitCode.ENVIRONMENT, hint2);
|
|
49
49
|
}
|
|
50
|
-
function notFoundError(message,
|
|
51
|
-
return new PifyError(message, ExitCode.NOT_FOUND,
|
|
50
|
+
function notFoundError(message, hint2) {
|
|
51
|
+
return new PifyError(message, ExitCode.NOT_FOUND, hint2);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// src/exec.ts
|
|
@@ -305,9 +305,9 @@ function resolvePifyName(input) {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
// src/pi.ts
|
|
308
|
-
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
308
|
+
import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync2, realpathSync, statSync as statSync2 } from "fs";
|
|
309
309
|
import { homedir as homedir2 } from "os";
|
|
310
|
-
import { join as join3 } from "path";
|
|
310
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
311
311
|
|
|
312
312
|
// src/ui.ts
|
|
313
313
|
function detectColor(stream) {
|
|
@@ -340,6 +340,10 @@ function success(msg) {
|
|
|
340
340
|
process.stdout.write(`${style.green(msg)}
|
|
341
341
|
`);
|
|
342
342
|
}
|
|
343
|
+
function hint(msg) {
|
|
344
|
+
process.stdout.write(`${style.dim(msg)}
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
343
347
|
|
|
344
348
|
// src/pi.ts
|
|
345
349
|
var PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
@@ -388,38 +392,107 @@ function assertNodeVersion() {
|
|
|
388
392
|
);
|
|
389
393
|
}
|
|
390
394
|
}
|
|
395
|
+
function officialInstallerCommand() {
|
|
396
|
+
return isWindows ? 'powershell -c "irm https://pi.dev/install.ps1 | iex"' : "curl -fsSL https://pi.dev/install.sh | sh";
|
|
397
|
+
}
|
|
398
|
+
function writableOrCreatable(path) {
|
|
399
|
+
let current = path;
|
|
400
|
+
for (; ; ) {
|
|
401
|
+
try {
|
|
402
|
+
statSync2(current);
|
|
403
|
+
break;
|
|
404
|
+
} catch {
|
|
405
|
+
const parent = dirname2(current);
|
|
406
|
+
if (parent === current) return false;
|
|
407
|
+
current = parent;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
if (!statSync2(current).isDirectory()) return false;
|
|
412
|
+
accessSync2(current, constants2.W_OK);
|
|
413
|
+
return true;
|
|
414
|
+
} catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function npmInstallPiArgs(spec, npmMajor, prefix) {
|
|
419
|
+
const args = ["install", "-g", "--ignore-scripts"];
|
|
420
|
+
if (npmMajor >= 11) args.push("--min-release-age=0");
|
|
421
|
+
args.push("--no-fund", "--no-audit");
|
|
422
|
+
if (prefix) args.push("--prefix", prefix);
|
|
423
|
+
args.push(spec);
|
|
424
|
+
return args;
|
|
425
|
+
}
|
|
426
|
+
function npmMajorVersion() {
|
|
427
|
+
const res = runCapture("npm", ["--version"]);
|
|
428
|
+
const major = Number.parseInt(res.stdout.trim().split(".")[0] ?? "", 10);
|
|
429
|
+
return Number.isNaN(major) ? 0 : major;
|
|
430
|
+
}
|
|
391
431
|
async function installPi(version) {
|
|
392
432
|
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
433
|
if (process.env.PI_MANAGED_INSTALL_ROOT && !piStatus().installed) {
|
|
400
434
|
throw environmentError(
|
|
401
435
|
"PI_MANAGED_INSTALL_ROOT is set but pi is not installed.",
|
|
402
|
-
|
|
436
|
+
`Use the official installer: ${officialInstallerCommand()}`
|
|
403
437
|
);
|
|
404
438
|
}
|
|
405
439
|
const spec = version ? `${PI_PACKAGE}@${version}` : PI_PACKAGE;
|
|
406
|
-
|
|
407
|
-
const
|
|
440
|
+
const npmPath = which("npm");
|
|
441
|
+
const bunPath = which("bun");
|
|
442
|
+
if (!npmPath && !bunPath) {
|
|
443
|
+
throw environmentError(
|
|
444
|
+
"Neither npm nor bun was found on PATH.",
|
|
445
|
+
`Run the official installer (it can install Node too): ${officialInstallerCommand()}`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
let command;
|
|
449
|
+
let args;
|
|
450
|
+
let localPrefix = null;
|
|
451
|
+
if (npmPath) {
|
|
452
|
+
command = "npm";
|
|
453
|
+
if (!isWindows) {
|
|
454
|
+
const prefixRes = runCapture("npm", ["prefix", "-g"]);
|
|
455
|
+
const prefix = prefixRes.status === 0 ? prefixRes.stdout.trim() : "";
|
|
456
|
+
const prefixWritable = prefix !== "" && writableOrCreatable(join3(prefix, "lib", "node_modules")) && writableOrCreatable(join3(prefix, "bin"));
|
|
457
|
+
if (prefix && !prefixWritable) {
|
|
458
|
+
let staleGlobalPi = false;
|
|
459
|
+
try {
|
|
460
|
+
statSync2(join3(prefix, "bin", "pi"));
|
|
461
|
+
staleGlobalPi = true;
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
if (staleGlobalPi) {
|
|
465
|
+
throw environmentError(
|
|
466
|
+
`npm's global directory is not writable (${prefix}) and pi is already installed there.`,
|
|
467
|
+
`Update or remove it first: sudo npm install -g --ignore-scripts ${PI_PACKAGE}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
localPrefix = join3(homedir2(), ".local");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
args = npmInstallPiArgs(spec, npmMajorVersion(), localPrefix ?? void 0);
|
|
474
|
+
} else {
|
|
475
|
+
command = "bun";
|
|
476
|
+
args = ["add", "-g", "--ignore-scripts", spec];
|
|
477
|
+
}
|
|
478
|
+
step(`${command} ${args.join(" ")}`);
|
|
479
|
+
const code = await runInherit(command, args);
|
|
408
480
|
if (code !== 0) {
|
|
409
481
|
throw new PifyError(
|
|
410
|
-
|
|
482
|
+
`${command} ${args[0]} exited with code ${code}.`,
|
|
411
483
|
ExitCode.SUBPROCESS,
|
|
412
|
-
isWindows ? "If this is a permissions or file-lock error, close running pi sessions and retry from an elevated terminal." :
|
|
484
|
+
isWindows ? "If this is a permissions or file-lock error, close running pi sessions and retry from an elevated terminal." : `If the problem persists, try the official installer: ${officialInstallerCommand()}`
|
|
413
485
|
);
|
|
414
486
|
}
|
|
415
487
|
const st = piStatus();
|
|
416
488
|
if (!st.installed) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
"Add npm's global prefix to PATH (npm prefix -g) and open a new terminal."
|
|
420
|
-
);
|
|
489
|
+
const pathHint = localPrefix ? `Add ${join3(localPrefix, "bin")} to PATH and open a new terminal.` : "Add the global bin directory to PATH (npm prefix -g) and open a new terminal.";
|
|
490
|
+
throw environmentError("pi was installed but is not resolvable on PATH.", pathHint);
|
|
421
491
|
}
|
|
422
492
|
success(`pi ${st.version ?? ""} installed.`.replace(" ", " "));
|
|
493
|
+
if (localPrefix) {
|
|
494
|
+
hint(`Installed under ${localPrefix} because npm's global prefix is not writable.`);
|
|
495
|
+
}
|
|
423
496
|
}
|
|
424
497
|
async function updatePi() {
|
|
425
498
|
const st = piStatus();
|
|
@@ -615,6 +688,8 @@ export {
|
|
|
615
688
|
isOffline,
|
|
616
689
|
loadBundledCatalog,
|
|
617
690
|
loadCatalog,
|
|
691
|
+
npmInstallPiArgs,
|
|
692
|
+
officialInstallerCommand,
|
|
618
693
|
packageJsonTemplate,
|
|
619
694
|
parseInstallSpec,
|
|
620
695
|
piStatus,
|