@mohammadhprp/system-prompt 0.12.6 → 0.13.1
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/framework/skills/README.md +1 -0
- package/framework/skills/grilling/SKILL.md +34 -0
- package/framework/skills/grilling/examples.md +47 -0
- package/package.json +1 -1
- package/src/agent-configs.js +78 -10
- package/src/catalog.js +13 -0
- package/src/cli.js +355 -181
- package/src/doctor.js +24 -13
- package/src/installer.js +167 -65
- package/src/item-layout.js +26 -0
package/src/doctor.js
CHANGED
|
@@ -3,7 +3,8 @@ import { access, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
|
|
5
5
|
import { categories } from './catalog.js';
|
|
6
|
-
import { loadLockFile } from './installer.js';
|
|
6
|
+
import { loadLockFile, lockToSelections } from './installer.js';
|
|
7
|
+
import { itemRelativePath } from './item-layout.js';
|
|
7
8
|
|
|
8
9
|
async function exists(path) {
|
|
9
10
|
try {
|
|
@@ -14,6 +15,16 @@ async function exists(path) {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
async function checkManagedFile(absTarget, relativePath, expectedHash, issues) {
|
|
19
|
+
try {
|
|
20
|
+
const actual = createHash('sha256').update(await readFile(resolve(absTarget, relativePath))).digest('hex');
|
|
21
|
+
if (actual !== expectedHash) issues.push(`Modified managed file: ${relativePath}`);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT') issues.push(`Missing managed file: ${relativePath}`);
|
|
24
|
+
else throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export async function inspectInstallation(targetDir) {
|
|
18
29
|
const absTarget = resolve(process.cwd(), targetDir);
|
|
19
30
|
const issues = [];
|
|
@@ -31,24 +42,24 @@ export async function inspectInstallation(targetDir) {
|
|
|
31
42
|
return { targetDir: absTarget, issues };
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
for (const [path,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
for (const [path, entry] of Object.entries(lock.generated || {})) {
|
|
46
|
+
await checkManagedFile(absTarget, path, entry.computedHash, issues);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [category, entries] of Object.entries(lock)) {
|
|
50
|
+
if (!categories[category]) continue;
|
|
51
|
+
for (const entry of Object.values(entries)) {
|
|
52
|
+
for (const [path, expected] of Object.entries(entry.files || {})) {
|
|
53
|
+
await checkManagedFile(absTarget, path, expected, issues);
|
|
54
|
+
}
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
for (const [category, ids] of Object.entries(lock
|
|
58
|
+
for (const [category, ids] of Object.entries(lockToSelections(lock))) {
|
|
45
59
|
const config = categories[category];
|
|
46
60
|
for (const id of ids) {
|
|
47
61
|
const item = config.items.find(entry => entry.id === id);
|
|
48
|
-
|
|
49
|
-
? `${config.sourceDir.replace(/^framework\//, '')}/${id}.md`
|
|
50
|
-
: `${config.sourceDir.replace(/^framework\//, '')}/${id}`;
|
|
51
|
-
if (!item || !(await exists(resolve(absTarget, relativePath)))) {
|
|
62
|
+
if (!item || !(await exists(resolve(absTarget, itemRelativePath(category, id))))) {
|
|
52
63
|
issues.push(`Missing installed ${category} item: ${id}`);
|
|
53
64
|
}
|
|
54
65
|
}
|
package/src/installer.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
|
|
6
6
|
import { categories } from './catalog.js';
|
|
7
7
|
import { loadMcpConfigs, generateOpenCodeConfig, generateTuiConfig } from './agent-configs.js';
|
|
8
|
+
import { LOCK_CATEGORIES, isFileBased, isCopyable, itemSourcePath, itemRelativePath, targetSubdir } from './item-layout.js';
|
|
8
9
|
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const packageRoot = resolve(__dirname, '..');
|
|
@@ -86,6 +87,38 @@ package-lock.json
|
|
|
86
87
|
bun.lock
|
|
87
88
|
`;
|
|
88
89
|
|
|
90
|
+
export const LOCK_VERSION = 1;
|
|
91
|
+
|
|
92
|
+
export function lockToSelections(lock) {
|
|
93
|
+
const selections = {};
|
|
94
|
+
if (!lock) return selections;
|
|
95
|
+
for (const category of LOCK_CATEGORIES) {
|
|
96
|
+
const ids = Object.keys(lock[category] || {});
|
|
97
|
+
if (ids.length) selections[category] = ids;
|
|
98
|
+
}
|
|
99
|
+
return selections;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildComputedHash(files, fallbackId) {
|
|
103
|
+
const keys = Object.keys(files).sort();
|
|
104
|
+
if (keys.length === 0) return hash(fallbackId);
|
|
105
|
+
if (keys.length === 1) return files[keys[0]];
|
|
106
|
+
return hash(keys.map(path => `${path}:${files[path]}`).join('\n'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getExpectedHash(oldLock, relativePath) {
|
|
110
|
+
const generated = oldLock?.generated?.[relativePath]?.computedHash;
|
|
111
|
+
if (generated) return generated;
|
|
112
|
+
for (const category of LOCK_CATEGORIES) {
|
|
113
|
+
const entries = oldLock?.[category];
|
|
114
|
+
if (!entries) continue;
|
|
115
|
+
for (const entry of Object.values(entries)) {
|
|
116
|
+
const value = entry?.files?.[relativePath];
|
|
117
|
+
if (value) return value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
89
122
|
export async function getPackageVersion() {
|
|
90
123
|
try {
|
|
91
124
|
const pkg = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf-8'));
|
|
@@ -114,26 +147,63 @@ function validateLock(lock) {
|
|
|
114
147
|
if (!lock || typeof lock !== 'object' || Array.isArray(lock)) {
|
|
115
148
|
throw new Error('Invalid system-prompt lock: expected an object');
|
|
116
149
|
}
|
|
117
|
-
if (
|
|
118
|
-
throw new Error('Invalid system-prompt lock:
|
|
150
|
+
if (lock.selections !== undefined || lock.managedFiles !== undefined) {
|
|
151
|
+
throw new Error('Invalid system-prompt lock: legacy lock format no longer supported');
|
|
119
152
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
153
|
+
if (lock.version !== LOCK_VERSION) {
|
|
154
|
+
throw new Error('Invalid system-prompt lock: unsupported version');
|
|
155
|
+
}
|
|
156
|
+
if (typeof lock.agentType !== 'string' || typeof lock.installedAt !== 'string') {
|
|
157
|
+
throw new Error('Invalid system-prompt lock: missing installation metadata');
|
|
158
|
+
}
|
|
159
|
+
if (typeof lock.includeAgentsMd !== 'boolean') {
|
|
160
|
+
throw new Error('Invalid system-prompt lock: includeAgentsMd must be a boolean');
|
|
161
|
+
}
|
|
162
|
+
const allowed = new Set(['version', 'agentType', 'installedAt', 'includeAgentsMd', 'generated', ...LOCK_CATEGORIES]);
|
|
163
|
+
for (const key of Object.keys(lock)) {
|
|
164
|
+
if (!allowed.has(key)) {
|
|
165
|
+
throw new Error(`Invalid system-prompt lock: unknown key ${key}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const category of LOCK_CATEGORIES) {
|
|
169
|
+
const entries = lock[category];
|
|
170
|
+
if (entries === undefined) continue;
|
|
171
|
+
if (!entries || typeof entries !== 'object' || Array.isArray(entries)) {
|
|
123
172
|
throw new Error(`Invalid system-prompt lock: invalid ${category} selection`);
|
|
124
173
|
}
|
|
174
|
+
const config = categories[category];
|
|
125
175
|
const knownIds = new Set(config.items.map(item => item.id));
|
|
126
|
-
|
|
127
|
-
|
|
176
|
+
for (const [id, entry] of Object.entries(entries)) {
|
|
177
|
+
if (!knownIds.has(id)) {
|
|
178
|
+
throw new Error(`Invalid system-prompt lock: unknown ${category} item`);
|
|
179
|
+
}
|
|
180
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
181
|
+
throw new Error(`Invalid system-prompt lock: invalid ${category} entry`);
|
|
182
|
+
}
|
|
183
|
+
if (typeof entry.source !== 'string' || typeof entry.sourceType !== 'string' || typeof entry.itemPath !== 'string') {
|
|
184
|
+
throw new Error(`Invalid system-prompt lock: invalid ${category} entry source`);
|
|
185
|
+
}
|
|
186
|
+
if (!/^[a-f0-9]{64}$/.test(entry.computedHash || '')) {
|
|
187
|
+
throw new Error(`Invalid system-prompt lock: invalid ${category} entry hash`);
|
|
188
|
+
}
|
|
189
|
+
if (!entry.files || typeof entry.files !== 'object' || Array.isArray(entry.files)) {
|
|
190
|
+
throw new Error(`Invalid system-prompt lock: invalid ${category} entry files`);
|
|
191
|
+
}
|
|
192
|
+
for (const [path, checksum] of Object.entries(entry.files)) {
|
|
193
|
+
if (path.startsWith('/') || path.split('/').includes('..') || !/^[a-f0-9]{64}$/.test(checksum)) {
|
|
194
|
+
throw new Error('Invalid system-prompt lock: unsafe managed file entry');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
128
197
|
}
|
|
129
198
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
199
|
+
const generated = lock.generated;
|
|
200
|
+
if (generated !== undefined) {
|
|
201
|
+
if (!generated || typeof generated !== 'object' || Array.isArray(generated)) {
|
|
202
|
+
throw new Error('Invalid system-prompt lock: generated must be an object');
|
|
133
203
|
}
|
|
134
|
-
for (const [path,
|
|
135
|
-
if (path.startsWith('/') || path.split('/').includes('..') || !/^[a-f0-9]{64}$/.test(
|
|
136
|
-
throw new Error('Invalid system-prompt lock: unsafe
|
|
204
|
+
for (const [path, entry] of Object.entries(generated)) {
|
|
205
|
+
if (path.startsWith('/') || path.split('/').includes('..') || !/^[a-f0-9]{64}$/.test(entry?.computedHash || '')) {
|
|
206
|
+
throw new Error('Invalid system-prompt lock: unsafe generated file entry');
|
|
137
207
|
}
|
|
138
208
|
}
|
|
139
209
|
}
|
|
@@ -152,7 +222,7 @@ async function canWrite(destFile, relativePath, oldLock, force) {
|
|
|
152
222
|
if (force) return true;
|
|
153
223
|
try {
|
|
154
224
|
const existing = await readFile(destFile);
|
|
155
|
-
const previousHash = oldLock
|
|
225
|
+
const previousHash = getExpectedHash(oldLock, relativePath);
|
|
156
226
|
return Boolean(previousHash && previousHash === hash(existing));
|
|
157
227
|
} catch (error) {
|
|
158
228
|
if (error.code === 'ENOENT') return true;
|
|
@@ -160,7 +230,7 @@ async function canWrite(destFile, relativePath, oldLock, force) {
|
|
|
160
230
|
}
|
|
161
231
|
}
|
|
162
232
|
|
|
163
|
-
async function writeManagedFile(destFile, content, relativePath, options) {
|
|
233
|
+
async function writeManagedFile(destFile, content, relativePath, options, owner) {
|
|
164
234
|
await assertSafeDestination(options.targetDir, destFile);
|
|
165
235
|
if (!options.allowExistingMerge && !(await canWrite(destFile, relativePath, options.oldLock, options.force))) {
|
|
166
236
|
console.warn(` ⚠ Preserving existing file: ${relativePath}`);
|
|
@@ -169,10 +239,11 @@ async function writeManagedFile(destFile, content, relativePath, options) {
|
|
|
169
239
|
if (!options.dryRun) await mkdir(dirname(destFile), { recursive: true });
|
|
170
240
|
if (!options.dryRun) await writeFile(destFile, content);
|
|
171
241
|
options.managedFiles[relativePath] = hash(content);
|
|
242
|
+
if (owner) options.fileOwners[relativePath] = owner;
|
|
172
243
|
return true;
|
|
173
244
|
}
|
|
174
245
|
|
|
175
|
-
async function copyDir(src, dest, relativeDir, options) {
|
|
246
|
+
async function copyDir(src, dest, relativeDir, options, owner) {
|
|
176
247
|
if (!options.dryRun) await mkdir(dest, { recursive: true });
|
|
177
248
|
const entries = await readdir(src, { withFileTypes: true });
|
|
178
249
|
|
|
@@ -183,10 +254,10 @@ async function copyDir(src, dest, relativeDir, options) {
|
|
|
183
254
|
const relativePath = `${relativeDir}/${entry.name}`;
|
|
184
255
|
|
|
185
256
|
if (entry.isDirectory()) {
|
|
186
|
-
await copyDir(srcPath, destPath, relativePath, options);
|
|
257
|
+
await copyDir(srcPath, destPath, relativePath, options, owner);
|
|
187
258
|
} else if (entry.isFile()) {
|
|
188
259
|
const content = await readFile(srcPath);
|
|
189
|
-
await writeManagedFile(destPath, content, relativePath, options);
|
|
260
|
+
await writeManagedFile(destPath, content, relativePath, options, owner);
|
|
190
261
|
}
|
|
191
262
|
}
|
|
192
263
|
}
|
|
@@ -229,22 +300,22 @@ async function copySelectedDirs(targetDir, category, selectedIds, options) {
|
|
|
229
300
|
const catConfig = categories[category];
|
|
230
301
|
if (!catConfig || !selectedIds?.length) return;
|
|
231
302
|
|
|
232
|
-
const
|
|
233
|
-
const destParent = resolve(targetDir, relativeDir);
|
|
303
|
+
const destParent = resolve(targetDir, targetSubdir(catConfig.sourceDir));
|
|
234
304
|
|
|
235
305
|
for (const id of selectedIds) {
|
|
236
306
|
if (await isRemoved(category, id)) continue;
|
|
237
307
|
|
|
238
|
-
const
|
|
308
|
+
const source = itemSourcePath(category, id);
|
|
309
|
+
const srcPath = resolveSource(source);
|
|
239
310
|
const destPath = resolve(destParent, id);
|
|
240
311
|
await assertSafeDestination(targetDir, destPath);
|
|
241
312
|
|
|
242
313
|
try {
|
|
243
314
|
await stat(srcPath);
|
|
244
|
-
await copyDir(srcPath, destPath,
|
|
315
|
+
await copyDir(srcPath, destPath, itemRelativePath(category, id), options, { category, id });
|
|
245
316
|
} catch (error) {
|
|
246
317
|
if (sourceMissing(error)) {
|
|
247
|
-
console.warn(` ⚠ Source not found: ${
|
|
318
|
+
console.warn(` ⚠ Source not found: ${source}`);
|
|
248
319
|
continue;
|
|
249
320
|
}
|
|
250
321
|
throw error;
|
|
@@ -256,22 +327,22 @@ async function copySelectedFiles(targetDir, category, selectedIds, options) {
|
|
|
256
327
|
const catConfig = categories[category];
|
|
257
328
|
if (!catConfig || !selectedIds?.length) return;
|
|
258
329
|
|
|
259
|
-
const
|
|
260
|
-
const destParent = resolve(targetDir, relativeDir);
|
|
330
|
+
const destParent = resolve(targetDir, targetSubdir(catConfig.sourceDir));
|
|
261
331
|
if (!options.dryRun) await mkdir(destParent, { recursive: true });
|
|
262
332
|
|
|
263
333
|
for (const id of selectedIds) {
|
|
264
334
|
if (await isRemoved(category, id)) continue;
|
|
265
335
|
|
|
266
|
-
const
|
|
267
|
-
const
|
|
336
|
+
const source = itemSourcePath(category, id);
|
|
337
|
+
const relativePath = itemRelativePath(category, id);
|
|
338
|
+
const destFile = resolve(targetDir, relativePath);
|
|
268
339
|
await assertSafeDestination(targetDir, destFile);
|
|
269
340
|
try {
|
|
270
|
-
const content = await readFile(
|
|
271
|
-
await writeManagedFile(destFile, content,
|
|
341
|
+
const content = await readFile(resolveSource(source));
|
|
342
|
+
await writeManagedFile(destFile, content, relativePath, options, { category, id });
|
|
272
343
|
} catch (error) {
|
|
273
344
|
if (sourceMissing(error)) {
|
|
274
|
-
console.warn(` ⚠ Source not found: ${
|
|
345
|
+
console.warn(` ⚠ Source not found: ${source}`);
|
|
275
346
|
continue;
|
|
276
347
|
}
|
|
277
348
|
throw error;
|
|
@@ -280,22 +351,14 @@ async function copySelectedFiles(targetDir, category, selectedIds, options) {
|
|
|
280
351
|
}
|
|
281
352
|
|
|
282
353
|
async function deleteSelectedItems(absTarget, category, ids, oldLock, force, dryRun) {
|
|
283
|
-
|
|
284
|
-
if (!catConfig || !ids?.length) return;
|
|
354
|
+
if (!categories[category] || !ids?.length || !isCopyable(category)) return;
|
|
285
355
|
|
|
286
|
-
const FILE_BASED = new Set(['agents', 'commands', 'memory', 'modes', 'standards', 'templates']);
|
|
287
|
-
if (!FILE_BASED.has(category) && category !== 'skills' && category !== 'styles') return;
|
|
288
|
-
|
|
289
|
-
const relativeDir = targetSubdir(catConfig.sourceDir);
|
|
290
356
|
for (const id of ids) {
|
|
291
|
-
const relativePath =
|
|
292
|
-
? `${relativeDir}/${id}.md`
|
|
293
|
-
: `${relativeDir}/${id}`;
|
|
357
|
+
const relativePath = itemRelativePath(category, id);
|
|
294
358
|
const destPath = resolve(absTarget, relativePath);
|
|
295
359
|
await assertSafeDestination(absTarget, destPath);
|
|
296
360
|
if (!force && oldLock) {
|
|
297
|
-
const managedEntries = Object.entries(oldLock
|
|
298
|
-
.filter(([path]) => path === relativePath || path.startsWith(`${relativePath}/`));
|
|
361
|
+
const managedEntries = Object.entries(oldLock?.[category]?.[id]?.files || {});
|
|
299
362
|
if (managedEntries.length === 0) {
|
|
300
363
|
console.warn(` ⚠ Preserving unmanaged item: ${relativePath}`);
|
|
301
364
|
continue;
|
|
@@ -313,7 +376,7 @@ async function deleteSelectedItems(absTarget, category, ids, oldLock, force, dry
|
|
|
313
376
|
console.warn(` ⚠ Preserving modified item: ${relativePath}`);
|
|
314
377
|
continue;
|
|
315
378
|
}
|
|
316
|
-
if (!
|
|
379
|
+
if (!isFileBased(category)) {
|
|
317
380
|
if (!dryRun) {
|
|
318
381
|
for (const [path] of managedEntries) await rm(resolve(absTarget, path), { force: true });
|
|
319
382
|
}
|
|
@@ -363,7 +426,7 @@ async function mergeJsonFile(destFile, generated, relativePath, options, previou
|
|
|
363
426
|
return writeManagedFile(destFile, Buffer.from(JSON.stringify(merged, null, 4)), relativePath, {
|
|
364
427
|
...options,
|
|
365
428
|
allowExistingMerge: !options.oldLock,
|
|
366
|
-
});
|
|
429
|
+
}, { generated: true });
|
|
367
430
|
}
|
|
368
431
|
|
|
369
432
|
async function mergeGitignore(destFile, options) {
|
|
@@ -378,7 +441,7 @@ async function mergeGitignore(destFile, options) {
|
|
|
378
441
|
return writeManagedFile(destFile, Buffer.from(`${[...lines].join('\n')}\n`), '.gitignore', {
|
|
379
442
|
...options,
|
|
380
443
|
allowExistingMerge: !options.oldLock,
|
|
381
|
-
});
|
|
444
|
+
}, { generated: true });
|
|
382
445
|
}
|
|
383
446
|
|
|
384
447
|
export function validateSelections(selections) {
|
|
@@ -394,7 +457,7 @@ export function validateSelections(selections) {
|
|
|
394
457
|
return selections;
|
|
395
458
|
}
|
|
396
459
|
|
|
397
|
-
export async function install({ targetDir, agentType, selections, includeAgentsMd = true, writeAgentsMd, oldSelections, oldLock, force = false, dryRun = false }) {
|
|
460
|
+
export async function install({ targetDir, agentType, selections, includeAgentsMd = true, writeAgentsMd, oldSelections, oldLock, force = false, dryRun = false, tuiPreferences }) {
|
|
398
461
|
validateSelections(selections);
|
|
399
462
|
if (oldLock) validateLock(oldLock);
|
|
400
463
|
writeAgentsMd = writeAgentsMd ?? includeAgentsMd;
|
|
@@ -405,8 +468,27 @@ export async function install({ targetDir, agentType, selections, includeAgentsM
|
|
|
405
468
|
oldLock,
|
|
406
469
|
force,
|
|
407
470
|
dryRun,
|
|
408
|
-
managedFiles: {
|
|
471
|
+
managedFiles: {},
|
|
472
|
+
fileOwners: {},
|
|
409
473
|
};
|
|
474
|
+
if (oldLock) {
|
|
475
|
+
for (const category of LOCK_CATEGORIES) {
|
|
476
|
+
for (const [id, entry] of Object.entries(oldLock[category] || {})) {
|
|
477
|
+
for (const [path, checksum] of Object.entries(entry.files || {})) {
|
|
478
|
+
if (!(path in options.managedFiles)) {
|
|
479
|
+
options.managedFiles[path] = checksum;
|
|
480
|
+
options.fileOwners[path] = { category, id };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
for (const [path, entry] of Object.entries(oldLock.generated || {})) {
|
|
486
|
+
if (!(path in options.managedFiles)) {
|
|
487
|
+
options.managedFiles[path] = entry.computedHash;
|
|
488
|
+
options.fileOwners[path] = { generated: true };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
410
492
|
|
|
411
493
|
// Delete items that are no longer selected.
|
|
412
494
|
if (oldSelections) {
|
|
@@ -419,16 +501,16 @@ export async function install({ targetDir, agentType, selections, includeAgentsM
|
|
|
419
501
|
}
|
|
420
502
|
|
|
421
503
|
const tasks = [];
|
|
422
|
-
for (const category of
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
504
|
+
for (const category of LOCK_CATEGORIES) {
|
|
505
|
+
if (!isCopyable(category) || !selections[category]?.length) continue;
|
|
506
|
+
tasks.push(isFileBased(category)
|
|
507
|
+
? copySelectedFiles(absTarget, category, selections[category], options)
|
|
508
|
+
: copySelectedDirs(absTarget, category, selections[category], options));
|
|
427
509
|
}
|
|
428
510
|
await Promise.all(tasks);
|
|
429
511
|
|
|
430
512
|
if (writeAgentsMd) {
|
|
431
|
-
await writeManagedFile(resolve(absTarget, 'AGENTS.md'), Buffer.from(AGENTS_MD), 'AGENTS.md', options);
|
|
513
|
+
await writeManagedFile(resolve(absTarget, 'AGENTS.md'), Buffer.from(AGENTS_MD), 'AGENTS.md', options, { generated: true });
|
|
432
514
|
}
|
|
433
515
|
|
|
434
516
|
if (agentType === 'opencode') {
|
|
@@ -437,18 +519,19 @@ export async function install({ targetDir, agentType, selections, includeAgentsM
|
|
|
437
519
|
let previousOpenCodeConfig;
|
|
438
520
|
let previousTuiConfig;
|
|
439
521
|
if (oldLock) {
|
|
522
|
+
const oldSelections = lockToSelections(oldLock);
|
|
440
523
|
let previousMcpEntries = {};
|
|
441
|
-
if (
|
|
524
|
+
if (oldSelections.mcps?.length) previousMcpEntries = await loadMcpConfigs(oldSelections.mcps);
|
|
442
525
|
previousOpenCodeConfig = JSON.parse(generateOpenCodeConfig({
|
|
443
|
-
selections:
|
|
526
|
+
selections: oldSelections,
|
|
444
527
|
mcpEntries: previousMcpEntries,
|
|
445
528
|
includeAgentsMd: oldLock.includeAgentsMd ?? true,
|
|
446
529
|
}));
|
|
447
|
-
previousTuiConfig = JSON.parse(generateTuiConfig({ selections:
|
|
530
|
+
previousTuiConfig = JSON.parse(generateTuiConfig({ selections: oldSelections, preferences: tuiPreferences }));
|
|
448
531
|
}
|
|
449
532
|
const configJson = generateOpenCodeConfig({ selections, mcpEntries, includeAgentsMd });
|
|
450
533
|
await mergeJsonFile(resolve(absTarget, 'opencode.json'), JSON.parse(configJson), 'opencode.json', options, previousOpenCodeConfig);
|
|
451
|
-
await mergeJsonFile(resolve(absTarget, 'tui.json'), JSON.parse(generateTuiConfig({ selections })), 'tui.json', options, previousTuiConfig);
|
|
534
|
+
await mergeJsonFile(resolve(absTarget, 'tui.json'), JSON.parse(generateTuiConfig({ selections, preferences: tuiPreferences })), 'tui.json', options, previousTuiConfig);
|
|
452
535
|
await mergeGitignore(resolve(absTarget, '.gitignore'), options);
|
|
453
536
|
}
|
|
454
537
|
|
|
@@ -458,16 +541,38 @@ export async function install({ targetDir, agentType, selections, includeAgentsM
|
|
|
458
541
|
}
|
|
459
542
|
|
|
460
543
|
if (dryRun) return absTarget;
|
|
461
|
-
const
|
|
544
|
+
const pkgVersion = await getPackageVersion();
|
|
545
|
+
const source = `system-prompt@${pkgVersion}`;
|
|
462
546
|
const lockData = {
|
|
463
|
-
version,
|
|
547
|
+
version: LOCK_VERSION,
|
|
464
548
|
agentType,
|
|
465
|
-
targetDir,
|
|
466
549
|
installedAt: new Date().toISOString(),
|
|
467
|
-
selections: Object.fromEntries(Object.entries(selections).map(([k, v]) => [k, [...v]])),
|
|
468
550
|
includeAgentsMd,
|
|
469
|
-
managedFiles: options.managedFiles,
|
|
470
551
|
};
|
|
552
|
+
for (const category of LOCK_CATEGORIES) {
|
|
553
|
+
const entries = {};
|
|
554
|
+
for (const id of selections[category] || []) {
|
|
555
|
+
if (await isRemoved(category, id)) continue;
|
|
556
|
+
const files = {};
|
|
557
|
+
for (const [path, checksum] of Object.entries(options.managedFiles)) {
|
|
558
|
+
const owner = options.fileOwners[path];
|
|
559
|
+
if (owner?.category === category && owner?.id === id) files[path] = checksum;
|
|
560
|
+
}
|
|
561
|
+
entries[id] = {
|
|
562
|
+
source,
|
|
563
|
+
sourceType: 'bundled',
|
|
564
|
+
itemPath: itemSourcePath(category, id),
|
|
565
|
+
computedHash: buildComputedHash(files, id),
|
|
566
|
+
files,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
lockData[category] = entries;
|
|
570
|
+
}
|
|
571
|
+
const generated = {};
|
|
572
|
+
for (const [path, checksum] of Object.entries(options.managedFiles)) {
|
|
573
|
+
if (options.fileOwners[path]?.generated) generated[path] = { computedHash: checksum };
|
|
574
|
+
}
|
|
575
|
+
lockData.generated = generated;
|
|
471
576
|
await writeFile(resolve(absTarget, 'system-prompt-lock.json'), JSON.stringify(lockData, null, 2));
|
|
472
577
|
return absTarget;
|
|
473
578
|
}
|
|
@@ -476,10 +581,6 @@ function resolveSource(subpath) {
|
|
|
476
581
|
return resolve(packageRoot, subpath);
|
|
477
582
|
}
|
|
478
583
|
|
|
479
|
-
function targetSubdir(sourceDir) {
|
|
480
|
-
return sourceDir.replace(/^framework\//, '');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
584
|
async function isRemoved(category, id) {
|
|
484
585
|
const catConfig = categories[category];
|
|
485
586
|
if (!catConfig) return false;
|
|
@@ -548,4 +649,5 @@ async function writeMergedEnv(absTarget, examples, options) {
|
|
|
548
649
|
const content = Buffer.from(`${existingContent}${separator}${additions.join('\n')}\n`);
|
|
549
650
|
if (!options.dryRun) await writeFile(envPath, content);
|
|
550
651
|
options.managedFiles['.env'] = hash(content);
|
|
652
|
+
options.fileOwners['.env'] = { generated: true };
|
|
551
653
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { categories } from './catalog.js';
|
|
2
|
+
|
|
3
|
+
export const LOCK_CATEGORIES = Object.keys(categories);
|
|
4
|
+
|
|
5
|
+
export function isFileBased(category) {
|
|
6
|
+
return categories[category]?.itemLayout === 'file';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isCopyable(category) {
|
|
10
|
+
const config = categories[category];
|
|
11
|
+
return Boolean(config?.itemLayout) && config.copyItems !== false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function targetSubdir(sourceDir) {
|
|
15
|
+
return sourceDir.replace(/^framework\//, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function itemSourcePath(category, id) {
|
|
19
|
+
const config = categories[category];
|
|
20
|
+
return isFileBased(category) ? `${config.sourceDir}/${id}.md` : `${config.sourceDir}/${id}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function itemRelativePath(category, id) {
|
|
24
|
+
const dir = targetSubdir(categories[category].sourceDir);
|
|
25
|
+
return isFileBased(category) ? `${dir}/${id}.md` : `${dir}/${id}`;
|
|
26
|
+
}
|