@townco/cli 0.1.81 → 0.1.83

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.
Files changed (51) hide show
  1. package/dist/commands/batch-wrapper.d.ts +26 -0
  2. package/dist/commands/batch-wrapper.js +35 -0
  3. package/dist/commands/batch.js +2 -2
  4. package/dist/commands/create-wrapper.d.ts +29 -0
  5. package/dist/commands/create-wrapper.js +92 -0
  6. package/dist/commands/create.js +38 -12
  7. package/dist/commands/deploy.d.ts +14 -0
  8. package/dist/commands/deploy.js +88 -0
  9. package/dist/commands/login.js +1 -1
  10. package/dist/commands/run-wrapper.d.ts +32 -0
  11. package/dist/commands/run-wrapper.js +32 -0
  12. package/dist/commands/run.js +23 -16
  13. package/dist/commands/secret.d.ts +62 -0
  14. package/dist/commands/secret.js +119 -0
  15. package/dist/components/LogsPane.d.ts +1 -1
  16. package/dist/components/LogsPane.js +12 -3
  17. package/dist/components/ProcessPane.d.ts +1 -1
  18. package/dist/components/ProcessPane.js +9 -1
  19. package/dist/components/StatusLine.js +4 -1
  20. package/dist/components/TabbedOutput.d.ts +1 -1
  21. package/dist/components/TabbedOutput.js +2 -1
  22. package/dist/index.js +21 -338
  23. package/dist/lib/command.d.ts +8 -0
  24. package/dist/lib/command.js +1 -0
  25. package/package.json +11 -15
  26. package/dist/commands/delete.d.ts +0 -1
  27. package/dist/commands/delete.js +0 -60
  28. package/dist/commands/edit.d.ts +0 -1
  29. package/dist/commands/edit.js +0 -92
  30. package/dist/commands/list.d.ts +0 -1
  31. package/dist/commands/list.js +0 -55
  32. package/dist/commands/mcp-add.d.ts +0 -14
  33. package/dist/commands/mcp-add.js +0 -494
  34. package/dist/commands/mcp-list.d.ts +0 -3
  35. package/dist/commands/mcp-list.js +0 -63
  36. package/dist/commands/mcp-remove.d.ts +0 -3
  37. package/dist/commands/mcp-remove.js +0 -120
  38. package/dist/commands/tool-add.d.ts +0 -6
  39. package/dist/commands/tool-add.js +0 -349
  40. package/dist/commands/tool-list.d.ts +0 -3
  41. package/dist/commands/tool-list.js +0 -61
  42. package/dist/commands/tool-register.d.ts +0 -7
  43. package/dist/commands/tool-register.js +0 -291
  44. package/dist/commands/tool-remove.d.ts +0 -3
  45. package/dist/commands/tool-remove.js +0 -202
  46. package/dist/components/MergedLogsPane.d.ts +0 -11
  47. package/dist/components/MergedLogsPane.js +0 -205
  48. package/dist/lib/auth-storage.d.ts +0 -38
  49. package/dist/lib/auth-storage.js +0 -89
  50. package/dist/lib/mcp-storage.d.ts +0 -32
  51. package/dist/lib/mcp-storage.js +0 -111
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { readFile } from "node:fs/promises";
@@ -13,7 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13
13
  import { LogsPane } from "../components/LogsPane.js";
14
14
  import { TabbedOutput } from "../components/TabbedOutput.js";
15
15
  import { ensurePortAvailable } from "../lib/port-utils.js";
