@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.
- package/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +342 -0
- package/bin/sigmaskills.js +7 -0
- package/manifest.json +28 -0
- package/package.json +42 -0
- package/registry/agent-hosts.json +2404 -0
- package/registry/schema.json +110 -0
- package/registry/skill-baselines.json +4 -0
- package/registry/source.json +6 -0
- package/sigmabrief/SKILL.md +56 -0
- package/sigmabrief/agents/openai.yaml +12 -0
- package/sigmabrief/references/brief-method.md +73 -0
- package/sigmabrief/references/prompt-contract.md +174 -0
- package/sigmaperformance/SKILL.md +118 -0
- package/sigmaperformance/agents/openai.yaml +12 -0
- package/sigmaperformance/references/audit-method.md +112 -0
- package/sigmaperformance/references/calibration.md +45 -0
- package/sigmaperformance/references/report-contract.md +103 -0
- package/sigmareview/SKILL.md +133 -0
- package/sigmareview/agents/openai.yaml +12 -0
- package/sigmareview/references/report-contract.md +217 -0
- package/sigmareview/references/review-method.md +233 -0
- package/sigmawrite/SKILL.md +45 -0
- package/sigmawrite/agents/openai.yaml +12 -0
- package/src/adoption.js +370 -0
- package/src/backup.js +398 -0
- package/src/catalog.js +211 -0
- package/src/cli.js +657 -0
- package/src/customization.js +344 -0
- package/src/destinations.js +491 -0
- package/src/interactive.js +959 -0
- package/src/links.js +157 -0
- package/src/plan.js +429 -0
- package/src/prepack.js +10 -0
- package/src/project-lock.js +169 -0
- package/src/purge.js +477 -0
- package/src/registry/automation-ci.js +411 -0
- package/src/registry/automation.js +554 -0
- package/src/registry/diff.js +149 -0
- package/src/registry/normalize.js +67 -0
- package/src/registry/parse.js +184 -0
- package/src/registry/sync.js +230 -0
- package/src/registry/validate.js +223 -0
- package/src/release-ci.js +12 -0
- package/src/release.js +837 -0
- package/src/restore.js +518 -0
- package/src/revision.js +82 -0
- package/src/state.js +480 -0
- package/src/status.js +469 -0
- package/src/transaction.js +636 -0
- package/src/uninstall.js +647 -0
- 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
|
+
}
|