fifony 0.1.47 → 0.1.48

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 (89) hide show
  1. package/README.md +78 -0
  2. package/app/dist/assets/CommandPalette-CZDG20HW.js +1 -0
  3. package/app/dist/assets/{KeyboardShortcutsHelp-CqEFfGcE.js → KeyboardShortcutsHelp-TYhQc4aA.js} +1 -1
  4. package/app/dist/assets/OnboardingWizard-CQ9YmVIT.js +1 -0
  5. package/app/dist/assets/agents.lazy-CgakDm_P.js +1 -0
  6. package/app/dist/assets/analytics.lazy-C0rw3sov.js +1 -0
  7. package/app/dist/assets/{createLucideIcon-luywpIq4.js → createLucideIcon-B3bah5lk.js} +1 -1
  8. package/app/dist/assets/hooks-CNPue7d7.js +1 -0
  9. package/app/dist/assets/index-B8XCmr0-.css +1 -0
  10. package/app/dist/assets/index-Dfn02uW3.js +47 -0
  11. package/app/dist/assets/index.lazy-JdqhBwPd.js +44 -0
  12. package/app/dist/assets/services-CHpVij2M.css +1 -0
  13. package/app/dist/assets/services.lazy-BShKUCOt.js +12 -0
  14. package/app/dist/assets/vendor-X6HTElZW.js +9 -0
  15. package/app/dist/assets/viz-Dsh_q2DK.js +4 -0
  16. package/app/dist/index.html +5 -4
  17. package/app/dist/service-worker.js +32 -1
  18. package/dist/agent/run-local.js +89 -76
  19. package/dist/{agent-DFSFG6DG.js → agent-DJ4SCNBZ.js} +22 -17
  20. package/dist/{analytics-broadcaster-O4AE3RUK.js → analytics-broadcaster-INNYWHDJ.js} +25 -20
  21. package/dist/approve-plan.command-WE2CO3H2.js +21 -0
  22. package/dist/{chunk-HOIOVUHI.js → chunk-5M7PBFMZ.js} +8 -6
  23. package/dist/chunk-7R7XFXJM.js +1247 -0
  24. package/dist/{chunk-2PRRKBG6.js → chunk-A4P2MYJF.js} +22 -9
  25. package/dist/chunk-AFOV3ZAF.js +722 -0
  26. package/dist/chunk-AFP36N23.js +134 -0
  27. package/dist/{chunk-AAZKYWOY.js → chunk-AFYKGVSP.js} +103 -8
  28. package/dist/chunk-APJOZXRP.js +737 -0
  29. package/dist/chunk-DLSPRIQL.js +241 -0
  30. package/dist/{chunk-5AMWD66T.js → chunk-EDIPHR5B.js} +6 -4
  31. package/dist/{chunk-K36BWMUV.js → chunk-JU3MF3MW.js} +2526 -736
  32. package/dist/{chunk-7TXZYZR5.js → chunk-N5HCNY4O.js} +7 -5
  33. package/dist/{chunk-JRLWLZOD.js → chunk-NKMZYPIS.js} +31 -23
  34. package/dist/{chunk-PI7Y77R3.js → chunk-OFIVTM2E.js} +17 -7
  35. package/dist/{chunk-QH6VCTET.js → chunk-RCSJFMQG.js} +909 -98
  36. package/dist/{chunk-AAVROEQC.js → chunk-UR7T7IA6.js} +253 -349
  37. package/dist/{chunk-QHISYRXJ.js → chunk-VOYLU3MI.js} +57 -3
  38. package/dist/{chunk-EBCSQFPR.js → chunk-W5IULOWV.js} +2 -3
  39. package/dist/chunk-X37RNTWU.js +193 -0
  40. package/dist/{chunk-PACI3T4I.js → chunk-XY2APMDE.js} +13 -5
  41. package/dist/chunk-Z6ZWNWWR.js +34 -0
  42. package/dist/cli.js +45 -17
  43. package/dist/constants-AAP7ZGCX.js +124 -0
  44. package/dist/create-issue.command-SX3AXXIC.js +29 -0
  45. package/dist/fsm-agent-JGV22WK4.js +59 -0
  46. package/dist/{fsm-issue-EHTSKMFN.js → fsm-issue-LHIJM5VB.js} +12 -8
  47. package/dist/{fsm-service-7O4AJG2R.js → fsm-service-GGDKUTWS.js} +13 -4
  48. package/dist/{helpers-ON2S7UEF.js → helpers-AENVYEZJ.js} +6 -2
  49. package/dist/{issue-log-broadcaster-FZGVEEIX.js → issue-log-broadcaster-QQWM7LOV.js} +29 -18
  50. package/dist/{issues-3YNNTB4U.js → issues-RXFKKSXB.js} +10 -7
  51. package/dist/{log-analyzer-EIX6R6PP.js → log-analyzer-4LNXQISY.js} +30 -20
  52. package/dist/{logger-IFLXTQPS.js → logger-4F6ATWNA.js} +2 -1
  53. package/dist/mcp/server.js +6 -2
  54. package/dist/merge-workspace.command-ZNGIZC4O.js +29 -0
  55. package/dist/{parallel-executor-DWESCNX3.js → parallel-executor-OL5CB33L.js} +78 -19
  56. package/dist/{pid-manager-UBWXVSMD.js → pid-manager-EDT4DHAU.js} +2 -1
  57. package/dist/queue-workers-NSKIIMQ2.js +43 -0
  58. package/dist/replan-issue.command-73PETERX.js +21 -0
  59. package/dist/retry-issue.command-DIDP4OCS.js +21 -0
  60. package/dist/reverse-proxy-server-QSS3H4UH.js +97 -0
  61. package/dist/scheduler-5YORYECF.js +37 -0
  62. package/dist/service-log-broadcaster-JIUP2L3D.js +21 -0
  63. package/dist/{settings-SOTIS6ZD.js → settings-ZNDXYL46.js} +34 -23
  64. package/dist/settings.resource-OKUHXICJ.js +35 -0
  65. package/dist/{store-S3NAYZ3S.js → store-P3ACO6YA.js} +22 -17
  66. package/dist/telemetry-KVUFHDQS.js +828 -0
  67. package/dist/template-variants-HEPLYKMP.js +24 -0
  68. package/dist/trace-bundle-IJOV7IWH.js +41 -0
  69. package/dist/{web-push-QCTLS7EJ.js → web-push-X2LLMQ4M.js} +2 -1
  70. package/dist/websocket-Q2TUCIC2.js +103 -0
  71. package/dist/{workspace-OS7GPMCN.js → workspace-TDX3NJCX.js} +10 -6
  72. package/package.json +12 -9
  73. package/app/dist/assets/CommandPalette-CL8p78lG.js +0 -1
  74. package/app/dist/assets/OnboardingWizard-BmI50ZUv.js +0 -1
  75. package/app/dist/assets/analytics.lazy-CXGjZabc.js +0 -1
  76. package/app/dist/assets/index-CEaccpYh.js +0 -96
  77. package/app/dist/assets/index-CzzWGzux.css +0 -1
  78. package/app/dist/assets/vendor-uqBx3VSC.js +0 -9
  79. package/dist/approve-plan.command-QGQZZXTQ.js +0 -17
  80. package/dist/chunk-N4KFNX2G.js +0 -370
  81. package/dist/chunk-VM5QAYP5.js +0 -404
  82. package/dist/create-issue.command-VAKYRECC.js +0 -24
  83. package/dist/merge-workspace.command-T2NIGR4M.js +0 -24
  84. package/dist/queue-workers-V57BYXAY.js +0 -38
  85. package/dist/replan-issue.command-2GQ3QXCR.js +0 -17
  86. package/dist/retry-issue.command-GJBUUYDJ.js +0 -17
  87. package/dist/scheduler-KYILMWLD.js +0 -32
  88. package/dist/settings.resource-JMD3JQOS.js +0 -30
  89. package/dist/websocket-T2Y3BY4B.js +0 -61
