@testchimp/cli 0.1.46 → 0.1.47

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.
@@ -3,8 +3,8 @@
3
3
  * Relies on TESTCHIMP_API_KEY (+ optional TESTCHIMP_BACKEND_URL; defaults to prod).
4
4
  * Does not write mcp.json — TestChimp MCP is wired via opencode.json for OpenCode.
5
5
  */
6
- import { spawn } from "node:child_process";
7
- import { mkdirSync, writeFileSync } from "node:fs";
6
+ import { execSync, spawn } from "node:child_process";
7
+ import { mkdirSync, openSync, writeFileSync } from "node:fs";
8
8
  import http from "node:http";
9
9
  import https from "node:https";
10
10
  import { URL } from "node:url";
@@ -372,7 +372,16 @@ function writeOpencodeConfig(backend, apiKey, boot) {
372
372
  return model;
373
373
  }
374
374
  function buildOpencodeArgs(prompt, model, opencodeSessionId, attachUrl) {
375
- const args = ["run", prompt, "--model", model, "--format", "json", "--agent", OPENCODE_AGENT_ID];
375
+ const args = [
376
+ "run",
377
+ prompt,
378
+ "--model",
379
+ model,
380
+ "--format",
381
+ "json",
382
+ "--agent",
383
+ OPENCODE_AGENT_ID,
384
+ ];
376
385
  if (opencodeSessionId?.trim()) {
377
386
  args.push("--session", opencodeSessionId.trim());
378
387
  }
@@ -381,6 +390,74 @@ function buildOpencodeArgs(prompt, model, opencodeSessionId, attachUrl) {
381
390
  }
382
391
  return args;
383
392
  }
393
+ function killListenersOnPort(port) {
394
+ try {
395
+ const out = execSync(`lsof -tiTCP:${port} -sTCP:LISTEN`, {
396
+ encoding: "utf8",
397
+ stdio: ["ignore", "pipe", "ignore"],
398
+ }).trim();
399
+ for (const pid of out.split(/\s+/).filter(Boolean)) {
400
+ const n = Number(pid);
401
+ if (!Number.isFinite(n) || n <= 0)
402
+ continue;
403
+ try {
404
+ process.kill(n, "SIGTERM");
405
+ }
406
+ catch {
407
+ /* already gone */
408
+ }
409
+ }
410
+ }
411
+ catch {
412
+ /* nothing listening */
413
+ }
414
+ }
415
+ async function waitForOpencodeHttp(attachUrl, timeoutMs) {
416
+ const base = attachUrl.replace(/\/$/, "");
417
+ const deadline = Date.now() + timeoutMs;
418
+ while (Date.now() < deadline) {
419
+ for (const path of ["/global/health", "/"]) {
420
+ try {
421
+ const res = await fetch(`${base}${path}`);
422
+ if (res.ok || res.status === 401 || res.status === 404)
423
+ return;
424
+ }
425
+ catch {
426
+ /* retry */
427
+ }
428
+ }
429
+ await sleep(400);
430
+ }
431
+ throw new Error(`OpenCode server not ready at ${attachUrl} within ${timeoutMs}ms`);
432
+ }
433
+ /**
434
+ * Restart local `opencode serve` after writing opencode.json so the server loads
435
+ * TestChimp provider + default_agent. Workflow may have started serve earlier without config.
436
+ */
437
+ async function restartLocalOpencodeServer(attachUrl) {
438
+ const u = new URL(attachUrl);
439
+ const hostname = u.hostname || "127.0.0.1";
440
+ const port = u.port || (u.protocol === "https:" ? "443" : "80");
441
+ killListenersOnPort(port);
442
+ await sleep(300);
443
+ const logFd = openSync("opencode-server.log", "a");
444
+ const child = spawn("opencode", ["serve", "--port", port, "--hostname", hostname], {
445
+ detached: true,
446
+ stdio: ["ignore", logFd, logFd],
447
+ env: process.env,
448
+ });
449
+ child.unref();
450
+ if (child.pid) {
451
+ try {
452
+ writeFileSync("/tmp/opencode-server.pid", String(child.pid));
453
+ }
454
+ catch {
455
+ /* best effort */
456
+ }
457
+ }
458
+ console.error(`ChimpHands restarted OpenCode serve on ${hostname}:${port} (pid ${child.pid ?? "?"})`);
459
+ await waitForOpencodeHttp(attachUrl, 60_000);
460
+ }
384
461
  function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, attachUrl) {
385
462
  let activeSessionId = opencodeSessionId?.trim() || undefined;
386
463
  const baseArgs = buildOpencodeArgs(prompt, model, activeSessionId, attachUrl);
@@ -649,6 +726,9 @@ export async function runChimphands(opts) {
649
726
  console.error(`ChimpHands OpenCode model: ${opencodeModel}`);
650
727
  if (attachUrl) {
651
728
  console.error(`ChimpHands OpenCode attach: ${attachUrl}`);
729
+ // Serve must load opencode.json (provider + default_agent). Workflow often starts
730
+ // serve before this file exists; restart so attach mode can omit --agent safely.
731
+ await restartLocalOpencodeServer(attachUrl);
652
732
  }
653
733
  let opencodeSessionId = bootStr(boot, "opencode_session_id", "opencodeSessionId") || undefined;
654
734
  const conversationSummary = bootStr(boot, "conversation_summary", "conversationSummary");
@@ -1213,18 +1293,6 @@ export async function serveChimphands(opts) {
1213
1293
  if (!attachUrl) {
1214
1294
  throw new Error("--attach URL is required for chimphands serve");
1215
1295
  }
1216
- // Wait for OpenCode server readiness.
1217
- const deadline = Date.now() + 60_000;
1218
- while (Date.now() < deadline) {
1219
- try {
1220
- const res = await fetch(attachUrl.replace(/\/$/, "") + "/");
1221
- if (res.ok || res.status === 401 || res.status === 404)
1222
- break;
1223
- }
1224
- catch {
1225
- /* retry */
1226
- }
1227
- await sleep(500);
1228
- }
1296
+ // Server is (re)started inside runChimphands after opencode.json is written.
1229
1297
  await runChimphands({ ...opts, attachUrl });
1230
1298
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "description": "TestChimp CLI and MCP server — coverage, plans, EaaS, TrueCoverage, API operations (calls /api/mcp/*)",
5
5
  "type": "module",
6
6
  "main": "dist/bin/testchimp.js",