@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/state.js ADDED
@@ -0,0 +1,480 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { UNIVERSAL_PROJECT_DESTINATION } from './destinations.js';
4
+
5
+ export const STATE_FILENAME = 'state.json';
6
+ export const STATE_SCHEMA_VERSION = 1;
7
+
8
+ /**
9
+ * Resolve directory path where private project machine state is stored.
10
+ *
11
+ * @param {string} projectRoot
12
+ * @param {string} [customStateDir]
13
+ * @returns {string}
14
+ */
15
+ export function getProjectStateDir(projectRoot, customStateDir) {
16
+ if (customStateDir) {
17
+ return path.resolve(customStateDir);
18
+ }
19
+ if (process.env.SIGMA_STATE_DIR) {
20
+ return path.resolve(process.env.SIGMA_STATE_DIR);
21
+ }
22
+ return path.join(projectRoot, '.agents');
23
+ }
24
+
25
+ /**
26
+ * Resolve full path to project state.json file.
27
+ *
28
+ * @param {string} projectRoot
29
+ * @param {string} [customStateDir]
30
+ * @returns {string}
31
+ */
32
+ export function getProjectStatePath(projectRoot, customStateDir) {
33
+ return path.join(getProjectStateDir(projectRoot, customStateDir), STATE_FILENAME);
34
+ }
35
+
36
+ /**
37
+ * Load and parse the private project state file.
38
+ * Returns default initial state if file does not exist.
39
+ *
40
+ * @param {string} projectRoot
41
+ * @param {string} [customStateDir]
42
+ * @returns {object}
43
+ */
44
+ export function loadProjectState(projectRoot, customStateDir) {
45
+ const statePath = getProjectStatePath(projectRoot, customStateDir);
46
+ if (!fs.existsSync(statePath)) {
47
+ return {
48
+ schemaVersion: STATE_SCHEMA_VERSION,
49
+ scope: 'project',
50
+ skills: {},
51
+ };
52
+ }
53
+
54
+ try {
55
+ const raw = fs.readFileSync(statePath, 'utf8');
56
+ const parsed = JSON.parse(raw);
57
+ validateProjectState(parsed);
58
+ return parsed;
59
+ } catch (err) {
60
+ throw new Error(`failed to read project state at ${statePath}: ${err.message}`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Validate that a project state structure is valid.
66
+ *
67
+ * @param {object} state
68
+ */
69
+ function validateManagedState(state, expectedScope) {
70
+ const label = expectedScope === 'global' ? 'global state' : 'project state';
71
+ if (!state || typeof state !== 'object') {
72
+ throw new Error(`invalid ${label}: expected JSON object`);
73
+ }
74
+ if (typeof state.schemaVersion !== 'number' || state.schemaVersion < 1) {
75
+ throw new Error(`invalid ${label}: missing or invalid schemaVersion`);
76
+ }
77
+ if (state.schemaVersion > STATE_SCHEMA_VERSION) {
78
+ throw new Error(
79
+ `unsupported ${label} schemaVersion ${state.schemaVersion}; this installer supports ${STATE_SCHEMA_VERSION}`,
80
+ );
81
+ }
82
+ if (state.scope !== expectedScope) {
83
+ throw new Error(`invalid ${label}: expected scope '${expectedScope}', got '${state.scope}'`);
84
+ }
85
+ if (!state.skills || typeof state.skills !== 'object' || Array.isArray(state.skills)) {
86
+ throw new Error(`invalid ${label}: skills must be an object`);
87
+ }
88
+
89
+ for (const [skillId, skillState] of Object.entries(state.skills)) {
90
+ if (!skillState || typeof skillState !== 'object') {
91
+ throw new Error(`invalid ${label} entry for '${skillId}'`);
92
+ }
93
+ if (!skillState.revision || typeof skillState.revision !== 'string') {
94
+ throw new Error(`invalid ${label} for '${skillId}': missing revision`);
95
+ }
96
+ if (!skillState.method || typeof skillState.method !== 'string') {
97
+ throw new Error(`invalid ${label} for '${skillId}': missing method`);
98
+ }
99
+ if (!skillState.destination || typeof skillState.destination !== 'string') {
100
+ throw new Error(`invalid ${label} for '${skillId}': missing destination`);
101
+ }
102
+ if (!Array.isArray(skillState.ownedPaths)) {
103
+ throw new Error(`invalid ${label} for '${skillId}': missing ownedPaths array`);
104
+ }
105
+ if (!skillState.baseHashes || typeof skillState.baseHashes !== 'object') {
106
+ throw new Error(`invalid ${label} for '${skillId}': missing baseHashes map`);
107
+ }
108
+ if (skillState.lastBackup !== undefined && skillState.lastBackup !== null && typeof skillState.lastBackup !== 'string') {
109
+ throw new Error(`invalid ${label} for '${skillId}': lastBackup must be a string or null`);
110
+ }
111
+ if (skillState.cleanupDebt !== undefined) {
112
+ if (!Array.isArray(skillState.cleanupDebt) || skillState.cleanupDebt.some((item) => typeof item !== 'string')) {
113
+ throw new Error(`invalid ${label} for '${skillId}': cleanupDebt must be an array of strings`);
114
+ }
115
+ }
116
+ if (skillState.copies !== undefined) {
117
+ if (!Array.isArray(skillState.copies)) {
118
+ throw new Error(`invalid ${label} for '${skillId}': copies must be an array`);
119
+ }
120
+ for (const copy of skillState.copies) {
121
+ if (!copy || typeof copy !== 'object') {
122
+ throw new Error(`invalid ${label} for '${skillId}': copy entry is not an object`);
123
+ }
124
+ if (copy.kind !== 'canonical' && copy.kind !== 'host') {
125
+ throw new Error(`invalid ${label} for '${skillId}': copy kind must be canonical or host`);
126
+ }
127
+ if (!copy.destination || typeof copy.destination !== 'string') {
128
+ throw new Error(`invalid ${label} for '${skillId}': copy is missing destination`);
129
+ }
130
+ if (!Array.isArray(copy.hostIds)) {
131
+ throw new Error(`invalid ${label} for '${skillId}': copy is missing hostIds`);
132
+ }
133
+ if (!Array.isArray(copy.ownedPaths)) {
134
+ throw new Error(`invalid ${label} for '${skillId}': copy is missing ownedPaths`);
135
+ }
136
+ if (copy.method !== undefined && copy.method !== 'copy' && copy.method !== 'symlink' && copy.method !== 'junction') {
137
+ throw new Error(`invalid ${label} for '${skillId}': copy method must be copy, symlink, or junction`);
138
+ }
139
+ if (copy.dependsOn !== undefined && copy.dependsOn !== null && typeof copy.dependsOn !== 'string') {
140
+ throw new Error(`invalid ${label} for '${skillId}': copy dependsOn must be a string or null`);
141
+ }
142
+ if (copy.baseHashes !== undefined && (typeof copy.baseHashes !== 'object' || Array.isArray(copy.baseHashes) || copy.baseHashes === null)) {
143
+ throw new Error(`invalid ${label} for '${skillId}': copy baseHashes must be an object`);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Validate that a project state structure is valid.
152
+ *
153
+ * @param {object} state
154
+ */
155
+ export function validateProjectState(state) {
156
+ validateManagedState(state, 'project');
157
+ }
158
+
159
+ /**
160
+ * Validate that a global state structure is valid.
161
+ *
162
+ * @param {object} state
163
+ */
164
+ export function validateGlobalState(state) {
165
+ validateManagedState(state, 'global');
166
+ }
167
+
168
+ /**
169
+ * Serialize and atomically write project state.json file.
170
+ *
171
+ * @param {string} projectRoot
172
+ * @param {object} state
173
+ * @param {string} [customStateDir]
174
+ */
175
+ function writeStateFile(stateDir, state, scope) {
176
+ if (!fs.existsSync(stateDir)) {
177
+ fs.mkdirSync(stateDir, { recursive: true });
178
+ }
179
+
180
+ const sortedSkills = {};
181
+ for (const key of Object.keys(state.skills || {}).sort()) {
182
+ sortedSkills[key] = state.skills[key];
183
+ }
184
+
185
+ const cleanState = {
186
+ schemaVersion: state.schemaVersion || STATE_SCHEMA_VERSION,
187
+ scope,
188
+ skills: sortedSkills,
189
+ };
190
+
191
+ const content = JSON.stringify(cleanState, null, 2) + '\n';
192
+ const statePath = path.join(stateDir, STATE_FILENAME);
193
+ const tempPath = path.join(stateDir, `${STATE_FILENAME}.tmp.${process.pid}.${Date.now()}`);
194
+
195
+ fs.writeFileSync(tempPath, content, 'utf8');
196
+ try {
197
+ fs.renameSync(tempPath, statePath);
198
+ } catch (err) {
199
+ try {
200
+ fs.copyFileSync(tempPath, statePath);
201
+ fs.unlinkSync(tempPath);
202
+ } catch {
203
+ throw err;
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Serialize and atomically write project state.json file.
210
+ *
211
+ * @param {string} projectRoot
212
+ * @param {object} state
213
+ * @param {string} [customStateDir]
214
+ */
215
+ export function saveProjectState(projectRoot, state, customStateDir) {
216
+ validateProjectState(state);
217
+ writeStateFile(getProjectStateDir(projectRoot, customStateDir), state, 'project');
218
+ }
219
+
220
+ /**
221
+ * Resolve directory path where private Global Installation state is stored.
222
+ *
223
+ * @param {string} homeDir
224
+ * @param {string} [customStateDir]
225
+ * @returns {string}
226
+ */
227
+ export function getGlobalStateDir(homeDir, customStateDir) {
228
+ if (customStateDir) {
229
+ return path.resolve(customStateDir);
230
+ }
231
+ if (process.env.SIGMA_STATE_DIR) {
232
+ return path.resolve(process.env.SIGMA_STATE_DIR);
233
+ }
234
+ return path.join(path.resolve(homeDir), '.agents');
235
+ }
236
+
237
+ /**
238
+ * Resolve full path to global state.json file.
239
+ *
240
+ * @param {string} homeDir
241
+ * @param {string} [customStateDir]
242
+ * @returns {string}
243
+ */
244
+ export function getGlobalStatePath(homeDir, customStateDir) {
245
+ return path.join(getGlobalStateDir(homeDir, customStateDir), STATE_FILENAME);
246
+ }
247
+
248
+ function emptyGlobalState() {
249
+ return {
250
+ schemaVersion: STATE_SCHEMA_VERSION,
251
+ scope: 'global',
252
+ skills: {},
253
+ };
254
+ }
255
+
256
+ function migrateGlobalState(parsed) {
257
+ if (!parsed || typeof parsed !== 'object') {
258
+ throw new Error('invalid global state: expected JSON object');
259
+ }
260
+ if (typeof parsed.schemaVersion === 'number' && parsed.schemaVersion > STATE_SCHEMA_VERSION) {
261
+ throw new Error(
262
+ `unsupported global state schemaVersion ${parsed.schemaVersion}; this installer supports ${STATE_SCHEMA_VERSION}`,
263
+ );
264
+ }
265
+
266
+ const skills = {};
267
+ for (const [skillId, entry] of Object.entries(parsed.skills || {})) {
268
+ if (!entry || typeof entry !== 'object') {
269
+ throw new Error(`invalid global state entry for '${skillId}'`);
270
+ }
271
+ skills[skillId] = {
272
+ ...entry,
273
+ lastBackup: entry.lastBackup || entry.backup || null,
274
+ };
275
+ }
276
+
277
+ const migrated = {
278
+ schemaVersion: STATE_SCHEMA_VERSION,
279
+ scope: 'global',
280
+ skills,
281
+ };
282
+ validateGlobalState(migrated);
283
+ return migrated;
284
+ }
285
+
286
+ /**
287
+ * Load and parse the private global state file.
288
+ * Unknown newer schemas fail without writing. Older schemas migrate in memory.
289
+ *
290
+ * @param {string} homeDir
291
+ * @param {string} [customStateDir]
292
+ * @returns {object}
293
+ */
294
+ export function loadGlobalState(homeDir, customStateDir) {
295
+ const statePath = getGlobalStatePath(homeDir, customStateDir);
296
+ if (!fs.existsSync(statePath)) {
297
+ return emptyGlobalState();
298
+ }
299
+
300
+ try {
301
+ const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
302
+ if (typeof parsed?.schemaVersion === 'number' && parsed.schemaVersion > STATE_SCHEMA_VERSION) {
303
+ throw new Error(
304
+ `unsupported global state schemaVersion ${parsed.schemaVersion}; this installer supports ${STATE_SCHEMA_VERSION}`,
305
+ );
306
+ }
307
+ if (parsed?.schemaVersion === STATE_SCHEMA_VERSION && parsed.scope === 'global') {
308
+ validateGlobalState(parsed);
309
+ return parsed;
310
+ }
311
+ if (parsed?.schemaVersion === STATE_SCHEMA_VERSION) {
312
+ throw new Error(`invalid global state: expected scope 'global', got '${parsed.scope}'`);
313
+ }
314
+ return migrateGlobalState(parsed);
315
+ } catch (err) {
316
+ if (/unsupported global state schemaVersion/.test(err.message) || /expected scope 'global'/.test(err.message)) {
317
+ throw err;
318
+ }
319
+ throw new Error(`failed to read global state at ${statePath}: ${err.message}`);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Serialize and atomically write global state.json file.
325
+ *
326
+ * @param {string} homeDir
327
+ * @param {object} state
328
+ * @param {string} [customStateDir]
329
+ */
330
+ export function saveGlobalState(homeDir, state, customStateDir) {
331
+ if (typeof state?.schemaVersion === 'number' && state.schemaVersion > STATE_SCHEMA_VERSION) {
332
+ throw new Error(
333
+ `unsupported global state schemaVersion ${state.schemaVersion}; this installer supports ${STATE_SCHEMA_VERSION}`,
334
+ );
335
+ }
336
+ validateGlobalState(state);
337
+ writeStateFile(getGlobalStateDir(homeDir, customStateDir), state, 'global');
338
+ }
339
+
340
+ function recordedCopyDestinations(entry) {
341
+ const destinations = [];
342
+ if (entry?.destination) destinations.push(entry.destination);
343
+ if (Array.isArray(entry?.copies)) {
344
+ for (const copy of entry.copies) {
345
+ if (copy?.destination) destinations.push(copy.destination);
346
+ }
347
+ }
348
+ return destinations;
349
+ }
350
+
351
+ /**
352
+ * Check whether a target skill destination is recorded as owned by Sigma in project state.
353
+ *
354
+ * @param {string} projectRoot
355
+ * @param {string} skillId
356
+ * @param {string} destinationPath
357
+ * @param {string} [customStateDir]
358
+ * @returns {boolean}
359
+ */
360
+ export function isDestinationOwned(projectRoot, skillId, destinationPath, customStateDir, options = {}) {
361
+ const scope = options.scope || 'project';
362
+ const state = scope === 'global'
363
+ ? loadGlobalState(projectRoot, customStateDir)
364
+ : loadProjectState(projectRoot, customStateDir);
365
+ const entry = state.skills?.[skillId];
366
+ if (!entry) {
367
+ return false;
368
+ }
369
+
370
+ const relDest = path.relative(projectRoot, destinationPath).replace(/\\/g, '/');
371
+ return recordedCopyDestinations(entry).some((recorded) => {
372
+ const recordedRel = recorded.replace(/\\/g, '/');
373
+ return relDest === recordedRel || path.resolve(destinationPath) === path.resolve(projectRoot, recordedRel);
374
+ });
375
+ }
376
+
377
+ /**
378
+ * Record or update a skill installation in project state.
379
+ *
380
+ * @param {object} state
381
+ * @param {object} details
382
+ * @returns {object} Updated state object
383
+ */
384
+ export function recordSkillInState(state, details) {
385
+ const {
386
+ skillId,
387
+ release,
388
+ revision,
389
+ method = 'copy',
390
+ destination,
391
+ projectRoot,
392
+ ownedPaths = [],
393
+ baseHashes = {},
394
+ installedAt,
395
+ copies,
396
+ lastBackup,
397
+ cleanupDebt,
398
+ scope,
399
+ } = details;
400
+
401
+ const toRelative = (value) => {
402
+ if (path.isAbsolute(value)) {
403
+ return path.relative(projectRoot, value).replace(/\\/g, '/');
404
+ }
405
+ return value.replace(/\\/g, '/');
406
+ };
407
+
408
+ const relDest = toRelative(destination);
409
+ const normalizedOwnedPaths = ownedPaths.map((p) => toRelative(p));
410
+ const normalizedCopies = Array.isArray(copies)
411
+ ? copies.map((copy) => {
412
+ const entry = {
413
+ kind: copy.kind,
414
+ destination: toRelative(copy.destination),
415
+ method: copy.method || method,
416
+ dependsOn: copy.dependsOn == null ? null : toRelative(copy.dependsOn),
417
+ hostIds: [...(copy.hostIds || [])],
418
+ ownedPaths: (copy.ownedPaths || []).map((p) => toRelative(p)),
419
+ };
420
+ if (copy.baseHashes && typeof copy.baseHashes === 'object') {
421
+ entry.baseHashes = copy.baseHashes;
422
+ }
423
+ return entry;
424
+ })
425
+ : [{
426
+ kind: relDest.replace(/\\/g, '/').startsWith(`${UNIVERSAL_PROJECT_DESTINATION}/`) ? 'canonical' : 'host',
427
+ destination: relDest,
428
+ hostIds: [],
429
+ ownedPaths: normalizedOwnedPaths,
430
+ }];
431
+
432
+ const now = new Date().toISOString();
433
+ const existing = state.skills?.[skillId];
434
+
435
+ const updatedSkills = { ...(state.skills || {}) };
436
+ updatedSkills[skillId] = {
437
+ release: release || existing?.release || null,
438
+ revision,
439
+ method,
440
+ destination: relDest,
441
+ copies: normalizedCopies,
442
+ ownedPaths: normalizedOwnedPaths,
443
+ baseHashes,
444
+ installedAt: installedAt || existing?.installedAt || now,
445
+ updatedAt: now,
446
+ lastBackup: lastBackup === undefined
447
+ ? (existing?.lastBackup || null)
448
+ : (lastBackup == null ? null : toRelative(lastBackup)),
449
+ };
450
+ if (cleanupDebt !== undefined) {
451
+ if (Array.isArray(cleanupDebt) && cleanupDebt.length > 0) {
452
+ updatedSkills[skillId].cleanupDebt = cleanupDebt.map((item) => toRelative(item));
453
+ }
454
+ } else if (Array.isArray(existing?.cleanupDebt) && existing.cleanupDebt.length > 0) {
455
+ updatedSkills[skillId].cleanupDebt = existing.cleanupDebt;
456
+ }
457
+
458
+ return {
459
+ schemaVersion: state.schemaVersion || STATE_SCHEMA_VERSION,
460
+ scope: scope || state.scope || 'project',
461
+ skills: updatedSkills,
462
+ };
463
+ }
464
+
465
+ /**
466
+ * Drop a skill from managed state after a successful uninstall.
467
+ *
468
+ * @param {object} state
469
+ * @param {string} skillId
470
+ * @returns {object}
471
+ */
472
+ export function removeSkillFromState(state, skillId) {
473
+ const skills = { ...(state.skills || {}) };
474
+ delete skills[skillId];
475
+ return {
476
+ schemaVersion: state.schemaVersion || STATE_SCHEMA_VERSION,
477
+ scope: state.scope || 'project',
478
+ skills,
479
+ };
480
+ }