@townco/cli 0.1.78 → 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.
- package/dist/commands/batch.js +68 -42
- package/dist/commands/run.js +79 -93
- package/dist/index.js +36 -19
- package/dist/lib/port-utils.d.ts +4 -0
- package/dist/lib/port-utils.js +9 -0
- package/package.json +8 -8
package/dist/commands/batch.js
CHANGED
|
@@ -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
|
-
//
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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);
|
package/dist/commands/run.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
179
|
-
const {
|
|
180
|
-
const agentPort =
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
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:
|
|
493
|
-
guiPort
|
|
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 ${
|
|
497
|
-
console.log(`GUI dev server will run on port
|
|
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
|
|
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:
|
|
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)
|
|
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: `${process.env.EXT_HOST}:${
|
|
529
|
-
VITE_DEBUGGER_URL:
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
573
|
-
|
|
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
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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:${
|
|
593
|
-
console.log(` http://localhost:${
|
|
594
|
-
console.log(` http://localhost:${
|
|
595
|
-
// Run the agent in HTTP mode
|
|
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:
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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 {
|
|
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({
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
});
|
package/dist/lib/port-utils.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/port-utils.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
29
|
-
"@townco/core": "0.0.
|
|
30
|
-
"@townco/debugger": "0.1.
|
|
31
|
-
"@townco/env": "0.1.
|
|
32
|
-
"@townco/secret": "0.1.
|
|
33
|
-
"@townco/ui": "0.1.
|
|
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",
|