@@ -1,13 +1,21 @@
1
1
  import {
2
- S3DB_SERVICES_RESOURCE,
2
+ buildProxyEnvVars,
3
+ getMeshRuntimePortSnapshot
4
+ } from "./chunk-7R7XFXJM.js";
5
+ import {
6
+ isProcessAlive
7
+ } from "./chunk-3NE23NYW.js";
8
+ import {
9
+ init_helpers,
3
10
  now
4
- } from "./chunk-VM5QAYP5.js";
11
+ } from "./chunk-DLSPRIQL.js";
12
+ import {
13
+ S3DB_SERVICES_RESOURCE,
14
+ init_constants
15
+ } from "./chunk-X37RNTWU.js";
5
16
  import {
6
17
  logger
7
18
  } from "./chunk-PXTIWKLQ.js";
8
- import {
9
- isProcessAlive
10
- } from "./chunk-3NE23NYW.js";
11
19
 
12
20
  // src/persistence/plugins/fsm-service.ts
13
21
  import {
@@ -16,12 +24,14 @@ import {
16
24
  openSync,
17
25
  readFileSync,
18
26
  readSync,
27
+ renameSync,
19
28
  rmSync,
20
29
  statSync,
21
30
  writeFileSync
22
31
  } from "fs";
23
32
  import { join, resolve } from "path";
24
- import { spawn } from "child_process";
33
+ import { spawn, execSync } from "child_process";
34
+ init_helpers();
25
35
 
26
36
  // src/domains/service-env.ts
27
37
  var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -57,285 +67,54 @@ function mergeServiceEnvironment(globalEnv, serviceEnv) {
57
67
  function shellQuoteEnvValue(value) {
58
68
  return `'${value.replace(/'/g, `'"'"'`)}'`;
59
69
  }
60
- function buildServiceCommand(command, globalEnv, serviceEnv) {
70
+ function buildServiceCommand(command, globalEnv, serviceEnv, enforcedEnv) {
61
71
  const baseCommand = command.trim();
62
72
  if (!baseCommand) return "";
63
- const env = mergeServiceEnvironment(globalEnv, serviceEnv);
73
+ const env = {
74
+ ...mergeServiceEnvironment(globalEnv, serviceEnv),
75
+ ...enforcedEnv ?? {}
76
+ };
64
77
  const assignments = Object.entries(env).map(([key, value]) => `${key}=${shellQuoteEnvValue(value)}`);
65
78
  return assignments.length > 0 ? `${assignments.join(" ")} ${baseCommand}` : baseCommand;
66
79
  }
67
80
 
68
- // src/persistence/plugins/traffic-proxy-server.ts
69
- import { createServer } from "http";
70
- import {
71
- createHttpForwardProxy
72
- } from "raffel";
73
-
74
- // src/domains/traffic-proxy.ts
75
- var TrafficRingBuffer = class {
76
- constructor(capacity = 1e3) {
77
- this.capacity = capacity;
78
- this.buf = new Array(capacity);
79
- }
80
- buf;
81
- head = 0;
82
- count = 0;
83
- push(entry) {
84
- this.buf[this.head] = entry;
85
- this.head = (this.head + 1) % this.capacity;
86
- if (this.count < this.capacity) this.count++;
87
- }
88
- getAll() {
89
- if (this.count === 0) return [];
90
- if (this.count < this.capacity) return this.buf.slice(0, this.count);
91
- return [...this.buf.slice(this.head), ...this.buf.slice(0, this.head)];
92
- }
93
- getRecent(n) {
94
- const all = this.getAll();
95
- return n >= all.length ? all : all.slice(-n);
96
- }
97
- clear() {
98
- this.head = 0;
99
- this.count = 0;
100
- }
101
- get size() {
102
- return this.count;
103
- }
104
- };
105
- function edgeKey(source, target) {
106
- return `${source}\0${target}`;
81
+ // src/persistence/plugins/fsm-service.ts
82
+ init_constants();
83
+ var STARTING_GRACE_MS = 3e3;
84
+ var STOPPING_KILL_MS = 5e3;
85
+ var SERVICE_WATCHER_INTERVAL_MS = 5e3;
86
+ function pidPath(fifonyDir, id) {
87
+ return join(fifonyDir, `service-${id}.pid`);
107
88
  }
108
- var MAX_TOP_PATHS = 5;
109
- var MAX_P95_SAMPLES = 200;
110
- var ServiceGraphAccumulator = class {
111
- edges = /* @__PURE__ */ new Map();
112
- totalRequests = 0;
113
- capturedSince = (/* @__PURE__ */ new Date()).toISOString();
114
- record(entry) {
115
- const src = entry.sourceServiceId ?? "unknown";
116
- const tgt = entry.targetServiceId ?? "external";
117
- const key = edgeKey(src, tgt);
118
- this.totalRequests++;
119
- let edge = this.edges.get(key);
120
- if (!edge) {
121
- edge = {
122
- source: src,
123
- target: tgt,
124
- requestCount: 0,
125
- errorCount: 0,
126
- latency: { sum: 0, count: 0, values: [] },
127
- lastSeenAt: entry.startedAt,
128
- pathCounts: /* @__PURE__ */ new Map()
129
- };
130
- this.edges.set(key, edge);
131
- }
132
- edge.requestCount++;
133
- if (entry.statusCode >= 400 || entry.error) edge.errorCount++;
134
- edge.latency.sum += entry.durationMs;
135
- edge.latency.count++;
136
- if (edge.latency.values.length >= MAX_P95_SAMPLES) edge.latency.values.shift();
137
- edge.latency.values.push(entry.durationMs);
138
- edge.lastSeenAt = entry.startedAt;
139
- edge.pathCounts.set(entry.path, (edge.pathCounts.get(entry.path) ?? 0) + 1);
140
- }
141
- getGraph(services) {
142
- const nodes = services.map((s) => ({
143
- id: s.id,
144
- name: s.name,
145
- state: s.state,
146
- port: s.port
147
- }));
148
- const edges = [];
149
- for (const e of this.edges.values()) {
150
- const sorted = [...e.latency.values].sort((a, b) => a - b);
151
- const pct = (p) => sorted.length > 0 ? sorted[Math.min(Math.floor(sorted.length * p), sorted.length - 1)] : 0;
152
- const topPaths = [...e.pathCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_TOP_PATHS).map(([path, count]) => ({ path, count }));
153
- edges.push({
154
- source: e.source,
155
- target: e.target,
156
- requestCount: e.requestCount,
157
- errorCount: e.errorCount,
158
- avgLatencyMs: e.latency.count > 0 ? Math.round(e.latency.sum / e.latency.count) : 0,
159
- p50LatencyMs: pct(0.5),
160
- p90LatencyMs: pct(0.9),
161
- p95LatencyMs: pct(0.95),
162
- p99LatencyMs: pct(0.99),
163
- lastSeenAt: e.lastSeenAt,
164
- topPaths
165
- });
166
- }
167
- return {
168
- nodes,
169
- edges,
170
- capturedSince: this.capturedSince,
171
- totalRequests: this.totalRequests
172
- };
173
- }
174
- reset() {
175
- this.edges.clear();
176
- this.totalRequests = 0;
177
- this.capturedSince = (/* @__PURE__ */ new Date()).toISOString();
178
- }
179
- };
180
- function extractServiceIdFromProxyAuth(headers) {
181
- const auth = headers["proxy-authorization"];
182
- if (!auth) return null;
183
- const match = auth.match(/^Basic\s+(.+)$/i);
184
- if (!match) return null;
89
+ function serviceLogPath(fifonyDir, id) {
90
+ return join(fifonyDir, `service-${id}.log`);
91
+ }
92
+ function serviceLogGenerationPath(fifonyDir, id, generation) {
93
+ const base = serviceLogPath(fifonyDir, id);
94
+ return generation === 0 ? base : `${base}.${generation}`;
95
+ }
96
+ function rotateServiceLogs(fifonyDir, id) {
97
+ const base = serviceLogPath(fifonyDir, id);
185
98
  try {
186
- const decoded = Buffer.from(match[1], "base64").toString("utf8");
187
- const colon = decoded.indexOf(":");
188
- return colon > 0 ? decoded.slice(0, colon) : decoded;
99
+ if (!existsSync(base) || statSync(base).size === 0) return;
189
100
  } catch {
190
- return null;
101
+ return;
191
102
  }
192
- }
193
- function resolveTargetService(url, services) {
194
103
  try {
195
- const parsed = new URL(url);
196
- const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
197
- const match = services.find((s) => s.port === port);
198
- return match?.id ?? null;
104
+ if (existsSync(`${base}.2`)) rmSync(`${base}.2`);
199
105
  } catch {
200
- return null;
201
106
  }
202
- }
203
- function buildProxyEnvVars(proxyPort, serviceId, dashboardPort, extraNoPorts = []) {
204
- const proxyUrl = `http://${encodeURIComponent(serviceId)}:fifony@localhost:${proxyPort}`;
205
- const noProxyList = [`localhost:${dashboardPort}`, ...extraNoPorts.map((p) => `localhost:${p}`)].join(",");
206
- return {
207
- HTTP_PROXY: proxyUrl,
208
- http_proxy: proxyUrl,
209
- NO_PROXY: noProxyList,
210
- no_proxy: noProxyList
211
- };
212
- }
213
- var entrySeq = 0;
214
- function buildTrafficEntry(method, url, requestSize, statusCode, responseSize, sourceServiceId, targetServiceId, startTime, error) {
215
- let path;
216
107
  try {
217
- path = new URL(url).pathname;
108
+ if (existsSync(`${base}.1`)) renameSync(`${base}.1`, `${base}.2`);
218
109
  } catch {
219
- path = url;
220
110
  }
221
- return {
222
- id: `tr_${Date.now()}_${++entrySeq}`,
223
- sourceServiceId,
224
- targetServiceId,
225
- method,
226
- url,
227
- path,
228
- statusCode,
229
- requestSize,
230
- responseSize,
231
- startedAt: new Date(startTime).toISOString(),
232
- durationMs: Date.now() - startTime,
233
- error
234
- };
235
- }
236
-
237
- // src/persistence/plugins/traffic-proxy-server.ts
238
- var server = null;
239
- var proxy = null;
240
- var buffer = null;
241
- var graph = null;
242
- var boundPort = null;
243
- var onEntryCallback = null;
244
- var servicesAccessor = null;
245
- function setServicesAccessor(fn) {
246
- servicesAccessor = fn;
247
- }
248
- async function startTrafficProxy(options = {}) {
249
- if (server) {
250
- logger.warn("Traffic proxy already running, skipping start");
251
- return boundPort;
111
+ try {
112
+ renameSync(base, `${base}.1`);
113
+ } catch {
252
114
  }
253
- const port = options.port ?? 0;
254
- const bufferSize = options.bufferSize ?? 1e3;
255
- buffer = new TrafficRingBuffer(bufferSize);
256
- graph = new ServiceGraphAccumulator();
257
- onEntryCallback = options.onEntry ?? null;
258
- proxy = createHttpForwardProxy({
259
- timeout: 3e4,
260
- maxBodySize: 10 * 1024 * 1024
261
- });
262
- server = createServer((req, res) => {
263
- const startTime = Date.now();
264
- const method = req.method ?? "GET";
265
- const url = req.url ?? "";
266
- const sourceId = extractServiceIdFromProxyAuth(
267
- req.headers
268
- );
269
- const services = servicesAccessor?.() ?? [];
270
- const targetId = resolveTargetService(url, services);
271
- const requestSize = Number(req.headers["content-length"] ?? 0);
272
- res.on("finish", () => {
273
- const entry = buildTrafficEntry(
274
- method,
275
- url,
276
- requestSize,
277
- res.statusCode,
278
- Number(res.getHeader("content-length") ?? 0),
279
- sourceId,
280
- targetId,
281
- startTime
282
- );
283
- buffer?.push(entry);
284
- graph?.record(entry);
285
- onEntryCallback?.(entry);
286
- });
287
- proxy.requestHandler(req, res);
288
- });
289
- return new Promise((resolve2, reject) => {
290
- server.listen(port, "127.0.0.1", () => {
291
- const addr = server.address();
292
- boundPort = typeof addr === "object" && addr ? addr.port : port;
293
- logger.info({ port: boundPort }, "Mesh traffic proxy started");
294
- resolve2(boundPort);
295
- });
296
- server.on("error", (err) => {
297
- logger.error({ err }, "Mesh traffic proxy failed to start");
298
- reject(err);
299
- });
300
- });
301
- }
302
- async function stopTrafficProxy() {
303
- if (!server) return;
304
- return new Promise((resolve2) => {
305
- server.close(() => {
306
- logger.info("Mesh traffic proxy stopped");
307
- server = null;
308
- proxy = null;
309
- boundPort = null;
310
- resolve2();
311
- });
312
- });
313
- }
314
- function getTrafficProxyPort() {
315
- return boundPort;
316
- }
317
- function isTrafficProxyRunning() {
318
- return server !== null;
319
- }
320
- function getTrafficBuffer() {
321
- return buffer;
322
- }
323
- function getServiceGraph() {
324
- return graph;
325
- }
326
- function getTrafficProxyStats() {
327
- return proxy?.stats ?? null;
328
- }
329
-
330
- // src/persistence/plugins/fsm-service.ts
331
- var STARTING_GRACE_MS = 3e3;
332
- var STOPPING_KILL_MS = 5e3;
333
- var SERVICE_WATCHER_INTERVAL_MS = 5e3;
334
- function pidPath(fifonyDir, id) {
335
- return join(fifonyDir, `service-${id}.pid`);
336
- }
337
- function serviceLogPath(fifonyDir, id) {
338
- return join(fifonyDir, `service-${id}.log`);
115
+ errorCountCache.delete(base);
116
+ errorCountCache.delete(`${base}.1`);
117
+ errorCountCache.delete(`${base}.2`);
339
118
  }
340
119
  var ERROR_PATTERN = /\b(ERROR|Exception|FATAL|FAIL)\b/gi;
341
120
  var errorCountCache = /* @__PURE__ */ new Map();
@@ -397,12 +176,16 @@ function spawnProcess(entry, targetRoot, fifonyDir, globalEnv) {
397
176
  const cwd = entry.cwd ? resolve(targetRoot, entry.cwd) : targetRoot;
398
177
  const log = serviceLogPath(fifonyDir, entry.id);
399
178
  let mergedGlobalEnv = globalEnv;
400
- const proxyPort = getTrafficProxyPort();
179
+ const proxyPort = getMeshRuntimePortSnapshot();
401
180
  if (proxyPort) {
402
181
  const dashPort = Number(process.env.FIFONY_PORT ?? 4e3);
403
182
  mergedGlobalEnv = { ...globalEnv ?? {}, ...buildProxyEnvVars(proxyPort, entry.id, dashPort) };
404
183
  }
405
- const command = buildServiceCommand(entry.command, mergedGlobalEnv, entry.env);
184
+ const enforcedEnv = entry.port ? {
185
+ PORT: String(entry.port)
186
+ } : {};
187
+ const command = buildServiceCommand(entry.command, mergedGlobalEnv, entry.env, enforcedEnv);
188
+ rotateServiceLogs(fifonyDir, entry.id);
406
189
  try {
407
190
  writeFileSync(log, "");
408
191
  } catch {
@@ -424,6 +207,65 @@ function spawnProcess(entry, targetRoot, fifonyDir, globalEnv) {
424
207
  }
425
208
  return { pid: child.pid, command };
426
209
  }
210
+ function killProcessTree(pid) {
211
+ try {
212
+ const children = execSync(`pgrep -P ${pid} 2>/dev/null || true`, { encoding: "utf8" }).trim();
213
+ if (children) {
214
+ for (const childStr of children.split("\n")) {
215
+ const childPid = parseInt(childStr.trim(), 10);
216
+ if (!isNaN(childPid) && childPid > 0) {
217
+ killProcessTree(childPid);
218
+ }
219
+ }
220
+ }
221
+ try {
222
+ process.kill(pid, "SIGKILL");
223
+ } catch {
224
+ }
225
+ } catch {
226
+ }
227
+ }
228
+ function killProcessesOnPort(port) {
229
+ try {
230
+ const pids = execSync(`lsof -ti tcp:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
231
+ if (!pids) return;
232
+ for (const pidStr of pids.split("\n")) {
233
+ const pid = parseInt(pidStr.trim(), 10);
234
+ if (!isNaN(pid) && pid > 0) {
235
+ try {
236
+ process.kill(pid, "SIGKILL");
237
+ } catch {
238
+ }
239
+ }
240
+ }
241
+ logger.debug({ port, pids }, "[ServiceFSM] Killed orphaned processes on port");
242
+ } catch {
243
+ }
244
+ }
245
+ function cleanupServiceProcesses(pid, port) {
246
+ if (pid && pid > 0) {
247
+ let allPorts = [];
248
+ try {
249
+ const lsofOut = execSync(`lsof -aPi tcp -sTCP:LISTEN -p ${pid} -Fn 2>/dev/null || true`, { encoding: "utf8" });
250
+ for (const line of lsofOut.split("\n")) {
251
+ if (line.startsWith("n")) {
252
+ const match = line.match(/:(\d+)$/);
253
+ if (match) allPorts.push(parseInt(match[1], 10));
254
+ }
255
+ }
256
+ } catch {
257
+ }
258
+ try {
259
+ process.kill(-pid, "SIGKILL");
260
+ } catch {
261
+ }
262
+ killProcessTree(pid);
263
+ for (const p of allPorts) {
264
+ killProcessesOnPort(p);
265
+ }
266
+ }
267
+ if (port) killProcessesOnPort(port);
268
+ }
427
269
  function autoRestartBackoffMs(crashCount) {
428
270
  return Math.min(Math.pow(2, crashCount) * 1e3, 6e4);
429
271
  }
@@ -475,13 +317,12 @@ function getServiceStatus(entry, fifonyDir) {
475
317
  function getAllServiceStatuses(entries, fifonyDir) {
476
318
  return entries.map((e) => getServiceStatus(e, fifonyDir));
477
319
  }
478
- function readServiceLogTail(id, fifonyDir, bytes = 8192) {
479
- const log = serviceLogPath(fifonyDir, id);
480
- if (!existsSync(log)) return "";
320
+ function readLogFileTail(filePath, bytes) {
321
+ if (!existsSync(filePath)) return "";
481
322
  try {
482
- const size = statSync(log).size;
323
+ const size = statSync(filePath).size;
483
324
  const readSize = Math.min(size, bytes);
484
- const fd = openSync(log, "r");
325
+ const fd = openSync(filePath, "r");
485
326
  const buf = Buffer.alloc(readSize);
486
327
  readSync(fd, buf, 0, readSize, Math.max(0, size - readSize));
487
328
  closeSync(fd);
@@ -490,12 +331,27 @@ function readServiceLogTail(id, fifonyDir, bytes = 8192) {
490
331
  return "";
491
332
  }
492
333
  }
334
+ function readServiceLogTail(id, fifonyDir, bytes = 8192) {
335
+ return readLogFileTail(serviceLogPath(fifonyDir, id), bytes);
336
+ }
337
+ function readServiceLogGenerationTail(id, fifonyDir, generation, bytes = 16384) {
338
+ return readLogFileTail(serviceLogGenerationPath(fifonyDir, id, generation), bytes);
339
+ }
340
+ function listServiceLogGenerations(fifonyDir, id) {
341
+ const base = serviceLogPath(fifonyDir, id);
342
+ const generations = [];
343
+ if (existsSync(base)) generations.push(0);
344
+ if (existsSync(`${base}.1`)) generations.push(1);
345
+ if (existsSync(`${base}.2`)) generations.push(2);
346
+ return generations;
347
+ }
493
348
  function reconcileServiceStates(entries, fifonyDir) {
494
349
  for (const entry of entries) {
495
350
  const info = readPidInfo(fifonyDir, entry.id);
496
351
  if (!info) continue;
497
352
  if (info.state === "stopped") continue;
498
353
  if (!isProcessAlive(info.pid)) {
354
+ cleanupServiceProcesses(info.pid, entry.port);
499
355
  const crashCount = (info.crashCount ?? 0) + 1;
500
356
  writePidInfo(fifonyDir, entry.id, {
501
357
  ...info,
@@ -503,7 +359,7 @@ function reconcileServiceStates(entries, fifonyDir) {
503
359
  crashCount,
504
360
  lastCrashAt: now()
505
361
  });
506
- logger.info({ id: entry.id, crashCount }, "[Service] Boot: process dead \u2192 crashed");
362
+ logger.info({ id: entry.id, crashCount }, "[Service] Boot: process dead \u2192 crashed (port cleaned)");
507
363
  }
508
364
  }
509
365
  }
@@ -521,19 +377,69 @@ function setServiceResourceStateApi(api) {
521
377
  serviceResourceStateApi = api;
522
378
  }
523
379
  async function sendServiceEvent(entityId, event, context = {}) {
524
- if (!serviceResourceStateApi) {
525
- throw new Error("Service state machine not initialized");
380
+ if (!serviceRuntime) throw new Error("Service runtime not initialized");
381
+ const { fifonyDir, targetRoot, getEntries, getGlobalEnv, onTransition } = serviceRuntime;
382
+ const entry = (getEntries() ?? []).find((e) => e.id === entityId);
383
+ if (event === "START") {
384
+ const existing = readPidInfo(fifonyDir, entityId);
385
+ cleanupServiceProcesses(existing?.pid ?? null, entry?.port);
386
+ if (!entry) throw new Error(`Service entry not found: ${entityId}`);
387
+ const globalEnv = getGlobalEnv() ?? {};
388
+ const serviceVaulterEnv = serviceRuntime.getServiceEnv?.(entityId) ?? {};
389
+ const effectiveEntry = Object.keys(serviceVaulterEnv).length > 0 ? { ...entry, env: { ...serviceVaulterEnv, ...entry.env ?? {} } } : entry;
390
+ const spawned = spawnProcess(effectiveEntry, targetRoot, fifonyDir, globalEnv);
391
+ writePidInfo(fifonyDir, entityId, {
392
+ pid: spawned.pid,
393
+ command: spawned.command,
394
+ startedAt: now(),
395
+ state: "starting",
396
+ crashCount: 0
397
+ });
398
+ const transition = {
399
+ id: entityId,
400
+ from: "stopped",
401
+ to: "starting",
402
+ reason: "manual start",
403
+ pid: spawned.pid
404
+ };
405
+ onTransition?.(transition);
406
+ logger.info({ id: entityId, pid: spawned.pid }, "[ServiceFSM] START \u2192 starting");
407
+ return { pid: spawned.pid, state: "starting" };
526
408
  }
527
- try {
528
- await serviceResourceStateApi.send(entityId, event, context);
529
- } catch (err) {
530
- if (String(err).includes("not found") || String(err).includes("not initialized")) {
531
- await serviceResourceStateApi.initialize(entityId, { state: "stopped" });
532
- await serviceResourceStateApi.send(entityId, event, context);
533
- } else {
534
- throw err;
409
+ if (event === "STOP") {
410
+ const info = readPidInfo(fifonyDir, entityId);
411
+ if (!info || info.state === "stopped") return { state: "stopped" };
412
+ if (entry?.stopCommand) {
413
+ try {
414
+ const cwd = entry.cwd ? join(targetRoot, entry.cwd) : targetRoot;
415
+ execSync(entry.stopCommand, { cwd, stdio: "pipe", timeout: 15e3 });
416
+ logger.info({ id: entityId, stopCommand: entry.stopCommand }, "[ServiceFSM] stopCommand executed");
417
+ } catch (err) {
418
+ logger.warn({ err, id: entityId }, "[ServiceFSM] stopCommand failed, falling back to SIGTERM");
419
+ if (isProcessAlive(info.pid)) {
420
+ try {
421
+ process.kill(-info.pid, "SIGTERM");
422
+ } catch {
423
+ }
424
+ }
425
+ }
426
+ } else if (isProcessAlive(info.pid)) {
427
+ try {
428
+ process.kill(-info.pid, "SIGTERM");
429
+ } catch {
430
+ }
431
+ try {
432
+ process.kill(info.pid, "SIGTERM");
433
+ } catch {
434
+ }
535
435
  }
436
+ writePidInfo(fifonyDir, entityId, { ...info, state: "stopping", stoppingAt: now() });
437
+ const transition = { id: entityId, from: info.state, to: "stopping", reason: "manual stop", pid: info.pid };
438
+ onTransition?.(transition);
439
+ logger.info({ id: entityId, pid: info.pid }, "[ServiceFSM] STOP \u2192 stopping");
440
+ return { state: "stopping" };
536
441
  }
442
+ throw new Error(`Unknown service event: ${event}`);
537
443
  }
538
444
  function resolveServiceEntry(entityId) {
539
445
  if (!serviceRuntime) return null;
@@ -552,10 +458,13 @@ var serviceStateMachineConfig = {
552
458
  stateField: "state",
553
459
  initialState: "stopped",
554
460
  autoCleanup: false,
461
+ hooks: {
462
+ afterTransition: "onServiceTransition"
463
+ },
555
464
  states: {
556
465
  stopped: {
557
466
  on: { START: "starting" },
558
- entry: "onEnterStopped"
467
+ afterEnter: "onEnterStopped"
559
468
  },
560
469
  starting: {
561
470
  on: {
@@ -563,7 +472,7 @@ var serviceStateMachineConfig = {
563
472
  PROCESS_DIED: "crashed",
564
473
  STOP: "stopping"
565
474
  },
566
- entry: "spawnService",
475
+ afterEnter: "spawnService",
567
476
  triggers: [{
568
477
  type: "function",
569
478
  interval: TRIGGER_INTERVAL_MS,
@@ -592,7 +501,7 @@ var serviceStateMachineConfig = {
592
501
  PROCESS_DIED: "crashed",
593
502
  STOP: "stopping"
594
503
  },
595
- entry: "onEnterRunning",
504
+ afterEnter: "onEnterRunning",
596
505
  triggers: [{
597
506
  type: "function",
598
507
  interval: TRIGGER_INTERVAL_MS,
@@ -610,7 +519,7 @@ var serviceStateMachineConfig = {
610
519
  PROCESS_EXITED: "stopped",
611
520
  KILL_TIMEOUT: "stopped"
612
521
  },
613
- entry: "sendSigterm",
522
+ afterEnter: "sendSigterm",
614
523
  triggers: [{
615
524
  type: "function",
616
525
  interval: TRIGGER_INTERVAL_MS,
@@ -640,7 +549,7 @@ var serviceStateMachineConfig = {
640
549
  START: "starting",
641
550
  STOP: "stopping"
642
551
  },
643
- entry: "recordCrash",
552
+ afterEnter: "recordCrash",
644
553
  triggers: [{
645
554
  type: "function",
646
555
  interval: TRIGGER_INTERVAL_MS,
@@ -665,6 +574,30 @@ var serviceStateMachineConfig = {
665
574
  // ── Actions ────────────────────────────────────────────────────────────────
666
575
  // (context, event, machine) — context is the payload from send()
667
576
  actions: {
577
+ // ── Machine-level afterTransition hook — consolidates WS broadcast + log broadcaster ──
578
+ onServiceTransition: async (context, _event, machine) => {
579
+ if (!serviceRuntime) return;
580
+ const { fifonyDir } = serviceRuntime;
581
+ const info = readPidInfo(fifonyDir, machine.entityId);
582
+ const currentState = info?.state ?? "stopped";
583
+ const pid = info?.pid ?? null;
584
+ logger.info({ id: machine.entityId, event: _event, state: currentState }, "[ServiceFSM] Transition");
585
+ serviceRuntime.onTransition?.({
586
+ id: machine.entityId,
587
+ from: context.previousState ?? "none",
588
+ to: currentState,
589
+ pid,
590
+ reason: _event
591
+ });
592
+ if (currentState === "starting") {
593
+ const { startServiceLogBroadcasting, stopServiceLogBroadcasting } = await import("./service-log-broadcaster-JIUP2L3D.js");
594
+ stopServiceLogBroadcasting(machine.entityId);
595
+ startServiceLogBroadcasting(machine.entityId, fifonyDir);
596
+ } else if (currentState === "stopped" || currentState === "crashed") {
597
+ const { stopServiceLogBroadcasting } = await import("./service-log-broadcaster-JIUP2L3D.js");
598
+ stopServiceLogBroadcasting(machine.entityId);
599
+ }
600
+ },
668
601
  spawnService: async (context, _event, machine) => {
669
602
  if (!serviceRuntime) {
670
603
  logger.warn({ entityId: machine.entityId }, "[ServiceFSM] spawnService called but runtime not set");
@@ -677,18 +610,11 @@ var serviceStateMachineConfig = {
677
610
  }
678
611
  const { fifonyDir, targetRoot } = serviceRuntime;
679
612
  const globalEnv = serviceRuntime.getGlobalEnv();
613
+ const serviceVaulterEnv = serviceRuntime.getServiceEnv?.(entry.id) ?? {};
614
+ const effectiveEntry = Object.keys(serviceVaulterEnv).length > 0 ? { ...entry, env: { ...serviceVaulterEnv, ...entry.env ?? {} } } : entry;
680
615
  const existing = readPidInfo(fifonyDir, entry.id);
681
- if (existing && isProcessAlive(existing.pid)) {
682
- try {
683
- process.kill(-existing.pid, "SIGKILL");
684
- } catch {
685
- }
686
- try {
687
- process.kill(existing.pid, "SIGKILL");
688
- } catch {
689
- }
690
- }
691
- const spawned = spawnProcess(entry, targetRoot, fifonyDir, globalEnv);
616
+ cleanupServiceProcesses(existing?.pid ?? null, entry.port);
617
+ const spawned = spawnProcess(effectiveEntry, targetRoot, fifonyDir, globalEnv);
692
618
  const isAutoRestart = _event === "AUTO_RESTART";
693
619
  const prevCrashCount = existing?.crashCount ?? 0;
694
620
  writePidInfo(fifonyDir, entry.id, {
@@ -702,20 +628,28 @@ var serviceStateMachineConfig = {
702
628
  { id: entry.id, pid: spawned.pid, event: _event },
703
629
  `[ServiceFSM] spawnService \u2192 starting (${isAutoRestart ? "auto-restart" : "manual"})`
704
630
  );
705
- serviceRuntime.onTransition?.({
706
- id: entry.id,
707
- from: context.previousState ?? "none",
708
- to: "starting",
709
- pid: spawned.pid,
710
- reason: isAutoRestart ? `auto-restart (crash #${prevCrashCount})` : "manual start"
711
- });
712
631
  },
713
632
  sendSigterm: async (context, _event, machine) => {
714
633
  if (!serviceRuntime) return;
715
- const { fifonyDir } = serviceRuntime;
634
+ const { fifonyDir, targetRoot } = serviceRuntime;
635
+ const entry = resolveServiceEntry(machine.entityId);
716
636
  const info = readPidInfo(fifonyDir, machine.entityId);
717
637
  if (!info) return;
718
- if (isProcessAlive(info.pid)) {
638
+ if (entry?.stopCommand) {
639
+ try {
640
+ const cwd = entry.cwd ? join(targetRoot, entry.cwd) : targetRoot;
641
+ execSync(entry.stopCommand, { cwd, stdio: "pipe", timeout: 15e3 });
642
+ logger.info({ id: machine.entityId, stopCommand: entry.stopCommand }, "[ServiceFSM] stopCommand executed");
643
+ } catch (err) {
644
+ logger.warn({ err, id: machine.entityId }, "[ServiceFSM] stopCommand failed, falling back to SIGTERM");
645
+ if (isProcessAlive(info.pid)) {
646
+ try {
647
+ process.kill(-info.pid, "SIGTERM");
648
+ } catch {
649
+ }
650
+ }
651
+ }
652
+ } else if (isProcessAlive(info.pid)) {
719
653
  try {
720
654
  process.kill(-info.pid, "SIGTERM");
721
655
  } catch {
@@ -726,14 +660,7 @@ var serviceStateMachineConfig = {
726
660
  state: "stopping",
727
661
  stoppingAt: now()
728
662
  });
729
- logger.info({ id: machine.entityId, pid: info.pid }, "[ServiceFSM] sendSigterm \u2192 stopping");
730
- serviceRuntime.onTransition?.({
731
- id: machine.entityId,
732
- from: context.previousState ?? "running",
733
- to: "stopping",
734
- pid: info.pid,
735
- reason: "manual stop"
736
- });
663
+ logger.info({ id: machine.entityId, pid: info.pid, hasStopCommand: !!entry?.stopCommand }, "[ServiceFSM] sendSigterm \u2192 stopping");
737
664
  },
738
665
  recordCrash: async (context, _event, machine) => {
739
666
  if (!serviceRuntime) return;
@@ -756,13 +683,6 @@ var serviceStateMachineConfig = {
756
683
  { id: machine.entityId, crashCount, nextRetryAt },
757
684
  "[ServiceFSM] recordCrash \u2192 crashed"
758
685
  );
759
- serviceRuntime.onTransition?.({
760
- id: machine.entityId,
761
- from: context.previousState ?? "running",
762
- to: "crashed",
763
- pid: null,
764
- reason: `process died (crash #${crashCount})`
765
- });
766
686
  },
767
687
  onEnterStopped: async (context, _event, machine) => {
768
688
  if (!serviceRuntime) return;
@@ -781,15 +701,11 @@ var serviceStateMachineConfig = {
781
701
  }
782
702
  logger.info({ id: machine.entityId }, "[ServiceFSM] onEnterStopped \u2014 SIGKILL after stop timeout");
783
703
  }
704
+ const entry = resolveServiceEntry(machine.entityId);
705
+ const info2 = readPidInfo(fifonyDir, machine.entityId);
706
+ cleanupServiceProcesses(info2?.pid ?? null, entry?.port);
784
707
  removePidInfo(fifonyDir, machine.entityId);
785
708
  logger.info({ id: machine.entityId, event: _event }, "[ServiceFSM] onEnterStopped \u2014 pid file removed");
786
- serviceRuntime.onTransition?.({
787
- id: machine.entityId,
788
- from: context.previousState ?? "stopping",
789
- to: "stopped",
790
- pid: null,
791
- reason: _event === "KILL_TIMEOUT" ? "SIGKILL after stop timeout" : "process exited"
792
- });
793
709
  },
794
710
  onEnterRunning: async (context, _event, machine) => {
795
711
  if (!serviceRuntime) return;
@@ -799,13 +715,6 @@ var serviceStateMachineConfig = {
799
715
  writePidInfo(fifonyDir, machine.entityId, { ...info, state: "running" });
800
716
  }
801
717
  logger.info({ id: machine.entityId, pid: info?.pid }, "[ServiceFSM] onEnterRunning \u2014 grace period elapsed");
802
- serviceRuntime.onTransition?.({
803
- id: machine.entityId,
804
- from: "starting",
805
- to: "running",
806
- pid: info?.pid ?? null,
807
- reason: "startup grace period elapsed"
808
- });
809
718
  }
810
719
  },
811
720
  // ── Guards ─────────────────────────────────────────────────────────────────
@@ -835,19 +744,14 @@ var serviceStateMachineConfig = {
835
744
 
836
745
  export {
837
746
  normalizeServiceEnvironment,
838
- setServicesAccessor,
839
- startTrafficProxy,
840
- stopTrafficProxy,
841
- getTrafficProxyPort,
842
- isTrafficProxyRunning,
843
- getTrafficBuffer,
844
- getServiceGraph,
845
- getTrafficProxyStats,
846
747
  SERVICE_WATCHER_INTERVAL_MS,
847
748
  serviceLogPath,
749
+ serviceLogGenerationPath,
848
750
  getServiceStatus,
849
751
  getAllServiceStatuses,
850
752
  readServiceLogTail,
753
+ readServiceLogGenerationTail,
754
+ listServiceLogGenerations,
851
755
  reconcileServiceStates,
852
756
  SERVICE_STATE_MACHINE_ID,
853
757
  setServiceRuntime,
@@ -856,4 +760,4 @@ export {
856
760
  sendServiceEvent,
857
761
  serviceStateMachineConfig
858
762
  };
859
- //# sourceMappingURL=chunk-AAVROEQC.js.map
763
+ //# sourceMappingURL=chunk-UR7T7IA6.js.map