superpowers-manager 0.4.0 → 0.5.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 +62 -533
- package/dist/adapter.js +8 -4
- package/dist/artifact-tree.js +110 -0
- package/dist/atomic.js +103 -7
- package/dist/cli-arguments.js +37 -0
- package/dist/cli.js +48 -11
- package/dist/codex-compatibility.js +126 -0
- package/dist/codex-conflicts.js +99 -0
- package/dist/codex-harness.js +51 -1
- package/dist/codex-json.js +7 -0
- package/dist/codex-prepare.js +69 -13
- package/dist/codex-presentation.js +24 -1
- package/dist/commands/install.js +115 -16
- package/dist/commands/mutation.js +71 -0
- package/dist/commands/prepare.js +19 -13
- package/dist/commands/probe.js +160 -10
- package/dist/commands/uninstall.js +6 -2
- package/dist/commands/update.js +5 -1
- package/dist/generated-plugin.js +3 -3
- package/dist/harness-compatibility.js +13 -0
- package/dist/manifest-overlay.js +1 -1
- package/dist/pi-compatibility.js +73 -0
- package/dist/pi-harness.js +63 -0
- package/dist/pi-install.js +495 -0
- package/dist/pi-native.js +81 -0
- package/dist/pi-package.js +153 -0
- package/dist/pi-paths.js +30 -0
- package/dist/pi-prepare.js +111 -0
- package/dist/pi-presentation.js +67 -0
- package/dist/pi-settings.js +171 -0
- package/dist/pi-shared-skills.js +413 -0
- package/dist/pi-state.js +311 -0
- package/dist/resource-lock.js +265 -0
- package/dist/safe-path.js +21 -5
- package/dist/upstream-workspace.js +8 -0
- package/dist/validator.js +2 -1
- package/package.json +11 -8
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { readdir, readFile, readlink, realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { classifyPathNoFollow } from "./safe-path.js";
|
|
5
|
+
const PI_IGNORE_FILES = [".gitignore", ".ignore", ".fdignore"];
|
|
6
|
+
function toPosixPath(path) {
|
|
7
|
+
return path.split(sep).join("/");
|
|
8
|
+
}
|
|
9
|
+
async function ignoreControlState(directory) {
|
|
10
|
+
for (const filename of PI_IGNORE_FILES) {
|
|
11
|
+
const path = join(directory, filename);
|
|
12
|
+
let kind;
|
|
13
|
+
try {
|
|
14
|
+
kind = await classifyPathNoFollow(path);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "indeterminate";
|
|
18
|
+
}
|
|
19
|
+
if (kind === "missing")
|
|
20
|
+
continue;
|
|
21
|
+
if (kind !== "regular-file")
|
|
22
|
+
return "indeterminate";
|
|
23
|
+
let contents;
|
|
24
|
+
try {
|
|
25
|
+
contents = await readFile(path, "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return "indeterminate";
|
|
29
|
+
}
|
|
30
|
+
const meaningful = contents.split(/\r?\n/u).some((line) => {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
return (trimmed.length > 0 &&
|
|
33
|
+
!(trimmed.startsWith("#") && !trimmed.startsWith("\\#")));
|
|
34
|
+
});
|
|
35
|
+
if (meaningful)
|
|
36
|
+
return "indeterminate";
|
|
37
|
+
}
|
|
38
|
+
return "none";
|
|
39
|
+
}
|
|
40
|
+
async function collectSharedSkillDirectory(directory) {
|
|
41
|
+
if ((await ignoreControlState(directory)) === "indeterminate") {
|
|
42
|
+
return { kind: "indeterminate" };
|
|
43
|
+
}
|
|
44
|
+
const declaredSkill = join(directory, "SKILL.md");
|
|
45
|
+
let declaredKind;
|
|
46
|
+
try {
|
|
47
|
+
declaredKind = await classifyPathNoFollow(declaredSkill);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { kind: "indeterminate" };
|
|
51
|
+
}
|
|
52
|
+
if (declaredKind === "regular-file") {
|
|
53
|
+
return { kind: "candidates", paths: [declaredSkill] };
|
|
54
|
+
}
|
|
55
|
+
if (declaredKind !== "missing")
|
|
56
|
+
return { kind: "indeterminate" };
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { kind: "indeterminate" };
|
|
63
|
+
}
|
|
64
|
+
const candidates = [];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
67
|
+
continue;
|
|
68
|
+
if (entry.name === "SKILL.md")
|
|
69
|
+
return { kind: "indeterminate" };
|
|
70
|
+
const path = join(directory, entry.name);
|
|
71
|
+
let kind;
|
|
72
|
+
try {
|
|
73
|
+
kind = await classifyPathNoFollow(path);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { kind: "indeterminate" };
|
|
77
|
+
}
|
|
78
|
+
if (kind === "symlink" || kind === "missing") {
|
|
79
|
+
return { kind: "indeterminate" };
|
|
80
|
+
}
|
|
81
|
+
if (kind === "directory") {
|
|
82
|
+
const nested = await collectSharedSkillDirectory(path);
|
|
83
|
+
if (nested.kind === "indeterminate")
|
|
84
|
+
return nested;
|
|
85
|
+
if (nested.kind === "candidates")
|
|
86
|
+
candidates.push(...nested.paths);
|
|
87
|
+
}
|
|
88
|
+
else if (kind === "regular-file" && entry.name.endsWith(".md")) {
|
|
89
|
+
candidates.push(path);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { kind: "candidates", paths: candidates };
|
|
93
|
+
}
|
|
94
|
+
async function collectSharedPiSkills(paths) {
|
|
95
|
+
const agentsRoot = join(paths.homeDir, ".agents");
|
|
96
|
+
const skillsRoot = join(agentsRoot, "skills");
|
|
97
|
+
const sharedRoot = join(skillsRoot, "superpowers");
|
|
98
|
+
for (const directory of [agentsRoot, skillsRoot]) {
|
|
99
|
+
let kind;
|
|
100
|
+
try {
|
|
101
|
+
kind = await classifyPathNoFollow(directory);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { kind: "indeterminate" };
|
|
105
|
+
}
|
|
106
|
+
if (kind === "missing")
|
|
107
|
+
return { kind: "none" };
|
|
108
|
+
if (kind !== "directory")
|
|
109
|
+
return { kind: "indeterminate" };
|
|
110
|
+
}
|
|
111
|
+
let sharedRootKind;
|
|
112
|
+
try {
|
|
113
|
+
sharedRootKind = await classifyPathNoFollow(sharedRoot);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { kind: "indeterminate" };
|
|
117
|
+
}
|
|
118
|
+
if (sharedRootKind === "missing")
|
|
119
|
+
return { kind: "none" };
|
|
120
|
+
if ((await ignoreControlState(skillsRoot)) === "indeterminate") {
|
|
121
|
+
return { kind: "indeterminate" };
|
|
122
|
+
}
|
|
123
|
+
const discoveryRootSkill = join(skillsRoot, "SKILL.md");
|
|
124
|
+
let discoveryRootSkillKind;
|
|
125
|
+
try {
|
|
126
|
+
discoveryRootSkillKind = await classifyPathNoFollow(discoveryRootSkill);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { kind: "indeterminate" };
|
|
130
|
+
}
|
|
131
|
+
if (discoveryRootSkillKind === "regular-file")
|
|
132
|
+
return { kind: "none" };
|
|
133
|
+
if (discoveryRootSkillKind !== "missing") {
|
|
134
|
+
return { kind: "indeterminate" };
|
|
135
|
+
}
|
|
136
|
+
if (sharedRootKind !== "directory")
|
|
137
|
+
return { kind: "indeterminate" };
|
|
138
|
+
return collectSharedSkillDirectory(sharedRoot);
|
|
139
|
+
}
|
|
140
|
+
function exactSkillIdentities(path, agentsRoot) {
|
|
141
|
+
const identities = [
|
|
142
|
+
toPosixPath(relative(agentsRoot, path)),
|
|
143
|
+
toPosixPath(path),
|
|
144
|
+
];
|
|
145
|
+
if (basename(path) === "SKILL.md") {
|
|
146
|
+
identities.push(toPosixPath(relative(agentsRoot, dirname(path))), toPosixPath(dirname(path)));
|
|
147
|
+
}
|
|
148
|
+
return identities;
|
|
149
|
+
}
|
|
150
|
+
function literalExcludeIdentities(path, agentsRoot) {
|
|
151
|
+
const identities = [
|
|
152
|
+
toPosixPath(relative(agentsRoot, path)),
|
|
153
|
+
basename(path),
|
|
154
|
+
toPosixPath(path),
|
|
155
|
+
];
|
|
156
|
+
if (basename(path) === "SKILL.md") {
|
|
157
|
+
identities.push(toPosixPath(relative(agentsRoot, dirname(path))), basename(dirname(path)), toPosixPath(dirname(path)));
|
|
158
|
+
}
|
|
159
|
+
return identities;
|
|
160
|
+
}
|
|
161
|
+
function normalizeExactSkillPattern(pattern) {
|
|
162
|
+
const normalized = pattern.startsWith("./") || pattern.startsWith(".\\")
|
|
163
|
+
? pattern.slice(2)
|
|
164
|
+
: pattern;
|
|
165
|
+
return toPosixPath(normalized);
|
|
166
|
+
}
|
|
167
|
+
function hasUnknownMinimatchSyntax(pattern) {
|
|
168
|
+
return (pattern.startsWith("#") ||
|
|
169
|
+
pattern.includes("!") ||
|
|
170
|
+
/[*?[\]{}()|+@\\]/u.test(pattern) ||
|
|
171
|
+
toPosixPath(pattern).includes("//"));
|
|
172
|
+
}
|
|
173
|
+
function sharedSkillActivity(path, agentsRoot, controls) {
|
|
174
|
+
const exactIdentities = exactSkillIdentities(path, agentsRoot);
|
|
175
|
+
const forceExcludes = controls
|
|
176
|
+
.filter((control) => control.startsWith("-"))
|
|
177
|
+
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
178
|
+
if (forceExcludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
179
|
+
return "disabled";
|
|
180
|
+
}
|
|
181
|
+
const forceIncludes = controls
|
|
182
|
+
.filter((control) => control.startsWith("+"))
|
|
183
|
+
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
184
|
+
if (forceIncludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
185
|
+
return "active";
|
|
186
|
+
}
|
|
187
|
+
const excludePatterns = controls
|
|
188
|
+
.filter((control) => control.startsWith("!"))
|
|
189
|
+
.map((control) => control.slice(1));
|
|
190
|
+
if (excludePatterns.some(hasUnknownMinimatchSyntax))
|
|
191
|
+
return "indeterminate";
|
|
192
|
+
const excludeIdentities = literalExcludeIdentities(path, agentsRoot);
|
|
193
|
+
return excludePatterns
|
|
194
|
+
.map(toPosixPath)
|
|
195
|
+
.some((pattern) => excludeIdentities.includes(pattern))
|
|
196
|
+
? "disabled"
|
|
197
|
+
: "active";
|
|
198
|
+
}
|
|
199
|
+
function isTopLevelSkillPattern(value) {
|
|
200
|
+
return (value.startsWith("!") ||
|
|
201
|
+
value.startsWith("+") ||
|
|
202
|
+
value.startsWith("-") ||
|
|
203
|
+
value.includes("*") ||
|
|
204
|
+
value.includes("?"));
|
|
205
|
+
}
|
|
206
|
+
function normalizeWindowsShellPath(path) {
|
|
207
|
+
if (process.platform !== "win32" ||
|
|
208
|
+
!path.startsWith("/") ||
|
|
209
|
+
path.startsWith("//") ||
|
|
210
|
+
path.includes("\\")) {
|
|
211
|
+
return path;
|
|
212
|
+
}
|
|
213
|
+
const match = /^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/iu.exec(path);
|
|
214
|
+
if (match === null)
|
|
215
|
+
return path;
|
|
216
|
+
const suffix = match[2]?.replaceAll("/", "\\");
|
|
217
|
+
return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
|
|
218
|
+
}
|
|
219
|
+
function resolveTopLevelSkillPath(source, paths) {
|
|
220
|
+
let normalized = normalizeWindowsShellPath(source.trim());
|
|
221
|
+
if (normalized === "~" ||
|
|
222
|
+
normalized.startsWith("~/") ||
|
|
223
|
+
(process.platform === "win32" && normalized.startsWith("~\\"))) {
|
|
224
|
+
normalized =
|
|
225
|
+
normalized === "~"
|
|
226
|
+
? paths.homeDir
|
|
227
|
+
: join(paths.homeDir, normalized.slice(2));
|
|
228
|
+
}
|
|
229
|
+
else if (normalized.startsWith("file://")) {
|
|
230
|
+
normalized = fileURLToPath(normalized);
|
|
231
|
+
}
|
|
232
|
+
return isAbsolute(normalized)
|
|
233
|
+
? resolve(normalized)
|
|
234
|
+
: resolve(paths.agentDir, normalized);
|
|
235
|
+
}
|
|
236
|
+
function containedBy(root, candidate) {
|
|
237
|
+
const suffix = relative(root, candidate);
|
|
238
|
+
return (suffix === "" ||
|
|
239
|
+
(!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
|
|
240
|
+
}
|
|
241
|
+
async function hasSafeSharedSkillAncestors(path, sharedRoot) {
|
|
242
|
+
const suffix = relative(sharedRoot, dirname(path));
|
|
243
|
+
const directories = [sharedRoot];
|
|
244
|
+
let cursor = sharedRoot;
|
|
245
|
+
for (const segment of suffix.split(sep)) {
|
|
246
|
+
if (segment.length === 0)
|
|
247
|
+
continue;
|
|
248
|
+
cursor = join(cursor, segment);
|
|
249
|
+
directories.push(cursor);
|
|
250
|
+
}
|
|
251
|
+
for (const directory of directories) {
|
|
252
|
+
try {
|
|
253
|
+
if ((await classifyPathNoFollow(directory)) !== "directory")
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
function explicitSharedSkillActivity(path, paths, controls) {
|
|
263
|
+
const exactIdentities = exactSkillIdentities(path, paths.agentDir);
|
|
264
|
+
const forceExcludes = controls
|
|
265
|
+
.filter((control) => control.startsWith("-"))
|
|
266
|
+
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
267
|
+
if (forceExcludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
268
|
+
return "disabled";
|
|
269
|
+
}
|
|
270
|
+
const forceIncludes = controls
|
|
271
|
+
.filter((control) => control.startsWith("+"))
|
|
272
|
+
.map((control) => normalizeExactSkillPattern(control.slice(1)));
|
|
273
|
+
if (forceIncludes.some((pattern) => exactIdentities.includes(pattern))) {
|
|
274
|
+
return "active";
|
|
275
|
+
}
|
|
276
|
+
const excludePatterns = controls
|
|
277
|
+
.filter((control) => control.startsWith("!"))
|
|
278
|
+
.map((control) => control.slice(1));
|
|
279
|
+
const excludeIdentities = literalExcludeIdentities(path, paths.agentDir);
|
|
280
|
+
if (excludePatterns
|
|
281
|
+
.filter((pattern) => !hasUnknownMinimatchSyntax(pattern))
|
|
282
|
+
.map(toPosixPath)
|
|
283
|
+
.some((pattern) => excludeIdentities.includes(pattern))) {
|
|
284
|
+
return "disabled";
|
|
285
|
+
}
|
|
286
|
+
const positivePatterns = controls.filter((control) => !control.startsWith("!") &&
|
|
287
|
+
!control.startsWith("+") &&
|
|
288
|
+
!control.startsWith("-") &&
|
|
289
|
+
(control.includes("*") || control.includes("?")));
|
|
290
|
+
return excludePatterns.some(hasUnknownMinimatchSyntax) ||
|
|
291
|
+
positivePatterns.length > 0
|
|
292
|
+
? "indeterminate"
|
|
293
|
+
: "active";
|
|
294
|
+
}
|
|
295
|
+
async function explicitSharedSkillActivities(settings, paths) {
|
|
296
|
+
const activities = new Map();
|
|
297
|
+
const sharedRoot = join(paths.homeDir, ".agents", "skills", "superpowers");
|
|
298
|
+
let canonicalSharedRoot = sharedRoot;
|
|
299
|
+
try {
|
|
300
|
+
canonicalSharedRoot = await realpath(sharedRoot);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// A missing or unresolvable root cannot contain a resolved explicit file.
|
|
304
|
+
}
|
|
305
|
+
let indeterminate = false;
|
|
306
|
+
for (const source of settings.skills ?? []) {
|
|
307
|
+
if (isTopLevelSkillPattern(source))
|
|
308
|
+
continue;
|
|
309
|
+
let path;
|
|
310
|
+
try {
|
|
311
|
+
path = resolveTopLevelSkillPath(source, paths);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
if (source.includes(".agents") && source.includes("superpowers")) {
|
|
315
|
+
indeterminate = true;
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
let lexicalKind;
|
|
320
|
+
try {
|
|
321
|
+
lexicalKind = await classifyPathNoFollow(path);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
if (containedBy(sharedRoot, path) || containedBy(path, sharedRoot)) {
|
|
325
|
+
indeterminate = true;
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (lexicalKind === "missing")
|
|
330
|
+
continue;
|
|
331
|
+
let canonicalPath;
|
|
332
|
+
try {
|
|
333
|
+
canonicalPath = await realpath(path);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
let related = containedBy(sharedRoot, path) || containedBy(path, sharedRoot);
|
|
337
|
+
if (!related && lexicalKind === "symlink") {
|
|
338
|
+
try {
|
|
339
|
+
const target = resolve(dirname(path), await readlink(path));
|
|
340
|
+
related =
|
|
341
|
+
containedBy(sharedRoot, target) || containedBy(target, sharedRoot);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Without a relationship to the known route, preserve unrelated state.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (related)
|
|
348
|
+
indeterminate = true;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const related = containedBy(sharedRoot, path) ||
|
|
352
|
+
containedBy(path, sharedRoot) ||
|
|
353
|
+
containedBy(canonicalSharedRoot, canonicalPath) ||
|
|
354
|
+
containedBy(canonicalPath, canonicalSharedRoot);
|
|
355
|
+
if (!related)
|
|
356
|
+
continue;
|
|
357
|
+
let canonicalKind;
|
|
358
|
+
try {
|
|
359
|
+
canonicalKind = await classifyPathNoFollow(canonicalPath);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
indeterminate = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (canonicalKind !== "regular-file" ||
|
|
366
|
+
!path.endsWith(".md") ||
|
|
367
|
+
!(await hasSafeSharedSkillAncestors(canonicalPath, canonicalSharedRoot))) {
|
|
368
|
+
indeterminate = true;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (!activities.has(canonicalPath)) {
|
|
372
|
+
activities.set(canonicalPath, explicitSharedSkillActivity(path, paths, settings.skills ?? []));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return { activities, indeterminate };
|
|
376
|
+
}
|
|
377
|
+
export async function sharedPiSkillsActivity(settings, paths) {
|
|
378
|
+
const explicit = await explicitSharedSkillActivities(settings, paths);
|
|
379
|
+
const collection = await collectSharedPiSkills(paths);
|
|
380
|
+
if (collection.kind === "indeterminate")
|
|
381
|
+
return "indeterminate";
|
|
382
|
+
const agentsRoot = join(paths.homeDir, ".agents");
|
|
383
|
+
let active = false;
|
|
384
|
+
let indeterminate = explicit.indeterminate;
|
|
385
|
+
for (const activity of explicit.activities.values()) {
|
|
386
|
+
if (activity === "active")
|
|
387
|
+
active = true;
|
|
388
|
+
if (activity === "indeterminate")
|
|
389
|
+
indeterminate = true;
|
|
390
|
+
}
|
|
391
|
+
if (collection.kind === "candidates") {
|
|
392
|
+
for (const path of collection.paths) {
|
|
393
|
+
let canonicalPath;
|
|
394
|
+
try {
|
|
395
|
+
canonicalPath = await realpath(path);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
indeterminate = true;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (explicit.activities.has(canonicalPath))
|
|
402
|
+
continue;
|
|
403
|
+
const activity = sharedSkillActivity(path, agentsRoot, settings.skills ?? []);
|
|
404
|
+
if (activity === "active")
|
|
405
|
+
active = true;
|
|
406
|
+
if (activity === "indeterminate")
|
|
407
|
+
indeterminate = true;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (indeterminate)
|
|
411
|
+
return "indeterminate";
|
|
412
|
+
return active ? "active" : "inactive";
|
|
413
|
+
}
|