@mohammadhprp/system-prompt 0.11.2 → 0.12.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/agents/researcher.md +1 -1
- package/framework/mcps/github-mcp/README.md +1 -1
- package/framework/mcps/github-mcp/configs/.env.example +1 -1
- package/framework/plugins/ponytail/README.md +1 -1
- package/framework/plugins/ponytail/capabilities.md +1 -1
- package/framework/references/standards/pull-requests.md +1 -1
- package/framework/skills/README.md +3 -3
- package/framework/skills/diagram-design/SKILL.md +1 -1
- package/framework/skills/effective-html/SKILL.md +63 -0
- package/framework/skills/effective-html/examples.md +19 -0
- package/framework/skills/effective-html/references/charts-and-data.md +32 -0
- package/framework/skills/effective-html/references/creative-direction.md +48 -0
- package/framework/skills/effective-html/references/design-artifact.md +78 -0
- package/framework/skills/effective-html/references/diagrams.md +68 -0
- package/framework/skills/effective-html/references/documents-and-presentations.md +28 -0
- package/framework/skills/effective-html/references/html-diagram.md +43 -0
- package/framework/skills/effective-html/references/html-plan.md +40 -0
- package/framework/skills/effective-html/references/html-prototype.md +97 -0
- package/framework/skills/effective-html/references/html-wireframe.md +81 -0
- package/framework/skills/effective-html/references/html.md +72 -0
- package/framework/skills/effective-html/references/interfaces.md +17 -0
- package/framework/skills/great-interface/SKILL.md +43 -0
- package/framework/skills/great-interface/references/animations.md +205 -0
- package/framework/skills/great-interface/references/better-accessibility.md +106 -0
- package/framework/skills/great-interface/references/better-colors.md +100 -0
- package/framework/skills/great-interface/references/better-interface.md +132 -0
- package/framework/skills/great-interface/references/better-layout.md +76 -0
- package/framework/skills/great-interface/references/better-typography.md +157 -0
- package/framework/skills/great-interface/references/better-ui.md +107 -0
- package/framework/skills/great-interface/references/better-writing.md +109 -0
- package/framework/skills/great-interface/references/choosing-fonts.md +64 -0
- package/framework/skills/great-interface/references/color-formats.md +90 -0
- package/framework/skills/great-interface/references/color-usage.md +118 -0
- package/framework/skills/great-interface/references/contrast.md +79 -0
- package/framework/skills/great-interface/references/css-cheat-sheet.md +65 -0
- package/framework/skills/great-interface/references/details-and-accessibility.md +119 -0
- package/framework/skills/great-interface/references/enter-exit.md +147 -0
- package/framework/skills/great-interface/references/explain-interface.md +126 -0
- package/framework/skills/great-interface/references/find-the-effect.md +94 -0
- package/framework/skills/great-interface/references/focus-and-keyboard.md +131 -0
- package/framework/skills/great-interface/references/forms.md +84 -0
- package/framework/skills/great-interface/references/from-an-image.md +55 -0
- package/framework/skills/great-interface/references/grouping-and-alignment.md +123 -0
- package/framework/skills/great-interface/references/hit-areas.md +94 -0
- package/framework/skills/great-interface/references/icon-transitions.md +102 -0
- package/framework/skills/great-interface/references/icons.md +110 -0
- package/framework/skills/great-interface/references/interface-review.md +148 -0
- package/framework/skills/great-interface/references/motion-and-zoom.md +79 -0
- package/framework/skills/great-interface/references/no-browser.md +73 -0
- package/framework/skills/great-interface/references/palette-generation.md +104 -0
- package/framework/skills/great-interface/references/palette-structure.md +76 -0
- package/framework/skills/great-interface/references/performance.md +88 -0
- package/framework/skills/great-interface/references/picker.md +76 -0
- package/framework/skills/great-interface/references/read-the-system.md +178 -0
- package/framework/skills/great-interface/references/removed-signals.md +38 -0
- package/framework/skills/great-interface/references/review-format.md +46 -0
- package/framework/skills/great-interface/references/scope-resolution.md +88 -0
- package/framework/skills/great-interface/references/screen-readers.md +101 -0
- package/framework/skills/great-interface/references/semantics-and-aria.md +84 -0
- package/framework/skills/great-interface/references/spacing-and-adaptivity.md +159 -0
- package/framework/skills/great-interface/references/spacing-and-sizing.md +121 -0
- package/framework/skills/great-interface/references/surfaces.md +219 -0
- package/framework/skills/great-interface/references/token-naming.md +97 -0
- package/framework/skills/great-interface/references/variable-fonts-and-opentype.md +105 -0
- package/framework/skills/great-interface/references/variant.md +104 -0
- package/framework/skills/great-interface/references/wrapping-and-punctuation.md +55 -0
- package/framework/skills/taste/SKILL.md +47 -0
- package/framework/skills/taste/examples.md +73 -0
- package/framework/skills/taste/references/brandkit.md +793 -0
- package/framework/skills/taste/references/design-taste-frontend-v1.md +221 -0
- package/framework/skills/taste/references/design-taste-frontend.md +1201 -0
- package/framework/skills/taste/references/full-output-enforcement.md +44 -0
- package/framework/skills/taste/references/gpt-taste.md +69 -0
- package/framework/skills/taste/references/high-end-visual-design.md +93 -0
- package/framework/skills/taste/references/image-to-code.md +1223 -0
- package/framework/skills/taste/references/imagegen-frontend-mobile.md +1460 -0
- package/framework/skills/taste/references/imagegen-frontend-web.md +982 -0
- package/framework/skills/taste/references/industrial-brutalist-ui.md +87 -0
- package/framework/skills/taste/references/minimalist-ui.md +80 -0
- package/framework/skills/taste/references/redesign-existing-projects.md +173 -0
- package/framework/skills/taste/references/stitch-design-taste-design.md +121 -0
- package/framework/skills/taste/references/stitch-design-taste.md +179 -0
- package/package.json +1 -1
- package/src/agent-configs.js +6 -2
- package/src/catalog.js +3 -2
- package/src/cli.js +82 -2
- package/src/doctor.js +84 -0
- package/src/installer.js +337 -154
- package/framework/skills/brand-guidelines/LICENSE.txt +0 -202
- package/framework/skills/brand-guidelines/SKILL.md +0 -73
- package/framework/skills/lavish/SKILL.md +0 -67
- package/framework/skills/lavish/examples.md +0 -31
package/src/installer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import { mkdir,
|
|
2
|
+
import { dirname, relative, resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { mkdir, writeFile, readdir, stat, lstat, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
|
|
5
6
|
import { categories } from './catalog.js';
|
|
6
7
|
import { loadMcpConfigs, generateOpenCodeConfig, generateTuiConfig } from './agent-configs.js';
|
|
@@ -97,46 +98,134 @@ export async function getPackageVersion() {
|
|
|
97
98
|
export async function loadLockFile(absTarget) {
|
|
98
99
|
try {
|
|
99
100
|
const content = await readFile(resolve(absTarget, 'system-prompt-lock.json'), 'utf-8');
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
101
|
+
const lock = JSON.parse(content);
|
|
102
|
+
validateLock(lock);
|
|
103
|
+
return lock;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code === 'ENOENT') return null;
|
|
106
|
+
if (error instanceof SyntaxError || error.message?.startsWith('Invalid system-prompt lock')) {
|
|
107
|
+
throw new Error(`${error.message}. Remove or repair system-prompt-lock.json before reinstalling.`);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
103
110
|
}
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
function
|
|
107
|
-
|
|
113
|
+
function validateLock(lock) {
|
|
114
|
+
if (!lock || typeof lock !== 'object' || Array.isArray(lock)) {
|
|
115
|
+
throw new Error('Invalid system-prompt lock: expected an object');
|
|
116
|
+
}
|
|
117
|
+
if (!lock.selections || typeof lock.selections !== 'object' || Array.isArray(lock.selections)) {
|
|
118
|
+
throw new Error('Invalid system-prompt lock: selections must be an object');
|
|
119
|
+
}
|
|
120
|
+
for (const [category, ids] of Object.entries(lock.selections)) {
|
|
121
|
+
const config = categories[category];
|
|
122
|
+
if (!config || !Array.isArray(ids) || ids.some(id => typeof id !== 'string')) {
|
|
123
|
+
throw new Error(`Invalid system-prompt lock: invalid ${category} selection`);
|
|
124
|
+
}
|
|
125
|
+
const knownIds = new Set(config.items.map(item => item.id));
|
|
126
|
+
if (ids.some(id => !knownIds.has(id))) {
|
|
127
|
+
throw new Error(`Invalid system-prompt lock: unknown ${category} item`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (lock.managedFiles !== undefined) {
|
|
131
|
+
if (!lock.managedFiles || typeof lock.managedFiles !== 'object' || Array.isArray(lock.managedFiles)) {
|
|
132
|
+
throw new Error('Invalid system-prompt lock: managedFiles must be an object');
|
|
133
|
+
}
|
|
134
|
+
for (const [path, checksum] of Object.entries(lock.managedFiles)) {
|
|
135
|
+
if (path.startsWith('/') || path.split('/').includes('..') || !/^[a-f0-9]{64}$/.test(checksum)) {
|
|
136
|
+
throw new Error('Invalid system-prompt lock: unsafe managed file entry');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
108
140
|
}
|
|
109
141
|
|
|
110
|
-
function
|
|
111
|
-
return
|
|
142
|
+
function hash(content) {
|
|
143
|
+
return createHash('sha256').update(content).digest('hex');
|
|
112
144
|
}
|
|
113
145
|
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
function isInside(parent, child) {
|
|
147
|
+
const relativePath = relative(parent, child);
|
|
148
|
+
return relativePath === '' || (!relativePath.startsWith('../') && relativePath !== '..' && !isAbsolute(relativePath));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function canWrite(destFile, relativePath, oldLock, force) {
|
|
152
|
+
if (force) return true;
|
|
153
|
+
try {
|
|
154
|
+
const existing = await readFile(destFile);
|
|
155
|
+
const previousHash = oldLock?.managedFiles?.[relativePath];
|
|
156
|
+
return Boolean(previousHash && previousHash === hash(existing));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code === 'ENOENT') return true;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function writeManagedFile(destFile, content, relativePath, options) {
|
|
164
|
+
await assertSafeDestination(options.targetDir, destFile);
|
|
165
|
+
if (!options.allowExistingMerge && !(await canWrite(destFile, relativePath, options.oldLock, options.force))) {
|
|
166
|
+
console.warn(` ⚠ Preserving existing file: ${relativePath}`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (!options.dryRun) await mkdir(dirname(destFile), { recursive: true });
|
|
170
|
+
if (!options.dryRun) await writeFile(destFile, content);
|
|
171
|
+
options.managedFiles[relativePath] = hash(content);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function copyDir(src, dest, relativeDir, options) {
|
|
176
|
+
if (!options.dryRun) await mkdir(dest, { recursive: true });
|
|
116
177
|
const entries = await readdir(src, { withFileTypes: true });
|
|
117
178
|
|
|
118
179
|
for (const entry of entries) {
|
|
119
180
|
if (entry.name === '.DS_Store') continue;
|
|
120
181
|
const srcPath = resolve(src, entry.name);
|
|
121
182
|
const destPath = resolve(dest, entry.name);
|
|
183
|
+
const relativePath = `${relativeDir}/${entry.name}`;
|
|
122
184
|
|
|
123
185
|
if (entry.isDirectory()) {
|
|
124
|
-
await copyDir(srcPath, destPath);
|
|
186
|
+
await copyDir(srcPath, destPath, relativePath, options);
|
|
125
187
|
} else if (entry.isFile()) {
|
|
126
|
-
|
|
127
|
-
await
|
|
188
|
+
const content = await readFile(srcPath);
|
|
189
|
+
await writeManagedFile(destPath, content, relativePath, options);
|
|
128
190
|
}
|
|
129
191
|
}
|
|
130
192
|
}
|
|
131
193
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
194
|
+
function sourceMissing(error) {
|
|
195
|
+
return error?.code === 'ENOENT';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function assertSafePath(targetDir, path) {
|
|
199
|
+
if (!isInside(targetDir, path)) {
|
|
200
|
+
throw new Error(`Refusing to access path outside installation directory: ${path}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function assertSafeDestination(targetDir, path) {
|
|
205
|
+
assertSafePath(targetDir, path);
|
|
206
|
+
try {
|
|
207
|
+
if ((await lstat(targetDir)).isSymbolicLink()) {
|
|
208
|
+
throw new Error(`Refusing to install through symlink target: ${targetDir}`);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error.code !== 'ENOENT') throw error;
|
|
212
|
+
}
|
|
213
|
+
const relativePath = relative(targetDir, path);
|
|
214
|
+
let current = targetDir;
|
|
215
|
+
for (const part of relativePath.split('/').filter(Boolean)) {
|
|
216
|
+
current = resolve(current, part);
|
|
217
|
+
try {
|
|
218
|
+
if ((await lstat(current)).isSymbolicLink()) {
|
|
219
|
+
throw new Error(`Refusing to access symlink inside installation directory: ${current}`);
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error.code === 'ENOENT') break;
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
137
226
|
}
|
|
138
227
|
|
|
139
|
-
async function copySelectedDirs(targetDir, category, selectedIds) {
|
|
228
|
+
async function copySelectedDirs(targetDir, category, selectedIds, options) {
|
|
140
229
|
const catConfig = categories[category];
|
|
141
230
|
if (!catConfig || !selectedIds?.length) return;
|
|
142
231
|
|
|
@@ -148,81 +237,49 @@ async function copySelectedDirs(targetDir, category, selectedIds) {
|
|
|
148
237
|
|
|
149
238
|
const srcPath = resolveSource(`${catConfig.sourceDir}/${id}`);
|
|
150
239
|
const destPath = resolve(destParent, id);
|
|
240
|
+
await assertSafeDestination(targetDir, destPath);
|
|
151
241
|
|
|
152
242
|
try {
|
|
153
243
|
await stat(srcPath);
|
|
154
|
-
await copyDir(srcPath, destPath);
|
|
155
|
-
} catch {
|
|
156
|
-
|
|
244
|
+
await copyDir(srcPath, destPath, `${relativeDir}/${id}`, options);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (sourceMissing(error)) {
|
|
247
|
+
console.warn(` ⚠ Source not found: ${catConfig.sourceDir}/${id}`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
157
251
|
}
|
|
158
252
|
}
|
|
159
253
|
}
|
|
160
254
|
|
|
161
|
-
async function copySelectedFiles(targetDir, category, selectedIds,
|
|
255
|
+
async function copySelectedFiles(targetDir, category, selectedIds, options) {
|
|
162
256
|
const catConfig = categories[category];
|
|
163
257
|
if (!catConfig || !selectedIds?.length) return;
|
|
164
258
|
|
|
165
259
|
const relativeDir = targetSubdir(catConfig.sourceDir);
|
|
166
260
|
const destParent = resolve(targetDir, relativeDir);
|
|
167
|
-
await mkdir(destParent, { recursive: true });
|
|
261
|
+
if (!options.dryRun) await mkdir(destParent, { recursive: true });
|
|
168
262
|
|
|
169
263
|
for (const id of selectedIds) {
|
|
170
264
|
if (await isRemoved(category, id)) continue;
|
|
171
265
|
|
|
172
266
|
const srcFile = resolveSource(`${catConfig.sourceDir}/${id}.md`);
|
|
173
267
|
const destFile = resolve(destParent, `${id}.md`);
|
|
268
|
+
await assertSafeDestination(targetDir, destFile);
|
|
174
269
|
try {
|
|
175
|
-
await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// dest doesn't exist — proceed to copy
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
await copyFile(srcFile, destFile);
|
|
185
|
-
} catch {
|
|
186
|
-
console.warn(` ⚠ Source not found: ${catConfig.sourceDir}/${id}.md`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function parseEnv(content) {
|
|
192
|
-
const vars = new Map();
|
|
193
|
-
for (const line of content.split('\n')) {
|
|
194
|
-
const trimmed = line.trim();
|
|
195
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
196
|
-
const eqIndex = trimmed.indexOf('=');
|
|
197
|
-
if (eqIndex === -1) {
|
|
198
|
-
vars.set(trimmed, '');
|
|
199
|
-
} else {
|
|
200
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
201
|
-
const value = trimmed.slice(eqIndex + 1).trim();
|
|
202
|
-
if (key) vars.set(key, value);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return vars;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function collectMcpEnvExamples(mcpIds) {
|
|
209
|
-
const combined = new Map();
|
|
210
|
-
for (const id of mcpIds) {
|
|
211
|
-
try {
|
|
212
|
-
const examplePath = resolveSource(`framework/mcps/${id}/configs/.env.example`);
|
|
213
|
-
const content = await readFile(examplePath, 'utf-8');
|
|
214
|
-
const parsed = parseEnv(content);
|
|
215
|
-
for (const [key, value] of parsed) {
|
|
216
|
-
if (!combined.has(key)) combined.set(key, value);
|
|
270
|
+
const content = await readFile(srcFile);
|
|
271
|
+
await writeManagedFile(destFile, content, `${relativeDir}/${id}.md`, options);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (sourceMissing(error)) {
|
|
274
|
+
console.warn(` ⚠ Source not found: ${catConfig.sourceDir}/${id}.md`);
|
|
275
|
+
continue;
|
|
217
276
|
}
|
|
218
|
-
|
|
219
|
-
// Silently skip MCPs without .env.example
|
|
277
|
+
throw error;
|
|
220
278
|
}
|
|
221
279
|
}
|
|
222
|
-
return combined;
|
|
223
280
|
}
|
|
224
281
|
|
|
225
|
-
async function deleteSelectedItems(absTarget, category, ids) {
|
|
282
|
+
async function deleteSelectedItems(absTarget, category, ids, oldLock, force, dryRun) {
|
|
226
283
|
const catConfig = categories[category];
|
|
227
284
|
if (!catConfig || !ids?.length) return;
|
|
228
285
|
|
|
@@ -230,139 +287,265 @@ async function deleteSelectedItems(absTarget, category, ids) {
|
|
|
230
287
|
if (!FILE_BASED.has(category) && category !== 'skills' && category !== 'styles') return;
|
|
231
288
|
|
|
232
289
|
const relativeDir = targetSubdir(catConfig.sourceDir);
|
|
233
|
-
const destParent = resolve(absTarget, relativeDir);
|
|
234
|
-
|
|
235
290
|
for (const id of ids) {
|
|
236
|
-
const
|
|
237
|
-
?
|
|
238
|
-
:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
291
|
+
const relativePath = FILE_BASED.has(category)
|
|
292
|
+
? `${relativeDir}/${id}.md`
|
|
293
|
+
: `${relativeDir}/${id}`;
|
|
294
|
+
const destPath = resolve(absTarget, relativePath);
|
|
295
|
+
await assertSafeDestination(absTarget, destPath);
|
|
296
|
+
if (!force && oldLock) {
|
|
297
|
+
const managedEntries = Object.entries(oldLock.managedFiles || {})
|
|
298
|
+
.filter(([path]) => path === relativePath || path.startsWith(`${relativePath}/`));
|
|
299
|
+
if (managedEntries.length === 0) {
|
|
300
|
+
console.warn(` ⚠ Preserving unmanaged item: ${relativePath}`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
let modified = false;
|
|
304
|
+
for (const [path, checksum] of managedEntries) {
|
|
305
|
+
await assertSafeDestination(absTarget, resolve(absTarget, path));
|
|
306
|
+
try {
|
|
307
|
+
if (hash(await readFile(resolve(absTarget, path))) !== checksum) modified = true;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (!sourceMissing(error)) throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (modified) {
|
|
313
|
+
console.warn(` ⚠ Preserving modified item: ${relativePath}`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (!FILE_BASED.has(category)) {
|
|
317
|
+
if (!dryRun) {
|
|
318
|
+
for (const [path] of managedEntries) await rm(resolve(absTarget, path), { force: true });
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
243
322
|
}
|
|
323
|
+
if (!dryRun) await rm(destPath, { recursive: true, force: true });
|
|
244
324
|
}
|
|
245
325
|
}
|
|
246
326
|
|
|
247
|
-
async function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const envPath = resolve(absTarget, '.env');
|
|
251
|
-
const existing = new Map();
|
|
252
|
-
|
|
327
|
+
async function mergeJsonFile(destFile, generated, relativePath, options, previousGenerated = generated) {
|
|
328
|
+
let existing = {};
|
|
253
329
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
330
|
+
existing = JSON.parse(await readFile(destFile, 'utf-8'));
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error.code !== 'ENOENT') {
|
|
333
|
+
if (error instanceof SyntaxError) throw new Error(`Cannot merge invalid JSON file: ${relativePath}`);
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
259
336
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
337
|
+
const merged = { ...existing, ...generated };
|
|
338
|
+
if (Array.isArray(existing.instructions) && Array.isArray(generated.instructions)) {
|
|
339
|
+
const previous = new Set(previousGenerated.instructions || []);
|
|
340
|
+
merged.instructions = [...new Set([
|
|
341
|
+
...existing.instructions.filter(item => !previous.has(item)),
|
|
342
|
+
...generated.instructions,
|
|
343
|
+
])];
|
|
264
344
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
345
|
+
if (Array.isArray(existing.plugin)) {
|
|
346
|
+
const previous = new Set(previousGenerated.plugin || []);
|
|
347
|
+
const plugins = [...new Set([
|
|
348
|
+
...existing.plugin.filter(item => !previous.has(item)),
|
|
349
|
+
...(generated.plugin || []),
|
|
350
|
+
])];
|
|
351
|
+
if (plugins.length) merged.plugin = plugins;
|
|
352
|
+
else delete merged.plugin;
|
|
353
|
+
}
|
|
354
|
+
for (const key of ['mcp', 'references']) {
|
|
355
|
+
if (existing[key] && typeof existing[key] === 'object') {
|
|
356
|
+
const previous = previousGenerated[key] || {};
|
|
357
|
+
const preserved = Object.fromEntries(Object.entries(existing[key]).filter(([name]) => !(name in previous)));
|
|
358
|
+
const values = { ...preserved, ...(generated[key] || {}) };
|
|
359
|
+
if (Object.keys(values).length) merged[key] = values;
|
|
360
|
+
else delete merged[key];
|
|
361
|
+
}
|
|
271
362
|
}
|
|
272
|
-
|
|
363
|
+
return writeManagedFile(destFile, Buffer.from(JSON.stringify(merged, null, 4)), relativePath, {
|
|
364
|
+
...options,
|
|
365
|
+
allowExistingMerge: !options.oldLock,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
273
368
|
|
|
274
|
-
|
|
369
|
+
async function mergeGitignore(destFile, options) {
|
|
370
|
+
let existing = '';
|
|
371
|
+
try {
|
|
372
|
+
existing = await readFile(destFile, 'utf-8');
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error.code !== 'ENOENT') throw error;
|
|
375
|
+
}
|
|
376
|
+
const lines = new Set(existing.split('\n').filter(Boolean));
|
|
377
|
+
for (const line of OPENCODE_GITIGNORE.split('\n').filter(Boolean)) lines.add(line);
|
|
378
|
+
return writeManagedFile(destFile, Buffer.from(`${[...lines].join('\n')}\n`), '.gitignore', {
|
|
379
|
+
...options,
|
|
380
|
+
allowExistingMerge: !options.oldLock,
|
|
381
|
+
});
|
|
275
382
|
}
|
|
276
383
|
|
|
277
|
-
function
|
|
278
|
-
|
|
384
|
+
export function validateSelections(selections) {
|
|
385
|
+
if (!selections || typeof selections !== 'object' || Array.isArray(selections)) {
|
|
386
|
+
throw new Error('Selections must be an object');
|
|
387
|
+
}
|
|
388
|
+
for (const [category, ids] of Object.entries(selections)) {
|
|
389
|
+
const config = categories[category];
|
|
390
|
+
if (!config || !Array.isArray(ids)) throw new Error(`Unknown or invalid category: ${category}`);
|
|
391
|
+
const knownIds = new Set(config.items.map(item => item.id));
|
|
392
|
+
if (ids.some(id => !knownIds.has(id))) throw new Error(`Unknown ${category} item selected`);
|
|
393
|
+
}
|
|
394
|
+
return selections;
|
|
279
395
|
}
|
|
280
396
|
|
|
281
|
-
export async function install({ targetDir, agentType, selections, includeAgentsMd = true, writeAgentsMd, oldSelections }) {
|
|
397
|
+
export async function install({ targetDir, agentType, selections, includeAgentsMd = true, writeAgentsMd, oldSelections, oldLock, force = false, dryRun = false }) {
|
|
398
|
+
validateSelections(selections);
|
|
399
|
+
if (oldLock) validateLock(oldLock);
|
|
282
400
|
writeAgentsMd = writeAgentsMd ?? includeAgentsMd;
|
|
283
401
|
const absTarget = resolve(process.cwd(), targetDir);
|
|
284
|
-
await mkdir(absTarget, { recursive: true });
|
|
402
|
+
if (!dryRun) await mkdir(absTarget, { recursive: true });
|
|
403
|
+
const options = {
|
|
404
|
+
targetDir: absTarget,
|
|
405
|
+
oldLock,
|
|
406
|
+
force,
|
|
407
|
+
dryRun,
|
|
408
|
+
managedFiles: { ...(oldLock?.managedFiles || {}) },
|
|
409
|
+
};
|
|
285
410
|
|
|
286
|
-
// Delete items that are no longer selected
|
|
411
|
+
// Delete items that are no longer selected.
|
|
287
412
|
if (oldSelections) {
|
|
288
413
|
for (const cat of Object.keys(oldSelections)) {
|
|
289
414
|
const oldIds = new Set(oldSelections[cat] || []);
|
|
290
415
|
const newIds = new Set(selections[cat] || []);
|
|
291
416
|
const removedIds = [...oldIds].filter(id => !newIds.has(id));
|
|
292
|
-
|
|
293
|
-
await deleteSelectedItems(absTarget, cat, removedIds);
|
|
294
|
-
}
|
|
417
|
+
await deleteSelectedItems(absTarget, cat, removedIds, oldLock, force, dryRun);
|
|
295
418
|
}
|
|
296
419
|
}
|
|
297
420
|
|
|
298
421
|
const tasks = [];
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
tasks.push(copySelectedDirs(absTarget, 'skills', selections.skills));
|
|
302
|
-
}
|
|
303
|
-
if (selections.agents?.length) {
|
|
304
|
-
tasks.push(copySelectedFiles(absTarget, 'agents', selections.agents));
|
|
305
|
-
}
|
|
306
|
-
if (selections.commands?.length) {
|
|
307
|
-
tasks.push(copySelectedFiles(absTarget, 'commands', selections.commands));
|
|
308
|
-
}
|
|
309
|
-
if (selections.styles?.length) {
|
|
310
|
-
tasks.push(copySelectedDirs(absTarget, 'styles', selections.styles));
|
|
311
|
-
}
|
|
312
|
-
if (selections.modes?.length) {
|
|
313
|
-
tasks.push(copySelectedFiles(absTarget, 'modes', selections.modes));
|
|
314
|
-
}
|
|
315
|
-
if (selections.memory?.length) {
|
|
316
|
-
tasks.push(copySelectedFiles(absTarget, 'memory', selections.memory, true));
|
|
422
|
+
for (const category of ['skills', 'styles']) {
|
|
423
|
+
if (selections[category]?.length) tasks.push(copySelectedDirs(absTarget, category, selections[category], options));
|
|
317
424
|
}
|
|
318
|
-
|
|
319
|
-
tasks.push(copySelectedFiles(absTarget,
|
|
425
|
+
for (const category of ['agents', 'commands', 'modes', 'memory', 'standards', 'templates']) {
|
|
426
|
+
if (selections[category]?.length) tasks.push(copySelectedFiles(absTarget, category, selections[category], options));
|
|
320
427
|
}
|
|
321
|
-
if (selections.templates?.length) {
|
|
322
|
-
tasks.push(copySelectedFiles(absTarget, 'templates', selections.templates));
|
|
323
|
-
}
|
|
324
|
-
|
|
325
428
|
await Promise.all(tasks);
|
|
326
429
|
|
|
327
430
|
if (writeAgentsMd) {
|
|
328
|
-
|
|
329
|
-
await writeFile(resolve(absTarget, 'AGENTS.md'), agentsContent);
|
|
431
|
+
await writeManagedFile(resolve(absTarget, 'AGENTS.md'), Buffer.from(AGENTS_MD), 'AGENTS.md', options);
|
|
330
432
|
}
|
|
331
433
|
|
|
332
434
|
if (agentType === 'opencode') {
|
|
333
435
|
let mcpEntries = {};
|
|
334
|
-
if (selections.mcps?.length)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
436
|
+
if (selections.mcps?.length) mcpEntries = await loadMcpConfigs(selections.mcps);
|
|
437
|
+
let previousOpenCodeConfig;
|
|
438
|
+
let previousTuiConfig;
|
|
439
|
+
if (oldLock) {
|
|
440
|
+
let previousMcpEntries = {};
|
|
441
|
+
if (oldLock.selections.mcps?.length) previousMcpEntries = await loadMcpConfigs(oldLock.selections.mcps);
|
|
442
|
+
previousOpenCodeConfig = JSON.parse(generateOpenCodeConfig({
|
|
443
|
+
selections: oldLock.selections,
|
|
444
|
+
mcpEntries: previousMcpEntries,
|
|
445
|
+
includeAgentsMd: oldLock.includeAgentsMd ?? true,
|
|
446
|
+
}));
|
|
447
|
+
previousTuiConfig = JSON.parse(generateTuiConfig({ selections: oldLock.selections }));
|
|
344
448
|
}
|
|
345
|
-
|
|
346
|
-
await
|
|
449
|
+
const configJson = generateOpenCodeConfig({ selections, mcpEntries, includeAgentsMd });
|
|
450
|
+
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);
|
|
452
|
+
await mergeGitignore(resolve(absTarget, '.gitignore'), options);
|
|
347
453
|
}
|
|
348
454
|
|
|
349
455
|
if (selections.mcps?.length) {
|
|
350
456
|
const envExamples = await collectMcpEnvExamples(selections.mcps);
|
|
351
|
-
await writeMergedEnv(absTarget, envExamples);
|
|
457
|
+
await writeMergedEnv(absTarget, envExamples, options);
|
|
352
458
|
}
|
|
353
459
|
|
|
460
|
+
if (dryRun) return absTarget;
|
|
354
461
|
const version = await getPackageVersion();
|
|
355
462
|
const lockData = {
|
|
356
463
|
version,
|
|
357
464
|
agentType,
|
|
358
465
|
targetDir,
|
|
359
466
|
installedAt: new Date().toISOString(),
|
|
360
|
-
selections: Object.fromEntries(
|
|
361
|
-
Object.entries(selections).map(([k, v]) => [k, [...v]])
|
|
362
|
-
),
|
|
467
|
+
selections: Object.fromEntries(Object.entries(selections).map(([k, v]) => [k, [...v]])),
|
|
363
468
|
includeAgentsMd,
|
|
469
|
+
managedFiles: options.managedFiles,
|
|
364
470
|
};
|
|
365
471
|
await writeFile(resolve(absTarget, 'system-prompt-lock.json'), JSON.stringify(lockData, null, 2));
|
|
366
|
-
|
|
367
472
|
return absTarget;
|
|
368
473
|
}
|
|
474
|
+
|
|
475
|
+
function resolveSource(subpath) {
|
|
476
|
+
return resolve(packageRoot, subpath);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function targetSubdir(sourceDir) {
|
|
480
|
+
return sourceDir.replace(/^framework\//, '');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function isRemoved(category, id) {
|
|
484
|
+
const catConfig = categories[category];
|
|
485
|
+
if (!catConfig) return false;
|
|
486
|
+
const item = catConfig.items.find(i => i.id === id);
|
|
487
|
+
return item?.removed === true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parseEnv(content) {
|
|
491
|
+
const vars = new Map();
|
|
492
|
+
for (const line of content.split('\n')) {
|
|
493
|
+
const trimmed = line.trim();
|
|
494
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
495
|
+
const eqIndex = trimmed.indexOf('=');
|
|
496
|
+
if (eqIndex === -1) {
|
|
497
|
+
vars.set(trimmed, '');
|
|
498
|
+
} else {
|
|
499
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
500
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
501
|
+
if (key) vars.set(key, value);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return vars;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function collectMcpEnvExamples(mcpIds) {
|
|
508
|
+
const combined = new Map();
|
|
509
|
+
for (const id of mcpIds) {
|
|
510
|
+
try {
|
|
511
|
+
const examplePath = resolveSource(`framework/mcps/${id}/configs/.env.example`);
|
|
512
|
+
const content = await readFile(examplePath, 'utf-8');
|
|
513
|
+
const parsed = parseEnv(content);
|
|
514
|
+
for (const [key, value] of parsed) {
|
|
515
|
+
if (!combined.has(key)) combined.set(key, value);
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (error.code !== 'ENOENT') throw error;
|
|
519
|
+
// MCPs may not provide an environment example.
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return combined;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function writeMergedEnv(absTarget, examples, options) {
|
|
526
|
+
if (examples.size === 0) return;
|
|
527
|
+
|
|
528
|
+
const envPath = resolve(absTarget, '.env');
|
|
529
|
+
await assertSafeDestination(options.targetDir, envPath);
|
|
530
|
+
const existing = new Map();
|
|
531
|
+
let existingContent = '';
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
existingContent = await readFile(envPath, 'utf-8');
|
|
535
|
+
const parsed = parseEnv(existingContent);
|
|
536
|
+
for (const [key, value] of parsed) existing.set(key, value);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (error.code !== 'ENOENT') throw error;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const additions = [];
|
|
542
|
+
for (const [key, value] of examples) {
|
|
543
|
+
if (!existing.has(key)) additions.push(`${key}=${value}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (additions.length === 0) return;
|
|
547
|
+
const separator = existingContent && !existingContent.endsWith('\n') ? '\n' : '';
|
|
548
|
+
const content = Buffer.from(`${existingContent}${separator}${additions.join('\n')}\n`);
|
|
549
|
+
if (!options.dryRun) await writeFile(envPath, content);
|
|
550
|
+
options.managedFiles['.env'] = hash(content);
|
|
551
|
+
}
|