@lesscap/baton-cli 0.7.5 → 0.7.7

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 +438 -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
  });
@@ -5375,6 +5390,20 @@ var labelAttachments = (attachments) => {
5375
5390
  // ../shared/src/domain/channel.ts
5376
5391
  var isMessageFor = (msg, name) => !msg.to || msg.to.length === 0 || msg.to.includes(name);
5377
5392
 
5393
+ // ../shared/src/domain/service.ts
5394
+ var SERVICE_PUBLIC_URL_MAX_LENGTH = 2048;
5395
+ var SERVICE_NOTE_MAX_LENGTH = 500;
5396
+ var isServicePublicUrl = (value) => {
5397
+ if (value.length === 0 || value.length > SERVICE_PUBLIC_URL_MAX_LENGTH) return false;
5398
+ try {
5399
+ const url = new URL(value);
5400
+ return url.protocol === "http:" || url.protocol === "https:";
5401
+ } catch {
5402
+ return false;
5403
+ }
5404
+ };
5405
+ var isServiceNote = (value) => value.length > 0 && value.length <= SERVICE_NOTE_MAX_LENGTH;
5406
+
5378
5407
  // ../shared/src/domain/session-event.ts
5379
5408
  var agentMessageText = (payload) => {
5380
5409
  if (typeof payload !== "object" || payload === null) return null;
@@ -6251,6 +6280,173 @@ var requirement = defineCommand({
6251
6280
  }
6252
6281
  });
6253
6282
 
6283
+ // src/worker/service-child.ts
6284
+ import { spawn } from "node:child_process";
6285
+
6286
+ // src/worker/proc.ts
6287
+ var killProcessGroup = (proc) => {
6288
+ if (proc.pid === void 0) return;
6289
+ killPidGroup(proc.pid);
6290
+ };
6291
+ var killPidGroup = (pid, signal = "SIGTERM") => {
6292
+ try {
6293
+ process.kill(-pid, signal);
6294
+ } catch {
6295
+ }
6296
+ };
6297
+
6298
+ // src/worker/service-child.ts
6299
+ var isStart = (message) => {
6300
+ if (typeof message !== "object" || message === null) return false;
6301
+ const value = message;
6302
+ return value.type === "start" && typeof value.cwd === "string" && Array.isArray(value.argv) && value.argv.length > 0 && value.argv.every((arg) => typeof arg === "string");
6303
+ };
6304
+ var isStop = (message) => typeof message === "object" && message !== null && message.type === "stop";
6305
+ var send = (message) => {
6306
+ if (process.connected) process.send?.(message);
6307
+ };
6308
+ var runServiceChild = () => {
6309
+ let app = null;
6310
+ let killTimer = null;
6311
+ let stopping = false;
6312
+ const stop = () => {
6313
+ if (stopping) return;
6314
+ stopping = true;
6315
+ if (!app?.pid) {
6316
+ process.exit(0);
6317
+ return;
6318
+ }
6319
+ killPidGroup(app.pid);
6320
+ killTimer = setTimeout(() => {
6321
+ if (app?.pid) killPidGroup(app.pid, "SIGKILL");
6322
+ }, 5e3);
6323
+ killTimer.unref();
6324
+ };
6325
+ process.on("disconnect", stop);
6326
+ process.on("SIGINT", stop);
6327
+ process.on("SIGTERM", stop);
6328
+ process.on("message", (message) => {
6329
+ if (isStop(message)) return stop();
6330
+ if (!isStart(message) || app) return;
6331
+ const [command, ...args] = message.argv;
6332
+ app = spawn(command ?? "", args, {
6333
+ cwd: message.cwd,
6334
+ detached: true,
6335
+ stdio: "inherit",
6336
+ env: process.env
6337
+ });
6338
+ app.once("spawn", () => send({ type: "started", pid: app?.pid }));
6339
+ app.once("error", (error) => {
6340
+ send({ type: "error", error: error.message });
6341
+ process.exit(1);
6342
+ });
6343
+ app.once("exit", (code, signal) => {
6344
+ if (killTimer) clearTimeout(killTimer);
6345
+ process.exit(code ?? (signal ? 1 : 0));
6346
+ });
6347
+ });
6348
+ };
6349
+ var serviceChildCommand = defineCommand({
6350
+ meta: { name: "child", description: "internal service process wrapper", hidden: true },
6351
+ run: runServiceChild
6352
+ });
6353
+
6354
+ // src/commands/service.ts
6355
+ var serviceHandle = (service2) => `W-${service2.workerId}/${service2.name}`;
6356
+ var formatUptime = (startedAt, now = Date.now()) => {
6357
+ const seconds = Math.max(0, Math.floor((now - startedAt) / 1e3));
6358
+ if (seconds >= 86400) return `${Math.floor(seconds / 86400)}d`;
6359
+ if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h`;
6360
+ if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
6361
+ return `${seconds}s`;
6362
+ };
6363
+ var currentContext = () => {
6364
+ const config = loadProjectConfig(projectConfigPath());
6365
+ if (!config.worker || !config.session)
6366
+ throw new Error("service run requires a Baton session worktree");
6367
+ return { workerId: config.worker.id, sessionId: config.session };
6368
+ };
6369
+ var currentWorker = () => {
6370
+ const worker2 = loadProjectConfig(projectConfigPath()).worker;
6371
+ if (!worker2) throw new Error("no worker in scope");
6372
+ return worker2.id;
6373
+ };
6374
+ var parseTarget = (target) => {
6375
+ const explicit = target.match(/^W-(\d+)\/(.+)$/i);
6376
+ return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: currentWorker(), name: target };
6377
+ };
6378
+ var runCommand2 = defineCommand({
6379
+ meta: { name: "run", description: "run a service on the current worker" },
6380
+ args: {
6381
+ name: { type: "positional", required: true, description: "service name" },
6382
+ "public-url": { type: "string", description: "public HTTP(S) URL for this service" },
6383
+ note: { type: "string", description: "short note describing this service" },
6384
+ ...common
6385
+ },
6386
+ run: async ({ args, rawArgs }) => {
6387
+ const separator = rawArgs.indexOf("--");
6388
+ const argv = separator === -1 ? [] : rawArgs.slice(separator + 1);
6389
+ if (argv.length === 0) throw new Error("command required after `--`");
6390
+ const publicUrl = args["public-url"]?.trim() || void 0;
6391
+ const note = args.note?.trim() || void 0;
6392
+ if (publicUrl && !isServicePublicUrl(publicUrl))
6393
+ throw new Error("--public-url must be an absolute HTTP(S) URL");
6394
+ if (note && !isServiceNote(note))
6395
+ throw new Error("service note must be 500 characters or fewer");
6396
+ const context = currentContext();
6397
+ const result = await clientFor(args).services.run(context.workerId, {
6398
+ sessionId: context.sessionId,
6399
+ name: args.name,
6400
+ argv,
6401
+ ...publicUrl ? { publicUrl } : {},
6402
+ ...note ? { note } : {}
6403
+ });
6404
+ if (!result.ok) throw new Error(result.error);
6405
+ if (args.json) return console.log(toJson(result));
6406
+ console.log(`running W-${context.workerId}/${args.name}`);
6407
+ if (publicUrl) console.log(`public: ${publicUrl}`);
6408
+ if (result.logPath) console.log(`logs: ${result.logPath}`);
6409
+ }
6410
+ });
6411
+ var lsCommand = defineCommand({
6412
+ meta: { name: "ls", description: "list live services in the current project" },
6413
+ args: common,
6414
+ run: async ({ args }) => {
6415
+ const client = clientFor(args);
6416
+ const projectId = resolveProjectId({});
6417
+ const services = await client.services.listByProject(projectId);
6418
+ if (args.json) return console.log(toJson(services));
6419
+ if (services.length === 0) return console.log("(none)");
6420
+ const workers = await client.workers.listByProject(projectId);
6421
+ const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
6422
+ services.sort((a, b) => a.workerId - b.workerId || a.name.localeCompare(b.name)).map(
6423
+ (item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} session=${item.sessionId} uptime=${formatUptime(item.startedAt)}`
6424
+ ).forEach((line) => {
6425
+ console.log(line);
6426
+ });
6427
+ }
6428
+ });
6429
+ var stopCommand = defineCommand({
6430
+ meta: { name: "stop", description: "stop and remove a live service" },
6431
+ args: {
6432
+ service: {
6433
+ type: "positional",
6434
+ required: true,
6435
+ description: "service name, or W-N/name"
6436
+ },
6437
+ ...common
6438
+ },
6439
+ run: async ({ args }) => {
6440
+ const target = parseTarget(args.service);
6441
+ await clientFor(args).services.stop(target.workerId, target.name);
6442
+ console.log(args.json ? toJson({ ok: true, service: args.service }) : `stopped ${args.service}`);
6443
+ }
6444
+ });
6445
+ var service = defineCommand({
6446
+ meta: { name: "service", description: "run and stop worker-managed development services" },
6447
+ subCommands: { run: runCommand2, ls: lsCommand, stop: stopCommand, child: serviceChildCommand }
6448
+ });
6449
+
6254
6450
  // src/commands/session/abort.ts
