@townco/cli 0.1.77 → 0.1.79

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.
@@ -5,6 +5,16 @@ import { isInsideTownProject } from "@townco/agent/storage";
5
5
  import { createLogger } from "@townco/core";
6
6
  import { AcpClient } from "@townco/ui";
7
7
  import { findAvailablePort } from "../lib/port-utils.js";
8
+ // Helper to check if server is already running
9
+ async function isServerRunning(url) {
10
+ try {
11
+ const response = await fetch(url);
12
+ return response.ok;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
8
18
  // Helper to wait for server to be ready
9
19
  async function waitForServer(url, maxRetries = 30) {
10
20
  for (let i = 0; i < maxRetries; i++) {
@@ -196,51 +206,67 @@ export async function batchCommand(options) {
196
206
  process.exit(0);
197
207
  });
198
208
  try {
199
- // 1. Start debugger process
200
- console.log("Starting debugger...");
201
- const debuggerPkgPath = require.resolve("@townco/debugger/package.json");
202
- const debuggerDir = dirname(debuggerPkgPath);
203
- debuggerProcess = spawn("bun", ["src/index.ts"], {
204
- cwd: debuggerDir,
205
- stdio: "ignore",
206
- detached: true,
207
- env: {
208
- ...process.env,
209
- DB_PATH: tracesDbPath,
210
- AGENT_NAME: agentDisplayName,
211
- },
212
- });
213
- // Wait for debugger to be ready
214
- const debuggerReady = await waitForServer("http://localhost:4318/health");
215
- if (!debuggerReady) {
216
- throw new Error("Debugger failed to start");
209
+ // Check if services are already running
210
+ const debuggerRunning = await isServerRunning("http://localhost:4318/health");
211
+ const agentRunning = await isServerRunning(`http://localhost:${port}/health`);
212
+ let availablePort = port;
213
+ // 1. Start debugger if not running
214
+ if (debuggerRunning) {
215
+ console.log("✓ Using existing debugger (UI: http://localhost:4000)\n");
216
+ logger.info("Debugger already running");
217
217
  }
218
- console.log("✓ Debugger ready (UI: http://localhost:4000)\n");
219
- logger.info("Debugger started");
220
- // 2. Start agent HTTP server
221
- console.log("Starting agent HTTP server...");
222
- // Find available port
223
- const availablePort = await findAvailablePort(port);
224
- if (availablePort !== port) {
225
- console.log(`Port ${port} in use, using port ${availablePort} instead`);
218
+ else {
219
+ console.log("Starting debugger...");
220
+ const debuggerPkgPath = require.resolve("@townco/debugger/package.json");
221
+ const debuggerDir = dirname(debuggerPkgPath);
222
+ debuggerProcess = spawn("bun", ["src/index.ts"], {
223
+ cwd: debuggerDir,
224
+ stdio: "ignore",
225
+ detached: true,
226
+ env: {
227
+ ...process.env,
228
+ DB_PATH: tracesDbPath,
229
+ AGENT_NAME: agentDisplayName,
230
+ },
231
+ });
232
+ // Wait for debugger to be ready
233
+ const debuggerReady = await waitForServer("http://localhost:4318/health");
234
+ if (!debuggerReady) {
235
+ throw new Error("Debugger failed to start");
236
+ }
237
+ console.log("✓ Debugger ready (UI: http://localhost:4000)\n");
238
+ logger.info("Debugger started");
226
239
  }
227
- agentProcess = spawn("bun", [binPath, "http"], {
228
- cwd: agentPath,
229
- stdio: "ignore",
230
- detached: true,
231
- env: {
232
- ...process.env,
233
- PORT: String(availablePort),
234
- ENABLE_TELEMETRY: "true",
235
- },
236
- });
237
- // Wait for agent to be ready
238
- const agentReady = await waitForServer(`http://localhost:${availablePort}/health`);
239
- if (!agentReady) {
240
- throw new Error("Agent failed to start");
240
+ // 2. Start agent HTTP server if not running
241
+ if (agentRunning) {
242
+ console.log(`✓ Using existing agent (port ${port})\n`);
243
+ logger.info("Agent already running", { port });
244
+ }
245
+ else {
246
+ console.log("Starting agent HTTP server...");
247
+ // Find available port
248
+ availablePort = await findAvailablePort(port);
249
+ if (availablePort !== port) {
250
+ console.log(`Port ${port} in use, using port ${availablePort} instead`);
251
+ }
252
+ agentProcess = spawn("bun", [binPath, "http"], {
253
+ cwd: agentPath,
254
+ stdio: "ignore",
255
+ detached: true,
256
+ env: {
257
+ ...process.env,
258
+ PORT: String(availablePort),
259
+ ENABLE_TELEMETRY: "true",
260
+ },
261
+ });
262
+ // Wait for agent to be ready
263
+ const agentReady = await waitForServer(`http://localhost:${availablePort}/health`);
264
+ if (!agentReady) {
265
+ throw new Error("Agent failed to start");
266
+ }
267
+ console.log(`✓ Agent ready (port ${availablePort})\n`);
268
+ logger.info("Agent started", { port: availablePort });
241
269
  }
242
- console.log(`✓ Agent ready (port ${availablePort})\n`);
243
- logger.info("Agent started", { port: availablePort });
244
270
  // 3. Run queries in parallel
245
271
  console.log(`Running ${queries.length} queries...\n`);
246
272
  await runQueriesWithConcurrency(availablePort, queries, concurrency, logger);
@@ -12,7 +12,7 @@ import open from "open";
12
12
  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
- import { findAvailablePort } from "../lib/port-utils.js";
15
+ import { ensurePortAvailable } from "../lib/port-utils.js";
16
16
  function TuiRunner({ agentPath, workingDir, noSession, onExit, }) {
17
17
  const [client, setClient] = useState(null);
18
18
  const [error, setError] = useState(null);
@@ -175,14 +175,14 @@ async function runCliMode(options) {
175
175
  }
176
176
  }
177
177
  // Start HTTP server for the agent (silently in CLI mode)
178
- // Find available port
179
- const { findAvailablePort } = await import("../lib/port-utils.js");
180
- const agentPort = await findAvailablePort(3100);
178
+ // Ensure port is available
179
+ const { ensurePortAvailable } = await import("../lib/port-utils.js");
180
+ const agentPort = 3100;
181
+ await ensurePortAvailable(agentPort, "agent HTTP server");
181
182
  // Spawn agent process with HTTP mode
182
183
  const agentProcess = spawn("bun", [binPath, "http"], {
183
184
  cwd: workingDir,
184
185
  stdio: ["ignore", "inherit", "inherit"], // Show agent output for debugging
185
- detached: true,
186
186
  env: {
187
187
  ...process.env,
188
188
  ...configEnvVars,
@@ -337,19 +337,17 @@ async function runCliMode(options) {
337
337
  // Disconnect client and cleanup
338
338
  await client.disconnect();
339
339
  // Gracefully shutdown agent process with SIGINT (triggers telemetry flush)
340
- if (agentProcess.pid) {
341
- try {
342
- process.kill(-agentProcess.pid, "SIGINT");
343
- // Wait for agent to fully exit (including telemetry flush)
344
- await new Promise((resolve) => {
345
- agentProcess.on("exit", () => resolve());
346
- // Fallback timeout in case agent doesn't exit
347
- setTimeout(() => resolve(), 3000);
348
- });
349
- }
350
- catch (e) {
351
- // Process may already be dead
352
- }
340
+ try {
341
+ agentProcess.kill("SIGINT");
342
+ // Wait for agent to fully exit (including telemetry flush)
343
+ await new Promise((resolve) => {
344
+ agentProcess.on("exit", () => resolve());
345
+ // Fallback timeout in case agent doesn't exit
346
+ setTimeout(() => resolve(), 3000);
347
+ });
348
+ }
349
+ catch (e) {
350
+ // Process may already be dead
353
351
  }
354
352
  }
355
353
  catch (error) {
@@ -362,13 +360,11 @@ async function runCliMode(options) {
362
360
  console.error("Failed to disconnect:", disconnectError);
363
361
  }
364
362
  // Kill agent process on error
365
- if (agentProcess.pid) {
366
- try {
367
- process.kill(-agentProcess.pid, "SIGKILL");
368
- }
369
- catch (e) {
370
- // Process may already be dead
371
- }
363
+ try {
364
+ agentProcess.kill("SIGKILL");
365
+ }
366
+ catch (e) {
367
+ // Process may already be dead
372
368
  }
373
369
  process.exit(1);
374
370
  }
@@ -413,10 +409,14 @@ export async function runCommand(options) {
413
409
  let cleanupDebugger = null;
414
410
  const debuggerPkgPath = require.resolve("@townco/debugger/package.json");
415
411
  const debuggerDir = dirname(debuggerPkgPath);
412
+ // Check debugger ports are available before starting
413
+ const debuggerUiPort = 4000;
414
+ const debuggerOtlpPort = 4318;
415
+ await ensurePortAvailable(debuggerUiPort, "debugger UI");
416
+ await ensurePortAvailable(debuggerOtlpPort, "OTLP collector");
416
417
  debuggerProcess = spawn("bun", ["src/index.ts"], {
417
418
  cwd: debuggerDir,
418
419
  stdio: cli ? "ignore" : "inherit", // Silent in CLI mode
419
- detached: true,
420
420
  env: {
421
421
  ...process.env,
422
422
  DB_PATH: join(projectRoot, "agents", name, ".traces.db"),
@@ -424,7 +424,7 @@ export async function runCommand(options) {
424
424
  },
425
425
  });
426
426
  if (!cli) {
427
- console.log(`Debugger UI: http://localhost:4000`);
427
+ console.log(`Debugger UI: http://localhost:${debuggerUiPort}`);
428
428
  }
429
429
  // Cleanup debugger process on exit
430
430
  let isDebuggerCleaningUp = false;
@@ -434,7 +434,7 @@ export async function runCommand(options) {
434
434
  isDebuggerCleaningUp = true;
435
435
  if (debuggerProcess?.pid) {
436
436
  try {
437
- process.kill(-debuggerProcess.pid, "SIGKILL");
437
+ debuggerProcess.kill("SIGTERM");
438
438
  }
439
439
  catch (e) {
440
440
  // Process may already be dead
@@ -479,41 +479,35 @@ export async function runCommand(options) {
479
479
  console.log(`Recreate the agent with "town create" to include the GUI.`);
480
480
  process.exit(1);
481
481
  }
482
- // Find an available port for the agent
483
- const availablePort = await findAvailablePort(port);
484
- if (availablePort !== port) {
485
- logger.info("Port in use, using alternative", {
486
- requestedPort: port,
487
- actualPort: availablePort,
488
- });
489
- console.log(`Port ${port} is in use, using port ${availablePort} instead`);
490
- }
482
+ // Ensure agent and GUI ports are available (debugger ports already checked above)
483
+ const guiPort = 5173;
484
+ await ensurePortAvailable(port, "agent HTTP server");
485
+ await ensurePortAvailable(guiPort, "GUI dev server");
491
486
  logger.info("Starting GUI mode", {
492
- agentPort: availablePort,
493
- guiPort: 5173,
487
+ agentPort: port,
488
+ guiPort,
494
489
  });
495
490
  console.log(`Starting agent "${name}" with GUI...`);
496
- console.log(`Agent HTTP server will run on port ${availablePort}`);
497
- console.log(`GUI dev server will run on port 5173\n`);
491
+ console.log(`Agent HTTP server will run on port ${port}`);
492
+ console.log(`GUI dev server will run on port ${guiPort}\n`);
498
493
  // Set stdin to raw mode for Ink
499
494
  if (process.stdin.isTTY) {
500
495
  process.stdin.setRawMode(true);
501
496
  }
502
- // Start the agent in HTTP mode with detached process group
497
+ // Start the agent in HTTP mode
503
498
  const agentProcess = spawn("bun", [binPath, "http"], {
504
499
  cwd: agentPath,
505
500
  stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
506
- detached: true,
507
501
  env: {
508
502
  ...process.env,
509
503
  ...configEnvVars,
510
504
  NODE_ENV: process.env.NODE_ENV || "production",
511
- PORT: availablePort.toString(),
505
+ PORT: port.toString(),
512
506
  ENABLE_TELEMETRY: "true",
513
507
  ...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
514
508
  },
515
509
  });
516
- // Start the GUI dev server (no package.json, run vite directly) with detached process group
510
+ // Start the GUI dev server (no package.json, run vite directly)
517
511
  const viteArgs = ["vite"];
518
512
  if (process.env.BIND_HOST) {
519
513
  viteArgs.push("--host", process.env.BIND_HOST);
@@ -521,12 +515,11 @@ export async function runCommand(options) {
521
515
  const guiProcess = spawn("bunx", viteArgs, {
522
516
  cwd: guiPath,
523
517
  stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
524
- detached: true,
525
518
  env: {
526
519
  ...process.env,
527
520
  ...configEnvVars,
528
- VITE_AGENT_URL: `http://localhost:${availablePort}`,
529
- VITE_DEBUGGER_URL: "http://localhost:4000",
521
+ VITE_AGENT_URL: `${process.env.EXT_HOST || "http://localhost"}:${port}`,
522
+ VITE_DEBUGGER_URL: `http://localhost:${debuggerUiPort}`,
530
523
  // If agent uses bibliotecha MCP, pass BIBLIOTECHA_URL to GUI for auth
531
524
  ...(usesBibliotechaMcp &&
532
525
  process.env.BIBLIOTECHA_URL && {
@@ -541,67 +534,62 @@ export async function runCommand(options) {
541
534
  if (isCleaningUp)
542
535
  return;
543
536
  isCleaningUp = true;
544
- // Kill entire process group by using negative PID
545
- if (agentProcess.pid) {
546
- try {
547
- process.kill(-agentProcess.pid, "SIGKILL");
548
- }
549
- catch (e) {
550
- // Process may already be dead
551
- }
537
+ // Kill child processes - since they're not detached, they're in our process group
538
+ try {
539
+ agentProcess.kill("SIGTERM");
552
540
  }
553
- if (guiProcess.pid) {
554
- try {
555
- process.kill(-guiProcess.pid, "SIGKILL");
556
- }
557
- catch (e) {
558
- // Process may already be dead
559
- }
541
+ catch (e) {
542
+ // Process may already be dead
543
+ }
544
+ try {
545
+ guiProcess.kill("SIGTERM");
546
+ }
547
+ catch (e) {
548
+ // Process may already be dead
560
549
  }
561
550
  // Also cleanup debugger
562
551
  cleanupDebugger?.();
563
552
  };
564
- process.on("exit", cleanupProcesses);
565
- // Handle SIGINT (Control-C) explicitly and exit
553
+ // Render the tabbed UI with dynamic port detection
554
+ const { waitUntilExit } = render(_jsx(GuiRunner, { agentProcess: agentProcess, guiProcess: guiProcess, agentPort: port, agentPath: agentPath, logger: logger, onExit: () => {
555
+ cleanupProcesses();
556
+ } }));
557
+ // Register signal handlers AFTER Ink render to ensure they take precedence
558
+ // Use a wrapper that forces cleanup before Ink's handlers can interfere
566
559
  const handleSigint = () => {
567
560
  cleanupProcesses();
561
+ // Force exit immediately to prevent Ink from keeping process alive
568
562
  process.exit(0);
569
563
  };
564
+ // Remove any existing listeners and add ours with highest priority
565
+ process.removeAllListeners("SIGINT");
566
+ process.removeAllListeners("SIGTERM");
570
567
  process.on("SIGINT", handleSigint);
571
568
  process.on("SIGTERM", handleSigint);
572
- // Render the tabbed UI with dynamic port detection
573
- const { waitUntilExit } = render(_jsx(GuiRunner, { agentProcess: agentProcess, guiProcess: guiProcess, agentPort: availablePort, agentPath: agentPath, logger: logger, onExit: () => {
574
- cleanupProcesses();
575
- } }));
569
+ process.on("exit", cleanupProcesses);
570
+ process.on("beforeExit", cleanupProcesses);
576
571
  await waitUntilExit();
572
+ cleanupProcesses();
577
573
  process.exit(0);
578
574
  }
579
575
  else if (http) {
580
- // Find an available port for the agent
581
- const availablePort = await findAvailablePort(port);
582
- if (availablePort !== port) {
583
- logger.info("Port in use, using alternative", {
584
- requestedPort: port,
585
- actualPort: availablePort,
586
- });
587
- console.log(`Port ${port} is in use, using port ${availablePort} instead\n`);
588
- }
589
- logger.info("Starting HTTP mode", { port: availablePort });
590
- console.log(`Starting agent "${name}" in HTTP mode on port ${availablePort}...`);
576
+ // Ensure agent port is available (debugger ports already checked above)
577
+ await ensurePortAvailable(port, "agent HTTP server");
578
+ logger.info("Starting HTTP mode", { port });
579
+ console.log(`Starting agent "${name}" in HTTP mode on port ${port}...`);
591
580
  console.log(`\nEndpoints:`);
592
- console.log(` http://localhost:${availablePort}/health - Health check`);
593
- console.log(` http://localhost:${availablePort}/rpc - RPC endpoint`);
594
- console.log(` http://localhost:${availablePort}/events - SSE event stream\n`);
595
- // Run the agent in HTTP mode with detached process group
581
+ console.log(` http://localhost:${port}/health - Health check`);
582
+ console.log(` http://localhost:${port}/rpc - RPC endpoint`);
583
+ console.log(` http://localhost:${port}/events - SSE event stream\n`);
584
+ // Run the agent in HTTP mode
596
585
  const agentProcess = spawn("bun", [binPath, "http"], {
597
586
  cwd: agentPath,
598
587
  stdio: "inherit",
599
- detached: true,
600
588
  env: {
601
589
  ...process.env,
602
590
  ...configEnvVars,
603
591
  NODE_ENV: process.env.NODE_ENV || "production",
604
- PORT: availablePort.toString(),
592
+ PORT: port.toString(),
605
593
  ENABLE_TELEMETRY: "true",
606
594
  ...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
607
595
  },
@@ -612,13 +600,11 @@ export async function runCommand(options) {
612
600
  if (isCleaningUp)
613
601
  return;
614
602
  isCleaningUp = true;
615
- if (agentProcess.pid) {
616
- try {
617
- process.kill(-agentProcess.pid, "SIGKILL");
618
- }
619
- catch (e) {
620
- // Process may already be dead
621
- }
603
+ try {
604
+ agentProcess.kill("SIGTERM");
605
+ }
606
+ catch (e) {
607
+ // Process may already be dead
622
608
  }
623
609
  // Also cleanup debugger
624
610
  cleanupDebugger?.();
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import fs from "node:fs";
3
+ import afs from "node:fs/promises";
3
4
  import { join } from "node:path";
4
5
  import { argument, command, constant, flag, multiple, object, option, optional, or, } from "@optique/core";
5
6
  import { message } from "@optique/core/message";
@@ -7,7 +8,7 @@ import { integer, string } from "@optique/core/valueparser";
7
8
  import { run } from "@optique/run";
8
9
  import { initForClaudeCode } from "@townco/agent/scaffold";
9
10
  import { isInsideTownProject } from "@townco/agent/storage";
10
- import { API_ROUTES, findRoot } from "@townco/core";
11
+ import { findRoot } from "@townco/core";
11
12
  import { updateSchema as updateEnvSchema } from "@townco/env/update-schema";
12
13
  import { createSecret, deleteSecret, genenv, listSecrets, updateSecret, } from "@townco/secret";
13
14
  import { createTRPCClient, httpLink, httpSubscriptionLink, splitLink, } from "@trpc/client";
@@ -25,6 +26,7 @@ import { loginCommand } from "./commands/login.js";
25
26
  import { runCommand } from "./commands/run.js";
26
27
  import { upgradeCommand } from "./commands/upgrade.js";
27
28
  import { whoamiCommand } from "./commands/whoami.js";
29
+ import { getValidCredentials } from "./lib/auth-fetch";
28
30
  /**
29
31
  * Securely prompt for a secret value without echoing to the terminal
30
32
  */
@@ -39,7 +41,10 @@ const promptSecret = async (secretName) => {
39
41
  ]);
40
42
  return answers.value;
41
43
  };
42
- const parser = or(command("deploy", object({ command: constant("deploy") }), {
44
+ const parser = or(command("deploy", object({
45
+ command: constant("deploy"),
46
+ agent: option("-a", "--agent", string()),
47
+ }), {
43
48
  brief: message `Deploy agents.`,
44
49
  description: message `Deploy agents to the Town cloud.`,
45
50
  }), command("configure", constant("configure"), {
@@ -112,45 +117,57 @@ const meta = {
112
117
  const main = async (parser, meta) => await match(run(parser, meta))
113
118
  .with("login", loginCommand)
114
119
  .with("whoami", whoamiCommand)
115
- .with({ command: "deploy" }, async () => {
116
- const url = "http://localhost:3000/api/trpc";
120
+ .with({ command: "deploy" }, async ({ agent }) => {
121
+ const projectRoot = await isInsideTownProject();
122
+ if (!projectRoot)
123
+ throw new Error("Not inside a Town project");
124
+ if (!(await afs.exists(join(projectRoot, "agents", agent))))
125
+ throw new Error(`Agent ${agent} not found`);
126
+ const { accessToken, shedUrl } = await getValidCredentials();
127
+ const authHeader = { Authorization: `Bearer ${accessToken}` };
128
+ const url = `${shedUrl}/api/trpc`;
129
+ const baseLinkOpts = { url, transformer: superjson };
117
130
  const client = createTRPCClient({
118
131
  links: [
119
132
  splitLink({
120
133
  condition: (op) => op.type === "subscription",
121
134
  true: httpSubscriptionLink({
122
- url,
123
- transformer: superjson,
135
+ ...baseLinkOpts,
124
136
  EventSource,
137
+ eventSourceOptions: {
138
+ fetch: async (url, init) => fetch(url, {
139
+ ...init,
140
+ headers: { ...init.headers, ...authHeader },
141
+ }),
142
+ },
125
143
  }),
126
- false: splitLink({
127
- condition: (op) => op.path === API_ROUTES.UPLOAD_ARCHIVE,
128
- true: httpLink({
129
- url,
130
- transformer: { serialize: (o) => o, deserialize: (o) => o },
131
- }),
132
- false: httpLink({ url, transformer: superjson }),
133
- }),
144
+ false: httpLink({ ...baseLinkOpts, headers: authHeader }),
134
145
  }),
135
146
  ],
136
147
  });
137
148
  console.log("Creating archive...");
138
- const root = await findRoot();
149
+ const root = await findRoot({ rootMarker: "package.json" });
139
150
  const arc = archiver("tar", { gzip: true });
151
+ const chunks = [];
152
+ const done = new Promise((ok, err) => {
153
+ arc.on("data", (chunk) => chunks.push(chunk));
154
+ arc.on("end", () => ok(Buffer.concat(chunks)));
155
+ arc.on("error", err);
156
+ });
140
157
  (await walk({ path: root, ignoreFiles: [".gitignore"] }))
141
158
  .filter((path) => path.split("/")[0] !== ".git")
142
159
  .forEach((path) => {
143
160
  if (!fs.statSync(path).isFile())
144
161
  return;
145
- arc.append(path, { name: path });
162
+ arc.append(fs.createReadStream(path), { name: path });
146
163
  });
147
164
  arc.finalize();
148
165
  console.log("Uploading archive...");
149
- const { sha256, cached } = await client.uploadArchive.mutate(Buffer.from(await Array.fromAsync(arc)));
166
+ const { sha256, cached } = await client.uploadArchive.mutate(await done);
150
167
  console.log(`Archive uploaded: ${sha256} (${cached ? "cached" : "new"})`);
151
168
  console.log("Deploying...");
152
- client.deploy.subscribe({ sha256 }, {
153
- onData: ({ status }) => console.log(` ${status}`),
169
+ client.deploy.subscribe({ sha256, agent, shedUrl }, {
170
+ onData: ({ status, error }) => console.log(status ? status : error),
154
171
  onError: (err) => console.error(err),
155
172
  onComplete: () => console.log("\n✓ Deployment complete!"),
156
173
  });
@@ -6,3 +6,7 @@ export declare function isPortAvailable(port: number): Promise<boolean>;
6
6
  * Find the next available port starting from the given port
7
7
  */
8
8
  export declare function findAvailablePort(startPort: number, maxAttempts?: number): Promise<number>;
9
+ /**
10
+ * Ensure a specific port is available, throwing an error if it's in use
11
+ */
12
+ export declare function ensurePortAvailable(port: number, serviceName: string): Promise<void>;
@@ -33,3 +33,12 @@ export async function findAvailablePort(startPort, maxAttempts = 10) {
33
33
  }
34
34
  throw new Error(`Could not find an available port between ${startPort} and ${startPort + maxAttempts - 1}`);
35
35
  }
36
+ /**
37
+ * Ensure a specific port is available, throwing an error if it's in use
38
+ */
39
+ export async function ensurePortAvailable(port, serviceName) {
40
+ const available = await isPortAvailable(port);
41
+ if (!available) {
42
+ throw new Error(`Port ${port} is already in use. Cannot start ${serviceName}. Please free up the port and try again.`);
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "town": "./dist/index.js"
@@ -15,7 +15,7 @@
15
15
  "build": "tsc"
16
16
  },
17
17
  "devDependencies": {
18
- "@townco/tsconfig": "0.1.69",
18
+ "@townco/tsconfig": "0.1.71",
19
19
  "@types/archiver": "^7.0.0",
20
20
  "@types/bun": "^1.3.1",
21
21
  "@types/ignore-walk": "^4.0.3",
@@ -25,12 +25,12 @@
25
25
  "dependencies": {
26
26
  "@optique/core": "^0.6.2",
27
27
  "@optique/run": "^0.6.2",
28
- "@townco/agent": "0.1.77",
29
- "@townco/core": "0.0.50",
30
- "@townco/debugger": "0.1.27",
31
- "@townco/env": "0.1.22",
32
- "@townco/secret": "0.1.72",
33
- "@townco/ui": "0.1.72",
28
+ "@townco/agent": "0.1.79",
29
+ "@townco/core": "0.0.52",
30
+ "@townco/debugger": "0.1.29",
31
+ "@townco/env": "0.1.24",
32
+ "@townco/secret": "0.1.74",
33
+ "@townco/ui": "0.1.74",
34
34
  "@trpc/client": "^11.7.2",
35
35
  "@types/inquirer": "^9.0.9",
36
36
  "@types/ws": "^8.5.13",