@hatchway/cli 0.50.68 → 0.50.70

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.
@@ -0,0 +1,696 @@
1
+ // Hatchway CLI - Built with Rollup
2
+ import chalk from 'chalk';
3
+ import { homedir, userInfo } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { r as reactExports, u as useInput, j as jsxRuntimeExports, B as Box, c as colors, T as Text, a as useStdout, b as render, R as React } from './theme-CzLXk_6s.js';
6
+ import { l as logger } from './logger-6V5cBxba.js';
7
+ import { c as configManager } from './config-manager-DST6RbP8.js';
8
+ import { startRunner } from '../index.js';
9
+ import 'node:stream';
10
+ import 'node:process';
11
+ import 'node:events';
12
+ import { u as useApp } from './use-app-Dw8M2HNg.js';
13
+ import { u as useBuildState, a as useLogEntries, B as BuildPanel } from './useBuildState-c-9oir2y.js';
14
+ import { g as getVersionInfo, B as Banner } from './Banner-WeSdAE2V.js';
15
+ import { T as TextInput } from './index-CovlIWnu.js';
16
+ import { g as getLogBuffer, i as initRunnerLogger, s as setFileLoggerTuiMode } from './runner-logger-instance-Dj_JMznn.js';
17
+ import { o as openBrowser, h as hasStoredToken, g as getStoredToken, p as performOAuthLogin, s as storeToken } from './cli-auth-B4Do-N8Y.js';
18
+ import 'node:fs';
19
+ import 'assert';
20
+ import 'events';
21
+ import 'module';
22
+ import 'node:buffer';
23
+ import 'conf';
24
+ import '@anthropic-ai/claude-agent-sdk';
25
+ import '@openai/codex-sdk';
26
+ import 'dotenv';
27
+ import 'node:url';
28
+ import 'fs/promises';
29
+ import 'path';
30
+ import 'ws';
31
+ import 'drizzle-orm/node-postgres';
32
+ import 'pg';
33
+ import 'drizzle-orm/pg-core';
34
+ import 'drizzle-orm';
35
+ import 'crypto';
36
+ import 'drizzle-orm/node-postgres/migrator';
37
+ import 'zod';
38
+ import 'node:child_process';
39
+ import 'node:crypto';
40
+ import 'express';
41
+ import 'node:net';
42
+ import 'node:fs/promises';
43
+ import 'simple-git';
44
+ import 'os';
45
+ import 'fs';
46
+ import './manager-0U0BIO9r.js';
47
+ import 'http';
48
+ import 'http-proxy';
49
+ import 'zlib';
50
+ import 'node:http';
51
+
52
+ function LogPanel({ entries, isVerbose, width, height, isFocused }) {
53
+ const [scrollOffset, setScrollOffset] = reactExports.useState(0);
54
+ const [autoScroll, setAutoScroll] = reactExports.useState(true);
55
+ // Filter entries based on verbose mode
56
+ const visibleEntries = isVerbose
57
+ ? entries
58
+ : entries.filter(e => !e.verbose);
59
+ // Available height for log lines (subtract 2 for border, 1 for header)
60
+ const visibleLines = Math.max(1, height - 3);
61
+ // Auto-scroll when new entries arrive
62
+ reactExports.useEffect(() => {
63
+ if (autoScroll && visibleEntries.length > 0) {
64
+ const maxScroll = Math.max(0, visibleEntries.length - visibleLines);
65
+ setScrollOffset(maxScroll);
66
+ }
67
+ }, [visibleEntries.length, autoScroll, visibleLines]);
68
+ // Handle keyboard navigation
69
+ useInput((input, key) => {
70
+ if (!isFocused)
71
+ return;
72
+ if (key.upArrow) {
73
+ setAutoScroll(false);
74
+ setScrollOffset(prev => Math.max(0, prev - 1));
75
+ }
76
+ else if (key.downArrow) {
77
+ const maxScroll = Math.max(0, visibleEntries.length - visibleLines);
78
+ setScrollOffset(prev => Math.min(maxScroll, prev + 1));
79
+ // Re-enable auto-scroll if we're at the bottom
80
+ if (scrollOffset >= maxScroll - 1) {
81
+ setAutoScroll(true);
82
+ }
83
+ }
84
+ else if (key.pageUp) {
85
+ setAutoScroll(false);
86
+ setScrollOffset(prev => Math.max(0, prev - visibleLines));
87
+ }
88
+ else if (key.pageDown) {
89
+ const maxScroll = Math.max(0, visibleEntries.length - visibleLines);
90
+ setScrollOffset(prev => Math.min(maxScroll, prev + visibleLines));
91
+ }
92
+ });
93
+ // Get visible slice of entries
94
+ const displayedEntries = visibleEntries.slice(scrollOffset, scrollOffset + visibleLines);
95
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "single", borderColor: isFocused ? colors.cyan : colors.darkGray, paddingX: 1, children: [jsxRuntimeExports.jsxs(Box, { justifyContent: "space-between", marginBottom: 0, children: [jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: "LOGS" }), jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["[verbose: ", isVerbose ? 'on' : 'off', "]"] })] }), jsxRuntimeExports.jsx(Box, { flexDirection: "column", flexGrow: 1, children: displayedEntries.length === 0 ? (jsxRuntimeExports.jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "Waiting for logs..." }) })) : (displayedEntries.map((entry, index) => (jsxRuntimeExports.jsx(LogEntryRow, { entry: entry, maxWidth: width - 4 }, entry.id)))) }), visibleEntries.length > visibleLines && (jsxRuntimeExports.jsx(Box, { justifyContent: "flex-end", children: jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: [scrollOffset + 1, "-", Math.min(scrollOffset + visibleLines, visibleEntries.length), "/", visibleEntries.length, autoScroll ? ' (auto)' : ''] }) }))] }));
96
+ }
97
+ // Individual log entry row
98
+ function LogEntryRow({ entry, maxWidth }) {
99
+ const time = new Date(entry.timestamp).toLocaleTimeString('en-US', {
100
+ hour12: false,
101
+ hour: '2-digit',
102
+ minute: '2-digit',
103
+ second: '2-digit',
104
+ });
105
+ const levelColors = {
106
+ debug: colors.dimGray,
107
+ info: colors.cyan,
108
+ success: colors.success,
109
+ warn: colors.warning,
110
+ error: colors.error,
111
+ };
112
+ const levelIcons = {
113
+ debug: ' ',
114
+ info: '●',
115
+ success: '✓',
116
+ warn: '⚠',
117
+ error: '✗',
118
+ };
119
+ // Tool calls get special formatting
120
+ if (entry.toolName) {
121
+ const argsText = entry.toolArgs ? ` ${entry.toolArgs}` : '';
122
+ `${entry.toolName}${argsText}`;
123
+ return (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: time }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: " \uD83D\uDD27 " }), jsxRuntimeExports.jsx(Text, { color: colors.white, children: entry.toolName }), entry.toolArgs && jsxRuntimeExports.jsxs(Text, { color: colors.gray, children: [" ", entry.toolArgs] })] }));
124
+ }
125
+ // Regular log entries
126
+ const color = levelColors[entry.level];
127
+ const icon = levelIcons[entry.level];
128
+ // Truncate message if needed
129
+ const availableWidth = maxWidth - 12; // time + space + icon + space
130
+ const truncatedMessage = entry.message.length > availableWidth
131
+ ? entry.message.substring(0, availableWidth - 3) + '...'
132
+ : entry.message;
133
+ return (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: time }), jsxRuntimeExports.jsxs(Text, { color: color, children: [" ", icon, " "] }), jsxRuntimeExports.jsx(Text, { color: color, children: truncatedMessage })] }));
134
+ }
135
+
136
+ function StatusBar({ isConnected, isVerbose, buildCount = 0, currentBuildIndex = 0, view, }) {
137
+ // Get version info
138
+ const versionInfo = getVersionInfo();
139
+ // Connection indicator
140
+ const connectionIndicator = (jsxRuntimeExports.jsxs(Box, { marginRight: 2, children: [jsxRuntimeExports.jsx(Text, { color: isConnected ? colors.success : colors.error, children: isConnected ? '●' : '○' }), jsxRuntimeExports.jsxs(Text, { color: colors.gray, children: [' ', isConnected ? 'Connected' : 'Disconnected'] })] }));
141
+ // Build count indicator (only show if multiple builds)
142
+ const buildIndicator = buildCount > 1 ? (jsxRuntimeExports.jsx(Box, { marginRight: 2, children: jsxRuntimeExports.jsxs(Text, { color: colors.gray, children: ["Build ", currentBuildIndex + 1, "/", buildCount] }) })) : null;
143
+ // Shortcuts based on current view
144
+ const shortcuts = view === 'dashboard' ? (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Shortcut$1, { letter: "q", label: "quit" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "b", label: "browser" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "v", label: `verbose: ${isVerbose ? 'on' : 'off'}` }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "c", label: "copy" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "t", label: "text view" }), buildCount > 1 && jsxRuntimeExports.jsx(Shortcut$1, { letter: "n/p", label: "switch build" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "\u2191\u2193", label: "scroll" })] })) : view === 'fullLog' ? (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Shortcut$1, { letter: "t", label: "dashboard" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "c", label: "copy" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "/", label: "search" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "f", label: "filter" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "\u2191\u2193", label: "scroll" }), jsxRuntimeExports.jsx(Shortcut$1, { letter: "PgUp/Dn", label: "page" })] })) : (jsxRuntimeExports.jsx(Box, { children: jsxRuntimeExports.jsx(Shortcut$1, { letter: "Esc", label: "cancel" }) }));
145
+ // Version display
146
+ const versionDisplay = (jsxRuntimeExports.jsx(Box, { marginLeft: 2, children: jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: versionInfo.display }) }));
147
+ return (jsxRuntimeExports.jsxs(Box, { borderStyle: "single", borderColor: colors.darkGray, paddingX: 1, justifyContent: "space-between", children: [jsxRuntimeExports.jsxs(Box, { children: [connectionIndicator, buildIndicator] }), jsxRuntimeExports.jsxs(Box, { children: [shortcuts, versionDisplay] })] }));
148
+ }
149
+ // Helper component for shortcuts
150
+ function Shortcut$1({ letter, label }) {
151
+ return (jsxRuntimeExports.jsxs(Box, { marginRight: 2, children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "[" }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: letter }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "]" }), jsxRuntimeExports.jsx(Text, { color: colors.gray, children: label })] }));
152
+ }
153
+
154
+ function FullLogView({ entries, onBack, onCopy }) {
155
+ const { stdout } = useStdout();
156
+ const terminalHeight = stdout?.rows || 24;
157
+ stdout?.columns || 80;
158
+ const [searchQuery, setSearchQuery] = reactExports.useState('');
159
+ const [isSearching, setIsSearching] = reactExports.useState(false);
160
+ const [filterMode, setFilterMode] = reactExports.useState('all');
161
+ const [scrollOffset, setScrollOffset] = reactExports.useState(0);
162
+ const [searchMode, setSearchMode] = reactExports.useState('highlight');
163
+ // Available height for log lines
164
+ const headerHeight = 3;
165
+ const footerHeight = 2;
166
+ const visibleLines = Math.max(1, terminalHeight - headerHeight - footerHeight);
167
+ // Filter and search entries
168
+ const processedEntries = entries.filter(entry => {
169
+ // Apply filter mode
170
+ if (filterMode === 'errors' && entry.level !== 'error' && entry.level !== 'warn') {
171
+ return false;
172
+ }
173
+ if (filterMode === 'tools' && !entry.toolName) {
174
+ return false;
175
+ }
176
+ if (filterMode !== 'verbose' && entry.verbose) {
177
+ return false;
178
+ }
179
+ // Apply search filter (if in filter mode)
180
+ if (searchQuery && searchMode === 'filter') {
181
+ const query = searchQuery.toLowerCase();
182
+ const messageMatch = entry.message.toLowerCase().includes(query);
183
+ const toolMatch = entry.toolName?.toLowerCase().includes(query);
184
+ const argsMatch = entry.toolArgs?.toLowerCase().includes(query);
185
+ return messageMatch || toolMatch || argsMatch;
186
+ }
187
+ return true;
188
+ });
189
+ // Calculate max scroll
190
+ const maxScroll = Math.max(0, processedEntries.length - visibleLines);
191
+ // Get visible entries
192
+ const visibleEntries = processedEntries.slice(scrollOffset, scrollOffset + visibleLines);
193
+ // Handle keyboard input
194
+ useInput((input, key) => {
195
+ if (isSearching) {
196
+ if (key.escape || key.return) {
197
+ setIsSearching(false);
198
+ }
199
+ return;
200
+ }
201
+ if (input === 't') {
202
+ onBack();
203
+ }
204
+ else if (input === 'c') {
205
+ onCopy();
206
+ }
207
+ else if (input === '/') {
208
+ setIsSearching(true);
209
+ }
210
+ else if (input === 'f') {
211
+ // Cycle through filter modes
212
+ const modes = ['all', 'errors', 'tools', 'verbose'];
213
+ const currentIndex = modes.indexOf(filterMode);
214
+ setFilterMode(modes[(currentIndex + 1) % modes.length]);
215
+ setScrollOffset(0);
216
+ }
217
+ else if (input === 'm') {
218
+ // Toggle search mode
219
+ setSearchMode(prev => prev === 'filter' ? 'highlight' : 'filter');
220
+ }
221
+ else if (key.upArrow) {
222
+ setScrollOffset(prev => Math.max(0, prev - 1));
223
+ }
224
+ else if (key.downArrow) {
225
+ setScrollOffset(prev => Math.min(maxScroll, prev + 1));
226
+ }
227
+ else if (key.pageUp) {
228
+ setScrollOffset(prev => Math.max(0, prev - visibleLines));
229
+ }
230
+ else if (key.pageDown) {
231
+ setScrollOffset(prev => Math.min(maxScroll, prev + visibleLines));
232
+ }
233
+ else if (key.escape) {
234
+ if (searchQuery) {
235
+ setSearchQuery('');
236
+ }
237
+ else {
238
+ onBack();
239
+ }
240
+ }
241
+ });
242
+ // Format time
243
+ const formatTime = (timestamp) => {
244
+ return new Date(timestamp).toLocaleTimeString('en-US', {
245
+ hour12: false,
246
+ hour: '2-digit',
247
+ minute: '2-digit',
248
+ second: '2-digit',
249
+ });
250
+ };
251
+ // Check if text matches search query
252
+ const highlightSearch = (text) => {
253
+ if (!searchQuery || searchMode !== 'highlight') {
254
+ return text;
255
+ }
256
+ const query = searchQuery.toLowerCase();
257
+ const index = text.toLowerCase().indexOf(query);
258
+ if (index === -1) {
259
+ return text;
260
+ }
261
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [text.slice(0, index), jsxRuntimeExports.jsx(Text, { backgroundColor: colors.warning, color: "black", children: text.slice(index, index + searchQuery.length) }), text.slice(index + searchQuery.length)] }));
262
+ };
263
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [jsxRuntimeExports.jsxs(Box, { borderStyle: "single", borderColor: colors.darkGray, paddingX: 1, justifyContent: "space-between", children: [jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: "LOGS" }), jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "Search: " }), isSearching ? (jsxRuntimeExports.jsx(Box, { borderStyle: "round", borderColor: colors.cyan, paddingX: 1, children: jsxRuntimeExports.jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "type to search..." }) })) : (jsxRuntimeExports.jsxs(Text, { color: searchQuery ? colors.white : colors.dimGray, children: ["[", searchQuery || 'none', "] (", searchMode, ")"] })), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: " [/]" })] })] }), jsxRuntimeExports.jsx(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: colors.darkGray, borderTop: false, borderBottom: false, paddingX: 1, children: visibleEntries.map((entry, index) => {
264
+ const time = formatTime(entry.timestamp);
265
+ const levelColors = {
266
+ debug: colors.dimGray,
267
+ info: colors.cyan,
268
+ success: colors.success,
269
+ warn: colors.warning,
270
+ error: colors.error,
271
+ };
272
+ const levelIcons = {
273
+ debug: ' ',
274
+ info: '●',
275
+ success: '✓',
276
+ warn: '⚠',
277
+ error: '✗',
278
+ };
279
+ if (entry.toolName) {
280
+ return (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: time }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: " \uD83D\uDD27 " }), jsxRuntimeExports.jsx(Text, { color: colors.white, children: highlightSearch(entry.toolName) }), entry.toolArgs && (jsxRuntimeExports.jsxs(Text, { color: colors.gray, children: [" ", highlightSearch(entry.toolArgs)] }))] }, entry.id));
281
+ }
282
+ return (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: time }), jsxRuntimeExports.jsxs(Text, { color: levelColors[entry.level], children: [" ", levelIcons[entry.level], " "] }), jsxRuntimeExports.jsx(Text, { color: levelColors[entry.level], children: highlightSearch(entry.message) })] }, entry.id));
283
+ }) }), jsxRuntimeExports.jsxs(Box, { borderStyle: "single", borderColor: colors.darkGray, paddingX: 1, justifyContent: "space-between", children: [jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Shortcut, { letter: "t", label: "dashboard" }), jsxRuntimeExports.jsx(Shortcut, { letter: "c", label: "copy" }), jsxRuntimeExports.jsx(Shortcut, { letter: "/", label: "search" }), jsxRuntimeExports.jsx(Shortcut, { letter: "f", label: `filter: ${filterMode}` }), jsxRuntimeExports.jsx(Shortcut, { letter: "m", label: `mode: ${searchMode}` })] }), jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: [scrollOffset + 1, "-", Math.min(scrollOffset + visibleLines, processedEntries.length), "/", processedEntries.length] })] })] }));
284
+ }
285
+ function Shortcut({ letter, label }) {
286
+ return (jsxRuntimeExports.jsxs(Box, { marginRight: 2, children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "[" }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: letter }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "]" }), jsxRuntimeExports.jsx(Text, { color: colors.gray, children: label })] }));
287
+ }
288
+
289
+ function CopyMenu({ onSelect, onCancel, visibleCount, totalCount }) {
290
+ useInput((input, key) => {
291
+ if (key.escape) {
292
+ onCancel();
293
+ }
294
+ else if (input === '1') {
295
+ onSelect('visible');
296
+ }
297
+ else if (input === '2') {
298
+ onSelect('last50');
299
+ }
300
+ else if (input === '3') {
301
+ onSelect('last100');
302
+ }
303
+ else if (input === '4') {
304
+ onSelect('all');
305
+ }
306
+ else if (input === '5') {
307
+ onSelect('range');
308
+ }
309
+ });
310
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.cyan, paddingX: 2, paddingY: 1, children: [jsxRuntimeExports.jsx(Box, { marginBottom: 1, children: jsxRuntimeExports.jsx(Text, { color: colors.cyan, bold: true, children: "Copy Logs" }) }), jsxRuntimeExports.jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [jsxRuntimeExports.jsx(CopyOption, { number: "1", label: `Copy visible (${visibleCount} lines)` }), jsxRuntimeExports.jsx(CopyOption, { number: "2", label: "Copy last 50 lines" }), jsxRuntimeExports.jsx(CopyOption, { number: "3", label: "Copy last 100 lines" }), jsxRuntimeExports.jsx(CopyOption, { number: "4", label: `Copy all from file (${totalCount} lines)` }), jsxRuntimeExports.jsx(CopyOption, { number: "5", label: "Copy range..." })] }), jsxRuntimeExports.jsx(Box, { children: jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "[Esc] Cancel" }) })] }));
311
+ }
312
+ function CopyOption({ number, label }) {
313
+ return (jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "[" }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: number }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: "]" }), jsxRuntimeExports.jsxs(Text, { color: colors.white, children: [" ", label] })] }));
314
+ }
315
+
316
+ function RunnerDashboard({ config, onQuit }) {
317
+ const { exit } = useApp();
318
+ const { stdout } = useStdout();
319
+ const terminalHeight = stdout?.rows || 24;
320
+ const terminalWidth = stdout?.columns || 80;
321
+ // State management
322
+ const [buildState, buildActions] = useBuildState();
323
+ const logEntries = useLogEntries(100);
324
+ const [view, setView] = reactExports.useState('dashboard');
325
+ // Track if we've opened browser on first connection
326
+ const hasOpenedBrowserRef = reactExports.useRef(false);
327
+ // Get the dashboard URL (use apiUrl if provided, otherwise default to hatchway.sh)
328
+ const dashboardUrl = config.apiUrl || 'https://hatchway.sh';
329
+ // Open browser handler
330
+ const handleOpenBrowser = reactExports.useCallback(() => {
331
+ openBrowser(dashboardUrl).catch(() => {
332
+ // Silently fail - user can manually open the URL
333
+ });
334
+ }, [dashboardUrl]);
335
+ // Auto-open browser on first successful connection
336
+ reactExports.useEffect(() => {
337
+ if (buildState.isConnected && !hasOpenedBrowserRef.current) {
338
+ hasOpenedBrowserRef.current = true;
339
+ // Small delay to ensure the TUI is fully rendered before opening browser
340
+ setTimeout(() => {
341
+ handleOpenBrowser();
342
+ }, 500);
343
+ }
344
+ }, [buildState.isConnected, handleOpenBrowser]);
345
+ // Handle keyboard input
346
+ useInput((input, key) => {
347
+ // Global quit handler
348
+ if (input === 'q' && view !== 'copyMenu') {
349
+ if (onQuit) {
350
+ onQuit();
351
+ }
352
+ exit();
353
+ return;
354
+ }
355
+ // Copy menu is modal - handle separately
356
+ if (view === 'copyMenu') {
357
+ return; // CopyMenu handles its own input
358
+ }
359
+ // Dashboard shortcuts
360
+ if (view === 'dashboard') {
361
+ if (input === 'v') {
362
+ buildActions.toggleVerbose();
363
+ }
364
+ else if (input === 'c') {
365
+ setView('copyMenu');
366
+ }
367
+ else if (input === 't') {
368
+ setView('fullLog');
369
+ }
370
+ else if (input === 'n') {
371
+ buildActions.nextBuild();
372
+ }
373
+ else if (input === 'p') {
374
+ buildActions.prevBuild();
375
+ }
376
+ else if (input === 'b') {
377
+ handleOpenBrowser();
378
+ }
379
+ }
380
+ // Full log view shortcuts
381
+ if (view === 'fullLog') {
382
+ if (input === 't') {
383
+ setView('dashboard');
384
+ }
385
+ else if (input === 'c') {
386
+ setView('copyMenu');
387
+ }
388
+ }
389
+ });
390
+ // Handle copy action
391
+ const handleCopy = reactExports.useCallback(async (option) => {
392
+ try {
393
+ const buffer = getLogBuffer();
394
+ let entriesToCopy = [];
395
+ switch (option) {
396
+ case 'visible':
397
+ entriesToCopy = logEntries.slice(-20); // Approximate visible count
398
+ break;
399
+ case 'last50':
400
+ entriesToCopy = buffer.getRecent(50);
401
+ break;
402
+ case 'last100':
403
+ entriesToCopy = buffer.getRecent(100);
404
+ break;
405
+ case 'all':
406
+ entriesToCopy = buffer.readFromFile();
407
+ break;
408
+ case 'range':
409
+ // TODO: Implement range selection
410
+ entriesToCopy = buffer.getRecent(100);
411
+ break;
412
+ }
413
+ const text = buffer.toText(entriesToCopy);
414
+ // Copy to clipboard using pbcopy on macOS
415
+ const { spawn } = await import('child_process');
416
+ const pbcopy = spawn('pbcopy');
417
+ pbcopy.stdin.write(text);
418
+ pbcopy.stdin.end();
419
+ // TODO: Show success message
420
+ }
421
+ catch (error) {
422
+ console.error('Failed to copy to clipboard:', error);
423
+ }
424
+ setView('dashboard');
425
+ }, [logEntries]);
426
+ // Calculate panel dimensions
427
+ const bannerHeight = 7; // ASCII art banner
428
+ const headerHeight = 3; // Config/status line
429
+ const statusBarHeight = 3;
430
+ const contentHeight = Math.max(1, terminalHeight - bannerHeight - headerHeight - statusBarHeight);
431
+ // 20/80 split
432
+ const buildPanelWidth = Math.floor(terminalWidth * 0.2);
433
+ const logPanelWidth = terminalWidth - buildPanelWidth;
434
+ // Show build panel only when there's an active build
435
+ const showBuildPanel = buildState.currentBuild !== null;
436
+ // Check for available update (set by auto-update check in index.ts)
437
+ const updateAvailable = process.env.HATCHWAY_UPDATE_AVAILABLE;
438
+ return (jsxRuntimeExports.jsxs(Box, { flexDirection: "column", height: terminalHeight, width: terminalWidth, children: [jsxRuntimeExports.jsx(Banner, {}), updateAvailable && (jsxRuntimeExports.jsxs(Box, { justifyContent: "center", paddingY: 0, children: [jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: "\u2B06 Update available: " }), jsxRuntimeExports.jsx(Text, { color: colors.success, children: updateAvailable }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: " \u2014 Run " }), jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: "hatchway upgrade" }), jsxRuntimeExports.jsx(Text, { color: colors.dimGray, children: " to update" })] })), jsxRuntimeExports.jsxs(Box, { borderStyle: "single", borderColor: colors.darkGray, paddingX: 1, justifyContent: "space-between", children: [jsxRuntimeExports.jsxs(Text, { color: colors.dimGray, children: ["Runner: ", jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: config.runnerId }), " \u2022 Server: ", jsxRuntimeExports.jsx(Text, { color: colors.cyan, children: config.serverUrl.replace(/^wss?:\/\//, '') })] }), jsxRuntimeExports.jsxs(Box, { children: [jsxRuntimeExports.jsx(Text, { color: buildState.isConnected ? colors.success : colors.error, children: buildState.isConnected ? '●' : '○' }), jsxRuntimeExports.jsxs(Text, { color: colors.gray, children: [' ', buildState.isConnected ? 'Connected' : 'Disconnected'] })] })] }), view === 'dashboard' && (jsxRuntimeExports.jsxs(Box, { flexGrow: 1, height: contentHeight, children: [showBuildPanel && (jsxRuntimeExports.jsx(BuildPanel, { build: buildState.currentBuild, width: buildPanelWidth, height: contentHeight })), jsxRuntimeExports.jsx(LogPanel, { entries: logEntries, isVerbose: buildState.isVerbose, width: showBuildPanel ? logPanelWidth : terminalWidth, height: contentHeight, isFocused: true })] })), view === 'fullLog' && (jsxRuntimeExports.jsx(FullLogView, { entries: logEntries, onBack: () => setView('dashboard'), onCopy: () => setView('copyMenu') })), view === 'copyMenu' && (jsxRuntimeExports.jsx(Box, { position: "absolute", flexDirection: "column", justifyContent: "center", alignItems: "center", width: terminalWidth, height: terminalHeight, children: jsxRuntimeExports.jsx(CopyMenu, { onSelect: handleCopy, onCancel: () => setView('dashboard'), visibleCount: Math.min(20, logEntries.length), totalCount: getLogBuffer().readFromFile().length }) })), view === 'dashboard' && (jsxRuntimeExports.jsx(StatusBar, { isConnected: buildState.isConnected, isVerbose: buildState.isVerbose, buildCount: buildState.builds.length, currentBuildIndex: buildState.currentBuildIndex, view: view }))] }));
439
+ }
440
+
441
+ // Default public Hatchway instance
442
+ const DEFAULT_URL = 'https://hatchway.sh';
443
+ const DEFAULT_WORKSPACE = join(homedir(), 'hatchway-workspace');
444
+ /**
445
+ * Normalize URL by adding protocol if missing
446
+ * Uses http:// for localhost, https:// for everything else
447
+ */
448
+ function normalizeUrl(url) {
449
+ if (!url)
450
+ return url;
451
+ // If protocol already present, return as-is
452
+ if (url.match(/^https?:\/\//i)) {
453
+ return url;
454
+ }
455
+ // For localhost or 127.0.0.1, use http://
456
+ if (url.match(/^(localhost|127\.0\.0\.1)(:|\/|$)/i)) {
457
+ return `http://${url}`;
458
+ }
459
+ // For everything else, use https://
460
+ return `https://${url}`;
461
+ }
462
+ /**
463
+ * Derive WebSocket URL from a base HTTP/HTTPS URL
464
+ * Converts https://example.com to wss://example.com/ws/runner
465
+ */
466
+ function deriveWsUrl(baseUrl) {
467
+ const normalized = normalizeUrl(baseUrl);
468
+ const wsProtocol = normalized.startsWith('https://') ? 'wss://' : 'ws://';
469
+ const hostPath = normalized.replace(/^https?:\/\//, '');
470
+ // Remove trailing slash if present
471
+ const cleanHostPath = hostPath.replace(/\/$/, '');
472
+ return `${wsProtocol}${cleanHostPath}/ws/runner`;
473
+ }
474
+ /**
475
+ * Get the current system username
476
+ */
477
+ function getSystemUsername() {
478
+ try {
479
+ return userInfo().username;
480
+ }
481
+ catch {
482
+ // Fallback if userInfo() fails
483
+ return process.env.USER || process.env.USERNAME || 'runner';
484
+ }
485
+ }
486
+ /**
487
+ * Check if we should use TUI
488
+ */
489
+ function shouldUseTUI(options) {
490
+ // Explicit flag
491
+ if (options.noTui)
492
+ return false;
493
+ // CI/CD environments
494
+ if (process.env.CI === '1' || process.env.CI === 'true')
495
+ return false;
496
+ // Not a TTY
497
+ if (!process.stdout.isTTY)
498
+ return false;
499
+ // Explicit env var to disable
500
+ if (process.env.NO_TUI === '1')
501
+ return false;
502
+ return true;
503
+ }
504
+ async function runCommand(options) {
505
+ // Set local mode environment variable if requested
506
+ if (options.local) {
507
+ process.env.HATCHWAY_LOCAL_MODE = 'true';
508
+ logger.info(chalk.yellow('Local mode enabled - authentication bypassed'));
509
+ }
510
+ const useTUI = shouldUseTUI(options);
511
+ // Build runner options from CLI flags or smart defaults
512
+ // NOTE: For the `runner` command, we intentionally ignore local config values
513
+ // and default to the public Hatchway instance. This command is specifically
514
+ // for connecting to remote servers, not local development.
515
+ // Users can still override with CLI flags if needed.
516
+ // Resolve API URL: CLI flag > default public instance (ignore config)
517
+ const apiUrl = normalizeUrl(options.url || DEFAULT_URL);
518
+ // Resolve WebSocket URL: CLI broker flag > derive from API URL (ignore config)
519
+ const wsUrl = options.broker || deriveWsUrl(apiUrl);
520
+ // Resolve workspace: CLI flag > config > default ~/hatchway-workspace
521
+ // (workspace from config is fine since it's user's preference for where projects go)
522
+ const config = configManager.get();
523
+ const workspace = options.workspace || config.workspace || DEFAULT_WORKSPACE;
524
+ // Resolve runner ID: CLI flag > system username (ignore config 'local' default)
525
+ const runnerId = options.runnerId || getSystemUsername();
526
+ // Resolve secret: CLI flag > config (required)
527
+ // Only use config secret if it looks like a valid token (starts with sv_)
528
+ // This prevents the default 'dev-secret' from being used in runner mode
529
+ const configSecret = configManager.getSecret();
530
+ const sharedSecret = options.secret || (configSecret?.startsWith('sv_') ? configSecret : undefined);
531
+ const runnerOptions = {
532
+ wsUrl,
533
+ apiUrl,
534
+ sharedSecret,
535
+ runnerId,
536
+ workspace,
537
+ verbose: options.verbose,
538
+ tuiMode: useTUI,
539
+ };
540
+ // Validate required options - secret is required
541
+ // If not provided, try to use stored OAuth token or trigger OAuth flow
542
+ if (!runnerOptions.sharedSecret) {
543
+ // Check if we have a stored OAuth token
544
+ if (hasStoredToken()) {
545
+ const storedToken = getStoredToken();
546
+ if (storedToken) {
547
+ runnerOptions.sharedSecret = storedToken;
548
+ logger.info(`Using stored runner token: ${chalk.cyan(storedToken.substring(0, 12) + '...')}`);
549
+ }
550
+ }
551
+ // If still no secret and not in local mode, trigger OAuth flow
552
+ if (!runnerOptions.sharedSecret && !options.local) {
553
+ logger.info('No runner token found. Starting OAuth authentication...');
554
+ logger.info('');
555
+ const result = await performOAuthLogin({
556
+ apiUrl: runnerOptions.apiUrl,
557
+ silent: false,
558
+ });
559
+ if (result.success && result.token) {
560
+ storeToken(result.token, runnerOptions.apiUrl);
561
+ runnerOptions.sharedSecret = result.token;
562
+ logger.log('');
563
+ logger.success('Authentication successful!');
564
+ logger.info(`Token: ${chalk.cyan(result.token.substring(0, 12) + '...')}`);
565
+ logger.log('');
566
+ }
567
+ else {
568
+ logger.error(result.error || 'Authentication failed');
569
+ logger.info('');
570
+ logger.info('You can also provide a token manually:');
571
+ logger.info(` ${chalk.cyan('hatchway runner --secret <your-secret>')}`);
572
+ logger.info('');
573
+ logger.info('Or login first:');
574
+ logger.info(` ${chalk.cyan('hatchway login')}`);
575
+ process.exit(1);
576
+ }
577
+ }
578
+ // Final check - if still no secret (and not local mode)
579
+ if (!runnerOptions.sharedSecret && !options.local) {
580
+ logger.error('Shared secret is required');
581
+ logger.info('');
582
+ logger.info('Get a runner key from your Hatchway dashboard, or provide via:');
583
+ logger.info(` ${chalk.cyan('hatchway runner --secret <your-secret>')}`);
584
+ logger.info('');
585
+ logger.info('Or login with OAuth:');
586
+ logger.info(` ${chalk.cyan('hatchway login')}`);
587
+ process.exit(1);
588
+ }
589
+ }
590
+ // ========================================
591
+ // PLAIN TEXT MODE (--no-tui)
592
+ // ========================================
593
+ if (!useTUI) {
594
+ // Display startup info
595
+ logger.section('Starting Hatchway Runner');
596
+ logger.info(`Server: ${chalk.cyan(runnerOptions.wsUrl)}`);
597
+ logger.info(`API URL: ${chalk.cyan(runnerOptions.apiUrl)}`);
598
+ logger.info(`Runner ID: ${chalk.cyan(runnerOptions.runnerId)}`);
599
+ logger.info(`Workspace: ${chalk.cyan(runnerOptions.workspace)}`);
600
+ logger.log('');
601
+ if (options.verbose) {
602
+ logger.debug('Verbose logging enabled');
603
+ logger.debug(`Full options: ${JSON.stringify(runnerOptions, null, 2)}`);
604
+ }
605
+ try {
606
+ // Start the runner (runs indefinitely)
607
+ await startRunner(runnerOptions);
608
+ }
609
+ catch (error) {
610
+ logger.error('Failed to start runner:');
611
+ logger.error(error instanceof Error ? error.message : 'Unknown error');
612
+ if (error instanceof Error && error.stack) {
613
+ logger.debug(error.stack);
614
+ }
615
+ process.exit(1);
616
+ }
617
+ return;
618
+ }
619
+ // ========================================
620
+ // TUI MODE (default)
621
+ // ========================================
622
+ // Initialize the logger BEFORE rendering TUI so the TUI can subscribe to events
623
+ // This must happen before startRunner() which would create its own logger
624
+ initRunnerLogger({
625
+ verbose: options.verbose || false,
626
+ tuiMode: true,
627
+ });
628
+ // Enable TUI mode in file-logger to suppress terminal output
629
+ setFileLoggerTuiMode(true);
630
+ // Track runner cleanup function
631
+ let runnerCleanupFn;
632
+ // Clear screen and enter alternate buffer for clean TUI
633
+ process.stdout.write('\x1b[?1049h'); // Enter alternate screen
634
+ process.stdout.write('\x1b[2J\x1b[H'); // Clear and home
635
+ // Ensure stdin is in raw mode for keyboard input
636
+ if (process.stdin.setRawMode) {
637
+ process.stdin.setRawMode(true);
638
+ }
639
+ process.stdin.resume();
640
+ // Handle quit from TUI
641
+ const handleQuit = async () => {
642
+ // Exit alternate screen buffer
643
+ process.stdout.write('\x1b[?1049l');
644
+ console.log('\n' + chalk.yellow('Shutting down runner...'));
645
+ if (runnerCleanupFn) {
646
+ try {
647
+ await runnerCleanupFn();
648
+ console.log(chalk.green('✓') + ' Runner stopped');
649
+ }
650
+ catch (e) {
651
+ console.error(chalk.red('✗') + ' Error stopping runner:', e);
652
+ }
653
+ }
654
+ process.exit(0);
655
+ };
656
+ // Handle SIGINT (Ctrl+C)
657
+ process.on('SIGINT', handleQuit);
658
+ // Render the TUI dashboard
659
+ const { waitUntilExit, clear } = render(React.createElement(RunnerDashboard, {
660
+ config: {
661
+ runnerId,
662
+ serverUrl: wsUrl,
663
+ workspace,
664
+ apiUrl,
665
+ },
666
+ onQuit: handleQuit,
667
+ }), {
668
+ stdin: process.stdin,
669
+ stdout: process.stdout,
670
+ stderr: process.stderr,
671
+ exitOnCtrlC: false, // We handle this ourselves
672
+ patchConsole: false, // We use our own logging
673
+ });
674
+ try {
675
+ // Start the runner and get cleanup function
676
+ runnerCleanupFn = await startRunner(runnerOptions);
677
+ // Wait for TUI to exit (user pressed 'q')
678
+ await waitUntilExit();
679
+ // Clean up
680
+ clear();
681
+ await handleQuit();
682
+ }
683
+ catch (error) {
684
+ clear();
685
+ process.stdout.write('\x1b[?1049l'); // Exit alternate screen
686
+ logger.error('Failed to start runner:');
687
+ logger.error(error instanceof Error ? error.message : 'Unknown error');
688
+ if (error instanceof Error && error.stack) {
689
+ logger.debug(error.stack);
690
+ }
691
+ process.exit(1);
692
+ }
693
+ }
694
+
695
+ export { runCommand };
696
+ //# sourceMappingURL=run-jC7sIavB.js.map