@hatchway/cli 0.50.71 → 0.50.73

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