@djordje-stojanovic/sigmaskills 0.2.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +342 -0
  4. package/bin/sigmaskills.js +7 -0
  5. package/manifest.json +28 -0
  6. package/package.json +42 -0
  7. package/registry/agent-hosts.json +2404 -0
  8. package/registry/schema.json +110 -0
  9. package/registry/skill-baselines.json +4 -0
  10. package/registry/source.json +6 -0
  11. package/sigmabrief/SKILL.md +56 -0
  12. package/sigmabrief/agents/openai.yaml +12 -0
  13. package/sigmabrief/references/brief-method.md +73 -0
  14. package/sigmabrief/references/prompt-contract.md +174 -0
  15. package/sigmaperformance/SKILL.md +118 -0
  16. package/sigmaperformance/agents/openai.yaml +12 -0
  17. package/sigmaperformance/references/audit-method.md +112 -0
  18. package/sigmaperformance/references/calibration.md +45 -0
  19. package/sigmaperformance/references/report-contract.md +103 -0
  20. package/sigmareview/SKILL.md +133 -0
  21. package/sigmareview/agents/openai.yaml +12 -0
  22. package/sigmareview/references/report-contract.md +217 -0
  23. package/sigmareview/references/review-method.md +233 -0
  24. package/sigmawrite/SKILL.md +45 -0
  25. package/sigmawrite/agents/openai.yaml +12 -0
  26. package/src/adoption.js +370 -0
  27. package/src/backup.js +398 -0
  28. package/src/catalog.js +211 -0
  29. package/src/cli.js +657 -0
  30. package/src/customization.js +344 -0
  31. package/src/destinations.js +491 -0
  32. package/src/interactive.js +959 -0
  33. package/src/links.js +157 -0
  34. package/src/plan.js +429 -0
  35. package/src/prepack.js +10 -0
  36. package/src/project-lock.js +169 -0
  37. package/src/purge.js +477 -0
  38. package/src/registry/automation-ci.js +411 -0
  39. package/src/registry/automation.js +554 -0
  40. package/src/registry/diff.js +149 -0
  41. package/src/registry/normalize.js +67 -0
  42. package/src/registry/parse.js +184 -0
  43. package/src/registry/sync.js +230 -0
  44. package/src/registry/validate.js +223 -0
  45. package/src/release-ci.js +12 -0
  46. package/src/release.js +837 -0
  47. package/src/restore.js +518 -0
  48. package/src/revision.js +82 -0
  49. package/src/state.js +480 -0
  50. package/src/status.js +469 -0
  51. package/src/transaction.js +636 -0
  52. package/src/uninstall.js +647 -0
  53. package/src/update.js +815 -0
