@lesscap/baton-cli 0.7.5 → 0.7.6

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 (2) hide show
  1. package/dist/baton.mjs +403 -53
  2. package/package.json +1 -1
package/dist/baton.mjs CHANGED
@@ -4815,7 +4815,7 @@ var viewWorker = (cfg) => {
4815
4815
  apiToken: worker2.apiToken
4816
4816
  };
4817
4817
  };
4818
- var worktreeConfig = (cfg) => ({
4818
+ var worktreeConfig = (cfg, session2) => ({
4819
4819
  server: cfg.server,
4820
4820
  project: cfg.projectId,
4821
4821
  baseBranch: cfg.baseBranch,
@@ -4825,7 +4825,8 @@ var worktreeConfig = (cfg) => ({
4825
4825
  agentKind: cfg.agentKind,
4826
4826
  machineId: cfg.machineId,
4827
4827
  apiToken: cfg.apiToken
4828
- }
4828
+ },
4829
+ ...session2 === void 0 ? {} : { session: session2 }
4829
4830
  });
4830
4831
 
4831
4832
  // src/config.ts
@@ -4990,6 +4991,19 @@ var withRetry = async (fn, opts = {}) => {
4990
4991
  throw lastErr;
4991
4992
  };
4992
4993
 
4994
+ // src/client/services.ts
4995
+ var servicesClient = (baseUrl) => {
4996
+ const u = (path) => `${baseUrl}${path}`;
4997
+ return {
4998
+ listByProject: (projectId) => request(u(`/projects/${projectId}/services`), { method: "GET" }),
4999
+ run: (workerId, input) => request(u(`/workers/${workerId}/services`), { method: "POST", body: input }),
5000
+ stop: (workerId, name) => request(u(`/workers/${workerId}/services/${encodeURIComponent(name)}`), {
5001
+ method: "DELETE"
5002
+ }),
5003
+ report: (input) => request(u("/workers/me/services"), { method: "PUT", body: input })
5004
+ };
5005
+ };
5006
+
4993
5007
  // src/client/sessions.ts
4994
5008
  var sessionsClient = (baseUrl) => {
4995
5009
  const u = (p) => `${baseUrl}${p}`;
@@ -5113,6 +5127,7 @@ var clientFromBase = (baseUrl) => ({
5113
5127
  requirements: requirementClient(baseUrl),
5114
5128
  tasks: taskClient(baseUrl),
5115
5129
  sessions: sessionsClient(baseUrl),
5130
+ services: servicesClient(baseUrl),
5116
5131
  loops: loopsClient(baseUrl),
5117
5132
  workers: workersClient(baseUrl)
5118
5133
  });
@@ -6251,6 +6266,162 @@ var requirement = defineCommand({
6251
6266
  }
6252
6267
  });
6253
6268
 
6269
+ // src/worker/service-child.ts
6270
+ import { spawn } from "node:child_process";
6271
+
6272
+ // src/worker/proc.ts
6273
+ var killProcessGroup = (proc) => {
6274
+ if (proc.pid === void 0) return;
6275
+ killPidGroup(proc.pid);
6276
+ };
6277
+ var killPidGroup = (pid, signal = "SIGTERM") => {
6278
+ try {
6279
+ process.kill(-pid, signal);
6280
+ } catch {
6281
+ }
6282
+ };
6283
+
6284
+ // src/worker/service-child.ts
6285
+ var isStart = (message) => {
6286
+ if (typeof message !== "object" || message === null) return false;
6287
+ const value = message;
6288
+ return value.type === "start" && typeof value.cwd === "string" && Array.isArray(value.argv) && value.argv.length > 0 && value.argv.every((arg) => typeof arg === "string");
6289
+ };
6290
+ var isStop = (message) => typeof message === "object" && message !== null && message.type === "stop";
6291
+ var send = (message) => {
6292
+ if (process.connected) process.send?.(message);
6293
+ };
6294
+ var runServiceChild = () => {
6295
+ let app = null;
6296
+ let killTimer = null;
6297
+ let stopping = false;
6298
+ const stop = () => {
6299
+ if (stopping) return;
6300
+ stopping = true;
6301
+ if (!app?.pid) {
6302
+ process.exit(0);
6303
+ return;
6304
+ }
6305
+ killPidGroup(app.pid);
6306
+ killTimer = setTimeout(() => {
6307
+ if (app?.pid) killPidGroup(app.pid, "SIGKILL");
6308
+ }, 5e3);
6309
+ killTimer.unref();
6310
+ };
6311
+ process.on("disconnect", stop);
6312
+ process.on("SIGINT", stop);
6313
+ process.on("SIGTERM", stop);
6314
+ process.on("message", (message) => {
6315
+ if (isStop(message)) return stop();
6316
+ if (!isStart(message) || app) return;
6317
+ const [command, ...args] = message.argv;
6318
+ app = spawn(command ?? "", args, {
6319
+ cwd: message.cwd,
6320
+ detached: true,
6321
+ stdio: "inherit",
6322
+ env: process.env
6323
+ });
6324
+ app.once("spawn", () => send({ type: "started", pid: app?.pid }));
6325
+ app.once("error", (error) => {
6326
+ send({ type: "error", error: error.message });
6327
+ process.exit(1);
6328
+ });
6329
+ app.once("exit", (code, signal) => {
6330
+ if (killTimer) clearTimeout(killTimer);
6331
+ process.exit(code ?? (signal ? 1 : 0));
6332
+ });
6333
+ });
6334
+ };
6335
+ var serviceChildCommand = defineCommand({
6336
+ meta: { name: "child", description: "internal service process wrapper", hidden: true },
6337
+ run: runServiceChild
6338
+ });
6339
+
6340
+ // src/commands/service.ts
6341
+ var serviceHandle = (service2) => `W-${service2.workerId}/${service2.name}`;
6342
+ var formatUptime = (startedAt, now = Date.now()) => {
6343
+ const seconds = Math.max(0, Math.floor((now - startedAt) / 1e3));
6344
+ if (seconds >= 86400) return `${Math.floor(seconds / 86400)}d`;
6345
+ if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h`;
6346
+ if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
6347
+ return `${seconds}s`;
6348
+ };
6349
+ var currentContext = () => {
6350
+ const config = loadProjectConfig(projectConfigPath());
6351
+ if (!config.worker || !config.session)
6352
+ throw new Error("service run requires a Baton session worktree");
6353
+ return { workerId: config.worker.id, sessionId: config.session };
6354
+ };
6355
+ var currentWorker = () => {
6356
+ const worker2 = loadProjectConfig(projectConfigPath()).worker;
6357
+ if (!worker2) throw new Error("no worker in scope");
6358
+ return worker2.id;
6359
+ };
6360
+ var parseTarget = (target) => {
6361
+ const explicit = target.match(/^W-(\d+)\/(.+)$/i);
6362
+ return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: currentWorker(), name: target };
6363
+ };
6364
+ var runCommand2 = defineCommand({
6365
+ meta: { name: "run", description: "run a service on the current worker" },
6366
+ args: {
6367
+ name: { type: "positional", required: true, description: "service name" },
6368
+ ...common
6369
+ },
6370
+ run: async ({ args, rawArgs }) => {
6371
+ const separator = rawArgs.indexOf("--");
6372
+ const argv = separator === -1 ? [] : rawArgs.slice(separator + 1);
6373
+ if (argv.length === 0) throw new Error("command required after `--`");
6374
+ const context = currentContext();
6375
+ const result = await clientFor(args).services.run(context.workerId, {
6376
+ sessionId: context.sessionId,
6377
+ name: args.name,
6378
+ argv
6379
+ });
6380
+ if (!result.ok) throw new Error(result.error);
6381
+ if (args.json) return console.log(toJson(result));
6382
+ console.log(`running W-${context.workerId}/${args.name}`);
6383
+ if (result.logPath) console.log(`logs: ${result.logPath}`);
6384
+ }
6385
+ });
6386
+ var lsCommand = defineCommand({
6387
+ meta: { name: "ls", description: "list live services in the current project" },
6388
+ args: common,
6389
+ run: async ({ args }) => {
6390
+ const client = clientFor(args);
6391
+ const projectId = resolveProjectId({});
6392
+ const services = await client.services.listByProject(projectId);
6393
+ if (args.json) return console.log(toJson(services));
6394
+ if (services.length === 0) return console.log("(none)");
6395
+ const workers = await client.workers.listByProject(projectId);
6396
+ const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
6397
+ services.sort((a, b) => a.workerId - b.workerId || a.name.localeCompare(b.name)).map(
6398
+ (item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} session=${item.sessionId} uptime=${formatUptime(item.startedAt)}`
6399
+ ).forEach((line) => {
6400
+ console.log(line);
6401
+ });
6402
+ }
6403
+ });
6404
+ var stopCommand = defineCommand({
6405
+ meta: { name: "stop", description: "stop and remove a live service" },
6406
+ args: {
6407
+ service: {
6408
+ type: "positional",
6409
+ required: true,
6410
+ description: "service name, or W-N/name"
6411
+ },
6412
+ ...common
6413
+ },
6414
+ run: async ({ args }) => {
6415
+ const target = parseTarget(args.service);
6416
+ await clientFor(args).services.stop(target.workerId, target.name);
6417
+ console.log(args.json ? toJson({ ok: true, service: args.service }) : `stopped ${args.service}`);
6418
+ }
6419
+ });
6420
+ var service = defineCommand({
6421
+ meta: { name: "service", description: "run and stop worker-managed development services" },
6422
+ subCommands: { run: runCommand2, ls: lsCommand, stop: stopCommand, child: serviceChildCommand }
6423
+ });
6424
+
6254
6425
  // src/commands/session/abort.ts
