@pify/cli 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/catalog.json +15 -4
- package/dist/cli.js +295 -15
- package/dist/index.d.ts +39 -6
- package/dist/index.js +96 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,10 @@ pify install goal task # short names resolve to @pify/goal, @pify/task
|
|
|
27
27
|
pify install suite # the whole suite in one go (also: core, agents)
|
|
28
28
|
pify install goal -l -a # project scope, pre-approved (CI-friendly)
|
|
29
29
|
pify remove goal
|
|
30
|
+
pify update --check # what is out of date, changing nothing (v0.4)
|
|
30
31
|
pify update # update pi + every installed @pify package
|
|
32
|
+
pify profile save # write the installed suite + versions to a file (v0.4)
|
|
33
|
+
pify profile apply f.json # diff it against this machine; --yes to apply
|
|
31
34
|
pify update pi # agent only
|
|
32
35
|
pify doctor # diagnose node / npm / pi / settings
|
|
33
36
|
pify init my-extension # scaffold a new Pi Package
|
|
@@ -101,6 +104,36 @@ npm run typecheck
|
|
|
101
104
|
|
|
102
105
|
Requires Node >= 22.19.0 (same floor as the pi coding agent). Tested with pi 0.84.4.
|
|
103
106
|
|
|
107
|
+
## Profiles (v0.4)
|
|
108
|
+
|
|
109
|
+
The suite you actually run, written down — which packages, at which versions, in which scope — so a second machine can reproduce it:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pify profile save # → pify-profile.json
|
|
113
|
+
pify profile apply pify-profile.json # prints the plan, changes nothing
|
|
114
|
+
pify profile apply pify-profile.json --yes # applies it
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Applying always shows the difference first:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
change goal 0.5.0 → 0.4.0
|
|
121
|
+
extra todo installed here, not in the profile (left alone)
|
|
122
|
+
install swarm (latest)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A package installed here but absent from the profile is reported as `extra` and **never removed**: a profile says what must be present, not what must be deleted. Every name still goes through the same catalog resolution as a typed install, so a profile file cannot reach outside the `@pify` scope. (Profiles, and the rule that applying passes through a review, are from [`pi-extmgr`](https://github.com/ayagmar/pi-extmgr); what this drops is the interactive screen — a CLI's review is a diff and an explicit flag.)
|
|
126
|
+
|
|
127
|
+
## Conflict detection (v0.4)
|
|
128
|
+
|
|
129
|
+
Some extensions cannot run beside each other: they register the same command, tool, or flag, so pi loads both and one silently wins — and which one is not something you chose. `pify doctor` now reads pi's whole package list and says so:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
warn conflicts @pify/memory + pi-memory; @pify/pretty + pi-pretty-tui register the same commands or tools - remove one
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The pairs come from the catalog, where each is taken from the package's own README. (pi-extmgr detects this at runtime through `pi.getCommands()`, which a CLI outside pi cannot call; this is the static equivalent.)
|
|
136
|
+
|
|
104
137
|
## License
|
|
105
138
|
|
|
106
139
|
MIT © [Pify maintainers](https://github.com/pifydev)
|
package/catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 18,
|
|
3
3
|
"org": "pifydev",
|
|
4
4
|
"scope": "@pify",
|
|
5
5
|
"remote": "https://raw.githubusercontent.com/pifydev/cli/main/catalog.json",
|
|
@@ -30,21 +30,32 @@
|
|
|
30
30
|
"npm": "@pify/memory",
|
|
31
31
|
"repo": "https://github.com/pifydev/memory",
|
|
32
32
|
"description": "Persistent memory across sessions: two-tier markdown, FTS5 search, secret-scanned writes, opt-in LLM consolidation",
|
|
33
|
-
"status": "published"
|
|
33
|
+
"status": "published",
|
|
34
|
+
"conflicts": [
|
|
35
|
+
"pi-memory"
|
|
36
|
+
]
|
|
34
37
|
},
|
|
35
38
|
{
|
|
36
39
|
"name": "plan-mode",
|
|
37
40
|
"npm": "@pify/plan-mode",
|
|
38
41
|
"repo": "https://github.com/pifydev/plan-mode",
|
|
39
42
|
"description": "Read-only planning with an approve-then-execute gate: enforced tool policy, tracked plan steps, saved-plan library, HTML export",
|
|
40
|
-
"status": "published"
|
|
43
|
+
"status": "published",
|
|
44
|
+
"conflicts": [
|
|
45
|
+
"@narumitw/pi-plan-mode"
|
|
46
|
+
]
|
|
41
47
|
},
|
|
42
48
|
{
|
|
43
49
|
"name": "pretty",
|
|
44
50
|
"npm": "@pify/pretty",
|
|
45
51
|
"repo": "https://github.com/pifydev/pretty",
|
|
46
52
|
"description": "Compact theme-aware rendering for built-in tools: summaries, highlighted reads, colorized diffs, configurable caps",
|
|
47
|
-
"status": "published"
|
|
53
|
+
"status": "published",
|
|
54
|
+
"conflicts": [
|
|
55
|
+
"pi-pretty",
|
|
56
|
+
"pi-pretty-tui",
|
|
57
|
+
"pi-tool-display"
|
|
58
|
+
]
|
|
48
59
|
},
|
|
49
60
|
{
|
|
50
61
|
"name": "subagent",
|
package/dist/cli.js
CHANGED
|
@@ -87,14 +87,14 @@ function runCapture(command, args) {
|
|
|
87
87
|
}
|
|
88
88
|
function runInherit(command, args) {
|
|
89
89
|
const [cmd, argv, opts] = spawnPlan(command, args);
|
|
90
|
-
return new Promise((
|
|
90
|
+
return new Promise((resolve3, reject) => {
|
|
91
91
|
const child = spawn(cmd, argv, {
|
|
92
92
|
...opts,
|
|
93
93
|
stdio: "inherit",
|
|
94
94
|
windowsHide: true
|
|
95
95
|
});
|
|
96
96
|
child.on("error", reject);
|
|
97
|
-
child.on("close", (code) =>
|
|
97
|
+
child.on("close", (code) => resolve3(code ?? 1));
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
function runOfficialInstaller(kind) {
|
|
@@ -102,10 +102,10 @@ function runOfficialInstaller(kind) {
|
|
|
102
102
|
"powershell",
|
|
103
103
|
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "irm https://pi.dev/install.ps1 | iex"]
|
|
104
104
|
] : ["sh", ["-c", "curl -fsSL https://pi.dev/install.sh | sh"]];
|
|
105
|
-
return new Promise((
|
|
105
|
+
return new Promise((resolve3, reject) => {
|
|
106
106
|
const child = spawn(cmd, argv, { stdio: "inherit", windowsHide: false });
|
|
107
107
|
child.on("error", reject);
|
|
108
|
-
child.on("close", (code) =>
|
|
108
|
+
child.on("close", (code) => resolve3(code ?? 1));
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
function which(command) {
|
|
@@ -247,7 +247,7 @@ function npmPrefixEvidence(binPath) {
|
|
|
247
247
|
const res = runCapture("npm", ["prefix", "-g"]);
|
|
248
248
|
const prefix = res.status === 0 ? res.stdout.trim() : "";
|
|
249
249
|
if (!prefix) return { managed: false, prefix: null };
|
|
250
|
-
const
|
|
250
|
+
const resolve3 = (p) => {
|
|
251
251
|
try {
|
|
252
252
|
return realpathSync(p);
|
|
253
253
|
} catch {
|
|
@@ -255,7 +255,7 @@ function npmPrefixEvidence(binPath) {
|
|
|
255
255
|
}
|
|
256
256
|
};
|
|
257
257
|
const normalize = (p) => isWindows ? p.toLowerCase() : p;
|
|
258
|
-
const managed = normalize(
|
|
258
|
+
const managed = normalize(resolve3(binPath)).startsWith(normalize(resolve3(prefix)));
|
|
259
259
|
return { managed, prefix };
|
|
260
260
|
}
|
|
261
261
|
function guardNpmManaged(binPath) {
|
|
@@ -400,6 +400,21 @@ function readSettings(scope) {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
var PIFY_SOURCE = /^npm:@pify\/([A-Za-z0-9._-]+?)(?:@(.+))?$/;
|
|
403
|
+
function configuredNpmPackages() {
|
|
404
|
+
const found = [];
|
|
405
|
+
for (const scope of ["user", "project"]) {
|
|
406
|
+
const settings = readSettings(scope);
|
|
407
|
+
for (const entry of settings?.packages ?? []) {
|
|
408
|
+
const source = typeof entry === "string" ? entry : entry.source;
|
|
409
|
+
if (typeof source !== "string" || !source.startsWith("npm:")) continue;
|
|
410
|
+
const spec = source.slice("npm:".length);
|
|
411
|
+
const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
|
|
412
|
+
const name = at > 0 ? spec.slice(0, at) : spec;
|
|
413
|
+
if (name && !found.some((f) => f.name === name && f.scope === scope)) found.push({ name, scope });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return found;
|
|
417
|
+
}
|
|
403
418
|
function installedPifyPackages() {
|
|
404
419
|
const result = /* @__PURE__ */ new Map();
|
|
405
420
|
for (const scope of ["user", "project"]) {
|
|
@@ -415,6 +430,24 @@ function installedPifyPackages() {
|
|
|
415
430
|
}
|
|
416
431
|
return result;
|
|
417
432
|
}
|
|
433
|
+
async function fetchLatestPackageVersion(name) {
|
|
434
|
+
if (isOffline()) return null;
|
|
435
|
+
try {
|
|
436
|
+
const controller = new AbortController();
|
|
437
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
438
|
+
const res = await fetch(`https://registry.npmjs.org/@pify/${encodeURIComponent(name)}/latest`, {
|
|
439
|
+
signal: controller.signal,
|
|
440
|
+
headers: { "user-agent": `pify/${VERSION}`, accept: "application/json" },
|
|
441
|
+
redirect: "error"
|
|
442
|
+
});
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
if (!res.ok) return null;
|
|
445
|
+
const data = await res.json();
|
|
446
|
+
return typeof data.version === "string" ? data.version : null;
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
418
451
|
function installedVersionOnDisk(name, scope) {
|
|
419
452
|
const root = scope === "user" ? join2(agentDir(), "npm") : join2(process.cwd(), ".pi", "npm");
|
|
420
453
|
try {
|
|
@@ -491,6 +524,12 @@ function validateCatalog(data) {
|
|
|
491
524
|
if (typeof p.name !== "string" || !/^[a-z0-9-]+$/.test(p.name)) return false;
|
|
492
525
|
if (typeof p.npm !== "string" || !p.npm.startsWith("@pify/")) return false;
|
|
493
526
|
if (p.status !== "published" && p.status !== "planned") return false;
|
|
527
|
+
if (p.conflicts !== void 0) {
|
|
528
|
+
if (!Array.isArray(p.conflicts)) return false;
|
|
529
|
+
if (!p.conflicts.every((c2) => typeof c2 === "string" && /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(c2))) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
494
533
|
}
|
|
495
534
|
if (c.bundles !== void 0) {
|
|
496
535
|
if (!Array.isArray(c.bundles)) return false;
|
|
@@ -776,7 +815,171 @@ async function remove(names, opts) {
|
|
|
776
815
|
return 0;
|
|
777
816
|
}
|
|
778
817
|
|
|
818
|
+
// src/commands/profile.ts
|
|
819
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
820
|
+
import { dirname as dirname3, resolve } from "path";
|
|
821
|
+
var PROFILE_VERSION = 1;
|
|
822
|
+
function buildProfile(now = /* @__PURE__ */ new Date()) {
|
|
823
|
+
const installed = installedPifyPackages();
|
|
824
|
+
const packages = [...installed.values()].map((entry) => ({
|
|
825
|
+
name: entry.name,
|
|
826
|
+
version: installedVersionOnDisk(entry.name, entry.scope).version,
|
|
827
|
+
scope: entry.scope
|
|
828
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
829
|
+
return { version: PROFILE_VERSION, createdAt: now.toISOString(), packages };
|
|
830
|
+
}
|
|
831
|
+
function parseProfile(raw) {
|
|
832
|
+
let parsed;
|
|
833
|
+
try {
|
|
834
|
+
parsed = JSON.parse(raw);
|
|
835
|
+
} catch {
|
|
836
|
+
throw usageError("That profile is not valid JSON.");
|
|
837
|
+
}
|
|
838
|
+
if (typeof parsed !== "object" || parsed === null) throw usageError("That profile is not an object.");
|
|
839
|
+
const candidate = parsed;
|
|
840
|
+
if (candidate.version !== PROFILE_VERSION) {
|
|
841
|
+
throw usageError(
|
|
842
|
+
`Unsupported profile version ${String(candidate.version)}.`,
|
|
843
|
+
`This pify writes and reads version ${PROFILE_VERSION}.`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if (!Array.isArray(candidate.packages)) throw usageError("That profile has no packages array.");
|
|
847
|
+
const packages = [];
|
|
848
|
+
for (const entry of candidate.packages) {
|
|
849
|
+
if (typeof entry !== "object" || entry === null) throw usageError("A profile entry is not an object.");
|
|
850
|
+
const e = entry;
|
|
851
|
+
if (typeof e.name !== "string" || !/^[a-z0-9._-]+$/.test(e.name)) {
|
|
852
|
+
throw usageError(`Invalid package name in profile: ${JSON.stringify(e.name)}`);
|
|
853
|
+
}
|
|
854
|
+
if (e.version !== null && typeof e.version !== "string") {
|
|
855
|
+
throw usageError(`Invalid version for ${e.name} in profile.`);
|
|
856
|
+
}
|
|
857
|
+
if (e.scope !== "user" && e.scope !== "project") {
|
|
858
|
+
throw usageError(`Invalid scope for ${e.name} in profile.`);
|
|
859
|
+
}
|
|
860
|
+
packages.push({ name: e.name, version: e.version, scope: e.scope });
|
|
861
|
+
}
|
|
862
|
+
return { version: PROFILE_VERSION, createdAt: String(candidate.createdAt ?? ""), packages };
|
|
863
|
+
}
|
|
864
|
+
function planApply(profile2, installedNow) {
|
|
865
|
+
const rows = [];
|
|
866
|
+
for (const entry of profile2.packages) {
|
|
867
|
+
const current = installedNow.get(entry.name);
|
|
868
|
+
if (!current) {
|
|
869
|
+
rows.push({ name: entry.name, action: "install", from: null, to: entry.version });
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const onDisk = installedVersionOnDisk(entry.name, current.scope).version;
|
|
873
|
+
if (entry.version && onDisk && entry.version !== onDisk) {
|
|
874
|
+
rows.push({ name: entry.name, action: "change", from: onDisk, to: entry.version });
|
|
875
|
+
} else {
|
|
876
|
+
rows.push({ name: entry.name, action: "keep", from: onDisk, to: entry.version });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
for (const [name] of installedNow) {
|
|
880
|
+
if (!profile2.packages.some((p) => p.name === name)) {
|
|
881
|
+
rows.push({ name, action: "extra", from: installedVersionOnDisk(name, "user").version, to: null });
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
885
|
+
}
|
|
886
|
+
function describe(row) {
|
|
887
|
+
switch (row.action) {
|
|
888
|
+
case "install":
|
|
889
|
+
return `install ${row.to ? `@ ${row.to}` : "(latest)"}`;
|
|
890
|
+
case "change":
|
|
891
|
+
return `${row.from} \u2192 ${row.to}`;
|
|
892
|
+
case "keep":
|
|
893
|
+
return `already ${row.from ?? "installed"}`;
|
|
894
|
+
case "extra":
|
|
895
|
+
return `installed here, not in the profile (left alone)`;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function profile(args, opts) {
|
|
899
|
+
const [sub, file] = args;
|
|
900
|
+
if (sub === "save") {
|
|
901
|
+
const target = resolve(file ?? "pify-profile.json");
|
|
902
|
+
const built = buildProfile();
|
|
903
|
+
if (built.packages.length === 0) {
|
|
904
|
+
warn("No @pify packages are installed; saving an empty profile.");
|
|
905
|
+
}
|
|
906
|
+
mkdirSync2(dirname3(target), { recursive: true });
|
|
907
|
+
writeFileSync2(target, `${JSON.stringify(built, null, 2)}
|
|
908
|
+
`);
|
|
909
|
+
success(`Saved ${built.packages.length} package(s) to ${target}`);
|
|
910
|
+
return 0;
|
|
911
|
+
}
|
|
912
|
+
if (sub === "apply") {
|
|
913
|
+
if (!file) throw usageError("Which profile?", "Usage: pify profile apply <file> [--yes]");
|
|
914
|
+
const target = resolve(file);
|
|
915
|
+
if (!existsSync(target)) throw notFoundError(`No profile at ${target}.`);
|
|
916
|
+
const parsed = parseProfile(readFileSync3(target, "utf8"));
|
|
917
|
+
const catalog = await loadCatalog();
|
|
918
|
+
for (const entry of parsed.packages) resolveInstallTarget(catalog, entry.name);
|
|
919
|
+
const rows = planApply(parsed, installedPifyPackages());
|
|
920
|
+
if (opts.json) {
|
|
921
|
+
out(JSON.stringify({ profile: target, plan: rows }, null, 2));
|
|
922
|
+
return 0;
|
|
923
|
+
}
|
|
924
|
+
const width = rows.reduce((m, r) => Math.max(m, r.name.length), 0);
|
|
925
|
+
out(style.bold(`Applying ${target}`));
|
|
926
|
+
out();
|
|
927
|
+
for (const row of rows) {
|
|
928
|
+
out(` ${row.action.padEnd(8)}${row.name.padEnd(width + 2)}${style.dim(describe(row))}`);
|
|
929
|
+
}
|
|
930
|
+
out();
|
|
931
|
+
const work = rows.filter((r) => r.action === "install" || r.action === "change");
|
|
932
|
+
if (work.length === 0) {
|
|
933
|
+
success("Nothing to do \u2014 this machine already matches the profile.");
|
|
934
|
+
return 0;
|
|
935
|
+
}
|
|
936
|
+
if (!opts.yes) {
|
|
937
|
+
hint(`${work.length} package(s) would change. Re-run with --yes to apply.`);
|
|
938
|
+
return 0;
|
|
939
|
+
}
|
|
940
|
+
requirePi();
|
|
941
|
+
const failed = [];
|
|
942
|
+
for (const row of work) {
|
|
943
|
+
const spec = row.to ? `npm:@pify/${row.name}@${row.to}` : `npm:@pify/${row.name}`;
|
|
944
|
+
const argv = ["install", spec];
|
|
945
|
+
step(`pi ${argv.join(" ")}`);
|
|
946
|
+
if (await delegate(argv) !== 0) failed.push(row.name);
|
|
947
|
+
}
|
|
948
|
+
if (failed.length > 0) {
|
|
949
|
+
throw new PifyError(`Failed for: ${failed.join(", ")}`, ExitCode.SUBPROCESS);
|
|
950
|
+
}
|
|
951
|
+
success(`Applied ${work.length} package(s).`);
|
|
952
|
+
return 0;
|
|
953
|
+
}
|
|
954
|
+
throw usageError(
|
|
955
|
+
`Unknown profile subcommand ${JSON.stringify(sub ?? "")}.`,
|
|
956
|
+
"Usage: pify profile save [file] | pify profile apply <file> [--yes]"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
779
960
|
// src/commands/update.ts
|
|
961
|
+
async function checkUpdates() {
|
|
962
|
+
const installed = installedPifyPackages();
|
|
963
|
+
const names = [...installed.keys()].sort();
|
|
964
|
+
const statuses = await Promise.all(
|
|
965
|
+
names.map(async (name) => {
|
|
966
|
+
const entry = installed.get(name);
|
|
967
|
+
const onDisk = installedVersionOnDisk(name, entry.scope);
|
|
968
|
+
const latest = await fetchLatestPackageVersion(name);
|
|
969
|
+
if (!onDisk.present) return { name, installed: null, latest, state: "missing" };
|
|
970
|
+
if (!onDisk.version || !latest) {
|
|
971
|
+
return { name, installed: onDisk.version, latest, state: "unknown" };
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
name,
|
|
975
|
+
installed: onDisk.version,
|
|
976
|
+
latest,
|
|
977
|
+
state: compareSemver(onDisk.version, latest) < 0 ? "outdated" : "current"
|
|
978
|
+
};
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
return statuses;
|
|
982
|
+
}
|
|
780
983
|
async function update(targets, opts) {
|
|
781
984
|
if (isOffline()) {
|
|
782
985
|
warn("PIFY_OFFLINE/PI_OFFLINE is set - updates skipped.");
|
|
@@ -794,6 +997,27 @@ async function update(targets, opts) {
|
|
|
794
997
|
}
|
|
795
998
|
return 0;
|
|
796
999
|
}
|
|
1000
|
+
if (opts.check) {
|
|
1001
|
+
if (targets.length > 0) throw usageError("--check reports on everything; drop the package names.");
|
|
1002
|
+
const statuses = await checkUpdates();
|
|
1003
|
+
if (opts.json) {
|
|
1004
|
+
out(JSON.stringify({ packages: statuses }, null, 2));
|
|
1005
|
+
return 0;
|
|
1006
|
+
}
|
|
1007
|
+
if (statuses.length === 0) {
|
|
1008
|
+
hint("No @pify packages installed.");
|
|
1009
|
+
return 0;
|
|
1010
|
+
}
|
|
1011
|
+
const width = statuses.reduce((m, s) => Math.max(m, s.name.length), 0);
|
|
1012
|
+
for (const status of statuses) {
|
|
1013
|
+
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}`;
|
|
1014
|
+
out(` ${status.state.padEnd(9)}${status.name.padEnd(width + 2)}${detail}`);
|
|
1015
|
+
}
|
|
1016
|
+
const outdated = statuses.filter((s) => s.state === "outdated").length;
|
|
1017
|
+
out();
|
|
1018
|
+
out(outdated > 0 ? `${outdated} package(s) can be updated: pify update` : "Everything is current.");
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
797
1021
|
const updateEverything = targets.length === 0;
|
|
798
1022
|
const piOnly = targets.length === 1 && (targets[0] === "pi" || targets[0] === "self");
|
|
799
1023
|
const installed = installedPifyPackages();
|
|
@@ -921,7 +1145,7 @@ async function list(opts) {
|
|
|
921
1145
|
}
|
|
922
1146
|
|
|
923
1147
|
// src/commands/doctor.ts
|
|
924
|
-
import { existsSync } from "fs";
|
|
1148
|
+
import { existsSync as existsSync2 } from "fs";
|
|
925
1149
|
async function doctor(opts) {
|
|
926
1150
|
const checks = [];
|
|
927
1151
|
const push = (id, status, detail) => checks.push({ id, status, detail });
|
|
@@ -987,7 +1211,7 @@ async function doctor(opts) {
|
|
|
987
1211
|
for (const scope of ["user", "project"]) {
|
|
988
1212
|
const id = scope === "user" ? "settings-user" : "settings-project";
|
|
989
1213
|
const path = settingsPath(scope);
|
|
990
|
-
if (!
|
|
1214
|
+
if (!existsSync2(path)) {
|
|
991
1215
|
push(id, "info", scope === "user" ? "not found (run pi once to create it)" : "none");
|
|
992
1216
|
continue;
|
|
993
1217
|
}
|
|
@@ -1015,6 +1239,26 @@ async function doctor(opts) {
|
|
|
1015
1239
|
push("pify-packages", "ok", `${installed.size} configured, all present on disk`);
|
|
1016
1240
|
}
|
|
1017
1241
|
}
|
|
1242
|
+
const catalog = await loadCatalog();
|
|
1243
|
+
const configured = configuredNpmPackages();
|
|
1244
|
+
const configuredNames2 = new Set(configured.map((p) => p.name));
|
|
1245
|
+
const clashes = [];
|
|
1246
|
+
for (const entry of catalog.packages) {
|
|
1247
|
+
if (!entry.conflicts?.length) continue;
|
|
1248
|
+
if (!configuredNames2.has(entry.npm)) continue;
|
|
1249
|
+
for (const other of entry.conflicts) {
|
|
1250
|
+
if (configuredNames2.has(other)) clashes.push(`${entry.npm} + ${other}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (clashes.length === 0) {
|
|
1254
|
+
push("conflicts", "ok", `no conflicting packages among ${configured.length} configured`);
|
|
1255
|
+
} else {
|
|
1256
|
+
push(
|
|
1257
|
+
"conflicts",
|
|
1258
|
+
"warn",
|
|
1259
|
+
`${clashes.join("; ")} register the same commands or tools - remove one (pi remove npm:<name>)`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1018
1262
|
const envVars = ["PI_OFFLINE", "PIFY_OFFLINE", "PI_CODING_AGENT_DIR", "NO_COLOR", "PIFY_CATALOG_URL"].filter((name) => process.env[name] !== void 0).map((name) => `${name}=${process.env[name]}`);
|
|
1019
1263
|
push("env", "info", envVars.length > 0 ? envVars.join(", ") : "none set");
|
|
1020
1264
|
const problems = checks.filter((c) => c.status === "fail").length;
|
|
@@ -1044,8 +1288,8 @@ async function doctor(opts) {
|
|
|
1044
1288
|
}
|
|
1045
1289
|
|
|
1046
1290
|
// src/commands/init.ts
|
|
1047
|
-
import { existsSync as
|
|
1048
|
-
import { basename, join as join4, resolve } from "path";
|
|
1291
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1292
|
+
import { basename, join as join4, resolve as resolve2 } from "path";
|
|
1049
1293
|
|
|
1050
1294
|
// src/templates.ts
|
|
1051
1295
|
function interpolate(template, vars) {
|
|
@@ -1185,8 +1429,8 @@ Tested with pi {{piVersion}}.
|
|
|
1185
1429
|
var NPM_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
1186
1430
|
async function init(dirArg, opts) {
|
|
1187
1431
|
const dir = dirArg ?? ".";
|
|
1188
|
-
const target =
|
|
1189
|
-
if (
|
|
1432
|
+
const target = resolve2(dir);
|
|
1433
|
+
if (existsSync3(target) && readdirSync(target).length > 0) {
|
|
1190
1434
|
throw new PifyError(
|
|
1191
1435
|
`Target directory ${JSON.stringify(dir)} is not empty.`,
|
|
1192
1436
|
ExitCode.FAILURE,
|
|
@@ -1209,7 +1453,7 @@ async function init(dirArg, opts) {
|
|
|
1209
1453
|
piVersion: TESTED_PI_VERSION,
|
|
1210
1454
|
dir: basename(target)
|
|
1211
1455
|
};
|
|
1212
|
-
|
|
1456
|
+
mkdirSync3(target, { recursive: true });
|
|
1213
1457
|
const files = [
|
|
1214
1458
|
["package.json", interpolate(packageJsonTemplate(name.startsWith("@")), vars)],
|
|
1215
1459
|
["index.ts", interpolate(INDEX_TS, vars)],
|
|
@@ -1218,7 +1462,7 @@ async function init(dirArg, opts) {
|
|
|
1218
1462
|
["README.md", interpolate(README_MD, vars)]
|
|
1219
1463
|
];
|
|
1220
1464
|
for (const [file, content] of files) {
|
|
1221
|
-
|
|
1465
|
+
writeFileSync3(join4(target, file), content);
|
|
1222
1466
|
}
|
|
1223
1467
|
success(`Created ${files.length} files in ${basename(target)}/`);
|
|
1224
1468
|
for (const [file] of files) out(` ${file}`);
|
|
@@ -1277,6 +1521,8 @@ var COMMAND_SPECS = [
|
|
|
1277
1521
|
positional: "updateTargets",
|
|
1278
1522
|
flags: [
|
|
1279
1523
|
{ name: "catalog", description: "Refresh the catalog cache only" },
|
|
1524
|
+
{ name: "check", description: "Report what is out of date and change nothing" },
|
|
1525
|
+
{ name: "json", description: "Machine-readable output (with --check)" },
|
|
1280
1526
|
{ name: "dry-run", description: "Print the update plan without executing" }
|
|
1281
1527
|
]
|
|
1282
1528
|
},
|
|
@@ -1294,6 +1540,16 @@ var COMMAND_SPECS = [
|
|
|
1294
1540
|
positional: "none",
|
|
1295
1541
|
flags: [{ name: "json", description: "Machine-readable output" }]
|
|
1296
1542
|
},
|
|
1543
|
+
{
|
|
1544
|
+
name: "profile",
|
|
1545
|
+
aliases: [],
|
|
1546
|
+
description: "Save the installed suite to a file, or apply one",
|
|
1547
|
+
positional: "profile",
|
|
1548
|
+
flags: [
|
|
1549
|
+
{ name: "yes", short: "y", description: "Apply the plan instead of only showing it" },
|
|
1550
|
+
{ name: "json", description: "Machine-readable plan" }
|
|
1551
|
+
]
|
|
1552
|
+
},
|
|
1297
1553
|
{
|
|
1298
1554
|
name: "init",
|
|
1299
1555
|
aliases: [],
|
|
@@ -1343,6 +1599,8 @@ function positionalWords(spec) {
|
|
|
1343
1599
|
return ["pi", ...packageNames()];
|
|
1344
1600
|
case "shell":
|
|
1345
1601
|
return [...COMPLETION_SHELLS];
|
|
1602
|
+
case "profile":
|
|
1603
|
+
return ["save", "apply"];
|
|
1346
1604
|
default:
|
|
1347
1605
|
return [];
|
|
1348
1606
|
}
|
|
@@ -1495,6 +1753,7 @@ Commands:
|
|
|
1495
1753
|
update [pi|<name...>] Update pi and installed @pify packages
|
|
1496
1754
|
list Show the @pify catalog with install state
|
|
1497
1755
|
doctor Diagnose the local pi/pify environment
|
|
1756
|
+
profile save|apply Save the installed suite to a file, or apply one
|
|
1498
1757
|
init [dir] Scaffold a new Pi Package
|
|
1499
1758
|
completions <shell> Print a completion script (bash|zsh|fish|powershell)
|
|
1500
1759
|
|
|
@@ -1557,6 +1816,21 @@ Examples:
|
|
|
1557
1816
|
|
|
1558
1817
|
# Full native experience (Windows: install.ps1, Unix: install.sh)
|
|
1559
1818
|
pify setup --installer
|
|
1819
|
+
`,
|
|
1820
|
+
profile: `Usage:
|
|
1821
|
+
pify profile save [file] # default: pify-profile.json
|
|
1822
|
+
pify profile apply <file> [--yes]
|
|
1823
|
+
|
|
1824
|
+
Write down the suite you actually run \u2014 which packages, at which versions \u2014
|
|
1825
|
+
so a second machine can reproduce it. Applying always prints the plan first
|
|
1826
|
+
and changes nothing without --yes.
|
|
1827
|
+
|
|
1828
|
+
A package installed here but absent from the profile is reported and left
|
|
1829
|
+
alone: a profile says what must be present, not what must be removed.
|
|
1830
|
+
|
|
1831
|
+
Options:
|
|
1832
|
+
-y, --yes Apply the plan instead of only showing it
|
|
1833
|
+
--json Machine-readable plan
|
|
1560
1834
|
`,
|
|
1561
1835
|
install: `Usage:
|
|
1562
1836
|
pify install <name...> [-l] [-a] [--dry-run]
|
|
@@ -1682,6 +1956,7 @@ function commandHelp(command) {
|
|
|
1682
1956
|
|
|
1683
1957
|
// src/cli.ts
|
|
1684
1958
|
var RUNNERS = {
|
|
1959
|
+
profile: (p, v) => profile(p, { yes: Boolean(v.yes), json: Boolean(v.json) }),
|
|
1685
1960
|
setup: (_p, v) => setup({
|
|
1686
1961
|
force: Boolean(v.force),
|
|
1687
1962
|
piVersion: v["pi-version"],
|
|
@@ -1697,7 +1972,12 @@ var RUNNERS = {
|
|
|
1697
1972
|
approve: Boolean(v.approve),
|
|
1698
1973
|
dryRun: Boolean(v["dry-run"])
|
|
1699
1974
|
}),
|
|
1700
|
-
update: (p, v) => update(p, {
|
|
1975
|
+
update: (p, v) => update(p, {
|
|
1976
|
+
catalogOnly: Boolean(v.catalog),
|
|
1977
|
+
dryRun: Boolean(v["dry-run"]),
|
|
1978
|
+
check: Boolean(v.check),
|
|
1979
|
+
json: Boolean(v.json)
|
|
1980
|
+
}),
|
|
1701
1981
|
list: (_p, v) => list({ json: Boolean(v.json) }),
|
|
1702
1982
|
doctor: (_p, v) => doctor({ json: Boolean(v.json) }),
|
|
1703
1983
|
init: (p, v) => init(p[0], {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
/** CLI version, read from the packaged package.json (always in the tarball). */
|
|
2
2
|
declare const VERSION: string;
|
|
3
3
|
|
|
4
|
+
interface ProfileEntry {
|
|
5
|
+
name: string;
|
|
6
|
+
/** Exact version recorded at save time, or null when it was unknown. */
|
|
7
|
+
version: string | null;
|
|
8
|
+
scope: "user" | "project";
|
|
9
|
+
}
|
|
10
|
+
interface Profile {
|
|
11
|
+
version: number;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
packages: ProfileEntry[];
|
|
14
|
+
}
|
|
15
|
+
declare function buildProfile(now?: Date): Profile;
|
|
16
|
+
/** Read a profile file, refusing anything this version cannot honour. */
|
|
17
|
+
declare function parseProfile(raw: string): Profile;
|
|
18
|
+
type PlanAction = "install" | "change" | "keep" | "extra";
|
|
19
|
+
interface PlanRow {
|
|
20
|
+
name: string;
|
|
21
|
+
action: PlanAction;
|
|
22
|
+
from: string | null;
|
|
23
|
+
to: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What applying this profile would do. Packages installed here but absent from
|
|
27
|
+
* the profile are reported as `extra` and never removed: a profile says what
|
|
28
|
+
* must be present, and deleting whatever else someone installed is not a
|
|
29
|
+
* decision this command gets to make silently.
|
|
30
|
+
*/
|
|
31
|
+
declare function planApply(profile: Profile, installedNow: Map<string, {
|
|
32
|
+
scope: "user" | "project";
|
|
33
|
+
}>): PlanRow[];
|
|
34
|
+
|
|
4
35
|
interface CatalogPackage {
|
|
5
36
|
/** Short name used on the pify command line, e.g. "goal". */
|
|
6
37
|
name: string;
|
|
@@ -10,6 +41,12 @@ interface CatalogPackage {
|
|
|
10
41
|
description: string;
|
|
11
42
|
/** "published" once it exists on the npm registry; "planned" before that. */
|
|
12
43
|
status: "published" | "planned";
|
|
44
|
+
/**
|
|
45
|
+
* npm packages that cannot run alongside this one — they register the same
|
|
46
|
+
* command, tool, or flag, so pi loads both and one silently wins. Each entry
|
|
47
|
+
* is documented in that package's own README (v0.4).
|
|
48
|
+
*/
|
|
49
|
+
conflicts?: string[];
|
|
13
50
|
}
|
|
14
51
|
/** A named set of packages installable in one go, e.g. `pify install suite`. */
|
|
15
52
|
interface CatalogBundle {
|
|
@@ -161,10 +198,6 @@ interface InstalledPifyPackage {
|
|
|
161
198
|
source: string;
|
|
162
199
|
pin: string | null;
|
|
163
200
|
}
|
|
164
|
-
/**
|
|
165
|
-
* @pify packages configured in pi's settings, both scopes, deduped by name
|
|
166
|
-
* (project wins, matching pi's own scope precedence).
|
|
167
|
-
*/
|
|
168
201
|
declare function installedPifyPackages(): Map<string, InstalledPifyPackage>;
|
|
169
202
|
interface OnDiskState {
|
|
170
203
|
present: boolean;
|
|
@@ -196,7 +229,7 @@ interface FlagSpec {
|
|
|
196
229
|
takesValue?: boolean;
|
|
197
230
|
description: string;
|
|
198
231
|
}
|
|
199
|
-
type PositionalKind = "none" | "packages" | "updateTargets" | "directory" | "shell";
|
|
232
|
+
type PositionalKind = "none" | "packages" | "updateTargets" | "directory" | "shell" | "profile";
|
|
200
233
|
interface CommandSpec {
|
|
201
234
|
name: string;
|
|
202
235
|
aliases: string[];
|
|
@@ -259,4 +292,4 @@ declare class PifyError extends Error {
|
|
|
259
292
|
constructor(message: string, code?: ExitCodeValue, hint?: string);
|
|
260
293
|
}
|
|
261
294
|
|
|
262
|
-
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, README_MD, TESTED_PI_VERSION, type TemplateVars, VERSION, agentDir, assertSafeArg, compareSemver, expandBundles, fetchLatestPiVersion, findBundle, generateCompletion, installPi, installedPifyPackages, installedVersionOnDisk, interpolate, isOffline, loadBundledCatalog, loadCatalog, npmInstallPiArgs, officialInstallerCommand, packageJsonTemplate, parseArgsOptionsFor, parseInstallSpec, piStatus, readSettings, refreshCatalog, resolveInstallTarget, resolvePifyName, updatePi, validateCatalog, which };
|
|
295
|
+
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 };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,10 @@ import { createRequire } from "module";
|
|
|
5
5
|
var require2 = createRequire(import.meta.url);
|
|
6
6
|
var VERSION = require2("../package.json").version;
|
|
7
7
|
|
|
8
|
+
// src/commands/profile.ts
|
|
9
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
10
|
+
import { dirname as dirname3, resolve } from "path";
|
|
11
|
+
|
|
8
12
|
// src/catalog.ts
|
|
9
13
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
10
14
|
import { homedir } from "os";
|
|
@@ -90,14 +94,14 @@ function runCapture(command, args) {
|
|
|
90
94
|
}
|
|
91
95
|
function runInherit(command, args) {
|
|
92
96
|
const [cmd, argv, opts] = spawnPlan(command, args);
|
|
93
|
-
return new Promise((
|
|
97
|
+
return new Promise((resolve2, reject) => {
|
|
94
98
|
const child = spawn(cmd, argv, {
|
|
95
99
|
...opts,
|
|
96
100
|
stdio: "inherit",
|
|
97
101
|
windowsHide: true
|
|
98
102
|
});
|
|
99
103
|
child.on("error", reject);
|
|
100
|
-
child.on("close", (code) =>
|
|
104
|
+
child.on("close", (code) => resolve2(code ?? 1));
|
|
101
105
|
});
|
|
102
106
|
}
|
|
103
107
|
function which(command) {
|
|
@@ -149,6 +153,12 @@ function validateCatalog(data) {
|
|
|
149
153
|
if (typeof p.name !== "string" || !/^[a-z0-9-]+$/.test(p.name)) return false;
|
|
150
154
|
if (typeof p.npm !== "string" || !p.npm.startsWith("@pify/")) return false;
|
|
151
155
|
if (p.status !== "published" && p.status !== "planned") return false;
|
|
156
|
+
if (p.conflicts !== void 0) {
|
|
157
|
+
if (!Array.isArray(p.conflicts)) return false;
|
|
158
|
+
if (!p.conflicts.every((c2) => typeof c2 === "string" && /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(c2))) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
152
162
|
}
|
|
153
163
|
if (c.bundles !== void 0) {
|
|
154
164
|
if (!Array.isArray(c.bundles)) return false;
|
|
@@ -581,6 +591,73 @@ function installedVersionOnDisk(name, scope) {
|
|
|
581
591
|
}
|
|
582
592
|
}
|
|
583
593
|
|
|
594
|
+
// src/commands/profile.ts
|
|
595
|
+
var PROFILE_VERSION = 1;
|
|
596
|
+
function buildProfile(now = /* @__PURE__ */ new Date()) {
|
|
597
|
+
const installed = installedPifyPackages();
|
|
598
|
+
const packages = [...installed.values()].map((entry) => ({
|
|
599
|
+
name: entry.name,
|
|
600
|
+
version: installedVersionOnDisk(entry.name, entry.scope).version,
|
|
601
|
+
scope: entry.scope
|
|
602
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
603
|
+
return { version: PROFILE_VERSION, createdAt: now.toISOString(), packages };
|
|
604
|
+
}
|
|
605
|
+
function parseProfile(raw) {
|
|
606
|
+
let parsed;
|
|
607
|
+
try {
|
|
608
|
+
parsed = JSON.parse(raw);
|
|
609
|
+
} catch {
|
|
610
|
+
throw usageError("That profile is not valid JSON.");
|
|
611
|
+
}
|
|
612
|
+
if (typeof parsed !== "object" || parsed === null) throw usageError("That profile is not an object.");
|
|
613
|
+
const candidate = parsed;
|
|
614
|
+
if (candidate.version !== PROFILE_VERSION) {
|
|
615
|
+
throw usageError(
|
|
616
|
+
`Unsupported profile version ${String(candidate.version)}.`,
|
|
617
|
+
`This pify writes and reads version ${PROFILE_VERSION}.`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (!Array.isArray(candidate.packages)) throw usageError("That profile has no packages array.");
|
|
621
|
+
const packages = [];
|
|
622
|
+
for (const entry of candidate.packages) {
|
|
623
|
+
if (typeof entry !== "object" || entry === null) throw usageError("A profile entry is not an object.");
|
|
624
|
+
const e = entry;
|
|
625
|
+
if (typeof e.name !== "string" || !/^[a-z0-9._-]+$/.test(e.name)) {
|
|
626
|
+
throw usageError(`Invalid package name in profile: ${JSON.stringify(e.name)}`);
|
|
627
|
+
}
|
|
628
|
+
if (e.version !== null && typeof e.version !== "string") {
|
|
629
|
+
throw usageError(`Invalid version for ${e.name} in profile.`);
|
|
630
|
+
}
|
|
631
|
+
if (e.scope !== "user" && e.scope !== "project") {
|
|
632
|
+
throw usageError(`Invalid scope for ${e.name} in profile.`);
|
|
633
|
+
}
|
|
634
|
+
packages.push({ name: e.name, version: e.version, scope: e.scope });
|
|
635
|
+
}
|
|
636
|
+
return { version: PROFILE_VERSION, createdAt: String(candidate.createdAt ?? ""), packages };
|
|
637
|
+
}
|
|
638
|
+
function planApply(profile, installedNow) {
|
|
639
|
+
const rows = [];
|
|
640
|
+
for (const entry of profile.packages) {
|
|
641
|
+
const current = installedNow.get(entry.name);
|
|
642
|
+
if (!current) {
|
|
643
|
+
rows.push({ name: entry.name, action: "install", from: null, to: entry.version });
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const onDisk = installedVersionOnDisk(entry.name, current.scope).version;
|
|
647
|
+
if (entry.version && onDisk && entry.version !== onDisk) {
|
|
648
|
+
rows.push({ name: entry.name, action: "change", from: onDisk, to: entry.version });
|
|
649
|
+
} else {
|
|
650
|
+
rows.push({ name: entry.name, action: "keep", from: onDisk, to: entry.version });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
for (const [name] of installedNow) {
|
|
654
|
+
if (!profile.packages.some((p) => p.name === name)) {
|
|
655
|
+
rows.push({ name, action: "extra", from: installedVersionOnDisk(name, "user").version, to: null });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
659
|
+
}
|
|
660
|
+
|
|
584
661
|
// src/registry.ts
|
|
585
662
|
var GLOBAL_FLAGS = [
|
|
586
663
|
{ name: "help", short: "h", description: "Show help" },
|
|
@@ -628,6 +705,8 @@ var COMMAND_SPECS = [
|
|
|
628
705
|
positional: "updateTargets",
|
|
629
706
|
flags: [
|
|
630
707
|
{ name: "catalog", description: "Refresh the catalog cache only" },
|
|
708
|
+
{ name: "check", description: "Report what is out of date and change nothing" },
|
|
709
|
+
{ name: "json", description: "Machine-readable output (with --check)" },
|
|
631
710
|
{ name: "dry-run", description: "Print the update plan without executing" }
|
|
632
711
|
]
|
|
633
712
|
},
|
|
@@ -645,6 +724,16 @@ var COMMAND_SPECS = [
|
|
|
645
724
|
positional: "none",
|
|
646
725
|
flags: [{ name: "json", description: "Machine-readable output" }]
|
|
647
726
|
},
|
|
727
|
+
{
|
|
728
|
+
name: "profile",
|
|
729
|
+
aliases: [],
|
|
730
|
+
description: "Save the installed suite to a file, or apply one",
|
|
731
|
+
positional: "profile",
|
|
732
|
+
flags: [
|
|
733
|
+
{ name: "yes", short: "y", description: "Apply the plan instead of only showing it" },
|
|
734
|
+
{ name: "json", description: "Machine-readable plan" }
|
|
735
|
+
]
|
|
736
|
+
},
|
|
648
737
|
{
|
|
649
738
|
name: "init",
|
|
650
739
|
aliases: [],
|
|
@@ -694,6 +783,8 @@ function positionalWords(spec) {
|
|
|
694
783
|
return ["pi", ...packageNames()];
|
|
695
784
|
case "shell":
|
|
696
785
|
return [...COMPLETION_SHELLS];
|
|
786
|
+
case "profile":
|
|
787
|
+
return ["save", "apply"];
|
|
697
788
|
default:
|
|
698
789
|
return [];
|
|
699
790
|
}
|
|
@@ -954,6 +1045,7 @@ export {
|
|
|
954
1045
|
VERSION,
|
|
955
1046
|
agentDir,
|
|
956
1047
|
assertSafeArg,
|
|
1048
|
+
buildProfile,
|
|
957
1049
|
compareSemver,
|
|
958
1050
|
expandBundles,
|
|
959
1051
|
fetchLatestPiVersion,
|
|
@@ -971,7 +1063,9 @@ export {
|
|
|
971
1063
|
packageJsonTemplate,
|
|
972
1064
|
parseArgsOptionsFor,
|
|
973
1065
|
parseInstallSpec,
|
|
1066
|
+
parseProfile,
|
|
974
1067
|
piStatus,
|
|
1068
|
+
planApply,
|
|
975
1069
|
readSettings,
|
|
976
1070
|
refreshCatalog,
|
|
977
1071
|
resolveInstallTarget,
|