@@ -0,0 +1,647 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { commitSkillBackup, copySkillTree, exportSkillTree, pruneOlderBackups } from './backup.js';
4
+ import { resolveHomeDir } from './destinations.js';
5
+ import { createSkillLink, inspectManagedPath, pathExists, removeManagedPath } from './links.js';
6
+ import { loadProjectLock, removeProjectLockSkill, saveProjectLock } from './project-lock.js';
7
+ import {
8
+ getGlobalStateDir,
9
+ getProjectStateDir,
10
+ loadGlobalState,
11
+ loadProjectState,
12
+ removeSkillFromState,
13
+ saveGlobalState,
14
+ saveProjectState,
15
+ } from './state.js';
16
+ import { collectStatus } from './status.js';
17
+ import { acquireConcurrencyLock } from './transaction.js';
18
+
19
+ export const UNINSTALL_SCHEMA_VERSION = 1;
20
+ export const UNINSTALL_JOURNAL_FILENAME = 'uninstall-journal.json';
21
+
22
+ const CHANGED_KINDS = new Set([
23
+ 'outside-change',
24
+ 'outside-addition',
25
+ 'outside-deletion',
26
+ 'extra-resource',
27
+ 'missing-resource',
28
+ 'copy-disagreement',
29
+ ]);
30
+
31
+ function codedError(message, code) {
32
+ const err = new Error(message);
33
+ err.code = code;
34
+ return err;
35
+ }
36
+
37
+ function resolveRoot(options) {
38
+ const scope = options.scope || 'project';
39
+ if (scope === 'global') return path.resolve(options.homeDir || resolveHomeDir(options.env || process.env));
40
+ return path.resolve(options.projectRoot || process.cwd());
41
+ }
42
+
43
+ function resolveStateDir(scope, root, customStateDir) {
44
+ return scope === 'global'
45
+ ? getGlobalStateDir(root, customStateDir)
46
+ : getProjectStateDir(root, customStateDir);
47
+ }
48
+
49
+ function loadState(scope, root, customStateDir) {
50
+ return scope === 'global'
51
+ ? loadGlobalState(root, customStateDir)
52
+ : loadProjectState(root, customStateDir);
53
+ }
54
+
55
+ function posix(value) {
56
+ return String(value || '').replace(/\\/g, '/');
57
+ }
58
+
59
+ function reviewKindFrom(classifications) {
60
+ const kinds = classifications || [];
61
+ if (kinds.includes('malformed-markers')) return 'malformed';
62
+ if (kinds.some((kind) => CHANGED_KINDS.has(kind))) return 'changed';
63
+ if (kinds.includes('valid-customization')) return 'customized';
64
+ return 'clean';
65
+ }
66
+
67
+ function choicesFor(reviewKind) {
68
+ return reviewKind === 'clean' ? ['keep', 'remove'] : ['backup', 'delete', 'export', 'keep'];
69
+ }
70
+
71
+ function deletionOrder(destinations) {
72
+ const links = destinations.filter((dest) => dest.method && dest.method !== 'copy');
73
+ const hostCopies = destinations.filter((dest) => dest.method === 'copy' && dest.kind !== 'canonical');
74
+ const canonical = destinations.filter((dest) => dest.kind === 'canonical');
75
+ const rest = destinations.filter((dest) => !links.includes(dest) && !hostCopies.includes(dest) && !canonical.includes(dest));
76
+ return [...links, ...hostCopies, ...rest, ...canonical];
77
+ }
78
+
79
+ function resolveExpectedTarget(dest, root, canonicalAbs) {
80
+ if (dest.method === 'copy' || !dest.method) return null;
81
+ if (dest.dependsOn) return path.resolve(root, ...posix(dest.dependsOn).split('/'));
82
+ return canonicalAbs;
83
+ }
84
+
85
+ function classifyUninstallSkill(options, skillId) {
86
+ const scope = options.scope || 'project';
87
+ const root = resolveRoot(options);
88
+ const state = loadState(scope, root, options.customStateDir);
89
+ const entry = state.skills?.[skillId];
90
+ const status = collectStatus({
91
+ ...options,
92
+ projectRoot: scope === 'project' ? root : options.projectRoot,
93
+ homeDir: scope === 'global' ? root : options.homeDir,
94
+ scope,
95
+ });
96
+ const statusSkill = (status.skills || []).find((skill) => skill.id === skillId);
97
+ const fallbackRel = entry?.destination || `.agents/skills/${skillId}`;
98
+ const canonicalRel = posix(
99
+ (Array.isArray(entry?.copies) ? entry.copies.find((copy) => copy.kind === 'canonical')?.destination : null)
100
+ || entry?.destination
101
+ || fallbackRel,
102
+ );
103
+ const canonicalAbs = path.resolve(root, ...canonicalRel.split('/'));
104
+ const copies = Array.isArray(entry?.copies) && entry.copies.length > 0
105
+ ? entry.copies
106
+ : (entry ? [{
107
+ kind: 'canonical',
108
+ destination: canonicalRel,
109
+ method: entry.method || 'copy',
110
+ dependsOn: null,
111
+ hostIds: [],
112
+ ownedPaths: entry.ownedPaths || [canonicalRel],
113
+ }] : []);
114
+
115
+ const classifications = statusSkill
116
+ ? statusSkill.destinations.flatMap((dest) => dest.classifications || [])
117
+ : [];
118
+ const reviewKind = reviewKindFrom(classifications);
119
+ const destinations = copies.map((copy) => {
120
+ const relativeDestination = posix(copy.destination);
121
+ const absolutePath = path.resolve(root, ...relativeDestination.split('/'));
122
+ const statusDest = statusSkill?.destinations?.find((dest) => posix(dest.relativeDestination) === relativeDestination);
123
+ const method = copy.method || statusDest?.method || 'copy';
124
+ const kind = copy.kind || (relativeDestination === canonicalRel ? 'canonical' : 'host');
125
+ const dependsOn = copy.dependsOn || null;
126
+ const inspect = pathExists(absolutePath)
127
+ ? inspectManagedPath(absolutePath, resolveExpectedTarget({ method, dependsOn, absolutePath }, root, canonicalAbs))
128
+ : { missing: true, method: null, wrongTarget: false, broken: false };
129
+ const recordedLink = method && method !== 'copy';
130
+ const liveLink = inspect.method && inspect.method !== 'copy';
131
+ const methodMismatch = !inspect.missing && Boolean(recordedLink) !== Boolean(liveLink);
132
+ return {
133
+ relativeDestination,
134
+ absolutePath,
135
+ method,
136
+ kind,
137
+ dependsOn,
138
+ hostIds: copy.hostIds || statusDest?.hostIds || [],
139
+ classifications: statusDest?.classifications || (inspect.missing ? ['missing-destination'] : ['clean']),
140
+ missing: Boolean(inspect.missing),
141
+ wrongTarget: Boolean(inspect.wrongTarget),
142
+ broken: Boolean(inspect.broken),
143
+ unownedReplacement: Boolean(methodMismatch),
144
+ };
145
+ });
146
+
147
+ const blockedReasons = [];
148
+ if (!entry) {
149
+ blockedReasons.push('stale-state');
150
+ if (pathExists(canonicalAbs)) blockedReasons.push('unowned');
151
+ }
152
+ for (const dest of destinations) {
153
+ if (dest.missing) blockedReasons.push('missing-destination');
154
+ if (dest.wrongTarget) blockedReasons.push('wrong-target');
155
+ if (dest.broken) blockedReasons.push('broken-link');
156
+ if (dest.unownedReplacement) blockedReasons.push('unowned');
157
+ if ((dest.classifications || []).includes('copy-disagreement')) blockedReasons.push('copy-disagreement');
158
+ if ((dest.classifications || []).includes('stale-state')) blockedReasons.push('stale-state');
159
+ }
160
+
161
+ const uniqueReasons = [...new Set(blockedReasons)];
162
+ const remainingCanonicalDependencies = destinations
163
+ .filter((dest) => dest.dependsOn && posix(dest.dependsOn) === canonicalRel)
164
+ .map((dest) => dest.relativeDestination);
165
+
166
+ return {
167
+ id: skillId,
168
+ reviewKind,
169
+ choices: choicesFor(reviewKind),
170
+ choice: null,
171
+ scope,
172
+ lastBackup: entry?.lastBackup || null,
173
+ canonicalRel,
174
+ canonicalAbs,
175
+ remainingCanonicalDependencies,
176
+ destinations,
177
+ blocked: uniqueReasons.length > 0,
178
+ blockedReasons: uniqueReasons,
179
+ owned: Boolean(entry),
180
+ };
181
+ }
182
+
183
+ function resolveChoice(skill, options) {
184
+ if (skill.reviewKind === 'clean') return options.clean || null;
185
+ return options.changed || null;
186
+ }
187
+
188
+ function actionName(choice) {
189
+ return choice || 'remove';
190
+ }
191
+
192
+ function plannedOutcome(skill) {
193
+ if (skill.blocked && skill.choice !== 'keep') return 'skipped';
194
+ if (skill.choice === 'keep') return 'retained';
195
+ if (!skill.choice) return 'skipped';
196
+ return 'removed';
197
+ }
198
+
199
+ function stateChangeFor(skill) {
200
+ if (skill.choice === 'keep' || skill.outcome === 'retained' || skill.outcome === 'skipped' || skill.outcome === 'failed') {
201
+ return 'unchanged';
202
+ }
203
+ return 'drop ownership';
204
+ }
205
+
206
+ function summarize(skills) {
207
+ const summary = {
208
+ removed: [],
209
+ retained: [],
210
+ skipped: [],
211
+ failed: [],
212
+ };
213
+ for (const skill of skills) {
214
+ const bucket = skill.outcome;
215
+ if (summary[bucket]) summary[bucket].push(skill.id);
216
+ }
217
+ return summary;
218
+ }
219
+
220
+ function getJournalPath(stateDir) {
221
+ return path.join(stateDir, UNINSTALL_JOURNAL_FILENAME);
222
+ }
223
+
224
+ function persistJournal(stateDir, journal) {
225
+ fs.mkdirSync(stateDir, { recursive: true });
226
+ const target = getJournalPath(stateDir);
227
+ const tmp = `${target}.${process.pid}.tmp`;
228
+ fs.writeFileSync(tmp, `${JSON.stringify(journal, null, 2)}\n`, 'utf8');
229
+ fs.renameSync(tmp, target);
230
+ }
231
+
232
+ function clearJournal(stateDir) {
233
+ const target = getJournalPath(stateDir);
234
+ if (pathExists(target)) fs.rmSync(target, { force: true });
235
+ }
236
+
237
+ function journalFrom(plan, status) {
238
+ return {
239
+ schemaVersion: UNINSTALL_SCHEMA_VERSION,
240
+ command: 'uninstall',
241
+ all: true,
242
+ scope: plan.scope,
243
+ status,
244
+ startedAt: plan.startedAt,
245
+ skills: (plan.skills || []).map((skill) => ({
246
+ id: skill.id,
247
+ outcome: skill.outcome || 'pending',
248
+ choice: skill.choice || null,
249
+ lastBackup: skill.lastBackup || null,
250
+ error: skill.error || null,
251
+ })),
252
+ };
253
+ }
254
+
255
+ function recordedSkillIds(options) {
256
+ const requested = Array.isArray(options.skillIds) ? options.skillIds.filter(Boolean) : [];
257
+ if (options.all && requested.length > 0) {
258
+ throw codedError('uninstall --all cannot be combined with --skill', 'usage');
259
+ }
260
+ if (!options.all) return requested;
261
+ const scope = options.scope || 'project';
262
+ const root = resolveRoot(options);
263
+ return Object.keys(loadState(scope, root, options.customStateDir).skills || {}).sort();
264
+ }
265
+
266
+ /**
267
+ * Preview Uninstall Review for selected skills or every recorded skill in one scope.
268
+ *
269
+ * @param {object} options
270
+ * @returns {object}
271
+ */
272
+ export function createUninstallPlan(options = {}) {
273
+ const scope = options.scope || 'project';
274
+ const skillIds = recordedSkillIds(options);
275
+ const resolved = {
276
+ ...options,
277
+ clean: options.clean || (options.yes || options.all ? 'remove' : undefined),
278
+ changed: options.changed || (options.all ? 'backup' : undefined),
279
+ };
280
+ const skills = skillIds.map((skillId) => {
281
+ const skill = classifyUninstallSkill(resolved, skillId);
282
+ return { ...skill, choice: resolveChoice(skill, resolved) };
283
+ });
284
+ const plan = {
285
+ schemaVersion: UNINSTALL_SCHEMA_VERSION,
286
+ command: 'uninstall',
287
+ all: Boolean(options.all),
288
+ scope,
289
+ dryRun: Boolean(options.dryRun),
290
+ skills,
291
+ };
292
+ if (plan.all) {
293
+ for (const skill of skills) {
294
+ skill.outcome = plannedOutcome(skill);
295
+ skill.stateChange = stateChangeFor(skill);
296
+ }
297
+ plan.summary = summarize(skills);
298
+ }
299
+ return plan;
300
+ }
301
+
302
+ function inspectLiveLeaf(dest, root, canonicalAbs) {
303
+ if (!pathExists(dest.absolutePath)) {
304
+ return { missing: true, method: null, wrongTarget: false, broken: false };
305
+ }
306
+ return inspectManagedPath(
307
+ dest.absolutePath,
308
+ resolveExpectedTarget(dest, root, canonicalAbs),
309
+ );
310
+ }
311
+
312
+ function leafStopReason(dest, inspect) {
313
+ if (inspect.wrongTarget) return 'wrong-target';
314
+ const recordedLink = dest.method && dest.method !== 'copy';
315
+ const liveLink = inspect.method && inspect.method !== 'copy';
316
+ if (!inspect.missing && Boolean(recordedLink) !== Boolean(liveLink)) return 'unowned';
317
+ return null;
318
+ }
319
+
320
+ function restoreStaged(root, staged) {
321
+ for (const item of staged.slice().reverse()) {
322
+ try {
323
+ if (item.kind === 'link') {
324
+ if (!pathExists(item.absolutePath) && item.target && pathExists(item.target)) {
325
+ createSkillLink(item.absolutePath, item.target, root);
326
+ }
327
+ } else if (item.kind === 'tree' && !pathExists(item.absolutePath) && pathExists(item.staged)) {
328
+ fs.mkdirSync(path.dirname(item.absolutePath), { recursive: true });
329
+ copySkillTree(item.staged, item.absolutePath);
330
+ }
331
+ } catch {
332
+ // Rollback continues for remaining leaves.
333
+ }
334
+ }
335
+ }
336
+
337
+ function applyOneUninstall(options, skill) {
338
+ const scope = options.scope || 'project';
339
+ const root = resolveRoot(options);
340
+ const stateDir = resolveStateDir(scope, root, options.customStateDir);
341
+ const stagingParent = path.join(stateDir, '.sigma-uninstall-staging');
342
+ const stagingDir = path.join(
343
+ stagingParent,
344
+ `${skill.id}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
345
+ );
346
+ const staged = [];
347
+ let backupDir = null;
348
+
349
+ const cleanupStaging = () => {
350
+ try {
351
+ if (pathExists(stagingDir)) fs.rmSync(stagingDir, { recursive: true, force: true });
352
+ if (pathExists(stagingParent) && fs.readdirSync(stagingParent).length === 0) {
353
+ fs.rmSync(stagingParent, { recursive: true, force: true });
354
+ }
355
+ } catch {
356
+ // Staging cleanup is best-effort.
357
+ }
358
+ };
359
+
360
+ try {
361
+ const live = classifyUninstallSkill(options, skill.id);
362
+ if (skill.choice === 'keep') {
363
+ return { action: 'keep', backupDir: null, staged, cleanupStaging };
364
+ }
365
+ const hard = live.blockedReasons.filter((reason) => (
366
+ reason === 'wrong-target'
367
+ || reason === 'unowned'
368
+ || reason === 'missing-destination'
369
+ || reason === 'stale-state'
370
+ || reason === 'broken-link'
371
+ || (reason === 'copy-disagreement' && skill.reviewKind === 'clean')
372
+ ));
373
+ if (hard.length > 0) {
374
+ throw codedError(`${hard[0]} stopped uninstall of '${skill.id}'`, hard[0]);
375
+ }
376
+
377
+ fs.mkdirSync(stagingDir, { recursive: true });
378
+ const ordered = deletionOrder(live.destinations);
379
+ for (const dest of ordered) {
380
+ const inspect = inspectLiveLeaf(dest, root, live.canonicalAbs);
381
+ const stop = leafStopReason(dest, inspect);
382
+ if (stop) throw codedError(`${stop} stopped uninstall of '${skill.id}'`, stop);
383
+ if (inspect.missing) continue;
384
+ if (inspect.method && inspect.method !== 'copy') {
385
+ staged.push({
386
+ kind: 'link',
387
+ absolutePath: dest.absolutePath,
388
+ target: inspect.target || live.canonicalAbs,
389
+ });
390
+ continue;
391
+ }
392
+ const stagedPath = path.join(stagingDir, dest.relativeDestination.replace(/[\\/]/g, '__'));
393
+ copySkillTree(dest.absolutePath, stagedPath);
394
+ staged.push({
395
+ kind: 'tree',
396
+ absolutePath: dest.absolutePath,
397
+ staged: stagedPath,
398
+ });
399
+ }
400
+
401
+ if (skill.choice === 'export') {
402
+ exportSkillTree({
403
+ sourceDir: live.canonicalAbs,
404
+ exportRoot: options.exportDir || path.join(root, 'sigmaskills-export'),
405
+ skillId: skill.id,
406
+ refuseCollision: true,
407
+ });
408
+ }
409
+
410
+ if (skill.choice === 'backup') {
411
+ const state = loadState(scope, root, options.customStateDir);
412
+ const entry = state.skills?.[skill.id];
413
+ const backupFn = options.backupSkill || commitSkillBackup;
414
+ backupDir = backupFn({
415
+ stateDir,
416
+ skillId: skill.id,
417
+ sourceDir: live.canonicalAbs,
418
+ ownership: {
419
+ scope,
420
+ release: entry?.release || null,
421
+ revision: entry?.revision || null,
422
+ method: entry?.method || 'copy',
423
+ canonicalTarget: live.canonicalRel,
424
+ copies: entry?.copies || [],
425
+ ownedPaths: entry?.ownedPaths || [live.canonicalRel],
426
+ },
427
+ });
428
+ }
429
+
430
+ for (const dest of ordered) {
431
+ const inspect = inspectLiveLeaf(dest, root, live.canonicalAbs);
432
+ const stop = leafStopReason(dest, inspect);
433
+ if (stop) throw codedError(`${stop} stopped uninstall of '${skill.id}'`, stop);
434
+ if (inspect.missing) continue;
435
+ if (dest.kind === 'canonical') {
436
+ const dependents = live.destinations.filter((item) => (
437
+ item.dependsOn
438
+ && posix(item.dependsOn) === live.canonicalRel
439
+ && pathExists(item.absolutePath)
440
+ ));
441
+ if (dependents.length > 0) {
442
+ throw codedError(
443
+ `canonical content remains while destinations still depend on it: ${skill.id}`,
444
+ 'canonical-dependency',
445
+ );
446
+ }
447
+ }
448
+ removeManagedPath(dest.absolutePath);
449
+ if (dest.method && dest.method !== 'copy' && typeof options.afterUnlink === 'function') {
450
+ options.afterUnlink(dest.absolutePath);
451
+ }
452
+ }
453
+
454
+ if (typeof options.afterStage === 'function') options.afterStage(stagingDir);
455
+ return { action: actionName(skill.choice), backupDir, staged, cleanupStaging };
456
+ } catch (err) {
457
+ restoreStaged(root, staged);
458
+ if (backupDir && pathExists(backupDir) && skill.choice === 'backup') {
459
+ try {
460
+ fs.rmSync(backupDir, { recursive: true, force: true });
461
+ } catch {
462
+ // Drop the unused snapshot so the retained backup stays the latest.
463
+ }
464
+ }
465
+ cleanupStaging();
466
+ throw err;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Uninstall selected skills, or every recorded skill in one scope, after Uninstall Review.
472
+ *
473
+ * @param {object} options
474
+ * @returns {object}
475
+ */
476
+ export function executeUninstall(options = {}) {
477
+ const plan = createUninstallPlan(options);
478
+ if (options.dryRun) {
479
+ return { ...plan, dryRun: true };
480
+ }
481
+
482
+ for (const skill of plan.skills) {
483
+ if (!skill.choice) {
484
+ if (skill.reviewKind === 'clean') {
485
+ throw codedError('uninstall of a clean skill needs --clean remove or keep', 'review');
486
+ }
487
+ throw codedError(
488
+ `changed, customized, or malformed skill '${skill.id}' needs --changed backup, keep, export, or delete`,
489
+ 'review',
490
+ );
491
+ }
492
+ if (skill.reviewKind === 'clean' && skill.choice !== 'remove' && skill.choice !== 'keep') {
493
+ throw codedError('--clean must be remove or keep', 'review');
494
+ }
495
+ if (skill.reviewKind !== 'clean' && !['backup', 'keep', 'export', 'delete'].includes(skill.choice)) {
496
+ throw codedError('--changed must be backup, keep, export, or delete', 'review');
497
+ }
498
+ }
499
+
500
+ const scope = options.scope || 'project';
501
+ const root = resolveRoot(options);
502
+ const customStateDir = options.customStateDir;
503
+ const stateDir = resolveStateDir(scope, root, customStateDir);
504
+ const releaseLock = acquireConcurrencyLock(root, customStateDir);
505
+ let state = loadState(scope, root, customStateDir);
506
+ plan.startedAt = new Date().toISOString();
507
+
508
+ const writeAllJournal = (status) => {
509
+ if (!plan.all) return;
510
+ persistJournal(stateDir, journalFrom(plan, status));
511
+ };
512
+
513
+ try {
514
+ if (plan.all) writeAllJournal('in-progress');
515
+
516
+ for (let index = 0; index < plan.skills.length; index += 1) {
517
+ const skill = plan.skills[index];
518
+ if (plan.all && skill.choice !== 'keep' && skill.blocked) {
519
+ skill.outcome = 'skipped';
520
+ skill.error = (skill.blockedReasons || []).join(', ') || 'blocked';
521
+ skill.stateChange = stateChangeFor(skill);
522
+ writeAllJournal('in-progress');
523
+ continue;
524
+ }
525
+
526
+ try {
527
+ const applied = applyOneUninstall(options, skill);
528
+ skill.action = applied.action;
529
+ if (skill.choice === 'keep') {
530
+ skill.outcome = 'retained';
531
+ skill.stateChange = stateChangeFor(skill);
532
+ writeAllJournal('in-progress');
533
+ continue;
534
+ }
535
+
536
+ const priorState = state;
537
+ const persist = options.saveState || (scope === 'global' ? saveGlobalState : saveProjectState);
538
+ try {
539
+ state = removeSkillFromState(state, skill.id);
540
+ persist(root, state, customStateDir);
541
+ if (scope !== 'global') {
542
+ const lock = loadProjectLock(root);
543
+ saveProjectLock(root, removeProjectLockSkill(lock, skill.id));
544
+ }
545
+ } catch (err) {
546
+ restoreStaged(root, applied.staged || []);
547
+ persist(root, priorState, customStateDir);
548
+ if (typeof applied.cleanupStaging === 'function') applied.cleanupStaging();
549
+ throw err;
550
+ }
551
+ if (skill.choice === 'backup' && applied.backupDir) {
552
+ pruneOlderBackups({
553
+ stateDir,
554
+ skillId: skill.id,
555
+ keepPath: applied.backupDir,
556
+ });
557
+ skill.lastBackup = path.relative(stateDir, applied.backupDir).replace(/\\/g, '/');
558
+ }
559
+ if (typeof applied.cleanupStaging === 'function') applied.cleanupStaging();
560
+ skill.outcome = 'removed';
561
+ skill.stateChange = stateChangeFor(skill);
562
+ writeAllJournal('in-progress');
563
+ } catch (err) {
564
+ skill.outcome = 'failed';
565
+ skill.error = err.message;
566
+ skill.stateChange = stateChangeFor(skill);
567
+ if (plan.all) {
568
+ for (const remaining of plan.skills.slice(index + 1)) {
569
+ remaining.outcome = 'skipped';
570
+ remaining.error = 'stopped-after-failure';
571
+ remaining.stateChange = stateChangeFor(remaining);
572
+ }
573
+ writeAllJournal('failed');
574
+ break;
575
+ }
576
+ throw err;
577
+ }
578
+ }
579
+
580
+ if (plan.all) {
581
+ plan.summary = summarize(plan.skills);
582
+ if ((plan.summary.failed || []).length === 0) {
583
+ clearJournal(stateDir);
584
+ }
585
+ }
586
+ return { ...plan, dryRun: false, state };
587
+ } finally {
588
+ releaseLock();
589
+ }
590
+ }
591
+
592
+ /**
593
+ * @param {object} result
594
+ * @returns {string}
595
+ */
596
+ export function formatUninstallJson(result) {
597
+ return `${JSON.stringify(result, null, 2)}\n`;
598
+ }
599
+
600
+ /**
601
+ * @param {object} result
602
+ * @returns {string}
603
+ */
604
+ export function formatUninstallHuman(result) {
605
+ const lines = [
606
+ `SigmaSkills Uninstall ${result.dryRun ? 'Preview' : 'Result'}`,
607
+ `Scope: ${result.scope}`,
608
+ '',
609
+ ];
610
+ if (result.all) {
611
+ lines.push('Mode: every recorded skill in this scope');
612
+ lines.push('');
613
+ }
614
+ for (const skill of result.skills || []) {
615
+ lines.push(`Skill: ${skill.id}`);
616
+ lines.push(` Review: ${skill.reviewKind}`);
617
+ lines.push(` Choices: ${(skill.choices || []).join(', ')}`);
618
+ if (skill.choice) lines.push(` Choice: ${skill.choice}`);
619
+ if (skill.action) lines.push(` Action: ${skill.action}`);
620
+ if (skill.outcome) lines.push(` Outcome: ${skill.outcome}`);
621
+ lines.push(` Scope: ${skill.scope}`);
622
+ if (skill.canonicalRel) lines.push(` Canonical: ${skill.canonicalRel}`);
623
+ const deps = (skill.remainingCanonicalDependencies || []).join(', ');
624
+ lines.push(` Remaining canonical dependencies: ${deps || 'none'}`);
625
+ if (result.all) {
626
+ lines.push(` Retained backup: ${skill.lastBackup || 'none'}`);
627
+ lines.push(` State: ${skill.stateChange || stateChangeFor(skill)}`);
628
+ }
629
+ for (const dest of skill.destinations || []) {
630
+ lines.push(` Path: ${dest.relativeDestination}`);
631
+ lines.push(` Method: ${dest.method || 'none'}`);
632
+ if (dest.dependsOn) lines.push(` Depends on: ${dest.dependsOn}`);
633
+ if (dest.hostIds?.length) lines.push(` Agent Hosts: ${dest.hostIds.join(', ')}`);
634
+ }
635
+ if (skill.blockedReasons?.length) lines.push(` Blocked: ${skill.blockedReasons.join(', ')}`);
636
+ if (skill.error) lines.push(` Error: ${skill.error}`);
637
+ lines.push('');
638
+ }
639
+ if (result.summary) {
640
+ lines.push(`Removed: ${(result.summary.removed || []).join(', ') || 'none'}`);
641
+ lines.push(`Retained: ${(result.summary.retained || []).join(', ') || 'none'}`);
642
+ lines.push(`Skipped: ${(result.summary.skipped || []).join(', ') || 'none'}`);
643
+ lines.push(`Failed: ${(result.summary.failed || []).join(', ') || 'none'}`);
644
+ lines.push('');
645
+ }
646
+ return lines.join('\n');
647
+ }