pantheon-opencode 1.1.1-beta.3 → 1.1.1-beta.5

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.
@@ -27,10 +27,13 @@ function printUsage() {
27
27
  console.log(' npx pantheon-opencode init # Install globally');
28
28
  console.log(' npx pantheon-opencode init --project # Install in project');
29
29
  console.log(' npx pantheon-opencode init --dry-run # Preview only');
30
- console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv');
31
- console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv');
32
- console.log(' npx pantheon-opencode init --doctor # Run health check after install');
33
- console.log(' npx pantheon-opencode --help # Show this help');
30
+ console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv');
31
+ console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv');
32
+ console.log(' npx pantheon-opencode init --doctor # Run health check after install');
33
+ console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode');
34
+ console.log(' npx pantheon-opencode init --headless # Force non-interactive mode');
35
+ console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults');
36
+ console.log(' npx pantheon-opencode --help # Show this help');
34
37
  }
35
38
 
36
39
  async function main() {
@@ -45,6 +48,9 @@ async function main() {
45
48
  const skipMCP = args.includes('--no-mcp');
46
49
  const forceReinstall = args.includes('--force');
47
50
  const runDoctor = args.includes('--doctor');
51
+ const forceInteractive = args.includes('--interactive');
52
+ const forceHeadless = args.includes('--headless');
53
+ const autoYes = args.includes('--yes') || args.includes('-y');
48
54
 
49
55
  const components = ['agents', 'skills', 'instructions', 'commands', 'plugins'];
50
56
  if (!skipMCP) components.push('runtime');
@@ -56,13 +62,20 @@ async function main() {
56
62
  version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '?';
57
63
  } catch { /* use default */ }
58
64
 
59
- console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`);
60
65
  console.log('');
66
+ if (!forceInteractive || isDryRun) {
67
+ console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`);
68
+ console.log('');
69
+ }
61
70
 
