@openbuilder/cli 0.31.11

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.
Files changed (78) hide show
  1. package/README.md +1053 -0
  2. package/bin/openbuilder.js +31 -0
  3. package/dist/chunks/Banner-D4tqKfzA.js +113 -0
  4. package/dist/chunks/Banner-D4tqKfzA.js.map +1 -0
  5. package/dist/chunks/auto-update-Dj3lWPWO.js +350 -0
  6. package/dist/chunks/auto-update-Dj3lWPWO.js.map +1 -0
  7. package/dist/chunks/build-D0qYqIq0.js +116 -0
  8. package/dist/chunks/build-D0qYqIq0.js.map +1 -0
  9. package/dist/chunks/cleanup-qVTsA3tk.js +141 -0
  10. package/dist/chunks/cleanup-qVTsA3tk.js.map +1 -0
  11. package/dist/chunks/cli-error-BjQwvWtK.js +140 -0
  12. package/dist/chunks/cli-error-BjQwvWtK.js.map +1 -0
  13. package/dist/chunks/config-BGP1jZJ4.js +167 -0
  14. package/dist/chunks/config-BGP1jZJ4.js.map +1 -0
  15. package/dist/chunks/config-manager-BkbjtN-H.js +133 -0
  16. package/dist/chunks/config-manager-BkbjtN-H.js.map +1 -0
  17. package/dist/chunks/database-BvAbD4sP.js +68 -0
  18. package/dist/chunks/database-BvAbD4sP.js.map +1 -0
  19. package/dist/chunks/database-setup-BYjIRAmT.js +253 -0
  20. package/dist/chunks/database-setup-BYjIRAmT.js.map +1 -0
  21. package/dist/chunks/exports-ij9sv4UM.js +7793 -0
  22. package/dist/chunks/exports-ij9sv4UM.js.map +1 -0
  23. package/dist/chunks/init-CZoN6soU.js +468 -0
  24. package/dist/chunks/init-CZoN6soU.js.map +1 -0
  25. package/dist/chunks/init-tui-BNzk_7Yx.js +1127 -0
  26. package/dist/chunks/init-tui-BNzk_7Yx.js.map +1 -0
  27. package/dist/chunks/logger-ZpJi7chw.js +38 -0
  28. package/dist/chunks/logger-ZpJi7chw.js.map +1 -0
  29. package/dist/chunks/main-tui-Cq1hLCx-.js +644 -0
  30. package/dist/chunks/main-tui-Cq1hLCx-.js.map +1 -0
  31. package/dist/chunks/manager-CvGX9qqe.js +1161 -0
  32. package/dist/chunks/manager-CvGX9qqe.js.map +1 -0
  33. package/dist/chunks/port-allocator-BRFzgH9b.js +749 -0
  34. package/dist/chunks/port-allocator-BRFzgH9b.js.map +1 -0
  35. package/dist/chunks/process-killer-CaUL7Kpl.js +87 -0
  36. package/dist/chunks/process-killer-CaUL7Kpl.js.map +1 -0
  37. package/dist/chunks/prompts-1QbE_bRr.js +128 -0
  38. package/dist/chunks/prompts-1QbE_bRr.js.map +1 -0
  39. package/dist/chunks/repo-cloner-CpOQjFSo.js +219 -0
  40. package/dist/chunks/repo-cloner-CpOQjFSo.js.map +1 -0
  41. package/dist/chunks/repo-detector-B_oj696o.js +66 -0
  42. package/dist/chunks/repo-detector-B_oj696o.js.map +1 -0
  43. package/dist/chunks/run-D23hg4xy.js +630 -0
  44. package/dist/chunks/run-D23hg4xy.js.map +1 -0
  45. package/dist/chunks/runner-logger-instance-nDWv2h2T.js +899 -0
  46. package/dist/chunks/runner-logger-instance-nDWv2h2T.js.map +1 -0
  47. package/dist/chunks/spinner-BJL9zWAJ.js +53 -0
  48. package/dist/chunks/spinner-BJL9zWAJ.js.map +1 -0
  49. package/dist/chunks/start-BygPCbvw.js +1708 -0
  50. package/dist/chunks/start-BygPCbvw.js.map +1 -0
  51. package/dist/chunks/start-traditional-uoLZXdxm.js +255 -0
  52. package/dist/chunks/start-traditional-uoLZXdxm.js.map +1 -0
  53. package/dist/chunks/status-cS8YwtUx.js +97 -0
  54. package/dist/chunks/status-cS8YwtUx.js.map +1 -0
  55. package/dist/chunks/theme-DhorI2Hb.js +44 -0
  56. package/dist/chunks/theme-DhorI2Hb.js.map +1 -0
  57. package/dist/chunks/upgrade-CT6w0lKp.js +323 -0
  58. package/dist/chunks/upgrade-CT6w0lKp.js.map +1 -0
  59. package/dist/chunks/useBuildState-CdBSu9y_.js +331 -0
  60. package/dist/chunks/useBuildState-CdBSu9y_.js.map +1 -0
  61. package/dist/cli/index.js +694 -0
  62. package/dist/cli/index.js.map +1 -0
  63. package/dist/index.js +14358 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/instrument.js +64226 -0
  66. package/dist/instrument.js.map +1 -0
  67. package/dist/templates.json +295 -0
  68. package/package.json +98 -0
  69. package/scripts/install-vendor-deps.js +34 -0
  70. package/scripts/install-vendor.js +167 -0
  71. package/scripts/prepare-release.js +71 -0
  72. package/templates/config.template.json +18 -0
  73. package/templates.json +295 -0
  74. package/vendor/ai-sdk-provider-claude-code-LOCAL.tgz +0 -0
  75. package/vendor/sentry-core-LOCAL.tgz +0 -0
  76. package/vendor/sentry-nextjs-LOCAL.tgz +0 -0
  77. package/vendor/sentry-node-LOCAL.tgz +0 -0
  78. package/vendor/sentry-node-core-LOCAL.tgz +0 -0
