@bigking67/pi-67 0.10.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/CHANGELOG.md +49 -0
- package/README.md +182 -0
- package/bin/pi-67.mjs +13 -0
- package/package.json +47 -0
- package/schemas/pi67-distro-manifest.schema.json +130 -0
- package/schemas/pi67-extension-registry.schema.json +110 -0
- package/schemas/pi67-publish-check.schema.json +122 -0
- package/schemas/pi67-state.schema.json +31 -0
- package/schemas/pi67-update-plan.schema.json +141 -0
- package/scripts/check.mjs +401 -0
- package/src/cli.mjs +108 -0
- package/src/commands/backups.mjs +124 -0
- package/src/commands/doctor.mjs +21 -0
- package/src/commands/extensions.mjs +184 -0
- package/src/commands/external.mjs +63 -0
- package/src/commands/install.mjs +48 -0
- package/src/commands/manifest.mjs +74 -0
- package/src/commands/publish-check.mjs +366 -0
- package/src/commands/report.mjs +20 -0
- package/src/commands/self-update.mjs +16 -0
- package/src/commands/skills.mjs +49 -0
- package/src/commands/smoke.mjs +18 -0
- package/src/commands/status.mjs +21 -0
- package/src/commands/themes.mjs +69 -0
- package/src/commands/update.mjs +138 -0
- package/src/commands/version.mjs +51 -0
- package/src/commands/xtalpi.mjs +69 -0
- package/src/data/distro-manifest.json +85 -0
- package/src/data/extension-registry.json +147 -0
- package/src/lib/args.mjs +87 -0
- package/src/lib/config-json.mjs +20 -0
- package/src/lib/distro-manifest.mjs +131 -0
- package/src/lib/distro-scripts.mjs +27 -0
- package/src/lib/extension-registry.mjs +250 -0
- package/src/lib/external-repos.mjs +71 -0
- package/src/lib/git.mjs +55 -0
- package/src/lib/npm-registry.mjs +288 -0
- package/src/lib/output.mjs +35 -0
- package/src/lib/paths.mjs +79 -0
- package/src/lib/platform.mjs +27 -0
- package/src/lib/shell-runner.mjs +40 -0
- package/src/lib/skill-policy.mjs +85 -0
- package/src/lib/state-store.mjs +31 -0
- package/src/lib/theme-policy.mjs +34 -0
- package/src/lib/update-plan.mjs +312 -0
- package/src/lib/update-safety.mjs +310 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { captureCommand } from "./shell-runner.mjs";
|
|
2
|
+
|
|
3
|
+
export function npmLatestVersion(packageName, options = {}) {
|
|
4
|
+
if (options.noRemote) {
|
|
5
|
+
return { skipped: true, ok: false, latestVersion: "", outdated: false, message: "remote checks skipped" };
|
|
6
|
+
}
|
|
7
|
+
const result = captureCommand("npm", ["view", packageName, "version", "--json"], {
|
|
8
|
+
timeoutMs: options.timeoutMs || 8000,
|
|
9
|
+
});
|
|
10
|
+
if (!result.ok) {
|
|
11
|
+
const rawMessage = result.stderr || result.error || "npm registry lookup failed";
|
|
12
|
+
return {
|
|
13
|
+
skipped: false,
|
|
14
|
+
ok: false,
|
|
15
|
+
latestVersion: "",
|
|
16
|
+
outdated: false,
|
|
17
|
+
message: rawMessage.includes("E404") ? "not published on npm registry yet" : compactMessage(rawMessage),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const latestVersion = parseNpmVersion(result.stdout);
|
|
21
|
+
return {
|
|
22
|
+
skipped: false,
|
|
23
|
+
ok: Boolean(latestVersion),
|
|
24
|
+
latestVersion,
|
|
25
|
+
outdated: latestVersion ? compareSemver(options.currentVersion || "", latestVersion) < 0 : false,
|
|
26
|
+
message: latestVersion ? "" : "npm registry returned no version",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function npmPackageScopeStatus(packageName, options = {}) {
|
|
31
|
+
const scope = packageScope(packageName);
|
|
32
|
+
if (!scope) {
|
|
33
|
+
return {
|
|
34
|
+
skipped: false,
|
|
35
|
+
ok: true,
|
|
36
|
+
blocking: false,
|
|
37
|
+
scoped: false,
|
|
38
|
+
scope: "",
|
|
39
|
+
code: "unscoped",
|
|
40
|
+
message: "unscoped npm package",
|
|
41
|
+
probes: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (options.noRemote) {
|
|
45
|
+
return {
|
|
46
|
+
skipped: true,
|
|
47
|
+
ok: false,
|
|
48
|
+
blocking: false,
|
|
49
|
+
scoped: true,
|
|
50
|
+
scope,
|
|
51
|
+
code: "skipped",
|
|
52
|
+
message: "remote scope check skipped",
|
|
53
|
+
probes: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const scopeName = scope.slice(1);
|
|
58
|
+
const accessResult = captureCommand("npm", ["access", "list", "packages", scope, "--json"], {
|
|
59
|
+
timeoutMs: options.timeoutMs || 8000,
|
|
60
|
+
});
|
|
61
|
+
const accessProbe = npmProbe("npm access list packages", accessResult);
|
|
62
|
+
if (accessResult.ok) {
|
|
63
|
+
const packages = parseJsonObject(accessResult.stdout);
|
|
64
|
+
const packageCount = packages ? Object.keys(packages).length : 0;
|
|
65
|
+
return {
|
|
66
|
+
skipped: false,
|
|
67
|
+
ok: true,
|
|
68
|
+
blocking: false,
|
|
69
|
+
scoped: true,
|
|
70
|
+
scope,
|
|
71
|
+
code: "scope_visible",
|
|
72
|
+
message: `npm scope ${scope} is visible via npm access list packages (${packageCount} packages)`,
|
|
73
|
+
probes: [accessProbe],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const accessMessage = accessResult.stderr || accessResult.stdout || accessResult.error || "npm scope lookup failed";
|
|
78
|
+
const missing = isMissingNpmResource(accessMessage);
|
|
79
|
+
const forbidden = isForbiddenNpmResource(accessMessage);
|
|
80
|
+
if (missing || forbidden) {
|
|
81
|
+
return {
|
|
82
|
+
skipped: false,
|
|
83
|
+
ok: false,
|
|
84
|
+
blocking: true,
|
|
85
|
+
scoped: true,
|
|
86
|
+
scope,
|
|
87
|
+
code: missing ? "scope_missing" : "scope_forbidden",
|
|
88
|
+
message: missing
|
|
89
|
+
? `npm scope ${scope} was not found; create/claim the npm user or org before publishing`
|
|
90
|
+
: `npm scope ${scope} is not accessible to this publisher; verify npm ownership, collaborators, or trusted publisher setup`,
|
|
91
|
+
probes: [accessProbe],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const orgResult = captureCommand("npm", ["org", "ls", scopeName, "--json"], {
|
|
96
|
+
timeoutMs: options.timeoutMs || 8000,
|
|
97
|
+
});
|
|
98
|
+
const orgProbe = npmProbe("npm org ls", orgResult);
|
|
99
|
+
if (orgResult.ok) {
|
|
100
|
+
const orgPackages = parseJsonObject(orgResult.stdout);
|
|
101
|
+
const packageCount = orgPackages ? Object.keys(orgPackages).length : 0;
|
|
102
|
+
return {
|
|
103
|
+
skipped: false,
|
|
104
|
+
ok: true,
|
|
105
|
+
blocking: false,
|
|
106
|
+
scoped: true,
|
|
107
|
+
scope,
|
|
108
|
+
code: "scope_org_visible_access_probe_inconclusive",
|
|
109
|
+
message: `npm scope ${scope} is visible via npm org ls; package-access probe was inconclusive (${packageCount} org entries)`,
|
|
110
|
+
probes: [accessProbe, orgProbe],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const orgMessage = orgResult.stderr || orgResult.stdout || orgResult.error || "npm scope lookup failed";
|
|
115
|
+
const orgMissing = isMissingNpmResource(orgMessage);
|
|
116
|
+
return {
|
|
117
|
+
skipped: false,
|
|
118
|
+
ok: false,
|
|
119
|
+
blocking: orgMissing,
|
|
120
|
+
scoped: true,
|
|
121
|
+
scope,
|
|
122
|
+
code: orgMissing ? "scope_missing" : "scope_probe_failed",
|
|
123
|
+
message: orgMissing
|
|
124
|
+
? `npm scope ${scope} was not found; create/claim the npm user or org before publishing`
|
|
125
|
+
: compactMessage(accessMessage),
|
|
126
|
+
probes: [accessProbe, orgProbe],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function npmPublishTargetStatus(packageName, options = {}) {
|
|
131
|
+
if (options.noRemote) {
|
|
132
|
+
return {
|
|
133
|
+
skipped: true,
|
|
134
|
+
ok: false,
|
|
135
|
+
blocking: false,
|
|
136
|
+
packageName,
|
|
137
|
+
firstPublish: false,
|
|
138
|
+
allowFirstPublish: Boolean(options.allowFirstPublish),
|
|
139
|
+
code: "skipped",
|
|
140
|
+
message: "remote publish target check skipped",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const registry = options.registry || {};
|
|
145
|
+
const scope = options.scope || npmPackageScopeStatus(packageName, options);
|
|
146
|
+
const registryLookupFailed = !registry.skipped && !registry.ok && registry.message !== "not published on npm registry yet";
|
|
147
|
+
const firstPublish = !registry.skipped && !registry.ok && registry.message === "not published on npm registry yet";
|
|
148
|
+
|
|
149
|
+
if (registryLookupFailed) {
|
|
150
|
+
return {
|
|
151
|
+
skipped: false,
|
|
152
|
+
ok: false,
|
|
153
|
+
blocking: true,
|
|
154
|
+
packageName,
|
|
155
|
+
firstPublish: false,
|
|
156
|
+
allowFirstPublish: Boolean(options.allowFirstPublish),
|
|
157
|
+
code: "registry_lookup_failed",
|
|
158
|
+
message: `npm registry lookup failed for ${packageName}: ${compactMessage(registry.message)}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (scope.blocking && firstPublish && options.allowFirstPublish && scope.code === "scope_missing") {
|
|
163
|
+
return {
|
|
164
|
+
skipped: false,
|
|
165
|
+
ok: true,
|
|
166
|
+
blocking: false,
|
|
167
|
+
packageName,
|
|
168
|
+
firstPublish: true,
|
|
169
|
+
allowFirstPublish: true,
|
|
170
|
+
code: "first_publish_scope_probe_confirmed",
|
|
171
|
+
message: `first publish for ${packageName} explicitly confirmed; npm publish remains the authority for new-scope write permission`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (scope.blocking) {
|
|
176
|
+
return {
|
|
177
|
+
skipped: false,
|
|
178
|
+
ok: false,
|
|
179
|
+
blocking: true,
|
|
180
|
+
packageName,
|
|
181
|
+
firstPublish,
|
|
182
|
+
allowFirstPublish: Boolean(options.allowFirstPublish),
|
|
183
|
+
code: scope.code || "scope_blocked",
|
|
184
|
+
message: scope.message,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (firstPublish && !options.allowFirstPublish) {
|
|
189
|
+
return {
|
|
190
|
+
skipped: false,
|
|
191
|
+
ok: false,
|
|
192
|
+
blocking: true,
|
|
193
|
+
packageName,
|
|
194
|
+
firstPublish: true,
|
|
195
|
+
allowFirstPublish: false,
|
|
196
|
+
code: "first_publish_requires_confirmation",
|
|
197
|
+
message: `first publish for ${packageName} requires explicit --allow-first-publish after npm scope and Trusted Publisher are configured`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (firstPublish) {
|
|
202
|
+
return {
|
|
203
|
+
skipped: false,
|
|
204
|
+
ok: true,
|
|
205
|
+
blocking: false,
|
|
206
|
+
packageName,
|
|
207
|
+
firstPublish: true,
|
|
208
|
+
allowFirstPublish: true,
|
|
209
|
+
code: "first_publish_confirmed",
|
|
210
|
+
message: `first publish for ${packageName} explicitly confirmed; npm will validate Trusted Publisher only during npm publish`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
skipped: false,
|
|
216
|
+
ok: true,
|
|
217
|
+
blocking: false,
|
|
218
|
+
packageName,
|
|
219
|
+
firstPublish: false,
|
|
220
|
+
allowFirstPublish: Boolean(options.allowFirstPublish),
|
|
221
|
+
code: "published_package_target_ready",
|
|
222
|
+
message: `npm publish target ${packageName} is already present on the registry`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function packageScope(packageName) {
|
|
227
|
+
const match = String(packageName || "").match(/^(@[^/]+)\//);
|
|
228
|
+
return match ? match[1] : "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseNpmVersion(value) {
|
|
232
|
+
const trimmed = String(value || "").trim();
|
|
233
|
+
if (!trimmed) return "";
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(trimmed);
|
|
236
|
+
return typeof parsed === "string" ? parsed : "";
|
|
237
|
+
} catch {
|
|
238
|
+
return trimmed.replace(/^"|"$/g, "");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseJsonObject(value) {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(String(value || "").trim() || "{}");
|
|
245
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function npmProbe(name, result) {
|
|
252
|
+
return {
|
|
253
|
+
name,
|
|
254
|
+
ok: Boolean(result.ok),
|
|
255
|
+
status: typeof result.status === "number" ? result.status : null,
|
|
256
|
+
message: result.ok ? "ok" : compactMessage(result.stderr || result.stdout || result.error || "failed"),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isMissingNpmResource(message) {
|
|
261
|
+
const value = String(message || "");
|
|
262
|
+
return value.includes("Scope not found") || value.includes("E404") || value.includes("Not Found");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isForbiddenNpmResource(message) {
|
|
266
|
+
const value = String(message || "");
|
|
267
|
+
return value.includes("E403") || value.includes("Forbidden");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function compareSemver(left, right) {
|
|
271
|
+
const a = semverParts(left);
|
|
272
|
+
const b = semverParts(right);
|
|
273
|
+
for (let index = 0; index < 3; index += 1) {
|
|
274
|
+
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
275
|
+
}
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function semverParts(value) {
|
|
280
|
+
const match = String(value || "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
281
|
+
if (!match) return [0, 0, 0];
|
|
282
|
+
return match.slice(1).map((part) => Number(part));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function compactMessage(value) {
|
|
286
|
+
const line = String(value || "").split(/\r?\n/).find((item) => item.trim()) || "";
|
|
287
|
+
return line.trim().slice(0, 240);
|
|
288
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
constructor(message, exitCode = 1) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "CliError";
|
|
5
|
+
this.exitCode = exitCode;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function printJson(value) {
|
|
10
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function section(title) {
|
|
14
|
+
process.stdout.write(`\n${title}\n`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function pass(message) {
|
|
18
|
+
process.stdout.write(` PASS ${message}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function warn(message) {
|
|
22
|
+
process.stdout.write(` WARN ${message}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function info(message) {
|
|
26
|
+
process.stdout.write(` INFO ${message}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function fail(message) {
|
|
30
|
+
process.stdout.write(` FAIL ${message}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function keyValue(label, value) {
|
|
34
|
+
process.stdout.write(`${label.padEnd(18)}: ${value ?? ""}\n`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_REPO_URL = "https://github.com/bigKING67/pi-67.git";
|
|
7
|
+
|
|
8
|
+
export function packageRoot() {
|
|
9
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readCliPackageJson() {
|
|
13
|
+
const file = path.join(packageRoot(), "package.json");
|
|
14
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function expandHome(input) {
|
|
18
|
+
if (!input) return input;
|
|
19
|
+
if (input === "~") return os.homedir();
|
|
20
|
+
if (input.startsWith("~/") || input.startsWith("~\\")) {
|
|
21
|
+
return path.join(os.homedir(), input.slice(2));
|
|
22
|
+
}
|
|
23
|
+
return input;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveContext(globalOptions = {}) {
|
|
27
|
+
const agentDir = path.resolve(expandHome(
|
|
28
|
+
globalOptions.agentDir ||
|
|
29
|
+
process.env.PI67_AGENT_DIR ||
|
|
30
|
+
process.env.PI_CODING_AGENT_DIR ||
|
|
31
|
+
path.join(os.homedir(), ".pi", "agent"),
|
|
32
|
+
));
|
|
33
|
+
const repoRoot = path.resolve(expandHome(globalOptions.repoRoot || agentDir));
|
|
34
|
+
const skillsDir = path.resolve(expandHome(
|
|
35
|
+
globalOptions.skillsDir ||
|
|
36
|
+
process.env.PI67_SKILLS_DIR ||
|
|
37
|
+
path.join(os.homedir(), ".agents", "skills"),
|
|
38
|
+
));
|
|
39
|
+
const packagesDir = path.resolve(expandHome(
|
|
40
|
+
globalOptions.packagesDir ||
|
|
41
|
+
process.env.PI67_PACKAGES_DIR ||
|
|
42
|
+
path.join(os.homedir(), ".agents", "packages"),
|
|
43
|
+
));
|
|
44
|
+
const stateDir = path.join(os.homedir(), ".pi", "pi67");
|
|
45
|
+
return {
|
|
46
|
+
agentDir,
|
|
47
|
+
repoRoot,
|
|
48
|
+
skillsDir,
|
|
49
|
+
packagesDir,
|
|
50
|
+
stateDir,
|
|
51
|
+
reportsDir: path.join(stateDir, "reports"),
|
|
52
|
+
logsDir: path.join(stateDir, "logs"),
|
|
53
|
+
json: Boolean(globalOptions.json),
|
|
54
|
+
dryRun: Boolean(globalOptions.dryRun),
|
|
55
|
+
yes: Boolean(globalOptions.yes),
|
|
56
|
+
noRemote: Boolean(globalOptions.noRemote),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function scriptPath(ctx, scriptName) {
|
|
61
|
+
return path.join(ctx.repoRoot, "scripts", scriptName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function pathExists(file) {
|
|
65
|
+
try {
|
|
66
|
+
fs.accessSync(file);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readTextIfExists(file) {
|
|
74
|
+
try {
|
|
75
|
+
return fs.readFileSync(file, "utf8");
|
|
76
|
+
} catch {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
export function isWindows() {
|
|
5
|
+
return process.platform === "win32";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function platformName() {
|
|
9
|
+
return `${process.platform}-${os.arch()}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function commandExists(name) {
|
|
13
|
+
const result = isWindows()
|
|
14
|
+
? spawnSync("where.exe", [name], { encoding: "utf8" })
|
|
15
|
+
: spawnSync("sh", ["-lc", `command -v ${shellQuote(name)}`], { encoding: "utf8" });
|
|
16
|
+
return result.status === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function findPowerShell() {
|
|
20
|
+
if (commandExists("pwsh")) return "pwsh";
|
|
21
|
+
if (commandExists("powershell")) return "powershell";
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function shellQuote(value) {
|
|
26
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { CliError, info } from "./output.mjs";
|
|
3
|
+
|
|
4
|
+
export function runCommand(command, args = [], options = {}) {
|
|
5
|
+
const cwd = options.cwd || process.cwd();
|
|
6
|
+
if (options.dryRun) {
|
|
7
|
+
info(`DRY-RUN (${cwd}) ${[command, ...args].join(" ")}`);
|
|
8
|
+
return { status: 0, stdout: "", stderr: "" };
|
|
9
|
+
}
|
|
10
|
+
const result = spawnSync(command, args, {
|
|
11
|
+
cwd,
|
|
12
|
+
stdio: options.stdio || "inherit",
|
|
13
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
timeout: options.timeoutMs,
|
|
16
|
+
});
|
|
17
|
+
if (result.error) {
|
|
18
|
+
throw new CliError(`failed to run ${command}: ${result.error.message}`);
|
|
19
|
+
}
|
|
20
|
+
if (result.status !== 0) {
|
|
21
|
+
throw new CliError(`${command} exited with ${result.status}`, result.status || 1);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function captureCommand(command, args = [], options = {}) {
|
|
27
|
+
const result = spawnSync(command, args, {
|
|
28
|
+
cwd: options.cwd || process.cwd(),
|
|
29
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
timeout: options.timeoutMs,
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
ok: result.status === 0 && !result.error,
|
|
35
|
+
status: result.status,
|
|
36
|
+
stdout: result.stdout || "",
|
|
37
|
+
stderr: result.stderr || "",
|
|
38
|
+
error: result.error ? result.error.message : "",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function inventorySkills(ctx) {
|
|
6
|
+
const sourceRoot = path.join(ctx.repoRoot, "shared-skills");
|
|
7
|
+
const sourceNames = listSkillDirs(sourceRoot);
|
|
8
|
+
const entries = sourceNames.map((name) => {
|
|
9
|
+
const source = path.join(sourceRoot, name);
|
|
10
|
+
const target = path.join(ctx.skillsDir, name);
|
|
11
|
+
const sourceHash = hashDir(source);
|
|
12
|
+
const targetExists = fs.existsSync(target);
|
|
13
|
+
const targetHash = targetExists ? hashDir(target) : "";
|
|
14
|
+
const identical = targetExists && sourceHash === targetHash;
|
|
15
|
+
const conflict = targetExists && !identical;
|
|
16
|
+
return { name, source, target, sourceHash, targetExists, targetHash, identical, conflict };
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
schema: "pi67.skills-inventory.v1",
|
|
20
|
+
sourceRoot,
|
|
21
|
+
skillsDir: ctx.skillsDir,
|
|
22
|
+
summary: {
|
|
23
|
+
source: entries.length,
|
|
24
|
+
missing: entries.filter((entry) => !entry.targetExists).length,
|
|
25
|
+
identical: entries.filter((entry) => entry.identical).length,
|
|
26
|
+
conflicts: entries.filter((entry) => entry.conflict).length,
|
|
27
|
+
},
|
|
28
|
+
entries,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function syncSkills(ctx, { dryRun = false } = {}) {
|
|
33
|
+
const inventory = inventorySkills(ctx);
|
|
34
|
+
const actions = [];
|
|
35
|
+
fs.mkdirSync(ctx.skillsDir, { recursive: true });
|
|
36
|
+
for (const entry of inventory.entries) {
|
|
37
|
+
if (entry.identical) {
|
|
38
|
+
actions.push({ name: entry.name, action: "skip", reason: "identical" });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (entry.conflict) {
|
|
42
|
+
actions.push({ name: entry.name, action: "warn", reason: "target differs; preserved" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
actions.push({ name: entry.name, action: dryRun ? "copy-dry-run" : "copy", reason: "missing" });
|
|
46
|
+
if (!dryRun) {
|
|
47
|
+
fs.cpSync(entry.source, entry.target, { recursive: true, errorOnExist: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { ...inventory, actions };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listSkillDirs(root) {
|
|
54
|
+
if (!fs.existsSync(root)) return [];
|
|
55
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
56
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(root, entry.name, "SKILL.md")))
|
|
57
|
+
.map((entry) => entry.name)
|
|
58
|
+
.sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hashDir(root) {
|
|
62
|
+
if (!fs.existsSync(root)) return "";
|
|
63
|
+
const hash = crypto.createHash("sha256");
|
|
64
|
+
const files = [];
|
|
65
|
+
walk(root, files);
|
|
66
|
+
for (const file of files.sort()) {
|
|
67
|
+
const rel = path.relative(root, file).replace(/\\/g, "/");
|
|
68
|
+
hash.update(rel);
|
|
69
|
+
hash.update("\0");
|
|
70
|
+
hash.update(fs.readFileSync(file));
|
|
71
|
+
hash.update("\0");
|
|
72
|
+
}
|
|
73
|
+
return hash.digest("hex");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function walk(dir, files) {
|
|
77
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
78
|
+
const full = path.join(dir, entry.name);
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
walk(full, files);
|
|
81
|
+
} else if (entry.isFile()) {
|
|
82
|
+
files.push(full);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { gitStatus } from "./git.mjs";
|
|
4
|
+
import { currentTheme } from "./theme-policy.mjs";
|
|
5
|
+
import { readJsonFileIfExists, writeJsonAtomic } from "./config-json.mjs";
|
|
6
|
+
import { readCliPackageJson, readTextIfExists } from "./paths.mjs";
|
|
7
|
+
|
|
8
|
+
export function writeState(ctx, operation) {
|
|
9
|
+
const pkg = readCliPackageJson();
|
|
10
|
+
const git = gitStatus(ctx.repoRoot);
|
|
11
|
+
const settings = readJsonFileIfExists(path.join(ctx.agentDir, "settings.json")) || {};
|
|
12
|
+
const state = {
|
|
13
|
+
schema: "pi67.state.v1",
|
|
14
|
+
updatedAt: new Date().toISOString(),
|
|
15
|
+
operation,
|
|
16
|
+
managerPackage: pkg.name,
|
|
17
|
+
managerVersion: pkg.version,
|
|
18
|
+
agentDir: ctx.agentDir,
|
|
19
|
+
repoRoot: ctx.repoRoot,
|
|
20
|
+
skillsDir: ctx.skillsDir,
|
|
21
|
+
packagesDir: ctx.packagesDir,
|
|
22
|
+
lastKnownVersion: readTextIfExists(path.join(ctx.repoRoot, "VERSION")).trim(),
|
|
23
|
+
lastKnownCommit: git.commit || "",
|
|
24
|
+
lastTheme: currentTheme(ctx),
|
|
25
|
+
lastProvider: settings.defaultProvider || "",
|
|
26
|
+
lastModel: settings.defaultModel || "",
|
|
27
|
+
};
|
|
28
|
+
fs.mkdirSync(ctx.stateDir, { recursive: true });
|
|
29
|
+
writeJsonAtomic(path.join(ctx.stateDir, "state.json"), state);
|
|
30
|
+
return state;
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJsonFileIfExists } from "./config-json.mjs";
|
|
4
|
+
|
|
5
|
+
const THEME_PACKAGE = "@victor-software-house/pi-curated-themes";
|
|
6
|
+
|
|
7
|
+
export function currentTheme(ctx) {
|
|
8
|
+
const settings = readJsonFileIfExists(path.join(ctx.agentDir, "settings.json")) || {};
|
|
9
|
+
return settings.theme || "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function themeDirs(ctx) {
|
|
13
|
+
return [
|
|
14
|
+
path.join(ctx.agentDir, "npm", "node_modules", THEME_PACKAGE, "themes"),
|
|
15
|
+
path.join(ctx.repoRoot, "npm", "node_modules", THEME_PACKAGE, "themes"),
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function listThemes(ctx) {
|
|
20
|
+
const names = new Set();
|
|
21
|
+
for (const dir of themeDirs(ctx)) {
|
|
22
|
+
if (!fs.existsSync(dir)) continue;
|
|
23
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
25
|
+
names.add(entry.name.replace(/\.json$/, ""));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...names].sort();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hasTheme(ctx, name) {
|
|
33
|
+
return listThemes(ctx).includes(name);
|
|
34
|
+
}
|