@pify/cli 0.4.8 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/catalog.json +4 -4
- package/dist/cli.js +70 -31
- package/dist/index.d.ts +123 -88
- package/dist/index.js +65 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ npx @pify/cli setup
|
|
|
22
22
|
|
|
23
23
|
```
|
|
24
24
|
pify setup # install the pi coding agent (safe to re-run)
|
|
25
|
-
pify setup --pi-version 0.
|
|
25
|
+
pify setup --pi-version 0.87.0 # pin the version your team has tested
|
|
26
26
|
pify setup --installer # run pi's official interactive installer (ps1/sh)
|
|
27
27
|
pify list # show the @pify catalog with install state
|
|
28
28
|
pify install goal task # short names resolve to @pify/goal, @pify/task
|
package/catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 32,
|
|
3
3
|
"org": "pifydev",
|
|
4
4
|
"scope": "@pify",
|
|
5
5
|
"remote": "https://raw.githubusercontent.com/pifydev/cli/main/catalog.json",
|
|
@@ -112,14 +112,14 @@
|
|
|
112
112
|
"name": "subagent",
|
|
113
113
|
"npm": "@pify/subagent",
|
|
114
114
|
"repo": "https://github.com/pifydev/subagent",
|
|
115
|
-
"description": "Spawn scoped subagents: agent_run/agent_result, @agent mentions, custom agent types, ask_supervisor, shell gates with bounded repair, task outcome separate from session status, results delivered not polled",
|
|
115
|
+
"description": "Spawn scoped subagents: agent_run/agent_result, @agent mentions, custom agent types, ask_supervisor, shell gates run async (wait on agent_result) with bounded repair, task outcome separate from session status, results delivered not polled",
|
|
116
116
|
"status": "published"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"name": "swarm",
|
|
120
120
|
"npm": "@pify/swarm",
|
|
121
121
|
"repo": "https://github.com/pifydev/swarm",
|
|
122
|
-
"description": "Run many pi agents in parallel: fan-out with per-item routing, needs-DAG with skip-on-upstream-failure, per-item shell gates, concurrency queue, shared mailbox, first hard failure interrupts early, results delivered not polled",
|
|
122
|
+
"description": "Run many pi agents in parallel: fan-out with per-item routing, needs-DAG with skip-on-upstream-failure, per-item shell gates, concurrency queue, shared mailbox, /swarm stop, first hard failure interrupts early, wait for results (delivered not polled)",
|
|
123
123
|
"status": "published"
|
|
124
124
|
},
|
|
125
125
|
{
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"name": "workflow",
|
|
148
148
|
"npm": "@pify/workflow",
|
|
149
149
|
"repo": "https://github.com/pifydev/workflow",
|
|
150
|
-
"description": "Deterministic agent orchestration through JavaScript scripts: agent()/parallel()/pipeline(), gates, resume",
|
|
150
|
+
"description": "Deterministic agent orchestration through JavaScript scripts: agent()/parallel()/pipeline(), gates, resume, /workflows stop, failure ledger, wait for results",
|
|
151
151
|
"status": "published"
|
|
152
152
|
},
|
|
153
153
|
{
|
package/dist/cli.js
CHANGED
|
@@ -199,7 +199,7 @@ var VERSION = require2("../package.json").version;
|
|
|
199
199
|
// src/pi.ts
|
|
200
200
|
var PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
201
201
|
var NODE_FLOOR = "22.19.0";
|
|
202
|
-
var TESTED_PI_VERSION = "0.
|
|
202
|
+
var TESTED_PI_VERSION = "0.87.0";
|
|
203
203
|
var LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
|
|
204
204
|
function piStatus() {
|
|
205
205
|
const binPath = which("pi");
|
|
@@ -515,16 +515,23 @@ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
|
515
515
|
function loadBundledCatalog() {
|
|
516
516
|
return JSON.parse(readFileSync2(bundledCatalogPath(), "utf8"));
|
|
517
517
|
}
|
|
518
|
+
var CLEAN_TEXT = /^[^\x00-\x1f\x7f]{1,500}$/;
|
|
519
|
+
var CATALOG_REPO = /^https:\/\/github\.com\/pifydev\/[A-Za-z0-9._-]+$/;
|
|
520
|
+
var CATALOG_IDENT = /^[@a-z0-9-]+$/;
|
|
518
521
|
function validateCatalog(data) {
|
|
519
522
|
if (typeof data !== "object" || data === null) return false;
|
|
520
523
|
const c = data;
|
|
521
524
|
if (!Number.isInteger(c.version)) return false;
|
|
525
|
+
if (typeof c.org !== "string" || !CATALOG_IDENT.test(c.org)) return false;
|
|
526
|
+
if (typeof c.scope !== "string" || !CATALOG_IDENT.test(c.scope)) return false;
|
|
522
527
|
if (!Array.isArray(c.packages)) return false;
|
|
523
528
|
for (const entry of c.packages) {
|
|
524
529
|
if (typeof entry !== "object" || entry === null) return false;
|
|
525
530
|
const p = entry;
|
|
526
531
|
if (typeof p.name !== "string" || !/^[a-z0-9-]+$/.test(p.name)) return false;
|
|
527
532
|
if (typeof p.npm !== "string" || !p.npm.startsWith("@pify/")) return false;
|
|
533
|
+
if (typeof p.description !== "string" || !CLEAN_TEXT.test(p.description)) return false;
|
|
534
|
+
if (typeof p.repo !== "string" || !CATALOG_REPO.test(p.repo)) return false;
|
|
528
535
|
if (p.status !== "published" && p.status !== "planned") return false;
|
|
529
536
|
if (p.conflicts !== void 0) {
|
|
530
537
|
if (!Array.isArray(p.conflicts)) return false;
|
|
@@ -540,7 +547,7 @@ function validateCatalog(data) {
|
|
|
540
547
|
if (typeof entry !== "object" || entry === null) return false;
|
|
541
548
|
const b = entry;
|
|
542
549
|
if (typeof b.name !== "string" || !/^[a-z0-9-]+$/.test(b.name)) return false;
|
|
543
|
-
if (typeof b.description !== "string") return false;
|
|
550
|
+
if (typeof b.description !== "string" || !CLEAN_TEXT.test(b.description)) return false;
|
|
544
551
|
if (!Array.isArray(b.packages) || b.packages.length === 0) return false;
|
|
545
552
|
for (const member of b.packages) {
|
|
546
553
|
if (typeof member !== "string" || !known.has(member)) return false;
|
|
@@ -965,27 +972,50 @@ async function profile(args, opts) {
|
|
|
965
972
|
}
|
|
966
973
|
|
|
967
974
|
// src/commands/update.ts
|
|
968
|
-
|
|
969
|
-
const
|
|
975
|
+
function piRow(installed, latest) {
|
|
976
|
+
const base = { name: "pi", installed, latest, pin: null };
|
|
977
|
+
if (installed === null) return { ...base, state: "missing" };
|
|
978
|
+
if (latest === null) return { ...base, state: "unknown" };
|
|
979
|
+
return { ...base, state: compareSemver(installed, latest) < 0 ? "outdated" : "current" };
|
|
980
|
+
}
|
|
981
|
+
async function checkUpdates(deps = {}) {
|
|
982
|
+
const installed = deps.installed ?? installedPifyPackages();
|
|
983
|
+
const versionOnDisk = deps.versionOnDisk ?? installedVersionOnDisk;
|
|
984
|
+
const latestPackage = deps.latestPackage ?? fetchLatestPackageVersion;
|
|
985
|
+
const piVersion = deps.piVersion !== void 0 ? deps.piVersion : piStatus().version;
|
|
986
|
+
const latestPi = deps.latestPi ?? fetchLatestPiVersion;
|
|
970
987
|
const names = [...installed.keys()].sort();
|
|
971
|
-
const
|
|
972
|
-
|
|
988
|
+
const [piLatest, ...packages] = await Promise.all([
|
|
989
|
+
latestPi(),
|
|
990
|
+
...names.map(async (name) => {
|
|
973
991
|
const entry = installed.get(name);
|
|
974
|
-
const onDisk =
|
|
975
|
-
const latest = await
|
|
976
|
-
|
|
992
|
+
const onDisk = versionOnDisk(name, entry.scope);
|
|
993
|
+
const latest = await latestPackage(name);
|
|
994
|
+
const pin = entry.pin;
|
|
995
|
+
if (!onDisk.present) return { name, installed: null, latest, pin, state: "missing" };
|
|
977
996
|
if (!onDisk.version || !latest) {
|
|
978
|
-
return { name, installed: onDisk.version, latest, state: "unknown" };
|
|
997
|
+
return { name, installed: onDisk.version, latest, pin, state: "unknown" };
|
|
979
998
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
latest,
|
|
984
|
-
state: compareSemver(onDisk.version, latest) < 0 ? "outdated" : "current"
|
|
985
|
-
};
|
|
999
|
+
const behind = compareSemver(onDisk.version, latest) < 0;
|
|
1000
|
+
const state = behind ? pin !== null ? "pinned" : "outdated" : "current";
|
|
1001
|
+
return { name, installed: onDisk.version, latest, pin, state };
|
|
986
1002
|
})
|
|
987
|
-
);
|
|
988
|
-
return
|
|
1003
|
+
]);
|
|
1004
|
+
return { pi: piRow(piVersion, piLatest), packages };
|
|
1005
|
+
}
|
|
1006
|
+
function checkDetail(status) {
|
|
1007
|
+
switch (status.state) {
|
|
1008
|
+
case "outdated":
|
|
1009
|
+
return `${status.installed} \u2192 ${status.latest}`;
|
|
1010
|
+
case "pinned":
|
|
1011
|
+
return `${status.installed} pinned, ${status.latest} available - pify install ${status.name}@${status.latest}`;
|
|
1012
|
+
case "missing":
|
|
1013
|
+
return status.name === "pi" ? "not installed" : "configured but not on disk";
|
|
1014
|
+
case "unknown":
|
|
1015
|
+
return `${status.installed ?? "?"} (registry unreachable)`;
|
|
1016
|
+
default:
|
|
1017
|
+
return `${status.installed}`;
|
|
1018
|
+
}
|
|
989
1019
|
}
|
|
990
1020
|
async function update(targets, opts) {
|
|
991
1021
|
if (isOffline()) {
|
|
@@ -1006,23 +1036,30 @@ async function update(targets, opts) {
|
|
|
1006
1036
|
}
|
|
1007
1037
|
if (opts.check) {
|
|
1008
1038
|
if (targets.length > 0) throw usageError("--check reports on everything; drop the package names.");
|
|
1009
|
-
const
|
|
1039
|
+
const report = await checkUpdates();
|
|
1010
1040
|
if (opts.json) {
|
|
1011
|
-
out(JSON.stringify({ packages:
|
|
1041
|
+
out(JSON.stringify({ pi: report.pi, packages: report.packages }, null, 2));
|
|
1012
1042
|
return 0;
|
|
1013
1043
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const width = statuses.reduce((m, s) => Math.max(m, s.name.length), 0);
|
|
1019
|
-
for (const status of statuses) {
|
|
1020
|
-
const detail = status.state === "outdated" ? `${status.installed} \u2192 ${status.latest}` : status.state === "missing" ? "configured but not on disk" : status.state === "unknown" ? `${status.installed ?? "?"} (registry unreachable)` : `${status.installed}`;
|
|
1021
|
-
out(` ${status.state.padEnd(9)}${status.name.padEnd(width + 2)}${detail}`);
|
|
1044
|
+
const rows = [report.pi, ...report.packages];
|
|
1045
|
+
const width = rows.reduce((m, s) => Math.max(m, s.name.length), 0);
|
|
1046
|
+
for (const status of rows) {
|
|
1047
|
+
out(` ${status.state.padEnd(9)}${status.name.padEnd(width + 2)}${checkDetail(status)}`);
|
|
1022
1048
|
}
|
|
1023
|
-
|
|
1049
|
+
if (report.packages.length === 0) hint("No @pify packages installed.");
|
|
1050
|
+
const outdatedPackages = report.packages.filter((s) => s.state === "outdated").length;
|
|
1051
|
+
const pinnedCount = report.packages.filter((s) => s.state === "pinned").length;
|
|
1052
|
+
const piOutdated = report.pi.state === "outdated";
|
|
1024
1053
|
out();
|
|
1025
|
-
|
|
1054
|
+
if (outdatedPackages > 0) {
|
|
1055
|
+
out(`${outdatedPackages} package(s) can be updated: pify update`);
|
|
1056
|
+
} else if (piOutdated) {
|
|
1057
|
+
out("pi can be updated: pify update pi");
|
|
1058
|
+
} else if (pinnedCount > 0) {
|
|
1059
|
+
out(`Everything current except ${pinnedCount} pinned package(s) \u2014 re-pin to advance.`);
|
|
1060
|
+
} else {
|
|
1061
|
+
out("Everything is current.");
|
|
1062
|
+
}
|
|
1026
1063
|
return 0;
|
|
1027
1064
|
}
|
|
1028
1065
|
const updateEverything = targets.length === 0;
|
|
@@ -1698,7 +1735,9 @@ function powershellScript() {
|
|
|
1698
1735
|
const commands = COMMAND_SPECS.flatMap(allNames);
|
|
1699
1736
|
const caseBlocks = COMMAND_SPECS.map((spec) => {
|
|
1700
1737
|
const words = [...positionalWords(spec), ...flagTokens(spec)].map((w) => `'${w}'`).join(", ");
|
|
1701
|
-
|
|
1738
|
+
const names = allNames(spec);
|
|
1739
|
+
const condition = names.length > 1 ? `{ $_ -in ${names.map((n) => `'${n}'`).join(", ")} }` : `'${names[0]}'`;
|
|
1740
|
+
return ` ${condition} { @(${words}) }`;
|
|
1702
1741
|
}).join("\n");
|
|
1703
1742
|
return `# PowerShell completion for pify \u2014 generated by \`pify completions powershell\`
|
|
1704
1743
|
# Install: pify completions powershell | Out-String | Invoke-Expression (add to $PROFILE)
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,128 @@ declare function planApply(profile: Profile, installedNow: Map<string, {
|
|
|
39
39
|
scope: "user" | "project";
|
|
40
40
|
}>): PlanRow[];
|
|
41
41
|
|
|
42
|
+
/** The npm package that provides the `pi` binary. */
|
|
43
|
+
declare const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
44
|
+
/** Minimum Node version required by the pi coding agent. */
|
|
45
|
+
declare const NODE_FLOOR = "22.19.0";
|
|
46
|
+
/** The pi version this CLI release was developed and tested against. */
|
|
47
|
+
declare const TESTED_PI_VERSION = "0.87.0";
|
|
48
|
+
interface PiStatus {
|
|
49
|
+
installed: boolean;
|
|
50
|
+
/** Absolute path to the `pi` launcher, when installed. */
|
|
51
|
+
binPath: string | null;
|
|
52
|
+
/** Version reported by `pi --version`, when installed. */
|
|
53
|
+
version: string | null;
|
|
54
|
+
}
|
|
55
|
+
declare function piStatus(): PiStatus;
|
|
56
|
+
/** Latest pi version from pi's own release endpoint. Null offline/on error. */
|
|
57
|
+
declare function fetchLatestPiVersion(): Promise<string | null>;
|
|
58
|
+
declare function compareSemver(a: string, b: string): number;
|
|
59
|
+
/** The official installer one-liner for this OS, for hints and --installer. */
|
|
60
|
+
declare function officialInstallerCommand(): string;
|
|
61
|
+
/**
|
|
62
|
+
* The npm install argv pi's official installers use:
|
|
63
|
+
* `--ignore-scripts` (supply-chain-safe; matches pi's self-update),
|
|
64
|
+
* `--min-release-age=0` (pi ships npm-shrinkwrap.json, so bypassing npm's
|
|
65
|
+
* release-age gate does not reopen transitive ranges; npm >= 11 only),
|
|
66
|
+
* `--no-fund --no-audit` (noise). Exported for tests.
|
|
67
|
+
*/
|
|
68
|
+
declare function npmInstallPiArgs(spec: string, npmMajor: number, prefix?: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Install pi with the same decisions as pi's official installers, minus the
|
|
71
|
+
* interactive parts. This is the one operation pi cannot do for itself —
|
|
72
|
+
* `pi update` requires a working pi.
|
|
73
|
+
*
|
|
74
|
+
* Per-OS behavior (ported from install.sh / install.ps1):
|
|
75
|
+
* - POSIX, npm prefix unwritable, no global pi there: fall back to
|
|
76
|
+
* `--prefix ~/.local` — never sudo.
|
|
77
|
+
* - POSIX, npm prefix unwritable but an old global pi lives there: stop
|
|
78
|
+
* (a ~/.local copy would be shadowed by the stale global one).
|
|
79
|
+
* - No npm but bun on PATH: `bun add -g --ignore-scripts` (documented
|
|
80
|
+
* alternative channel).
|
|
81
|
+
* - Neither: point at the official installer, which can bootstrap Node too.
|
|
82
|
+
*/
|
|
83
|
+
declare function installPi(version?: string): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Update pi. Delegates to bare `pi update` (its default target is self):
|
|
86
|
+
* pi's updater owns install-method detection, the pi.dev rename protocol,
|
|
87
|
+
* managed-install staging, and the Windows native-module quarantine.
|
|
88
|
+
* Reimplementing any of it would rot. When pi is absent, fall through to a
|
|
89
|
+
* fresh install so `npx @pify/cli update` works on a bare machine.
|
|
90
|
+
*/
|
|
91
|
+
declare function updatePi(): Promise<void>;
|
|
92
|
+
interface PiSettings {
|
|
93
|
+
packages?: Array<string | {
|
|
94
|
+
source: string;
|
|
95
|
+
[k: string]: unknown;
|
|
96
|
+
}>;
|
|
97
|
+
[k: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
/** pi's agent directory: tilde-expanded PI_CODING_AGENT_DIR or ~/.pi/agent. */
|
|
100
|
+
declare function agentDir(): string;
|
|
101
|
+
/**
|
|
102
|
+
* Read-only view of pi settings. All writes go through the pi CLI — pi owns
|
|
103
|
+
* that state (dedup rules, npm workspace, trust model), and two writers would
|
|
104
|
+
* eventually disagree. Parse failures are treated as absent (doctor warns).
|
|
105
|
+
*/
|
|
106
|
+
declare function readSettings(scope: "user" | "project"): PiSettings | null;
|
|
107
|
+
interface InstalledPifyPackage {
|
|
108
|
+
name: string;
|
|
109
|
+
scope: "user" | "project";
|
|
110
|
+
source: string;
|
|
111
|
+
pin: string | null;
|
|
112
|
+
}
|
|
113
|
+
declare function installedPifyPackages(): Map<string, InstalledPifyPackage>;
|
|
114
|
+
interface OnDiskState {
|
|
115
|
+
present: boolean;
|
|
116
|
+
version: string | null;
|
|
117
|
+
}
|
|
118
|
+
/** Version of an installed @pify package as materialized in pi's npm root. */
|
|
119
|
+
declare function installedVersionOnDisk(name: string, scope: "user" | "project"): OnDiskState;
|
|
120
|
+
|
|
121
|
+
interface UpdateStatus {
|
|
122
|
+
name: string;
|
|
123
|
+
installed: string | null;
|
|
124
|
+
latest: string | null;
|
|
125
|
+
/** The exact pin holding this package back, if any (packages only). */
|
|
126
|
+
pin: string | null;
|
|
127
|
+
/**
|
|
128
|
+
* "pinned" means behind the registry but held at an exact version, so
|
|
129
|
+
* `pify update` will not (and should not) advance it — the user must re-pin.
|
|
130
|
+
*/
|
|
131
|
+
state: "current" | "outdated" | "missing" | "unknown" | "pinned";
|
|
132
|
+
}
|
|
133
|
+
/** pi plus every installed @pify package, in one snapshot. */
|
|
134
|
+
interface UpdateReport {
|
|
135
|
+
pi: UpdateStatus;
|
|
136
|
+
packages: UpdateStatus[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Seams for the check, so a test can drive checkUpdates without a real pi
|
|
140
|
+
* install or a network round-trip. Every field defaults to the live primitive.
|
|
141
|
+
*/
|
|
142
|
+
interface CheckUpdatesDeps {
|
|
143
|
+
installed?: Map<string, InstalledPifyPackage>;
|
|
144
|
+
versionOnDisk?: (name: string, scope: "user" | "project") => OnDiskState;
|
|
145
|
+
latestPackage?: (name: string) => Promise<string | null>;
|
|
146
|
+
/** pi's own installed version; `undefined` means ask piStatus(). */
|
|
147
|
+
piVersion?: string | null;
|
|
148
|
+
latestPi?: () => Promise<string | null>;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Compare what is installed against what is published, without installing
|
|
152
|
+
* anything. Separating the question from the action is the point: "is there
|
|
153
|
+
* anything to do" is asked far more often than "do it", and it should not
|
|
154
|
+
* cost a package install to find out.
|
|
155
|
+
*
|
|
156
|
+
* pi is checked alongside the packages because `pify update` updates pi first;
|
|
157
|
+
* a check that skipped it would say "current" and then `pify update` would
|
|
158
|
+
* upgrade the agent the user was told nothing about. Its latest-version fetch
|
|
159
|
+
* rides in the SAME Promise.all as the packages', so adding it costs no serial
|
|
160
|
+
* latency.
|
|
161
|
+
*/
|
|
162
|
+
declare function checkUpdates(deps?: CheckUpdatesDeps): Promise<UpdateReport>;
|
|
163
|
+
|
|
42
164
|
interface CatalogPackage {
|
|
43
165
|
/** Short name used on the pify command line, e.g. "goal". */
|
|
44
166
|
name: string;
|
|
@@ -72,14 +194,6 @@ interface Catalog {
|
|
|
72
194
|
bundles?: CatalogBundle[];
|
|
73
195
|
}
|
|
74
196
|
declare function loadBundledCatalog(): Catalog;
|
|
75
|
-
/**
|
|
76
|
-
* SECURITY-CRITICAL. A fetched catalog is either fully valid or fully
|
|
77
|
-
* discarded: every npm name must stay inside the @pify scope so a tampered
|
|
78
|
-
* catalog cannot redirect installs outside the org (npm publish rights are a
|
|
79
|
-
* separate credential from GitHub). Combined with the install rule that only
|
|
80
|
-
* `npm:@pify/...` specs are ever delegated to pi, this bounds the blast
|
|
81
|
-
* radius of a compromised remote to "packages the org itself published".
|
|
82
|
-
*/
|
|
83
197
|
declare function validateCatalog(data: unknown): data is Catalog;
|
|
84
198
|
/** The bundle by that name, or null. Package names always win over bundles. */
|
|
85
199
|
declare function findBundle(catalog: Catalog, name: string): CatalogBundle | null;
|
|
@@ -134,85 +248,6 @@ declare function resolveInstallTarget(catalog: Catalog, input: string): InstallT
|
|
|
134
248
|
*/
|
|
135
249
|
declare function resolvePifyName(input: string): string;
|
|
136
250
|
|
|
137
|
-
/** The npm package that provides the `pi` binary. */
|
|
138
|
-
declare const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
139
|
-
/** Minimum Node version required by the pi coding agent. */
|
|
140
|
-
declare const NODE_FLOOR = "22.19.0";
|
|
141
|
-
/** The pi version this CLI release was developed and tested against. */
|
|
142
|
-
declare const TESTED_PI_VERSION = "0.84.4";
|
|
143
|
-
interface PiStatus {
|
|
144
|
-
installed: boolean;
|
|
145
|
-
/** Absolute path to the `pi` launcher, when installed. */
|
|
146
|
-
binPath: string | null;
|
|
147
|
-
/** Version reported by `pi --version`, when installed. */
|
|
148
|
-
version: string | null;
|
|
149
|
-
}
|
|
150
|
-
declare function piStatus(): PiStatus;
|
|
151
|
-
/** Latest pi version from pi's own release endpoint. Null offline/on error. */
|
|
152
|
-
declare function fetchLatestPiVersion(): Promise<string | null>;
|
|
153
|
-
declare function compareSemver(a: string, b: string): number;
|
|
154
|
-
/** The official installer one-liner for this OS, for hints and --installer. */
|
|
155
|
-
declare function officialInstallerCommand(): string;
|
|
156
|
-
/**
|
|
157
|
-
* The npm install argv pi's official installers use:
|
|
158
|
-
* `--ignore-scripts` (supply-chain-safe; matches pi's self-update),
|
|
159
|
-
* `--min-release-age=0` (pi ships npm-shrinkwrap.json, so bypassing npm's
|
|
160
|
-
* release-age gate does not reopen transitive ranges; npm >= 11 only),
|
|
161
|
-
* `--no-fund --no-audit` (noise). Exported for tests.
|
|
162
|
-
*/
|
|
163
|
-
declare function npmInstallPiArgs(spec: string, npmMajor: number, prefix?: string): string[];
|
|
164
|
-
/**
|
|
165
|
-
* Install pi with the same decisions as pi's official installers, minus the
|
|
166
|
-
* interactive parts. This is the one operation pi cannot do for itself —
|
|
167
|
-
* `pi update` requires a working pi.
|
|
168
|
-
*
|
|
169
|
-
* Per-OS behavior (ported from install.sh / install.ps1):
|
|
170
|
-
* - POSIX, npm prefix unwritable, no global pi there: fall back to
|
|
171
|
-
* `--prefix ~/.local` — never sudo.
|
|
172
|
-
* - POSIX, npm prefix unwritable but an old global pi lives there: stop
|
|
173
|
-
* (a ~/.local copy would be shadowed by the stale global one).
|
|
174
|
-
* - No npm but bun on PATH: `bun add -g --ignore-scripts` (documented
|
|
175
|
-
* alternative channel).
|
|
176
|
-
* - Neither: point at the official installer, which can bootstrap Node too.
|
|
177
|
-
*/
|
|
178
|
-
declare function installPi(version?: string): Promise<void>;
|
|
179
|
-
/**
|
|
180
|
-
* Update pi. Delegates to bare `pi update` (its default target is self):
|
|
181
|
-
* pi's updater owns install-method detection, the pi.dev rename protocol,
|
|
182
|
-
* managed-install staging, and the Windows native-module quarantine.
|
|
183
|
-
* Reimplementing any of it would rot. When pi is absent, fall through to a
|
|
184
|
-
* fresh install so `npx @pify/cli update` works on a bare machine.
|
|
185
|
-
*/
|
|
186
|
-
declare function updatePi(): Promise<void>;
|
|
187
|
-
interface PiSettings {
|
|
188
|
-
packages?: Array<string | {
|
|
189
|
-
source: string;
|
|
190
|
-
[k: string]: unknown;
|
|
191
|
-
}>;
|
|
192
|
-
[k: string]: unknown;
|
|
193
|
-
}
|
|
194
|
-
/** pi's agent directory: tilde-expanded PI_CODING_AGENT_DIR or ~/.pi/agent. */
|
|
195
|
-
declare function agentDir(): string;
|
|
196
|
-
/**
|
|
197
|
-
* Read-only view of pi settings. All writes go through the pi CLI — pi owns
|
|
198
|
-
* that state (dedup rules, npm workspace, trust model), and two writers would
|
|
199
|
-
* eventually disagree. Parse failures are treated as absent (doctor warns).
|
|
200
|
-
*/
|
|
201
|
-
declare function readSettings(scope: "user" | "project"): PiSettings | null;
|
|
202
|
-
interface InstalledPifyPackage {
|
|
203
|
-
name: string;
|
|
204
|
-
scope: "user" | "project";
|
|
205
|
-
source: string;
|
|
206
|
-
pin: string | null;
|
|
207
|
-
}
|
|
208
|
-
declare function installedPifyPackages(): Map<string, InstalledPifyPackage>;
|
|
209
|
-
interface OnDiskState {
|
|
210
|
-
present: boolean;
|
|
211
|
-
version: string | null;
|
|
212
|
-
}
|
|
213
|
-
/** Version of an installed @pify package as materialized in pi's npm root. */
|
|
214
|
-
declare function installedVersionOnDisk(name: string, scope: "user" | "project"): OnDiskState;
|
|
215
|
-
|
|
216
251
|
/** True when the user asked for offline behavior (pi's env var is honored too). */
|
|
217
252
|
declare function isOffline(): boolean;
|
|
218
253
|
declare function assertSafeArg(arg: string, what?: string): void;
|
|
@@ -299,4 +334,4 @@ declare class PifyError extends Error {
|
|
|
299
334
|
constructor(message: string, code?: ExitCodeValue, hint?: string);
|
|
300
335
|
}
|
|
301
336
|
|
|
302
|
-
export { COMMAND_SPECS, COMPLETION_SHELLS, type Catalog, type CatalogBundle, type CatalogPackage, type CommandSpec, type CompletionShell, ExitCode, type FlagSpec, GLOBAL_FLAGS, INDEX_TS, type InstallSpec, type InstallTarget, type InstalledPifyPackage, NODE_FLOOR, PI_PACKAGE, type PiSettings, type PiStatus, PifyError, type PlanRow, type Profile, type ProfileEntry, README_MD, TESTED_PI_VERSION, type TemplateVars, VERSION, agentDir, assertSafeArg, buildProfile, compareSemver, expandBundles, fetchLatestPiVersion, findBundle, generateCompletion, installPi, installedPifyPackages, installedVersionOnDisk, interpolate, isOffline, loadBundledCatalog, loadCatalog, npmInstallPiArgs, officialInstallerCommand, packageJsonTemplate, parseArgsOptionsFor, parseInstallSpec, parseProfile, piStatus, planApply, readSettings, refreshCatalog, resolveInstallTarget, resolvePifyName, updatePi, validateCatalog, which };
|
|
337
|
+
export { COMMAND_SPECS, COMPLETION_SHELLS, type Catalog, type CatalogBundle, type CatalogPackage, type CheckUpdatesDeps, type CommandSpec, type CompletionShell, ExitCode, type FlagSpec, GLOBAL_FLAGS, INDEX_TS, type InstallSpec, type InstallTarget, type InstalledPifyPackage, NODE_FLOOR, PI_PACKAGE, type PiSettings, type PiStatus, PifyError, type PlanRow, type Profile, type ProfileEntry, README_MD, TESTED_PI_VERSION, type TemplateVars, type UpdateReport, type UpdateStatus, VERSION, agentDir, assertSafeArg, buildProfile, checkUpdates, compareSemver, expandBundles, fetchLatestPiVersion, findBundle, generateCompletion, installPi, installedPifyPackages, installedVersionOnDisk, interpolate, isOffline, loadBundledCatalog, loadCatalog, npmInstallPiArgs, officialInstallerCommand, packageJsonTemplate, parseArgsOptionsFor, parseInstallSpec, parseProfile, piStatus, planApply, readSettings, refreshCatalog, resolveInstallTarget, resolvePifyName, updatePi, validateCatalog, which };
|
package/dist/index.js
CHANGED
|
@@ -142,16 +142,23 @@ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
|
142
142
|
function loadBundledCatalog() {
|
|
143
143
|
return JSON.parse(readFileSync(bundledCatalogPath(), "utf8"));
|
|
144
144
|
}
|
|
145
|
+
var CLEAN_TEXT = /^[^\x00-\x1f\x7f]{1,500}$/;
|
|
146
|
+
var CATALOG_REPO = /^https:\/\/github\.com\/pifydev\/[A-Za-z0-9._-]+$/;
|
|
147
|
+
var CATALOG_IDENT = /^[@a-z0-9-]+$/;
|
|
145
148
|
function validateCatalog(data) {
|
|
146
149
|
if (typeof data !== "object" || data === null) return false;
|
|
147
150
|
const c = data;
|
|
148
151
|
if (!Number.isInteger(c.version)) return false;
|
|
152
|
+
if (typeof c.org !== "string" || !CATALOG_IDENT.test(c.org)) return false;
|
|
153
|
+
if (typeof c.scope !== "string" || !CATALOG_IDENT.test(c.scope)) return false;
|
|
149
154
|
if (!Array.isArray(c.packages)) return false;
|
|
150
155
|
for (const entry of c.packages) {
|
|
151
156
|
if (typeof entry !== "object" || entry === null) return false;
|
|
152
157
|
const p = entry;
|
|
153
158
|
if (typeof p.name !== "string" || !/^[a-z0-9-]+$/.test(p.name)) return false;
|
|
154
159
|
if (typeof p.npm !== "string" || !p.npm.startsWith("@pify/")) return false;
|
|
160
|
+
if (typeof p.description !== "string" || !CLEAN_TEXT.test(p.description)) return false;
|
|
161
|
+
if (typeof p.repo !== "string" || !CATALOG_REPO.test(p.repo)) return false;
|
|
155
162
|
if (p.status !== "published" && p.status !== "planned") return false;
|
|
156
163
|
if (p.conflicts !== void 0) {
|
|
157
164
|
if (!Array.isArray(p.conflicts)) return false;
|
|
@@ -167,7 +174,7 @@ function validateCatalog(data) {
|
|
|
167
174
|
if (typeof entry !== "object" || entry === null) return false;
|
|
168
175
|
const b = entry;
|
|
169
176
|
if (typeof b.name !== "string" || !/^[a-z0-9-]+$/.test(b.name)) return false;
|
|
170
|
-
if (typeof b.description !== "string") return false;
|
|
177
|
+
if (typeof b.description !== "string" || !CLEAN_TEXT.test(b.description)) return false;
|
|
171
178
|
if (!Array.isArray(b.packages) || b.packages.length === 0) return false;
|
|
172
179
|
for (const member of b.packages) {
|
|
173
180
|
if (typeof member !== "string" || !known.has(member)) return false;
|
|
@@ -389,7 +396,7 @@ function hint(msg) {
|
|
|
389
396
|
// src/pi.ts
|
|
390
397
|
var PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
391
398
|
var NODE_FLOOR = "22.19.0";
|
|
392
|
-
var TESTED_PI_VERSION = "0.
|
|
399
|
+
var TESTED_PI_VERSION = "0.87.0";
|
|
393
400
|
var LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
|
|
394
401
|
function piStatus() {
|
|
395
402
|
const binPath = which("pi");
|
|
@@ -581,6 +588,25 @@ function installedPifyPackages() {
|
|
|
581
588
|
}
|
|
582
589
|
return result;
|
|
583
590
|
}
|
|
591
|
+
async function fetchLatestPackageVersion(name) {
|
|
592
|
+
if (isOffline()) return null;
|
|
593
|
+
const controller = new AbortController();
|
|
594
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
595
|
+
try {
|
|
596
|
+
const res = await fetch(`https://registry.npmjs.org/@pify/${encodeURIComponent(name)}/latest`, {
|
|
597
|
+
signal: controller.signal,
|
|
598
|
+
headers: { "user-agent": `pify/${VERSION}`, accept: "application/json" },
|
|
599
|
+
redirect: "error"
|
|
600
|
+
});
|
|
601
|
+
if (!res.ok) return null;
|
|
602
|
+
const data = await res.json();
|
|
603
|
+
return typeof data.version === "string" ? data.version : null;
|
|
604
|
+
} catch {
|
|
605
|
+
return null;
|
|
606
|
+
} finally {
|
|
607
|
+
clearTimeout(timer);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
584
610
|
function installedVersionOnDisk(name, scope) {
|
|
585
611
|
const root = scope === "user" ? join3(agentDir(), "npm") : join3(process.cwd(), ".pi", "npm");
|
|
586
612
|
try {
|
|
@@ -662,6 +688,39 @@ function planApply(profile, installedNow) {
|
|
|
662
688
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
663
689
|
}
|
|
664
690
|
|
|
691
|
+
// src/commands/update.ts
|
|
692
|
+
function piRow(installed, latest) {
|
|
693
|
+
const base = { name: "pi", installed, latest, pin: null };
|
|
694
|
+
if (installed === null) return { ...base, state: "missing" };
|
|
695
|
+
if (latest === null) return { ...base, state: "unknown" };
|
|
696
|
+
return { ...base, state: compareSemver(installed, latest) < 0 ? "outdated" : "current" };
|
|
697
|
+
}
|
|
698
|
+
async function checkUpdates(deps = {}) {
|
|
699
|
+
const installed = deps.installed ?? installedPifyPackages();
|
|
700
|
+
const versionOnDisk = deps.versionOnDisk ?? installedVersionOnDisk;
|
|
701
|
+
const latestPackage = deps.latestPackage ?? fetchLatestPackageVersion;
|
|
702
|
+
const piVersion = deps.piVersion !== void 0 ? deps.piVersion : piStatus().version;
|
|
703
|
+
const latestPi = deps.latestPi ?? fetchLatestPiVersion;
|
|
704
|
+
const names = [...installed.keys()].sort();
|
|
705
|
+
const [piLatest, ...packages] = await Promise.all([
|
|
706
|
+
latestPi(),
|
|
707
|
+
...names.map(async (name) => {
|
|
708
|
+
const entry = installed.get(name);
|
|
709
|
+
const onDisk = versionOnDisk(name, entry.scope);
|
|
710
|
+
const latest = await latestPackage(name);
|
|
711
|
+
const pin = entry.pin;
|
|
712
|
+
if (!onDisk.present) return { name, installed: null, latest, pin, state: "missing" };
|
|
713
|
+
if (!onDisk.version || !latest) {
|
|
714
|
+
return { name, installed: onDisk.version, latest, pin, state: "unknown" };
|
|
715
|
+
}
|
|
716
|
+
const behind = compareSemver(onDisk.version, latest) < 0;
|
|
717
|
+
const state = behind ? pin !== null ? "pinned" : "outdated" : "current";
|
|
718
|
+
return { name, installed: onDisk.version, latest, pin, state };
|
|
719
|
+
})
|
|
720
|
+
]);
|
|
721
|
+
return { pi: piRow(piVersion, piLatest), packages };
|
|
722
|
+
}
|
|
723
|
+
|
|
665
724
|
// src/registry.ts
|
|
666
725
|
var GLOBAL_FLAGS = [
|
|
667
726
|
{ name: "help", short: "h", description: "Show help" },
|
|
@@ -879,7 +938,9 @@ function powershellScript() {
|
|
|
879
938
|
const commands = COMMAND_SPECS.flatMap(allNames);
|
|
880
939
|
const caseBlocks = COMMAND_SPECS.map((spec) => {
|
|
881
940
|
const words = [...positionalWords(spec), ...flagTokens(spec)].map((w) => `'${w}'`).join(", ");
|
|
882
|
-
|
|
941
|
+
const names = allNames(spec);
|
|
942
|
+
const condition = names.length > 1 ? `{ $_ -in ${names.map((n) => `'${n}'`).join(", ")} }` : `'${names[0]}'`;
|
|
943
|
+
return ` ${condition} { @(${words}) }`;
|
|
883
944
|
}).join("\n");
|
|
884
945
|
return `# PowerShell completion for pify \u2014 generated by \`pify completions powershell\`
|
|
885
946
|
# Install: pify completions powershell | Out-String | Invoke-Expression (add to $PROFILE)
|
|
@@ -1050,6 +1111,7 @@ export {
|
|
|
1050
1111
|
agentDir,
|
|
1051
1112
|
assertSafeArg,
|
|
1052
1113
|
buildProfile,
|
|
1114
|
+
checkUpdates,
|
|
1053
1115
|
compareSemver,
|
|
1054
1116
|
expandBundles,
|
|
1055
1117
|
fetchLatestPiVersion,
|