@@ -0,0 +1,644 @@
1
+ // OpenBuilder CLI - Built with Rollup
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { useState, useRef, useEffect, useCallback } from 'react';
4
+ import { useInput, Box, Text, useStdout, render } from 'ink';
5
+ import { userInfo, homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { B as Banner } from './Banner-D4tqKfzA.js';
8
+ import TextInput from 'ink-text-input';
9
+ import 'node:fs';
10
+ import 'node:events';
11
+ import 'chalk';
12
+ import { c as colors, s as symbols } from './theme-DhorI2Hb.js';
13
+ import { c as configManager } from './config-manager-BkbjtN-H.js';
14
+ import 'node:child_process';
15
+ import 'node:url';
16
+ import 'conf';
17
+
18
+ /**
19
+ * Arrow-key navigable menu component
20
+ *
21
+ * > Initialize OpenBuilder
22
+ * Start Runner
23
+ * Exit
24
+ */
25
+ function Menu({ items, onSelect }) {
26
+ const [selectedIndex, setSelectedIndex] = useState(0);
27
+ useInput((input, key) => {
28
+ if (key.upArrow) {
29
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : items.length - 1));
30
+ }
31
+ else if (key.downArrow) {
32
+ setSelectedIndex(prev => (prev < items.length - 1 ? prev + 1 : 0));
33
+ }
34
+ else if (key.return) {
35
+ onSelect(items[selectedIndex]);
36
+ }
37
+ });
38
+ return (jsx(Box, { flexDirection: "column", alignItems: "flex-start", children: items.map((item, index) => {
39
+ const isSelected = index === selectedIndex;
40
+ return (jsxs(Box, { marginY: 0, children: [jsx(Text, { color: isSelected ? colors.cyan : colors.gray, children: isSelected ? '› ' : ' ' }), jsx(Text, { color: isSelected ? colors.white : colors.gray, bold: isSelected, children: item.label }), item.description && (jsxs(Text, { color: colors.dimGray, children: [' ', item.description] }))] }, item.id));
41
+ }) }));
42
+ }
43
+
44
+ /**
45
+ * Horizontal card selector component
46
+ *
47
+ * ┌─────────────────┐ ┌─────────────────┐
48
+ * │ Local Mode │ │ Runner Mode │
49
+ * │ │ │ │
50
+ * │ Run OpenBuilder │ │ Connect to a │
51
+ * │ locally │ │ remote server │
52
+ * └─────────────────┘ └─────────────────┘
53
+ * [SELECTED]
54
+ */
55
+ function HorizontalSelector({ options, onSelect, onEscape }) {
56
+ const [selectedIndex, setSelectedIndex] = useState(0);
57
+ useInput((input, key) => {
58
+ if (key.leftArrow) {
59
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : options.length - 1));
60
+ }
61
+ else if (key.rightArrow) {
62
+ setSelectedIndex(prev => (prev < options.length - 1 ? prev + 1 : 0));
63
+ }
64
+ else if (key.return) {
65
+ onSelect(options[selectedIndex]);
66
+ }
67
+ else if (key.escape && onEscape) {
68
+ onEscape();
69
+ }
70
+ });
71
+ return (jsxs(Box, { flexDirection: "column", alignItems: "center", children: [jsx(Box, { flexDirection: "row", gap: 2, children: options.map((option, index) => {
72
+ const isSelected = index === selectedIndex;
73
+ return (jsxs(Box, { flexDirection: "column", alignItems: "center", borderStyle: "round", borderColor: isSelected ? colors.cyan : colors.darkGray, paddingX: 3, paddingY: 1, width: 25, children: [jsx(Text, { color: isSelected ? colors.white : colors.gray, bold: isSelected, children: option.title }), jsx(Box, { marginTop: 1, children: jsx(Text, { color: isSelected ? colors.gray : colors.dimGray, wrap: "wrap", children: option.description }) })] }, option.id));
74
+ }) }), jsx(Box, { marginTop: 1, children: options.map((option, index) => (jsx(Box, { width: 25, justifyContent: "center", marginX: 1, children: index === selectedIndex && (jsx(Text, { color: colors.cyan, children: '▲' })) }, option.id))) })] }));
75
+ }
76
+
77
+ /**
78
+ * Masked text input for sensitive data like keys/passwords
79
+ * Shows first N characters (default: 5) then mask characters for the rest
80
+ * Supports paste (multi-character input) with buffering to prevent visual glitches
81
+ */
82
+ function MaskedInput({ value, onChange, placeholder = '', maskChar = '*', focused = false, visiblePrefixLength = 5, }) {
83
+ const [cursorVisible, setCursorVisible] = useState(true);
84
+ // Buffer to accumulate rapid input (for paste detection)
85
+ const inputBufferRef = useRef('');
86
+ const flushTimeoutRef = useRef(null);
87
+ // Keep current value in ref for use in timeout callbacks
88
+ const valueRef = useRef(value);
89
+ valueRef.current = value;
90
+ // Blink cursor when focused
91
+ useEffect(() => {
92
+ if (!focused) {
93
+ setCursorVisible(false);
94
+ return;
95
+ }
96
+ setCursorVisible(true);
97
+ const interval = setInterval(() => {
98
+ setCursorVisible(prev => !prev);
99
+ }, 500);
100
+ return () => clearInterval(interval);
101
+ }, [focused]);
102
+ // Flush buffered input - called after a brief delay to batch paste operations
103
+ const flushBuffer = useCallback(() => {
104
+ if (inputBufferRef.current) {
105
+ onChange(valueRef.current + inputBufferRef.current);
106
+ inputBufferRef.current = '';
107
+ }
108
+ }, [onChange]);
109
+ useInput((input, key) => {
110
+ if (!focused)
111
+ return;
112
+ if (key.backspace || key.delete) {
113
+ // Clear any pending buffer on backspace
114
+ if (flushTimeoutRef.current) {
115
+ clearTimeout(flushTimeoutRef.current);
116
+ flushTimeoutRef.current = null;
117
+ }
118
+ if (inputBufferRef.current) {
119
+ // Remove from buffer first
120
+ inputBufferRef.current = inputBufferRef.current.slice(0, -1);
121
+ if (!inputBufferRef.current) {
122
+ onChange(value.slice(0, -1));
123
+ }
124
+ }
125
+ else {
126
+ onChange(value.slice(0, -1));
127
+ }
128
+ }
129
+ else if (!key.escape && !key.return && !key.tab &&
130
+ !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow) {
131
+ // Allow any printable input including pasted text (multi-character)
132
+ // Filter out control characters but allow regular text
133
+ const printable = input.replace(/[\x00-\x1F\x7F]/g, '');
134
+ if (printable.length > 0) {
135
+ // Buffer the input for batching (helps with paste)
136
+ inputBufferRef.current += printable;
137
+ // Clear any existing timeout
138
+ if (flushTimeoutRef.current) {
139
+ clearTimeout(flushTimeoutRef.current);
140
+ }
141
+ // Set a short timeout to flush - if more input comes quickly (paste),
142
+ // it will be batched together
143
+ flushTimeoutRef.current = setTimeout(() => {
144
+ flushBuffer();
145
+ flushTimeoutRef.current = null;
146
+ }, 10);
147
+ }
148
+ }
149
+ }, { isActive: focused });
150
+ // Cleanup timeout on unmount
151
+ useEffect(() => {
152
+ return () => {
153
+ if (flushTimeoutRef.current) {
154
+ clearTimeout(flushTimeoutRef.current);
155
+ }
156
+ };
157
+ }, []);
158
+ // Show first N characters unmasked, mask the rest
159
+ const getDisplayValue = () => {
160
+ if (!value)
161
+ return '';
162
+ if (value.length <= visiblePrefixLength) {
163
+ return value;
164
+ }
165
+ const visiblePart = value.substring(0, visiblePrefixLength);
166
+ const maskedPart = maskChar.repeat(value.length - visiblePrefixLength);
167
+ return visiblePart + maskedPart;
168
+ };
169
+ const displayValue = getDisplayValue();
170
+ const cursor = focused && cursorVisible ? '│' : ' ';
171
+ if (!value && !focused) {
172
+ return (jsx(Text, { color: colors.dimGray, children: placeholder }));
173
+ }
174
+ return (jsxs(Text, { color: colors.white, children: [displayValue, focused && jsx(Text, { color: colors.cyan, children: cursor })] }));
175
+ }
176
+
177
+ /**
178
+ * Radio button group component
179
+ *
180
+ * ● Option 1
181
+ * ○ Option 2
182
+ */
183
+ function RadioGroup({ options, selected, onChange, focused = false }) {
184
+ useInput((input, key) => {
185
+ if (!focused)
186
+ return;
187
+ const currentIndex = options.findIndex(opt => opt.id === selected);
188
+ if (key.upArrow) {
189
+ const newIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1;
190
+ onChange(options[newIndex].id);
191
+ }
192
+ else if (key.downArrow) {
193
+ const newIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0;
194
+ onChange(options[newIndex].id);
195
+ }
196
+ else ;
197
+ }, { isActive: focused });
198
+ return (jsx(Box, { flexDirection: "column", children: options.map((option) => {
199
+ const isSelected = option.id === selected;
200
+ const isFocusedOption = focused && isSelected;
201
+ return (jsxs(Box, { marginY: 0, children: [jsx(Text, { color: isSelected ? colors.cyan : colors.gray, children: isSelected ? symbols.filledDot : symbols.hollowDot }), jsx(Text, { children: " " }), jsx(Text, { color: isFocusedOption ? colors.white : (isSelected ? colors.gray : colors.dimGray), bold: isFocusedOption, children: option.label })] }, option.id));
202
+ }) }));
203
+ }
204
+
205
+ const modeOptions = [
206
+ {
207
+ id: 'local',
208
+ title: 'Local Mode',
209
+ description: 'Run OpenBuilder on this machine',
210
+ },
211
+ {
212
+ id: 'runner',
213
+ title: 'Runner Mode',
214
+ description: 'Connect to a remote server',
215
+ },
216
+ ];
217
+ /**
218
+ * Initial mode selection screen
219
+ * Shows two horizontal cards for Local Mode vs Runner Mode
220
+ */
221
+ function ModeSelectScreen({ onSelect, onEscape }) {
222
+ const { stdout } = useStdout();
223
+ // Check for available update (set by auto-update check in index.ts)
224
+ const updateAvailable = process.env.OPENBUILDER_UPDATE_AVAILABLE;
225
+ // Calculate vertical centering
226
+ const terminalHeight = stdout?.rows || 24;
227
+ const contentHeight = updateAvailable ? 20 : 18; // Extra space for update notice
228
+ const topPadding = Math.max(0, Math.floor((terminalHeight - contentHeight) / 3));
229
+ const handleSelect = (option) => {
230
+ onSelect(option.id);
231
+ };
232
+ return (jsxs(Box, { flexDirection: "column", alignItems: "center", paddingTop: topPadding, children: [jsx(Banner, {}), updateAvailable && (jsxs(Box, { marginTop: 1, children: [jsx(Text, { color: colors.cyan, children: "\u2B06 Update available: " }), jsx(Text, { color: colors.success, children: updateAvailable }), jsx(Text, { color: colors.dimGray, children: " \u2014 Run " }), jsx(Text, { color: colors.cyan, children: "openbuilder upgrade" }), jsx(Text, { color: colors.dimGray, children: " to update" })] })), jsx(Box, { marginTop: 2 }), jsx(HorizontalSelector, { options: modeOptions, onSelect: handleSelect, onEscape: onEscape }), jsx(Box, { marginTop: 2 }), jsxs(Text, { color: colors.dimGray, children: ["Use ", '<-', " ", '->', " arrows to navigate, Enter to select, Esc to exit"] })] }));
233
+ }
234
+
235
+ /**
236
+ * Local mode options screen
237
+ * Shows Initialize/Reinitialize and Start options based on config state
238
+ */
239
+ function LocalModeScreen({ isInitialized, onSelect, onEscape }) {
240
+ const { stdout } = useStdout();
241
+ // Calculate vertical centering
242
+ const terminalHeight = stdout?.rows || 24;
243
+ const contentHeight = 16;
244
+ const topPadding = Math.max(0, Math.floor((terminalHeight - contentHeight) / 3));
245
+ // Handle escape key
246
+ useInput((input, key) => {
247
+ if (key.escape) {
248
+ onEscape();
249
+ }
250
+ });
251
+ // Build menu items based on initialization state
252
+ const menuItems = [];
253
+ if (!isInitialized) {
254
+ menuItems.push({
255
+ id: 'init',
256
+ label: 'Initialize OpenBuilder',
257
+ description: 'Set up workspace and configuration',
258
+ });
259
+ }
260
+ else {
261
+ menuItems.push({
262
+ id: 'init',
263
+ label: 'Reinitialize OpenBuilder',
264
+ description: 'Reset and reconfigure',
265
+ });
266
+ menuItems.push({
267
+ id: 'start',
268
+ label: 'Start OpenBuilder',
269
+ description: 'Launch the full stack',
270
+ });
271
+ }
272
+ const handleSelect = (item) => {
273
+ onSelect(item.id);
274
+ };
275
+ return (jsxs(Box, { flexDirection: "column", alignItems: "center", paddingTop: topPadding, children: [jsx(Banner, {}), jsx(Box, { marginTop: 1 }), jsx(Text, { color: colors.cyan, bold: true, children: "Local Mode" }), jsx(Box, { marginTop: 1 }), isInitialized ? (jsx(Text, { color: colors.success, children: "\u25CF Configured" })) : (jsx(Text, { color: colors.warning, children: "\u25CB Not configured" })), jsx(Box, { marginTop: 2 }), jsx(Menu, { items: menuItems, onSelect: handleSelect }), jsx(Box, { marginTop: 2 }), jsx(Text, { color: colors.dimGray, children: "Use up/down arrows to navigate, Enter to select, Esc to go back" })] }));
276
+ }
277
+
278
+ // Fixed label width for alignment
279
+ const LABEL_WIDTH$1 = 14;
280
+ /**
281
+ * Runner mode screen with key and runner ID inputs
282
+ */
283
+ function RunnerModeScreen({ initialKey = '', initialRunnerId = '', onStart, onEscape, }) {
284
+ const { stdout } = useStdout();
285
+ const [runnerId, setRunnerId] = useState(initialRunnerId);
286
+ const [focusedField, setFocusedField] = useState('key');
287
+ // We need a separate state for the actual key value since MaskedInput doesn't use ink-text-input
288
+ const [runnerKey, setRunnerKey] = useState(initialKey);
289
+ // Calculate vertical centering
290
+ const terminalHeight = stdout?.rows || 24;
291
+ const contentHeight = 18;
292
+ const topPadding = Math.max(0, Math.floor((terminalHeight - contentHeight) / 3));
293
+ const isLastField = focusedField === 'runnerId';
294
+ const handleSubmit = () => {
295
+ if (runnerKey.trim()) {
296
+ onStart({ key: runnerKey, runnerId: runnerId.trim() || initialRunnerId });
297
+ }
298
+ };
299
+ useInput((input, key) => {
300
+ if (key.escape) {
301
+ onEscape();
302
+ return;
303
+ }
304
+ // Shift+Enter to submit immediately from any field
305
+ if (key.return && key.shift) {
306
+ handleSubmit();
307
+ return;
308
+ }
309
+ // Regular Enter moves to next field, or submits if on last field
310
+ if (key.return && !key.shift) {
311
+ if (isLastField) {
312
+ handleSubmit();
313
+ }
314
+ else {
315
+ setFocusedField('runnerId');
316
+ }
317
+ return;
318
+ }
319
+ if (key.tab || key.downArrow) {
320
+ setFocusedField(prev => prev === 'key' ? 'runnerId' : 'key');
321
+ return;
322
+ }
323
+ if (key.upArrow) {
324
+ setFocusedField(prev => prev === 'runnerId' ? 'key' : 'runnerId');
325
+ return;
326
+ }
327
+ });
328
+ const handleKeyChange = (value) => {
329
+ setRunnerKey(value);
330
+ };
331
+ return (jsxs(Box, { flexDirection: "column", alignItems: "center", paddingTop: topPadding, children: [jsx(Banner, {}), jsx(Box, { marginTop: 1 }), jsx(Text, { color: colors.purple, bold: true, children: "Runner Mode" }), jsx(Box, { marginTop: 2 }), jsxs(Box, { flexDirection: "column", gap: 1, children: [jsxs(Box, { flexDirection: "row", alignItems: "center", children: [jsx(Box, { width: LABEL_WIDTH$1, justifyContent: "flex-end", marginRight: 1, children: jsx(Text, { color: focusedField === 'key' ? colors.cyan : colors.gray, children: "Runner Key" }) }), jsx(Box, { borderStyle: "round", borderColor: focusedField === 'key' ? colors.cyan : colors.darkGray, paddingX: 1, width: 40, children: jsx(MaskedInput, { value: runnerKey, onChange: handleKeyChange, placeholder: "Paste your runner key", focused: focusedField === 'key' }) })] }), initialKey && focusedField === 'key' && (jsx(Box, { marginLeft: LABEL_WIDTH$1 + 2, children: jsx(Text, { color: colors.dimGray, italic: true, children: "(auto-filled from previous config)" }) })), jsxs(Box, { flexDirection: "row", alignItems: "center", children: [jsx(Box, { width: LABEL_WIDTH$1, justifyContent: "flex-end", marginRight: 1, children: jsx(Text, { color: focusedField === 'runnerId' ? colors.cyan : colors.gray, children: "Runner ID" }) }), jsx(Box, { borderStyle: "round", borderColor: focusedField === 'runnerId' ? colors.cyan : colors.darkGray, paddingX: 1, width: 40, children: focusedField === 'runnerId' ? (jsx(TextInput, { value: runnerId, onChange: setRunnerId, placeholder: initialRunnerId || 'Enter runner ID' })) : (jsx(Text, { color: runnerId ? colors.white : colors.dimGray, children: runnerId || initialRunnerId || 'Enter runner ID' })) })] })] }), jsx(Box, { marginTop: 3 }), jsxs(Text, { color: colors.dimGray, children: ["Enter: ", isLastField ? 'Start runner' : 'Next field', " | Shift+Enter: Start now | Esc: Back"] }), !runnerKey.trim() && (jsx(Box, { marginTop: 1, children: jsx(Text, { color: colors.warning, children: "Runner key is required" }) }))] }));
332
+ }
333
+
334
+ const databaseOptions = [
335
+ { id: 'neon', label: 'Use Neon (automatic setup)' },
336
+ { id: 'custom', label: 'Custom PostgreSQL' },
337
+ ];
338
+ // Fixed label width for alignment
339
+ const LABEL_WIDTH = 14;
340
+ /**
341
+ * Interactive configuration form for init/reinit
342
+ */
343
+ function ConfigFormScreen({ initialConfig, onSubmit, onEscape, error, }) {
344
+ const { stdout } = useStdout();
345
+ // Form state
346
+ const [branch, setBranch] = useState(initialConfig?.branch || 'main');
347
+ // Clear error when user starts typing in branch field
348
+ const handleBranchChange = (value) => {
349
+ setBranch(value);
350
+ };
351
+ const [workspace, setWorkspace] = useState(initialConfig?.workspace || '~/openbuilder-workspace');
352
+ const [databaseType, setDatabaseType] = useState(initialConfig?.useNeon === false ? 'custom' : 'neon');
353
+ const [databaseUrl, setDatabaseUrl] = useState(initialConfig?.databaseUrl || '');
354
+ const [focusedField, setFocusedField] = useState('branch');
355
+ // Calculate vertical centering
356
+ const terminalHeight = stdout?.rows || 24;
357
+ const contentHeight = 22;
358
+ const topPadding = Math.max(0, Math.floor((terminalHeight - contentHeight) / 3));
359
+ // Field order for navigation
360
+ const fieldOrder = databaseType === 'custom'
361
+ ? ['branch', 'workspace', 'database', 'databaseUrl']
362
+ : ['branch', 'workspace', 'database'];
363
+ const currentFieldIndex = fieldOrder.indexOf(focusedField);
364
+ const isLastField = currentFieldIndex === fieldOrder.length - 1;
365
+ useInput((input, key) => {
366
+ if (key.escape) {
367
+ onEscape();
368
+ return;
369
+ }
370
+ // Shift+Enter to submit immediately from any field
371
+ if (key.return && key.shift) {
372
+ handleSubmit();
373
+ return;
374
+ }
375
+ // Regular Enter moves to next field, or submits if on last field
376
+ if (key.return && !key.shift) {
377
+ if (isLastField) {
378
+ handleSubmit();
379
+ }
380
+ else {
381
+ setFocusedField(fieldOrder[currentFieldIndex + 1]);
382
+ }
383
+ return;
384
+ }
385
+ // Tab or Down arrow moves to next field
386
+ if (key.tab || key.downArrow) {
387
+ if (focusedField !== 'database') {
388
+ const nextIndex = (currentFieldIndex + 1) % fieldOrder.length;
389
+ setFocusedField(fieldOrder[nextIndex]);
390
+ }
391
+ return;
392
+ }
393
+ // Up arrow moves to previous field
394
+ if (key.upArrow) {
395
+ if (focusedField !== 'database') {
396
+ const prevIndex = currentFieldIndex > 0 ? currentFieldIndex - 1 : fieldOrder.length - 1;
397
+ setFocusedField(fieldOrder[prevIndex]);
398
+ }
399
+ return;
400
+ }
401
+ });
402
+ const handleSubmit = () => {
403
+ const config = {
404
+ branch: branch.trim() || 'main',
405
+ workspace: workspace.trim() || '~/openbuilder-workspace',
406
+ useNeon: databaseType === 'neon',
407
+ databaseUrl: databaseType === 'custom' ? databaseUrl.trim() : undefined,
408
+ };
409
+ onSubmit(config);
410
+ };
411
+ const handleDatabaseTypeChange = (id) => {
412
+ setDatabaseType(id);
413
+ // If switching to custom, focus the URL field
414
+ if (id === 'custom') {
415
+ setFocusedField('databaseUrl');
416
+ }
417
+ };
418
+ return (jsxs(Box, { flexDirection: "column", alignItems: "center", paddingTop: topPadding, children: [jsx(Banner, {}), jsx(Box, { marginTop: 1 }), jsx(Text, { color: colors.cyan, bold: true, children: "Configure OpenBuilder" }), jsx(Box, { marginTop: 2 }), jsxs(Box, { flexDirection: "column", gap: 1, children: [jsxs(Box, { flexDirection: "column", children: [jsxs(Box, { flexDirection: "row", alignItems: "center", children: [jsx(Box, { width: LABEL_WIDTH, justifyContent: "flex-end", marginRight: 1, children: jsx(Text, { color: error ? colors.error : (focusedField === 'branch' ? colors.cyan : colors.gray), children: "Branch" }) }), jsx(Box, { borderStyle: "round", borderColor: error ? colors.error : (focusedField === 'branch' ? colors.cyan : colors.darkGray), paddingX: 1, children: focusedField === 'branch' ? (jsx(TextInput, { value: branch, onChange: handleBranchChange, placeholder: "main" })) : (jsx(Text, { color: branch ? colors.white : colors.dimGray, children: branch || 'main' })) })] }), error && (jsx(Box, { marginLeft: LABEL_WIDTH + 2, children: jsxs(Text, { color: colors.error, children: [symbols.cross, " ", error] }) }))] }), jsxs(Box, { flexDirection: "row", alignItems: "center", children: [jsx(Box, { width: LABEL_WIDTH, justifyContent: "flex-end", marginRight: 1, children: jsx(Text, { color: focusedField === 'workspace' ? colors.cyan : colors.gray, children: "Workspace" }) }), jsx(Box, { borderStyle: "round", borderColor: focusedField === 'workspace' ? colors.cyan : colors.darkGray, paddingX: 1, width: 40, children: focusedField === 'workspace' ? (jsx(TextInput, { value: workspace, onChange: setWorkspace, placeholder: "~/openbuilder-workspace" })) : (jsx(Text, { color: workspace ? colors.white : colors.dimGray, children: workspace || '~/openbuilder-workspace' })) })] }), jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "center", children: [jsx(Box, { width: LABEL_WIDTH, justifyContent: "flex-end", marginRight: 1, children: jsx(Text, { color: focusedField === 'database' ? colors.cyan : colors.gray, children: "Database" }) }), jsx(RadioGroup, { options: databaseOptions, selected: databaseType, onChange: handleDatabaseTypeChange, focused: focusedField === 'database' })] }), databaseType === 'custom' && (jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "center", children: [jsx(Box, { width: LABEL_WIDTH, justifyContent: "flex-end", marginRight: 1, children: jsx(Text, { color: focusedField === 'databaseUrl' ? colors.cyan : colors.gray, children: "URL" }) }), jsx(Box, { borderStyle: "round", borderColor: focusedField === 'databaseUrl' ? colors.cyan : colors.darkGray, paddingX: 1, width: 50, children: focusedField === 'databaseUrl' ? (jsx(TextInput, { value: databaseUrl, onChange: setDatabaseUrl, placeholder: "postgres://user:pass@host:5432/db" })) : (jsx(Text, { color: databaseUrl ? colors.white : colors.dimGray, children: databaseUrl || 'postgres://user:pass@host:5432/db' })) })] }))] }), jsx(Box, { marginTop: 3 }), jsxs(Box, { flexDirection: "column", alignItems: "center", children: [jsxs(Text, { color: colors.dimGray, children: ["Enter: ", isLastField ? 'Start' : 'Next field', " | Shift+Enter: Start now | Esc: Back"] }), databaseType === 'custom' && !databaseUrl.trim() && (jsx(Box, { marginTop: 1, children: jsxs(Text, { color: colors.warning, children: [symbols.hollowDot, " Database URL required for custom PostgreSQL"] }) }))] })] }));
419
+ }
420
+
421
+ /**
422
+ * Main TUI App component with screen navigation
423
+ */
424
+ function App({ initialState, onExit, onRunnerStart, onLocalStart, onInitStart, }) {
425
+ const [state, setState] = useState(initialState);
426
+ // Screen navigation handlers
427
+ const navigateTo = (screen) => {
428
+ setState(prev => ({ ...prev, screen }));
429
+ };
430
+ // Navigate back to config form with error
431
+ const navigateToConfigWithError = (error, lastBranch) => {
432
+ setState(prev => ({
433
+ ...prev,
434
+ screen: { type: 'config-form', error, lastBranch }
435
+ }));
436
+ };
437
+ // Mode Select handlers
438
+ const handleModeSelect = (mode) => {
439
+ if (mode === 'local') {
440
+ navigateTo({ type: 'local-mode' });
441
+ }
442
+ else {
443
+ navigateTo({ type: 'runner-mode' });
444
+ }
445
+ };
446
+ // Local Mode handlers
447
+ const handleLocalAction = (action) => {
448
+ if (action === 'init') {
449
+ navigateTo({ type: 'config-form' });
450
+ }
451
+ else if (action === 'start') {
452
+ onLocalStart();
453
+ }
454
+ };
455
+ // Runner Mode handler
456
+ const handleRunnerStart = (config) => {
457
+ onRunnerStart(config);
458
+ };
459
+ // Config Form handler - validates branch before proceeding
460
+ const handleConfigSubmit = async (config) => {
461
+ // Skip validation if using main branch (always exists)
462
+ if (config.branch === 'main') {
463
+ onInitStart(config);
464
+ return;
465
+ }
466
+ // Validate branch exists before proceeding
467
+ const { execSync } = await import('child_process');
468
+ try {
469
+ execSync(`git ls-remote --exit-code --heads https://github.com/codyde/openbuilder.git ${config.branch}`, { stdio: 'pipe' });
470
+ // Branch exists, proceed with init
471
+ onInitStart(config);
472
+ }
473
+ catch {
474
+ // Branch doesn't exist, show error
475
+ navigateToConfigWithError(`Branch "${config.branch}" not found`, config.branch);
476
+ }
477
+ };
478
+ // Render current screen
479
+ switch (state.screen.type) {
480
+ case 'mode-select':
481
+ return (jsx(ModeSelectScreen, { onSelect: handleModeSelect, onEscape: onExit }));
482
+ case 'local-mode':
483
+ return (jsx(LocalModeScreen, { isInitialized: state.isInitialized, onSelect: handleLocalAction, onEscape: () => navigateTo({ type: 'mode-select' }) }));
484
+ case 'runner-mode':
485
+ return (jsx(RunnerModeScreen, { initialKey: state.existingKey, initialRunnerId: state.existingRunnerId, onStart: handleRunnerStart, onEscape: () => navigateTo({ type: 'mode-select' }) }));
486
+ case 'config-form':
487
+ return (jsx(ConfigFormScreen, { initialConfig: {
488
+ branch: state.screen.lastBranch || 'main',
489
+ workspace: state.existingWorkspace || join(homedir(), 'openbuilder-workspace'),
490
+ useNeon: true,
491
+ }, onSubmit: handleConfigSubmit, onEscape: () => navigateTo({ type: 'local-mode' }), error: state.screen.error }));
492
+ default:
493
+ return null;
494
+ }
495
+ }
496
+ /**
497
+ * Get system username for default runner ID
498
+ */
499
+ function getSystemUsername() {
500
+ try {
501
+ return userInfo().username;
502
+ }
503
+ catch {
504
+ return process.env.USER || process.env.USERNAME || 'runner';
505
+ }
506
+ }
507
+ /**
508
+ * Run the main TUI menu
509
+ */
510
+ async function mainTUICommand() {
511
+ // Clear screen for fullscreen experience
512
+ console.clear();
513
+ const isInitialized = configManager.isInitialized();
514
+ const existingKey = configManager.getSecret() || '';
515
+ const config = configManager.get();
516
+ // Use lastRunnerId if available, otherwise fall back to system username
517
+ const existingRunnerId = config.runner?.lastRunnerId || getSystemUsername();
518
+ const existingWorkspace = config.workspace || '';
519
+ const initialState = {
520
+ screen: { type: 'mode-select' },
521
+ isInitialized,
522
+ existingKey,
523
+ existingRunnerId,
524
+ existingWorkspace,
525
+ };
526
+ return new Promise((resolve, reject) => {
527
+ let exitReason = null;
528
+ let runnerConfig = null;
529
+ let initConfig = null;
530
+ const { unmount, waitUntilExit } = render(jsx(App, { initialState: initialState, onExit: () => {
531
+ exitReason = 'exit';
532
+ unmount();
533
+ }, onRunnerStart: (config) => {
534
+ exitReason = 'runner-start';
535
+ runnerConfig = config;
536
+ unmount();
537
+ }, onLocalStart: () => {
538
+ exitReason = 'local-start';
539
+ unmount();
540
+ }, onInitStart: (config) => {
541
+ exitReason = 'init-start';
542
+ initConfig = config;
543
+ unmount();
544
+ } }), {
545
+ exitOnCtrlC: true,
546
+ });
547
+ waitUntilExit().then(async () => {
548
+ if (!exitReason) {
549
+ // User pressed Ctrl+C
550
+ console.clear();
551
+ process.exit(0);
552
+ }
553
+ switch (exitReason) {
554
+ case 'exit':
555
+ console.clear();
556
+ console.log('\n Goodbye!\n');
557
+ process.exit(0);
558
+ break;
559
+ case 'runner-start':
560
+ if (runnerConfig) {
561
+ console.clear();
562
+ await startRunner(runnerConfig);
563
+ }
564
+ break;
565
+ case 'local-start':
566
+ console.clear();
567
+ await startLocalMode();
568
+ break;
569
+ case 'init-start':
570
+ if (initConfig) {
571
+ // Clear screen with ANSI codes to ensure clean slate
572
+ process.stdout.write('\x1b[2J\x1b[H');
573
+ await runInitialization(initConfig);
574
+ }
575
+ break;
576
+ }
577
+ resolve();
578
+ }).catch(reject);
579
+ });
580
+ }
581
+ /**
582
+ * Start runner mode (connects to remote server)
583
+ */
584
+ async function startRunner(config) {
585
+ // Save the key to config for future use
586
+ if (config.key) {
587
+ const serverConfig = configManager.get('server') || {};
588
+ configManager.set('server', {
589
+ ...serverConfig,
590
+ secret: config.key,
591
+ });
592
+ }
593
+ // Save the runner ID to config for future use
594
+ if (config.runnerId) {
595
+ const runnerConf = configManager.get('runner') || {};
596
+ configManager.set('runner', {
597
+ ...runnerConf,
598
+ lastRunnerId: config.runnerId,
599
+ });
600
+ }
601
+ console.log('\n Starting OpenBuilder Runner...\n');
602
+ console.log(` Runner ID: ${config.runnerId}`);
603
+ console.log(' Connecting to remote server...\n');
604
+ const { runCommand } = await import('./run-D23hg4xy.js');
605
+ await runCommand({
606
+ secret: config.key,
607
+ runnerId: config.runnerId,
608
+ });
609
+ }
610
+ /**
611
+ * Start local mode (full stack)
612
+ */
613
+ async function startLocalMode() {
614
+ console.log('\n Starting OpenBuilder in Local Mode...\n');
615
+ const { startCommand } = await import('./start-BygPCbvw.js');
616
+ await startCommand({});
617
+ }
618
+ /**
619
+ * Run initialization with form config
620
+ */
621
+ async function runInitialization(config) {
622
+ // Expand ~ in workspace path
623
+ const workspace = config.workspace.startsWith('~')
624
+ ? config.workspace.replace('~', homedir())
625
+ : config.workspace;
626
+ const { initTUICommand } = await import('./init-tui-BNzk_7Yx.js');
627
+ // Build options based on form input
628
+ const options = {
629
+ workspace,
630
+ branch: config.branch,
631
+ yes: true,
632
+ };
633
+ // Handle database option
634
+ if (config.useNeon) {
635
+ options.database = true; // Use Neon auto-setup
636
+ }
637
+ else if (config.databaseUrl) {
638
+ options.database = config.databaseUrl; // Custom connection string
639
+ }
640
+ await initTUICommand(options);
641
+ }
642
+
643
+ export { mainTUICommand };
644
+ //# sourceMappingURL=main-tui-Cq1hLCx-.js.map