@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
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { findPackageRoot } from './catalog.js';
|
|
5
|
+
import { inspectManagedPath, pathExists } from './links.js';
|
|
6
|
+
|
|
7
|
+
export const UNIVERSAL_PROJECT_DESTINATION = '.agents/skills';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load the bundled Agent Host registry snapshot.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} [packageRoot]
|
|
13
|
+
* @returns {object}
|
|
14
|
+
*/
|
|
15
|
+
export function loadHostRegistry(packageRoot = findPackageRoot()) {
|
|
16
|
+
const snapshotPath = path.join(packageRoot, 'registry', 'agent-hosts.json');
|
|
17
|
+
if (!fs.existsSync(snapshotPath)) {
|
|
18
|
+
throw new Error(`missing Agent Host registry at ${snapshotPath}`);
|
|
19
|
+
}
|
|
20
|
+
let snapshot;
|
|
21
|
+
try {
|
|
22
|
+
snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
throw new Error(`failed to read Agent Host registry at ${snapshotPath}: ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (!snapshot || !Array.isArray(snapshot.hosts)) {
|
|
27
|
+
throw new Error('registry hosts must be an array');
|
|
28
|
+
}
|
|
29
|
+
return snapshot;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a project-relative destination root to posix form without a trailing slash.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} relativeRoot
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeRelativeRoot(relativeRoot) {
|
|
39
|
+
return String(relativeRoot || '')
|
|
40
|
+
.replace(/\\/g, '/')
|
|
41
|
+
.replace(/^\.\//, '')
|
|
42
|
+
.replace(/\/+$/, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a host is labeled detected from environment metadata.
|
|
47
|
+
* Detection never selects destinations.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} host
|
|
50
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
export function detectHost(host, env = process.env) {
|
|
54
|
+
const envVars = host?.detection?.envVars;
|
|
55
|
+
if (!Array.isArray(envVars) || envVars.length === 0) return false;
|
|
56
|
+
return envVars.some((name) => {
|
|
57
|
+
const value = env[name];
|
|
58
|
+
return value !== undefined && value !== '';
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function expandProjectRelativeRoot(formula) {
|
|
63
|
+
if (!formula || formula.kind === 'none') return null;
|
|
64
|
+
if (formula.kind !== 'literal' || typeof formula.path !== 'string') return null;
|
|
65
|
+
const relativeRoot = normalizeRelativeRoot(formula.path);
|
|
66
|
+
if (!relativeRoot) return null;
|
|
67
|
+
return relativeRoot;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compareIds(a, b) {
|
|
71
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function compareRoots(a, b) {
|
|
75
|
+
if (a === UNIVERSAL_PROJECT_DESTINATION && b !== UNIVERSAL_PROJECT_DESTINATION) return -1;
|
|
76
|
+
if (b === UNIVERSAL_PROJECT_DESTINATION && a !== UNIVERSAL_PROJECT_DESTINATION) return 1;
|
|
77
|
+
return compareIds(a, b);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Group every Agent Host by its Project Installation destination.
|
|
82
|
+
* Hosts without a project destination still appear under a non-selectable group.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} params
|
|
85
|
+
* @param {object} params.registry
|
|
86
|
+
* @param {string} params.projectRoot
|
|
87
|
+
* @param {NodeJS.ProcessEnv} [params.env]
|
|
88
|
+
* @returns {object[]}
|
|
89
|
+
*/
|
|
90
|
+
export function listProjectDestinationGroups({ registry, projectRoot, env = process.env }) {
|
|
91
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
92
|
+
const grouped = new Map();
|
|
93
|
+
|
|
94
|
+
for (const host of registry.hosts || []) {
|
|
95
|
+
const relativeRoot = expandProjectRelativeRoot(host.destinations?.project);
|
|
96
|
+
const key = relativeRoot || `__none__:${host.id}`;
|
|
97
|
+
if (!grouped.has(key)) {
|
|
98
|
+
const universal = relativeRoot === UNIVERSAL_PROJECT_DESTINATION;
|
|
99
|
+
grouped.set(key, {
|
|
100
|
+
key,
|
|
101
|
+
relativeRoot: relativeRoot || '',
|
|
102
|
+
absoluteRoot: relativeRoot ? path.resolve(resolvedProjectRoot, ...relativeRoot.split('/')) : '',
|
|
103
|
+
universal,
|
|
104
|
+
selectedByDefault: universal,
|
|
105
|
+
selectable: Boolean(relativeRoot),
|
|
106
|
+
hosts: [],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
grouped.get(key).hosts.push({
|
|
110
|
+
id: host.id,
|
|
111
|
+
name: host.name,
|
|
112
|
+
displayName: host.displayName,
|
|
113
|
+
aliases: [...(host.aliases || [])],
|
|
114
|
+
detected: detectHost(host, env),
|
|
115
|
+
relativeRoot: relativeRoot || '',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const groups = [...grouped.values()].sort((a, b) => compareRoots(a.relativeRoot || a.key, b.relativeRoot || b.key));
|
|
120
|
+
for (const group of groups) {
|
|
121
|
+
group.hosts.sort((a, b) => compareIds(a.id, b.id));
|
|
122
|
+
}
|
|
123
|
+
return groups;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Default selected destination roots: only the universal `.agents/skills` path.
|
|
128
|
+
*
|
|
129
|
+
* @param {object[]} groups
|
|
130
|
+
* @returns {string[]}
|
|
131
|
+
*/
|
|
132
|
+
export function defaultSelectedRoots(groups) {
|
|
133
|
+
return groups.filter((group) => group.selectedByDefault).map((group) => group.relativeRoot);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Search all hosts by id, display name, name, or alias. Detection does not hide hosts.
|
|
138
|
+
*
|
|
139
|
+
* @param {object[]} groups
|
|
140
|
+
* @param {string} query
|
|
141
|
+
* @returns {object[]}
|
|
142
|
+
*/
|
|
143
|
+
export function searchHosts(groups, query) {
|
|
144
|
+
const needle = String(query || '').trim().toLowerCase();
|
|
145
|
+
const hosts = groups.flatMap((group) => group.hosts.map((host) => ({ ...host, group })));
|
|
146
|
+
if (!needle) return hosts;
|
|
147
|
+
return hosts.filter((host) => {
|
|
148
|
+
const haystacks = [host.id, host.name, host.displayName, ...(host.aliases || [])];
|
|
149
|
+
return haystacks.some((value) => String(value || '').toLowerCase().includes(needle));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isInsideProject(projectRoot, absolutePath) {
|
|
154
|
+
const relative = path.relative(path.resolve(projectRoot), path.resolve(absolutePath));
|
|
155
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve one skill folder under a project destination root.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} projectRoot
|
|
162
|
+
* @param {string} relativeRoot
|
|
163
|
+
* @param {string} skillId
|
|
164
|
+
* @returns {{ destination: string, relativeDestination: string, relativeRoot: string }}
|
|
165
|
+
*/
|
|
166
|
+
export function resolveSkillPath(projectRoot, relativeRoot, skillId) {
|
|
167
|
+
const normalizedRoot = normalizeRelativeRoot(relativeRoot);
|
|
168
|
+
if (!normalizedRoot || normalizedRoot.split('/').some((segment) => segment === '..')) {
|
|
169
|
+
throw new Error(`invalid destination '${relativeRoot}'`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const destination = path.resolve(projectRoot, ...normalizedRoot.split('/'), skillId);
|
|
173
|
+
if (!isInsideProject(projectRoot, destination)) {
|
|
174
|
+
throw new Error(`invalid destination '${relativeRoot}': resolved path escapes the project`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const relativeDestination = path.relative(path.resolve(projectRoot), destination).replace(/\\/g, '/');
|
|
178
|
+
if (!relativeDestination || relativeDestination.startsWith('../')) {
|
|
179
|
+
throw new Error(`invalid destination '${relativeRoot}'`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
destination,
|
|
184
|
+
relativeDestination,
|
|
185
|
+
relativeRoot: normalizedRoot,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const GLOBAL_BASES = Object.freeze({
|
|
190
|
+
home: (homeDir) => homeDir,
|
|
191
|
+
homeDir: (homeDir) => homeDir,
|
|
192
|
+
configHome: (homeDir, env) => env.XDG_CONFIG_HOME || path.join(homeDir, '.config'),
|
|
193
|
+
claudeHome: (homeDir, env) => env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude'),
|
|
194
|
+
codexHome: (homeDir, env) => env.CODEX_HOME || path.join(homeDir, '.codex'),
|
|
195
|
+
grokHome: (homeDir, env) => env.GROK_HOME || path.join(homeDir, '.grok'),
|
|
196
|
+
hermesHome: (homeDir, env) => env.HERMES_HOME || path.join(homeDir, '.hermes'),
|
|
197
|
+
vibeHome: (homeDir, env) => env.VIBE_HOME || path.join(homeDir, '.vibe'),
|
|
198
|
+
autohandHome: (homeDir, env) => env.AUTOHAND_HOME || path.join(homeDir, '.autohand'),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the user home used for Global Installation. Env wins so tests can sandbox.
|
|
203
|
+
*
|
|
204
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
export function resolveHomeDir(env = process.env) {
|
|
208
|
+
const fromEnv = env.HOME || env.USERPROFILE;
|
|
209
|
+
return path.resolve(fromEnv || os.homedir());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function posixPath(value) {
|
|
213
|
+
return String(value || '').replace(/\\/g, '/');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isUncPath(value) {
|
|
217
|
+
return /^[/\\]{2}[^/\\]+/.test(String(value || ''));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isFilesystemRoot(absolutePath) {
|
|
221
|
+
const resolved = path.resolve(absolutePath);
|
|
222
|
+
const parsed = path.parse(resolved);
|
|
223
|
+
return resolved === parsed.root || posixPath(resolved) === '/' || /^[A-Za-z]:[\\/]?$/.test(resolved);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isInsideHome(homeDir, absolutePath) {
|
|
227
|
+
const relative = path.relative(path.resolve(homeDir), path.resolve(absolutePath));
|
|
228
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function assertSafeUserPath(homeDir, absolutePath, label, { allowHome = false } = {}) {
|
|
232
|
+
const resolved = path.resolve(absolutePath);
|
|
233
|
+
if (isUncPath(resolved) || isUncPath(absolutePath)) {
|
|
234
|
+
throw new Error(`invalid destination '${label}': UNC path`);
|
|
235
|
+
}
|
|
236
|
+
if (isFilesystemRoot(resolved)) {
|
|
237
|
+
throw new Error(`invalid destination '${label}': filesystem root`);
|
|
238
|
+
}
|
|
239
|
+
if (!isInsideHome(homeDir, resolved)) {
|
|
240
|
+
throw new Error(`invalid destination '${label}': resolved path escapes the user home`);
|
|
241
|
+
}
|
|
242
|
+
if (!allowHome && path.resolve(resolved) === path.resolve(homeDir)) {
|
|
243
|
+
throw new Error(`invalid destination '${label}': user home root`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function splitSafeSegments(segment, label) {
|
|
248
|
+
const posix = posixPath(segment);
|
|
249
|
+
if (isUncPath(posix) || path.isAbsolute(segment) || /^[A-Za-z]:/.test(segment)) {
|
|
250
|
+
throw new Error(`invalid destination '${label}'`);
|
|
251
|
+
}
|
|
252
|
+
const parts = posix.split('/').filter((part) => part && part !== '.');
|
|
253
|
+
if (parts.some((part) => part === '..')) {
|
|
254
|
+
throw new Error(`invalid destination '${label}': traversal`);
|
|
255
|
+
}
|
|
256
|
+
return parts;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveKnownBase(baseName, homeDir, env, label) {
|
|
260
|
+
const resolver = GLOBAL_BASES[baseName];
|
|
261
|
+
if (!resolver) {
|
|
262
|
+
throw new Error(`unsupported global destination base '${baseName}'`);
|
|
263
|
+
}
|
|
264
|
+
const base = path.resolve(resolver(homeDir, env));
|
|
265
|
+
assertSafeUserPath(homeDir, base, label, { allowHome: true });
|
|
266
|
+
return base;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Expand a declarative global destination formula into an absolute path inside the user home.
|
|
271
|
+
*
|
|
272
|
+
* @param {object} formula
|
|
273
|
+
* @param {string} homeDir
|
|
274
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
275
|
+
* @returns {string|null}
|
|
276
|
+
*/
|
|
277
|
+
export function expandGlobalDestination(formula, homeDir, env = process.env) {
|
|
278
|
+
if (!formula || formula.kind === 'none') return null;
|
|
279
|
+
const label = formula.raw || formula.path || formula.kind;
|
|
280
|
+
if (formula.kind === 'literal') {
|
|
281
|
+
const relativeRoot = normalizeRelativeRoot(formula.path);
|
|
282
|
+
const destination = path.resolve(homeDir, ...splitSafeSegments(relativeRoot, label));
|
|
283
|
+
assertSafeUserPath(homeDir, destination, label);
|
|
284
|
+
return destination;
|
|
285
|
+
}
|
|
286
|
+
if (formula.kind === 'join') {
|
|
287
|
+
const base = resolveKnownBase(formula.base, homeDir, env, label);
|
|
288
|
+
const parts = (formula.segments || []).flatMap((segment) => splitSafeSegments(segment, label));
|
|
289
|
+
const destination = path.resolve(base, ...parts);
|
|
290
|
+
assertSafeUserPath(homeDir, destination, label);
|
|
291
|
+
return destination;
|
|
292
|
+
}
|
|
293
|
+
if (formula.kind === 'conditional') {
|
|
294
|
+
const base = resolveKnownBase(formula.base || 'homeDir', homeDir, env, label);
|
|
295
|
+
for (const option of formula.cases || []) {
|
|
296
|
+
if (option.probe == null) {
|
|
297
|
+
return expandGlobalDestination(option.formula, homeDir, env);
|
|
298
|
+
}
|
|
299
|
+
const probeParts = splitSafeSegments(option.probe, label);
|
|
300
|
+
const probePath = path.resolve(base, ...probeParts);
|
|
301
|
+
if (isInsideHome(homeDir, probePath) && fs.existsSync(probePath)) {
|
|
302
|
+
return expandGlobalDestination(option.formula, homeDir, env);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
throw new Error(`invalid destination '${label}'`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function expandGlobalRelativeRoot(formula, homeDir, env) {
|
|
311
|
+
try {
|
|
312
|
+
const absoluteRoot = expandGlobalDestination(formula, homeDir, env);
|
|
313
|
+
if (!absoluteRoot) return null;
|
|
314
|
+
const relativeRoot = path.relative(path.resolve(homeDir), absoluteRoot).replace(/\\/g, '/');
|
|
315
|
+
if (!relativeRoot || relativeRoot.startsWith('..') || path.isAbsolute(relativeRoot)) return null;
|
|
316
|
+
return { relativeRoot, absoluteRoot };
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Group every Agent Host by its Global Installation destination.
|
|
324
|
+
*
|
|
325
|
+
* @param {object} params
|
|
326
|
+
* @returns {object[]}
|
|
327
|
+
*/
|
|
328
|
+
export function listGlobalDestinationGroups({ registry, homeDir, env = process.env }) {
|
|
329
|
+
const resolvedHome = path.resolve(homeDir);
|
|
330
|
+
const grouped = new Map();
|
|
331
|
+
|
|
332
|
+
for (const host of registry.hosts || []) {
|
|
333
|
+
const expanded = expandGlobalRelativeRoot(host.destinations?.global, resolvedHome, env);
|
|
334
|
+
const relativeRoot = expanded?.relativeRoot || '';
|
|
335
|
+
const key = relativeRoot || `__none__:${host.id}`;
|
|
336
|
+
if (!grouped.has(key)) {
|
|
337
|
+
const universal = relativeRoot === UNIVERSAL_PROJECT_DESTINATION;
|
|
338
|
+
grouped.set(key, {
|
|
339
|
+
key,
|
|
340
|
+
relativeRoot,
|
|
341
|
+
absoluteRoot: expanded?.absoluteRoot || '',
|
|
342
|
+
universal,
|
|
343
|
+
selectedByDefault: universal,
|
|
344
|
+
selectable: Boolean(relativeRoot),
|
|
345
|
+
hosts: [],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
grouped.get(key).hosts.push({
|
|
349
|
+
id: host.id,
|
|
350
|
+
name: host.name,
|
|
351
|
+
displayName: host.displayName,
|
|
352
|
+
aliases: [...(host.aliases || [])],
|
|
353
|
+
detected: detectHost(host, env),
|
|
354
|
+
relativeRoot,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const groups = [...grouped.values()].sort((a, b) => compareRoots(a.relativeRoot || a.key, b.relativeRoot || b.key));
|
|
359
|
+
for (const group of groups) {
|
|
360
|
+
group.hosts.sort((a, b) => compareIds(a.id, b.id));
|
|
361
|
+
}
|
|
362
|
+
return groups;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Resolve one skill folder under a Global Installation destination root.
|
|
367
|
+
*
|
|
368
|
+
* @param {string} homeDir
|
|
369
|
+
* @param {string} relativeRoot
|
|
370
|
+
* @param {string} skillId
|
|
371
|
+
* @returns {{ destination: string, relativeDestination: string, relativeRoot: string }}
|
|
372
|
+
*/
|
|
373
|
+
export function resolveGlobalSkillPath(homeDir, relativeRoot, skillId) {
|
|
374
|
+
const resolvedHome = path.resolve(homeDir);
|
|
375
|
+
const normalizedRoot = normalizeRelativeRoot(relativeRoot);
|
|
376
|
+
if (!normalizedRoot) {
|
|
377
|
+
throw new Error(`invalid destination '${relativeRoot}': filesystem root`);
|
|
378
|
+
}
|
|
379
|
+
if (isUncPath(relativeRoot) || isUncPath(normalizedRoot)) {
|
|
380
|
+
throw new Error(`invalid destination '${relativeRoot}': UNC path`);
|
|
381
|
+
}
|
|
382
|
+
if (path.isAbsolute(relativeRoot) || path.isAbsolute(normalizedRoot) || /^[A-Za-z]:/.test(normalizedRoot)) {
|
|
383
|
+
throw new Error(`invalid destination '${relativeRoot}': filesystem root`);
|
|
384
|
+
}
|
|
385
|
+
const resolved = resolveSkillPath(resolvedHome, normalizedRoot, skillId);
|
|
386
|
+
assertSafeUserPath(resolvedHome, resolved.destination, relativeRoot);
|
|
387
|
+
return resolved;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeAbs(absolutePath) {
|
|
391
|
+
const resolved = path.resolve(absolutePath);
|
|
392
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function classifyPathPair(a, b) {
|
|
396
|
+
const left = normalizeAbs(a);
|
|
397
|
+
const right = normalizeAbs(b);
|
|
398
|
+
if (left === right) return 'duplicate';
|
|
399
|
+
const sep = path.sep;
|
|
400
|
+
if (left.startsWith(right + sep) || right.startsWith(left + sep)) return 'overlap';
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Fail-closed checks for selected Project Installation destinations.
|
|
406
|
+
* Returns human-readable error strings; an empty array means the set is safe.
|
|
407
|
+
*
|
|
408
|
+
* @param {object} params
|
|
409
|
+
* @returns {string[]}
|
|
410
|
+
*/
|
|
411
|
+
export function findDestinationConflicts({ projectRoot, skillIds, selectedRoots, isOwned, classify, resolvePath }) {
|
|
412
|
+
const errors = [];
|
|
413
|
+
const normalizedRoots = selectedRoots.map((root) => normalizeRelativeRoot(root));
|
|
414
|
+
const seenRoots = new Set();
|
|
415
|
+
const resolve = resolvePath || resolveSkillPath;
|
|
416
|
+
|
|
417
|
+
for (const root of normalizedRoots) {
|
|
418
|
+
if (seenRoots.has(root)) {
|
|
419
|
+
errors.push(`duplicate resolved destination '${root}'`);
|
|
420
|
+
}
|
|
421
|
+
seenRoots.add(root);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const resolved = [];
|
|
425
|
+
for (const root of [...seenRoots]) {
|
|
426
|
+
for (const skillId of skillIds) {
|
|
427
|
+
let skillPath;
|
|
428
|
+
try {
|
|
429
|
+
skillPath = resolve(projectRoot, root, skillId);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
errors.push(err.message);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
resolved.push({ root, skillId, ...skillPath });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
439
|
+
for (let j = i + 1; j < resolved.length; j++) {
|
|
440
|
+
const kind = classifyPathPair(resolved[i].destination, resolved[j].destination);
|
|
441
|
+
if (kind === 'duplicate') {
|
|
442
|
+
errors.push(
|
|
443
|
+
`duplicate resolved destination '${resolved[i].relativeDestination}'`,
|
|
444
|
+
);
|
|
445
|
+
} else if (kind === 'overlap') {
|
|
446
|
+
errors.push(
|
|
447
|
+
`overlapping destinations '${resolved[i].relativeDestination}' and '${resolved[j].relativeDestination}'`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const item of resolved) {
|
|
454
|
+
if (process.platform === 'win32' && item.destination.length >= 260) {
|
|
455
|
+
errors.push(
|
|
456
|
+
`Destination '${item.destination}' exceeds the Windows path length limit. Installation aborted.`,
|
|
457
|
+
);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (!pathExists(item.destination)) continue;
|
|
461
|
+
const expectedCanonical = resolveSkillPath(projectRoot, UNIVERSAL_PROJECT_DESTINATION, item.skillId);
|
|
462
|
+
const inspected = inspectManagedPath(item.destination, expectedCanonical.destination);
|
|
463
|
+
const owned = typeof isOwned === 'function'
|
|
464
|
+
? isOwned(item.skillId, item.destination)
|
|
465
|
+
: false;
|
|
466
|
+
if (inspected.broken) {
|
|
467
|
+
errors.push(
|
|
468
|
+
`Destination '${item.destination}' is a broken link. Installation aborted.`,
|
|
469
|
+
);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const classified = typeof classify === 'function'
|
|
473
|
+
? classify(item.skillId, item.destination)
|
|
474
|
+
: null;
|
|
475
|
+
if (classified?.adoptable) continue;
|
|
476
|
+
if (classified?.migratable) continue;
|
|
477
|
+
if (inspected.wrongTarget) {
|
|
478
|
+
errors.push(
|
|
479
|
+
`Destination '${item.destination}' is a wrong-target link. Installation aborted.`,
|
|
480
|
+
);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (!owned) {
|
|
484
|
+
errors.push(
|
|
485
|
+
`Destination '${item.destination}' already exists and is not owned by SigmaSkills. Safe adoption is not enabled. Installation aborted.`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return errors;
|
|
491
|
+
}
|