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