6255
6426
  var sessionAbortCommand = defineCommand({
6256
6427
  meta: {
@@ -7777,21 +7948,26 @@ var HEARTBEAT_TIMEOUT_MS = 15e3;
7777
7948
  var HEARTBEAT_DEAD_MS = 15e4;
7778
7949
  var STREAM_STALE_MS = 9e4;
7779
7950
  var createLiveness = (deps) => {
7780
- const { client, cfg, log, isStreamOpen, onTrip } = deps;
7951
+ const { heartbeat, log, isStreamOpen, reportRuntime, onTrip } = deps;
7781
7952
  let heartbeatOkAt = Date.now();
7782
7953
  let streamOkAt = Date.now();
7783
7954
  let timer = null;
7784
7955
  const ping = async () => {
7956
+ if (reportRuntime)
7957
+ void reportRuntime().catch((error) => log(`service report failed: ${String(error)}`));
7958
+ let timeout;
7785
7959
  try {
7786
7960
  await Promise.race([
7787
- client.workers.heartbeat(cfg.machineId),
7788
- new Promise(
7789
- (_, reject) => setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS)
7790
- )
7961
+ heartbeat(),
7962
+ new Promise((_, reject) => {
7963
+ timeout = setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS);
7964
+ })
7791
7965
  ]);
7792
7966
  heartbeatOkAt = Date.now();
7793
7967
  } catch (e) {
7794
7968
  log(`heartbeat failed: ${String(e)}`);
7969
+ } finally {
7970
+ if (timeout) clearTimeout(timeout);
7795
7971
  }
7796
7972
  };
7797
7973
  const tick = () => {
@@ -7818,20 +7994,185 @@ var createLiveness = (deps) => {
7818
7994
  };
7819
7995
  };
7820
7996
 
7997
+ // src/worker/service-supervisor.ts
7998
+ import { mkdirSync as mkdirSync4 } from "node:fs";
7999
+ import { join as join8 } from "node:path";
8000
+
8001
+ // src/worker/service-process.ts
8002
+ import { spawn as spawn2 } from "node:child_process";
8003
+ import { closeSync, existsSync as existsSync5, openSync } from "node:fs";
8004
+ import { dirname as dirname2, join as join7 } from "node:path";
8005
+ import { fileURLToPath } from "node:url";
8006
+ var START_TIMEOUT_MS = 1e4;
8007
+ var isWrapperMessage = (message) => {
8008
+ if (typeof message !== "object" || message === null) return false;
8009
+ const value = message;
8010
+ if (value.type === "started") return typeof value.pid === "number";
8011
+ return value.type === "error" && typeof value.error === "string";
8012
+ };
8013
+ var binPath = () => {
8014
+ const here = dirname2(fileURLToPath(import.meta.url));
8015
+ const devShim = join7(here, "..", "..", "bin", "baton.mjs");
8016
+ return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
8017
+ };
8018
+ var startServiceProcess = (input) => {
8019
+ const logFd = openSync(input.logPath, "a");
8020
+ let wrapper;
8021
+ try {
8022
+ wrapper = spawn2(process.execPath, [binPath(), "service", "child"], {
8023
+ detached: false,
8024
+ stdio: ["ignore", logFd, logFd, "ipc"],
8025
+ env: process.env
8026
+ });
8027
+ } finally {
8028
+ closeSync(logFd);
8029
+ }
8030
+ const started = new Promise((resolve2, reject) => {
8031
+ let settled = false;
8032
+ let timer;
8033
+ const cleanup = () => {
8034
+ clearTimeout(timer);
8035
+ wrapper.off("message", onMessage);
8036
+ wrapper.off("error", onError);
8037
+ wrapper.off("exit", onExit);
8038
+ };
8039
+ const finish = (result) => {
8040
+ if (settled) return;
8041
+ settled = true;
8042
+ cleanup();
8043
+ if (result instanceof Error) reject(result);
8044
+ else resolve2(result);
8045
+ };
8046
+ const onMessage = (message) => {
8047
+ if (!isWrapperMessage(message)) return;
8048
+ if (message.type === "started") finish(message.pid);
8049
+ else finish(new Error(message.error));
8050
+ };
8051
+ const onError = (error) => finish(error);
8052
+ const onExit = (code) => finish(new Error(`service wrapper exited before start (code=${code ?? -1})`));
8053
+ wrapper.on("message", onMessage);
8054
+ wrapper.once("error", onError);
8055
+ wrapper.once("exit", onExit);
8056
+ timer = setTimeout(() => {
8057
+ if (wrapper.connected) wrapper.send({ type: "stop" });
8058
+ finish(new Error("service wrapper did not start in time"));
8059
+ }, START_TIMEOUT_MS);
8060
+ timer.unref();
8061
+ wrapper.send({ type: "start", argv: input.argv, cwd: input.cwd }, (error) => {
8062
+ if (error) finish(error);
8063
+ });
8064
+ });
8065
+ return { wrapper, started };
8066
+ };
8067
+
8068
+ // src/worker/service-supervisor.ts
8069
+ var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve2) => child.once("exit", () => resolve2()));
8070
+ var createServiceSupervisor = (deps) => {
8071
+ const { getSession, sendReport, cfg, log } = deps;
8072
+ const entries = /* @__PURE__ */ new Map();
8073
+ let reportQueue = Promise.resolve();
8074
+ const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ sessionId, name, startedAt }) => ({ sessionId, name, startedAt }));
8075
+ const report = (result) => {
8076
+ const body = { services: snapshot(), ...result ? { result } : {} };
8077
+ const request2 = reportQueue.then(() => sendReport(body)).then(() => {
8078
+ });
8079
+ reportQueue = request2.catch(() => {
8080
+ });
8081
+ return request2;
8082
+ };
8083
+ const remove = (entry) => {
8084
+ if (entries.get(entry.name) !== entry) return;
8085
+ entries.delete(entry.name);
8086
+ void report().catch(() => {
8087
+ });
8088
+ };
8089
+ const stopEntry = (entry) => {
8090
+ if (entry.wrapper.connected) {
8091
+ entry.wrapper.send({ type: "stop" });
8092
+ return;
8093
+ }
8094
+ if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
8095
+ };
8096
+ const runOne = async (requestId, sessionId, name, argv) => {
8097
+ if (entries.has(name))
8098
+ return report({
8099
+ requestId,
8100
+ ok: false,
8101
+ error: `service "${name}" is already running`,
8102
+ status: 409
8103
+ });
8104
+ const session2 = await getSession(sessionId);
8105
+ if (session2.workerId !== cfg.workerId || !session2.worktreePath)
8106
+ return report({
8107
+ requestId,
8108
+ ok: false,
8109
+ error: "session is not materialized on this worker",
8110
+ status: 400
8111
+ });
8112
+ const logDir = join8(session2.worktreePath, ".baton-services");
8113
+ const logPath = join8(logDir, `${name}.log`);
8114
+ mkdirSync4(logDir, { recursive: true });
8115
+ const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
8116
+ const entry = {
8117
+ wrapper,
8118
+ sessionId,
8119
+ name,
8120
+ startedAt: Date.now()
8121
+ };
8122
+ entries.set(name, entry);
8123
+ wrapper.once("exit", (code) => {
8124
+ log(`service ${name} exited (code=${code ?? -1})`);
8125
+ remove(entry);
8126
+ });
8127
+ try {
8128
+ entry.appPid = await started;
8129
+ } catch (error) {
8130
+ remove(entry);
8131
+ return report({ requestId, ok: false, error: String(error), status: 500 });
8132
+ }
8133
+ const service2 = {
8134
+ workerId: cfg.workerId,
8135
+ sessionId,
8136
+ name,
8137
+ startedAt: entry.startedAt
8138
+ };
8139
+ log(`service ${name} started in ${session2.worktreePath}`);
8140
+ await report({ requestId, ok: true, service: service2, logPath });
8141
+ };
8142
+ const run = async (requestId, sessionId, name, argv) => {
8143
+ try {
8144
+ await runOne(requestId, sessionId, name, argv);
8145
+ } catch (error) {
8146
+ await report({ requestId, ok: false, error: String(error), status: 500 });
8147
+ }
8148
+ };
8149
+ const stop = async (requestId, name) => {
8150
+ const entry = entries.get(name);
8151
+ if (!entry)
8152
+ return report({ requestId, ok: false, error: `service "${name}" not found`, status: 404 });
8153
+ stopEntry(entry);
8154
+ await waitForExit(entry.wrapper);
8155
+ remove(entry);
8156
+ await report({ requestId, ok: true });
8157
+ };
8158
+ const stopSession = async (sessionId) => {
8159
+ const matches = [...entries.values()].filter((entry) => entry.sessionId === sessionId);
8160
+ matches.forEach(stopEntry);
8161
+ await Promise.all(matches.map((entry) => waitForExit(entry.wrapper)));
8162
+ matches.forEach(remove);
8163
+ };
8164
+ const killAll = () => entries.forEach(stopEntry);
8165
+ return { run, stop, stopSession, report, killAll };
8166
+ };
8167
+
7821
8168
  // src/worker/session-supervisor.ts