6255
6451
  var sessionAbortCommand = defineCommand({
6256
6452
  meta: {
@@ -7777,21 +7973,26 @@ var HEARTBEAT_TIMEOUT_MS = 15e3;
7777
7973
  var HEARTBEAT_DEAD_MS = 15e4;
7778
7974
  var STREAM_STALE_MS = 9e4;
7779
7975
  var createLiveness = (deps) => {
7780
- const { client, cfg, log, isStreamOpen, onTrip } = deps;
7976
+ const { heartbeat, log, isStreamOpen, reportRuntime, onTrip } = deps;
7781
7977
  let heartbeatOkAt = Date.now();
7782
7978
  let streamOkAt = Date.now();
7783
7979
  let timer = null;
7784
7980
  const ping = async () => {
7981
+ if (reportRuntime)
7982
+ void reportRuntime().catch((error) => log(`service report failed: ${String(error)}`));
7983
+ let timeout;
7785
7984
  try {
7786
7985
  await Promise.race([
7787
- client.workers.heartbeat(cfg.machineId),
7788
- new Promise(
7789
- (_, reject) => setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS)
7790
- )
7986
+ heartbeat(),
7987
+ new Promise((_, reject) => {
7988
+ timeout = setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS);
7989
+ })
7791
7990
  ]);
7792
7991
  heartbeatOkAt = Date.now();
7793
7992
  } catch (e) {
7794
7993
  log(`heartbeat failed: ${String(e)}`);
7994
+ } finally {
7995
+ if (timeout) clearTimeout(timeout);
7795
7996
  }
7796
7997
  };
7797
7998
  const tick = () => {
@@ -7818,20 +8019,195 @@ var createLiveness = (deps) => {
7818
8019
  };
7819
8020
  };
7820
8021
 
8022
+ // src/worker/service-supervisor.ts
8023
+ import { mkdirSync as mkdirSync4 } from "node:fs";
8024
+ import { join as join8 } from "node:path";
8025
+
8026
+ // src/worker/service-process.ts
8027
+ import { spawn as spawn2 } from "node:child_process";
8028
+ import { closeSync, existsSync as existsSync5, openSync } from "node:fs";
8029
+ import { dirname as dirname2, join as join7 } from "node:path";
8030
+ import { fileURLToPath } from "node:url";
8031
+ var START_TIMEOUT_MS = 1e4;
8032
+ var isWrapperMessage = (message) => {
8033
+ if (typeof message !== "object" || message === null) return false;
8034
+ const value = message;
8035
+ if (value.type === "started") return typeof value.pid === "number";
8036
+ return value.type === "error" && typeof value.error === "string";
8037
+ };
8038
+ var binPath = () => {
8039
+ const here = dirname2(fileURLToPath(import.meta.url));
8040
+ const devShim = join7(here, "..", "..", "bin", "baton.mjs");
8041
+ return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
8042
+ };
8043
+ var startServiceProcess = (input) => {
8044
+ const logFd = openSync(input.logPath, "a");
8045
+ let wrapper;
8046
+ try {
8047
+ wrapper = spawn2(process.execPath, [binPath(), "service", "child"], {
8048
+ detached: false,
8049
+ stdio: ["ignore", logFd, logFd, "ipc"],
8050
+ env: process.env
8051
+ });
8052
+ } finally {
8053
+ closeSync(logFd);
8054
+ }
8055
+ const started = new Promise((resolve2, reject) => {
8056
+ let settled = false;
8057
+ let timer;
8058
+ const cleanup = () => {
8059
+ clearTimeout(timer);
8060
+ wrapper.off("message", onMessage);
8061
+ wrapper.off("error", onError);
8062
+ wrapper.off("exit", onExit);
8063
+ };
8064
+ const finish = (result) => {
8065
+ if (settled) return;
8066
+ settled = true;
8067
+ cleanup();
8068
+ if (result instanceof Error) reject(result);
8069
+ else resolve2(result);
8070
+ };
8071
+ const onMessage = (message) => {
8072
+ if (!isWrapperMessage(message)) return;
8073
+ if (message.type === "started") finish(message.pid);
8074
+ else finish(new Error(message.error));
8075
+ };
8076
+ const onError = (error) => finish(error);
8077
+ const onExit = (code) => finish(new Error(`service wrapper exited before start (code=${code ?? -1})`));
8078
+ wrapper.on("message", onMessage);
8079
+ wrapper.once("error", onError);
8080
+ wrapper.once("exit", onExit);
8081
+ timer = setTimeout(() => {
8082
+ if (wrapper.connected) wrapper.send({ type: "stop" });
8083
+ finish(new Error("service wrapper did not start in time"));
8084
+ }, START_TIMEOUT_MS);
8085
+ timer.unref();
8086
+ wrapper.send({ type: "start", argv: input.argv, cwd: input.cwd }, (error) => {
8087
+ if (error) finish(error);
8088
+ });
8089
+ });
8090
+ return { wrapper, started };
8091
+ };
8092
+
8093
+ // src/worker/service-supervisor.ts
8094
+ var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve2) => child.once("exit", () => resolve2()));
8095
+ var createServiceSupervisor = (deps) => {
8096
+ const { getSession, sendReport, cfg, log } = deps;
8097
+ const entries = /* @__PURE__ */ new Map();
8098
+ let reportQueue = Promise.resolve();
8099
+ const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ sessionId, name, startedAt, publicUrl, note }) => ({
8100
+ sessionId,
8101
+ name,
8102
+ startedAt,
8103
+ ...publicUrl ? { publicUrl } : {},
8104
+ ...note ? { note } : {}
8105
+ }));
8106
+ const report = (result) => {
8107
+ const body = { services: snapshot(), ...result ? { result } : {} };
8108
+ const request2 = reportQueue.then(() => sendReport(body)).then(() => {
8109
+ });
8110
+ reportQueue = request2.catch(() => {
8111
+ });
8112
+ return request2;
8113
+ };
8114
+ const remove = (entry) => {
8115
+ if (entries.get(entry.name) !== entry) return;
8116
+ entries.delete(entry.name);
8117
+ void report().catch(() => {
8118
+ });
8119
+ };
8120
+ const stopEntry = (entry) => {
8121
+ if (entry.wrapper.connected) {
8122
+ entry.wrapper.send({ type: "stop" });
8123
+ return;
8124
+ }
8125
+ if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
8126
+ };
8127
+ const runOne = async (requestId, sessionId, name, argv, publicUrl, note) => {
8128
+ if (entries.has(name))
8129
+ return report({
8130
+ requestId,
8131
+ ok: false,
8132
+ error: `service "${name}" is already running`,
8133
+ status: 409
8134
+ });
8135
+ const session2 = await getSession(sessionId);
8136
+ if (session2.workerId !== cfg.workerId || !session2.worktreePath)
8137
+ return report({
8138
+ requestId,
8139
+ ok: false,
8140
+ error: "session is not materialized on this worker",
8141
+ status: 400
8142
+ });
8143
+ const logDir = join8(session2.worktreePath, ".baton-services");
8144
+ const logPath = join8(logDir, `${name}.log`);
8145
+ mkdirSync4(logDir, { recursive: true });
8146
+ const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
8147
+ const entry = {
8148
+ wrapper,
8149
+ sessionId,
8150
+ name,
8151
+ startedAt: Date.now(),
8152
+ publicUrl,
8153
+ note
8154
+ };
8155
+ entries.set(name, entry);
8156
+ wrapper.once("exit", (code) => {
8157
+ log(`service ${name} exited (code=${code ?? -1})`);
8158
+ remove(entry);
8159
+ });
8160
+ try {
8161
+ entry.appPid = await started;
8162
+ } catch (error) {
8163
+ remove(entry);
8164
+ return report({ requestId, ok: false, error: String(error), status: 500 });
8165
+ }
8166
+ const service2 = {
8167
+ workerId: cfg.workerId,
8168
+ sessionId,
8169
+ name,
8170
+ startedAt: entry.startedAt,
8171
+ ...publicUrl ? { publicUrl } : {},
8172
+ ...note ? { note } : {}
8173
+ };
8174
+ log(`service ${name} started in ${session2.worktreePath}`);
8175
+ await report({ requestId, ok: true, service: service2, logPath });
8176
+ };
8177
+ const run = async (requestId, sessionId, name, argv, publicUrl, note) => {
8178
+ try {
8179
+ await runOne(requestId, sessionId, name, argv, publicUrl, note);
8180
+ } catch (error) {
8181
+ await report({ requestId, ok: false, error: String(error), status: 500 });
8182
+ }
8183
+ };
8184
+ const stop = async (requestId, name) => {
8185
+ const entry = entries.get(name);
8186
+ if (!entry)
8187
+ return report({ requestId, ok: false, error: `service "${name}" not found`, status: 404 });
8188
+ stopEntry(entry);
8189
+ await waitForExit(entry.wrapper);
8190
+ remove(entry);
8191
+ await report({ requestId, ok: true });
8192
+ };
8193
+ const stopSession = async (sessionId) => {
8194
+ const matches = [...entries.values()].filter((entry) => entry.sessionId === sessionId);
8195
+ matches.forEach(stopEntry);
8196
+ await Promise.all(matches.map((entry) => waitForExit(entry.wrapper)));
8197
+ matches.forEach(remove);
8198
+ };
8199
+ const killAll = () => entries.forEach(stopEntry);
8200
+ return { run, stop, stopSession, report, killAll };
8201
+ };
8202
+
7821
8203
  // src/worker/session-supervisor.ts
