@townco/cli 0.1.27 → 0.1.28
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.
- package/dist/commands/run.js +18 -3
- package/dist/components/LogsPane.js +32 -17
- package/dist/components/MergedLogsPane.js +3 -13
- package/dist/components/StatusLine.d.ts +3 -1
- package/dist/components/StatusLine.js +2 -2
- package/dist/components/TabbedOutput.d.ts +1 -0
- package/dist/components/TabbedOutput.js +5 -7
- package/package.json +5 -5
package/dist/commands/run.js
CHANGED
|
@@ -5,8 +5,8 @@ import { readFile } from "node:fs/promises";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { isInsideTownProject } from "@townco/agent/storage";
|
|
7
7
|
import { createLogger } from "@townco/agent/utils";
|
|
8
|
-
import { AcpClient } from "@townco/ui";
|
|
9
|
-
import { ChatView } from "@townco/ui/tui";
|
|
8
|
+
import { AcpClient, configureLogsDir } from "@townco/ui";
|
|
9
|
+
import { ChatView, ChatViewStatus } from "@townco/ui/tui";
|
|
10
10
|
import { Box, render, Text } from "ink";
|
|
11
11
|
import open from "open";
|
|
12
12
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -16,10 +16,16 @@ import { findAvailablePort } from "../lib/port-utils.js";
|
|
|
16
16
|
function TuiRunner({ agentPath, workingDir, onExit }) {
|
|
17
17
|
const [client, setClient] = useState(null);
|
|
18
18
|
const [error, setError] = useState(null);
|
|
19
|
+
// Configure logs directory for UI package loggers BEFORE any loggers are created
|
|
20
|
+
// This must run synchronously before any component initialization
|
|
21
|
+
useMemo(() => {
|
|
22
|
+
configureLogsDir(join(workingDir, ".logs"));
|
|
23
|
+
}, [workingDir]);
|
|
19
24
|
// Session start time for log filtering - set once when component mounts
|
|
25
|
+
// Set to 5 seconds in the past to capture logs generated during initialization
|
|
20
26
|
const sessionStartTime = useMemo(() => {
|
|
21
27
|
const now = new Date();
|
|
22
|
-
now.setSeconds(now.getSeconds() -
|
|
28
|
+
now.setSeconds(now.getSeconds() - 5);
|
|
23
29
|
return now.toISOString();
|
|
24
30
|
}, []);
|
|
25
31
|
// Create logger with agent directory as logs location
|
|
@@ -62,6 +68,11 @@ function TuiRunner({ agentPath, workingDir, onExit }) {
|
|
|
62
68
|
}
|
|
63
69
|
return _jsx(ChatView, { client: client });
|
|
64
70
|
},
|
|
71
|
+
renderStatus: () => {
|
|
72
|
+
if (!client || error)
|
|
73
|
+
return null;
|
|
74
|
+
return _jsx(ChatViewStatus, { client: client });
|
|
75
|
+
},
|
|
65
76
|
},
|
|
66
77
|
{
|
|
67
78
|
name: "Logs",
|
|
@@ -274,6 +285,10 @@ export async function runCommand(options) {
|
|
|
274
285
|
}
|
|
275
286
|
// Default: Start TUI interface with the agent
|
|
276
287
|
logger.info("Starting TUI mode", { name, agentPath });
|
|
288
|
+
// Clear terminal and move cursor to top
|
|
289
|
+
if (process.stdout.isTTY) {
|
|
290
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
291
|
+
}
|
|
277
292
|
// Set stdin to raw mode for Ink
|
|
278
293
|
if (process.stdin.isTTY) {
|
|
279
294
|
process.stdin.setRawMode(true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -36,6 +36,18 @@ export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSes
|
|
|
36
36
|
const [scrollOffset, setScrollOffset] = useState(0); // 0 = at bottom, positive = scrolled up
|
|
37
37
|
const [searchMode, setSearchMode] = useState(false);
|
|
38
38
|
const [searchQuery, setSearchQuery] = useState("");
|
|
39
|
+
const [expandedLogs, setExpandedLogs] = useState(false); // Start collapsed
|
|
40
|
+
const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows ?? 30);
|
|
41
|
+
// Track terminal resize
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const handleResize = () => {
|
|
44
|
+
setTerminalHeight(process.stdout.rows ?? 30);
|
|
45
|
+
};
|
|
46
|
+
process.stdout.on("resize", handleResize);
|
|
47
|
+
return () => {
|
|
48
|
+
process.stdout.off("resize", handleResize);
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
39
51
|
// Use provided session start time or default to 1 second before component mount
|
|
40
52
|
const sessionStartTime = useMemo(() => {
|
|
41
53
|
if (providedSessionStartTime)
|
|
@@ -167,16 +179,6 @@ export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSes
|
|
|
167
179
|
if (searchMode) {
|
|
168
180
|
return;
|
|
169
181
|
}
|
|
170
|
-
// Up arrow to scroll up
|
|
171
|
-
if (key.upArrow) {
|
|
172
|
-
setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, filteredLogs.length - 25)));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
// Down arrow to scroll down
|
|
176
|
-
if (key.downArrow) {
|
|
177
|
-
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
182
|
// 's' to cycle through service filters
|
|
181
183
|
if (input === "s") {
|
|
182
184
|
if (serviceFilter === null) {
|
|
@@ -213,26 +215,39 @@ export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSes
|
|
|
213
215
|
setScrollOffset(0); // Also jump to bottom when clearing
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
218
|
+
// 'e' to toggle expanded/collapsed metadata view
|
|
219
|
+
if (input === "e") {
|
|
220
|
+
setExpandedLogs((prev) => !prev);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
216
223
|
});
|
|
217
224
|
// Apply fuzzy search if active
|
|
218
225
|
const searchedLogs = searchQuery
|
|
219
226
|
? filteredLogs.filter((log) => fuzzyMatch(searchQuery, log.message))
|
|
220
227
|
: filteredLogs;
|
|
221
|
-
// Calculate display window
|
|
228
|
+
// Calculate display window based on terminal height
|
|
229
|
+
// Reserve space for: status line (3), header (3), search box if active (3), footer (3), padding (1)
|
|
230
|
+
const reservedLines = 10 + (searchMode ? 3 : 0);
|
|
231
|
+
const availableLines = Math.max(10, terminalHeight - reservedLines);
|
|
232
|
+
// When expanded, each log entry with metadata takes ~5 lines on average
|
|
233
|
+
// When collapsed, each log entry takes 1 line
|
|
234
|
+
const avgLinesPerEntry = expandedLogs ? 5 : 1;
|
|
235
|
+
const maxLines = Math.floor(availableLines / avgLinesPerEntry);
|
|
222
236
|
const totalLogs = searchedLogs.length;
|
|
223
|
-
const maxLines = 25;
|
|
224
237
|
const isAtBottom = scrollOffset === 0;
|
|
225
238
|
// Calculate which slice of logs to show
|
|
226
239
|
const displayLogs = isAtBottom
|
|
227
|
-
? searchedLogs.slice(-maxLines) // Show last
|
|
240
|
+
? searchedLogs.slice(-maxLines) // Show last N lines when at bottom
|
|
228
241
|
: searchedLogs.slice(Math.max(0, totalLogs - maxLines - scrollOffset), totalLogs - scrollOffset);
|
|
229
242
|
const canScrollUp = totalLogs > maxLines && scrollOffset < totalLogs - maxLines;
|
|
230
243
|
const canScrollDown = scrollOffset > 0;
|
|
231
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%",
|
|
244
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })] })), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: displayLogs.length === 0 ? (_jsx(Text, { dimColor: true, children: "No logs match the current filters. Press 'c' to clear filters." })) : (displayLogs.map((log, idx) => {
|
|
232
245
|
const serviceColor = SERVICE_COLORS[availableServices.indexOf(log.service) % SERVICE_COLORS.length] || "white";
|
|
233
246
|
const levelColor = LOG_COLORS[log.level] || "white";
|
|
234
247
|
const time = new Date(log.timestamp).toLocaleTimeString();
|
|
235
248
|
const keyStr = `${log.timestamp}-${idx}`;
|
|
236
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ["[", time, "]"] }), _jsxs(Text, { color: serviceColor, children: [" [", log.service, "]"] }), _jsxs(Text, { color: levelColor, children: [" [", log.level.toUpperCase(), "]"] }), _jsxs(Text, { children: [" ", log.message] })] },
|
|
237
|
-
|
|
249
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ["[", time, "]"] }), _jsxs(Text, { color: serviceColor, children: [" [", log.service, "]"] }), _jsxs(Text, { color: levelColor, children: [" [", log.level.toUpperCase(), "]"] }), _jsxs(Text, { children: [" ", log.message] })] }), expandedLogs &&
|
|
250
|
+
log.metadata &&
|
|
251
|
+
Object.keys(log.metadata).length > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(log.metadata, null, 2) }) }))] }, keyStr));
|
|
252
|
+
})) }), _jsxs(Box, { borderStyle: "single", borderTop: true, borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [serviceFilter && _jsxs(Text, { children: ["[", serviceFilter, "] "] }), levelFilter && (_jsxs(Text, { children: ["[", ">=", levelFilter, "]", " "] })), searchQuery && _jsxs(Text, { color: "cyan", children: ["[SEARCH: ", searchQuery, "] "] }), !isAtBottom && _jsx(Text, { color: "yellow", children: "[SCROLLED] " }), expandedLogs && _jsx(Text, { color: "green", children: "[EXPANDED] " }), _jsxs(Text, { dimColor: true, children: [displayLogs.length, "/", totalLogs, " logs", scrollOffset > 0 && ` (${scrollOffset} from bottom)`] })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "(/)search | (s)ervice | (l)evel | (e)xpand | (c)lear | ESC" }) })] })] }));
|
|
238
253
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import { useEffect, useMemo, useState } from "react";
|
|
@@ -86,16 +86,6 @@ export function MergedLogsPane({ services, onClear }) {
|
|
|
86
86
|
if (searchMode) {
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
|
-
// Up arrow to scroll up
|
|
90
|
-
if (key.upArrow) {
|
|
91
|
-
setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, mergedLines.length - maxLines)));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
// Down arrow to scroll down
|
|
95
|
-
if (key.downArrow) {
|
|
96
|
-
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
89
|
// 's' to cycle through service filters
|
|
100
90
|
if (input === "s") {
|
|
101
91
|
if (serviceFilter === null) {
|
|
@@ -190,12 +180,12 @@ export function MergedLogsPane({ services, onClear }) {
|
|
|
190
180
|
: overallStatus === "starting"
|
|
191
181
|
? "yellow"
|
|
192
182
|
: "gray";
|
|
193
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [
|
|
183
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, marginBottom: 1, flexShrink: 0, children: [_jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })] })), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, paddingX: 1, children: displayLines.length === 0 ? (_jsx(Text, { dimColor: true, children: searchQuery || serviceFilter || levelFilter
|
|
194
184
|
? "No logs match the current filters. Press 'c' to clear, 's' for service, or 'l' for level."
|
|
195
185
|
: "Waiting for output..." })) : (displayLines.map((logLine, idx) => {
|
|
196
186
|
const serviceColor = SERVICE_COLORS[availableServices.indexOf(logLine.service) %
|
|
197
187
|
SERVICE_COLORS.length] || "white";
|
|
198
188
|
const keyStr = `${logLine.service}-${logLine.serviceIndex}-${logLine.index}-${idx}`;
|
|
199
189
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: serviceColor, children: ["[", logLine.service, "]"] }), _jsxs(Text, { children: [" ", logLine.line] })] }, keyStr));
|
|
200
|
-
})) }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Text, {
|
|
190
|
+
})) }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: statusColor, children: "\u25CF" }), _jsxs(Text, { children: [" ", overallStatus] }), serviceFilter && _jsxs(Text, { children: [" | [", serviceFilter, "]"] }), levelFilter && (_jsxs(Text, { children: [" ", "| [", ">=", levelFilter, "]"] })), searchQuery && _jsxs(Text, { color: "cyan", children: [" | [SEARCH: ", searchQuery, "]"] }), !isAtBottom && _jsx(Text, { color: "yellow", children: " | [SCROLLED]" }), _jsxs(Text, { dimColor: true, children: [" ", "| ", displayLines.length, "/", searchedLines.length, " lines"] }), scrollOffset > 0 && (_jsxs(Text, { dimColor: true, children: [" (", scrollOffset, " from bottom)"] }))] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "(/)search | (s)ervice | (l)evel | (c)lear | ESC" }) })] })] }));
|
|
201
191
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type React from "react";
|
|
1
2
|
export interface StatusLineProps {
|
|
2
3
|
activeTab: number;
|
|
3
4
|
tabs: string[];
|
|
5
|
+
rightContent?: React.ReactNode;
|
|
4
6
|
}
|
|
5
|
-
export declare function StatusLine({ activeTab, tabs }: StatusLineProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function StatusLine({ activeTab, tabs, rightContent }: StatusLineProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
export function StatusLine({ activeTab, tabs }) {
|
|
4
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, children: [tabs.map((tab, idx) => (_jsxs(Text, { children: [_jsx(Text, { color: activeTab === idx ? "green" : "gray", children: tab }), idx < tabs.length - 1 && _jsx(Text, { color: "gray", children: " | " })] }, tab))), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: "yellow", children: "tab" }), _jsx(Text, { color: "gray", children: "
|
|
3
|
+
export function StatusLine({ activeTab, tabs, rightContent }) {
|
|
4
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [tabs.map((tab, idx) => (_jsxs(Text, { children: [_jsx(Text, { color: activeTab === idx ? "green" : "gray", children: tab }), idx < tabs.length - 1 && _jsx(Text, { color: "gray", children: " | " })] }, tab))), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: "yellow", children: "tab" }), _jsx(Text, { color: "gray", children: " to switch" })] }), rightContent && _jsx(Box, { children: rightContent })] }));
|
|
5
5
|
}
|
|
@@ -44,12 +44,6 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
|
|
|
44
44
|
setActiveTab((activeTab + 1) % allTabs.length);
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
if (key.leftArrow && activeTab > 0) {
|
|
48
|
-
setActiveTab(activeTab - 1);
|
|
49
|
-
}
|
|
50
|
-
else if (key.rightArrow && activeTab < allTabs.length - 1) {
|
|
51
|
-
setActiveTab(activeTab + 1);
|
|
52
|
-
}
|
|
53
47
|
});
|
|
54
48
|
// Set up process output listeners
|
|
55
49
|
useEffect(() => {
|
|
@@ -208,5 +202,9 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
|
|
|
208
202
|
return (_jsx(Box, { flexDirection: "column", height: "100%", children: _jsx(MergedLogsPane, { services: serviceOutputs, onClear: handleClearMergedOutput }) }));
|
|
209
203
|
}
|
|
210
204
|
// TUI mode: has custom tabs - use tabbed interface
|
|
211
|
-
|
|
205
|
+
// Get status content from current tab if available
|
|
206
|
+
const statusContent = isCustomTab(currentTab) && currentTab.renderStatus
|
|
207
|
+
? currentTab.renderStatus()
|
|
208
|
+
: null;
|
|
209
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [isProcessTab(currentTab) ? (_jsx(ProcessPane, { title: currentTab.name, output: outputs[activeTab] || [], port: ports[activeTab], status: statuses[activeTab] || "starting", onClear: handleClearOutput })) : isCustomTab(currentTab) ? (currentTab.render()) : null, _jsx(StatusLine, { activeTab: activeTab, tabs: allTabs.map((t) => t.name), rightContent: statusContent })] }));
|
|
212
210
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"town": "./dist/index.js"
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
"build": "tsc"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@townco/tsconfig": "0.1.
|
|
21
|
+
"@townco/tsconfig": "0.1.20",
|
|
22
22
|
"@types/bun": "^1.3.1",
|
|
23
23
|
"@types/react": "^19.2.2"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@optique/core": "^0.6.2",
|
|
27
27
|
"@optique/run": "^0.6.2",
|
|
28
|
-
"@townco/agent": "0.1.
|
|
29
|
-
"@townco/secret": "0.1.
|
|
30
|
-
"@townco/ui": "0.1.
|
|
28
|
+
"@townco/agent": "0.1.28",
|
|
29
|
+
"@townco/secret": "0.1.23",
|
|
30
|
+
"@townco/ui": "0.1.23",
|
|
31
31
|
"@types/inquirer": "^9.0.9",
|
|
32
32
|
"ink": "^6.4.0",
|
|
33
33
|
"ink-text-input": "^6.0.0",
|