16
- function TuiRunner({ agentPath, workingDir, noSession, onExit, }) {
16
+ function TuiRunner({ agentPath, workingDir, agentName, noSession, onExit, }) {
17
17
  const [client, setClient] = useState(null);
18
18
  const [error, setError] = useState(null);
19
19
  // Configure logs directory for UI package loggers BEFORE any loggers are created
@@ -39,6 +39,7 @@ function TuiRunner({ agentPath, workingDir, noSession, onExit, }) {
39
39
  agentPath,
40
40
  workingDirectory: workingDir,
41
41
  environment: {
42
+ AGENT_NAME: agentName,
42
43
  ENABLE_TELEMETRY: "true",
43
44
  ...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
44
45
  },
@@ -58,14 +59,16 @@ function TuiRunner({ agentPath, workingDir, noSession, onExit, }) {
58
59
  setError(errorMsg);
59
60
  return undefined;
60
61
  }
61
- }, [agentPath, workingDir, logger, noSession]);
62
+ }, [agentPath, workingDir, logger, noSession, agentName]);
62
63
  const customTabs = useMemo(() => [
63
64
  {
64
65
  name: "Chat",
65
66
  type: "custom",
66
67
  render: () => {
67
68
  if (error) {
68
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", error] }), _jsx(Text, { color: "gray", children: "Please check your agent path and try again." })] }));
69
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [
70
+ _jsxs(Text, { color: "red", bold: true, children: ["Error: ", error] }), _jsx(Text, { color: "gray", children: "Please check your agent path and try again." })
71
+ ] }));
69
72
  }
70
73
  if (!client) {
71
74
  return (_jsx(Box, { padding: 1, children: _jsx(Text, { dimColor: true, children: "Loading chat interface..." }) }));
@@ -155,7 +158,7 @@ async function runCliMode(options) {
155
158
  let retries = 30; // 3 seconds
156
159
  while (!debuggerReady && retries > 0) {
157
160
  try {
158
- const response = await fetch(`http://localhost:4318/v1/traces`, {
161
+ const _response = await fetch(`http://localhost:4318/v1/traces`, {
159
162
  method: "POST",
160
163
  headers: { "Content-Type": "application/json" },
161
164
  body: JSON.stringify({}),
@@ -164,7 +167,7 @@ async function runCliMode(options) {
164
167
  debuggerReady = true;
165
168
  break;
166
169
  }
167
- catch (e) {
170
+ catch (_e) {
168
171
  // Server not ready yet
169
172
  }
170
173
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -186,6 +189,7 @@ async function runCliMode(options) {
186
189
  env: {
187
190
  ...process.env,
188
191
  ...configEnvVars,
192
+ AGENT_NAME: options.agentName,
189
193
  PORT: String(agentPort),
190
194
  ENABLE_TELEMETRY: "true",
191
195
  ...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
@@ -243,7 +247,7 @@ async function runCliMode(options) {
243
247
  await client.sendMessage(prompt, sessionId);
244
248
  // Track tool calls and message state
245
249
  const toolCalls = new Map();
246
- let currentAssistantMessage = "";
250
+ let _currentAssistantMessage = "";
247
251
  let isStreamingText = false;
248
252
  // Receive and render messages
249
253
  for await (const chunk of client.receiveMessages()) {
@@ -256,7 +260,7 @@ async function runCliMode(options) {
256
260
  isStreamingText = true;
257
261
  }
258
262
  process.stdout.write(chunk.contentDelta.text);
259
- currentAssistantMessage += chunk.contentDelta.text;
263
+ _currentAssistantMessage += chunk.contentDelta.text;
260
264
  }
261
265
  // Check if complete after processing content
262
266
  if (chunk.isComplete) {
@@ -346,7 +350,7 @@ async function runCliMode(options) {
346
350
  setTimeout(() => resolve(), 3000);
347
351
  });
348
352
  }
349
- catch (e) {
353
+ catch (_e) {
350
354
  // Process may already be dead
351
355
  }
352
356
  }
@@ -363,7 +367,7 @@ async function runCliMode(options) {
363
367
  try {
364
368
  agentProcess.kill("SIGKILL");
365
369
  }
366
- catch (e) {
370
+ catch (_e) {
367
371
  // Process may already be dead
368
372
  }
369
373
  process.exit(1);
@@ -400,7 +404,7 @@ export async function runCommand(options) {
400
404
  // Looking for patterns like: name: "bibliotecha" or name: 'bibliotecha'
401
405
  usesBibliotechaMcp = /name:\s*["']bibliotecha["']/.test(content);
402
406
  }
403
- catch (error) {
407
+ catch (_error) {
404
408
  // If we can't read the agent definition, just use the directory name
405
409
  // Silently fail - the directory name is a reasonable fallback
406
410
  }
@@ -436,7 +440,7 @@ export async function runCommand(options) {
436
440
  try {
437
441
  debuggerProcess.kill("SIGTERM");
438
442
  }
439
- catch (e) {
443
+ catch (_e) {
440
444
  // Process may already be dead
441
445
  }
442
446
  }
@@ -501,6 +505,7 @@ export async function runCommand(options) {
501
505
  env: {
502
506
  ...process.env,
503
507
  ...configEnvVars,
508
+ AGENT_NAME: name,
504
509
  NODE_ENV: process.env.NODE_ENV || "production",
505
510
  PORT: port.toString(),
506
511
  ENABLE_TELEMETRY: "true",
@@ -538,13 +543,13 @@ export async function runCommand(options) {
538
543
  try {
539
544
  agentProcess.kill("SIGTERM");
540
545
  }
541
- catch (e) {
546
+ catch (_e) {
542
547
  // Process may already be dead
543
548
  }
544
549
  try {
545
550
  guiProcess.kill("SIGTERM");
546
551
  }
547
- catch (e) {
552
+ catch (_e) {
548
553
  // Process may already be dead
549
554
  }
550
555
  // Also cleanup debugger
@@ -588,6 +593,7 @@ export async function runCommand(options) {
588
593
  env: {
589
594
  ...process.env,
590
595
  ...configEnvVars,
596
+ AGENT_NAME: name,
591
597
  NODE_ENV: process.env.NODE_ENV || "production",
592
598
  PORT: port.toString(),
593
599
  ENABLE_TELEMETRY: "true",
@@ -603,7 +609,7 @@ export async function runCommand(options) {
603
609
  try {
604
610
  agentProcess.kill("SIGTERM");
605
611
  }
606
- catch (e) {
612
+ catch (_e) {
607
613
  // Process may already be dead
608
614
  }
609
615
  // Also cleanup debugger
@@ -643,6 +649,7 @@ export async function runCommand(options) {
643
649
  await runCliMode({
644
650
  binPath,
645
651
  agentPath,
652
+ agentName: name,
646
653
  workingDir: agentPath,
647
654
  prompt,
648
655
  configEnvVars,
@@ -673,7 +680,7 @@ export async function runCommand(options) {
673
680
  process.on("SIGINT", handleTuiSigint);
674
681
  process.on("SIGTERM", handleTuiSigint);
675
682
  // Render the tabbed UI with Chat and Logs
676
- const { waitUntilExit } = render(_jsx(TuiRunner, { agentPath: binPath, workingDir: agentPath, noSession: noSession, onExit: () => {
683
+ const { waitUntilExit } = render(_jsx(TuiRunner, { agentPath: binPath, workingDir: agentPath, agentName: name, noSession: noSession, onExit: () => {
677
684
  // Cleanup is handled by the ACP client disconnect
678
685
  cleanupDebugger?.();
679
686
  } }));
@@ -0,0 +1,62 @@
1
+ declare const _default: {
2
+ def: import("@optique/core").Parser<{
3
+ readonly command: "secret";
4
+ readonly subcommand: {
5
+ readonly action: "list";
6
+ } | {
7
+ readonly action: "add";
8
+ readonly name: string;
9
+ readonly value: string | undefined;
10
+ } | {
11
+ readonly action: "update";
12
+ readonly name: string | undefined;
13
+ readonly value: string | undefined;
14
+ readonly genenv: true | undefined;
15
+ } | {
16
+ readonly action: "remove";
17
+ readonly name: string;
18
+ } | {
19
+ readonly action: "genenv";
20
+ };
21
+ }, ["matched", string] | ["parsing", {
22
+ readonly command: "secret";
23
+ readonly subcommand: [0, import("@optique/core").ParserResult<["matched", string] | ["parsing", {
24
+ readonly action: "list";
25
+ }] | undefined>] | [1, import("@optique/core").ParserResult<["matched", string] | ["parsing", {
26
+ readonly action: "add";
27
+ readonly name: import("@optique/core").ValueParserResult<string> | undefined;
28
+ readonly value: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
29
+ }] | undefined>] | [2, import("@optique/core").ParserResult<["matched", string] | ["parsing", {
30
+ readonly action: "update";
31
+ readonly name: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
32
+ readonly value: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
33
+ readonly genenv: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
34
+ }] | undefined>] | [3, import("@optique/core").ParserResult<["matched", string] | ["parsing", {
35
+ readonly action: "remove";
36
+ readonly name: import("@optique/core").ValueParserResult<string> | undefined;
37
+ }] | undefined>] | [4, import("@optique/core").ParserResult<["matched", string] | ["parsing", {
38
+ readonly action: "genenv";
39
+ }] | undefined>] | undefined;
40
+ }] | undefined>;
41
+ impl: (def: {
42
+ readonly command: "secret";
43
+ readonly subcommand: {
44
+ readonly action: "list";
45
+ } | {
46
+ readonly action: "add";
47
+ readonly name: string;
48
+ readonly value: string | undefined;
49
+ } | {
50
+ readonly action: "update";
51
+ readonly name: string | undefined;
52
+ readonly value: string | undefined;
53
+ readonly genenv: true | undefined;
54
+ } | {
55
+ readonly action: "remove";
56
+ readonly name: string;
57
+ } | {
58
+ readonly action: "genenv";
59
+ };
60
+ }) => unknown;
61
+ };
62
+ export default _default;
@@ -0,0 +1,119 @@
1
+ import { argument, command, constant, flag, message, object, optional, or, string, } from "@optique/core";
2
+ import { updateSchema as updateEnvSchema } from "@townco/env/update-schema";
3
+ import { createSecret, deleteSecret, genenv, listSecrets, updateSecret, } from "@townco/secret";
4
+ import inquirer from "inquirer";
5
+ import { match } from "ts-pattern";
6
+ import { createCommand } from "@/lib/command";
7
+ /**
8
+ * Securely prompt for a secret value without echoing to the terminal
9
+ */
10
+ const promptSecret = async (secretName) => {
11
+ const answers = await inquirer.prompt([
12
+ {
13
+ type: "password",
14
+ name: "value",
15
+ message: `Enter value for secret '${secretName}':`,
16
+ mask: "*",
17
+ },
18
+ ]);
19
+ return answers.value;
20
+ };
21
+ export default createCommand({
22
+ def: command("secret", object({
23
+ command: constant("secret"),
24
+ subcommand: or(command("list", object({ action: constant("list") }), {
25
+ brief: message `List secrets.`,
26
+ }), command("add", object({
27
+ action: constant("add"),
28
+ name: argument(string({ metavar: "NAME" })),
29
+ value: optional(argument(string({ metavar: "VALUE" }))),
30
+ }), { brief: message `Add a secret.` }), command("update", object({
31
+ action: constant("update"),
32
+ name: optional(argument(string({ metavar: "NAME" }))),
33
+ value: optional(argument(string({ metavar: "VALUE" }))),
34
+ genenv: optional(flag("-g", "--genenv", {
35
+ description: message `Regenerate .env file.`,
36
+ })),
37
+ }), { brief: message `Update a secret.` }), command("remove", object({
38
+ action: constant("remove"),
39
+ name: argument(string({ metavar: "NAME" })),
40
+ }), { brief: message `Remove a secret.` }), command("genenv", object({ action: constant("genenv") }), {
41
+ brief: message `Generate .env file.`,
42
+ })),
43
+ }), { brief: message `Secrets management.` }),
44
+ impl: async ({ subcommand }) => {
45
+ await match(subcommand)
46
+ .with({ action: "list" }, async () => {
47
+ const truncate = (str, maxLength = 50) => {
48
+ if (str.length <= maxLength)
49
+ return str;
50
+ return `${str.slice(0, maxLength - 3)}...`;
51
+ };
52
+ console.table((await listSecrets()).map((secret) => ({
53
+ Key: secret.key,
54
+ Valid: secret.valid ? "✓" : "✗",
55
+ Error: truncate(secret.error ?? ""),
56
+ })));
57
+ })
58
+ .with({ action: "add" }, async ({ name, value }) => {
59
+ // If value is not provided, prompt securely
60
+ const secretValue = value ?? (await promptSecret(name));
61
+ if (!secretValue) {
62
+ console.error("Error: Secret value cannot be empty");
63
+ process.exit(1);
64
+ }
65
+ await createSecret(name, secretValue);
66
+ await updateEnvSchema();
67
+ console.log(`Secret '${name}' added successfully (& @packages/env schema updated).`);
68
+ })
69
+ .with({ action: "update" }, async ({ name, value, genenv: regen }) => {
70
+ let secretName = name;
71
+ // If name is not provided, show a list prompt to select from existing secrets
72
+ if (!secretName) {
73
+ const secrets = await listSecrets();
74
+ if (secrets.length === 0) {
75
+ console.error("No secrets found to update.");
76
+ process.exit(1);
77
+ }
78
+ const answer = await inquirer.prompt([
79
+ {
80
+ type: "list",
81
+ name: "selectedSecret",
82
+ message: "Select a secret to update:",
83
+ choices: secrets.map((s) => ({
84
+ name: `${s.key} ${s.valid ? "✓" : "✗"}`,
85
+ value: s.key,
86
+ })),
87
+ },
88
+ ]);
89
+ secretName = answer.selectedSecret;
90
+ }
91
+ // If value is not provided, prompt securely
92
+ if (!secretName) {
93
+ console.error("Error: Secret name is required");
94
+ process.exit(1);
95
+ }
96
+ const secretValue = value ?? (await promptSecret(secretName));
97
+ if (!secretValue) {
98
+ console.error("Error: Secret value cannot be empty");
99
+ process.exit(1);
100
+ }
101
+ await updateSecret(secretName, secretValue);
102
+ console.log(`Secret '${secretName}' updated successfully.`);
103
+ if (regen) {
104
+ await genenv();
105
+ console.log(".env file generated successfully.");
106
+ }
107
+ })
108
+ .with({ action: "remove" }, async ({ name }) => {
109
+ await deleteSecret(name);
110
+ await updateEnvSchema();
111
+ console.log(`Secret '${name}' removed successfully (& @packages/env schema updated).`);
112
+ })
113
+ .with({ action: "genenv" }, async () => {
114
+ await genenv();
115
+ console.log(".env file generated successfully.");
116
+ })
117
+ .exhaustive();
118
+ },
119
+ });
@@ -2,4 +2,4 @@ export interface LogsPaneProps {
2
2
  logsDir?: string;
3
3
  sessionStartTime?: string;
4
4
  }
5
- export declare function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSessionStartTime, }?: LogsPaneProps): import("react/jsx-runtime").JSX.Element;
5
+ export declare function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSessionStartTime }?: LogsPaneProps): import("react/jsx-runtime").JSX.Element;
@@ -241,13 +241,22 @@ export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSes
241
241
  : searchedLogs.slice(Math.max(0, totalLogs - maxLines - scrollOffset), totalLogs - scrollOffset);
242
242
  const _canScrollUp = totalLogs > maxLines && scrollOffset < totalLogs - maxLines;
243
243
  const _canScrollDown = scrollOffset > 0;
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) => {
244
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, children: [
245
+ _jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })
246
+ ] })), _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) => {
245
247
  const serviceColor = SERVICE_COLORS[availableServices.indexOf(log.service) % SERVICE_COLORS.length] || "white";
246
248
  const levelColor = LOG_COLORS[log.level] || "white";
247
249
  const time = new Date(log.timestamp).toLocaleTimeString();
248
250
  const keyStr = `${log.timestamp}-${idx}`;
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 &&
251
+ return (_jsxs(Box, { flexDirection: "column", children: [
252
+ _jsxs(Box, { children: [
253
+ _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] })
254
+ ] }), expandedLogs &&
250
255
  log.metadata &&
251
256
  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" }) })] })] }));
257
+ })) }), _jsxs(Box, { borderStyle: "single", borderTop: true, borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [
258
+ _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)`] })
259
+ ] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "(/)search | (s)ervice | (l)evel | (e)xpand | (c)lear | ESC" }) })
260
+ ] })
261
+ ] }));
253
262
  }
@@ -5,4 +5,4 @@ export interface ProcessPaneProps {
5
5
  status: "starting" | "running" | "stopped" | "error";
6
6
  onClear?: () => void;
7
7
  }
8
- export declare function ProcessPane({ title, output, port, status, onClear, }: ProcessPaneProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function ProcessPane({ title, output, port, status, onClear }: ProcessPaneProps): import("react/jsx-runtime").JSX.Element;
@@ -84,5 +84,13 @@ export function ProcessPane({ title, output, port, status, onClear, }) {
84
84
  : status === "starting"
85
85
  ? "yellow"
86
86
  : "gray";
87
- return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { borderStyle: "single", paddingX: 1, marginBottom: 1, flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: title }), port && _jsxs(Text, { color: "gray", children: [" - http://localhost:", port] }), _jsx(Text, { children: " " }), _jsx(Text, { color: statusColor, children: "\u25CF" }), _jsxs(Text, { children: [" ", status] }), searchQuery && _jsxs(Text, { color: "cyan", children: [" [SEARCH: ", searchQuery, "]"] }), !isAtBottom && _jsx(Text, { color: "yellow", children: " [SCROLLED]" })] }), _jsx(Text, { color: "gray", children: "(/)search | \u2191\u2193 | (c)lear | ESC" })] }), 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: displayOutput.length === 0 ? (_jsx(Text, { color: "gray", children: "Waiting for output..." })) : (displayOutput.map((line, idx) => (_jsx(Text, { children: line }, `${idx}-${line.slice(0, 20)}`)))) }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Text, { color: "gray", children: ["Showing ", displayOutput.length, " of ", searchedOutput.length, " lines", searchQuery && ` (${searchedOutput.length} matches)`, scrollOffset > 0 && ` (${scrollOffset} from bottom)`] }), (canScrollUp || canScrollDown) && (_jsxs(Text, { color: "gray", children: [canScrollUp && "↑ ", canScrollDown && "↓"] }))] })] }));
87
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [
88
+ _jsxs(Box, { borderStyle: "single", paddingX: 1, marginBottom: 1, flexShrink: 0, justifyContent: "space-between", children: [
89
+ _jsxs(Box, { children: [
90
+ _jsx(Text, { color: "cyan", bold: true, children: title }), port && _jsxs(Text, { color: "gray", children: [" - http://localhost:", port] }), _jsx(Text, { children: " " }), _jsx(Text, { color: statusColor, children: "\u25CF" }), _jsxs(Text, { children: [" ", status] }), searchQuery && _jsxs(Text, { color: "cyan", children: [" [SEARCH: ", searchQuery, "]"] }), !isAtBottom && _jsx(Text, { color: "yellow", children: " [SCROLLED]" })] }), _jsx(Text, { color: "gray", children: "(/)search | \u2191\u2193 | (c)lear | ESC" })
91
+ ] }), searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, marginBottom: 1, flexShrink: 0, children: [
92
+ _jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })
93
+ ] })), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, paddingX: 1, children: displayOutput.length === 0 ? (_jsx(Text, { color: "gray", children: "Waiting for output..." })) : (displayOutput.map((line, idx) => (_jsx(Text, { children: line }, `${idx}-${line.slice(0, 20)}`)))) }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, justifyContent: "space-between", children: [
94
+ _jsxs(Text, { color: "gray", children: ["Showing ", displayOutput.length, " of ", searchedOutput.length, " lines", searchQuery && ` (${searchedOutput.length} matches)`, scrollOffset > 0 && ` (${scrollOffset} from bottom)`] }), (canScrollUp || canScrollDown) && (_jsxs(Text, { color: "gray", children: [canScrollUp && "↑ ", canScrollDown && "↓"] }))] })
95
+ ] }));
88
96
  }
@@ -1,5 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
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 })] }));
4
+ return (_jsxs(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, justifyContent: "space-between", children: [
5
+ _jsxs(Box, { children: [tabs.map((tab, idx) => (_jsxs(Text, { children: [
6
+ _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" })
7
+ ] }), rightContent && _jsx(Box, { children: rightContent })] }));
5
8
  }
@@ -19,4 +19,4 @@ export interface TabbedOutputProps {
19
19
  onExit: () => void;
20
20
  onPortDetected?: (processIndex: number, port: number) => void;
21
21
  }
22
- export declare function TabbedOutput({ processes, customTabs, logsDir, onExit, onPortDetected, }: TabbedOutputProps): import("react/jsx-runtime").JSX.Element | null;
22
+ export declare function TabbedOutput({ processes, customTabs, logsDir, onExit, onPortDetected }: TabbedOutputProps): import("react/jsx-runtime").JSX.Element | null;
@@ -195,5 +195,6 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
195
195
  const statusContent = isCustomTab(currentTab) && currentTab.renderStatus
196
196
  ? currentTab.renderStatus()
197
197
  : null;
198
- 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 })] }));
198
+ 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 })
199
+ ] }));
199
200
  }