7822
- import { spawn } from "node:child_process";
8169
+ import { spawn as spawn3 } from "node:child_process";
7823
8170
  import { randomUUID } from "node:crypto";
7824
- import { existsSync as existsSync5 } from "node:fs";
7825
- import { dirname as dirname2, join as join8 } from "node:path";
7826
- import { fileURLToPath } from "node:url";
8171
+ import { existsSync as existsSync6 } from "node:fs";
8172
+ import { dirname as dirname3, join as join10 } from "node:path";
8173
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7827
8174
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
7828
8175
 
7829
- // src/session/runner/title-codex.ts
7830
- import { mkdtemp, rm } from "node:fs/promises";
7831
- import { tmpdir as tmpdir3 } from "node:os";
7832
- import { join as join7 } from "node:path";
7833
- import { Codex as Codex2 } from "@openai/codex-sdk";
7834
-
7835
8176
  // src/session/runner/title.ts
7836
8177
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
7837
8178
  var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
@@ -7920,6 +8261,10 @@ var generateTitle = async (input) => {
7920
8261
  };
7921
8262
 
7922
8263
  // src/session/runner/title-codex.ts
8264
+ import { mkdtemp, rm } from "node:fs/promises";
8265
+ import { tmpdir as tmpdir3 } from "node:os";
8266
+ import { join as join9 } from "node:path";
8267
+ import { Codex as Codex2 } from "@openai/codex-sdk";
7923
8268
  var generateTitleWithCodex = async (input) => {
7924
8269
  const { userText, assistantText } = input;
7925
8270
  if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
@@ -7927,7 +8272,7 @@ var generateTitleWithCodex = async (input) => {
7927
8272
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
7928
8273
  let cwd = "";
7929
8274
  try {
7930
- cwd = await mkdtemp(join7(tmpdir3(), "baton-title-"));
8275
+ cwd = await mkdtemp(join9(tmpdir3(), "baton-title-"));
7931
8276
  const client = input.client ?? new Codex2();
7932
8277
  const thread = client.startThread({
7933
8278
  workingDirectory: cwd,
@@ -7953,20 +8298,11 @@ var generateTitleWithCodex = async (input) => {
7953
8298
  }
7954
8299
  };
7955
8300
 
7956
- // src/worker/proc.ts
7957
- var killProcessGroup = (proc) => {
7958
- if (proc.pid === void 0) return;
7959
- try {
7960
- process.kill(-proc.pid, "SIGTERM");
7961
- } catch {
7962
- }
7963
- };
7964
-
7965
8301
  // src/worker/session-supervisor.ts
7966
- var binPath = () => {
7967
- const here = dirname2(fileURLToPath(import.meta.url));
7968
- const devShim = join8(here, "..", "..", "bin", "baton.mjs");
7969
- return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
8302
+ var binPath2 = () => {
8303
+ const here = dirname3(fileURLToPath2(import.meta.url));
8304
+ const devShim = join10(here, "..", "..", "bin", "baton.mjs");
8305
+ return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
7970
8306
  };
7971
8307
  var createSessionSupervisor = (deps) => {
7972
8308
  const { client, cfg, repo, log, hasTerminal, closeTerminal } = deps;
@@ -7993,7 +8329,7 @@ var createSessionSupervisor = (deps) => {
7993
8329
  };
7994
8330
  const spawnChild = (sessionId, worktreePath) => {
7995
8331
  if (children.has(sessionId)) return;
7996
- const child = spawn(process.execPath, [binPath(), "session", "run", String(sessionId)], {
8332
+ const child = spawn3(process.execPath, [binPath2(), "session", "run", String(sessionId)], {
7997
8333
  detached: true,
7998
8334
  stdio: "inherit",
7999
8335
  env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
@@ -8019,7 +8355,7 @@ var createSessionSupervisor = (deps) => {
8019
8355
  if (!session2.agentSessionId || !worktreePath) {
8020
8356
  const sessionCode = randomUUID();
8021
8357
  const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
8022
- worktreePath = join8(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
8358
+ worktreePath = join10(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
8023
8359
  const base = await syncedBase();
8024
8360
  if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
8025
8361
  createWorktree({
@@ -8031,13 +8367,14 @@ var createSessionSupervisor = (deps) => {
8031
8367
  await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
8032
8368
  if (wasCanceled()) return log(`session #${sessionId} start canceled after materializing`);
8033
8369
  log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
8034
- } else if (!existsSync5(worktreePath)) {
8370
+ } else if (!existsSync6(worktreePath)) {
8035
8371
  await restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8), baseBranch);
8036
8372
  if (wasCanceled()) return log(`session #${sessionId} start canceled during worktree restore`);
8037
8373
  log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
8038
8374
  }
8039
8375
  ensureExcluded(repo, PROJECT_CONFIG_NAME);
8040
- saveProjectConfig(join8(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg));
8376
+ ensureExcluded(repo, ".baton-services/");
8377
+ saveProjectConfig(join10(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
8041
8378
  if (hasTerminal(sessionId))
8042
8379
  return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
8043
8380
  spawnChild(sessionId, worktreePath);
@@ -8125,8 +8462,8 @@ var createSessionSupervisor = (deps) => {
8125
8462
  };
8126
8463
 
8127
8464
  // src/worker/terminal-manager.ts
8128
- import { existsSync as existsSync7 } from "node:fs";
8129
- import { spawn as spawn2 } from "node-pty";
8465
+ import { existsSync as existsSync8 } from "node:fs";
8466
+ import { spawn as spawn4 } from "node-pty";
8130
8467
 
8131
8468
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
8132
8469
  var import_stream2 = __toESM(require_stream(), 1);
@@ -8139,16 +8476,16 @@ var import_websocket = __toESM(require_websocket(), 1);
8139
8476
  var import_websocket_server = __toESM(require_websocket_server(), 1);
8140
8477
 
8141
8478
  // src/worker/pty.ts
8142
- import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
8479
+ import { existsSync as existsSync7, readdirSync as readdirSync2 } from "node:fs";
8143
8480
  import { homedir as homedir3 } from "node:os";
8144
- import { join as join9 } from "node:path";
8481
+ import { join as join11 } from "node:path";
8145
8482
  var MAX_TERMINALS = 10;
8146
8483
  var ptyArgs = (agentSessionId, hasJsonl) => hasJsonl ? ["--resume", agentSessionId] : ["--session-id", agentSessionId];
8147
8484
  var hasSessionJsonl = (agentSessionId) => {
8148
- const root = join9(homedir3(), ".claude", "projects");
8149
- if (!existsSync6(root)) return false;
8485
+ const root = join11(homedir3(), ".claude", "projects");
8486
+ if (!existsSync7(root)) return false;
8150
8487
  for (const dir of readdirSync2(root))
8151
- if (existsSync6(join9(root, dir, `${agentSessionId}.jsonl`))) return true;
8488
+ if (existsSync7(join11(root, dir, `${agentSessionId}.jsonl`))) return true;
8152
8489
  return false;
8153
8490
  };
8154
8491
  var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
@@ -8159,13 +8496,13 @@ var createTerminalManager = (deps) => {
8159
8496
  const { cfg, log, hasChild } = deps;
8160
8497
  const terminals = /* @__PURE__ */ new Map();
8161
8498
  const open = (sessionId, agentSessionId, worktreePath) => {
8162
- const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !existsSync7(worktreePath) ? `worktree missing (${worktreePath})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
8499
+ const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !existsSync8(worktreePath) ? `worktree missing (${worktreePath})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
8163
8500
  if (why) {
8164
8501
  log(`terminal #${sessionId}: ${why} \u2014 refusing`);
8165
8502
  return;
8166
8503
  }
8167
8504
  const claudeBin = process.env.BATON_CLAUDE_BIN ?? "claude";
8168
- const term = spawn2(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
8505
+ const term = spawn4(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
8169
8506
  name: "xterm-color",
8170
8507
  cols: 80,
8171
8508
  rows: 24,
@@ -8251,14 +8588,20 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8251
8588
  log,
8252
8589
  hasChild: (id) => supervisor.has(id)
8253
8590
  });
8591
+ const services = createServiceSupervisor({
8592
+ getSession: (sessionId) => client.sessions.get(sessionId),
8593
+ sendReport: (report) => client.services.report(report),
8594
+ cfg,
8595
+ log
8596
+ });
8254
8597
  let fatal = false;
8255
8598
  let stop = () => {
8256
8599
  };
8257
8600
  const liveness = createLiveness({
8258
- client,
8259
- cfg,
8601
+ heartbeat: () => client.workers.heartbeat(cfg.machineId),
8260
8602
  log,
8261
8603
  isStreamOpen: () => es.readyState === ES_OPEN,
8604
+ reportRuntime: () => services.report(),
8262
8605
  onTrip: (reason) => {
8263
8606
  if (fatal) return;
8264
8607
  log(`${reason} \u2014 exiting so the supervisor restarts a clean worker`);
@@ -8278,6 +8621,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8278
8621
  es.onopen = () => {
8279
8622
  liveness.markStreamOk();
8280
8623
  void supervisor.reconcile().catch((err) => log(`reconcile failed: ${String(err)}`));
8624
+ void services.report().catch((err) => log(`service report failed: ${String(err)}`));
8281
8625
  };
8282
8626
  es.onmessage = (e) => {
8283
8627
  liveness.markStreamOk();
@@ -8286,14 +8630,18 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8286
8630
  if (cmd.cmd === "session.start")
8287
8631
  void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
8288
8632
  else if (cmd.cmd === "session.stop") supervisor.stop(cmd.sessionId);
8289
- else if (cmd.cmd === "session.delete") supervisor.remove(cmd.sessionId, cmd.worktreePath);
8633
+ else if (cmd.cmd === "session.delete")
8634
+ void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
8290
8635
  else if (cmd.cmd === "session.title")
8291
8636
  void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath).catch((err) => log(`title failed: ${String(err)}`));
8292
8637
  else if (cmd.cmd === "session.terminal") {
8293
8638
  if (cmd.action === "open")
8294
8639
  terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
8295
8640
  else terminals.close(cmd.sessionId);
8296
- }
8641
+ } else if (cmd.cmd === "service.run")
8642
+ void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv).catch((err) => log(`service run failed: ${String(err)}`));
8643
+ else if (cmd.cmd === "service.stop")
8644
+ void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
8297
8645
  } catch {
8298
8646
  }
8299
8647
  };
@@ -8307,6 +8655,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8307
8655
  });
8308
8656
  es.close();
8309
8657
  liveness.stop();
8658
+ services.killAll();
8310
8659
  supervisor.killAll();
8311
8660
  terminals.killAll();
8312
8661
  if (fatal) process.exit(1);
@@ -8314,17 +8663,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8314
8663
 
8315
8664
  // src/worker/machine-id.ts
8316
8665
  import { randomUUID as randomUUID2 } from "node:crypto";
8317
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
8666
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
8318
8667
  import { homedir as homedir4 } from "node:os";
8319
- import { dirname as dirname3, join as join10 } from "node:path";
8320
- var machineIdPath = (env = process.env) => join10(env.XDG_DATA_HOME ?? join10(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
8668
+ import { dirname as dirname4, join as join12 } from "node:path";
8669
+ var machineIdPath = (env = process.env) => join12(env.XDG_DATA_HOME ?? join12(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
8321
8670
  var readOrCreateMachineId = (path = machineIdPath()) => {
8322
- if (existsSync8(path)) {
8671
+ if (existsSync9(path)) {
8323
8672
  const s = readFileSync6(path, "utf8").trim();
8324
8673
  if (s) return s;
8325
8674
  }
8326
8675
  const id = randomUUID2();
8327
- mkdirSync4(dirname3(path), { recursive: true });
8676
+ mkdirSync5(dirname4(path), { recursive: true });
8328
8677
  writeFileSync5(path, `${id}
8329
8678
  `, "utf8");
8330
8679
  return id;
@@ -8639,6 +8988,7 @@ var main = defineCommand({
8639
8988
  init,
8640
8989
  worker,
8641
8990
  session,
8991
+ service,
8642
8992
  loop,
8643
8993
  relay,
8644
8994
  channel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesscap/baton-cli",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"