@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.
- package/README.md +1053 -0
- package/bin/openbuilder.js +31 -0
- package/dist/chunks/Banner-D4tqKfzA.js +113 -0
- package/dist/chunks/Banner-D4tqKfzA.js.map +1 -0
- package/dist/chunks/auto-update-Dj3lWPWO.js +350 -0
- package/dist/chunks/auto-update-Dj3lWPWO.js.map +1 -0
- package/dist/chunks/build-D0qYqIq0.js +116 -0
- package/dist/chunks/build-D0qYqIq0.js.map +1 -0
- package/dist/chunks/cleanup-qVTsA3tk.js +141 -0
- package/dist/chunks/cleanup-qVTsA3tk.js.map +1 -0
- package/dist/chunks/cli-error-BjQwvWtK.js +140 -0
- package/dist/chunks/cli-error-BjQwvWtK.js.map +1 -0
- package/dist/chunks/config-BGP1jZJ4.js +167 -0
- package/dist/chunks/config-BGP1jZJ4.js.map +1 -0
- package/dist/chunks/config-manager-BkbjtN-H.js +133 -0
- package/dist/chunks/config-manager-BkbjtN-H.js.map +1 -0
- package/dist/chunks/database-BvAbD4sP.js +68 -0
- package/dist/chunks/database-BvAbD4sP.js.map +1 -0
- package/dist/chunks/database-setup-BYjIRAmT.js +253 -0
- package/dist/chunks/database-setup-BYjIRAmT.js.map +1 -0
- package/dist/chunks/exports-ij9sv4UM.js +7793 -0
- package/dist/chunks/exports-ij9sv4UM.js.map +1 -0
- package/dist/chunks/init-CZoN6soU.js +468 -0
- package/dist/chunks/init-CZoN6soU.js.map +1 -0
- package/dist/chunks/init-tui-BNzk_7Yx.js +1127 -0
- package/dist/chunks/init-tui-BNzk_7Yx.js.map +1 -0
- package/dist/chunks/logger-ZpJi7chw.js +38 -0
- package/dist/chunks/logger-ZpJi7chw.js.map +1 -0
- package/dist/chunks/main-tui-Cq1hLCx-.js +644 -0
- package/dist/chunks/main-tui-Cq1hLCx-.js.map +1 -0
- package/dist/chunks/manager-CvGX9qqe.js +1161 -0
- package/dist/chunks/manager-CvGX9qqe.js.map +1 -0
- package/dist/chunks/port-allocator-BRFzgH9b.js +749 -0
- package/dist/chunks/port-allocator-BRFzgH9b.js.map +1 -0
- package/dist/chunks/process-killer-CaUL7Kpl.js +87 -0
- package/dist/chunks/process-killer-CaUL7Kpl.js.map +1 -0
- package/dist/chunks/prompts-1QbE_bRr.js +128 -0
- package/dist/chunks/prompts-1QbE_bRr.js.map +1 -0
- package/dist/chunks/repo-cloner-CpOQjFSo.js +219 -0
- package/dist/chunks/repo-cloner-CpOQjFSo.js.map +1 -0
- package/dist/chunks/repo-detector-B_oj696o.js +66 -0
- package/dist/chunks/repo-detector-B_oj696o.js.map +1 -0
- package/dist/chunks/run-D23hg4xy.js +630 -0
- package/dist/chunks/run-D23hg4xy.js.map +1 -0
- package/dist/chunks/runner-logger-instance-nDWv2h2T.js +899 -0
- package/dist/chunks/runner-logger-instance-nDWv2h2T.js.map +1 -0
- package/dist/chunks/spinner-BJL9zWAJ.js +53 -0
- package/dist/chunks/spinner-BJL9zWAJ.js.map +1 -0
- package/dist/chunks/start-BygPCbvw.js +1708 -0
- package/dist/chunks/start-BygPCbvw.js.map +1 -0
- package/dist/chunks/start-traditional-uoLZXdxm.js +255 -0
- package/dist/chunks/start-traditional-uoLZXdxm.js.map +1 -0
- package/dist/chunks/status-cS8YwtUx.js +97 -0
- package/dist/chunks/status-cS8YwtUx.js.map +1 -0
- package/dist/chunks/theme-DhorI2Hb.js +44 -0
- package/dist/chunks/theme-DhorI2Hb.js.map +1 -0
- package/dist/chunks/upgrade-CT6w0lKp.js +323 -0
- package/dist/chunks/upgrade-CT6w0lKp.js.map +1 -0
- package/dist/chunks/useBuildState-CdBSu9y_.js +331 -0
- package/dist/chunks/useBuildState-CdBSu9y_.js.map +1 -0
- package/dist/cli/index.js +694 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.js +14358 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.js +64226 -0
- package/dist/instrument.js.map +1 -0
- package/dist/templates.json +295 -0
- package/package.json +98 -0
- package/scripts/install-vendor-deps.js +34 -0
- package/scripts/install-vendor.js +167 -0
- package/scripts/prepare-release.js +71 -0
- package/templates/config.template.json +18 -0
- package/templates.json +295 -0
- package/vendor/ai-sdk-provider-claude-code-LOCAL.tgz +0 -0
- package/vendor/sentry-core-LOCAL.tgz +0 -0
- package/vendor/sentry-nextjs-LOCAL.tgz +0 -0
- package/vendor/sentry-node-LOCAL.tgz +0 -0
- 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
|