skillspub 0.1.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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/catalog.js +352 -0
- package/dist/cli.js +1562 -0
- package/dist/core.js +31 -0
- package/dist/explain.js +307 -0
- package/dist/harnesses/claude.js +95 -0
- package/dist/harnesses/grok.js +746 -0
- package/dist/harnesses/pi.js +1380 -0
- package/dist/harnesses/registry.js +30 -0
- package/dist/harnesses/target.js +5 -0
- package/dist/harnesses/types.js +1 -0
- package/dist/inventory.js +1437 -0
- package/dist/npx-skills.js +273 -0
- package/dist/reconcile.js +1014 -0
- package/dist/shared.js +1411 -0
- package/dist/source-verification.js +297 -0
- package/dist/targets/shared.js +13 -0
- package/dist/tui.js +2373 -0
- package/dist/view.js +321 -0
- package/package.json +51 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse } from 'smol-toml';
|
|
6
|
+
import { readStateFile, scanGlobalInventory, scanProjectInventory, writeStateFile, } from "../inventory.js";
|
|
7
|
+
import { sharedTargetDefinition } from "../targets/shared.js";
|
|
8
|
+
import { resolveHarnessTarget } from "./target.js";
|
|
9
|
+
const VERIFIED_REVISION = '19d42e35c07a9c9244f03f6df0c4c353f970d4f9';
|
|
10
|
+
const EVIDENCE = [
|
|
11
|
+
{
|
|
12
|
+
url: 'https://docs.x.ai/build/settings/reference',
|
|
13
|
+
verifiedVersion: VERIFIED_REVISION,
|
|
14
|
+
detail: 'GROK_HOME, Skills configuration, and Claude/Cursor compatibility settings.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
url: 'https://docs.x.ai/build/features/skills-plugins-marketplaces',
|
|
18
|
+
verifiedVersion: VERIFIED_REVISION,
|
|
19
|
+
detail: 'Grok Build Global, Project, Shared, and vendor-compatible Skill discovery roots.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
url: `https://github.com/xai-org/grok-build/blob/${VERIFIED_REVISION}/crates/codegen/xai-grok-agent/src/prompt/skills.rs`,
|
|
23
|
+
verifiedVersion: VERIFIED_REVISION,
|
|
24
|
+
detail: 'Canonical ignore-prefix filtering and compatibility discovery behavior.',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
function grokHome(target) {
|
|
28
|
+
return path.dirname(target.discoveryRoot);
|
|
29
|
+
}
|
|
30
|
+
function configFile(target) {
|
|
31
|
+
return path.join(grokHome(target), 'config.toml');
|
|
32
|
+
}
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function stringList(value, field) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string'))
|
|
40
|
+
throw new Error(`Grok config ${field} must be an array of strings`);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function compatibilitySkills(value, field) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return true;
|
|
46
|
+
if (typeof value !== 'boolean')
|
|
47
|
+
throw new Error(`Grok config ${field} must be a boolean`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function readRawConfig(file) {
|
|
51
|
+
try {
|
|
52
|
+
return fs.readFileSync(file, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === 'ENOENT')
|
|
56
|
+
return undefined;
|
|
57
|
+
throw new Error(`cannot read Grok config at ${file}: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function parseConfig(raw, file) {
|
|
61
|
+
try {
|
|
62
|
+
const value = parse(raw ?? '');
|
|
63
|
+
if (!isRecord(value))
|
|
64
|
+
throw new Error('must be a TOML table');
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`cannot parse Grok config at ${file}: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function optionalTable(value, field) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (!isRecord(value))
|
|
75
|
+
throw new Error(`Grok config ${field} must be a table`);
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function readConfig(file) {
|
|
79
|
+
const raw = readRawConfig(file);
|
|
80
|
+
const parsed = parseConfig(raw, file);
|
|
81
|
+
const skills = optionalTable(parsed.skills, 'skills');
|
|
82
|
+
const compat = optionalTable(parsed.compat, 'compat');
|
|
83
|
+
const claude = optionalTable(compat?.claude, 'compat.claude');
|
|
84
|
+
const cursor = optionalTable(compat?.cursor, 'compat.cursor');
|
|
85
|
+
return {
|
|
86
|
+
raw,
|
|
87
|
+
ignore: stringList(skills?.ignore, 'skills.ignore'),
|
|
88
|
+
paths: stringList(skills?.paths, 'skills.paths'),
|
|
89
|
+
disabled: stringList(skills?.disabled, 'skills.disabled'),
|
|
90
|
+
claudeSkills: compatibilitySkills(claude?.skills, 'compat.claude.skills'),
|
|
91
|
+
cursorSkills: compatibilitySkills(cursor?.skills, 'compat.cursor.skills'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function expandConfiguredPath(value) {
|
|
95
|
+
const expanded = value === '~' || value.startsWith('~/')
|
|
96
|
+
? path.join(os.homedir(), value.slice(2))
|
|
97
|
+
: value;
|
|
98
|
+
const absolute = path.resolve(expanded);
|
|
99
|
+
try {
|
|
100
|
+
return fs.realpathSync.native(absolute);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return absolute;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function contains(root, candidate) {
|
|
107
|
+
const relative = path.relative(expandConfiguredPath(root), expandConfiguredPath(candidate));
|
|
108
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
109
|
+
}
|
|
110
|
+
function containsPath(root, candidate) {
|
|
111
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
112
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
113
|
+
}
|
|
114
|
+
function uniquePaths(values) {
|
|
115
|
+
return [...new Map(values.map((value) => [expandConfiguredPath(value), value])).values()];
|
|
116
|
+
}
|
|
117
|
+
function coversRoot(ignore, root) {
|
|
118
|
+
return ignore.some((entry) => contains(entry, root));
|
|
119
|
+
}
|
|
120
|
+
function resolveGrokTarget(targets) {
|
|
121
|
+
return resolveHarnessTarget(targets, 'grok', () => grokAdapter.targetDefinition());
|
|
122
|
+
}
|
|
123
|
+
function resolveSharedTarget(targets) {
|
|
124
|
+
return resolveHarnessTarget(targets, 'shared', sharedTargetDefinition);
|
|
125
|
+
}
|
|
126
|
+
function operationReport(home, targets, projectPath) {
|
|
127
|
+
return projectPath
|
|
128
|
+
? scanProjectInventory(home, projectPath, targets, { persist: false })
|
|
129
|
+
: scanGlobalInventory(home, targets, { persist: false });
|
|
130
|
+
}
|
|
131
|
+
function projectCeiling(projectPath) {
|
|
132
|
+
for (let directory = projectPath;;) {
|
|
133
|
+
if (fs.existsSync(path.join(directory, '.git')))
|
|
134
|
+
return directory;
|
|
135
|
+
const parent = path.dirname(directory);
|
|
136
|
+
if (parent === directory)
|
|
137
|
+
return directory;
|
|
138
|
+
directory = parent;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function ancestorRoots(target, projectPath) {
|
|
142
|
+
if (!fs.existsSync(projectPath))
|
|
143
|
+
return [];
|
|
144
|
+
const roots = [];
|
|
145
|
+
const ceiling = projectCeiling(projectPath);
|
|
146
|
+
for (let directory = path.dirname(projectPath); contains(ceiling, directory); directory = path.dirname(directory)) {
|
|
147
|
+
const root = path.join(directory, target.projectPath);
|
|
148
|
+
if (fs.existsSync(root))
|
|
149
|
+
roots.push(root);
|
|
150
|
+
if (directory === ceiling)
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
return roots;
|
|
154
|
+
}
|
|
155
|
+
function projectedRoots(target, projectPath) {
|
|
156
|
+
if (!projectPath)
|
|
157
|
+
return [target.discoveryRoot];
|
|
158
|
+
return uniquePaths([
|
|
159
|
+
path.join(projectPath, target.projectPath),
|
|
160
|
+
...ancestorRoots(target, projectPath),
|
|
161
|
+
target.discoveryRoot,
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
function scopedRoots(target, projectPath) {
|
|
165
|
+
const roots = projectedRoots(target, projectPath);
|
|
166
|
+
if (!projectPath)
|
|
167
|
+
return [{ scope: 'global', discoveryRoot: roots[0] }];
|
|
168
|
+
return roots.map((discoveryRoot, index) => ({
|
|
169
|
+
scope: index === 0 ? 'project' : index === roots.length - 1 ? 'global' : 'parent',
|
|
170
|
+
discoveryRoot,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
function requiredSharedRoots(_home, targets, projectPath) {
|
|
174
|
+
return projectedRoots(resolveSharedTarget(targets), projectPath);
|
|
175
|
+
}
|
|
176
|
+
function operationGrokRoots(targets, projectPath) {
|
|
177
|
+
return projectedRoots(resolveGrokTarget(targets), projectPath);
|
|
178
|
+
}
|
|
179
|
+
function claimStateFile(home, projectPath) {
|
|
180
|
+
return projectPath
|
|
181
|
+
? path.join(projectPath, '.skillspub', 'state.json')
|
|
182
|
+
: path.join(home.configDir, 'state.json');
|
|
183
|
+
}
|
|
184
|
+
function managedClaim(home, file, roots, projectPath) {
|
|
185
|
+
const claim = readStateFile(claimStateFile(home, projectPath)).grokIsolation;
|
|
186
|
+
if (claim === undefined)
|
|
187
|
+
return 'none';
|
|
188
|
+
if (!isRecord(claim) || claim.file !== file || !Array.isArray(claim.roots))
|
|
189
|
+
return 'drift';
|
|
190
|
+
return claim.roots.length === roots.length &&
|
|
191
|
+
claim.roots.every((root, index) => root === roots[index])
|
|
192
|
+
? 'managed'
|
|
193
|
+
: 'drift';
|
|
194
|
+
}
|
|
195
|
+
function isolationStatus({ home, file, roots, isolated, projectPath, }) {
|
|
196
|
+
const claim = managedClaim(home, file, roots, projectPath);
|
|
197
|
+
if (claim === 'none')
|
|
198
|
+
return { status: 'unmanaged', detail: 'Grok Build isolation is not managed by SkillsPub.' };
|
|
199
|
+
return claim === 'managed' && isolated
|
|
200
|
+
? { status: 'managed', detail: 'SkillsPub-managed Grok Build isolation is active.' }
|
|
201
|
+
: { status: 'drift', detail: 'SkillsPub-managed Grok Build isolation has changed; run explicit reconcile.' };
|
|
202
|
+
}
|
|
203
|
+
function affectedLinks(report, roots, targetRoots) {
|
|
204
|
+
return report.relationships.flatMap((relationship) => {
|
|
205
|
+
const target = relationship.realPath;
|
|
206
|
+
if (relationship.targetKey !== 'grok' || relationship.form !== 'link' || !target ||
|
|
207
|
+
!targetRoots.some((root) => containsPath(root, relationship.path)) ||
|
|
208
|
+
!roots.some((root) => contains(root, target)))
|
|
209
|
+
return [];
|
|
210
|
+
return [{
|
|
211
|
+
path: relationship.path,
|
|
212
|
+
target,
|
|
213
|
+
targetId: relationship.targetId,
|
|
214
|
+
slot: relationship.slot,
|
|
215
|
+
}];
|
|
216
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
217
|
+
}
|
|
218
|
+
function relationshipGroups(report, links, targetRoots) {
|
|
219
|
+
const affectedPaths = new Set(links.map(({ path: linkPath }) => linkPath));
|
|
220
|
+
const effects = report.relationships.flatMap((relationship) => {
|
|
221
|
+
if (relationship.targetKey !== 'grok' ||
|
|
222
|
+
!targetRoots.some((root) => containsPath(root, relationship.path)))
|
|
223
|
+
return [];
|
|
224
|
+
const target = report.targets.find(({ id }) => id === relationship.targetId);
|
|
225
|
+
const sourcePath = relationship.realPath ?? relationship.target ?? relationship.path;
|
|
226
|
+
return [{
|
|
227
|
+
scope: target?.scope ?? report.scope,
|
|
228
|
+
targetId: relationship.targetId,
|
|
229
|
+
targetKey: relationship.targetKey,
|
|
230
|
+
resourceId: relationship.resourceId ?? sourcePath,
|
|
231
|
+
name: relationship.name,
|
|
232
|
+
slot: relationship.slot,
|
|
233
|
+
form: relationship.form,
|
|
234
|
+
activation: relationship.activation,
|
|
235
|
+
sourcePath,
|
|
236
|
+
targetPath: relationship.path,
|
|
237
|
+
plannedAction: affectedPaths.has(relationship.path) ? 'unlink' : 'retain',
|
|
238
|
+
sourcePreserved: true,
|
|
239
|
+
}];
|
|
240
|
+
}).sort((left, right) => left.scope.localeCompare(right.scope) ||
|
|
241
|
+
left.targetId.localeCompare(right.targetId) ||
|
|
242
|
+
left.targetPath.localeCompare(right.targetPath));
|
|
243
|
+
const groups = new Map();
|
|
244
|
+
for (const effect of effects) {
|
|
245
|
+
const key = `${effect.scope}\0${effect.targetId}\0${effect.targetKey}`;
|
|
246
|
+
const group = groups.get(key) ?? [];
|
|
247
|
+
group.push(effect);
|
|
248
|
+
groups.set(key, group);
|
|
249
|
+
}
|
|
250
|
+
return [...groups.entries()].map(([key, relationships]) => {
|
|
251
|
+
const [scope, targetId, targetKey] = key.split('\0');
|
|
252
|
+
return {
|
|
253
|
+
scope: scope,
|
|
254
|
+
targetId: targetId,
|
|
255
|
+
targetKey: targetKey,
|
|
256
|
+
relationships,
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function relationshipImpact(report, links, targetRoots, file, expectedHash, backupFile, manifestFile, change, inspection) {
|
|
261
|
+
const groups = relationshipGroups(report, links, targetRoots);
|
|
262
|
+
const effects = groups.flatMap(({ relationships }) => relationships);
|
|
263
|
+
const preservedSources = new Set(effects
|
|
264
|
+
.filter(({ plannedAction }) => plannedAction === 'unlink')
|
|
265
|
+
.map(({ resourceId }) => resourceId));
|
|
266
|
+
return {
|
|
267
|
+
summary: {
|
|
268
|
+
affectedRelationships: links.length,
|
|
269
|
+
unlinkedRelationships: links.length,
|
|
270
|
+
retainedRelationships: effects.length - links.length,
|
|
271
|
+
preservedSourceResources: preservedSources.size,
|
|
272
|
+
},
|
|
273
|
+
actual: {
|
|
274
|
+
relationshipCount: effects.length,
|
|
275
|
+
isolation: inspection.isolation.status,
|
|
276
|
+
},
|
|
277
|
+
desired: {
|
|
278
|
+
relationshipCount: effects.length - links.length,
|
|
279
|
+
isolation: 'managed',
|
|
280
|
+
},
|
|
281
|
+
drift: {
|
|
282
|
+
relationships: effects.filter(({ plannedAction }) => plannedAction === 'unlink'),
|
|
283
|
+
isolation: inspection.isolation.status !== 'managed',
|
|
284
|
+
},
|
|
285
|
+
groups,
|
|
286
|
+
configuration: {
|
|
287
|
+
path: file,
|
|
288
|
+
plannedAction: change ? 'write' : 'retain',
|
|
289
|
+
originalHash: expectedHash,
|
|
290
|
+
backupPath: backupFile,
|
|
291
|
+
},
|
|
292
|
+
recovery: {
|
|
293
|
+
manifestPath: manifestFile,
|
|
294
|
+
instructions: [
|
|
295
|
+
`Restore config: cp ${backupFile} ${file}`,
|
|
296
|
+
`Review removed Links: ${manifestFile}`,
|
|
297
|
+
'Recreate only the Links you still want; source Skill resources were not deleted.',
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function hash(value) {
|
|
303
|
+
return crypto.createHash('sha256').update(value ?? '').digest('hex');
|
|
304
|
+
}
|
|
305
|
+
function tomlValue(value) {
|
|
306
|
+
return typeof value === 'boolean'
|
|
307
|
+
? String(value)
|
|
308
|
+
: `[${value.map((entry) => JSON.stringify(entry)).join(', ')}]`;
|
|
309
|
+
}
|
|
310
|
+
function assignment(line) {
|
|
311
|
+
const match = line.match(/^(\s*)([A-Za-z0-9_-]+)\s*=/);
|
|
312
|
+
return match?.[2] ? { indent: match[1] ?? '', key: match[2] } : undefined;
|
|
313
|
+
}
|
|
314
|
+
function sectionEnd(lines, start) {
|
|
315
|
+
const offset = lines.slice(start + 1).findIndex((line) => /^\s*\[/.test(line));
|
|
316
|
+
return offset < 0 ? lines.length : start + offset + 1;
|
|
317
|
+
}
|
|
318
|
+
function scanTomlLine(line) {
|
|
319
|
+
let quote;
|
|
320
|
+
let escaped = false;
|
|
321
|
+
let delta = 0;
|
|
322
|
+
let opened = false;
|
|
323
|
+
for (let index = 0; index < line.length; index++) {
|
|
324
|
+
const character = line[index];
|
|
325
|
+
if (quote) {
|
|
326
|
+
if (escaped)
|
|
327
|
+
escaped = false;
|
|
328
|
+
else if (quote === '"' && character === '\\')
|
|
329
|
+
escaped = true;
|
|
330
|
+
else if (character === quote)
|
|
331
|
+
quote = undefined;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (character === '"' || character === "'")
|
|
335
|
+
quote = character;
|
|
336
|
+
else if (character === '#')
|
|
337
|
+
return { comment: line.slice(index), delta, opened };
|
|
338
|
+
else if (character === '[') {
|
|
339
|
+
delta++;
|
|
340
|
+
opened = true;
|
|
341
|
+
}
|
|
342
|
+
else if (character === ']')
|
|
343
|
+
delta--;
|
|
344
|
+
}
|
|
345
|
+
return { delta, opened };
|
|
346
|
+
}
|
|
347
|
+
function tomlComment(line) {
|
|
348
|
+
return scanTomlLine(line).comment;
|
|
349
|
+
}
|
|
350
|
+
function arrayAssignmentEnd(lines, start, end) {
|
|
351
|
+
let depth = 0;
|
|
352
|
+
let opened = false;
|
|
353
|
+
for (let index = start; index < end; index++) {
|
|
354
|
+
const change = scanTomlLine(lines[index] ?? '');
|
|
355
|
+
depth += change.delta;
|
|
356
|
+
opened ||= change.opened;
|
|
357
|
+
if (opened && depth <= 0)
|
|
358
|
+
return index + 1;
|
|
359
|
+
}
|
|
360
|
+
return start + 1;
|
|
361
|
+
}
|
|
362
|
+
function setTomlKey(raw, section, key, value) {
|
|
363
|
+
const lines = raw.split('\n');
|
|
364
|
+
const header = `[${section}]`;
|
|
365
|
+
const start = lines.findIndex((line) => line.trim().replace(/\s+#.*$/, '') === header);
|
|
366
|
+
if (start < 0) {
|
|
367
|
+
const prefix = raw && !raw.endsWith('\n') ? '\n' : '';
|
|
368
|
+
const separator = raw && !raw.endsWith('\n\n') ? '\n' : '';
|
|
369
|
+
return `${raw}${prefix}${separator}${header}\n${key} = ${tomlValue(value)}\n`;
|
|
370
|
+
}
|
|
371
|
+
const end = sectionEnd(lines, start);
|
|
372
|
+
const existing = lines.findIndex((line, index) => index > start && index < end && assignment(line)?.key === key);
|
|
373
|
+
if (existing < 0)
|
|
374
|
+
lines.splice(end, 0, `${key} = ${tomlValue(value)}`);
|
|
375
|
+
else {
|
|
376
|
+
const existingLine = lines[existing] ?? '';
|
|
377
|
+
const indent = assignment(existingLine)?.indent ?? '';
|
|
378
|
+
const comment = tomlComment(existingLine);
|
|
379
|
+
const replacement = `${indent}${key} = ${tomlValue(value)}${comment ? ` ${comment}` : ''}`;
|
|
380
|
+
const replacementEnd = Array.isArray(value) ? arrayAssignmentEnd(lines, existing, end) : existing + 1;
|
|
381
|
+
lines.splice(existing, replacementEnd - existing, replacement);
|
|
382
|
+
}
|
|
383
|
+
return lines.join('\n');
|
|
384
|
+
}
|
|
385
|
+
function updatedConfig(config, ignore) {
|
|
386
|
+
let raw = config.raw ?? '';
|
|
387
|
+
raw = setTomlKey(raw, 'skills', 'ignore', ignore);
|
|
388
|
+
raw = setTomlKey(raw, 'compat.claude', 'skills', false);
|
|
389
|
+
raw = setTomlKey(raw, 'compat.cursor', 'skills', false);
|
|
390
|
+
readConfigText(raw);
|
|
391
|
+
return raw;
|
|
392
|
+
}
|
|
393
|
+
function readConfigText(raw) {
|
|
394
|
+
try {
|
|
395
|
+
parse(raw);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
throw new Error(`generated Grok config is invalid: ${error.message}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function atomicWrite(file, raw) {
|
|
402
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
403
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
404
|
+
let descriptor;
|
|
405
|
+
try {
|
|
406
|
+
descriptor = fs.openSync(temporary, 'wx', 0o600);
|
|
407
|
+
fs.writeFileSync(descriptor, raw);
|
|
408
|
+
fs.closeSync(descriptor);
|
|
409
|
+
descriptor = undefined;
|
|
410
|
+
fs.renameSync(temporary, file);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (descriptor !== undefined)
|
|
414
|
+
fs.closeSync(descriptor);
|
|
415
|
+
fs.rmSync(temporary, { force: true });
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function assertSafeConfig(config, grokRoots) {
|
|
420
|
+
if (config.paths.length)
|
|
421
|
+
throw new Error('Grok setup requires skills.paths to be empty');
|
|
422
|
+
if (config.disabled.length)
|
|
423
|
+
throw new Error('Grok setup requires skills.disabled to be empty');
|
|
424
|
+
const conflict = config.ignore.find((entry) => grokRoots.some((root) => contains(entry, root)));
|
|
425
|
+
if (conflict)
|
|
426
|
+
throw new Error(`Grok skills.ignore conflicts with a managed Grok Target: ${conflict}`);
|
|
427
|
+
}
|
|
428
|
+
function buildPlan(home, targets, operation, selectedProject) {
|
|
429
|
+
const projectPath = selectedProject ? fs.realpathSync(selectedProject) : undefined;
|
|
430
|
+
const grokTarget = resolveGrokTarget(targets);
|
|
431
|
+
const file = configFile(grokTarget);
|
|
432
|
+
const config = readConfig(file);
|
|
433
|
+
const roots = requiredSharedRoots(home, targets, projectPath);
|
|
434
|
+
const inspection = grokAdapter.inspect(home, targets, projectPath);
|
|
435
|
+
if (operation === 'setup' && inspection.isolation.status === 'drift')
|
|
436
|
+
throw new Error('Grok Build isolation has drift; use explicit reconcile.');
|
|
437
|
+
const targetRoots = operationGrokRoots(targets, projectPath);
|
|
438
|
+
assertSafeConfig(config, targetRoots);
|
|
439
|
+
const rootsToAdd = roots.filter((root) => !coversRoot(config.ignore, root));
|
|
440
|
+
const ignore = uniquePaths([...config.ignore, ...rootsToAdd]);
|
|
441
|
+
const affectedRoots = operation === 'reconcile' ? roots : rootsToAdd;
|
|
442
|
+
const report = operationReport(home, targets, projectPath);
|
|
443
|
+
const links = affectedLinks(report, affectedRoots, targetRoots);
|
|
444
|
+
const updated = updatedConfig(config, ignore);
|
|
445
|
+
const id = `${Date.now()}-${crypto.randomUUID()}`;
|
|
446
|
+
const recoveryDir = projectPath
|
|
447
|
+
? path.join(projectPath, '.skillspub', 'grok-recovery')
|
|
448
|
+
: path.join(home.configDir, 'grok-recovery');
|
|
449
|
+
const backupFile = path.join(recoveryDir, `${id}.toml`);
|
|
450
|
+
const manifestFile = path.join(recoveryDir, `${id}.links.json`);
|
|
451
|
+
const change = rootsToAdd.length > 0 || config.claudeSkills || config.cursorSkills;
|
|
452
|
+
const expectedHash = hash(config.raw);
|
|
453
|
+
return {
|
|
454
|
+
file,
|
|
455
|
+
expected: config.raw,
|
|
456
|
+
expectedHash,
|
|
457
|
+
updated,
|
|
458
|
+
rootsToAdd,
|
|
459
|
+
affectedRoots,
|
|
460
|
+
targetRoots,
|
|
461
|
+
affectedLinks: links,
|
|
462
|
+
relationshipImpact: relationshipImpact(report, links, targetRoots, file, expectedHash, backupFile, manifestFile, change, inspection),
|
|
463
|
+
projectPath,
|
|
464
|
+
backupFile,
|
|
465
|
+
manifestFile,
|
|
466
|
+
change,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function sameLinks(left, right) {
|
|
470
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
471
|
+
}
|
|
472
|
+
function concurrentModification(message) {
|
|
473
|
+
return Object.assign(new Error(message), { code: 'concurrent_modification' });
|
|
474
|
+
}
|
|
475
|
+
function preflightApply(home, targets, plan) {
|
|
476
|
+
const current = readConfig(plan.file);
|
|
477
|
+
if (current.raw !== plan.expected || hash(current.raw) !== plan.expectedHash)
|
|
478
|
+
throw concurrentModification(`Grok config changed after preview: ${plan.file}`);
|
|
479
|
+
assertSafeConfig(current, operationGrokRoots(targets, plan.projectPath));
|
|
480
|
+
const report = operationReport(home, targets, plan.projectPath);
|
|
481
|
+
const currentLinks = affectedLinks(report, plan.affectedRoots, plan.targetRoots);
|
|
482
|
+
if (!sameLinks(currentLinks, plan.affectedLinks))
|
|
483
|
+
throw concurrentModification('Grok affected Links changed after preview');
|
|
484
|
+
const currentGroups = relationshipGroups(report, currentLinks, plan.targetRoots);
|
|
485
|
+
if (JSON.stringify(currentGroups) !== JSON.stringify(plan.relationshipImpact.groups))
|
|
486
|
+
throw concurrentModification('Grok Relationships changed after preview');
|
|
487
|
+
for (const link of plan.affectedLinks) {
|
|
488
|
+
const stat = fs.lstatSync(link.path);
|
|
489
|
+
if (!stat.isSymbolicLink() || fs.realpathSync(link.path) !== link.target)
|
|
490
|
+
throw concurrentModification(`Grok Link changed after preview: ${link.path}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function writeRecovery(plan) {
|
|
494
|
+
fs.mkdirSync(path.dirname(plan.backupFile), { recursive: true });
|
|
495
|
+
fs.writeFileSync(plan.backupFile, plan.expected ?? '', { flag: 'wx', mode: 0o600 });
|
|
496
|
+
fs.writeFileSync(plan.manifestFile, JSON.stringify({
|
|
497
|
+
config: plan.file,
|
|
498
|
+
originalHash: plan.expectedHash,
|
|
499
|
+
links: plan.relationshipImpact.groups.flatMap(({ relationships }) => relationships.filter(({ plannedAction }) => plannedAction === 'unlink')),
|
|
500
|
+
}, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
501
|
+
}
|
|
502
|
+
function saveClaim(home, targets, plan) {
|
|
503
|
+
const stateFile = claimStateFile(home, plan.projectPath);
|
|
504
|
+
const state = readStateFile(stateFile);
|
|
505
|
+
state.grokIsolation = {
|
|
506
|
+
file: plan.file,
|
|
507
|
+
roots: requiredSharedRoots(home, targets, plan.projectPath),
|
|
508
|
+
backupFile: plan.backupFile,
|
|
509
|
+
manifestFile: plan.manifestFile,
|
|
510
|
+
};
|
|
511
|
+
writeStateFile(stateFile, state);
|
|
512
|
+
}
|
|
513
|
+
function applyPlan(home, targets, plan) {
|
|
514
|
+
preflightApply(home, targets, plan);
|
|
515
|
+
writeRecovery(plan);
|
|
516
|
+
if (plan.change)
|
|
517
|
+
atomicWrite(plan.file, plan.updated);
|
|
518
|
+
for (const link of plan.affectedLinks)
|
|
519
|
+
fs.unlinkSync(link.path);
|
|
520
|
+
saveClaim(home, targets, plan);
|
|
521
|
+
}
|
|
522
|
+
function renderRelationshipImpact(plan) {
|
|
523
|
+
const { relationshipImpact: impact } = plan;
|
|
524
|
+
return [
|
|
525
|
+
`${impact.summary.unlinkedRelationships} Shared-backed Grok Link Relationships will be unlinked`,
|
|
526
|
+
`${impact.summary.preservedSourceResources} source Skill resources will be preserved`,
|
|
527
|
+
`${impact.summary.retainedRelationships} non-Shared Grok Relationship will be retained`,
|
|
528
|
+
`Actual\t${impact.actual.relationshipCount} Relationships; isolation ${impact.actual.isolation}`,
|
|
529
|
+
`Desired\t${impact.desired.relationshipCount} Relationships; isolation ${impact.desired.isolation}`,
|
|
530
|
+
`Drift\t${impact.drift.relationships.length} Relationship actions; ` +
|
|
531
|
+
`isolation ${impact.drift.isolation ? 'yes' : 'no'}`,
|
|
532
|
+
`configuration\t${impact.configuration.plannedAction}\t${impact.configuration.path}`,
|
|
533
|
+
`original hash\t${impact.configuration.originalHash}`,
|
|
534
|
+
`configuration backup\t${impact.configuration.backupPath}`,
|
|
535
|
+
`affected-Link manifest\t${impact.recovery.manifestPath}`,
|
|
536
|
+
...impact.groups.flatMap((group) => [
|
|
537
|
+
`scope ${group.scope}\tSkill Target ${group.targetKey} (${group.targetId})`,
|
|
538
|
+
...group.relationships.map((effect) => `${effect.plannedAction === 'unlink' ? 'Unlink' : 'Retain'}\t` +
|
|
539
|
+
`resource=${effect.resourceId}\tname=${effect.name}\tform=${effect.form}\t` +
|
|
540
|
+
`Activation=${effect.activation}\tsource=${effect.sourcePath}\t` +
|
|
541
|
+
`target=${effect.targetPath}\taction=${effect.plannedAction}`),
|
|
542
|
+
]),
|
|
543
|
+
'source preservation\tall listed source Skill resources remain on disk',
|
|
544
|
+
'verify\tActual, Desired, Drift, isolation, Relationship effects, and recovery evidence',
|
|
545
|
+
];
|
|
546
|
+
}
|
|
547
|
+
function operationResult(home, targets, plan, inspection) {
|
|
548
|
+
const report = operationReport(home, targets, plan.projectPath);
|
|
549
|
+
const present = new Set(report.relationships.map(({ targetId, path: relationshipPath }) => `${targetId}\0${relationshipPath}`));
|
|
550
|
+
const effects = plan.relationshipImpact.groups.flatMap(({ relationships }) => relationships);
|
|
551
|
+
const relationshipEffects = effects.map((effect) => {
|
|
552
|
+
const isPresent = present.has(`${effect.targetId}\0${effect.targetPath}`);
|
|
553
|
+
const desiredPresent = effect.plannedAction === 'retain';
|
|
554
|
+
return {
|
|
555
|
+
...effect,
|
|
556
|
+
outcome: isPresent === desiredPresent
|
|
557
|
+
? (desiredPresent ? 'retained' : 'unlinked')
|
|
558
|
+
: 'drift',
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
const unlinkSources = new Set(effects
|
|
562
|
+
.filter(({ plannedAction }) => plannedAction === 'unlink')
|
|
563
|
+
.map(({ sourcePath }) => sourcePath));
|
|
564
|
+
const actual = {
|
|
565
|
+
unlinkedRelationships: relationshipEffects.filter(({ outcome }) => outcome === 'unlinked').length,
|
|
566
|
+
retainedRelationships: relationshipEffects.filter(({ outcome }) => outcome === 'retained').length,
|
|
567
|
+
preservedSourceResources: [...unlinkSources].filter((sourcePath) => fs.existsSync(sourcePath)).length,
|
|
568
|
+
};
|
|
569
|
+
const desired = {
|
|
570
|
+
unlinkedRelationships: plan.relationshipImpact.summary.unlinkedRelationships,
|
|
571
|
+
retainedRelationships: plan.relationshipImpact.summary.retainedRelationships,
|
|
572
|
+
preservedSourceResources: plan.relationshipImpact.summary.preservedSourceResources,
|
|
573
|
+
};
|
|
574
|
+
return {
|
|
575
|
+
inspection,
|
|
576
|
+
actual,
|
|
577
|
+
desired,
|
|
578
|
+
drift: {
|
|
579
|
+
relationships: relationshipEffects
|
|
580
|
+
.filter(({ outcome }) => outcome === 'drift')
|
|
581
|
+
.map(({ outcome: _outcome, ...effect }) => effect),
|
|
582
|
+
isolation: inspection.isolation.status !== 'managed',
|
|
583
|
+
},
|
|
584
|
+
isolation: inspection.isolation,
|
|
585
|
+
relationshipEffects,
|
|
586
|
+
recovery: {
|
|
587
|
+
...plan.relationshipImpact.recovery,
|
|
588
|
+
configBackupPreserved: fs.existsSync(plan.backupFile),
|
|
589
|
+
manifestPreserved: fs.existsSync(plan.manifestFile),
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function planOperation(home, targets, operation, projectPath) {
|
|
594
|
+
const plan = buildPlan(home, targets, operation, projectPath);
|
|
595
|
+
return {
|
|
596
|
+
title: 'Grok Build isolation plan:',
|
|
597
|
+
lines: [
|
|
598
|
+
...plan.rootsToAdd.map((root) => `Shared ignore\t${root}`),
|
|
599
|
+
'compat.claude.skills\tfalse',
|
|
600
|
+
'compat.cursor.skills\tfalse',
|
|
601
|
+
...renderRelationshipImpact(plan),
|
|
602
|
+
],
|
|
603
|
+
recovery: plan.relationshipImpact.recovery.instructions,
|
|
604
|
+
relationshipImpact: plan.relationshipImpact,
|
|
605
|
+
apply: () => applyPlan(home, targets, plan),
|
|
606
|
+
verify() {
|
|
607
|
+
const inspection = grokAdapter.inspect(home, targets, plan.projectPath);
|
|
608
|
+
if (inspection.sharedConsumption.status !== 'excluded' || inspection.isolation.status !== 'managed')
|
|
609
|
+
throw new Error(`Grok isolation verification failed: ${inspection.sharedConsumption.detail}`);
|
|
610
|
+
return inspection;
|
|
611
|
+
},
|
|
612
|
+
result: (inspection) => operationResult(home, targets, plan, inspection),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
export const grokAdapter = {
|
|
616
|
+
key: 'grok',
|
|
617
|
+
name: 'Grok Build',
|
|
618
|
+
targetDefinition() {
|
|
619
|
+
const root = process.env.GROK_HOME || path.join(os.homedir(), '.grok');
|
|
620
|
+
return {
|
|
621
|
+
key: 'grok',
|
|
622
|
+
kind: 'harness',
|
|
623
|
+
discoveryRoot: path.join(root, 'skills'),
|
|
624
|
+
parkingRoot: path.join(root, '.skillspub-off', 'skills'),
|
|
625
|
+
projectPath: '.grok/skills',
|
|
626
|
+
relationship: { support: 'managed', link: 'unsupported' },
|
|
627
|
+
};
|
|
628
|
+
},
|
|
629
|
+
inspect(home, targets, projectPath) {
|
|
630
|
+
const grokTarget = resolveGrokTarget(targets);
|
|
631
|
+
const sharedTarget = resolveSharedTarget(targets);
|
|
632
|
+
const claudeTarget = targets.find(({ key }) => key === 'claude');
|
|
633
|
+
const cursorRoot = path.join(os.homedir(), '.cursor');
|
|
634
|
+
const cursorTarget = {
|
|
635
|
+
key: 'cursor',
|
|
636
|
+
kind: 'harness',
|
|
637
|
+
discoveryRoot: path.join(cursorRoot, 'skills'),
|
|
638
|
+
parkingRoot: path.join(cursorRoot, '.skillspub-off', 'skills'),
|
|
639
|
+
projectPath: '.cursor/skills',
|
|
640
|
+
};
|
|
641
|
+
const root = grokHome(grokTarget);
|
|
642
|
+
const selectedProject = projectPath ? path.resolve(projectPath) : undefined;
|
|
643
|
+
const file = configFile(grokTarget);
|
|
644
|
+
const detected = fs.existsSync(root) || Boolean(selectedProject && fs.existsSync(path.join(selectedProject, '.grok')));
|
|
645
|
+
const sharedRootPaths = requiredSharedRoots(home, targets, selectedProject);
|
|
646
|
+
const grokRoots = scopedRoots(grokTarget, selectedProject);
|
|
647
|
+
const sharedRoots = scopedRoots(sharedTarget, selectedProject);
|
|
648
|
+
const compatibilityRoots = [
|
|
649
|
+
...(claudeTarget ? scopedRoots(claudeTarget, selectedProject).map((entry) => ({
|
|
650
|
+
...entry,
|
|
651
|
+
targetKey: 'claude',
|
|
652
|
+
})) : []),
|
|
653
|
+
...scopedRoots(cursorTarget, selectedProject).map((entry) => ({
|
|
654
|
+
...entry,
|
|
655
|
+
targetKey: 'cursor',
|
|
656
|
+
})),
|
|
657
|
+
];
|
|
658
|
+
let sharedConsumption;
|
|
659
|
+
let isolation;
|
|
660
|
+
let discoveryRoots;
|
|
661
|
+
try {
|
|
662
|
+
const config = readConfig(file);
|
|
663
|
+
assertSafeConfig(config, grokRoots.map(({ discoveryRoot }) => discoveryRoot));
|
|
664
|
+
const sharedExcluded = sharedRootPaths.every((sharedRoot) => coversRoot(config.ignore, sharedRoot));
|
|
665
|
+
const compatibilityExcluded = compatibilityRoots.every(({ discoveryRoot, targetKey }) => coversRoot(config.ignore, discoveryRoot) ||
|
|
666
|
+
(targetKey === 'claude' ? !config.claudeSkills : !config.cursorSkills));
|
|
667
|
+
const isolated = sharedExcluded && compatibilityExcluded;
|
|
668
|
+
sharedConsumption = sharedExcluded
|
|
669
|
+
? { status: 'excluded', detail: `Grok Build excludes Shared skills at ${sharedRootPaths.join(', ')}.` }
|
|
670
|
+
: { status: 'enabled', detail: `Grok Build still discovers Shared skills at ${sharedRootPaths.join(', ')}.` };
|
|
671
|
+
isolation = isolationStatus({
|
|
672
|
+
home,
|
|
673
|
+
file,
|
|
674
|
+
roots: sharedRootPaths,
|
|
675
|
+
isolated,
|
|
676
|
+
projectPath: selectedProject,
|
|
677
|
+
});
|
|
678
|
+
discoveryRoots = [
|
|
679
|
+
...grokRoots.map((entry) => ({
|
|
680
|
+
kind: 'harness',
|
|
681
|
+
targetKey: 'grok',
|
|
682
|
+
...entry,
|
|
683
|
+
consumption: 'consumed',
|
|
684
|
+
reason: 'Grok Build discovers this native Skill Target.',
|
|
685
|
+
})),
|
|
686
|
+
...sharedRoots.map((entry) => {
|
|
687
|
+
const excluded = coversRoot(config.ignore, entry.discoveryRoot);
|
|
688
|
+
return {
|
|
689
|
+
kind: 'shared',
|
|
690
|
+
targetKey: 'shared',
|
|
691
|
+
...entry,
|
|
692
|
+
consumption: excluded ? 'excluded' : 'consumed',
|
|
693
|
+
reason: excluded
|
|
694
|
+
? 'Grok Build settings ignore this Shared root.'
|
|
695
|
+
: 'Grok Build settings allow this Shared root.',
|
|
696
|
+
};
|
|
697
|
+
}),
|
|
698
|
+
...compatibilityRoots.map((entry) => {
|
|
699
|
+
const enabled = entry.targetKey === 'claude' ? config.claudeSkills : config.cursorSkills;
|
|
700
|
+
const excluded = !enabled || coversRoot(config.ignore, entry.discoveryRoot);
|
|
701
|
+
return {
|
|
702
|
+
kind: 'compatibility',
|
|
703
|
+
...entry,
|
|
704
|
+
consumption: excluded ? 'excluded' : 'consumed',
|
|
705
|
+
reason: excluded
|
|
706
|
+
? `Grok Build excludes ${entry.targetKey} compatibility Skills here.`
|
|
707
|
+
: `Grok Build consumes ${entry.targetKey} compatibility Skills here.`,
|
|
708
|
+
};
|
|
709
|
+
}),
|
|
710
|
+
];
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
const detail = error.message;
|
|
714
|
+
sharedConsumption = { status: 'unknown', detail };
|
|
715
|
+
isolation = { status: 'unknown', detail };
|
|
716
|
+
discoveryRoots = [
|
|
717
|
+
...grokRoots.map((entry) => ({ kind: 'harness', targetKey: 'grok', ...entry })),
|
|
718
|
+
...sharedRoots.map((entry) => ({ kind: 'shared', targetKey: 'shared', ...entry })),
|
|
719
|
+
...compatibilityRoots.map((entry) => ({ kind: 'compatibility', ...entry })),
|
|
720
|
+
].map((entry) => ({ ...entry, consumption: 'unknown', reason: detail }));
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
key: 'grok',
|
|
724
|
+
name: 'Grok Build',
|
|
725
|
+
detected,
|
|
726
|
+
support: 'managed',
|
|
727
|
+
evidence: EVIDENCE,
|
|
728
|
+
targets: [
|
|
729
|
+
{ scope: 'global', discoveryRoot: grokTarget.discoveryRoot },
|
|
730
|
+
...(selectedProject ? [{
|
|
731
|
+
scope: 'project',
|
|
732
|
+
discoveryRoot: path.join(selectedProject, grokTarget.projectPath),
|
|
733
|
+
}] : []),
|
|
734
|
+
],
|
|
735
|
+
roots: discoveryRoots,
|
|
736
|
+
sharedConsumption,
|
|
737
|
+
isolation,
|
|
738
|
+
link: { supported: false },
|
|
739
|
+
mirror: { supported: true },
|
|
740
|
+
};
|
|
741
|
+
},
|
|
742
|
+
operations: {
|
|
743
|
+
setup: (home, targets, projectPath) => planOperation(home, targets, 'setup', projectPath),
|
|
744
|
+
reconcile: (home, targets, projectPath) => planOperation(home, targets, 'reconcile', projectPath),
|
|
745
|
+
},
|
|
746
|
+
};
|