@oevery/rsp 3.1.0-beta.1 → 3.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -87
- package/README.zh-CN.md +88 -64
- package/dist/{chunk-QLXIDVKK.mjs → chunk-7KKAUBKN.mjs} +309 -1255
- package/dist/chunk-JOMFFQRG.mjs +99 -0
- package/dist/chunk-NX55NIC3.mjs +358 -0
- package/dist/chunk-OTJFL6XH.mjs +1022 -0
- package/dist/cli.mjs +533 -561
- package/dist/entry-WZDVKDUM.mjs +174 -0
- package/dist/{entry-LAPR3TNI.mjs → entry-X23GPCVS.mjs} +8 -92
- package/package.json +11 -10
- package/rules/rsp-rules.md +16 -14
- package/skills/rsp/SKILL.md +48 -176
- package/skills/rsp/references/conflict-handling.md +11 -0
- package/skills/rsp/references/durable-review.md +17 -0
- package/skills/rsp/references/groups-dependencies.md +11 -0
- package/skills/rsp/references/managed-routing.md +51 -0
- package/skills/rsp/references/setup-repair.md +11 -0
- package/skills/rsp-commit/SKILL.md +42 -0
- package/skills/rsp-design/SKILL.md +14 -9
- package/skills/rsp-manage/SKILL.md +38 -26
- package/skills/rsp-release-docs/SKILL.md +36 -128
- package/skills/rsp-release-docs/references/evidence-and-surfaces.md +21 -0
- package/skills/rsp-release-docs/references/output-contracts.md +0 -18
- package/skills/rsp-release-docs/references/publication-lifecycle.md +24 -0
- package/skills/{rsp-address-review → rsp-resolve-findings}/SKILL.md +7 -7
- package/skills/rsp-shape/SKILL.md +19 -17
- package/skills/rsp-shape/references/complex-shaping.md +2 -2
- package/skills/rsp-structural-audit/SKILL.md +71 -0
- package/skills/rsp-structural-audit/references/structural-lenses.md +41 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/tui/terminal.ts
|
|
4
|
+
function openTerminalSession(stdout, options) {
|
|
5
|
+
let cleaned = false;
|
|
6
|
+
if (!options.screenReader)
|
|
7
|
+
stdout.write("\x1B[?1049h\x1B[?25l");
|
|
8
|
+
return {
|
|
9
|
+
cleanup() {
|
|
10
|
+
if (cleaned)
|
|
11
|
+
return;
|
|
12
|
+
cleaned = true;
|
|
13
|
+
if (!options.screenReader)
|
|
14
|
+
stdout.write("\x1B[?25h\x1B[?1049l");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/tui/display.ts
|
|
20
|
+
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
21
|
+
function isWide(codePoint) {
|
|
22
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 129791 || codePoint >= 131072 && codePoint <= 262141);
|
|
23
|
+
}
|
|
24
|
+
function displayWidth(value) {
|
|
25
|
+
let width = 0;
|
|
26
|
+
for (const { segment } of segmenter.segment(value)) {
|
|
27
|
+
const codePoint = segment.codePointAt(0) ?? 0;
|
|
28
|
+
if (/^\p{Mark}$/u.test(segment) || codePoint === 8205)
|
|
29
|
+
continue;
|
|
30
|
+
width += isWide(codePoint) ? 2 : 1;
|
|
31
|
+
}
|
|
32
|
+
return width;
|
|
33
|
+
}
|
|
34
|
+
function truncateDisplay(value, maxWidth) {
|
|
35
|
+
if (displayWidth(value) <= maxWidth)
|
|
36
|
+
return value;
|
|
37
|
+
if (maxWidth <= 1)
|
|
38
|
+
return "\u2026".slice(0, maxWidth);
|
|
39
|
+
let result = "";
|
|
40
|
+
for (const { segment } of segmenter.segment(value)) {
|
|
41
|
+
if (displayWidth(result + segment) > maxWidth - 1)
|
|
42
|
+
break;
|
|
43
|
+
result += segment;
|
|
44
|
+
}
|
|
45
|
+
return `${result}\u2026`;
|
|
46
|
+
}
|
|
47
|
+
function wrapDisplay(value, maxWidth, firstPrefix = "", continuationPrefix = firstPrefix) {
|
|
48
|
+
const width = Math.max(1, maxWidth);
|
|
49
|
+
if (displayWidth(firstPrefix) >= width)
|
|
50
|
+
return [truncateDisplay(`${firstPrefix}${value}`, width)];
|
|
51
|
+
const lines = [];
|
|
52
|
+
let prefix = firstPrefix;
|
|
53
|
+
let line = prefix;
|
|
54
|
+
const pushLine = () => {
|
|
55
|
+
lines.push(line);
|
|
56
|
+
prefix = continuationPrefix;
|
|
57
|
+
line = prefix;
|
|
58
|
+
};
|
|
59
|
+
const appendUnbroken = (token) => {
|
|
60
|
+
for (const { segment } of segmenter.segment(token)) {
|
|
61
|
+
if (displayWidth(line + segment) > width && line !== prefix)
|
|
62
|
+
pushLine();
|
|
63
|
+
if (displayWidth(line + segment) > width) {
|
|
64
|
+
lines.push(truncateDisplay(line + segment, width));
|
|
65
|
+
prefix = continuationPrefix;
|
|
66
|
+
line = prefix;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
line += segment;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
let pendingWhitespace = "";
|
|
73
|
+
for (const token of value.split(/(\s+)/u).filter(Boolean)) {
|
|
74
|
+
if (token.trim().length === 0) {
|
|
75
|
+
if (line !== prefix)
|
|
76
|
+
pendingWhitespace += token;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const separator = line === prefix ? "" : pendingWhitespace;
|
|
80
|
+
if (displayWidth(line + separator + token) <= width) {
|
|
81
|
+
line += `${separator}${token}`;
|
|
82
|
+
} else {
|
|
83
|
+
if (line !== prefix && separator)
|
|
84
|
+
pushLine();
|
|
85
|
+
appendUnbroken(token);
|
|
86
|
+
}
|
|
87
|
+
pendingWhitespace = "";
|
|
88
|
+
}
|
|
89
|
+
if (line !== prefix || lines.length === 0)
|
|
90
|
+
lines.push(line);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
openTerminalSession,
|
|
96
|
+
displayWidth,
|
|
97
|
+
truncateDisplay,
|
|
98
|
+
wrapDisplay
|
|
99
|
+
};
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ManagedPathError,
|
|
4
|
+
PKG_ROOT,
|
|
5
|
+
inspectManagedDirectory,
|
|
6
|
+
inspectManagedFileTree,
|
|
7
|
+
resolveManagedDirectoryChain
|
|
8
|
+
} from "./chunk-OTJFL6XH.mjs";
|
|
9
|
+
|
|
10
|
+
// src/commands/skills.ts
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
import { lstatSync } from "fs";
|
|
13
|
+
import { chmod, copyFile, mkdir, readdir, readFile, rename, rm } from "fs/promises";
|
|
14
|
+
import { dirname, join, relative } from "path";
|
|
15
|
+
var DEFAULT_PACKAGED_SKILL_NAMES = [
|
|
16
|
+
"rsp",
|
|
17
|
+
"rsp-commit",
|
|
18
|
+
"rsp-design",
|
|
19
|
+
"rsp-diagnose",
|
|
20
|
+
"rsp-implement",
|
|
21
|
+
"rsp-manage",
|
|
22
|
+
"rsp-release-docs",
|
|
23
|
+
"rsp-resolve-findings",
|
|
24
|
+
"rsp-review",
|
|
25
|
+
"rsp-shape",
|
|
26
|
+
"rsp-tdd"
|
|
27
|
+
];
|
|
28
|
+
var OBSOLETE_PACKAGED_SKILL_RENAMES = {
|
|
29
|
+
"rsp-address-review": "rsp-resolve-findings",
|
|
30
|
+
"rsp-codebase-audit": "rsp-structural-audit"
|
|
31
|
+
};
|
|
32
|
+
function unsupportedEntry(path, label) {
|
|
33
|
+
return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
function inspectDirectoryIdentity(path, label) {
|
|
36
|
+
const inspection = inspectManagedDirectory(path, label);
|
|
37
|
+
if (inspection.issue)
|
|
38
|
+
throw inspection.issue;
|
|
39
|
+
const stats = lstatSync(path);
|
|
40
|
+
return { path, dev: stats.dev, ino: stats.ino };
|
|
41
|
+
}
|
|
42
|
+
function assertDirectoryIdentities(identities, label) {
|
|
43
|
+
for (const identity of identities) {
|
|
44
|
+
const current = inspectDirectoryIdentity(identity.path, label);
|
|
45
|
+
if (current.dev !== identity.dev || current.ino !== identity.ino)
|
|
46
|
+
throw new ManagedPathError(identity.path, `${label} changed after validation: ${identity.path}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function prepareTargetRoot(projectRoot, targetRoot) {
|
|
50
|
+
const projectIdentity = inspectDirectoryIdentity(projectRoot, "project Skills root");
|
|
51
|
+
const agentsRoot = join(projectRoot, ".agents");
|
|
52
|
+
const identities = [projectIdentity];
|
|
53
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
54
|
+
await mkdir(agentsRoot).catch((error) => {
|
|
55
|
+
if (error.code !== "EEXIST")
|
|
56
|
+
throw error;
|
|
57
|
+
});
|
|
58
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
59
|
+
identities.push(inspectDirectoryIdentity(agentsRoot, "project Skills root"));
|
|
60
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
61
|
+
await mkdir(targetRoot).catch((error) => {
|
|
62
|
+
if (error.code !== "EEXIST")
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
65
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
66
|
+
identities.push(inspectDirectoryIdentity(targetRoot, "project Skills root"));
|
|
67
|
+
return identities;
|
|
68
|
+
}
|
|
69
|
+
async function inspectPackagedSkills(packageRoot) {
|
|
70
|
+
const skillsRoot = join(packageRoot, "skills");
|
|
71
|
+
const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
|
|
72
|
+
if (rootInspection.issue)
|
|
73
|
+
throw rootInspection.issue;
|
|
74
|
+
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
75
|
+
const skills = [];
|
|
76
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
77
|
+
if (entry.name.startsWith("."))
|
|
78
|
+
continue;
|
|
79
|
+
const root = join(skillsRoot, entry.name);
|
|
80
|
+
if (!entry.isDirectory())
|
|
81
|
+
throw unsupportedEntry(root, "packaged Skills");
|
|
82
|
+
const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`);
|
|
83
|
+
if (inspection.issues.length > 0)
|
|
84
|
+
throw inspection.issues[0];
|
|
85
|
+
skills.push({
|
|
86
|
+
name: entry.name,
|
|
87
|
+
root,
|
|
88
|
+
files: inspection.files.map((path) => relative(root, path))
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return skills;
|
|
92
|
+
}
|
|
93
|
+
function inspectTarget(path, name) {
|
|
94
|
+
try {
|
|
95
|
+
const stats = lstatSync(path);
|
|
96
|
+
if (stats.isDirectory())
|
|
97
|
+
return "directory";
|
|
98
|
+
throw unsupportedEntry(path, `installed Skill ${name}`);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.code === "ENOENT")
|
|
101
|
+
return "missing";
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function inspectInventoryTarget(path) {
|
|
106
|
+
try {
|
|
107
|
+
return lstatSync(path).isDirectory() ? "directory" : "divergent";
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code === "ENOENT")
|
|
110
|
+
return "missing";
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function treesEqual(source, targetRoot) {
|
|
115
|
+
const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`);
|
|
116
|
+
if (target.issues.length > 0)
|
|
117
|
+
throw target.issues[0];
|
|
118
|
+
const targetFiles = target.files.map((path) => relative(targetRoot, path));
|
|
119
|
+
if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
|
|
120
|
+
return false;
|
|
121
|
+
for (const path of source.files) {
|
|
122
|
+
const sourcePath = join(source.root, path);
|
|
123
|
+
const targetPath = join(targetRoot, path);
|
|
124
|
+
const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
|
|
125
|
+
if (!sourceStats.isFile() || !targetStats.isFile())
|
|
126
|
+
throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
|
|
127
|
+
if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
|
|
128
|
+
return false;
|
|
129
|
+
const [sourceContent, targetContent] = await Promise.all([readFile(sourcePath), readFile(targetPath)]);
|
|
130
|
+
if (!sourceContent.equals(targetContent))
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
async function inspectPackagedSkillInventory(options = {}) {
|
|
136
|
+
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
137
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
138
|
+
const packagedSkills = await inspectPackagedSkills(packageRoot);
|
|
139
|
+
const packagedNames = new Set(packagedSkills.map((skill) => skill.name));
|
|
140
|
+
const missingDefaults = DEFAULT_PACKAGED_SKILL_NAMES.filter((name) => !packagedNames.has(name));
|
|
141
|
+
if (missingDefaults.length > 0)
|
|
142
|
+
throw new Error(`packaged Skills missing default Skills: ${missingDefaults.join(", ")}`);
|
|
143
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
144
|
+
if (typeof manifest.name !== "string" || typeof manifest.version !== "string")
|
|
145
|
+
throw new Error("package.json must contain string name and version fields");
|
|
146
|
+
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
147
|
+
const defaults = new Set(DEFAULT_PACKAGED_SKILL_NAMES);
|
|
148
|
+
const skills = [];
|
|
149
|
+
for (const skill of packagedSkills) {
|
|
150
|
+
const target = join(targetRoot, skill.name);
|
|
151
|
+
const targetStatus = inspectInventoryTarget(target);
|
|
152
|
+
skills.push({
|
|
153
|
+
name: skill.name,
|
|
154
|
+
kind: defaults.has(skill.name) ? "default" : "optional",
|
|
155
|
+
status: targetStatus === "missing" ? "missing" : targetStatus === "divergent" ? "divergent" : await treesEqual(skill, target) ? "unchanged" : "divergent"
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
package: { name: manifest.name, version: manifest.version },
|
|
160
|
+
target: relative(projectRoot, targetRoot) || ".",
|
|
161
|
+
skills
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function printPackagedSkillInventory(inventory) {
|
|
165
|
+
console.log(` ${inventory.package.name}@${inventory.package.version}`);
|
|
166
|
+
console.log(` target: ${inventory.target}`);
|
|
167
|
+
for (const group of [
|
|
168
|
+
{ heading: "Default lifecycle Skills", kind: "default" },
|
|
169
|
+
{ heading: "Optional project Skills", kind: "optional" }
|
|
170
|
+
]) {
|
|
171
|
+
console.log("");
|
|
172
|
+
console.log(` ${group.heading}`);
|
|
173
|
+
const skills = inventory.skills.filter((skill) => skill.kind === group.kind);
|
|
174
|
+
if (skills.length === 0)
|
|
175
|
+
console.log(" none");
|
|
176
|
+
for (const skill of skills)
|
|
177
|
+
console.log(` ${skill.name} ${skill.status}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function copySkillTree(source, targetRoot) {
|
|
181
|
+
await mkdir(targetRoot, { recursive: true });
|
|
182
|
+
for (const path of source.files) {
|
|
183
|
+
const sourcePath = join(source.root, path);
|
|
184
|
+
const targetPath = join(targetRoot, path);
|
|
185
|
+
const stats = lstatSync(sourcePath);
|
|
186
|
+
if (!stats.isFile())
|
|
187
|
+
throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
|
|
188
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
189
|
+
await copyFile(sourcePath, targetPath);
|
|
190
|
+
await chmod(targetPath, stats.mode & 511);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function installPackagedSkills(args = {}, options = {}) {
|
|
194
|
+
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
195
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
196
|
+
const renamePath = options.renamePath ?? rename;
|
|
197
|
+
const packagedSkills = await inspectPackagedSkills(packageRoot);
|
|
198
|
+
const requestedNames = args.names?.length ? [...new Set(args.names)] : [...DEFAULT_PACKAGED_SKILL_NAMES];
|
|
199
|
+
const packagedSkillsByName = new Map(packagedSkills.map((skill) => [skill.name, skill]));
|
|
200
|
+
for (const name of requestedNames) {
|
|
201
|
+
if (!packagedSkillsByName.has(name))
|
|
202
|
+
throw new Error(`unknown packaged Skill: ${name}`);
|
|
203
|
+
}
|
|
204
|
+
const skills = requestedNames.map((name) => packagedSkillsByName.get(name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
205
|
+
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
206
|
+
const result = { installed: [], removed: [], unchanged: [], replaced: [] };
|
|
207
|
+
const conflicts = [];
|
|
208
|
+
const obsoleteConflicts = [];
|
|
209
|
+
const targetTreeIdentities = /* @__PURE__ */ new Map();
|
|
210
|
+
for (const skill of skills) {
|
|
211
|
+
const target = join(targetRoot, skill.name);
|
|
212
|
+
if (inspectTarget(target, skill.name) === "missing") {
|
|
213
|
+
result.installed.push(skill.name);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const targetIdentity = inspectDirectoryIdentity(target, `installed Skill ${skill.name}`);
|
|
217
|
+
if (await treesEqual(skill, target)) {
|
|
218
|
+
result.unchanged.push(skill.name);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
assertDirectoryIdentities([targetIdentity], `installed Skill ${skill.name}`);
|
|
222
|
+
if (args.force) {
|
|
223
|
+
result.replaced.push(skill.name);
|
|
224
|
+
targetTreeIdentities.set(skill.name, targetIdentity);
|
|
225
|
+
} else {
|
|
226
|
+
conflicts.push(skill.name);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const selectedNames = new Set(skills.map((skill) => skill.name));
|
|
230
|
+
const obsoleteTreeIdentities = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const [obsolete, replacement] of Object.entries(OBSOLETE_PACKAGED_SKILL_RENAMES)) {
|
|
232
|
+
if (!selectedNames.has(replacement))
|
|
233
|
+
continue;
|
|
234
|
+
const target = join(targetRoot, obsolete);
|
|
235
|
+
if (inspectTarget(target, obsolete) === "missing")
|
|
236
|
+
continue;
|
|
237
|
+
const targetIdentity = inspectDirectoryIdentity(target, `obsolete installed Skill ${obsolete}`);
|
|
238
|
+
assertDirectoryIdentities([targetIdentity], `obsolete installed Skill ${obsolete}`);
|
|
239
|
+
if (args.force) {
|
|
240
|
+
result.removed.push(obsolete);
|
|
241
|
+
obsoleteTreeIdentities.set(obsolete, targetIdentity);
|
|
242
|
+
} else {
|
|
243
|
+
obsoleteConflicts.push(`${obsolete} -> ${replacement}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const conflictMessages = [];
|
|
247
|
+
if (conflicts.length > 0)
|
|
248
|
+
conflictMessages.push(`conflicting packaged Skills: ${conflicts.join(", ")}`);
|
|
249
|
+
if (obsoleteConflicts.length > 0)
|
|
250
|
+
conflictMessages.push(`obsolete packaged Skill renames: ${obsoleteConflicts.join(", ")}`);
|
|
251
|
+
if (conflictMessages.length > 0)
|
|
252
|
+
throw new Error(`${conflictMessages.join("; ")}; rerun with --force to replace or remove only these package-owned directories`);
|
|
253
|
+
if (args.dryRun)
|
|
254
|
+
return result;
|
|
255
|
+
await options.onMutationStep?.({ phase: "before-target-root-mutation", targetRoot });
|
|
256
|
+
const targetIdentities = await prepareTargetRoot(projectRoot, targetRoot);
|
|
257
|
+
const stagingRoot = join(targetRoot, `.rsp-skills-install-${randomUUID()}`);
|
|
258
|
+
const nextRoot = join(stagingRoot, "next");
|
|
259
|
+
const previousRoot = join(stagingRoot, "previous");
|
|
260
|
+
const failedRoot = join(stagingRoot, "failed");
|
|
261
|
+
const movedPrevious = [];
|
|
262
|
+
const activated = [];
|
|
263
|
+
let removeStaging = true;
|
|
264
|
+
try {
|
|
265
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
266
|
+
await mkdir(nextRoot, { recursive: true });
|
|
267
|
+
await mkdir(previousRoot);
|
|
268
|
+
await mkdir(failedRoot);
|
|
269
|
+
for (const skill of skills) {
|
|
270
|
+
if (result.unchanged.includes(skill.name))
|
|
271
|
+
continue;
|
|
272
|
+
await copySkillTree(skill, join(nextRoot, skill.name));
|
|
273
|
+
}
|
|
274
|
+
for (const name of result.installed) {
|
|
275
|
+
const target = join(targetRoot, name);
|
|
276
|
+
if (inspectTarget(target, name) !== "missing")
|
|
277
|
+
throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
|
|
278
|
+
}
|
|
279
|
+
for (const name of result.replaced) {
|
|
280
|
+
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
281
|
+
}
|
|
282
|
+
for (const name of result.removed) {
|
|
283
|
+
assertDirectoryIdentities([obsoleteTreeIdentities.get(name)], `obsolete installed Skill ${name}`);
|
|
284
|
+
}
|
|
285
|
+
for (const name of result.removed) {
|
|
286
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
287
|
+
assertDirectoryIdentities([obsoleteTreeIdentities.get(name)], `obsolete installed Skill ${name}`);
|
|
288
|
+
await renamePath(join(targetRoot, name), join(previousRoot, name));
|
|
289
|
+
movedPrevious.push(name);
|
|
290
|
+
}
|
|
291
|
+
for (const name of result.replaced) {
|
|
292
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
293
|
+
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
294
|
+
await renamePath(join(targetRoot, name), join(previousRoot, name));
|
|
295
|
+
movedPrevious.push(name);
|
|
296
|
+
}
|
|
297
|
+
for (const name of [...result.installed, ...result.replaced].sort()) {
|
|
298
|
+
const target = join(targetRoot, name);
|
|
299
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
300
|
+
await options.onMutationStep?.({ phase: "before-activate", name, target });
|
|
301
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
302
|
+
if (inspectTarget(target, name) !== "missing")
|
|
303
|
+
throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
|
|
304
|
+
await renamePath(join(nextRoot, name), target);
|
|
305
|
+
activated.push(name);
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const rollbackErrors = [];
|
|
309
|
+
for (const name of activated.reverse()) {
|
|
310
|
+
const target = join(targetRoot, name);
|
|
311
|
+
try {
|
|
312
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
313
|
+
if (inspectTarget(target, name) === "directory")
|
|
314
|
+
await renamePath(target, join(failedRoot, name));
|
|
315
|
+
} catch (rollbackError) {
|
|
316
|
+
rollbackErrors.push(rollbackError instanceof Error ? rollbackError.message : String(rollbackError));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const name of movedPrevious.reverse()) {
|
|
320
|
+
const target = join(targetRoot, name);
|
|
321
|
+
const previous = join(previousRoot, name);
|
|
322
|
+
try {
|
|
323
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
324
|
+
await options.onMutationStep?.({ phase: "before-rollback-restore", name, target, previous });
|
|
325
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
326
|
+
if (inspectTarget(target, name) !== "missing")
|
|
327
|
+
throw new ManagedPathError(target, `cannot restore installed Skill because the target is occupied: ${target}`);
|
|
328
|
+
await renamePath(previous, target);
|
|
329
|
+
} catch (rollbackError) {
|
|
330
|
+
rollbackErrors.push(rollbackError instanceof Error ? rollbackError.message : String(rollbackError));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (rollbackErrors.length > 0) {
|
|
334
|
+
removeStaging = false;
|
|
335
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
336
|
+
throw new Error(`${message}; rollback incomplete; recover original Skills from ${previousRoot}; ${rollbackErrors.join("; ")}`);
|
|
337
|
+
}
|
|
338
|
+
throw error;
|
|
339
|
+
} finally {
|
|
340
|
+
if (removeStaging)
|
|
341
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
342
|
+
}
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
function printSkillInstallResult(result, dryRun = false) {
|
|
346
|
+
const prefix = dryRun ? "would be " : "";
|
|
347
|
+
for (const status of ["installed", "unchanged", "replaced", "removed"]) {
|
|
348
|
+
if (result[status].length > 0)
|
|
349
|
+
console.log(` ${prefix}${status}: ${result[status].join(", ")}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export {
|
|
354
|
+
inspectPackagedSkillInventory,
|
|
355
|
+
printPackagedSkillInventory,
|
|
356
|
+
installPackagedSkills,
|
|
357
|
+
printSkillInstallResult
|
|
358
|
+
};
|