@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/cli.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import { intro, outro, confirm, multiselect, spinner, isCancel } from '@clack/prompts';
|
|
1
|
+
import { intro, outro, confirm, multiselect, spinner, select, isCancel } from '@clack/prompts';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
4
|
|
|
4
5
|
import { categories } from './catalog.js';
|
|
5
|
-
import { getPackageVersion, install, loadLockFile } from './installer.js';
|
|
6
|
+
import { getPackageVersion, install, loadLockFile, lockToSelections } from './installer.js';
|
|
6
7
|
import { doctor } from './doctor.js';
|
|
8
|
+
import { normalizeTuiPreferences, tuiPreferencesFromConfig, TUI_THEMES } from './agent-configs.js';
|
|
7
9
|
|
|
8
10
|
const CATEGORY_FLAGS = new Set(Object.keys(categories));
|
|
9
11
|
|
|
12
|
+
const defaultUi = {
|
|
13
|
+
intro,
|
|
14
|
+
outro,
|
|
15
|
+
confirm,
|
|
16
|
+
multiselect,
|
|
17
|
+
spinner,
|
|
18
|
+
select,
|
|
19
|
+
isCancel,
|
|
20
|
+
log: (...args) => console.log(...args),
|
|
21
|
+
};
|
|
22
|
+
|
|
10
23
|
export function parseArgs(argv) {
|
|
11
24
|
const options = {
|
|
12
25
|
targetDir: '.opencode',
|
|
@@ -48,30 +61,29 @@ export function parseArgs(argv) {
|
|
|
48
61
|
return options;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
function allSelections() {
|
|
64
|
+
export function allSelections() {
|
|
52
65
|
return Object.fromEntries(Object.entries(categories).map(([category, config]) => [
|
|
53
66
|
category,
|
|
54
67
|
config.items.filter(item => !item.removed).map(item => item.id),
|
|
55
68
|
]));
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
function itemNames(config, ids) {
|
|
72
|
+
return ids.map(id => config?.items?.find(item => item.id === id)?.name || id);
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
function buildSummary(selections) {
|
|
59
76
|
const lines = [];
|
|
60
77
|
for (const [cat, ids] of Object.entries(selections)) {
|
|
61
78
|
if (!ids?.length) continue;
|
|
62
79
|
const catConfig = categories[cat];
|
|
63
|
-
|
|
64
|
-
const names = ids.map(id => {
|
|
65
|
-
const item = catConfig?.items.find(i => i.id === id);
|
|
66
|
-
return item ? item.name : id;
|
|
67
|
-
});
|
|
68
|
-
lines.push(` ${label}: ${names.join(', ')}`);
|
|
80
|
+
lines.push(` ${catConfig?.title || cat}: ${itemNames(catConfig, ids).join(', ')}`);
|
|
69
81
|
}
|
|
70
82
|
return lines.join('\n');
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
function computeDiff(oldLock, selections) {
|
|
74
|
-
const oldSels = oldLock
|
|
85
|
+
export function computeDiff(oldLock, selections) {
|
|
86
|
+
const oldSels = lockToSelections(oldLock);
|
|
75
87
|
const added = {};
|
|
76
88
|
const removed = {};
|
|
77
89
|
const kept = {};
|
|
@@ -95,240 +107,402 @@ function computeDiff(oldLock, selections) {
|
|
|
95
107
|
return { added, removed, kept };
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
function formatDiff(diff) {
|
|
99
|
-
const
|
|
110
|
+
export function formatDiff(diff) {
|
|
111
|
+
const sections = [
|
|
112
|
+
['+ Added', diff.added],
|
|
113
|
+
['- Removed', diff.removed],
|
|
114
|
+
['~ Unchanged', diff.kept],
|
|
115
|
+
];
|
|
100
116
|
const lines = [];
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const names = data.ids.map(id => {
|
|
106
|
-
const item = data.config?.items?.find(i => i.id === id);
|
|
107
|
-
return item?.name || id;
|
|
108
|
-
});
|
|
109
|
-
lines.push(` ${data.config?.title || cat}: ${names.join(', ')}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (Object.keys(removed).length) {
|
|
118
|
+
for (const [label, group] of sections) {
|
|
119
|
+
const entries = Object.entries(group);
|
|
120
|
+
if (!entries.length) continue;
|
|
114
121
|
if (lines.length) lines.push('');
|
|
115
|
-
lines.push(
|
|
116
|
-
for (const [cat, data] of
|
|
117
|
-
|
|
118
|
-
const item = data.config?.items?.find(i => i.id === id);
|
|
119
|
-
return item?.name || id;
|
|
120
|
-
});
|
|
121
|
-
lines.push(` ${data.config?.title || cat}: ${names.join(', ')}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (Object.keys(kept).length) {
|
|
126
|
-
if (lines.length) lines.push('');
|
|
127
|
-
lines.push(' ~ Unchanged:');
|
|
128
|
-
for (const [cat, data] of Object.entries(kept)) {
|
|
129
|
-
const names = data.ids.map(id => {
|
|
130
|
-
const item = data.config?.items?.find(i => i.id === id);
|
|
131
|
-
return item?.name || id;
|
|
132
|
-
});
|
|
133
|
-
lines.push(` ${data.config?.title || cat}: ${names.join(', ')}`);
|
|
122
|
+
lines.push(` ${label}:`);
|
|
123
|
+
for (const [cat, data] of entries) {
|
|
124
|
+
lines.push(` ${data.config?.title || cat}: ${itemNames(data.config, data.ids).join(', ')}`);
|
|
134
125
|
}
|
|
135
126
|
}
|
|
136
127
|
|
|
137
128
|
return lines.join('\n');
|
|
138
129
|
}
|
|
139
130
|
|
|
140
|
-
export
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
export function initialItemValues(visibleItems, existingIds = []) {
|
|
132
|
+
const visible = new Set(visibleItems.map(item => item.id));
|
|
133
|
+
return existingIds.filter(id => visible.has(id));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function shouldPreselectAll(visibleItems, existingIds = []) {
|
|
137
|
+
if (existingIds.length) return existingIds.length === visibleItems.length;
|
|
138
|
+
return visibleItems.length <= 12;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function readTuiPreferences(absTarget) {
|
|
142
|
+
try {
|
|
143
|
+
return tuiPreferencesFromConfig(JSON.parse(await readFile(resolve(absTarget, 'tui.json'), 'utf-8')));
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) return {};
|
|
146
|
+
throw error;
|
|
146
147
|
}
|
|
148
|
+
}
|
|
147
149
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
async function collectTuiPreferences(existing, ui) {
|
|
151
|
+
const base = normalizeTuiPreferences(existing);
|
|
152
|
+
|
|
153
|
+
const theme = await ui.select({
|
|
154
|
+
message: 'TUI theme',
|
|
155
|
+
options: TUI_THEMES.map(value => ({
|
|
156
|
+
value,
|
|
157
|
+
label: value,
|
|
158
|
+
hint: value === 'system' ? 'Adapts to your terminal' : undefined,
|
|
159
|
+
})),
|
|
160
|
+
initialValue: base.theme,
|
|
161
|
+
});
|
|
162
|
+
if (ui.isCancel(theme)) return null;
|
|
163
|
+
|
|
164
|
+
const diffStyle = await ui.select({
|
|
165
|
+
message: 'Diff style',
|
|
166
|
+
options: [
|
|
167
|
+
{ value: 'auto', label: 'auto', hint: 'Adapts to terminal width' },
|
|
168
|
+
{ value: 'stacked', label: 'stacked', hint: 'Always single column' },
|
|
169
|
+
],
|
|
170
|
+
initialValue: base.diff_style,
|
|
171
|
+
});
|
|
172
|
+
if (ui.isCancel(diffStyle)) return null;
|
|
150
173
|
|
|
151
|
-
const
|
|
152
|
-
|
|
174
|
+
const cursorStyle = await ui.select({
|
|
175
|
+
message: 'Cursor style',
|
|
176
|
+
options: ['block', 'underline', 'line', 'default'].map(value => ({ value, label: value })),
|
|
177
|
+
initialValue: base.cursor.style,
|
|
178
|
+
});
|
|
179
|
+
if (ui.isCancel(cursorStyle)) return null;
|
|
180
|
+
|
|
181
|
+
const scrollSpeed = await ui.select({
|
|
182
|
+
message: 'Scroll speed',
|
|
183
|
+
options: [
|
|
184
|
+
{ value: 1, label: '1', hint: 'Slow' },
|
|
185
|
+
{ value: 2, label: '2' },
|
|
186
|
+
{ value: 3, label: '3', hint: 'Default' },
|
|
187
|
+
{ value: 4, label: '4' },
|
|
188
|
+
{ value: 5, label: '5', hint: 'Fast' },
|
|
189
|
+
],
|
|
190
|
+
initialValue: base.scroll_speed,
|
|
191
|
+
});
|
|
192
|
+
if (ui.isCancel(scrollSpeed)) return null;
|
|
153
193
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
194
|
+
const scrollAcceleration = await ui.confirm({
|
|
195
|
+
message: 'Enable scroll acceleration?',
|
|
196
|
+
initialValue: base.scroll_acceleration,
|
|
197
|
+
});
|
|
198
|
+
if (ui.isCancel(scrollAcceleration)) return null;
|
|
199
|
+
|
|
200
|
+
const mouse = await ui.confirm({
|
|
201
|
+
message: 'Enable mouse support?',
|
|
202
|
+
initialValue: base.mouse,
|
|
203
|
+
});
|
|
204
|
+
if (ui.isCancel(mouse)) return null;
|
|
205
|
+
|
|
206
|
+
const attentionEnabled = await ui.confirm({
|
|
207
|
+
message: 'Enable attention notifications and sounds?',
|
|
208
|
+
initialValue: base.attention.enabled,
|
|
209
|
+
});
|
|
210
|
+
if (ui.isCancel(attentionEnabled)) return null;
|
|
211
|
+
|
|
212
|
+
const attention = { ...base.attention, enabled: attentionEnabled };
|
|
213
|
+
if (attentionEnabled) {
|
|
214
|
+
const notifications = await ui.confirm({
|
|
215
|
+
message: 'Desktop notifications?',
|
|
216
|
+
initialValue: base.attention.notifications,
|
|
168
217
|
});
|
|
169
|
-
|
|
170
|
-
|
|
218
|
+
if (ui.isCancel(notifications)) return null;
|
|
219
|
+
|
|
220
|
+
const sound = await ui.confirm({
|
|
221
|
+
message: 'Sound alerts?',
|
|
222
|
+
initialValue: base.attention.sound,
|
|
223
|
+
});
|
|
224
|
+
if (ui.isCancel(sound)) return null;
|
|
225
|
+
|
|
226
|
+
const volume = await ui.select({
|
|
227
|
+
message: 'Alert volume',
|
|
228
|
+
options: [
|
|
229
|
+
{ value: 0.2, label: '20%' },
|
|
230
|
+
{ value: 0.4, label: '40%', hint: 'Default' },
|
|
231
|
+
{ value: 0.6, label: '60%' },
|
|
232
|
+
{ value: 0.8, label: '80%' },
|
|
233
|
+
{ value: 1, label: '100%' },
|
|
234
|
+
],
|
|
235
|
+
initialValue: base.attention.volume,
|
|
236
|
+
});
|
|
237
|
+
if (ui.isCancel(volume)) return null;
|
|
238
|
+
|
|
239
|
+
attention.notifications = notifications;
|
|
240
|
+
attention.sound = sound;
|
|
241
|
+
attention.volume = volume;
|
|
171
242
|
}
|
|
172
243
|
|
|
173
|
-
|
|
174
|
-
|
|
244
|
+
return normalizeTuiPreferences({
|
|
245
|
+
theme,
|
|
246
|
+
diff_style: diffStyle,
|
|
247
|
+
cursor: { style: cursorStyle, blinking: base.cursor.blinking },
|
|
248
|
+
scroll_speed: scrollSpeed,
|
|
249
|
+
scroll_acceleration: scrollAcceleration,
|
|
250
|
+
mouse,
|
|
251
|
+
attention,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function cancelled(ui) {
|
|
256
|
+
ui.outro('Cancelled.');
|
|
257
|
+
return { status: 'cancelled' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function buildCategoryOptions() {
|
|
261
|
+
return Object.entries(categories).map(([key, cat]) => {
|
|
262
|
+
const visible = cat.items.filter(item => !item.removed);
|
|
175
263
|
return {
|
|
176
264
|
value: key,
|
|
177
265
|
label: cat.title,
|
|
178
266
|
hint: `${visible.length} ${key === 'mcps' ? 'MCPs' : cat.title.toLowerCase()}`,
|
|
179
267
|
};
|
|
180
268
|
});
|
|
269
|
+
}
|
|
181
270
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
271
|
+
async function installGeneratedFiles({ ui, targetDir, agentType, force, dryRun, oldLock, existingSelections, existingTuiPreferences }) {
|
|
272
|
+
const includeAgentsMd = await ui.confirm({
|
|
273
|
+
message: 'Generate AGENTS.md?',
|
|
274
|
+
initialValue: oldLock ? oldLock.includeAgentsMd : true,
|
|
186
275
|
});
|
|
187
|
-
if (isCancel(
|
|
188
|
-
|
|
189
|
-
|
|
276
|
+
if (ui.isCancel(includeAgentsMd)) return cancelled(ui);
|
|
277
|
+
|
|
278
|
+
const hasExisting = Object.keys(existingSelections).length > 0;
|
|
279
|
+
let keepExisting = false;
|
|
280
|
+
if (hasExisting) {
|
|
281
|
+
const removeAll = await ui.confirm({
|
|
282
|
+
message: 'Remove all previously installed components?',
|
|
283
|
+
initialValue: false,
|
|
284
|
+
});
|
|
285
|
+
if (ui.isCancel(removeAll)) return cancelled(ui);
|
|
286
|
+
keepExisting = !removeAll;
|
|
190
287
|
}
|
|
191
288
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
289
|
+
const progress = ui.spinner();
|
|
290
|
+
progress.start('Writing files...');
|
|
291
|
+
await install({
|
|
292
|
+
targetDir,
|
|
293
|
+
agentType,
|
|
294
|
+
selections: keepExisting ? existingSelections : {},
|
|
295
|
+
includeAgentsMd,
|
|
296
|
+
oldSelections: hasExisting ? existingSelections : undefined,
|
|
297
|
+
oldLock,
|
|
298
|
+
tuiPreferences: existingTuiPreferences,
|
|
299
|
+
force,
|
|
300
|
+
dryRun,
|
|
301
|
+
});
|
|
302
|
+
progress.stop('Done.');
|
|
201
303
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
agentType,
|
|
207
|
-
selections: {},
|
|
208
|
-
includeAgentsMd,
|
|
209
|
-
});
|
|
210
|
-
s.stop('Done.');
|
|
211
|
-
const installed = [];
|
|
212
|
-
if (includeAgentsMd) installed.push('AGENTS.md');
|
|
213
|
-
outro(`${installed.join(' and ')} written. Open them in your project to get started.`);
|
|
214
|
-
process.exit(0);
|
|
215
|
-
}
|
|
304
|
+
const installed = includeAgentsMd ? ['AGENTS.md'] : [];
|
|
305
|
+
ui.outro(`${installed.join(' and ')} written. Open them in your project to get started.`);
|
|
306
|
+
return { status: 'installed' };
|
|
307
|
+
}
|
|
216
308
|
|
|
217
|
-
|
|
309
|
+
function reportPlan({ ui, oldLock, selections, includeAgentsMd }) {
|
|
310
|
+
if (oldLock) {
|
|
311
|
+
const diffText = formatDiff(computeDiff(oldLock, selections));
|
|
312
|
+
ui.log('\nš¦ Changes from previous installation:\n');
|
|
313
|
+
if (diffText) {
|
|
314
|
+
ui.log(diffText);
|
|
315
|
+
ui.log();
|
|
316
|
+
} else {
|
|
317
|
+
ui.log(' No changes ā same selections as before.\n');
|
|
318
|
+
}
|
|
218
319
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
320
|
+
const generatedFiles = [];
|
|
321
|
+
if (includeAgentsMd) generatedFiles.push('AGENTS.md');
|
|
322
|
+
generatedFiles.push('opencode.json', 'tui.json', '.gitignore');
|
|
323
|
+
if (selections.mcps?.length) generatedFiles.push('.env');
|
|
324
|
+
if (selections.memory?.length) generatedFiles.push('memory/');
|
|
325
|
+
|
|
326
|
+
if (generatedFiles.length) {
|
|
327
|
+
ui.log(' Generated files:');
|
|
328
|
+
for (const file of generatedFiles) ui.log(` š ${file}`);
|
|
329
|
+
ui.log();
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
226
332
|
}
|
|
227
333
|
|
|
334
|
+
ui.log('\nš¦ Summary of what will be installed:\n');
|
|
335
|
+
if (includeAgentsMd) ui.log(' š AGENTS.md');
|
|
336
|
+
ui.log(' š opencode.json');
|
|
337
|
+
ui.log(' š tui.json');
|
|
338
|
+
ui.log(' š .gitignore');
|
|
339
|
+
ui.log(buildSummary(selections));
|
|
340
|
+
ui.log();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function collectSelections({ ui, selectedCategories, existingSelections }) {
|
|
344
|
+
const selections = {};
|
|
228
345
|
for (const cat of selectedCategories) {
|
|
229
346
|
const catConfig = categories[cat];
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
const visibleItems = catConfig.items.filter(i => !i.removed);
|
|
347
|
+
const visibleItems = catConfig.items.filter(item => !item.removed);
|
|
348
|
+
const existingIds = initialItemValues(visibleItems, existingSelections[cat] || []);
|
|
233
349
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
outro('Cancelled.');
|
|
240
|
-
process.exit(0);
|
|
241
|
-
}
|
|
350
|
+
const all = await ui.confirm({
|
|
351
|
+
message: `Install all ${catConfig.title.toLowerCase()}?`,
|
|
352
|
+
initialValue: shouldPreselectAll(visibleItems, existingIds),
|
|
353
|
+
});
|
|
354
|
+
if (ui.isCancel(all)) return null;
|
|
242
355
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const picked = await multiselect({
|
|
247
|
-
message: `Which ${catConfig.title.toLowerCase()} do you want?`,
|
|
248
|
-
options: visibleItems.map(item => ({
|
|
249
|
-
value: item.id,
|
|
250
|
-
label: item.deprecated ? `${item.name} (deprecated)` : item.name,
|
|
251
|
-
hint: item.deprecated ? 'ā Deprecated ā consider alternatives' : item.description,
|
|
252
|
-
})),
|
|
253
|
-
required: true,
|
|
254
|
-
});
|
|
255
|
-
if (isCancel(picked)) {
|
|
256
|
-
outro('Cancelled.');
|
|
257
|
-
process.exit(0);
|
|
258
|
-
}
|
|
259
|
-
selections[cat] = picked;
|
|
356
|
+
if (all) {
|
|
357
|
+
selections[cat] = visibleItems.map(item => item.id);
|
|
358
|
+
continue;
|
|
260
359
|
}
|
|
261
|
-
}
|
|
262
360
|
|
|
263
|
-
|
|
264
|
-
|
|
361
|
+
const picked = await ui.multiselect({
|
|
362
|
+
message: `Which ${catConfig.title.toLowerCase()} do you want?`,
|
|
363
|
+
options: visibleItems.map(item => ({
|
|
364
|
+
value: item.id,
|
|
365
|
+
label: item.deprecated ? `${item.name} (deprecated)` : item.name,
|
|
366
|
+
hint: item.deprecated ? 'ā Deprecated ā consider alternatives' : item.description,
|
|
367
|
+
})),
|
|
368
|
+
initialValues: existingIds,
|
|
369
|
+
required: true,
|
|
370
|
+
});
|
|
371
|
+
if (ui.isCancel(picked)) return null;
|
|
372
|
+
selections[cat] = picked;
|
|
373
|
+
}
|
|
374
|
+
return selections;
|
|
375
|
+
}
|
|
265
376
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
377
|
+
async function installSelectedItems({ ui, targetDir, agentType, force, dryRun, oldLock, existingSelections, existingTuiPreferences }, selectedCategories) {
|
|
378
|
+
const includeAgentsMd = await ui.confirm({
|
|
379
|
+
message: 'Generate AGENTS.md?',
|
|
380
|
+
initialValue: oldLock ? oldLock.includeAgentsMd : true,
|
|
381
|
+
});
|
|
382
|
+
if (ui.isCancel(includeAgentsMd)) return cancelled(ui);
|
|
269
383
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
console.log(diffText);
|
|
273
|
-
console.log();
|
|
274
|
-
} else {
|
|
275
|
-
console.log(' No changes ā same selections as before.\n');
|
|
276
|
-
}
|
|
384
|
+
const selections = await collectSelections({ ui, selectedCategories, existingSelections });
|
|
385
|
+
if (!selections) return cancelled(ui);
|
|
277
386
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
387
|
+
let tuiPreferences = existingTuiPreferences;
|
|
388
|
+
const customizeTui = await ui.confirm({
|
|
389
|
+
message: 'Customize OpenCode TUI settings?',
|
|
390
|
+
initialValue: false,
|
|
391
|
+
});
|
|
392
|
+
if (ui.isCancel(customizeTui)) return cancelled(ui);
|
|
393
|
+
if (customizeTui) {
|
|
394
|
+
const custom = await collectTuiPreferences(existingTuiPreferences, ui);
|
|
395
|
+
if (!custom) return cancelled(ui);
|
|
396
|
+
tuiPreferences = custom;
|
|
397
|
+
}
|
|
283
398
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
399
|
+
if (oldLock) {
|
|
400
|
+
const initialDiff = computeDiff(oldLock, selections);
|
|
401
|
+
if (Object.keys(initialDiff.removed).length) {
|
|
402
|
+
const removeDeselected = await ui.confirm({
|
|
403
|
+
message: 'Remove deselected items from the previous installation?',
|
|
404
|
+
initialValue: false,
|
|
405
|
+
});
|
|
406
|
+
if (ui.isCancel(removeDeselected)) return cancelled(ui);
|
|
407
|
+
if (!removeDeselected) {
|
|
408
|
+
for (const [cat, data] of Object.entries(initialDiff.removed)) {
|
|
409
|
+
selections[cat] = [...new Set([...(selections[cat] || []), ...data.ids])];
|
|
410
|
+
}
|
|
288
411
|
}
|
|
289
|
-
console.log();
|
|
290
412
|
}
|
|
291
|
-
} else {
|
|
292
|
-
console.log('\nš¦ Summary of what will be installed:\n');
|
|
293
|
-
if (includeAgentsMd) console.log(' š AGENTS.md');
|
|
294
|
-
console.log(' š opencode.json');
|
|
295
|
-
console.log(' š tui.json');
|
|
296
|
-
console.log(' š .gitignore');
|
|
297
|
-
console.log(buildSummary(selections));
|
|
298
|
-
console.log();
|
|
299
413
|
}
|
|
300
414
|
|
|
301
|
-
|
|
415
|
+
reportPlan({ ui, oldLock, selections, includeAgentsMd });
|
|
416
|
+
|
|
417
|
+
const confirmed = await ui.confirm({
|
|
302
418
|
message: 'Proceed with installation?',
|
|
303
419
|
initialValue: true,
|
|
304
420
|
});
|
|
305
|
-
if (isCancel(confirmed) || !confirmed) {
|
|
306
|
-
outro('Installation cancelled.');
|
|
307
|
-
|
|
421
|
+
if (ui.isCancel(confirmed) || !confirmed) {
|
|
422
|
+
ui.outro('Installation cancelled.');
|
|
423
|
+
return { status: 'cancelled' };
|
|
308
424
|
}
|
|
309
425
|
|
|
310
|
-
const
|
|
311
|
-
|
|
426
|
+
const progress = ui.spinner();
|
|
427
|
+
progress.start(oldLock ? 'Updating files...' : 'Installing files...');
|
|
312
428
|
|
|
313
429
|
const finalTarget = await install({
|
|
314
430
|
targetDir,
|
|
315
431
|
agentType,
|
|
316
432
|
selections,
|
|
317
433
|
includeAgentsMd,
|
|
318
|
-
oldSelections: oldLock
|
|
434
|
+
oldSelections: oldLock ? existingSelections : undefined,
|
|
319
435
|
oldLock,
|
|
320
|
-
|
|
321
|
-
|
|
436
|
+
tuiPreferences,
|
|
437
|
+
force,
|
|
438
|
+
dryRun,
|
|
322
439
|
});
|
|
323
440
|
|
|
324
|
-
|
|
441
|
+
progress.stop('Installation complete!');
|
|
325
442
|
|
|
326
|
-
const fileCount = Object.values(selections).reduce((sum,
|
|
443
|
+
const fileCount = Object.values(selections).reduce((sum, ids) => sum + (ids?.length || 0), 0);
|
|
327
444
|
const verb = oldLock ? 'Updated' : 'Installed';
|
|
328
|
-
outro(`${verb} ${fileCount} components to ${finalTarget}
|
|
445
|
+
ui.outro(`${verb} ${fileCount} components to ${finalTarget}
|
|
329
446
|
|
|
330
447
|
Next steps:
|
|
331
448
|
${agentType === 'opencode' ? '- Open your project in OpenCode ā it will read opencode.json and AGENTS.md automatically' : '- Point your AI coding agent to AGENTS.md as the entry point'}
|
|
332
449
|
- Run /help in your agent to see available commands
|
|
333
450
|
`);
|
|
451
|
+
return { status: 'installed', target: finalTarget };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function runInteractive({ targetDir, force = false, dryRun = false, agentType = 'opencode', ui = defaultUi }) {
|
|
455
|
+
const absTarget = resolve(process.cwd(), targetDir);
|
|
456
|
+
const oldLock = await loadLockFile(absTarget);
|
|
457
|
+
const existingSelections = lockToSelections(oldLock);
|
|
458
|
+
const existingTuiPreferences = await readTuiPreferences(absTarget);
|
|
459
|
+
const context = { ui, targetDir, agentType, force, dryRun, oldLock, existingSelections, existingTuiPreferences };
|
|
460
|
+
|
|
461
|
+
const selectedCategories = await ui.multiselect({
|
|
462
|
+
message: 'What would you like to install?',
|
|
463
|
+
options: buildCategoryOptions(),
|
|
464
|
+
initialValues: Object.keys(existingSelections),
|
|
465
|
+
required: false,
|
|
466
|
+
});
|
|
467
|
+
if (ui.isCancel(selectedCategories)) return cancelled(ui);
|
|
468
|
+
|
|
469
|
+
if (!selectedCategories?.length) return installGeneratedFiles(context);
|
|
470
|
+
return installSelectedItems(context, selectedCategories);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function runNonInteractive({ targetDir, selections, all, includeAgentsMd, force = false, dryRun = false, agentType = 'opencode', ui = defaultUi }) {
|
|
474
|
+
const resolvedSelections = all ? allSelections() : selections;
|
|
475
|
+
const absTarget = resolve(process.cwd(), targetDir);
|
|
476
|
+
const oldLock = await loadLockFile(absTarget);
|
|
477
|
+
if (!dryRun) ui.log(`Installing selected components into ${absTarget}`);
|
|
478
|
+
await install({
|
|
479
|
+
targetDir,
|
|
480
|
+
agentType,
|
|
481
|
+
selections: resolvedSelections,
|
|
482
|
+
includeAgentsMd,
|
|
483
|
+
oldSelections: oldLock ? lockToSelections(oldLock) : undefined,
|
|
484
|
+
oldLock,
|
|
485
|
+
force,
|
|
486
|
+
dryRun,
|
|
487
|
+
});
|
|
488
|
+
ui.log(dryRun ? 'Dry run complete.' : 'Installation complete.');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
492
|
+
const args = parseArgs(argv);
|
|
493
|
+
if (args.doctor) {
|
|
494
|
+
const healthy = await doctor(args.targetDir);
|
|
495
|
+
if (!healthy) process.exitCode = 1;
|
|
496
|
+
return healthy;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const version = await getPackageVersion();
|
|
500
|
+
defaultUi.intro(`System prompt (v${version})`);
|
|
501
|
+
|
|
502
|
+
const agentType = 'opencode';
|
|
503
|
+
|
|
504
|
+
if (args.nonInteractive) {
|
|
505
|
+
return runNonInteractive({ ...args, agentType });
|
|
506
|
+
}
|
|
507
|
+
return runInteractive({ ...args, agentType });
|
|
334
508
|
}
|