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