@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
package/src/links.js ADDED
@@ -0,0 +1,157 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Recommended on-disk link method for this operating system.
6
+ *
7
+ * @returns {'junction' | 'symlink'}
8
+ */
9
+ export function recommendedLinkMethod() {
10
+ return process.platform === 'win32' ? 'junction' : 'symlink';
11
+ }
12
+
13
+ /**
14
+ * Whether a path exists without following a symbolic link or junction.
15
+ *
16
+ * @param {string} destPath
17
+ * @returns {boolean}
18
+ */
19
+ export function pathExists(destPath) {
20
+ try {
21
+ fs.lstatSync(destPath);
22
+ return true;
23
+ } catch (err) {
24
+ if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return false;
25
+ throw err;
26
+ }
27
+ }
28
+
29
+ function isInsideProject(projectRoot, absolutePath) {
30
+ const relative = path.relative(path.resolve(projectRoot), path.resolve(absolutePath));
31
+ return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
32
+ }
33
+
34
+ /**
35
+ * Remove a managed destination without following a link into its target.
36
+ *
37
+ * @param {string} destPath
38
+ */
39
+ export function removeManagedPath(destPath) {
40
+ if (!pathExists(destPath)) return;
41
+ const stat = fs.lstatSync(destPath);
42
+ if (stat.isSymbolicLink()) {
43
+ fs.unlinkSync(destPath);
44
+ return;
45
+ }
46
+ fs.rmSync(destPath, { recursive: true, force: true });
47
+ }
48
+
49
+ /**
50
+ * Inspect a managed destination without following it.
51
+ *
52
+ * @param {string} destPath
53
+ * @param {string} [expectedTarget]
54
+ * @returns {object}
55
+ */
56
+ export function inspectManagedPath(destPath, expectedTarget) {
57
+ if (!pathExists(destPath)) {
58
+ return { method: null, broken: false, wrongTarget: false, missing: true };
59
+ }
60
+
61
+ const stat = fs.lstatSync(destPath);
62
+ if (!stat.isSymbolicLink()) {
63
+ return { method: 'copy', broken: false, wrongTarget: false, missing: false };
64
+ }
65
+
66
+ const method = recommendedLinkMethod();
67
+ let rawTarget;
68
+ try {
69
+ rawTarget = fs.readlinkSync(destPath);
70
+ } catch {
71
+ return { method, broken: true, wrongTarget: false, missing: false };
72
+ }
73
+
74
+ const resolvedTarget = path.resolve(path.dirname(destPath), rawTarget);
75
+ let realPath = null;
76
+ let broken = false;
77
+ try {
78
+ realPath = fs.realpathSync(destPath);
79
+ if (!pathExists(resolvedTarget) && !fs.existsSync(resolvedTarget)) broken = true;
80
+ } catch {
81
+ broken = true;
82
+ }
83
+
84
+ let wrongTarget = false;
85
+ if (expectedTarget) {
86
+ if (realPath) {
87
+ try {
88
+ wrongTarget = realPath !== fs.realpathSync(expectedTarget);
89
+ } catch {
90
+ wrongTarget = true;
91
+ }
92
+ } else {
93
+ wrongTarget = path.resolve(resolvedTarget) !== path.resolve(expectedTarget);
94
+ }
95
+ }
96
+
97
+ return { method, broken, wrongTarget, missing: false, target: resolvedTarget };
98
+ }
99
+
100
+ /**
101
+ * Create a directory junction on Windows or a symbolic link elsewhere.
102
+ *
103
+ * @param {string} linkPath
104
+ * @param {string} targetPath
105
+ * @param {string} [projectRoot]
106
+ * @returns {{ method: 'junction' | 'symlink', linkPath: string, targetPath: string }}
107
+ */
108
+ export function createSkillLink(linkPath, targetPath, projectRoot) {
109
+ const resolvedLink = path.resolve(linkPath);
110
+ const resolvedTarget = path.resolve(targetPath);
111
+ const method = recommendedLinkMethod();
112
+
113
+ if (projectRoot) {
114
+ if (!isInsideProject(projectRoot, resolvedLink)) {
115
+ throw new Error(`link destination '${resolvedLink}' escapes the project`);
116
+ }
117
+ if (!isInsideProject(projectRoot, resolvedTarget)) {
118
+ throw new Error(`link target '${resolvedTarget}' escapes the project`);
119
+ }
120
+ }
121
+
122
+ if (!fs.existsSync(resolvedTarget)) {
123
+ throw new Error(`link target '${resolvedTarget}' does not exist`);
124
+ }
125
+
126
+ if (pathExists(resolvedLink)) {
127
+ throw new Error(`link destination '${resolvedLink}' already exists`);
128
+ }
129
+
130
+ const parent = path.dirname(resolvedLink);
131
+ if (!fs.existsSync(parent)) {
132
+ fs.mkdirSync(parent, { recursive: true });
133
+ }
134
+
135
+ const type = method === 'junction' ? 'junction' : 'dir';
136
+ const symlinkTarget = method === 'junction'
137
+ ? resolvedTarget
138
+ : (path.relative(parent, resolvedTarget) || '.');
139
+
140
+ try {
141
+ fs.symlinkSync(symlinkTarget, resolvedLink, type);
142
+ } catch (err) {
143
+ const wrapped = new Error(`failed to create ${method} at '${resolvedLink}': ${err.message}`);
144
+ wrapped.cause = err;
145
+ wrapped.code = err.code;
146
+ wrapped.linkMethod = method;
147
+ wrapped.linkPath = resolvedLink;
148
+ wrapped.targetPath = resolvedTarget;
149
+ throw wrapped;
150
+ }
151
+
152
+ return {
153
+ method,
154
+ linkPath: resolvedLink,
155
+ targetPath: resolvedTarget,
156
+ };
157
+ }
package/src/plan.js ADDED
@@ -0,0 +1,429 @@
1
+ import path from 'node:path';
2
+ import { chooseCanonical, classifySkillPath, loadSkillBaselines } from './adoption.js';
3
+ import { loadProjectLock } from './project-lock.js';
4
+ import { isDestinationOwned, loadGlobalState, loadProjectState } from './state.js';
5
+ import {
6
+ UNIVERSAL_PROJECT_DESTINATION,
7
+ defaultSelectedRoots,
8
+ findDestinationConflicts,
9
+ listGlobalDestinationGroups,
10
+ listProjectDestinationGroups,
11
+ loadHostRegistry,
12
+ normalizeRelativeRoot,
13
+ resolveGlobalSkillPath,
14
+ resolveHomeDir,
15
+ resolveSkillPath,
16
+ } from './destinations.js';
17
+ import { plannedExportDir } from './backup.js';
18
+ import { findPackageRoot } from './catalog.js';
19
+ import { pathExists, recommendedLinkMethod } from './links.js';
20
+
21
+ export const PLAN_SCHEMA_VERSION = 1;
22
+
23
+ function resolutionFor(classification, relativeDestination, options = {}) {
24
+ const explicit = options.resolutions?.[relativeDestination];
25
+ if (explicit) return explicit;
26
+ if (classification?.kind === 'changed') return options.adoptChanged;
27
+ if (classification?.kind === 'legacy') return options.adoptLegacy;
28
+ if (classification?.kind === 'unverified') return options.adoptUnverified;
29
+ if (classification?.kind === 'malformed-custom') return options.adoptMalformed;
30
+ return undefined;
31
+ }
32
+
33
+ /**
34
+ * Project Installation classifier: valid Sigma state, then exact Skill Revision, then resolved link.
35
+ *
36
+ * @param {object} params
37
+ * @returns {(skillId: string, destination: string) => object}
38
+ */
39
+ export function createProjectSkillClassifier({ catalog, projectRoot, customStateDir, packageRoot, scope = 'project', homeDir }) {
40
+ const root = scope === 'global' ? path.resolve(homeDir) : projectRoot;
41
+ const managedState = scope === 'global'
42
+ ? loadGlobalState(root, customStateDir)
43
+ : loadProjectState(root, customStateDir);
44
+ const baselines = loadSkillBaselines(packageRoot || findPackageRoot());
45
+ const resolvePath = scope === 'global' ? resolveGlobalSkillPath : resolveSkillPath;
46
+ return (skillId, destination) => {
47
+ const skill = catalog.skills.find((item) => item.id === skillId);
48
+ const expectedCanonical = resolvePath(root, UNIVERSAL_PROJECT_DESTINATION, skillId);
49
+ const entry = managedState.skills?.[skillId];
50
+ return classifySkillPath({
51
+ destPath: destination,
52
+ skillId,
53
+ bundledRevision: skill?.revision,
54
+ bundledFiles: skill?.files || {},
55
+ bundledBaselines: Array.isArray(baselines?.[skillId]) ? baselines[skillId] : [],
56
+ expectedCanonicalPath: expectedCanonical.destination,
57
+ sigmaOwned: isDestinationOwned(root, skillId, destination, customStateDir, { scope }),
58
+ sigmaRevision: entry?.revision || null,
59
+ baseHashes: entry?.baseHashes || null,
60
+ });
61
+ };
62
+ }
63
+
64
+ function destinationGroupsFor(options, root, scope) {
65
+ if (options.destinationGroups) return options.destinationGroups;
66
+ const packageRoot = options.packageRoot || findPackageRoot();
67
+ const registry = options.registry || loadHostRegistry(packageRoot);
68
+ const env = options.env || process.env;
69
+ if (scope === 'global') {
70
+ return listGlobalDestinationGroups({ registry, homeDir: root, env });
71
+ }
72
+ return listProjectDestinationGroups({ registry, projectRoot: root, env });
73
+ }
74
+
75
+ function selectedRootsFor(options, groups) {
76
+ if (Array.isArray(options.selectedRoots) && options.selectedRoots.length > 0) {
77
+ return options.selectedRoots.map((root) => normalizeRelativeRoot(root));
78
+ }
79
+ const defaults = defaultSelectedRoots(groups);
80
+ return defaults.length > 0 ? defaults : [UNIVERSAL_PROJECT_DESTINATION];
81
+ }
82
+
83
+ function requestedMethodFor(options, selectedRoots) {
84
+ if (options.method === 'copy' || options.method === 'link') return options.method;
85
+ const hasHost = selectedRoots.some((root) => root !== UNIVERSAL_PROJECT_DESTINATION);
86
+ return hasHost ? 'link' : 'copy';
87
+ }
88
+
89
+ function rootsForRequestedMethod(selectedRoots, method) {
90
+ if (method !== 'link') return selectedRoots;
91
+ const hasHost = selectedRoots.some((root) => root !== UNIVERSAL_PROJECT_DESTINATION);
92
+ if (!hasHost || selectedRoots.includes(UNIVERSAL_PROJECT_DESTINATION)) return selectedRoots;
93
+ return [UNIVERSAL_PROJECT_DESTINATION, ...selectedRoots];
94
+ }
95
+
96
+ function destinationMethod(kind, skillId, requestedMethod, copyRoots, relativeRoot) {
97
+ if (kind === 'canonical') {
98
+ return { method: 'copy', dependsOn: null };
99
+ }
100
+ if (requestedMethod === 'copy' || (Array.isArray(copyRoots) && copyRoots.includes(relativeRoot))) {
101
+ return { method: 'copy', dependsOn: null };
102
+ }
103
+ return {
104
+ method: recommendedLinkMethod(),
105
+ dependsOn: `${UNIVERSAL_PROJECT_DESTINATION}/${skillId}`,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Generate an install plan for a skill without mutating the filesystem.
111
+ *
112
+ * @param {object} catalog Catalog object containing manifest and validated skills
113
+ * @param {object} options
114
+ * @param {string} options.skillId
115
+ * @param {string} [options.projectRoot]
116
+ * @param {string} [options.customStateDir]
117
+ * @param {boolean} [options.dryRun]
118
+ * @param {string[]} [options.selectedRoots]
119
+ * @returns {object} Versioned plan object
120
+ */
121
+ function destinationImpact(dest) {
122
+ if (dest.migratable && !dest.resolution) {
123
+ return { overwrite: 'needs replace, skip, or export', delete: 'none until a choice is made', backup: 'none until a choice is made' };
124
+ }
125
+ if (dest.adoption) {
126
+ return { overwrite: 'adopt in place', delete: 'none', backup: 'none' };
127
+ }
128
+ if (dest.resolution === 'skip') {
129
+ return { overwrite: 'none', delete: 'none', backup: 'none' };
130
+ }
131
+ if (dest.resolution === 'export') {
132
+ return { overwrite: 'none', delete: 'none', backup: 'export copy' };
133
+ }
134
+ const deleted = dest.diff?.deleted?.length ? dest.diff.deleted.join(', ') : 'none';
135
+ if (dest.resolution === 'replace') {
136
+ return { overwrite: 'replace existing tree', delete: deleted, backup: 'private backup before replace' };
137
+ }
138
+ if (pathExists(dest.destination)) {
139
+ return { overwrite: 'replace existing tree', delete: deleted, backup: 'sidecar restore copy' };
140
+ }
141
+ return { overwrite: 'create', delete: 'none', backup: 'none' };
142
+ }
143
+
144
+ export function createInstallPlan(catalog, options) {
145
+ const { skillId, customStateDir, dryRun = false } = options;
146
+ const scope = options.scope || 'project';
147
+ const projectRoot = path.resolve(options.projectRoot || process.cwd());
148
+ const homeDir = path.resolve(options.homeDir || resolveHomeDir(options.env || process.env));
149
+ const root = scope === 'global' ? homeDir : projectRoot;
150
+ const resolvePath = scope === 'global' ? resolveGlobalSkillPath : resolveSkillPath;
151
+
152
+ if (!skillId || typeof skillId !== 'string') {
153
+ throw new Error('missing or invalid skillId for install plan');
154
+ }
155
+
156
+ const skill = catalog.skills.find((s) => s.id === skillId);
157
+ if (!skill) {
158
+ throw new Error(`skill '${skillId}' was not found in Skill Pack ${catalog.manifest.name}`);
159
+ }
160
+
161
+ const groups = destinationGroupsFor(options, root, scope);
162
+ const requestedRoots = selectedRootsFor(options, groups);
163
+ const requestedMethod = requestedMethodFor(options, requestedRoots);
164
+ const selectedRoots = rootsForRequestedMethod(requestedRoots, requestedMethod);
165
+ const classify = createProjectSkillClassifier({
166
+ catalog,
167
+ projectRoot: root,
168
+ customStateDir,
169
+ packageRoot: options.packageRoot,
170
+ scope,
171
+ homeDir,
172
+ });
173
+ const conflictErrors = findDestinationConflicts({
174
+ projectRoot: root,
175
+ skillIds: [skillId],
176
+ selectedRoots,
177
+ isOwned: (id, destination) => isDestinationOwned(root, id, destination, customStateDir, { scope }),
178
+ classify,
179
+ resolvePath: resolvePath,
180
+ });
181
+ const fatal = conflictErrors.filter((message) => !/not owned/i.test(message));
182
+ if (fatal.length > 0) {
183
+ throw new Error(fatal[0]);
184
+ }
185
+
186
+ const currentLock = scope === 'global' ? { skills: {} } : loadProjectLock(projectRoot);
187
+ const skillFiles = Object.keys(skill.files).sort();
188
+ const groupByRoot = new Map(groups.filter((group) => group.selectable).map((group) => [group.relativeRoot, group]));
189
+ const copyRoots = (options.copyRoots || []).map((copyRoot) => normalizeRelativeRoot(copyRoot));
190
+
191
+ const destinations = selectedRoots.map((relativeRoot) => {
192
+ const resolved = resolvePath(root, relativeRoot, skillId);
193
+ const group = groupByRoot.get(relativeRoot);
194
+ const kind = relativeRoot === UNIVERSAL_PROJECT_DESTINATION ? 'canonical' : 'host';
195
+ const owned = isDestinationOwned(root, skillId, resolved.destination, customStateDir, { scope });
196
+ const classification = classify(skillId, resolved.destination);
197
+ const planned = destinationMethod(kind, skillId, requestedMethod, copyRoots, relativeRoot);
198
+ const method = classification.adoptable ? (classification.method || 'copy') : planned.method;
199
+ const dependsOn = method === 'copy' ? null : `${UNIVERSAL_PROJECT_DESTINATION}/${skillId}`;
200
+ const resolution = classification.migratable
201
+ ? resolutionFor(classification, resolved.relativeDestination, options)
202
+ : undefined;
203
+ const migratable = Boolean(classification.migratable);
204
+ const dest = {
205
+ kind,
206
+ relativeRoot,
207
+ destination: resolved.destination,
208
+ relativeDestination: resolved.relativeDestination,
209
+ method,
210
+ dependsOn,
211
+ hosts: (group?.hosts || []).map((host) => ({
212
+ id: host.id,
213
+ displayName: host.displayName,
214
+ detected: Boolean(host.detected),
215
+ })),
216
+ adoption: classification.adoptable ? classification.kind : undefined,
217
+ recognition: classification.kind,
218
+ confidence: classification.confidence || 'none',
219
+ baselineRevision: classification.baselineRevision,
220
+ diff: classification.diff,
221
+ customization: classification.customization,
222
+ migratable,
223
+ resolution,
224
+ baseHashes: classification.files,
225
+ unownedConflict: !owned && pathExists(resolved.destination) && !classification.adoptable && !migratable,
226
+ };
227
+ if (scope === 'global') {
228
+ const impact = destinationImpact(dest);
229
+ dest.overwrite = impact.overwrite;
230
+ dest.backup = impact.backup;
231
+ dest.delete = impact.delete;
232
+ }
233
+ return dest;
234
+ });
235
+
236
+ const destPath = destinations[0].destination;
237
+ const relDest = destinations[0].relativeDestination;
238
+ const unownedConflict = destinations.some((dest) => dest.unownedConflict);
239
+ const exportRoot = options.exportDir || path.join(root, '.sigma-export');
240
+ const claimedExports = new Set();
241
+ for (const dest of destinations) {
242
+ if (dest.resolution !== 'export') continue;
243
+ let exportPath = plannedExportDir(exportRoot, skillId);
244
+ while (claimedExports.has(exportPath)) {
245
+ const dir = path.dirname(exportPath);
246
+ const name = path.basename(exportPath);
247
+ const match = name.match(/^(.*)-(\d+)$/);
248
+ const rootName = match ? match[1] : name;
249
+ const next = match ? Number(match[2]) + 1 : 2;
250
+ exportPath = path.join(dir, `${rootName}-${next}`);
251
+ }
252
+ claimedExports.add(exportPath);
253
+ dest.exportPath = exportPath;
254
+ }
255
+
256
+ const writes = destinations.flatMap((dest) => {
257
+ if (dest.adoption) return [];
258
+ if (dest.resolution === 'skip' || dest.resolution === 'export') return [];
259
+ if (dest.method === 'copy') {
260
+ return skillFiles.map((file) => `${dest.relativeDestination}/${file}`);
261
+ }
262
+ return [dest.relativeDestination];
263
+ });
264
+ const replacements = destinations.flatMap((dest) => {
265
+ if (dest.adoption) return [];
266
+ if (dest.resolution === 'skip' || dest.resolution === 'export') return [];
267
+ if (!pathExists(dest.destination)) return [];
268
+ if (dest.method === 'copy') {
269
+ return skillFiles.map((file) => `${dest.relativeDestination}/${file}`);
270
+ }
271
+ return [dest.relativeDestination];
272
+ });
273
+
274
+ const existingLockEntry = currentLock.skills?.[skillId] || null;
275
+ const lockChanges = {
276
+ file: 'skills-lock.json',
277
+ action: existingLockEntry ? 'update' : 'add',
278
+ previous: existingLockEntry,
279
+ next: {
280
+ revision: skill.revision,
281
+ },
282
+ };
283
+
284
+ const adoptionDecision = chooseCanonical(destinations.map((dest) => ({
285
+ relativeDestination: dest.relativeDestination,
286
+ relativeRoot: dest.relativeRoot,
287
+ destPath: dest.destination,
288
+ resolution: dest.resolution,
289
+ classification: {
290
+ kind: dest.recognition || dest.adoption || 'missing',
291
+ adoptable: Boolean(dest.adoption),
292
+ migratable: Boolean(dest.migratable),
293
+ confidence: dest.confidence,
294
+ },
295
+ })));
296
+ const adoption = {
297
+ canonical: adoptionDecision.canonical.relativeDestination,
298
+ copies: [adoptionDecision.canonical, ...adoptionDecision.others].map((copy) => ({
299
+ destination: copy.relativeDestination,
300
+ fate: copy.fate,
301
+ role: copy.role,
302
+ recognition: copy.classification?.kind || 'missing',
303
+ confidence: copy.classification?.confidence || 'none',
304
+ resolution: copy.resolution,
305
+ })),
306
+ };
307
+ const needsResolution = destinations.some((dest) => dest.migratable && !dest.resolution);
308
+
309
+ return {
310
+ schemaVersion: PLAN_SCHEMA_VERSION,
311
+ scope,
312
+ action: 'install',
313
+ skill: skillId,
314
+ title: skill.title,
315
+ release: catalog.manifest.version,
316
+ sourceRevision: skill.revision,
317
+ destination: destPath,
318
+ relativeDestination: relDest,
319
+ destinations,
320
+ method: requestedMethod,
321
+ files: skillFiles,
322
+ writes,
323
+ replacements,
324
+ lockChanges,
325
+ adoption,
326
+ exportDir: options.exportDir || null,
327
+ requiresApproval: needsResolution,
328
+ confirmationRequirements: scope === 'global' ? ['--global', '--yes'] : [],
329
+ unownedConflict,
330
+ dryRun: Boolean(dryRun),
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Format an install plan for human-readable display.
336
+ *
337
+ * @param {object} plan
338
+ * @returns {string}
339
+ */
340
+ export function formatPlanHuman(plan) {
341
+ const destinationLines = (plan.destinations || [{ destination: plan.destination, relativeDestination: plan.relativeDestination, hosts: [] }])
342
+ .flatMap((dest) => {
343
+ const hostNames = (dest.hosts || []).map((host) => host.displayName).join(', ');
344
+ const lines = [` ${dest.destination}`];
345
+ if (dest.relativeDestination) lines.push(` ${dest.relativeDestination}`);
346
+ if (dest.method) lines.push(` Method: ${dest.method}`);
347
+ if (dest.dependsOn) lines.push(` Depends on: ${dest.dependsOn}`);
348
+ if (dest.fallbackFrom) lines.push(` Fallback from: ${dest.fallbackFrom}`);
349
+ if (plan.scope === 'global' && dest.overwrite) lines.push(` Overwrite: ${dest.overwrite}`);
350
+ if (plan.scope === 'global' && dest.delete) lines.push(` Delete: ${dest.delete}`);
351
+ if (plan.scope === 'global' && dest.backup) lines.push(` Backup: ${dest.backup}`);
352
+ if (dest.recognition) lines.push(` Recognition: ${dest.recognition}`);
353
+ if (dest.confidence) lines.push(` Provenance: ${dest.confidence}`);
354
+ if (dest.resolution) lines.push(` Resolution: ${dest.resolution}`);
355
+ if (dest.exportPath) lines.push(` Export: ${dest.exportPath}`);
356
+ if (dest.diff) {
357
+ const added = dest.diff.added || [];
358
+ const replaced = dest.diff.replaced || [];
359
+ const deleted = dest.diff.deleted || [];
360
+ if (added.length) lines.push(` Additions: ${added.join(', ')}`);
361
+ if (replaced.length) lines.push(` Replacements: ${replaced.join(', ')}`);
362
+ if (deleted.length) lines.push(` Deletions: ${deleted.join(', ')}`);
363
+ }
364
+ if (hostNames) lines.push(` Agent Hosts: ${hostNames}`);
365
+ return lines;
366
+ });
367
+
368
+ const scopeLabel = plan.scope === 'global' ? 'Global Installation' : 'Project Installation';
369
+ const lines = [
370
+ `SigmaSkills ${scopeLabel} Plan: ${plan.title} (${plan.skill})`,
371
+ ` Scope: ${plan.scope}`,
372
+ ` Release: v${plan.release}`,
373
+ ` Source Revision: ${plan.sourceRevision}`,
374
+ ` Method: ${plan.method}`,
375
+ ` Destination: ${plan.destination}`,
376
+ ` Relative Path: ${plan.relativeDestination}`,
377
+ ` Canonical: ${plan.adoption?.canonical || plan.relativeDestination}`,
378
+ ` Resolved destinations:`,
379
+ ...destinationLines,
380
+ ` Required Approval: ${plan.requiresApproval ? 'Yes' : 'None'}`,
381
+ ...(plan.confirmationRequirements?.length
382
+ ? [` Required confirmation: ${plan.confirmationRequirements.join(' and ')}`]
383
+ : []),
384
+ ` Files to write (${plan.writes.length}):`,
385
+ ...plan.writes.map((w) => ` + ${w}`),
386
+ ];
387
+
388
+ if (plan.replacements.length > 0) {
389
+ lines.push(` Files to replace (${plan.replacements.length}):`);
390
+ lines.push(...plan.replacements.map((r) => ` ~ ${r}`));
391
+ }
392
+
393
+ if (plan.adoption?.copies?.length) {
394
+ lines.push(' Adoption:');
395
+ for (const copy of plan.adoption.copies) {
396
+ lines.push(` ${copy.destination} (${copy.role}, ${copy.fate})`);
397
+ }
398
+ }
399
+
400
+ if (plan.scope !== 'global' && plan.lockChanges) {
401
+ lines.push(` Lock changes (${plan.lockChanges.file}):`);
402
+ lines.push(` Action: ${plan.lockChanges.action}`);
403
+ lines.push(` Next Revision: ${plan.lockChanges.next.revision}`);
404
+ }
405
+
406
+ if (plan.unownedConflict) {
407
+ const conflictDest = (plan.destinations || []).find((dest) => dest.unownedConflict)?.destination || plan.destination;
408
+ lines.push('');
409
+ lines.push(` ⚠ UNOWNED CONFLICT: Destination '${conflictDest}' already exists and is not owned by Sigma.`);
410
+ lines.push(' Installation will fail closed.');
411
+ }
412
+
413
+ if (plan.dryRun) {
414
+ lines.push('');
415
+ lines.push('Dry run complete. No files, locks, or state were modified.');
416
+ }
417
+
418
+ return lines.join('\n');
419
+ }
420
+
421
+ /**
422
+ * Format plan as versioned JSON.
423
+ *
424
+ * @param {object} plan
425
+ * @returns {string}
426
+ */
427
+ export function formatPlanJson(plan) {
428
+ return JSON.stringify(plan, null, 2);
429
+ }
package/src/prepack.js ADDED
@@ -0,0 +1,10 @@
1
+ import { getCatalog, findPackageRoot } from './catalog.js';
2
+
3
+ try {
4
+ const rootDir = findPackageRoot();
5
+ const catalog = getCatalog(rootDir);
6
+ console.log(`Prepack validation successful: ${catalog.skills.length} skills validated for ${catalog.manifest.name} v${catalog.manifest.version}`);
7
+ } catch (err) {
8
+ console.error(`Prepack validation failed: ${err.message}`);
9
+ process.exit(1);
10
+ }