62
71
  try {
63
72
  const { installOpenCode } = await import('../scripts/install/opencode.mjs');
64
73
  const target = isProject ? process.cwd() : undefined;
65
- installOpenCode(target, isDryRun, forceReinstall, components);
74
+ await installOpenCode(target, isDryRun, forceReinstall, components, {
75
+ interactive: forceInteractive,
76
+ headless: forceHeadless,
77
+ yes: autoYes,
78
+ });
66
79
  } catch (err) {
67
80
  console.error(`❌ Installation failed: ${err.message}\n Run with --no-mcp to skip Python dependencies:\n npx pantheon-opencode init --no-mcp\n Or retry with --force to recreate the venv:\n npx pantheon-opencode init --force`);
68
81
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.1.1-beta.3",
3
+ "version": "1.1.1-beta.5",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -0,0 +1,160 @@
1
+ export const icons = {
2
+ success: '\u2705',
3
+ warning: '\u26a0\ufe0f ',
4
+ error: '\u274c',
5
+ info: '\u2139\ufe0f ',
6
+ progress: '\u23f3',
7
+ done: '\u2705',
8
+ arrow: '\u2192',
9
+ bullet: '\u2022',
10
+ star: '\u2b50',
11
+ wrench: '\U0001f527',
12
+ rocket: '\U0001f680',
13
+ package: '\U0001f4e6',
14
+ gear: '\u2699\ufe0f',
15
+ }
16
+
17
+ export const colors = {
18
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
19
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
20
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
21
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
22
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
23
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
24
+ }
25
+
26
+ let _quiet = false
27
+ let _dryRun = false
28
+
29
+ export function configure(opts = {}) {
30
+ if (opts.quiet !== undefined) _quiet = opts.quiet
31
+ if (opts.dryRun !== undefined) _dryRun = opts.dryRun
32
+ }
33
+
34
+ function tag(msg) {
35
+ return _dryRun ? `[DRY-RUN] ${msg}` : msg
36
+ }
37
+
38
+ function tty() {
39
+ return process.stdout.isTTY
40
+ }
41
+
42
+ export function section(title) {
43
+ if (_quiet) return
44
+ const width = Math.min(process.stdout.columns || 60, 72)
45
+ const text = `\u2500\u2500 ${title} `
46
+ const line = text + '\u2500'.repeat(Math.max(0, width - text.length + 1))
47
+ console.log(tag(line))
48
+ }
49
+
50
+ export function step(message) {
51
+ const text = tag(`${icons.progress} ${message}...`)
52
+ if (!_quiet) process.stdout.write(text)
53
+ return (ok = true) => {
54
+ const icon = ok ? icons.success : icons.error
55
+ const doneText = tag(`${icon} ${message}`)
56
+ if (_quiet && !ok) {
57
+ console.warn(doneText)
58
+ } else if (!_quiet) {
59
+ process.stdout.write(`\r${doneText}\n`)
60
+ }
61
+ }
62
+ }
63
+
64
+ export function success(message) {
65
+ console.log(tag(`${icons.success} ${message}`))
66
+ }
67
+
68
+ export function warning(message) {
69
+ console.warn(tag(`${icons.warning}${message}`))
70
+ }
71
+
72
+ export function error(message) {
73
+ console.error(tag(`${icons.error} ${message}`))
74
+ }
75
+
76
+ export function info(message) {
77
+ if (_quiet) return
78
+ console.log(tag(`${icons.info}${message}`))
79
+ }
80
+
81
+ export function bullet(text, indent = 0) {
82
+ if (_quiet) return
83
+ const pad = ' '.repeat(indent)
84
+ console.log(tag(`${pad}${icons.bullet} ${text}`))
85
+ }
86
+
87
+ export function summaryTable(items) {
88
+ if (_quiet || items.length === 0) return
89
+
90
+ const nameWidth = Math.max(...items.map((i) => i.name.length), 15)
91
+ const statusWidth = Math.max(...items.map((i) => String(i.status).length), 8)
92
+
93
+ const top = `\u250c${'\u2500'.repeat(nameWidth + 2)}\u252c${'\u2500'.repeat(statusWidth + 2)}\u2510`
94
+ const sep = `\u251c${'\u2500'.repeat(nameWidth + 2)}\u253c${'\u2500'.repeat(statusWidth + 2)}\u2524`
95
+ const bot = `\u2514${'\u2500'.repeat(nameWidth + 2)}\u2534${'\u2500'.repeat(statusWidth + 2)}\u2518`
96
+
97
+ console.log(tag(top))
98
+ console.log(tag(`\u2502 ${'Componente'.padEnd(nameWidth)} \u2502 ${'Status'.padEnd(statusWidth)} \u2502`))
99
+ console.log(tag(sep))
100
+ for (const item of items) {
101
+ console.log(tag(`\u2502 ${item.name.padEnd(nameWidth)} \u2502 ${String(item.status).padEnd(statusWidth)} \u2502`))
102
+ }
103
+ console.log(tag(bot))
104
+ }
105
+
106
+ export function spinner(message) {
107
+ const frames = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
108
+ let i = 0
109
+ let running = true
110
+
111
+ if (tty() && !_quiet) {
112
+ const interval = setInterval(() => {
113
+ if (!running) { clearInterval(interval); return }
114
+ process.stdout.write(`\r${frames[i % frames.length]} ${tag(message)}...`)
115
+ i++
116
+ }, 80)
117
+ process.stdout.write(`\r${frames[0]} ${tag(message)}...`)
118
+
119
+ return (ok = true) => {
120
+ running = false
121
+ clearInterval(interval)
122
+ const icon = ok ? icons.success : icons.error
123
+ process.stdout.write(`\r${icon} ${tag(message)}\n`)
124
+ }
125
+ } else {
126
+ const text = tag(`${icons.progress} ${message}...`)
127
+ if (!_quiet) process.stdout.write(text)
128
+ return (ok = true) => {
129
+ const icon = ok ? icons.success : icons.error
130
+ const doneText = tag(`${icon} ${message}`)
131
+ if (_quiet && !ok) {
132
+ console.warn(doneText)
133
+ } else if (!_quiet) {
134
+ process.stdout.write(`\r${doneText}\n`)
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ export function printSummary(target, platforms, stats) {
141
+ if (_quiet) return
142
+
143
+ const line = '\u2500'.repeat(Math.min(process.stdout.columns || 60, 60))
144
+ console.log(tag(`${icons.rocket} OpenCode instalado em ${target}`))
145
+ console.log(tag(line))
146
+ console.log('')
147
+
148
+ if (stats) {
149
+ console.log(tag(` Componentes:\t${stats.created} instalados, ${stats.skipped} pulados`))
150
+ if (stats.errors > 0) {
151
+ console.warn(tag(` ${icons.warning}${stats.errors} erro(s) encontrados`))
152
+ }
153
+ console.log('')
154
+ }
155
+
156
+ console.log(tag(` ${icons.star} Próximos passos:`))
157
+ console.log(tag(` ${icons.bullet} Configure seus agentes em opencode.json`))
158
+ console.log(tag(` ${icons.bullet} Adicione MCP servers em mcp.json`))
159
+ console.log(tag(` ${icons.bullet} Rode 'opencode doctor' para verificar a instalação`))
160
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * component.mjs — Component resolution utilities for the OpenCode installer
3
+ */
4
+
5
+ /** Available component identifiers */
6
+ export const COMPONENT_NAMES = [
7
+ 'agents',
8
+ 'skills',
9
+ 'instructions',
10
+ 'prompts',
11
+ 'commands',
12
+ 'plugins',
13
+ 'runtime',
14
+ ]
15
+
16
+ /**
17
+ * Parse --components flag into a Set.
18
+ * @param {string|string[]} spec - Comma-separated string or array of component names
19
+ * @returns {Set<string>}
20
+ */
21
+ export function resolveComponents(spec) {
22
+ if (Array.isArray(spec)) return new Set(spec)
23
+ return new Set(spec.split(',').map((s) => s.trim()))
24
+ }
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { emitKeypressEvents } from 'node:readline'
4
+ import { colors, icons } from './cli-ui.mjs'
5
+
6
+ const cursorHide = () => process.stdout.write('\x1b[?25l')
7
+ const cursorShow = () => process.stdout.write('\x1b[?25h')
8
+ const cursorUp = (n) => process.stdout.write(`\x1b[${n}A`)
9
+ const clearDown = () => process.stdout.write('\x1b[J')
10
+
11
+ function setupStdin() {
12
+ if (!process.stdin.isTTY) return false
13
+ if (!process.stdout.isTTY) return false
14
+ emitKeypressEvents(process.stdin)
15
+ try {
16
+ process.stdin.setRawMode(true)
17
+ } catch {
18
+ return false
19
+ }
20
+ process.stdin.resume()
21
+ return true
22
+ }
23
+
24
+ function teardownStdin() {
25
+ try {
26
+ process.stdin.setRawMode(false)
27
+ process.stdin.removeAllListeners('keypress')
28
+ process.stdin.pause()
29
+ } catch {
30
+ /* safe to ignore */
31
+ }
32
+ }
33
+
34
+ export async function promptMultiSelect(options, { title = '', selected = new Set(), pageSize = 6 } = {}) {
35
+ if (options.length === 0) return new Set()
36
+
37
+ const preSelected = selected instanceof Set ? selected : new Set(selected)
38
+ const items = options.map((o) => ({
39
+ name: o.name,
40
+ value: o.value,
41
+ description: o.description || '',
42
+ checked: o.checked || preSelected.has(o.value),
43
+ }))
44
+
45
+ let cursor = 0
46
+ let scroll = 0
47
+ const maxVisible = Math.min(pageSize, options.length)
48
+ let previousLineCount = 0
49
+
50
+ const isTTY = setupStdin()
51
+ if (!isTTY) {
52
+ return new Set(items.filter((i) => i.checked).map((i) => i.value))
53
+ }
54
+ cursorHide()
55
+
56
+ function render() {
57
+ if (cursor < scroll) scroll = cursor
58
+ if (cursor >= scroll + maxVisible) scroll = cursor - maxVisible + 1
59
+
60
+ const visible = items.slice(scroll, scroll + maxVisible)
61
+ const lines = []
62
+
63
+ if (title) lines.push(colors.bold(title))
64
+
65
+ const hasMoreAbove = scroll > 0
66
+ const hasMoreBelow = scroll + maxVisible < options.length
67
+
68
+ if (hasMoreAbove) lines.push(colors.dim(' ...'))
69
+
70
+ for (let i = 0; i < visible.length; i++) {
71
+ const item = visible[i]
72
+ const idx = scroll + i
73
+ const active = idx === cursor
74
+ const box = item.checked ? '[x]' : '[ ]'
75
+ const ptr = active ? `${colors.cyan('>')} ` : ' '
76
+ const name = active ? colors.cyan(item.name) : item.name
77
+ const desc = item.description ? colors.dim(` — ${item.description}`) : ''
78
+ lines.push(`${ptr}${colors.bold(box)} ${name}${desc}`)
79
+ }
80
+
81
+ if (hasMoreBelow) lines.push(colors.dim(' ...'))
82
+
83
+ const count = items.filter((i) => i.checked).length
84
+ const total = items.length
85
+ const noun = count === 1 ? 'selected' : 'selected'
86
+ lines.push(colors.dim(`(${count}/${total} ${noun}) — ${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Space')}${colors.dim(' toggle · ')}${colors.cyan('Enter')}${colors.dim(' confirm')}`))
87
+
88
+ const lineCount = lines.length
89
+ if (previousLineCount > 0) cursorUp(previousLineCount)
90
+ clearDown()
91
+ process.stdout.write(lines.join('\n') + '\n')
92
+ previousLineCount = lineCount
93
+ }
94
+
95
+ return new Promise((resolve, reject) => {
96
+ let settled = false
97
+
98
+ function finalize(result) {
99
+ if (settled) return
100
+ settled = true
101
+ cursorShow()
102
+ teardownStdin()
103
+ if (previousLineCount > 0) cursorUp(previousLineCount)
104
+ clearDown()
105
+ resolve(result)
106
+ }
107
+
108
+ function cancel() {
109
+ if (settled) return
110
+ settled = true
111
+ cursorShow()
112
+ teardownStdin()
113
+ if (previousLineCount > 0) cursorUp(previousLineCount)
114
+ clearDown()
115
+ reject(new Error('Canceled'))
116
+ }
117
+
118
+ function handler(str, key) {
119
+ if (!key) return
120
+ if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
121
+ cancel()
122
+ return
123
+ }
124
+ if (key.name === 'return' || key.name === 'enter') {
125
+ const result = new Set(items.filter((i) => i.checked).map((i) => i.value))
126
+ finalize(result)
127
+ return
128
+ }
129
+ if (key.name === 'up') {
130
+ cursor = (cursor - 1 + items.length) % items.length
131
+ render()
132
+ return
133
+ }
134
+ if (key.name === 'down') {
135
+ cursor = (cursor + 1) % items.length
136
+ render()
137
+ return
138
+ }
139
+ if (key.name === 'space') {
140
+ items[cursor].checked = !items[cursor].checked
141
+ render()
142
+ return
143
+ }
144
+ }
145
+
146
+ process.stdin.on('keypress', handler)
147
+ render()
148
+ })
149
+ }
150
+
151
+ export async function promptConfirm(message, { default: def = true } = {}) {
152
+ const label = def ? 'Y/n' : 'y/N'
153
+ const isTTY = setupStdin()
154
+ if (!isTTY) return def
155
+
156
+ cursorHide()
157
+ process.stdout.write(`${colors.bold('?')} ${message} ${colors.cyan(label)} `)
158
+
159
+ return new Promise((resolve, reject) => {
160
+ let settled = false
161
+
162
+ function finalize(result) {
163
+ if (settled) return
164
+ settled = true
165
+ cursorShow()
166
+ teardownStdin()
167
+ process.stdout.write(`${result ? 'Yes' : 'No'}\n`)
168
+ resolve(result)
169
+ }
170
+
171
+ function cancel() {
172
+ if (settled) return
173
+ settled = true
174
+ cursorShow()
175
+ teardownStdin()
176
+ process.stdout.write('\n')
177
+ reject(new Error('Canceled'))
178
+ }
179
+
180
+ function handler(str, key) {
181
+ if (!key) return
182
+ if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
183
+ cancel()
184
+ return
185
+ }
186
+ if (key.name === 'return' || key.name === 'enter') {
187
+ finalize(def)
188
+ return
189
+ }
190
+ if (str) {
191
+ const ch = str.toLowerCase()
192
+ if (ch === 'y' || ch === 'yes') {
193
+ finalize(true)
194
+ } else if (ch === 'n' || ch === 'no') {
195
+ finalize(false)
196
+ }
197
+ }
198
+ }
199
+
200
+ process.stdin.on('keypress', handler)
201
+ })
202
+ }
203
+
204
+ export async function promptList(options, { title = '' } = {}) {
205
+ if (options.length === 0) throw new Error('No options provided')
206
+ if (options.length === 1) return options[0].value
207
+
208
+ const items = options.map((o) => ({
209
+ name: o.name,
210
+ value: o.value,
211
+ description: o.description || '',
212
+ }))
213
+
214
+ let cursor = 0
215
+ const pageSize = Math.min(8, options.length)
216
+ let scroll = 0
217
+ const maxVisible = Math.min(pageSize, options.length)
218
+ let previousLineCount = 0
219
+
220
+ const isTTY = setupStdin()
221
+ if (!isTTY) return items[0].value
222
+ cursorHide()
223
+
224
+ function render() {
225
+ if (cursor < scroll) scroll = cursor
226
+ if (cursor >= scroll + maxVisible) scroll = cursor - maxVisible + 1
227
+
228
+ const visible = items.slice(scroll, scroll + maxVisible)
229
+ const lines = []
230
+
231
+ if (title) lines.push(colors.bold(title))
232
+
233
+ const hasMoreAbove = scroll > 0
234
+ const hasMoreBelow = scroll + maxVisible < options.length
235
+
236
+ if (hasMoreAbove) lines.push(colors.dim(' ...'))
237
+
238
+ for (let i = 0; i < visible.length; i++) {
239
+ const item = visible[i]
240
+ const idx = scroll + i
241
+ const active = idx === cursor
242
+ const ptr = active ? `${colors.cyan('>')} ` : ' '
243
+ const name = active ? colors.cyan(item.name) : item.name
244
+ const desc = item.description ? colors.dim(` — ${item.description}`) : ''
245
+ lines.push(`${ptr}${colors.bold('◉')} ${name}${desc}`)
246
+ }
247
+
248
+ if (hasMoreBelow) lines.push(colors.dim(' ...'))
249
+
250
+ lines.push(colors.dim(`(${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Enter')}${colors.dim(' select')}`))
251
+
252
+ const lineCount = lines.length
253
+ if (previousLineCount > 0) cursorUp(previousLineCount)
254
+ clearDown()
255
+ process.stdout.write(lines.join('\n') + '\n')
256
+ previousLineCount = lineCount
257
+ }
258
+
259
+ return new Promise((resolve, reject) => {
260
+ let settled = false
261
+
262
+ function finalize(result) {
263
+ if (settled) return
264
+ settled = true
265
+ cursorShow()
266
+ teardownStdin()
267
+ if (previousLineCount > 0) cursorUp(previousLineCount)
268
+ clearDown()
269
+ resolve(result)
270
+ }
271
+
272
+ function cancel() {
273
+ if (settled) return
274
+ settled = true
275
+ cursorShow()
276
+ teardownStdin()
277
+ if (previousLineCount > 0) cursorUp(previousLineCount)
278
+ clearDown()
279
+ reject(new Error('Canceled'))
280
+ }
281
+
282
+ function handler(str, key) {
283
+ if (!key) return
284
+ if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
285
+ cancel()
286
+ return
287
+ }
288
+ if (key.name === 'return' || key.name === 'enter') {
289
+ finalize(items[cursor].value)
290
+ return
291
+ }
292
+ if (key.name === 'up') {
293
+ cursor = (cursor - 1 + items.length) % items.length
294
+ render()
295
+ return
296
+ }
297
+ if (key.name === 'down') {
298
+ cursor = (cursor + 1) % items.length
299
+ render()
300
+ return
301
+ }
302
+ }
303
+
304
+ process.stdin.on('keypress', handler)
305
+ render()
306
+ })
307
+ }
308
+
309
+ export async function runInteractiveInstall({ components = [], defaultComponents = [] } = {}) {
310
+ const defaultSet = new Set(
311
+ defaultComponents.length > 0 ? defaultComponents : components,
312
+ )
313
+
314
+ const componentOptions = [
315
+ {
316
+ name: 'agents',
317
+ value: 'agents',
318
+ description: `Agent configurations and routing (${14} agents)`,
319
+ },
320
+ {
321
+ name: 'skills',
322
+ value: 'skills',
323
+ description: `Reusable skill workflows (${21} skills)`,
324
+ },
325
+ {
326
+ name: 'instructions',
327
+ value: 'instructions',
328
+ description: 'AGENTS.md and instructional files',
329
+ },
330
+ {
331
+ name: 'prompts',
332
+ value: 'prompts',
333
+ description: 'Prompt templates for agents',
334
+ },
335
+ {
336
+ name: 'commands',
337
+ value: 'commands',
338
+ description: 'OpenCode command shortcuts',
339
+ },
340
+ {
341
+ name: 'plugins',
342
+ value: 'plugins',
343
+ description: 'TUI sidebar plugin',
344
+ },
345
+ {
346
+ name: 'runtime',
347
+ value: 'runtime',
348
+ description: 'Python venv + MCP server scripts',
349
+ },
350
+ ]
351
+
352
+ process.stdout.write('\n')
353
+ process.stdout.write(`${colors.bold(colors.cyan('📦 OpenCode Installer'))}\n\n`)
354
+
355
+ const selectedComponents = await promptMultiSelect(componentOptions, {
356
+ title: 'Select components to install:',
357
+ selected: defaultSet,
358
+ pageSize: 7,
359
+ })
360
+
361
+ process.stdout.write('\n')
362
+
363
+ if (selectedComponents.size === 0) {
364
+ process.stdout.write(`${colors.yellow('No components selected — using defaults\n')}`)
365
+ defaultSet.forEach((v) => selectedComponents.add(v))
366
+ }
367
+
368
+ const targetOptions = [
369
+ {
370
+ name: 'Global directory',
371
+ value: 'global',
372
+ description: '~/.config/opencode (shared across projects)',
373
+ },
374
+ {
375
+ name: 'Current project',
376
+ value: 'project',
377
+ description: './.opencode (scoped to this project)',
378
+ },
379
+ ]
380
+
381
+ const target = await promptList(targetOptions, {
382
+ title: 'Install location:',
383
+ })
384
+
385
+ process.stdout.write('\n')
386
+
387
+ const confirmed = await promptConfirm(
388
+ `Install ${selectedComponents.size} component(s) in ${target === 'global' ? 'global config' : 'current project'}?`,
389
+ { default: true },
390
+ )
391
+
392
+ process.stdout.write('\n')
393
+
394
+ return { components: selectedComponents, target, confirmed }
395
+ }