7822
- import { spawn } from "node:child_process";
8204
+ import { spawn as spawn3 } from "node:child_process";
7823
8205
  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";
8206
+ import { existsSync as existsSync6 } from "node:fs";
8207
+ import { dirname as dirname3, join as join10 } from "node:path";
8208
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7827
8209
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
7828
8210
 
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
8211
  // src/session/runner/title.ts
7836
8212
  var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
7837
8213
  var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
@@ -7920,6 +8296,10 @@ var generateTitle = async (input) => {
7920
8296
  };
7921
8297
 
7922
8298
  // src/session/runner/title-codex.ts
8299
+ import { mkdtemp, rm } from "node:fs/promises";
8300
+ import { tmpdir as tmpdir3 } from "node:os";
8301
+ import { join as join9 } from "node:path";
8302
+ import { Codex as Codex2 } from "@openai/codex-sdk";
7923
8303
  var generateTitleWithCodex = async (input) => {
7924
8304
  const { userText, assistantText } = input;
7925
8305
  if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
@@ -7927,7 +8307,7 @@ var generateTitleWithCodex = async (input) => {
7927
8307
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
7928
8308
  let cwd = "";
7929
8309
  try {
7930
- cwd = await mkdtemp(join7(tmpdir3(), "baton-title-"));
8310
+ cwd = await mkdtemp(join9(tmpdir3(), "baton-title-"));
7931
8311
  const client = input.client ?? new Codex2();
7932
8312
  const thread = client.startThread({
7933
8313
  workingDirectory: cwd,
@@ -7953,20 +8333,11 @@ var generateTitleWithCodex = async (input) => {
7953
8333
  }
7954
8334
  };
7955
8335
 
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
8336
  // 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);
8337
+ var binPath2 = () => {
8338
+ const here = dirname3(fileURLToPath2(import.meta.url));
8339
+ const devShim = join10(here, "..", "..", "bin", "baton.mjs");
8340
+ return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
7970
8341
  };
7971
8342
  var createSessionSupervisor = (deps) => {
7972
8343
  const { client, cfg, repo, log, hasTerminal, closeTerminal } = deps;
@@ -7993,7 +8364,7 @@ var createSessionSupervisor = (deps) => {
7993
8364
  };
7994
8365
  const spawnChild = (sessionId, worktreePath) => {
7995
8366
  if (children.has(sessionId)) return;
7996
- const child = spawn(process.execPath, [binPath(), "session", "run", String(sessionId)], {
8367
+ const child = spawn3(process.execPath, [binPath2(), "session", "run", String(sessionId)], {
7997
8368
  detached: true,
7998
8369
  stdio: "inherit",
7999
8370
  env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
@@ -8019,7 +8390,7 @@ var createSessionSupervisor = (deps) => {
8019
8390
  if (!session2.agentSessionId || !worktreePath) {
8020
8391
  const sessionCode = randomUUID();
8021
8392
  const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
8022
- worktreePath = join8(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
8393
+ worktreePath = join10(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
8023
8394
  const base = await syncedBase();
8024
8395
  if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
8025
8396
  createWorktree({
@@ -8031,13 +8402,14 @@ var createSessionSupervisor = (deps) => {
8031
8402
  await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
8032
8403
  if (wasCanceled()) return log(`session #${sessionId} start canceled after materializing`);
8033
8404
  log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
8034
- } else if (!existsSync5(worktreePath)) {
8405
+ } else if (!existsSync6(worktreePath)) {
8035
8406
  await restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8), baseBranch);
8036
8407
  if (wasCanceled()) return log(`session #${sessionId} start canceled during worktree restore`);
8037
8408
  log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
8038
8409
  }
8039
8410
  ensureExcluded(repo, PROJECT_CONFIG_NAME);
8040
- saveProjectConfig(join8(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg));
8411
+ ensureExcluded(repo, ".baton-services/");
8412
+ saveProjectConfig(join10(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
8041
8413
  if (hasTerminal(sessionId))
8042
8414
  return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
8043
8415
  spawnChild(sessionId, worktreePath);
@@ -8125,8 +8497,8 @@ var createSessionSupervisor = (deps) => {
8125
8497
  };
8126
8498
 
8127
8499
  // src/worker/terminal-manager.ts
8128
- import { existsSync as existsSync7 } from "node:fs";
8129
- import { spawn as spawn2 } from "node-pty";
8500
+ import { existsSync as existsSync8 } from "node:fs";
8501
+ import { spawn as spawn4 } from "node-pty";
8130
8502
 
8131
8503
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
8132
8504
  var import_stream2 = __toESM(require_stream(), 1);
@@ -8139,16 +8511,16 @@ var import_websocket = __toESM(require_websocket(), 1);
8139
8511
  var import_websocket_server = __toESM(require_websocket_server(), 1);
8140
8512
 
8141
8513
  // src/worker/pty.ts
8142
- import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
8514
+ import { existsSync as existsSync7, readdirSync as readdirSync2 } from "node:fs";
8143
8515
  import { homedir as homedir3 } from "node:os";
8144
- import { join as join9 } from "node:path";
8516
+ import { join as join11 } from "node:path";
8145
8517
  var MAX_TERMINALS = 10;
8146
8518
  var ptyArgs = (agentSessionId, hasJsonl) => hasJsonl ? ["--resume", agentSessionId] : ["--session-id", agentSessionId];
8147
8519
  var hasSessionJsonl = (agentSessionId) => {
8148
- const root = join9(homedir3(), ".claude", "projects");
8149
- if (!existsSync6(root)) return false;
8520
+ const root = join11(homedir3(), ".claude", "projects");
8521
+ if (!existsSync7(root)) return false;
8150
8522
  for (const dir of readdirSync2(root))
8151
- if (existsSync6(join9(root, dir, `${agentSessionId}.jsonl`))) return true;
8523
+ if (existsSync7(join11(root, dir, `${agentSessionId}.jsonl`))) return true;
8152
8524
  return false;
8153
8525
  };
8154
8526
  var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
@@ -8159,13 +8531,13 @@ var createTerminalManager = (deps) => {
8159
8531
  const { cfg, log, hasChild } = deps;
8160
8532
  const terminals = /* @__PURE__ */ new Map();
8161
8533
  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;
8534
+ 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
8535
  if (why) {
8164
8536
  log(`terminal #${sessionId}: ${why} \u2014 refusing`);
8165
8537
  return;
8166
8538
  }
8167
8539
  const claudeBin = process.env.BATON_CLAUDE_BIN ?? "claude";
8168
- const term = spawn2(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
8540
+ const term = spawn4(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
8169
8541
  name: "xterm-color",
8170
8542
  cols: 80,
8171
8543
  rows: 24,
@@ -8251,14 +8623,20 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8251
8623
  log,
8252
8624
  hasChild: (id) => supervisor.has(id)
8253
8625
  });
8626
+ const services = createServiceSupervisor({
8627
+ getSession: (sessionId) => client.sessions.get(sessionId),
8628
+ sendReport: (report) => client.services.report(report),
8629
+ cfg,
8630
+ log
8631
+ });
8254
8632
  let fatal = false;
8255
8633
  let stop = () => {
8256
8634
  };
8257
8635
  const liveness = createLiveness({
8258
- client,
8259
- cfg,
8636
+ heartbeat: () => client.workers.heartbeat(cfg.machineId),
8260
8637
  log,
8261
8638
  isStreamOpen: () => es.readyState === ES_OPEN,
8639
+ reportRuntime: () => services.report(),
8262
8640
  onTrip: (reason) => {
8263
8641
  if (fatal) return;
8264
8642
  log(`${reason} \u2014 exiting so the supervisor restarts a clean worker`);
@@ -8278,6 +8656,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8278
8656
  es.onopen = () => {
8279
8657
  liveness.markStreamOk();
8280
8658
  void supervisor.reconcile().catch((err) => log(`reconcile failed: ${String(err)}`));
8659
+ void services.report().catch((err) => log(`service report failed: ${String(err)}`));
8281
8660
  };
8282
8661
  es.onmessage = (e) => {
8283
8662
  liveness.markStreamOk();
@@ -8286,14 +8665,18 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8286
8665
  if (cmd.cmd === "session.start")
8287
8666
  void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
8288
8667
  else if (cmd.cmd === "session.stop") supervisor.stop(cmd.sessionId);
8289
- else if (cmd.cmd === "session.delete") supervisor.remove(cmd.sessionId, cmd.worktreePath);
8668
+ else if (cmd.cmd === "session.delete")
8669
+ void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
8290
8670
  else if (cmd.cmd === "session.title")
8291
8671
  void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath).catch((err) => log(`title failed: ${String(err)}`));
8292
8672
  else if (cmd.cmd === "session.terminal") {
8293
8673
  if (cmd.action === "open")
8294
8674
  terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
8295
8675
  else terminals.close(cmd.sessionId);
8296
- }
8676
+ } else if (cmd.cmd === "service.run")
8677
+ void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
8678
+ else if (cmd.cmd === "service.stop")
8679
+ void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
8297
8680
  } catch {
8298
8681
  }
8299
8682
  };
@@ -8307,6 +8690,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8307
8690
  });
8308
8691
  es.close();
8309
8692
  liveness.stop();
8693
+ services.killAll();
8310
8694
  supervisor.killAll();
8311
8695
  terminals.killAll();
8312
8696
  if (fatal) process.exit(1);
@@ -8314,17 +8698,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
8314
8698
 
8315
8699
  // src/worker/machine-id.ts
8316
8700
  import { randomUUID as randomUUID2 } from "node:crypto";
8317
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
8701
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
8318
8702
  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");
8703
+ import { dirname as dirname4, join as join12 } from "node:path";
8704
+ var machineIdPath = (env = process.env) => join12(env.XDG_DATA_HOME ?? join12(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
8321
8705
  var readOrCreateMachineId = (path = machineIdPath()) => {
8322
- if (existsSync8(path)) {
8706
+ if (existsSync9(path)) {
8323
8707
  const s = readFileSync6(path, "utf8").trim();
8324
8708
  if (s) return s;
8325
8709
  }
8326
8710
  const id = randomUUID2();
8327
- mkdirSync4(dirname3(path), { recursive: true });
8711
+ mkdirSync5(dirname4(path), { recursive: true });
8328
8712
  writeFileSync5(path, `${id}
8329
8713
  `, "utf8");
8330
8714
  return id;
@@ -8639,6 +9023,7 @@ var main = defineCommand({
8639
9023
  init,
8640
9024
  worker,
8641
9025
  session,
9026
+ service,
8642
9027
  loop,
8643
9028
  relay,
8644
9029
  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.7",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"