@hatchway/cli 0.50.53

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