@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,1131 @@
1
+ // Hatchway CLI - Built with Rollup
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { existsSync, rmSync, realpathSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { randomBytes } from 'node:crypto';
6
+ import { j as jsxRuntimeExports, B as Box, r as reactExports, T as Text, s as symbols, c as colors, l as layout, a as useStdout, u as useInput, b as render } from './theme-NAQBkisB.js';
7
+ import 'chalk';
8
+ import 'node:stream';
9
+ import 'node:process';
10
+ import 'node:events';
11
+ import { u as useApp } from './use-app-Ct3w2jLI.js';
12
+ import { g as getVersionInfo, B as Banner } from './Banner-DL1Fpz_g.js';
13
+ import 'node:http';
14
+ import 'node:url';
15
+ import 'node:os';
16
+ import 'node:child_process';
17
+ import { c as configManager } from './config-manager-DST6RbP8.js';
18
+ import { i as isInsideMonorepo } from './repo-detector-36VydrlB.js';
19
+ import { isPnpmInstalled, cloneRepository, installDependencies, buildAgentCore } from './repo-cloner-UY3L2X7h.js';
20
+ import { p as pushDatabaseSchema, s as setupDatabase } from './database-setup-U31oEs90.js';
21
+ import { C as CLIError } from './cli-error-1drkrXNn.js';
22
+ import 'assert';
23
+ import 'events';
24
+ import 'module';
25
+ import 'node:buffer';
26
+ import 'conf';
27
+ import './logger-6V5cBxba.js';
28
+ import './spinner-DTH0QZQw.js';
29
+ import 'ora';
30
+ import './prompts-Beijr8dm.js';
31
+ import 'inquirer';
32
+
33
+ /**
34
+ * Horizontal progress stepper - just the dots and labels
35
+ *
36
+ * ●───────────●───────────○───────────○
37
+ * Setup Install Database Finalize
38
+ */
39
+ function ProgressStepper({ steps }) {
40
+ const getStepColor = (status) => {
41
+ switch (status) {
42
+ case 'completed':
43
+ return colors.success;
44
+ case 'active':
45
+ return colors.cyan;
46
+ case 'error':
47
+ return colors.error;
48
+ default:
49
+ return colors.dimGray;
50
+ }
51
+ };
52
+ const getStepSymbol = (status) => {
53
+ switch (status) {
54
+ case 'error':
55
+ return symbols.errorDot;
56
+ case 'completed':
57
+ case 'active':
58
+ return symbols.filledDot;
59
+ default:
60
+ return symbols.hollowDot;
61
+ }
62
+ };
63
+ // Cell and connector sizing
64
+ const cellWidth = 10;
65
+ const connectorWidth = 7;
66
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", children: [jsxRuntimeExports.jsx(Box, { children: steps.map((step, index) => (jsxRuntimeExports.jsxs(reactExports.Fragment, { children: [jsxRuntimeExports.jsx(Box, { width: cellWidth, justifyContent: "center", children: jsxRuntimeExports.jsx(Text, { color: getStepColor(step.status), children: getStepSymbol(step.status) }) }), index < steps.length - 1 && (jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: symbols.horizontalLine.repeat(connectorWidth) }))] }, step.id))) }), jsxRuntimeExports.jsx(Box, { children: steps.map((step, index) => (jsxRuntimeExports.jsxs(reactExports.Fragment, { children: [jsxRuntimeExports.jsx(Box, { width: cellWidth, justifyContent: "center", children: jsxRuntimeExports.jsx(Text, { color: step.status === 'pending' ? colors.dimGray : colors.gray, dimColor: step.status === 'pending', children: step.label }) }), index < steps.length - 1 && (jsxRuntimeExports.jsx(Box, { width: connectorWidth }))] }, step.id))) })] }));
67
+ }
68
+
69
+ // Match the stepper width: cellWidth(10) * 4 + connectorWidth(7) * 3 = 61
70
+ const BOX_WIDTH = 61;
71
+ const BOX_INNER_WIDTH = BOX_WIDTH - 4; // Account for "│ " and " │"
72
+ /**
73
+ * TaskStream - Shows current step's tasks in a terminal-style box
74
+ *
75
+ * ┌───────────────────────────────────────────────────────────┐
76
+ * │ ⠋ Configuring database │
77
+ * │ └── Setting up Neon database...▌ │
78
+ * └───────────────────────────────────────────────────────────┘
79
+ */
80
+ function TaskStream({ stepId, tasks, onTypewriterComplete }) {
81
+ const [spinnerIndex, setSpinnerIndex] = reactExports.useState(0);
82
+ const [cursorVisible, setCursorVisible] = reactExports.useState(true);
83
+ const [typedChars, setTypedChars] = reactExports.useState({});
84
+ const [completedTyping, setCompletedTyping] = reactExports.useState(new Set());
85
+ const prevStepRef = reactExports.useRef(stepId);
86
+ // Reset typed chars when step changes
87
+ reactExports.useEffect(() => {
88
+ if (stepId !== prevStepRef.current) {
89
+ setTypedChars({});
90
+ setCompletedTyping(new Set());
91
+ prevStepRef.current = stepId;
92
+ }
93
+ }, [stepId]);
94
+ // Spinner animation - runs independently
95
+ reactExports.useEffect(() => {
96
+ let mounted = true;
97
+ const animate = () => {
98
+ if (!mounted)
99
+ return;
100
+ setSpinnerIndex(prev => (prev + 1) % symbols.spinnerFrames.length);
101
+ setTimeout(animate, layout.spinnerInterval);
102
+ };
103
+ const timeout = setTimeout(animate, layout.spinnerInterval);
104
+ return () => {
105
+ mounted = false;
106
+ clearTimeout(timeout);
107
+ };
108
+ }, []);
109
+ // Cursor blink - runs independently
110
+ reactExports.useEffect(() => {
111
+ let mounted = true;
112
+ const blink = () => {
113
+ if (!mounted)
114
+ return;
115
+ setCursorVisible(prev => !prev);
116
+ setTimeout(blink, 400);
117
+ };
118
+ const timeout = setTimeout(blink, 400);
119
+ return () => {
120
+ mounted = false;
121
+ clearTimeout(timeout);
122
+ };
123
+ }, []);
124
+ // Find tasks by status
125
+ const runningTask = tasks.find(t => t.status === 'running');
126
+ const failedTask = tasks.find(t => t.status === 'failed');
127
+ const completedTasks = tasks.filter(t => t.status === 'completed');
128
+ // Primary task: running > failed > last completed (to prevent visual jump)
129
+ const primaryTask = runningTask || failedTask || completedTasks[completedTasks.length - 1];
130
+ // Previous completed tasks (all except the one shown as primary)
131
+ const previousCompleted = primaryTask?.status === 'completed'
132
+ ? completedTasks.slice(0, -1)
133
+ : completedTasks;
134
+ // Build display lines
135
+ const lines = [];
136
+ // Main task line (running, failed, or last completed)
137
+ if (primaryTask) {
138
+ const isRunning = primaryTask.status === 'running';
139
+ const isCompleted = primaryTask.status === 'completed';
140
+ primaryTask.status === 'failed';
141
+ let prefix;
142
+ let prefixColor;
143
+ let textColor;
144
+ if (isRunning) {
145
+ prefix = `${symbols.spinnerFrames[spinnerIndex]} `;
146
+ prefixColor = colors.cyan;
147
+ textColor = colors.white;
148
+ }
149
+ else if (isCompleted) {
150
+ prefix = `${symbols.check} `;
151
+ prefixColor = colors.success;
152
+ textColor = colors.white;
153
+ }
154
+ else {
155
+ prefix = `${symbols.cross} `;
156
+ prefixColor = colors.error;
157
+ textColor = colors.error;
158
+ }
159
+ lines.push({
160
+ id: `main-${primaryTask.id}`,
161
+ text: primaryTask.label,
162
+ displayedText: primaryTask.label,
163
+ color: textColor,
164
+ prefix,
165
+ prefixColor,
166
+ showCursor: false,
167
+ });
168
+ }
169
+ // Previous completed tasks as subtasks - with green checkmark
170
+ previousCompleted.forEach((task, index) => {
171
+ const hasMore = primaryTask?.detail || primaryTask?.error || index < previousCompleted.length - 1;
172
+ const connector = hasMore ? '├──' : '└──';
173
+ lines.push({
174
+ id: `completed-${task.id}`,
175
+ text: task.label,
176
+ displayedText: task.label,
177
+ color: colors.white,
178
+ prefix: ` ${connector} `,
179
+ prefixColor: colors.dimGray,
180
+ showCursor: false,
181
+ checkmark: true,
182
+ });
183
+ });
184
+ // Detail line (no typewriter for progress updates - show immediately)
185
+ if (primaryTask?.detail && primaryTask.status === 'running') {
186
+ lines.push({
187
+ id: `detail-${primaryTask.id}`,
188
+ text: primaryTask.detail,
189
+ displayedText: primaryTask.detail, // Show full text immediately for progress updates
190
+ color: colors.dimGray,
191
+ prefix: ' └── ',
192
+ prefixColor: colors.dimGray,
193
+ showCursor: false, // No cursor for instant updates
194
+ });
195
+ }
196
+ // Error line (typewriter effect)
197
+ if (primaryTask?.error && primaryTask.status === 'failed') {
198
+ const errorId = `error-${primaryTask.id}`;
199
+ const currentChars = typedChars[errorId] || 0;
200
+ const displayedText = primaryTask.error.slice(0, currentChars);
201
+ const isTyping = currentChars < primaryTask.error.length;
202
+ lines.push({
203
+ id: errorId,
204
+ text: primaryTask.error,
205
+ displayedText,
206
+ color: colors.error,
207
+ prefix: ' └── ',
208
+ prefixColor: colors.dimGray,
209
+ showCursor: isTyping && cursorVisible,
210
+ });
211
+ }
212
+ // Typewriter animation - only for errors now (details show instantly for progress)
213
+ const errorToType = primaryTask?.error && primaryTask.status === 'failed' ? primaryTask.error : null;
214
+ const textToType = errorToType;
215
+ const typeId = errorToType ? `error-${primaryTask?.id}` : null;
216
+ reactExports.useEffect(() => {
217
+ if (!typeId || !textToType)
218
+ return;
219
+ const currentChars = typedChars[typeId] || 0;
220
+ // Check if we just finished typing
221
+ if (currentChars >= textToType.length) {
222
+ // Fire callback once when typing completes
223
+ if (!completedTyping.has(typeId) && onTypewriterComplete && primaryTask) {
224
+ setCompletedTyping(prev => new Set(prev).add(typeId));
225
+ onTypewriterComplete(primaryTask.id);
226
+ }
227
+ return;
228
+ }
229
+ // Continue typing animation
230
+ const timeout = setTimeout(() => {
231
+ setTypedChars(prev => ({
232
+ ...prev,
233
+ [typeId]: (prev[typeId] || 0) + 1,
234
+ }));
235
+ }, 30);
236
+ return () => clearTimeout(timeout);
237
+ }, [typeId, textToType, typedChars[typeId || ''], completedTyping, onTypewriterComplete, primaryTask]);
238
+ // Empty state - show empty box to maintain layout
239
+ if (lines.length === 0) {
240
+ return (jsxRuntimeExports.jsx(Box, { flexDirection: "column", alignItems: "center", marginTop: 1, children: jsxRuntimeExports.jsxs(Box, { flexDirection: "column", width: BOX_WIDTH, children: [jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["\u250C", '─'.repeat(BOX_WIDTH - 2), "\u2510"] }), jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "\u2502" }), jsxRuntimeExports.jsx(Text, { children: ' '.repeat(BOX_WIDTH - 2) }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "\u2502" })] }), jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["\u2514", '─'.repeat(BOX_WIDTH - 2), "\u2518"] })] }) }));
241
+ }
242
+ return (jsxRuntimeExports.jsx(Box, { flexDirection: "column", alignItems: "center", marginTop: 1, children: jsxRuntimeExports.jsxs(Box, { flexDirection: "column", width: BOX_WIDTH, children: [jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["\u250C", '─'.repeat(BOX_WIDTH - 2), "\u2510"] }), lines.map(line => {
243
+ const checkmarkStr = line.checkmark ? `${symbols.check} ` : '';
244
+ const content = `${line.prefix}${checkmarkStr}${line.displayedText}`;
245
+ const cursor = line.showCursor ? '▌' : '';
246
+ const paddingNeeded = Math.max(0, BOX_INNER_WIDTH - content.length - cursor.length);
247
+ return (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "\u2502 " }), jsxRuntimeExports.jsx(Text, { color: line.prefixColor, children: line.prefix }), line.checkmark && jsxRuntimeExports.jsxs(Text, { color: colors.success, children: [symbols.check, " "] }), jsxRuntimeExports.jsx(Text, { color: line.color, children: line.displayedText }), line.showCursor && jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: "\u258C" }), jsxRuntimeExports.jsx(Text, { children: ' '.repeat(paddingNeeded) }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: " \u2502" })] }, line.id));
248
+ }), jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["\u2514", '─'.repeat(BOX_WIDTH - 2), "\u2518"] })] }) }));
249
+ }
250
+
251
+ /**
252
+ * Configuration summary display
253
+ * ───────────────────────────────────
254
+ * Workspace ~/hatchway-workspace
255
+ * Server http://localhost:3000
256
+ * Runner local
257
+ * ───────────────────────────────────
258
+ */
259
+ function ConfigSummary({ items, title }) {
260
+ const dividerWidth = 40;
261
+ const divider = symbols.horizontalLine.repeat(dividerWidth);
262
+ // Find the longest label for alignment
263
+ const maxLabelLength = Math.max(...items.map(item => item.label.length));
264
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: divider }), title && (jsxRuntimeExports.jsx(Box, { marginTop: 1, marginBottom: 1, children: jsxRuntimeExports.jsx(Text, { color: colors.white, bold: true, children: title }) })), jsxRuntimeExports.jsx(Box, { flexDirection: "column", alignItems: "flex-start", marginTop: title ? 0 : 1, children: items.map((item, index) => (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.gray, children: item.label.padEnd(maxLabelLength + 3) }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: item.value })] }, index))) }), jsxRuntimeExports.jsx(Box, { marginTop: 1, children: jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: divider }) })] }));
265
+ }
266
+ /**
267
+ * Next steps display for completion screen
268
+ */
269
+ function NextSteps({ command, url }) {
270
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", marginTop: 1, children: [jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.gray, children: "Run: " }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: command })] }), jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.gray, children: "Open: " }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: url })] })] }));
271
+ }
272
+ /**
273
+ * Error display with message and recovery suggestions
274
+ */
275
+ function ErrorSummary({ message, suggestions }) {
276
+ const dividerWidth = 44;
277
+ const divider = symbols.horizontalLine.repeat(dividerWidth);
278
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: divider }), message && (jsxRuntimeExports.jsx(Box, { marginTop: 1, marginBottom: 1, children: jsxRuntimeExports.jsxs(Text, { color: colors.error, bold: true, children: [symbols.cross, " ", message] }) })), jsxRuntimeExports.jsx(Box, { flexDirection: "column", alignItems: "flex-start", marginTop: message ? 0 : 1, children: suggestions.map((suggestion, index) => (jsxRuntimeExports.jsx(Box, { children: jsxRuntimeExports.jsx(Text, { color: suggestion.startsWith(' ') ? colors.cyan : colors.gray, children: suggestion }) }, index))) }), jsxRuntimeExports.jsx(Box, { marginTop: 1, children: jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: divider }) })] }));
279
+ }
280
+
281
+ /**
282
+ * Compact build error viewer with scrolling and copy support
283
+ * Displays in the center area, half-width, with scrollable content
284
+ */
285
+ function BuildErrorView({ title, errorLines, suggestions = [], onExit }) {
286
+ const { exit } = useApp();
287
+ const { stdout } = useStdout();
288
+ const terminalWidth = stdout?.columns || 80;
289
+ const [scrollOffset, setScrollOffset] = reactExports.useState(0);
290
+ const [showCopiedMessage, setShowCopiedMessage] = reactExports.useState(false);
291
+ // Fixed dimensions for compact view
292
+ const boxWidth = Math.min(70, Math.floor(terminalWidth * 0.6));
293
+ const maxVisibleLines = 12; // Show 12 lines of errors max
294
+ // Calculate max scroll
295
+ const maxScroll = Math.max(0, errorLines.length - maxVisibleLines);
296
+ // Get visible lines
297
+ const visibleLines = errorLines.slice(scrollOffset, scrollOffset + maxVisibleLines);
298
+ // Copy all error content to clipboard
299
+ const copyToClipboard = reactExports.useCallback(async () => {
300
+ const content = [
301
+ `Build Error: ${title}`,
302
+ '─'.repeat(60),
303
+ ...errorLines,
304
+ '─'.repeat(60),
305
+ '',
306
+ ...suggestions,
307
+ ].join('\n');
308
+ try {
309
+ const { exec } = await import('child_process');
310
+ // Detect platform and use appropriate clipboard command
311
+ const platform = process.platform;
312
+ let clipboardCmd;
313
+ if (platform === 'darwin') {
314
+ clipboardCmd = 'pbcopy';
315
+ }
316
+ else if (platform === 'win32') {
317
+ clipboardCmd = 'clip';
318
+ }
319
+ else {
320
+ clipboardCmd = 'xclip -selection clipboard';
321
+ }
322
+ // Write to clipboard
323
+ const child = exec(clipboardCmd);
324
+ child.stdin?.write(content);
325
+ child.stdin?.end();
326
+ setShowCopiedMessage(true);
327
+ // Show success message then exit after delay
328
+ setTimeout(() => {
329
+ if (onExit) {
330
+ onExit();
331
+ }
332
+ else {
333
+ exit();
334
+ }
335
+ }, 1500);
336
+ }
337
+ catch (error) {
338
+ // Clipboard failed - still show message
339
+ setShowCopiedMessage(true);
340
+ setTimeout(() => setShowCopiedMessage(false), 2000);
341
+ }
342
+ }, [title, errorLines, suggestions, onExit, exit]);
343
+ // Handle keyboard input
344
+ useInput((input, key) => {
345
+ if (input === 'c' || input === 'C') {
346
+ copyToClipboard();
347
+ }
348
+ else if (key.upArrow) {
349
+ setScrollOffset(prev => Math.max(0, prev - 1));
350
+ }
351
+ else if (key.downArrow) {
352
+ setScrollOffset(prev => Math.min(maxScroll, prev + 1));
353
+ }
354
+ else if (key.pageUp) {
355
+ setScrollOffset(prev => Math.max(0, prev - maxVisibleLines));
356
+ }
357
+ else if (key.pageDown) {
358
+ setScrollOffset(prev => Math.min(maxScroll, prev + maxVisibleLines));
359
+ }
360
+ else if (input === 'q' || key.escape) {
361
+ if (onExit) {
362
+ onExit();
363
+ }
364
+ else {
365
+ exit();
366
+ }
367
+ }
368
+ });
369
+ // Color error lines based on content
370
+ const getLineColor = (line) => {
371
+ if (/error|Error|ERR!/i.test(line))
372
+ return colors.error;
373
+ if (/warning|warn/i.test(line))
374
+ return colors.warning;
375
+ if (/:\d+:\d+/.test(line))
376
+ return colors.cyan;
377
+ if (line.startsWith('─') || line.startsWith('hatchway:'))
378
+ return colors.dimGray;
379
+ return colors.gray;
380
+ };
381
+ const divider = symbols.horizontalLine.repeat(boxWidth - 2);
382
+ const hasScroll = errorLines.length > maxVisibleLines;
383
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", width: "100%", children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: divider }), jsxRuntimeExports.jsx(Box, { marginTop: 1, marginBottom: 1, width: boxWidth, justifyContent: "center", children: jsxRuntimeExports.jsxs(Text, { color: colors.error, bold: true, children: [symbols.cross, " ", title] }) }), jsxRuntimeExports.jsxs(Box, { flexDirection: "column", width: boxWidth, borderStyle: "round", borderColor: colors.error, paddingX: 1, paddingY: 0, children: [hasScroll && (jsxRuntimeExports.jsx(Box, { justifyContent: "flex-end", marginBottom: 0, children: jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, dimColor: true, children: ["[", scrollOffset + 1, "-", Math.min(scrollOffset + maxVisibleLines, errorLines.length), "/", errorLines.length, "] \u2191\u2193"] }) })), visibleLines.length > 0 ? (visibleLines.map((line, index) => {
384
+ const truncatedLine = line.length > boxWidth - 4
385
+ ? line.substring(0, boxWidth - 7) + '...'
386
+ : line;
387
+ return (jsxRuntimeExports.jsx(Text, { color: getLineColor(line), wrap: "truncate", children: truncatedLine }, index));
388
+ })) : (jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "No error details captured" }))] }), suggestions.length > 0 && (jsxRuntimeExports.jsx(Box, { flexDirection: "column", marginTop: 1, width: boxWidth, children: suggestions.map((suggestion, index) => (jsxRuntimeExports.jsx(Text, { color: suggestion.startsWith(' ') ? colors.cyan : colors.gray, children: suggestion }, index))) })), jsxRuntimeExports.jsx(Box, { marginTop: 1, children: jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: divider }) }), jsxRuntimeExports.jsx(Box, { marginTop: 1, justifyContent: "center", children: showCopiedMessage ? (jsxRuntimeExports.jsxs(Text, { color: colors.success, bold: true, children: [symbols.check, " Copied to clipboard! Exiting..."] })) : (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "[" }), jsxRuntimeExports.jsx(Text, { color: colors.success, bold: true, children: "c" }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "]" }), jsxRuntimeExports.jsx(Text, { color: colors.success, children: "copy & exit" }), jsxRuntimeExports.jsx(Text, { children: " " }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "[" }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: "q" }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "]" }), jsxRuntimeExports.jsx(Text, { color: colors.gray, children: "quit" }), hasScroll && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx(Text, { children: " " }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "[" }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: "\u2191\u2193" }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "]" }), jsxRuntimeExports.jsx(Text, { color: colors.gray, children: "scroll" })] }))] })) })] }));
389
+ }
390
+
391
+ const INITIAL_STEPS = [
392
+ { id: 'repo', label: 'Clone', status: 'pending' },
393
+ { id: 'build', label: 'Build', status: 'pending' },
394
+ { id: 'database', label: 'Configure', status: 'pending' },
395
+ { id: 'ready', label: 'Finish', status: 'pending' },
396
+ ];
397
+ // Tasks now have a stepId to associate them with a step
398
+ const INITIAL_TASKS = [
399
+ { id: 'clone', label: 'Cloning repository', status: 'pending', stepId: 'repo' },
400
+ { id: 'deps', label: 'Installing dependencies', status: 'pending', stepId: 'build' },
401
+ { id: 'build', label: 'Building packages', status: 'pending', stepId: 'build' },
402
+ { id: 'database', label: 'Configuring database', status: 'pending', stepId: 'database' },
403
+ { id: 'config', label: 'Saving configuration', status: 'pending', stepId: 'ready' },
404
+ { id: 'services', label: 'Building services', status: 'pending', stepId: 'ready' },
405
+ ];
406
+ function useInitFlow() {
407
+ const [state, setState] = reactExports.useState({
408
+ phase: 'repo',
409
+ steps: INITIAL_STEPS,
410
+ tasks: INITIAL_TASKS,
411
+ config: [],
412
+ error: null,
413
+ isComplete: false,
414
+ });
415
+ // Step management
416
+ const setStepStatus = reactExports.useCallback((stepId, status) => {
417
+ setState(prev => ({
418
+ ...prev,
419
+ steps: prev.steps.map(step => step.id === stepId ? { ...step, status } : step),
420
+ }));
421
+ }, []);
422
+ const activateStep = reactExports.useCallback((stepId) => {
423
+ setState(prev => ({
424
+ ...prev,
425
+ phase: stepId,
426
+ steps: prev.steps.map(step => step.id === stepId ? { ...step, status: 'active' } : step),
427
+ }));
428
+ }, []);
429
+ const completeStep = reactExports.useCallback((stepId) => {
430
+ setStepStatus(stepId, 'completed');
431
+ }, [setStepStatus]);
432
+ const failStep = reactExports.useCallback((stepId) => {
433
+ setStepStatus(stepId, 'error');
434
+ }, [setStepStatus]);
435
+ // Task management
436
+ const setTaskStatus = reactExports.useCallback((taskId, status, detail) => {
437
+ setState(prev => ({
438
+ ...prev,
439
+ tasks: prev.tasks.map(task => task.id === taskId ? { ...task, status, detail: detail ?? task.detail } : task),
440
+ }));
441
+ }, []);
442
+ const startTask = reactExports.useCallback((taskId, detail) => {
443
+ setTaskStatus(taskId, 'running', detail);
444
+ }, [setTaskStatus]);
445
+ const completeTask = reactExports.useCallback((taskId) => {
446
+ setTaskStatus(taskId, 'completed');
447
+ }, [setTaskStatus]);
448
+ const failTask = reactExports.useCallback((taskId, error) => {
449
+ setState(prev => ({
450
+ ...prev,
451
+ tasks: prev.tasks.map(task => task.id === taskId ? { ...task, status: 'failed', error } : task),
452
+ }));
453
+ }, []);
454
+ // Get tasks for a specific step
455
+ const getTasksForStep = reactExports.useCallback((stepId) => {
456
+ return state.tasks.filter(task => task.stepId === stepId);
457
+ }, [state.tasks]);
458
+ // Get tasks for the currently active step
459
+ const getActiveStepTasks = reactExports.useCallback(() => {
460
+ return state.tasks.filter(task => task.stepId === state.phase);
461
+ }, [state.tasks, state.phase]);
462
+ // Config management
463
+ const setConfig = reactExports.useCallback((items) => {
464
+ setState(prev => ({
465
+ ...prev,
466
+ config: items,
467
+ }));
468
+ }, []);
469
+ // Error management
470
+ const setError = reactExports.useCallback((message, suggestions) => {
471
+ setState(prev => ({
472
+ ...prev,
473
+ error: { message, suggestions },
474
+ }));
475
+ }, []);
476
+ const setBuildError = reactExports.useCallback((message, errorLines, suggestions) => {
477
+ setState(prev => ({
478
+ ...prev,
479
+ error: {
480
+ message,
481
+ suggestions,
482
+ buildError: {
483
+ errorLines,
484
+ suggestions,
485
+ }
486
+ },
487
+ }));
488
+ }, []);
489
+ const clearError = reactExports.useCallback(() => {
490
+ setState(prev => ({
491
+ ...prev,
492
+ error: null,
493
+ }));
494
+ }, []);
495
+ // Completion
496
+ const markComplete = reactExports.useCallback(() => {
497
+ setState(prev => ({
498
+ ...prev,
499
+ isComplete: true,
500
+ phase: 'ready',
501
+ steps: prev.steps.map(step => ({ ...step, status: 'completed' })),
502
+ }));
503
+ }, []);
504
+ // Reset
505
+ const reset = reactExports.useCallback(() => {
506
+ setState({
507
+ phase: 'repo',
508
+ steps: INITIAL_STEPS,
509
+ tasks: INITIAL_TASKS,
510
+ config: [],
511
+ error: null,
512
+ isComplete: false,
513
+ });
514
+ }, []);
515
+ return {
516
+ state,
517
+ setStepStatus,
518
+ activateStep,
519
+ completeStep,
520
+ failStep,
521
+ setTaskStatus,
522
+ startTask,
523
+ completeTask,
524
+ failTask,
525
+ getTasksForStep,
526
+ getActiveStepTasks,
527
+ setConfig,
528
+ setError,
529
+ setBuildError,
530
+ clearError,
531
+ markComplete,
532
+ reset,
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Main init screen - fullscreen centered TUI
538
+ * Shows progress stepper with tasks appearing under the active step
539
+ */
540
+ function InitScreen({ onInit, onComplete, onError }) {
541
+ const { exit } = useApp();
542
+ const { stdout } = useStdout();
543
+ const flow = useInitFlow();
544
+ const [finalConfig, setFinalConfig] = reactExports.useState(null);
545
+ // Get version info
546
+ const versionInfo = getVersionInfo();
547
+ // Calculate vertical centering
548
+ const terminalHeight = stdout?.rows || 24;
549
+ const contentHeight = 20;
550
+ const topPadding = Math.max(0, Math.floor((terminalHeight - contentHeight) / 3));
551
+ // Run init flow
552
+ reactExports.useEffect(() => {
553
+ const callbacks = {
554
+ activateStep: flow.activateStep,
555
+ completeStep: flow.completeStep,
556
+ failStep: flow.failStep,
557
+ startTask: flow.startTask,
558
+ completeTask: flow.completeTask,
559
+ failTask: flow.failTask,
560
+ skipTask: (taskId) => {
561
+ flow.setTaskStatus(taskId, 'completed');
562
+ },
563
+ updateTaskLabel: (taskId, label) => {
564
+ flow.setTaskStatus(taskId, flow.state.tasks.find(t => t.id === taskId)?.status || 'pending');
565
+ },
566
+ setError: flow.setError,
567
+ setBuildError: flow.setBuildError,
568
+ };
569
+ onInit(callbacks)
570
+ .then((config) => {
571
+ setFinalConfig(config);
572
+ // Set config summary items
573
+ const configItems = [
574
+ { label: 'Workspace', value: config.workspace },
575
+ { label: 'Server', value: config.apiUrl },
576
+ { label: 'Runner', value: config.runnerId },
577
+ ];
578
+ if (config.monorepoPath) {
579
+ configItems.push({ label: 'Repository', value: config.monorepoPath });
580
+ }
581
+ flow.setConfig(configItems);
582
+ flow.markComplete();
583
+ if (onComplete) {
584
+ onComplete(config);
585
+ }
586
+ })
587
+ .catch((error) => {
588
+ if (onError) {
589
+ onError(error);
590
+ }
591
+ });
592
+ }, []);
593
+ const { state } = flow;
594
+ // Get tasks for current step only, mapped to StreamTask format
595
+ const currentStepTasks = flow.getActiveStepTasks().map(task => ({
596
+ id: task.id,
597
+ label: task.label,
598
+ status: task.status,
599
+ detail: task.detail,
600
+ error: task.error,
601
+ }));
602
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", paddingTop: topPadding, children: [jsxRuntimeExports.jsx(Banner, {}), jsxRuntimeExports.jsx(Box, { marginTop: 1, children: jsxRuntimeExports.jsx(Text, { color: colors.gray, dimColor: true, children: versionInfo.display }) }), jsxRuntimeExports.jsx(Box, { marginTop: 1 }), jsxRuntimeExports.jsx(ProgressStepper, { steps: state.steps }), !state.isComplete && !state.error && (jsxRuntimeExports.jsx(TaskStream, { stepId: state.phase, tasks: currentStepTasks })), state.error && state.error.buildError && (jsxRuntimeExports.jsx(BuildErrorView, { title: state.error.message, errorLines: state.error.buildError.errorLines, suggestions: state.error.buildError.suggestions, onExit: () => exit() })), state.error && !state.error.buildError && (jsxRuntimeExports.jsx(Box, { marginTop: 2, children: jsxRuntimeExports.jsx(ErrorSummary, { message: state.error.message, suggestions: state.error.suggestions }) })), state.isComplete && !state.error && (jsxRuntimeExports.jsxs(Box, { marginTop: 2, flexDirection: "column", alignItems: "center", children: [jsxRuntimeExports.jsxs(Text, { color: colors.success, bold: true, children: [symbols.check, " Setup complete!"] }), jsxRuntimeExports.jsx(Box, { marginTop: 1, flexDirection: "column", children: state.tasks.filter(t => t.status === 'completed').map(task => (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.success, children: symbols.check }), jsxRuntimeExports.jsxs(Text, { color: colors.gray, children: [" ", task.label] })] }, task.id))) }), jsxRuntimeExports.jsx(Box, { marginTop: 1 }), jsxRuntimeExports.jsx(ConfigSummary, { items: state.config }), jsxRuntimeExports.jsx(NextSteps, { command: "hatchway run", url: "http://localhost:3000" })] }))] }));
603
+ }
604
+
605
+ /**
606
+ * Post-init prompt asking if user wants to start Hatchway now
607
+ */
608
+ function StartPromptScreen({ onSelect }) {
609
+ const { stdout } = useStdout();
610
+ // Calculate vertical centering
611
+ const terminalHeight = stdout?.rows || 24;
612
+ const contentHeight = 14;
613
+ const topPadding = Math.max(0, Math.floor((terminalHeight - contentHeight) / 3));
614
+ useInput((input, key) => {
615
+ const char = input.toLowerCase();
616
+ // Enter = start
617
+ if (key.return) {
618
+ onSelect(true);
619
+ return;
620
+ }
621
+ // Y = start
622
+ if (char === 'y') {
623
+ onSelect(true);
624
+ return;
625
+ }
626
+ // N = don't start
627
+ if (char === 'n') {
628
+ onSelect(false);
629
+ return;
630
+ }
631
+ // Escape = don't start
632
+ if (key.escape) {
633
+ onSelect(false);
634
+ return;
635
+ }
636
+ });
637
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", paddingTop: topPadding, children: [jsxRuntimeExports.jsx(Banner, {}), jsxRuntimeExports.jsx(Box, { marginTop: 2 }), jsxRuntimeExports.jsx(Box, { flexDirection: "column", alignItems: "center", children: jsxRuntimeExports.jsxs(Text, { color: colors.success, bold: true, children: [symbols.check, " Hatchway is ready!"] }) }), jsxRuntimeExports.jsx(Box, { marginTop: 2 }), jsxRuntimeExports.jsxs(Box, { flexDirection: "column", alignItems: "center", children: [jsxRuntimeExports.jsx(Text, { color: colors.white, children: "Start Hatchway now?" }), jsxRuntimeExports.jsx(Box, { marginTop: 1, children: jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["Press ", jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: "Y" }), " or ", jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: "Enter" }), " to start, ", jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: "N" }), " or ", jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: "Esc" }), " to exit"] }) })] }), jsxRuntimeExports.jsx(Box, { marginTop: 2 }), jsxRuntimeExports.jsx(Box, { flexDirection: "column", alignItems: "center", children: jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["To start later, run: ", jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: "hatchway run" })] }) })] }));
638
+ }
639
+
640
+ /**
641
+ * Init App component that manages the init flow screens
642
+ */
643
+ function InitApp({ onInit, onComplete, onError }) {
644
+ const [screen, setScreen] = reactExports.useState('init');
645
+ const [config, setConfig] = reactExports.useState(null);
646
+ const handleInitComplete = (initConfig) => {
647
+ setConfig(initConfig);
648
+ // Show the start prompt screen
649
+ setScreen('prompt');
650
+ };
651
+ const handleStartChoice = (shouldStart) => {
652
+ if (config) {
653
+ onComplete(config, shouldStart);
654
+ }
655
+ };
656
+ if (screen === 'prompt' && config) {
657
+ return jsxRuntimeExports.jsx(StartPromptScreen, { onSelect: handleStartChoice });
658
+ }
659
+ return (jsxRuntimeExports.jsx(InitScreen, { onInit: onInit, onComplete: handleInitComplete, onError: onError }));
660
+ }
661
+ /**
662
+ * Render the TUI init screen
663
+ * Returns a promise that resolves with config and whether to start
664
+ */
665
+ async function runInitTUI(options) {
666
+ return new Promise((resolve, reject) => {
667
+ let result = null;
668
+ let error = null;
669
+ const { unmount, waitUntilExit } = render(jsxRuntimeExports.jsx(InitApp, { onInit: options.onInit, onComplete: (config, shouldStart) => {
670
+ result = { config, shouldStart };
671
+ // Give time for final render before unmounting
672
+ setTimeout(() => {
673
+ unmount();
674
+ }, 100);
675
+ }, onError: (err) => {
676
+ error = err;
677
+ // Keep error displayed, don't auto-unmount
678
+ } }), {
679
+ exitOnCtrlC: true,
680
+ });
681
+ waitUntilExit().then(() => {
682
+ if (error) {
683
+ reject(error);
684
+ }
685
+ else if (result) {
686
+ resolve(result);
687
+ }
688
+ else {
689
+ reject(new Error('Init cancelled'));
690
+ }
691
+ });
692
+ });
693
+ }
694
+
695
+ /**
696
+ * TUI-based init command with beautiful centered interface
697
+ * Uses Ink for React-based terminal rendering
698
+ */
699
+ /**
700
+ * Generate a secure random secret
701
+ */
702
+ function generateSecret() {
703
+ return randomBytes(32).toString('hex');
704
+ }
705
+ /**
706
+ * Check if a path is or contains the current working directory
707
+ */
708
+ function isCurrentWorkingDirectory(targetPath) {
709
+ try {
710
+ const cwd = realpathSync(process.cwd());
711
+ const target = realpathSync(resolve(targetPath));
712
+ return cwd === target || cwd.startsWith(target + '/');
713
+ }
714
+ catch {
715
+ const cwd = process.cwd();
716
+ const target = resolve(targetPath);
717
+ return cwd === target || cwd.startsWith(target + '/');
718
+ }
719
+ }
720
+ /**
721
+ * Normalize URL by adding protocol if missing
722
+ */
723
+ function normalizeUrl(url) {
724
+ if (url.match(/^https?:\/\//i))
725
+ return url;
726
+ if (url.match(/^(localhost|127\.0\.0\.1)(:|\/|$)/i)) {
727
+ return `http://${url}`;
728
+ }
729
+ return `https://${url}`;
730
+ }
731
+ /**
732
+ * Get default workspace path
733
+ */
734
+ function getDefaultWorkspace() {
735
+ return join(process.cwd(), 'hatchway-workspace');
736
+ }
737
+ /**
738
+ * Get default monorepo clone path
739
+ */
740
+ function getDefaultMonorepoPath() {
741
+ return join(process.cwd(), 'hatchway');
742
+ }
743
+ /**
744
+ * Sleep utility for deliberate pacing
745
+ */
746
+ function sleep(ms) {
747
+ return new Promise(resolve => setTimeout(resolve, ms));
748
+ }
749
+ /**
750
+ * Run the TUI-based init command
751
+ */
752
+ async function initTUICommand(options) {
753
+ // Clear screen for fullscreen experience
754
+ console.clear();
755
+ try {
756
+ const { shouldStart } = await runInitTUI({
757
+ onInit: async (callbacks) => {
758
+ return executeInitFlow(options, callbacks);
759
+ },
760
+ });
761
+ if (shouldStart) {
762
+ console.clear();
763
+ console.log('\n Starting Hatchway...\n');
764
+ // Import and run the start command (full TUI with web app + runner)
765
+ const { startCommand } = await import('./start-Dkuro1jp.js');
766
+ await startCommand({});
767
+ }
768
+ else {
769
+ console.clear();
770
+ console.log('\n ✨ Hatchway is ready!\n');
771
+ console.log(' To start later, run:\n');
772
+ console.log(' hatchway run\n');
773
+ console.log(' Then open: http://localhost:3000\n');
774
+ }
775
+ }
776
+ catch (error) {
777
+ // Error was already displayed in TUI
778
+ console.log('\n');
779
+ process.exit(1);
780
+ }
781
+ }
782
+ /**
783
+ * Execute the init flow, calling callbacks to update UI
784
+ */
785
+ async function executeInitFlow(options, callbacks) {
786
+ const { activateStep, completeStep, failStep, startTask, completeTask, failTask, setError, setBuildError } = callbacks;
787
+ let monorepoPath;
788
+ let databaseUrl;
789
+ const workspace = options.workspace || getDefaultWorkspace();
790
+ const generatedSecret = options.secret || generateSecret();
791
+ const apiUrl = normalizeUrl(options.url || 'http://localhost:3000');
792
+ // ============================================
793
+ // PHASE 1: Repository
794
+ // ============================================
795
+ activateStep('repo');
796
+ await sleep(layout.stepTransitionDelay);
797
+ // Reset config if exists
798
+ if (configManager.isInitialized()) {
799
+ configManager.reset();
800
+ }
801
+ // Check for existing monorepo
802
+ startTask('clone', 'Checking for repository...');
803
+ await sleep(300);
804
+ const repoCheck = await isInsideMonorepo();
805
+ if (repoCheck.inside && repoCheck.root) {
806
+ monorepoPath = repoCheck.root;
807
+ completeTask('clone');
808
+ await sleep(layout.taskCompletionDelay);
809
+ }
810
+ else {
811
+ // Need to clone
812
+ const hasPnpm = await isPnpmInstalled();
813
+ if (!hasPnpm) {
814
+ failTask('clone', 'pnpm not found');
815
+ failStep('repo');
816
+ setError('pnpm is required', [
817
+ 'Install pnpm: npm install -g pnpm',
818
+ 'Then retry: hatchway init -y',
819
+ ]);
820
+ throw new CLIError({
821
+ code: 'DEPENDENCIES_INSTALL_FAILED',
822
+ message: 'pnpm is not installed',
823
+ });
824
+ }
825
+ const clonePath = getDefaultMonorepoPath();
826
+ // Clean up existing installation
827
+ if (existsSync(clonePath)) {
828
+ if (isCurrentWorkingDirectory(clonePath)) {
829
+ failTask('clone', 'Cannot remove current directory');
830
+ failStep('repo');
831
+ setError('Cannot remove current directory', [
832
+ 'Run from a different directory',
833
+ 'Or manually remove: rm -rf ' + clonePath,
834
+ ]);
835
+ throw new CLIError({
836
+ code: 'CONFIG_INVALID',
837
+ message: 'Cannot remove current working directory',
838
+ });
839
+ }
840
+ rmSync(clonePath, { recursive: true, force: true });
841
+ }
842
+ try {
843
+ // Clone repository
844
+ startTask('clone', 'Cloning from GitHub...');
845
+ monorepoPath = await cloneRepository({
846
+ targetPath: clonePath,
847
+ branch: options.branch || 'main',
848
+ silent: true, // Suppress console output in TUI mode
849
+ });
850
+ completeTask('clone');
851
+ await sleep(layout.taskCompletionDelay);
852
+ }
853
+ catch (error) {
854
+ failTask('clone', 'Clone failed');
855
+ failStep('repo');
856
+ setError('Failed to clone repository', [
857
+ 'Check your internet connection',
858
+ 'Verify git is installed: git --version',
859
+ 'Try: git clone https://github.com/codyde/hatchway.git',
860
+ ]);
861
+ throw error;
862
+ }
863
+ }
864
+ completeStep('repo');
865
+ await sleep(layout.stepTransitionDelay);
866
+ // ============================================
867
+ // PHASE 2: Build
868
+ // ============================================
869
+ activateStep('build');
870
+ await sleep(layout.stepTransitionDelay);
871
+ // Install dependencies
872
+ startTask('deps', 'Running pnpm install...');
873
+ try {
874
+ await installDependencies(monorepoPath, true); // silent mode
875
+ completeTask('deps');
876
+ await sleep(layout.taskCompletionDelay);
877
+ }
878
+ catch (error) {
879
+ failTask('deps', 'Install failed');
880
+ failStep('build');
881
+ setError('Failed to install dependencies', [
882
+ 'Check pnpm is installed: pnpm --version',
883
+ 'Try manually: cd ' + monorepoPath + ' && pnpm install',
884
+ ]);
885
+ throw error;
886
+ }
887
+ // Build packages
888
+ startTask('build', '@hatchway/agent-core');
889
+ try {
890
+ await buildAgentCore(monorepoPath, true); // silent mode
891
+ completeTask('build');
892
+ await sleep(layout.taskCompletionDelay);
893
+ }
894
+ catch (error) {
895
+ failTask('build', 'Build failed');
896
+ failStep('build');
897
+ setError('Failed to build packages', [
898
+ 'Try manually: cd ' + monorepoPath + ' && pnpm build',
899
+ ]);
900
+ throw error;
901
+ }
902
+ completeStep('build');
903
+ await sleep(layout.stepTransitionDelay);
904
+ // ============================================
905
+ // PHASE 3: Database
906
+ // ============================================
907
+ activateStep('database');
908
+ await sleep(layout.stepTransitionDelay);
909
+ startTask('database', 'Setting up Neon database...');
910
+ // Handle database options
911
+ const dbOption = options.database;
912
+ const isConnectionString = typeof dbOption === 'string' &&
913
+ (dbOption.startsWith('postgres://') || dbOption.startsWith('postgresql://'));
914
+ try {
915
+ if (isConnectionString) {
916
+ databaseUrl = dbOption;
917
+ await pushDatabaseSchema(monorepoPath, databaseUrl, true); // silent mode
918
+ }
919
+ else {
920
+ // Default: setup Neon database
921
+ databaseUrl = await setupDatabase(monorepoPath, true) || undefined; // silent mode
922
+ if (databaseUrl) {
923
+ await pushDatabaseSchema(monorepoPath, databaseUrl, true); // silent mode
924
+ }
925
+ }
926
+ completeTask('database');
927
+ await sleep(layout.taskCompletionDelay);
928
+ }
929
+ catch (error) {
930
+ // Database setup is optional, don't fail hard
931
+ completeTask('database');
932
+ await sleep(layout.taskCompletionDelay);
933
+ }
934
+ completeStep('database');
935
+ await sleep(layout.stepTransitionDelay);
936
+ // ============================================
937
+ // PHASE 4: Configuration & Ready
938
+ // ============================================
939
+ activateStep('ready');
940
+ await sleep(layout.stepTransitionDelay);
941
+ // Create workspace directory
942
+ if (!existsSync(workspace)) {
943
+ await mkdir(workspace, { recursive: true });
944
+ }
945
+ // Save configuration
946
+ startTask('config', 'Writing configuration...');
947
+ try {
948
+ configManager.set('workspace', workspace);
949
+ if (monorepoPath) {
950
+ configManager.set('monorepoPath', monorepoPath);
951
+ }
952
+ if (databaseUrl) {
953
+ configManager.set('databaseUrl', databaseUrl);
954
+ }
955
+ configManager.set('apiUrl', apiUrl);
956
+ const wsProtocol = apiUrl.startsWith('https://') ? 'wss://' : 'ws://';
957
+ const hostPath = apiUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
958
+ const wsUrl = `${wsProtocol}${hostPath}/ws/runner`;
959
+ configManager.set('server', {
960
+ wsUrl: wsUrl,
961
+ secret: generatedSecret,
962
+ });
963
+ configManager.set('runner', {
964
+ id: 'local',
965
+ reconnectAttempts: 5,
966
+ heartbeatInterval: 15000,
967
+ });
968
+ configManager.set('tunnel', {
969
+ provider: 'cloudflare',
970
+ autoCreate: true,
971
+ });
972
+ // Create .env.local
973
+ if (monorepoPath) {
974
+ const envLocalPath = join(monorepoPath, 'apps', 'hatchway', '.env.local');
975
+ const envContent = [
976
+ '# Auto-generated by hatchway CLI',
977
+ `# Generated at: ${new Date().toISOString()}`,
978
+ '',
979
+ 'HATCHWAY_LOCAL_MODE=true',
980
+ `RUNNER_SHARED_SECRET=${generatedSecret}`,
981
+ `WORKSPACE_ROOT=${workspace}`,
982
+ 'RUNNER_ID=local',
983
+ 'RUNNER_DEFAULT_ID=local',
984
+ `DATABASE_URL=${databaseUrl || ''}`,
985
+ '',
986
+ ].join('\n');
987
+ await writeFile(envLocalPath, envContent);
988
+ }
989
+ completeTask('config');
990
+ await sleep(layout.taskCompletionDelay);
991
+ }
992
+ catch (error) {
993
+ failTask('config', 'Config save failed');
994
+ failStep('ready');
995
+ setError('Failed to save configuration', [
996
+ 'Check file permissions',
997
+ 'Try running from a different directory',
998
+ ]);
999
+ throw error;
1000
+ }
1001
+ // Build all services for production
1002
+ if (monorepoPath) {
1003
+ startTask('services', 'Building services (this may take a minute)...');
1004
+ // Track build output for error reporting (capture both stdout and stderr)
1005
+ let buildOutput = '';
1006
+ let buildError = '';
1007
+ try {
1008
+ const { spawn } = await import('child_process');
1009
+ await new Promise((resolve, reject) => {
1010
+ const buildProcess = spawn('pnpm', ['build:all'], {
1011
+ cwd: monorepoPath,
1012
+ stdio: 'pipe',
1013
+ shell: true,
1014
+ });
1015
+ // Capture stdout - many build tools output errors here
1016
+ buildProcess.stdout?.on('data', (data) => {
1017
+ buildOutput += data.toString();
1018
+ });
1019
+ // Capture stderr
1020
+ buildProcess.stderr?.on('data', (data) => {
1021
+ buildError += data.toString();
1022
+ });
1023
+ buildProcess.on('close', (code) => {
1024
+ if (code === 0)
1025
+ resolve();
1026
+ else
1027
+ reject(new Error(`Build failed with code ${code}`));
1028
+ });
1029
+ buildProcess.on('error', reject);
1030
+ });
1031
+ completeTask('services');
1032
+ await sleep(layout.taskCompletionDelay);
1033
+ }
1034
+ catch (error) {
1035
+ // Build failed - surface the error to the user
1036
+ failTask('services', 'Build failed');
1037
+ failStep('ready');
1038
+ // Combine all output for analysis
1039
+ const allOutput = (buildOutput + '\n' + buildError).trim();
1040
+ const allLines = allOutput.split('\n');
1041
+ // Find the most relevant error lines with better patterns
1042
+ const errorPatterns = [
1043
+ /error TS\d+:/i, // TypeScript errors
1044
+ /error:/i, // General errors
1045
+ /Error:/, // Error messages
1046
+ /ERR!/, // npm/pnpm errors
1047
+ /failed/i, // Failed messages
1048
+ /Cannot find/i, // Module not found
1049
+ /Module not found/i, // Webpack/Next.js errors
1050
+ /SyntaxError/i, // Syntax errors
1051
+ /TypeError/i, // Type errors
1052
+ /ReferenceError/i, // Reference errors
1053
+ /ENOENT/i, // File not found
1054
+ /✖|✗|×/, // Error symbols
1055
+ ];
1056
+ // Find lines that match error patterns
1057
+ const relevantLines = [];
1058
+ let inErrorBlock = false;
1059
+ for (let i = 0; i < allLines.length; i++) {
1060
+ const line = allLines[i];
1061
+ const isErrorLine = errorPatterns.some(pattern => pattern.test(line));
1062
+ if (isErrorLine) {
1063
+ inErrorBlock = true;
1064
+ // Include 1 line before for context if available
1065
+ if (i > 0 && relevantLines.length === 0) {
1066
+ const prevLine = allLines[i - 1].trim();
1067
+ if (prevLine && !prevLine.startsWith('>')) {
1068
+ relevantLines.push(prevLine);
1069
+ }
1070
+ }
1071
+ }
1072
+ if (inErrorBlock) {
1073
+ relevantLines.push(line.trim());
1074
+ // Collect more lines for scrollable view (max 50 lines)
1075
+ if (relevantLines.length >= 50)
1076
+ break;
1077
+ }
1078
+ // End error block on empty line or success indicators
1079
+ if (inErrorBlock && (line.trim() === '' || /successfully|completed/i.test(line))) {
1080
+ // Keep going in case there are more errors
1081
+ inErrorBlock = false;
1082
+ }
1083
+ }
1084
+ // If no specific errors found, show last 30 lines of output
1085
+ const displayLines = relevantLines.length > 0
1086
+ ? relevantLines
1087
+ : allLines.slice(-30);
1088
+ const suggestions = [
1089
+ 'To debug further, run manually:',
1090
+ ` cd ${monorepoPath} && pnpm build:all`,
1091
+ '',
1092
+ 'Common fixes:',
1093
+ ' - Run: pnpm install (missing dependencies)',
1094
+ ' - Check for TypeScript errors in the files mentioned above',
1095
+ ' - Ensure all environment variables are set',
1096
+ ];
1097
+ // Use setBuildError to show full-screen scrollable error view
1098
+ setBuildError('Production build failed', displayLines, suggestions);
1099
+ throw new CLIError({
1100
+ code: 'BUILD_FAILED',
1101
+ message: 'Failed to build services for production',
1102
+ suggestions: [
1103
+ `Run manually: cd ${monorepoPath} && pnpm build:all`,
1104
+ 'Check for TypeScript errors in the output above',
1105
+ 'Ensure all dependencies are installed: pnpm install',
1106
+ ],
1107
+ });
1108
+ }
1109
+ }
1110
+ // Validate configuration
1111
+ const validation = configManager.validate();
1112
+ if (!validation.valid) {
1113
+ failStep('ready');
1114
+ setError('Configuration invalid', validation.errors);
1115
+ throw new CLIError({
1116
+ code: 'CONFIG_INVALID',
1117
+ message: 'Configuration validation failed',
1118
+ });
1119
+ }
1120
+ completeStep('ready');
1121
+ return {
1122
+ workspace,
1123
+ monorepoPath,
1124
+ databaseUrl,
1125
+ apiUrl,
1126
+ runnerId: 'local',
1127
+ };
1128
+ }
1129
+
1130
+ export { initTUICommand };
1131
+ //# sourceMappingURL=init-tui-D2VOVdeK.js.map