@xenonbyte/xsk 0.1.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/LICENSE +21 -0
- package/README.md +107 -0
- package/README.zh-CN.md +107 -0
- package/bin/xsk.js +164 -0
- package/lib/adapters/claude.js +14 -0
- package/lib/adapters/codex.js +14 -0
- package/lib/adapters/gemini.js +14 -0
- package/lib/adapters/opencode.js +14 -0
- package/lib/capability.js +126 -0
- package/lib/content-hash.js +13 -0
- package/lib/generator.js +44 -0
- package/lib/input.js +93 -0
- package/lib/install.js +496 -0
- package/lib/manifest.js +288 -0
- package/lib/ownership.js +85 -0
- package/lib/skills.js +58 -0
- package/lib/status.js +171 -0
- package/lib/uninstall.js +577 -0
- package/package.json +36 -0
- package/shared/skill-common.md +6 -0
- package/skills/archive-req/SKILL.md +37 -0
- package/skills/bypass-claude/SKILL.md +53 -0
- package/skills/check/SKILL.md +73 -0
- package/skills/skill-scaffold/SKILL.md +56 -0
- package/skills/think/SKILL.md +56 -0
- package/skills/write-req/SKILL.md +63 -0
- package/templates/fragments/archive-req.behavior.md +7 -0
- package/templates/fragments/archive-req.output.md +1 -0
- package/templates/fragments/archive-req.purpose.md +1 -0
- package/templates/fragments/archive-req.triggers.md +5 -0
- package/templates/fragments/bypass-claude.behavior.md +23 -0
- package/templates/fragments/bypass-claude.output.md +1 -0
- package/templates/fragments/bypass-claude.purpose.md +1 -0
- package/templates/fragments/bypass-claude.triggers.md +5 -0
- package/templates/fragments/check.behavior.md +29 -0
- package/templates/fragments/check.output.md +13 -0
- package/templates/fragments/check.purpose.md +3 -0
- package/templates/fragments/check.triggers.md +5 -0
- package/templates/fragments/skill-scaffold.behavior.md +24 -0
- package/templates/fragments/skill-scaffold.output.md +3 -0
- package/templates/fragments/skill-scaffold.purpose.md +1 -0
- package/templates/fragments/skill-scaffold.triggers.md +5 -0
- package/templates/fragments/think.behavior.md +15 -0
- package/templates/fragments/think.output.md +9 -0
- package/templates/fragments/think.purpose.md +3 -0
- package/templates/fragments/think.triggers.md +6 -0
- package/templates/fragments/write-req.behavior.md +33 -0
- package/templates/fragments/write-req.output.md +1 -0
- package/templates/fragments/write-req.purpose.md +1 -0
- package/templates/fragments/write-req.triggers.md +5 -0
- package/templates/skill.md.tmpl +22 -0
package/lib/uninstall.js
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
read,
|
|
8
|
+
write,
|
|
9
|
+
validate,
|
|
10
|
+
validateOperationalSemantics,
|
|
11
|
+
create,
|
|
12
|
+
defaultXskRoot,
|
|
13
|
+
removeManifest,
|
|
14
|
+
manifestPath,
|
|
15
|
+
atomicWriteFile,
|
|
16
|
+
isInsideDir,
|
|
17
|
+
} = require('./manifest');
|
|
18
|
+
const { get } = require('./skills');
|
|
19
|
+
const { buildSkill } = require('./generator');
|
|
20
|
+
const {
|
|
21
|
+
MARKER,
|
|
22
|
+
PACKAGE_NAME,
|
|
23
|
+
isSymlink,
|
|
24
|
+
hasUnsafeSkillPath,
|
|
25
|
+
hasValidMarker,
|
|
26
|
+
safeBackupForSkill,
|
|
27
|
+
} = require('./ownership');
|
|
28
|
+
const { contentSha256 } = require('./content-hash');
|
|
29
|
+
|
|
30
|
+
const PARTIAL_EXIT = 2;
|
|
31
|
+
const FAILURE_EXIT = 1;
|
|
32
|
+
|
|
33
|
+
function generatedContentFor(skillName) {
|
|
34
|
+
const skill = get(skillName);
|
|
35
|
+
if (!skill) return null;
|
|
36
|
+
try {
|
|
37
|
+
return buildSkill(skill).content;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function classifyPaths(installedPaths) {
|
|
44
|
+
const skillDirs = new Set();
|
|
45
|
+
for (const p of installedPaths) {
|
|
46
|
+
const base = path.basename(p);
|
|
47
|
+
if (base === 'SKILL.md' || base === MARKER) {
|
|
48
|
+
skillDirs.add(path.dirname(p));
|
|
49
|
+
} else {
|
|
50
|
+
skillDirs.add(p);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...skillDirs];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function restoreOwnershipMarker(markerFile) {
|
|
57
|
+
try {
|
|
58
|
+
atomicWriteFile(markerFile, `${PACKAGE_NAME}\n`, {
|
|
59
|
+
dirLabel: 'marker dir',
|
|
60
|
+
tempLabel: 'marker temp file',
|
|
61
|
+
targetLabel: 'marker file',
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function installedHashByTarget(manifest) {
|
|
70
|
+
const records = Array.isArray(manifest.installed_hashes) ? manifest.installed_hashes : [];
|
|
71
|
+
return new Map(records.map((r) => [r.target, r.sha256]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function retainedInstalledHashes(manifest, retainedPaths) {
|
|
75
|
+
const retained = new Set(retainedPaths);
|
|
76
|
+
return (Array.isArray(manifest.installed_hashes) ? manifest.installed_hashes : [])
|
|
77
|
+
.filter((r) => retained.has(r.target))
|
|
78
|
+
.map((r) => ({ target: r.target, sha256: r.sha256 }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function appendError(current, message) {
|
|
82
|
+
return current ? `${current}; ${message}` : message;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateBackupTargets(backups, skillDirs, skillsRoot) {
|
|
86
|
+
const skillFiles = new Set(skillDirs.map((skillDir) => path.join(skillDir, 'SKILL.md')));
|
|
87
|
+
const seenTargets = new Set();
|
|
88
|
+
for (const backup of backups) {
|
|
89
|
+
if (seenTargets.has(backup.target)) {
|
|
90
|
+
return { valid: false, reason: `duplicate backup target: ${backup.target}` };
|
|
91
|
+
}
|
|
92
|
+
seenTargets.add(backup.target);
|
|
93
|
+
if (!isInsideDir(backup.target, skillsRoot)) {
|
|
94
|
+
return { valid: false, reason: `backup target escapes platform root: ${backup.target}` };
|
|
95
|
+
}
|
|
96
|
+
if (path.basename(backup.target) !== 'SKILL.md') {
|
|
97
|
+
return { valid: false, reason: `backup target is not a skill file: ${backup.target}` };
|
|
98
|
+
}
|
|
99
|
+
if (!skillFiles.has(backup.target)) {
|
|
100
|
+
return { valid: false, reason: `backup target is not installed: ${backup.target}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { valid: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function capturePathState(targetPath) {
|
|
107
|
+
try {
|
|
108
|
+
const stat = fs.lstatSync(targetPath);
|
|
109
|
+
if (stat.isDirectory()) {
|
|
110
|
+
return { exists: true, type: 'directory', mode: stat.mode };
|
|
111
|
+
}
|
|
112
|
+
if (stat.isSymbolicLink()) {
|
|
113
|
+
return { exists: true, type: 'symlink', link: fs.readlinkSync(targetPath) };
|
|
114
|
+
}
|
|
115
|
+
if (stat.isFile()) {
|
|
116
|
+
return { exists: true, type: 'file', content: fs.readFileSync(targetPath), mode: stat.mode };
|
|
117
|
+
}
|
|
118
|
+
return { exists: true, type: 'other' };
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) {
|
|
121
|
+
return { exists: false };
|
|
122
|
+
}
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function removePathForRestore(targetPath) {
|
|
128
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function restorePathState(targetPath, state) {
|
|
132
|
+
if (!state.exists) {
|
|
133
|
+
removePathForRestore(targetPath);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (state.type === 'directory') {
|
|
137
|
+
if (fs.existsSync(targetPath) && !fs.lstatSync(targetPath).isDirectory()) {
|
|
138
|
+
removePathForRestore(targetPath);
|
|
139
|
+
}
|
|
140
|
+
fs.mkdirSync(targetPath, { recursive: true, mode: state.mode });
|
|
141
|
+
try {
|
|
142
|
+
fs.chmodSync(targetPath, state.mode);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
/* best effort */
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
removePathForRestore(targetPath);
|
|
149
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
150
|
+
if (state.type === 'symlink') {
|
|
151
|
+
fs.symlinkSync(state.link, targetPath);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (state.type === 'file') {
|
|
155
|
+
fs.writeFileSync(targetPath, state.content, { mode: state.mode });
|
|
156
|
+
try {
|
|
157
|
+
fs.chmodSync(targetPath, state.mode);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
/* best effort */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createRollbackJournal() {
|
|
165
|
+
const states = new Map();
|
|
166
|
+
return {
|
|
167
|
+
capture(targetPath) {
|
|
168
|
+
if (!states.has(targetPath)) {
|
|
169
|
+
states.set(targetPath, capturePathState(targetPath));
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
restore() {
|
|
173
|
+
const entries = [...states.entries()].reverse();
|
|
174
|
+
for (const [targetPath, state] of entries) {
|
|
175
|
+
restorePathState(targetPath, state);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function uninstallPlatform({ platform, xskRoot, skillsRoot }) {
|
|
182
|
+
let manifest;
|
|
183
|
+
try {
|
|
184
|
+
manifest = read(platform, { xskRoot });
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return {
|
|
187
|
+
platform,
|
|
188
|
+
invalid: true,
|
|
189
|
+
removed: [],
|
|
190
|
+
restored: [],
|
|
191
|
+
retained: [],
|
|
192
|
+
skipped: [],
|
|
193
|
+
refused: [],
|
|
194
|
+
partial: false,
|
|
195
|
+
error: `manifest for ${platform} is not valid JSON: ${e.message}`,
|
|
196
|
+
exitCode: FAILURE_EXIT,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!manifest) {
|
|
201
|
+
return {
|
|
202
|
+
platform,
|
|
203
|
+
nothingInstalled: true,
|
|
204
|
+
removed: [],
|
|
205
|
+
restored: [],
|
|
206
|
+
retained: [],
|
|
207
|
+
skipped: [],
|
|
208
|
+
refused: [],
|
|
209
|
+
partial: false,
|
|
210
|
+
exitCode: 0,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!validate(manifest, { expectedPlatform: platform })) {
|
|
215
|
+
return {
|
|
216
|
+
platform,
|
|
217
|
+
invalid: true,
|
|
218
|
+
removed: [],
|
|
219
|
+
restored: [],
|
|
220
|
+
retained: [],
|
|
221
|
+
skipped: [],
|
|
222
|
+
refused: [],
|
|
223
|
+
partial: false,
|
|
224
|
+
error: `manifest for ${platform} failed shape validation; refusing to uninstall`,
|
|
225
|
+
exitCode: FAILURE_EXIT,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const sem = validateOperationalSemantics({ platform, skillsRoot, manifest });
|
|
230
|
+
if (!sem.valid) {
|
|
231
|
+
return {
|
|
232
|
+
platform,
|
|
233
|
+
invalid: true,
|
|
234
|
+
removed: [],
|
|
235
|
+
restored: [],
|
|
236
|
+
retained: [],
|
|
237
|
+
skipped: [],
|
|
238
|
+
refused: [],
|
|
239
|
+
partial: false,
|
|
240
|
+
error: sem.reason,
|
|
241
|
+
exitCode: FAILURE_EXIT,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const backups = manifest.backups || [];
|
|
246
|
+
const skillDirs = classifyPaths(manifest.installed_paths);
|
|
247
|
+
const backupTargets = validateBackupTargets(backups, skillDirs, skillsRoot);
|
|
248
|
+
if (!backupTargets.valid) {
|
|
249
|
+
return {
|
|
250
|
+
platform,
|
|
251
|
+
invalid: true,
|
|
252
|
+
removed: [],
|
|
253
|
+
restored: [],
|
|
254
|
+
retained: [],
|
|
255
|
+
skipped: [],
|
|
256
|
+
refused: [],
|
|
257
|
+
partial: false,
|
|
258
|
+
error: backupTargets.reason,
|
|
259
|
+
exitCode: FAILURE_EXIT,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const removed = [];
|
|
264
|
+
const restored = [];
|
|
265
|
+
const retainedFiles = [];
|
|
266
|
+
const retainedMarkers = [];
|
|
267
|
+
const retainedDirs = [];
|
|
268
|
+
const retainedBackups = [];
|
|
269
|
+
const skipped = [];
|
|
270
|
+
const refused = [];
|
|
271
|
+
const installedHashes = installedHashByTarget(manifest);
|
|
272
|
+
const rollbackJournal = createRollbackJournal();
|
|
273
|
+
let partial = false;
|
|
274
|
+
let error = null;
|
|
275
|
+
const manifestPaths = new Set(manifest.installed_paths);
|
|
276
|
+
|
|
277
|
+
const ownedDirs = new Set(
|
|
278
|
+
manifest.installed_paths.filter((p) => {
|
|
279
|
+
const base = path.basename(p);
|
|
280
|
+
return base !== 'SKILL.md' && base !== MARKER;
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
for (const skillDir of skillDirs) {
|
|
284
|
+
const skillName = path.basename(skillDir);
|
|
285
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
286
|
+
const markerFile = path.join(skillDir, MARKER);
|
|
287
|
+
const backupState = safeBackupForSkill(backups, skillFile, xskRoot, platform, skillsRoot);
|
|
288
|
+
const skillRollbackJournal = createRollbackJournal();
|
|
289
|
+
const captureSkillMutation = (targetPath) => {
|
|
290
|
+
rollbackJournal.capture(targetPath);
|
|
291
|
+
skillRollbackJournal.capture(targetPath);
|
|
292
|
+
};
|
|
293
|
+
const pathUnsafe = hasUnsafeSkillPath(skillDir, skillFile, markerFile);
|
|
294
|
+
const markerInvalid = !pathUnsafe && fs.existsSync(markerFile) && !hasValidMarker(markerFile);
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
pathUnsafe ||
|
|
298
|
+
markerInvalid ||
|
|
299
|
+
backupState.unsafe
|
|
300
|
+
) {
|
|
301
|
+
refused.push(skillDir);
|
|
302
|
+
if (pathUnsafe) {
|
|
303
|
+
if (ownedDirs.has(skillDir)) {
|
|
304
|
+
retainedDirs.push(skillDir);
|
|
305
|
+
}
|
|
306
|
+
if (manifestPaths.has(skillFile)) {
|
|
307
|
+
retainedFiles.push(skillFile);
|
|
308
|
+
}
|
|
309
|
+
if (manifestPaths.has(markerFile)) {
|
|
310
|
+
retainedMarkers.push(markerFile);
|
|
311
|
+
}
|
|
312
|
+
if (backupState.backup) {
|
|
313
|
+
retainedBackups.push(backupState.backup);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
if (ownedDirs.has(skillDir) && fs.existsSync(skillDir)) {
|
|
317
|
+
retainedDirs.push(skillDir);
|
|
318
|
+
}
|
|
319
|
+
if ((backupState.unsafe || markerInvalid) && fs.existsSync(skillFile)) {
|
|
320
|
+
retainedFiles.push(skillFile);
|
|
321
|
+
}
|
|
322
|
+
if ((backupState.unsafe || markerInvalid) && fs.existsSync(markerFile)) {
|
|
323
|
+
retainedMarkers.push(markerFile);
|
|
324
|
+
}
|
|
325
|
+
if (backupState.backup) {
|
|
326
|
+
retainedBackups.push(backupState.backup);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
partial = true;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (backupState.missing) {
|
|
334
|
+
refused.push(skillDir);
|
|
335
|
+
if (ownedDirs.has(skillDir) && fs.existsSync(skillDir)) {
|
|
336
|
+
retainedDirs.push(skillDir);
|
|
337
|
+
}
|
|
338
|
+
if (fs.existsSync(skillFile)) {
|
|
339
|
+
retainedFiles.push(skillFile);
|
|
340
|
+
}
|
|
341
|
+
if (fs.existsSync(markerFile)) {
|
|
342
|
+
retainedMarkers.push(markerFile);
|
|
343
|
+
}
|
|
344
|
+
retainedBackups.push(backupState.backup);
|
|
345
|
+
partial = true;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!fs.existsSync(markerFile)) {
|
|
350
|
+
const retainedDir = ownedDirs.has(skillDir) && fs.existsSync(skillDir);
|
|
351
|
+
const retainedFile = fs.existsSync(skillFile);
|
|
352
|
+
const bk = backupState.backup;
|
|
353
|
+
skipped.push(skillDir);
|
|
354
|
+
if (retainedDir) {
|
|
355
|
+
retainedDirs.push(skillDir);
|
|
356
|
+
}
|
|
357
|
+
if (retainedFile) {
|
|
358
|
+
retainedFiles.push(skillFile);
|
|
359
|
+
}
|
|
360
|
+
if (retainedDir || retainedFile || bk) {
|
|
361
|
+
retainedMarkers.push(markerFile);
|
|
362
|
+
if (bk) {
|
|
363
|
+
retainedBackups.push(bk);
|
|
364
|
+
}
|
|
365
|
+
partial = true;
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const fileExists = fs.existsSync(skillFile);
|
|
371
|
+
let fileModified = false;
|
|
372
|
+
if (fileExists) {
|
|
373
|
+
const onDisk = fs.readFileSync(skillFile);
|
|
374
|
+
const installedHash = installedHashes.get(skillFile);
|
|
375
|
+
if (installedHash) {
|
|
376
|
+
fileModified = contentSha256(onDisk) !== installedHash;
|
|
377
|
+
} else {
|
|
378
|
+
const gen = generatedContentFor(skillName);
|
|
379
|
+
fileModified = gen === null ? true : onDisk.toString('utf8') !== gen;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!fileExists || !fileModified) {
|
|
384
|
+
let removedSkillFile = false;
|
|
385
|
+
if (fileExists) {
|
|
386
|
+
captureSkillMutation(skillFile);
|
|
387
|
+
fs.rmSync(skillFile, { force: true });
|
|
388
|
+
removedSkillFile = true;
|
|
389
|
+
}
|
|
390
|
+
const bk = backupState.backup;
|
|
391
|
+
if (bk && fs.existsSync(bk.backup)) {
|
|
392
|
+
captureSkillMutation(bk.target);
|
|
393
|
+
rollbackJournal.capture(bk.backup);
|
|
394
|
+
fs.mkdirSync(path.dirname(bk.target), { recursive: true });
|
|
395
|
+
try {
|
|
396
|
+
fs.copyFileSync(bk.backup, bk.target);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
let rollbackError = null;
|
|
399
|
+
try {
|
|
400
|
+
skillRollbackJournal.restore();
|
|
401
|
+
} catch (rollbackErr) {
|
|
402
|
+
rollbackError = rollbackErr;
|
|
403
|
+
}
|
|
404
|
+
partial = true;
|
|
405
|
+
error = appendError(error, `backup restore failed: ${e.message}`);
|
|
406
|
+
if (rollbackError) {
|
|
407
|
+
error = appendError(error, `rollback failed: ${rollbackError.message}`);
|
|
408
|
+
}
|
|
409
|
+
if (ownedDirs.has(skillDir) && fs.existsSync(skillDir)) {
|
|
410
|
+
retainedDirs.push(skillDir);
|
|
411
|
+
}
|
|
412
|
+
if (fs.existsSync(skillFile)) {
|
|
413
|
+
retainedFiles.push(skillFile);
|
|
414
|
+
}
|
|
415
|
+
if (fs.existsSync(markerFile)) {
|
|
416
|
+
retainedMarkers.push(markerFile);
|
|
417
|
+
}
|
|
418
|
+
retainedBackups.push(bk);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
fs.rmSync(bk.backup, { force: true });
|
|
423
|
+
} catch (e) {
|
|
424
|
+
let rollbackError = null;
|
|
425
|
+
try {
|
|
426
|
+
skillRollbackJournal.restore();
|
|
427
|
+
} catch (rollbackErr) {
|
|
428
|
+
rollbackError = rollbackErr;
|
|
429
|
+
}
|
|
430
|
+
partial = true;
|
|
431
|
+
error = appendError(error, `backup cleanup failed: ${e.message}`);
|
|
432
|
+
if (rollbackError) {
|
|
433
|
+
error = appendError(error, `rollback failed: ${rollbackError.message}`);
|
|
434
|
+
}
|
|
435
|
+
if (ownedDirs.has(skillDir) && fs.existsSync(skillDir)) {
|
|
436
|
+
retainedDirs.push(skillDir);
|
|
437
|
+
}
|
|
438
|
+
if (fs.existsSync(skillFile)) {
|
|
439
|
+
retainedFiles.push(skillFile);
|
|
440
|
+
}
|
|
441
|
+
if (fs.existsSync(markerFile)) {
|
|
442
|
+
retainedMarkers.push(markerFile);
|
|
443
|
+
}
|
|
444
|
+
retainedBackups.push(bk);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
restored.push(bk.target);
|
|
448
|
+
}
|
|
449
|
+
if (removedSkillFile) {
|
|
450
|
+
removed.push(skillFile);
|
|
451
|
+
}
|
|
452
|
+
if (ownedDirs.has(skillDir)) {
|
|
453
|
+
let markerRemoved = false;
|
|
454
|
+
if (fs.existsSync(markerFile)) {
|
|
455
|
+
captureSkillMutation(markerFile);
|
|
456
|
+
fs.rmSync(markerFile, { force: true });
|
|
457
|
+
markerRemoved = true;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
captureSkillMutation(skillDir);
|
|
461
|
+
fs.rmdirSync(skillDir);
|
|
462
|
+
if (markerRemoved) {
|
|
463
|
+
removed.push(markerFile);
|
|
464
|
+
}
|
|
465
|
+
removed.push(skillDir);
|
|
466
|
+
} catch (e) {
|
|
467
|
+
if (fs.existsSync(skillDir)) {
|
|
468
|
+
if (markerRemoved && restoreOwnershipMarker(markerFile)) {
|
|
469
|
+
retainedMarkers.push(markerFile);
|
|
470
|
+
}
|
|
471
|
+
retainedDirs.push(skillDir);
|
|
472
|
+
partial = true;
|
|
473
|
+
} else if (markerRemoved) {
|
|
474
|
+
removed.push(markerFile);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} else if (fs.existsSync(markerFile)) {
|
|
478
|
+
captureSkillMutation(markerFile);
|
|
479
|
+
fs.rmSync(markerFile, { force: true });
|
|
480
|
+
removed.push(markerFile);
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
if (ownedDirs.has(skillDir)) {
|
|
484
|
+
retainedDirs.push(skillDir);
|
|
485
|
+
}
|
|
486
|
+
retainedFiles.push(skillFile);
|
|
487
|
+
retainedMarkers.push(markerFile);
|
|
488
|
+
const bk = backupState.backup;
|
|
489
|
+
if (bk) {
|
|
490
|
+
retainedBackups.push(bk);
|
|
491
|
+
}
|
|
492
|
+
partial = true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const retainedPaths = [...retainedDirs, ...retainedFiles, ...retainedMarkers];
|
|
497
|
+
let narrowed = null;
|
|
498
|
+
if (retainedPaths.length > 0 || retainedBackups.length > 0) {
|
|
499
|
+
narrowed = create(platform, manifest.version);
|
|
500
|
+
narrowed.installed_paths = retainedPaths;
|
|
501
|
+
narrowed.backups = retainedBackups;
|
|
502
|
+
narrowed.installed_hashes = retainedInstalledHashes(manifest, retainedFiles);
|
|
503
|
+
try {
|
|
504
|
+
rollbackJournal.capture(manifestPath(platform, { xskRoot }));
|
|
505
|
+
write(platform, narrowed, { xskRoot });
|
|
506
|
+
} catch (e) {
|
|
507
|
+
partial = true;
|
|
508
|
+
narrowed = manifest;
|
|
509
|
+
error = appendError(error, `manifest write failed: ${e.message}`);
|
|
510
|
+
try {
|
|
511
|
+
rollbackJournal.restore();
|
|
512
|
+
// Full rollback restored every removed/restored path, so the prior
|
|
513
|
+
// removal tallies no longer reflect disk state. Clear them so callers
|
|
514
|
+
// do not report removals that were undone.
|
|
515
|
+
removed.length = 0;
|
|
516
|
+
restored.length = 0;
|
|
517
|
+
} catch (rollbackErr) {
|
|
518
|
+
error = appendError(error, `rollback failed: ${rollbackErr.message}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
try {
|
|
523
|
+
removeManifest(platform, { xskRoot });
|
|
524
|
+
} catch (e) {
|
|
525
|
+
partial = true;
|
|
526
|
+
narrowed = manifest;
|
|
527
|
+
error = `manifest removal failed: ${e.message}`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const result = {
|
|
532
|
+
platform,
|
|
533
|
+
removed,
|
|
534
|
+
restored,
|
|
535
|
+
retained: retainedFiles,
|
|
536
|
+
skipped,
|
|
537
|
+
refused,
|
|
538
|
+
partial,
|
|
539
|
+
manifest: narrowed,
|
|
540
|
+
exitCode: partial ? PARTIAL_EXIT : 0,
|
|
541
|
+
};
|
|
542
|
+
if (error) {
|
|
543
|
+
result.error = error;
|
|
544
|
+
}
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function uninstall(options) {
|
|
549
|
+
const opts = options || {};
|
|
550
|
+
const platforms = opts.platforms || ['claude', 'codex', 'opencode', 'gemini'];
|
|
551
|
+
const xskRoot = opts.xskRoot || defaultXskRoot();
|
|
552
|
+
const { rootFor } = require('./install');
|
|
553
|
+
|
|
554
|
+
const summary = { platforms: {} };
|
|
555
|
+
let exitCode = 0;
|
|
556
|
+
for (const platform of platforms) {
|
|
557
|
+
const skillsRoot = rootFor(platform, opts.platformRoots);
|
|
558
|
+
const res = uninstallPlatform({ platform, xskRoot, skillsRoot });
|
|
559
|
+
summary.platforms[platform] = res;
|
|
560
|
+
if (res.exitCode > exitCode) {
|
|
561
|
+
exitCode = res.exitCode;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
summary.exitCode = exitCode;
|
|
565
|
+
return summary;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
module.exports = {
|
|
569
|
+
uninstall,
|
|
570
|
+
uninstallPlatform,
|
|
571
|
+
classifyPaths,
|
|
572
|
+
generatedContentFor,
|
|
573
|
+
isSymlink,
|
|
574
|
+
safeBackupForSkill,
|
|
575
|
+
PARTIAL_EXIT,
|
|
576
|
+
FAILURE_EXIT,
|
|
577
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xenonbyte/xsk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent skill aggregator. Install a curated set of agent skills across Claude Code, Codex, opencode, and Gemini with manifest-backed safety.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xsk": "bin/xsk.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "bin/xsk.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test",
|
|
12
|
+
"syntaxcheck": "node scripts/syntaxcheck.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"lib/",
|
|
20
|
+
"skills/",
|
|
21
|
+
"shared/",
|
|
22
|
+
"templates/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.zh-CN.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"agent",
|
|
29
|
+
"skills",
|
|
30
|
+
"claude-code",
|
|
31
|
+
"codex",
|
|
32
|
+
"opencode",
|
|
33
|
+
"gemini",
|
|
34
|
+
"cli"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## Conventions shared across xsk skills
|
|
2
|
+
|
|
3
|
+
- Triggers are matched by intent, not by exact wording. The phrases listed under "When to use" are cues, not a required incantation.
|
|
4
|
+
- Write in natural, direct prose. No formulaic openers, no filler conclusions, no restating the request before you answer it.
|
|
5
|
+
- When a decision would change the implementation, surface it as a short question and let the user decide. Do not pick silently.
|
|
6
|
+
- These are instruction skills. They shape how work is approached, not what the agent is technically capable of.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xsk-archive-req
|
|
3
|
+
description: Archive the active requirement document into requirements/archive/ and leave zero active docs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xsk-archive-req
|
|
7
|
+
|
|
8
|
+
Archive the active requirement document into `requirements/archive/`. After it runs, zero active requirement docs remain.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Match the intent, not the exact words. Common cues:
|
|
13
|
+
|
|
14
|
+
- "归档需求", "需求归档", "把这个需求存档"
|
|
15
|
+
- "archive requirement"
|
|
16
|
+
- any request to file away the current active requirement doc
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
1. Scan `requirements/*.md` (excluding `requirements/archive/`) for the single document whose frontmatter has `status: active`. If more than one exists, stop, list the offending paths, and report the broken invariant for the user to resolve.
|
|
21
|
+
2. If there is no active document, refuse with a one-line reason and stop. Do not archive anything.
|
|
22
|
+
3. Read the active doc slug and validate it against `^[a-z0-9]+(-[a-z0-9]+)*$`. If the slug is missing/invalid, stop with the reason before any write.
|
|
23
|
+
4. Check the archive target `requirements/archive/<slug>.md`. If it already exists, stop, ask the user how to proceed, and leave it unchanged, writing nothing.
|
|
24
|
+
5. Write the fully-updated archived content to `requirements/archive/<slug>.md`: set `status: archived`, add `archived_at: <ISO date>`, and preserve every other frontmatter field and the entire body unchanged.
|
|
25
|
+
6. Confirm it landed as written, then remove the source active doc.
|
|
26
|
+
7. After archiving, confirm zero active documents remain.
|
|
27
|
+
|
|
28
|
+
## Output
|
|
29
|
+
|
|
30
|
+
Either a one-line refusal that names the missing/invalid slug, a stop-and-ask response when `requirements/archive/<slug>.md` already exists and it was left unchanged, writing nothing, or the archived path (`requirements/archive/<slug>.md`) plus confirmation that it landed before the source active doc was removed and that no active requirement docs remain.
|
|
31
|
+
|
|
32
|
+
## Conventions shared across xsk skills
|
|
33
|
+
|
|
34
|
+
- Triggers are matched by intent, not by exact wording. The phrases listed under "When to use" are cues, not a required incantation.
|
|
35
|
+
- Write in natural, direct prose. No formulaic openers, no filler conclusions, no restating the request before you answer it.
|
|
36
|
+
- When a decision would change the implementation, surface it as a short question and let the user decide. Do not pick silently.
|
|
37
|
+
- These are instruction skills. They shape how work is approached, not what the agent is technically capable of.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xsk-bypass-claude
|
|
3
|
+
description: Set the current project to Claude Code bypass-permissions mode by writing .claude/settings.local.json. Claude only.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xsk-bypass-claude
|
|
7
|
+
|
|
8
|
+
Set the current project to Claude Code bypass-permissions mode (auto-approve tools) by writing `.claude/settings.local.json`. This is a Claude Code-only skill; it has no effect on Codex, opencode, or Gemini.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Match the intent, not the exact words. Common cues:
|
|
13
|
+
|
|
14
|
+
- "bypass permissions", "auto approve", "set bypassPermissions"
|
|
15
|
+
- "跳过权限", "免确认", "设置 bypassPermissions"
|
|
16
|
+
- any request to make Claude Code stop asking for tool approval in the current project
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
Operate on the **current project directory** only. The target is always `.claude/settings.local.json`.
|
|
21
|
+
|
|
22
|
+
1. If the current agent is not Claude Code, refuse the request, state that this is a Claude Code-only skill, write nothing, and stop.
|
|
23
|
+
|
|
24
|
+
2. Never write or modify `.claude/settings.json`.
|
|
25
|
+
|
|
26
|
+
3. If `.claude/settings.local.json` does not exist, create the `.claude/` directory and write exactly:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"permissions": {
|
|
31
|
+
"defaultMode": "bypassPermissions"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. If `.claude/settings.local.json` already exists, read it. If the file is invalid JSON or is not a JSON object, report that `.claude/settings.local.json` is malformed, write nothing, and stop.
|
|
37
|
+
|
|
38
|
+
5. Otherwise, set only `permissions.defaultMode` to `"bypassPermissions"` in `.claude/settings.local.json`, preserve every other field and its value, and write back with 2-space indentation and a trailing newline. Do not reorder, reformat, or drop existing keys.
|
|
39
|
+
|
|
40
|
+
6. If `permissions.defaultMode` is already `"bypassPermissions"`, make no change (idempotent no-op).
|
|
41
|
+
|
|
42
|
+
`permissions.defaultMode = "bypassPermissions"` is the verified Claude Code setting for auto-approving tools.
|
|
43
|
+
|
|
44
|
+
## Output
|
|
45
|
+
|
|
46
|
+
Report only the path written (`.claude/settings.local.json`) and that `permissions.defaultMode` is `bypassPermissions`. Do not include preserved setting values. Take no further action.
|
|
47
|
+
|
|
48
|
+
## Conventions shared across xsk skills
|
|
49
|
+
|
|
50
|
+
- Triggers are matched by intent, not by exact wording. The phrases listed under "When to use" are cues, not a required incantation.
|
|
51
|
+
- Write in natural, direct prose. No formulaic openers, no filler conclusions, no restating the request before you answer it.
|
|
52
|
+
- When a decision would change the implementation, surface it as a short question and let the user decide. Do not pick silently.
|
|
53
|
+
- These are instruction skills. They shape how work is approached, not what the agent is technically capable of.
|