@townco/cli 0.1.109 → 0.1.110

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.
@@ -144,7 +144,7 @@ async function loadEnvVars(projectRoot, logger) {
144
144
  }
145
145
  // CLI mode runner - outputs directly to stdout without React UI
146
146
  async function runCliMode(options) {
147
- const { binPath, workingDir, prompt, configEnvVars, noSession, waitForDebugger = false, } = options;
147
+ const { binPath, workingDir, prompt, configEnvVars, noSession, waitForDebugger = false, logger, } = options;
148
148
  // Wait for debugger to be ready BEFORE starting agent
149
149
  if (waitForDebugger) {
150
150
  let debuggerReady = false;
@@ -174,7 +174,14 @@ async function runCliMode(options) {
174
174
  // Ensure port is available
175
175
  const { ensurePortAvailable } = await import("../lib/port-utils.js");
176
176
  const agentPort = 3100;
177
- await ensurePortAvailable(agentPort, "agent HTTP server");
177
+ try {
178
+ await ensurePortAvailable(agentPort, "agent HTTP server");
179
+ }
180
+ catch (error) {
181
+ const errorMessage = error instanceof Error ? error.message : String(error);
182
+ logger.error("Port check failed", { error: errorMessage });
183
+ throw error;
184
+ }
178
185
  // Spawn agent process with HTTP mode
179
186
  const agentProcess = spawn("bun", [binPath, "http"], {
180
187
  cwd: workingDir,
@@ -377,6 +384,10 @@ export async function runCommand(options) {
377
384
  "to create a project.");
378
385
  process.exit(1);
379
386
  }
387
+ // Configure logs directory and create logger early so all errors can be logged
388
+ const logsDir = join(projectRoot, "agents", name, ".logs");
389
+ configureLogsDir(logsDir);
390
+ const logger = createLogger("cli", "debug");
380
391
  // Load environment variables from project .env
381
392
  const configEnvVars = await loadEnvVars(projectRoot);
382
393
  // Resolve agent path to load agent definition
@@ -409,17 +420,42 @@ export async function runCommand(options) {
409
420
  // Check debugger ports are available before starting
410
421
  const debuggerUiPort = 4000;
411
422
  const debuggerOtlpPort = 4318;
412
- await ensurePortAvailable(debuggerUiPort, "debugger UI");
413
- await ensurePortAvailable(debuggerOtlpPort, "OTLP collector");
423
+ try {
424
+ await ensurePortAvailable(debuggerUiPort, "debugger UI");
425
+ await ensurePortAvailable(debuggerOtlpPort, "OTLP collector");
426
+ }
427
+ catch (error) {
428
+ const errorMessage = error instanceof Error ? error.message : String(error);
429
+ logger.error("Port check failed", { error: errorMessage });
430
+ throw error;
431
+ }
414
432
  debuggerProcess = spawn("bun", ["src/index.ts"], {
415
433
  cwd: debuggerDir,
416
- stdio: cli ? "ignore" : "inherit", // Silent in CLI mode
434
+ stdio: cli ? ["ignore", "ignore", "pipe"] : "inherit", // Pipe stderr in CLI mode to capture errors
417
435
  env: {
418
436
  ...process.env,
419
437
  DB_PATH: join(projectRoot, "agents", name, ".traces.db"),
420
438
  AGENT_NAME: agentDisplayName,
421
439
  },
422
440
  });
441
+ // Log debugger stderr to capture any startup errors (e.g., port binding failures)
442
+ if (cli && debuggerProcess.stderr) {
443
+ debuggerProcess.stderr.on("data", (data) => {
444
+ const text = data.toString().trim();
445
+ if (text) {
446
+ logger.error("Debugger error", { stderr: text });
447
+ }
448
+ });
449
+ }
450
+ // Handle debugger process errors
451
+ debuggerProcess.on("error", (error) => {
452
+ logger.error("Debugger process error", { error: error.message });
453
+ });
454
+ debuggerProcess.on("exit", (code, signal) => {
455
+ if (code !== null && code !== 0) {
456
+ logger.error("Debugger process exited with error", { code, signal });
457
+ }
458
+ });
423
459
  if (!cli) {
424
460
  console.log(`Debugger UI: http://localhost:${debuggerUiPort}`);
425
461
  }
@@ -454,8 +490,6 @@ export async function runCommand(options) {
454
490
  process.exit(1);
455
491
  }
456
492
  const binPath = join(agentPath, "bin.ts");
457
- // Create logger with agent directory as logs location
458
- const logger = createLogger("cli", "debug");
459
493
  logger.info("Starting agent", {
460
494
  name,
461
495
  mode: gui ? "gui" : http ? "http" : cli ? "cli" : "tui",
@@ -478,8 +512,15 @@ export async function runCommand(options) {
478
512
  }
479
513
  // Ensure agent and GUI ports are available (debugger ports already checked above)
480
514
  const guiPort = 5173;
481
- await ensurePortAvailable(port, "agent HTTP server");
482
- await ensurePortAvailable(guiPort, "GUI dev server");
515
+ try {
516
+ await ensurePortAvailable(port, "agent HTTP server");
517
+ await ensurePortAvailable(guiPort, "GUI dev server");
518
+ }
519
+ catch (error) {
520
+ const errorMessage = error instanceof Error ? error.message : String(error);
521
+ logger.error("Port check failed", { error: errorMessage });
522
+ throw error;
523
+ }
483
524
  logger.info("Starting GUI mode", {
484
525
  agentPort: port,
485
526
  guiPort,
@@ -545,6 +586,16 @@ export async function runCommand(options) {
545
586
  catch (_e) {
546
587
  // Process may already be dead
547
588
  }
589
+ // Kill any remaining process on port 5173 (Vite server)
590
+ try {
591
+ const { spawnSync } = require("node:child_process");
592
+ spawnSync("sh", ["-c", "lsof -ti:5173 | xargs kill -9 2>/dev/null || true"], {
593
+ stdio: "ignore",
594
+ });
595
+ }
596
+ catch (_e) {
597
+ // Ignore errors
598
+ }
548
599
  // Also cleanup debugger
549
600
  cleanupDebugger?.();
550
601
  };
@@ -572,7 +623,14 @@ export async function runCommand(options) {
572
623
  }
573
624
  else if (http) {
574
625
  // Ensure agent port is available (debugger ports already checked above)
575
- await ensurePortAvailable(port, "agent HTTP server");
626
+ try {
627
+ await ensurePortAvailable(port, "agent HTTP server");
628
+ }
629
+ catch (error) {
630
+ const errorMessage = error instanceof Error ? error.message : String(error);
631
+ logger.error("Port check failed", { error: errorMessage });
632
+ throw error;
633
+ }
576
634
  logger.info("Starting HTTP mode", { port });
577
635
  console.log(`Starting agent "${name}" in HTTP mode on port ${port}...`);
578
636
  console.log(`\nEndpoints:`);
@@ -648,6 +706,7 @@ export async function runCommand(options) {
648
706
  configEnvVars,
649
707
  noSession,
650
708
  waitForDebugger: true, // Wait for debugger to be ready before starting agent
709
+ logger,
651
710
  });
652
711
  // Agent has exited cleanly, now cleanup debugger
653
712
  if (cleanupDebugger) {
@@ -1,19 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { LOG_FILE_NAME } from "@townco/core";
6
6
  import { Box, Static, Text, useInput } from "ink";
7
- import TextInput from "ink-text-input";
8
7
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
- const LOG_LEVELS = [
10
- "all",
11
- "debug",
12
- "info",
13
- "warn",
14
- "error",
15
- "fatal",
16
- ];
8
+ const LOG_LEVELS = ["all", "info", "warn", "error"];
17
9
  function buildCommand(logsDir, pretty, level, sessionStartTime) {
18
10
  const logFile = join(logsDir, LOG_FILE_NAME);
19
11
  const tailCmd = `tail -n +1 -f "${logFile}"`;
@@ -21,13 +13,8 @@ function buildCommand(logsDir, pretty, level, sessionStartTime) {
21
13
  const conditions = [];
22
14
  conditions.push(`.timestamp >= "${sessionStartTime}"`);
23
15
  if (level !== "all") {
24
- const levelOrder = ["debug", "info", "warn", "error", "fatal"];
25
- const levelIndex = levelOrder.indexOf(level);
26
- const allowedLevels = levelOrder.slice(levelIndex);
27
- const levelCondition = allowedLevels
28
- .map((l) => `.level == "${l}"`)
29
- .join(" or ");
30
- conditions.push(`(${levelCondition})`);
16
+ // Filter to exact level only
17
+ conditions.push(`.level == "${level}"`);
31
18
  }
32
19
  const jqFilter = `select(${conditions.join(" and ")})`;
33
20
  const jqFlags = pretty ? "-C --unbuffered" : "-c --unbuffered";
@@ -35,24 +22,26 @@ function buildCommand(logsDir, pretty, level, sessionStartTime) {
35
22
  }
36
23
  export function LogsPane({ logsDir: customLogsDir } = {}) {
37
24
  const logsDir = useMemo(() => customLogsDir ?? join(process.cwd(), ".logs"), [customLogsDir]);
38
- const sessionStartTime = useMemo(() => new Date().toISOString(), []);
25
+ const [sessionStartTime, setSessionStartTime] = useState(() => new Date().toISOString());
39
26
  const [pretty, setPretty] = useState(true);
40
27
  const [level, setLevel] = useState("all");
41
- const [editMode, setEditMode] = useState(false);
42
- const [customCommand, setCustomCommand] = useState("");
43
28
  const [outputLines, setOutputLines] = useState([]);
44
29
  const [error, setError] = useState(null);
45
30
  const processRef = useRef(null);
46
31
  const commandRunIdRef = useRef(0);
47
32
  const lineIdRef = useRef(0);
48
- const activeCommand = useMemo(() => editMode
49
- ? customCommand
50
- : buildCommand(logsDir, pretty, level, sessionStartTime), [editMode, customCommand, logsDir, pretty, level, sessionStartTime]);
33
+ const activeCommand = useMemo(() => buildCommand(logsDir, pretty, level, sessionStartTime), [logsDir, pretty, level, sessionStartTime]);
51
34
  // Spawn tail process
52
35
  useEffect(() => {
36
+ // Create logs directory if it doesn't exist
53
37
  if (!existsSync(logsDir)) {
54
- setError(`Logs directory not found: ${logsDir}`);
55
- return;
38
+ try {
39
+ mkdirSync(logsDir, { recursive: true });
40
+ }
41
+ catch (_error) {
42
+ setError(`Failed to create logs directory: ${logsDir}`);
43
+ return;
44
+ }
56
45
  }
57
46
  if (processRef.current) {
58
47
  processRef.current.kill();
@@ -62,7 +51,7 @@ export function LogsPane({ logsDir: customLogsDir } = {}) {
62
51
  setOutputLines([]);
63
52
  lineIdRef.current = 0;
64
53
  const runId = ++commandRunIdRef.current;
65
- const timeoutId = setTimeout(() => {
54
+ const commandTimeoutId = setTimeout(() => {
66
55
  if (runId !== commandRunIdRef.current)
67
56
  return;
68
57
  const proc = spawn("sh", ["-c", activeCommand], {
@@ -98,7 +87,7 @@ export function LogsPane({ logsDir: customLogsDir } = {}) {
98
87
  });
99
88
  }, 50);
100
89
  return () => {
101
- clearTimeout(timeoutId);
90
+ clearTimeout(commandTimeoutId);
102
91
  if (processRef.current) {
103
92
  processRef.current.kill();
104
93
  processRef.current = null;
@@ -108,27 +97,17 @@ export function LogsPane({ logsDir: customLogsDir } = {}) {
108
97
  const handleInput = useCallback((input, key) => {
109
98
  if (key.ctrl && input === "c")
110
99
  return;
111
- if (editMode) {
112
- if (key.escape) {
113
- setEditMode(false);
114
- return;
115
- }
116
- if (key.return) {
117
- setEditMode(false);
118
- return;
119
- }
120
- return;
121
- }
122
- if (input === "e") {
123
- setCustomCommand(activeCommand);
124
- setEditMode(true);
125
- return;
126
- }
100
+ // Helper to clear terminal before state changes
101
+ const clearTerminal = () => {
102
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
103
+ };
127
104
  if (input === "f") {
105
+ clearTerminal();
128
106
  setPretty((prev) => !prev);
129
107
  return;
130
108
  }
131
109
  if (input === "l") {
110
+ clearTerminal();
132
111
  setLevel((prev) => {
133
112
  const currentIndex = LOG_LEVELS.indexOf(prev);
134
113
  const nextIndex = (currentIndex + 1) % LOG_LEVELS.length;
@@ -137,23 +116,24 @@ export function LogsPane({ logsDir: customLogsDir } = {}) {
137
116
  return;
138
117
  }
139
118
  if (input === "c" && !key.ctrl) {
140
- setOutputLines([]);
119
+ // Clear terminal and update timestamp to only show new logs
120
+ clearTerminal();
121
+ setSessionStartTime(new Date().toISOString());
141
122
  return;
142
123
  }
143
- }, [editMode, activeCommand]);
124
+ }, []);
144
125
  useInput(handleInput);
145
126
  if (error) {
146
127
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { color: "red", children: error }) }));
147
128
  }
148
129
  return (_jsxs(Box, { flexDirection: "column", children: [
149
- _jsx(Static, { items: outputLines, children: (line) => _jsx(Text, { children: line.text }, line.id) }), outputLines.length === 0 && _jsx(Text, { dimColor: true, children: "Waiting for logs..." }), _jsxs(Box, { borderStyle: "single", borderTop: true, borderColor: "gray", paddingX: 1, flexDirection: "column", flexShrink: 0, children: [editMode ? (_jsxs(Box, { children: [
150
- _jsx(Text, { color: "cyan", children: "$ " }), _jsx(TextInput, { value: customCommand, onChange: setCustomCommand, placeholder: "Enter command..." }), _jsx(Text, { dimColor: true, children: " (Enter to apply, Esc to cancel)" })
151
- ] })) : (_jsxs(Box, { children: [
130
+ _jsx(Static, { items: outputLines, children: (line) => _jsx(Text, { children: line.text }, line.id) }), outputLines.length === 0 && _jsx(Text, { dimColor: true, children: "Waiting for logs..." }), _jsxs(Box, { borderStyle: "single", borderTop: true, borderColor: "gray", paddingX: 1, flexDirection: "column", flexShrink: 0, children: [
131
+ _jsxs(Box, { children: [
152
132
  _jsx(Text, { dimColor: true, children: "$ " }), _jsx(Text, { children: activeCommand })
153
- ] })), _jsxs(Box, { justifyContent: "space-between", children: [
133
+ ] }), _jsxs(Box, { justifyContent: "space-between", children: [
154
134
  _jsxs(Box, { children: [
155
135
  _jsxs(Text, { ...(pretty && { color: "green" }), children: ["[", pretty ? "formatted" : "raw", "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: ["[level: ", level, "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [outputLines.length, " lines"] })
156
- ] }), _jsx(Text, { dimColor: true, children: "(e)dit | (f)ormat | (l)evel | (c)lear" })
136
+ ] }), _jsx(Text, { dimColor: true, children: "(f)ormat | (l)evel | (c)lear" })
157
137
  ] })
158
138
  ] })
159
139
  ] }));
@@ -74,14 +74,31 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
74
74
  if (logsDir) {
75
75
  const logFile = join(logsDir, LOG_FILE_NAME);
76
76
  try {
77
+ const trimmedOutput = output.trim();
78
+ // Check if stdout output is already a valid JSON log entry
79
+ try {
80
+ const parsed = JSON.parse(trimmedOutput);
81
+ if (parsed &&
82
+ typeof parsed === "object" &&
83
+ "timestamp" in parsed &&
84
+ "level" in parsed &&
85
+ "message" in parsed) {
86
+ // Already a valid log entry, write as-is
87
+ appendFileSync(logFile, `${trimmedOutput}\n`, "utf-8");
88
+ return;
89
+ }
90
+ }
91
+ catch {
92
+ // Not valid JSON, continue to wrap it
93
+ }
94
+ // Wrap non-JSON stdout output as a log entry
77
95
  const timestamp = new Date().toISOString();
78
- const logEntry = JSON.stringify({
96
+ appendFileSync(logFile, `${JSON.stringify({
79
97
  timestamp,
80
98
  level: "info",
81
99
  service: processInfo.name.toLowerCase(),
82
- message: output.trim(),
83
- });
84
- appendFileSync(logFile, `${logEntry}\n`, "utf-8");
100
+ message: trimmedOutput,
101
+ })}\n`, "utf-8");
85
102
  }
86
103
  catch (error) {
87
104
  console.error("Failed to write to log file:", error);
@@ -118,14 +135,31 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
118
135
  if (logsDir) {
119
136
  const logFile = join(logsDir, LOG_FILE_NAME);
120
137
  try {
138
+ const trimmedText = text.trim();
139
+ // Check if stderr output is already a valid JSON log entry
140
+ try {
141
+ const parsed = JSON.parse(trimmedText);
142
+ if (parsed &&
143
+ typeof parsed === "object" &&
144
+ "timestamp" in parsed &&
145
+ "level" in parsed &&
146
+ "message" in parsed) {
147
+ // Already a valid log entry, write as-is
148
+ appendFileSync(logFile, `${trimmedText}\n`, "utf-8");
149
+ return;
150
+ }
151
+ }
152
+ catch {
153
+ // Not valid JSON, continue to wrap it
154
+ }
155
+ // Wrap non-JSON stderr output as a log entry
121
156
  const timestamp = new Date().toISOString();
122
- const logEntry = JSON.stringify({
157
+ appendFileSync(logFile, `${JSON.stringify({
123
158
  timestamp,
124
159
  level: "error",
125
160
  service: processInfo.name.toLowerCase(),
126
- message: text.trim(),
127
- });
128
- appendFileSync(logFile, `${logEntry}\n`, "utf-8");
161
+ message: trimmedText,
162
+ })}\n`, "utf-8");
129
163
  }
130
164
  catch (error) {
131
165
  console.error("Failed to write to log file:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.109",
3
+ "version": "0.1.110",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "town": "./dist/index.js"
@@ -15,7 +15,7 @@
15
15
  "build": "tsgo"
16
16
  },
17
17
  "devDependencies": {
18
- "@townco/tsconfig": "0.1.101",
18
+ "@townco/tsconfig": "0.1.102",
19
19
  "@types/archiver": "^7.0.0",
20
20
  "@types/bun": "^1.3.1",
21
21
  "@types/ignore-walk": "^4.0.3",
@@ -24,13 +24,13 @@
24
24
  "dependencies": {
25
25
  "@optique/core": "^0.6.2",
26
26
  "@optique/run": "^0.6.2",
27
- "@townco/agent": "0.1.112",
28
- "@townco/apiclient": "0.0.24",
29
- "@townco/core": "0.0.82",
30
- "@townco/debugger": "0.1.60",
31
- "@townco/env": "0.1.54",
32
- "@townco/secret": "0.1.104",
33
- "@townco/ui": "0.1.104",
27
+ "@townco/agent": "0.1.113",
28
+ "@townco/apiclient": "0.0.25",
29
+ "@townco/core": "0.0.83",
30
+ "@townco/debugger": "0.1.61",
31
+ "@townco/env": "0.1.55",
32
+ "@townco/secret": "0.1.105",
33
+ "@townco/ui": "0.1.105",
34
34
  "@trpc/client": "^11.7.2",
35
35
  "archiver": "^7.0.1",
36
36
  "eventsource": "^4.1.0",