@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/status.js ADDED
@@ -0,0 +1,469 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { findPackageRoot } from './catalog.js';
5
+ import { inspectCustomizationBlock, CUSTOM_BLOCK_END, CUSTOM_BLOCK_START } from './customization.js';
6
+ import {
7
+ UNIVERSAL_PROJECT_DESTINATION,
8
+ listGlobalDestinationGroups,
9
+ listProjectDestinationGroups,
10
+ loadHostRegistry,
11
+ resolveGlobalSkillPath,
12
+ resolveSkillPath,
13
+ } from './destinations.js';
14
+ import { pathExists, recommendedLinkMethod } from './links.js';
15
+ import { inspectProjectLock } from './project-lock.js';
16
+ import { loadGlobalState, loadProjectState } from './state.js';
17
+
18
+ export const STATUS_SCHEMA_VERSION = 1;
19
+
20
+ function hashBytes(bytes) {
21
+ return crypto.createHash('sha256').update(bytes).digest('hex');
22
+ }
23
+
24
+ function samePath(a, b) {
25
+ const left = path.resolve(a);
26
+ const right = path.resolve(b);
27
+ return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
28
+ }
29
+
30
+ function recordedDestinations(entry) {
31
+ const destinations = [];
32
+ if (entry?.destination) destinations.push(entry.destination);
33
+ if (Array.isArray(entry?.copies)) {
34
+ for (const copy of entry.copies) {
35
+ if (copy?.destination) destinations.push(copy.destination);
36
+ }
37
+ }
38
+ return [...new Set(destinations.map((value) => String(value).replace(/\\/g, '/')))];
39
+ }
40
+
41
+ function copyRecord(entry, relativeDestination) {
42
+ const copies = Array.isArray(entry?.copies) ? entry.copies : [];
43
+ return copies.find((copy) => String(copy?.destination || '').replace(/\\/g, '/') === relativeDestination) || null;
44
+ }
45
+
46
+ function relativeRootOf(relativeDestination, skillId) {
47
+ const posix = String(relativeDestination || '').replace(/\\/g, '/');
48
+ const suffix = `/${skillId}`;
49
+ if (posix.endsWith(suffix)) return posix.slice(0, -suffix.length);
50
+ const dirname = posix.split('/').slice(0, -1).join('/');
51
+ return dirname || UNIVERSAL_PROJECT_DESTINATION;
52
+ }
53
+
54
+ function hostIdsFor(groups, relativeRoot) {
55
+ const group = (groups || []).find((item) => item.relativeRoot === relativeRoot);
56
+ return group ? group.hosts.map((host) => host.id) : [];
57
+ }
58
+
59
+ function walkLiveFiles(dir) {
60
+ const files = {};
61
+ if (!pathExists(dir)) return files;
62
+ const top = fs.lstatSync(dir);
63
+ if (top.isSymbolicLink() || !top.isDirectory()) return files;
64
+
65
+ const visit = (current) => {
66
+ let entries;
67
+ try {
68
+ entries = fs.readdirSync(current, { withFileTypes: true });
69
+ } catch {
70
+ return;
71
+ }
72
+ for (const entry of entries) {
73
+ const full = path.join(current, entry.name);
74
+ let stat;
75
+ try {
76
+ stat = fs.lstatSync(full);
77
+ } catch {
78
+ continue;
79
+ }
80
+ if (stat.isSymbolicLink()) continue;
81
+ if (stat.isDirectory()) {
82
+ visit(full);
83
+ } else if (stat.isFile()) {
84
+ const rel = path.relative(dir, full).replace(/\\/g, '/');
85
+ files[rel] = hashBytes(fs.readFileSync(full));
86
+ }
87
+ }
88
+ };
89
+
90
+ visit(dir);
91
+ return files;
92
+ }
93
+
94
+ function officialMarkdownShell(markdown) {
95
+ const start = markdown.indexOf(CUSTOM_BLOCK_START);
96
+ const end = markdown.indexOf(CUSTOM_BLOCK_END);
97
+ if (start === -1 || end === -1 || end < start) return markdown;
98
+ return `${markdown.slice(0, start + CUSTOM_BLOCK_START.length)}${markdown.slice(end)}`;
99
+ }
100
+
101
+ function inspectSkillMarkdown(liveMarkdown, skillId, liveHash, bundledMarkdown, bundledHash) {
102
+ const inspection = inspectCustomizationBlock(liveMarkdown, skillId);
103
+ if (inspection.status !== 'valid') {
104
+ return {
105
+ status: inspection.status,
106
+ officialHash: liveHash,
107
+ hasCustomContent: false,
108
+ };
109
+ }
110
+ if (typeof bundledMarkdown === 'string' && officialMarkdownShell(liveMarkdown) === officialMarkdownShell(bundledMarkdown)) {
111
+ return {
112
+ status: 'valid',
113
+ officialHash: bundledHash || liveHash,
114
+ hasCustomContent: true,
115
+ };
116
+ }
117
+ return {
118
+ status: 'valid',
119
+ officialHash: liveHash,
120
+ hasCustomContent: true,
121
+ };
122
+ }
123
+
124
+ function isPackagedResource(file) {
125
+ return /^(references|scripts|assets|agents)\//.test(file);
126
+ }
127
+
128
+ function classifyLiveTree({ liveFiles, bundledFiles, skillId, skillMarkdown, bundledMarkdown }) {
129
+ const classifications = [];
130
+ const officialFiles = { ...liveFiles };
131
+
132
+ if (typeof skillMarkdown === 'string' && liveFiles['SKILL.md']) {
133
+ const markdown = inspectSkillMarkdown(
134
+ skillMarkdown,
135
+ skillId,
136
+ liveFiles['SKILL.md'],
137
+ bundledMarkdown,
138
+ bundledFiles['SKILL.md'],
139
+ );
140
+ officialFiles['SKILL.md'] = markdown.officialHash;
141
+ if (markdown.status === 'malformed') classifications.push('malformed-markers');
142
+ else if (markdown.hasCustomContent) classifications.push('valid-customization');
143
+ }
144
+
145
+ const added = [];
146
+ const replaced = [];
147
+ const deleted = [];
148
+ for (const file of Object.keys(officialFiles).sort()) {
149
+ if (!(file in bundledFiles)) added.push(file);
150
+ else if (officialFiles[file] !== bundledFiles[file]) replaced.push(file);
151
+ }
152
+ for (const file of Object.keys(bundledFiles).sort()) {
153
+ if (!(file in officialFiles)) deleted.push(file);
154
+ }
155
+
156
+ if (added.some((file) => !isPackagedResource(file))) classifications.push('outside-addition');
157
+ if (added.some((file) => isPackagedResource(file))) classifications.push('extra-resource');
158
+ if (replaced.length) classifications.push('outside-change');
159
+ if (deleted.some((file) => !isPackagedResource(file))) classifications.push('outside-deletion');
160
+ if (deleted.some((file) => isPackagedResource(file))) classifications.push('missing-resource');
161
+
162
+ const unique = [...new Set(classifications)];
163
+ if (unique.length === 0) unique.push('clean');
164
+ return {
165
+ classifications: unique,
166
+ files: { added, replaced, deleted },
167
+ fingerprint: JSON.stringify(
168
+ Object.keys(officialFiles).sort().map((file) => [file, officialFiles[file]]),
169
+ ),
170
+ };
171
+ }
172
+
173
+ function isInsideRoot(root, absolutePath) {
174
+ const relative = path.relative(path.resolve(root), path.resolve(absolutePath));
175
+ return relative === '' || (Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative));
176
+ }
177
+
178
+ function inspectLink(destPath, ownedAbsPaths, expectedTarget, root) {
179
+ const method = recommendedLinkMethod();
180
+ let raw;
181
+ try {
182
+ raw = fs.readlinkSync(destPath);
183
+ } catch {
184
+ return { method, broken: true };
185
+ }
186
+
187
+ const resolved = path.resolve(path.dirname(destPath), raw);
188
+ const ownedHit = ownedAbsPaths.some((owned) => samePath(owned, resolved));
189
+ if (!ownedHit) {
190
+ if (!isInsideRoot(root, resolved)) {
191
+ return { method, wrongTarget: true, target: resolved };
192
+ }
193
+ if (!pathExists(resolved)) {
194
+ return { method, broken: true, target: resolved };
195
+ }
196
+ return { method, wrongTarget: true, target: resolved };
197
+ }
198
+ if (!pathExists(resolved)) {
199
+ return { method, broken: true, target: resolved };
200
+ }
201
+ if (expectedTarget && !samePath(resolved, expectedTarget)) {
202
+ return { method, wrongTarget: true, target: resolved };
203
+ }
204
+ return { method, target: resolved };
205
+ }
206
+
207
+ function resolveRecordedPath(root, relativeDestination, skillId, scope) {
208
+ const relativeRoot = relativeRootOf(relativeDestination, skillId);
209
+ try {
210
+ return scope === 'global'
211
+ ? resolveGlobalSkillPath(root, relativeRoot, skillId)
212
+ : resolveSkillPath(root, relativeRoot, skillId);
213
+ } catch {
214
+ return {
215
+ destination: path.resolve(root, ...String(relativeDestination).split('/')),
216
+ relativeDestination,
217
+ relativeRoot,
218
+ };
219
+ }
220
+ }
221
+
222
+ function installedRelease(state, lock) {
223
+ for (const entry of Object.values(state.skills || {})) {
224
+ if (entry?.release) return entry.release;
225
+ }
226
+ return lock.release || null;
227
+ }
228
+
229
+ function destinationGroupsFor(options, root, scope) {
230
+ const packageRoot = options.packageRoot || findPackageRoot();
231
+ const registry = options.registry || loadHostRegistry(packageRoot);
232
+ const env = options.env || process.env;
233
+ if (scope === 'global') {
234
+ return listGlobalDestinationGroups({ registry, homeDir: root, env });
235
+ }
236
+ return listProjectDestinationGroups({ registry, projectRoot: root, env });
237
+ }
238
+
239
+ /**
240
+ * Read-only Project or Global Installation status. Never writes, never fetches.
241
+ *
242
+ * @param {object} options
243
+ * @returns {object}
244
+ */
245
+ export function collectStatus(options = {}) {
246
+ const catalog = options.catalog;
247
+ if (!catalog) throw new Error('status requires a catalog');
248
+
249
+ const scope = options.scope === 'global' ? 'global' : 'project';
250
+ const root = path.resolve(scope === 'global' ? (options.homeDir || options.projectRoot) : (options.projectRoot || process.cwd()));
251
+ const customStateDir = options.customStateDir;
252
+ const state = scope === 'global'
253
+ ? loadGlobalState(root, customStateDir)
254
+ : loadProjectState(root, customStateDir);
255
+ const lockInspect = scope === 'project' ? inspectProjectLock(root) : { kind: 'missing', lock: { skills: {}, release: null } };
256
+ const groups = destinationGroupsFor(options, root, scope);
257
+ const running = catalog.manifest.version;
258
+ const skillIds = [...new Set([
259
+ ...Object.keys(state.skills || {}),
260
+ ...Object.keys(lockInspect.lock.skills || {}),
261
+ ])].sort();
262
+
263
+ const skills = skillIds.map((skillId) => {
264
+ const catalogSkill = catalog.skills.find((item) => item.id === skillId);
265
+ const bundledFiles = catalogSkill?.files || {};
266
+ const entry = state.skills?.[skillId];
267
+ const recorded = recordedDestinations(entry);
268
+ const ownedAbs = recorded.map((rel) => path.resolve(root, ...rel.split('/')));
269
+ const canonicalRel = recorded.find((rel) => rel.replace(/\\/g, '/').startsWith(`${UNIVERSAL_PROJECT_DESTINATION}/`))
270
+ || `${UNIVERSAL_PROJECT_DESTINATION}/${skillId}`;
271
+ const canonicalAbs = path.resolve(root, ...canonicalRel.split('/'));
272
+ const lockRevision = lockInspect.lock.skills?.[skillId]?.revision || null;
273
+ const destinations = [];
274
+
275
+ if (recorded.length === 0) {
276
+ destinations.push({
277
+ relativeDestination: canonicalRel,
278
+ relativeRoot: UNIVERSAL_PROJECT_DESTINATION,
279
+ absolutePath: canonicalAbs,
280
+ method: null,
281
+ owned: false,
282
+ hostIds: hostIdsFor(groups, UNIVERSAL_PROJECT_DESTINATION),
283
+ classifications: ['missing-destination', 'stale-state'],
284
+ files: { added: [], replaced: [], deleted: [] },
285
+ });
286
+ }
287
+
288
+ for (const relativeDestination of recorded) {
289
+ const resolved = resolveRecordedPath(root, relativeDestination, skillId, scope);
290
+ const copyEntry = copyRecord(entry, relativeDestination);
291
+ const expectedTarget = copyEntry?.dependsOn
292
+ ? path.resolve(root, ...String(copyEntry.dependsOn).split('/'))
293
+ : (copyEntry?.method && copyEntry.method !== 'copy' ? canonicalAbs : null);
294
+ const hostIds = hostIdsFor(groups, resolved.relativeRoot);
295
+ const owned = Boolean(entry);
296
+
297
+ if (!pathExists(resolved.destination)) {
298
+ destinations.push({
299
+ relativeDestination,
300
+ relativeRoot: resolved.relativeRoot,
301
+ absolutePath: resolved.destination,
302
+ method: copyEntry?.method || null,
303
+ owned,
304
+ hostIds,
305
+ classifications: ['missing-destination', 'stale-state'],
306
+ files: { added: [], replaced: [], deleted: [] },
307
+ });
308
+ continue;
309
+ }
310
+
311
+ const stat = fs.lstatSync(resolved.destination);
312
+ if (stat.isSymbolicLink()) {
313
+ const link = inspectLink(resolved.destination, ownedAbs, expectedTarget, root);
314
+ const classifications = [];
315
+ if (link.broken) classifications.push('broken-link');
316
+ if (link.wrongTarget) classifications.push('wrong-target');
317
+ if (classifications.length === 0) classifications.push('clean');
318
+ destinations.push({
319
+ relativeDestination,
320
+ relativeRoot: resolved.relativeRoot,
321
+ absolutePath: resolved.destination,
322
+ method: link.method || copyEntry?.method || recommendedLinkMethod(),
323
+ owned,
324
+ hostIds,
325
+ classifications,
326
+ target: link.target || null,
327
+ files: { added: [], replaced: [], deleted: [] },
328
+ });
329
+ continue;
330
+ }
331
+
332
+ const liveFiles = walkLiveFiles(resolved.destination);
333
+ let skillMarkdown;
334
+ const skillMdPath = path.join(resolved.destination, 'SKILL.md');
335
+ if (pathExists(skillMdPath) && fs.lstatSync(skillMdPath).isFile()) {
336
+ skillMarkdown = fs.readFileSync(skillMdPath, 'utf8');
337
+ }
338
+ const bundledMarkdownPath = catalogSkill
339
+ ? path.join(options.packageRoot || findPackageRoot(), skillId, 'SKILL.md')
340
+ : null;
341
+ const bundledMarkdown = bundledMarkdownPath && pathExists(bundledMarkdownPath) && fs.lstatSync(bundledMarkdownPath).isFile()
342
+ ? fs.readFileSync(bundledMarkdownPath, 'utf8')
343
+ : null;
344
+ const classified = classifyLiveTree({
345
+ liveFiles,
346
+ bundledFiles,
347
+ skillId,
348
+ skillMarkdown,
349
+ bundledMarkdown,
350
+ });
351
+ destinations.push({
352
+ relativeDestination,
353
+ relativeRoot: resolved.relativeRoot,
354
+ absolutePath: resolved.destination,
355
+ method: copyEntry?.method || 'copy',
356
+ owned,
357
+ hostIds,
358
+ classifications: classified.classifications,
359
+ files: classified.files,
360
+ fingerprint: classified.fingerprint,
361
+ });
362
+ }
363
+
364
+ const copyDests = destinations.filter((dest) => dest.method === 'copy' && dest.fingerprint);
365
+ const fingerprints = new Set(copyDests.map((dest) => dest.fingerprint));
366
+ if (fingerprints.size > 1) {
367
+ for (const dest of copyDests) {
368
+ if (!dest.classifications.includes('copy-disagreement')) {
369
+ dest.classifications.push('copy-disagreement');
370
+ }
371
+ }
372
+ }
373
+
374
+ const corruptionKinds = new Set([
375
+ 'outside-change',
376
+ 'outside-addition',
377
+ 'outside-deletion',
378
+ 'extra-resource',
379
+ 'missing-resource',
380
+ 'malformed-markers',
381
+ 'missing-destination',
382
+ 'stale-state',
383
+ 'broken-link',
384
+ 'wrong-target',
385
+ 'copy-disagreement',
386
+ ]);
387
+ const allKinds = destinations.flatMap((dest) => dest.classifications);
388
+ const corruption = allKinds.some((kind) => corruptionKinds.has(kind));
389
+
390
+ for (const dest of destinations) {
391
+ delete dest.fingerprint;
392
+ }
393
+
394
+ return {
395
+ id: skillId,
396
+ title: catalogSkill?.title || skillId,
397
+ installedRevision: entry?.revision || lockRevision,
398
+ runningRevision: catalogSkill?.revision || null,
399
+ corruption,
400
+ destinations,
401
+ };
402
+ });
403
+
404
+ const drift = skills.some((skill) => skill.destinations.some((dest) => dest.classifications.some((kind) => kind !== 'clean')));
405
+
406
+ return {
407
+ schemaVersion: STATUS_SCHEMA_VERSION,
408
+ command: 'status',
409
+ scope,
410
+ release: {
411
+ installed: installedRelease(state, lockInspect.lock),
412
+ running,
413
+ },
414
+ readOnly: true,
415
+ drift,
416
+ skills,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Format status as versioned JSON.
422
+ *
423
+ * @param {object} report
424
+ * @returns {string}
425
+ */
426
+ export function formatStatusJson(report) {
427
+ return JSON.stringify(report, null, 2);
428
+ }
429
+
430
+ /**
431
+ * Format status for a terminal.
432
+ *
433
+ * @param {object} report
434
+ * @returns {string}
435
+ */
436
+ export function formatStatusHuman(report) {
437
+ const scopeLabel = report.scope === 'global' ? 'Global Installation' : 'Project Installation';
438
+ const lines = [
439
+ `SigmaSkills ${scopeLabel} Status`,
440
+ ` Scope: ${report.scope}`,
441
+ ` Release: installed ${report.release?.installed || 'none'} / running v${report.release?.running || 'unknown'}`,
442
+ ` Drift: ${report.drift ? 'Yes' : 'No'}`,
443
+ ];
444
+
445
+ if (!report.skills?.length) {
446
+ lines.push(' Skills: none managed');
447
+ return lines.join('\n');
448
+ }
449
+
450
+ for (const skill of report.skills) {
451
+ lines.push('');
452
+ lines.push(` ${skill.title} (${skill.id})`);
453
+ lines.push(` Installed Skill Revision: ${skill.installedRevision || 'none'}`);
454
+ lines.push(` Running Skill Revision: ${skill.runningRevision || 'none'}`);
455
+ lines.push(` Ownership: ${skill.destinations.some((dest) => dest.owned) ? 'managed' : 'unmanaged'}`);
456
+ lines.push(` Corruption: ${skill.corruption ? 'Yes' : 'No'}`);
457
+ for (const dest of skill.destinations) {
458
+ lines.push(` Destination: ${dest.relativeDestination}`);
459
+ lines.push(` Path: ${dest.absolutePath}`);
460
+ lines.push(` Method: ${dest.method || 'none'}`);
461
+ lines.push(` Owned: ${dest.owned ? 'Yes' : 'No'}`);
462
+ if (dest.hostIds?.length) lines.push(` Agent Hosts: ${dest.hostIds.join(', ')}`);
463
+ lines.push(` Classification: ${dest.classifications.join(', ')}`);
464
+ if (dest.target) lines.push(` Link target: ${dest.target}`);
465
+ }
466
+ }
467
+
468
+ return lines.join('\n');
469
+ }