@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/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
- const label = catConfig?.title || cat;
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.selections;
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 { added, removed, kept } = diff;
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
- if (Object.keys(added).length) {
103
- lines.push(' + Added:');
104
- for (const [cat, data] of Object.entries(added)) {
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(' - Removed:');
116
- for (const [cat, data] of Object.entries(removed)) {
117
- const names = data.ids.map(id => {
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 async function main(argv = process.argv.slice(2)) {
141
- const args = parseArgs(argv);
142
- if (args.doctor) {
143
- const healthy = await doctor(args.targetDir);
144
- if (!healthy) process.exitCode = 1;
145
- return healthy;
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
- const version = await getPackageVersion();
149
- intro(`System prompt (v${version})`);
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 agentType = 'opencode';
152
- const targetDir = args.targetDir;
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
- if (args.nonInteractive) {
155
- const selections = args.all ? allSelections() : args.selections;
156
- const absTarget = resolve(process.cwd(), targetDir);
157
- const oldLock = await loadLockFile(absTarget);
158
- if (!args.dryRun) console.log(`Installing selected components into ${absTarget}`);
159
- await install({
160
- targetDir,
161
- agentType,
162
- selections,
163
- includeAgentsMd: args.includeAgentsMd,
164
- oldSelections: oldLock?.selections,
165
- oldLock,
166
- force: args.force,
167
- dryRun: args.dryRun,
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
- console.log(args.dryRun ? 'Dry run complete.' : 'Installation complete.');
170
- return;
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
- const categoryOptions = Object.entries(categories).map(([key, cat]) => {
174
- const visible = cat.items.filter(i => !i.removed);
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
- const selectedCategories = await multiselect({
183
- message: 'What would you like to install?',
184
- options: categoryOptions,
185
- required: false,
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(selectedCategories)) {
188
- outro('Cancelled.');
189
- process.exit(0);
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
- if (!selectedCategories?.length) {
193
- const includeAgentsMd = await confirm({
194
- message: 'Generate AGENTS.md?',
195
- initialValue: true,
196
- });
197
- if (isCancel(includeAgentsMd)) {
198
- outro('Cancelled.');
199
- process.exit(0);
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
- const s = spinner();
203
- s.start('Writing files...');
204
- await install({
205
- targetDir,
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
- const selections = {};
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
- const includeAgentsMd = await confirm({
220
- message: 'Generate AGENTS.md?',
221
- initialValue: true,
222
- });
223
- if (isCancel(includeAgentsMd)) {
224
- outro('Cancelled.');
225
- process.exit(0);
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 allIds = catConfig.items.map(i => i.id);
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
- const all = await confirm({
235
- message: `Install all ${catConfig.title.toLowerCase()}?`,
236
- initialValue: visibleItems.length <= 12,
237
- });
238
- if (isCancel(all)) {
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
- if (all) {
244
- selections[cat] = visibleItems.map(i => i.id);
245
- } else {
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
- const absTarget = resolve(process.cwd(), targetDir);
264
- const oldLock = await loadLockFile(absTarget);
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
- if (oldLock) {
267
- const diff = computeDiff(oldLock, selections);
268
- const diffText = formatDiff(diff);
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
- console.log('\nšŸ“¦ Changes from previous installation:\n');
271
- if (diffText) {
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
- const genFiles = [];
279
- if (includeAgentsMd) genFiles.push('AGENTS.md');
280
- genFiles.push('opencode.json', 'tui.json', '.gitignore');
281
- if (selections.mcps?.length) genFiles.push('.env');
282
- if (selections.memory?.length) genFiles.push('memory/');
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
- if (genFiles.length) {
285
- console.log(' Generated files:');
286
- for (const f of genFiles) {
287
- console.log(` šŸ“„ ${f}`);
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
- const confirmed = await confirm({
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
- process.exit(0);
421
+ if (ui.isCancel(confirmed) || !confirmed) {
422
+ ui.outro('Installation cancelled.');
423
+ return { status: 'cancelled' };
308
424
  }
309
425
 
310
- const s = spinner();
311
- s.start(oldLock ? 'Updating files...' : 'Installing files...');
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?.selections,
434
+ oldSelections: oldLock ? existingSelections : undefined,
319
435
  oldLock,
320
- force: args.force,
321
- dryRun: args.dryRun,
436
+ tuiPreferences,
437
+ force,
438
+ dryRun,
322
439
  });
323
440
 
324
- s.stop('Installation complete!');
441
+ progress.stop('Installation complete!');
325
442
 
326
- const fileCount = Object.values(selections).reduce((sum, arr) => sum + (arr?.length || 0), 0);
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
  }