@chriscode/devmux 1.0.0 → 1.2.0

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.
@@ -0,0 +1,840 @@
1
+ import {
2
+ BUILTIN_PATTERN_SETS,
3
+ DedupeCache,
4
+ clearQueue,
5
+ computeContentHash,
6
+ createRingBuffer,
7
+ ensureOutputDir,
8
+ getPendingEvents,
9
+ getQueuePath,
10
+ isStackTraceLine,
11
+ matchPatterns,
12
+ readQueue,
13
+ resolvePatterns,
14
+ startWatcher,
15
+ updateEventStatus,
16
+ writeEvent
17
+ } from "./chunk-JDD6USSA.js";
18
+ import {
19
+ __export
20
+ } from "./chunk-MLKGABMK.js";
21
+
22
+ // src/config/loader.ts
23
+ import { readFileSync, existsSync } from "fs";
24
+ import { resolve, dirname } from "path";
25
+
26
+ // src/utils/worktree.ts
27
+ import { execSync } from "child_process";
28
+ function detectWorktreeName() {
29
+ try {
30
+ const gitDir = execSync("git rev-parse --git-dir", {
31
+ encoding: "utf-8",
32
+ stdio: ["pipe", "pipe", "pipe"]
33
+ }).trim();
34
+ const worktreeMatch = gitDir.match(/\.git\/worktrees\/([^/]+)/);
35
+ if (worktreeMatch) {
36
+ return worktreeMatch[1];
37
+ }
38
+ return null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ function sanitizeInstanceId(name) {
44
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
45
+ }
46
+ function resolveInstanceId() {
47
+ const envInstanceId = process.env.DEVMUX_INSTANCE_ID;
48
+ if (envInstanceId) {
49
+ return sanitizeInstanceId(envInstanceId);
50
+ }
51
+ const worktreeName = detectWorktreeName();
52
+ if (worktreeName) {
53
+ return sanitizeInstanceId(worktreeName);
54
+ }
55
+ return "";
56
+ }
57
+
58
+ // src/utils/port.ts
59
+ function calculatePortOffset(instanceId) {
60
+ if (!instanceId) return 0;
61
+ let hash = 0;
62
+ for (let i = 0; i < instanceId.length; i++) {
63
+ hash = (hash << 5) - hash + instanceId.charCodeAt(i);
64
+ hash = hash & hash;
65
+ }
66
+ return Math.abs(hash) % 999 + 1;
67
+ }
68
+ function resolvePort(basePort, instanceId) {
69
+ return basePort + calculatePortOffset(instanceId);
70
+ }
71
+
72
+ // src/config/loader.ts
73
+ var CONFIG_NAMES = [
74
+ "devmux.config.json",
75
+ ".devmuxrc.json",
76
+ ".devmuxrc"
77
+ ];
78
+ function findConfigFile(startDir) {
79
+ let dir = resolve(startDir);
80
+ const root = dirname(dir);
81
+ while (dir !== root) {
82
+ for (const name of CONFIG_NAMES) {
83
+ const configPath = resolve(dir, name);
84
+ if (existsSync(configPath)) {
85
+ return configPath;
86
+ }
87
+ }
88
+ const pkgPath = resolve(dir, "package.json");
89
+ if (existsSync(pkgPath)) {
90
+ try {
91
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
92
+ if (pkg.devmux) {
93
+ return pkgPath;
94
+ }
95
+ } catch {
96
+ }
97
+ }
98
+ dir = dirname(dir);
99
+ }
100
+ return null;
101
+ }
102
+ function loadConfigFromFile(configPath) {
103
+ const content = readFileSync(configPath, "utf-8");
104
+ if (configPath.endsWith("package.json")) {
105
+ const pkg = JSON.parse(content);
106
+ return pkg.devmux;
107
+ }
108
+ return JSON.parse(content);
109
+ }
110
+ function validateConfig(config) {
111
+ if (!config || typeof config !== "object") return false;
112
+ const c = config;
113
+ if (c.version !== 1) return false;
114
+ if (typeof c.project !== "string") return false;
115
+ if (!c.services || typeof c.services !== "object") return false;
116
+ return true;
117
+ }
118
+ function loadConfig(startDir = process.cwd()) {
119
+ const configPath = findConfigFile(startDir);
120
+ if (!configPath) {
121
+ throw new Error(
122
+ "No devmux config found. Create devmux.config.json or add 'devmux' to package.json"
123
+ );
124
+ }
125
+ const config = loadConfigFromFile(configPath);
126
+ if (!validateConfig(config)) {
127
+ throw new Error(`Invalid devmux config in ${configPath}`);
128
+ }
129
+ const configRoot = dirname(configPath);
130
+ const resolvedSessionPrefix = config.sessionPrefix ?? `omo-${config.project}`;
131
+ const instanceId = resolveInstanceId();
132
+ return {
133
+ ...config,
134
+ configRoot,
135
+ resolvedSessionPrefix,
136
+ instanceId
137
+ };
138
+ }
139
+ function getSessionName(config, serviceName) {
140
+ const service = config.services[serviceName];
141
+ if (service?.sessionName) {
142
+ return service.sessionName;
143
+ }
144
+ if (config.instanceId) {
145
+ return `${config.resolvedSessionPrefix}-${config.instanceId}-${serviceName}`;
146
+ }
147
+ return `${config.resolvedSessionPrefix}-${serviceName}`;
148
+ }
149
+ function getServiceCwd(config, serviceName) {
150
+ const service = config.services[serviceName];
151
+ if (!service) {
152
+ throw new Error(`Unknown service: ${serviceName}`);
153
+ }
154
+ return resolve(config.configRoot, service.cwd);
155
+ }
156
+ function getBasePort(health, explicitPort) {
157
+ if (explicitPort !== void 0) return explicitPort;
158
+ if (!health) return void 0;
159
+ if (health.type === "port") return health.port;
160
+ if (health.type === "http") {
161
+ try {
162
+ const url = new URL(health.url);
163
+ return parseInt(url.port) || (url.protocol === "https:" ? 443 : 80);
164
+ } catch {
165
+ return void 0;
166
+ }
167
+ }
168
+ return void 0;
169
+ }
170
+ function getResolvedPort(config, serviceName) {
171
+ const service = config.services[serviceName];
172
+ if (!service) return void 0;
173
+ const basePort = getBasePort(service.health, service.port);
174
+ if (basePort === void 0) return void 0;
175
+ return resolvePort(basePort, config.instanceId);
176
+ }
177
+
178
+ // src/tmux/driver.ts
179
+ var driver_exports = {};
180
+ __export(driver_exports, {
181
+ attachSession: () => attachSession,
182
+ hasSession: () => hasSession,
183
+ killSession: () => killSession,
184
+ listSessions: () => listSessions,
185
+ newSession: () => newSession,
186
+ setRemainOnExit: () => setRemainOnExit
187
+ });
188
+ import { execSync as execSync2, spawn } from "child_process";
189
+ function hasSession(sessionName) {
190
+ try {
191
+ execSync2(`tmux has-session -t ${sessionName}`, {
192
+ stdio: ["pipe", "pipe", "pipe"]
193
+ });
194
+ return true;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+ function listSessions(prefix) {
200
+ try {
201
+ const output = execSync2("tmux list-sessions -F #{session_name}", {
202
+ encoding: "utf-8",
203
+ stdio: ["pipe", "pipe", "pipe"]
204
+ });
205
+ const sessions = output.trim().split("\n").filter(Boolean);
206
+ if (prefix) {
207
+ return sessions.filter((s) => s.startsWith(prefix));
208
+ }
209
+ return sessions;
210
+ } catch {
211
+ return [];
212
+ }
213
+ }
214
+ function newSession(sessionName, cwd, command, env) {
215
+ const envPrefix = env ? Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ") + " " : "";
216
+ execSync2(
217
+ `tmux new-session -d -s "${sessionName}" -c "${cwd}" "${envPrefix}${command}"`,
218
+ { stdio: ["pipe", "pipe", "pipe"] }
219
+ );
220
+ }
221
+ function setRemainOnExit(sessionName, value) {
222
+ try {
223
+ execSync2(
224
+ `tmux set-option -t "${sessionName}" remain-on-exit ${value ? "on" : "off"}`,
225
+ { stdio: ["pipe", "pipe", "pipe"] }
226
+ );
227
+ } catch {
228
+ }
229
+ }
230
+ function killSession(sessionName) {
231
+ try {
232
+ execSync2(`tmux kill-session -t "${sessionName}"`, {
233
+ stdio: ["pipe", "pipe", "pipe"]
234
+ });
235
+ } catch {
236
+ }
237
+ }
238
+ function attachSession(sessionName) {
239
+ const child = spawn("tmux", ["attach", "-t", sessionName], {
240
+ stdio: "inherit"
241
+ });
242
+ child.on("error", () => {
243
+ });
244
+ }
245
+
246
+ // src/health/checkers.ts
247
+ var checkers_exports = {};
248
+ __export(checkers_exports, {
249
+ checkHealth: () => checkHealth,
250
+ checkHttp: () => checkHttp,
251
+ checkPort: () => checkPort,
252
+ checkTmuxPane: () => checkTmuxPane,
253
+ getHealthPort: () => getHealthPort
254
+ });
255
+ import { createConnection } from "net";
256
+ import { execSync as execSync3 } from "child_process";
257
+ function checkPort(port, host = "127.0.0.1") {
258
+ return new Promise((resolve3) => {
259
+ const socket = createConnection({ port, host });
260
+ socket.setTimeout(1e3);
261
+ socket.on("connect", () => {
262
+ socket.destroy();
263
+ resolve3(true);
264
+ });
265
+ socket.on("timeout", () => {
266
+ socket.destroy();
267
+ resolve3(false);
268
+ });
269
+ socket.on("error", () => {
270
+ socket.destroy();
271
+ resolve3(false);
272
+ });
273
+ });
274
+ }
275
+ async function checkHttp(url, expectStatus = 200) {
276
+ try {
277
+ const response = await fetch(url, {
278
+ method: "GET",
279
+ signal: AbortSignal.timeout(5e3)
280
+ });
281
+ if (expectStatus === 200) {
282
+ return response.ok || response.status === 404;
283
+ }
284
+ return response.status === expectStatus;
285
+ } catch {
286
+ return false;
287
+ }
288
+ }
289
+ function checkTmuxPane(sessionName) {
290
+ try {
291
+ const output = execSync3(
292
+ `tmux list-panes -t "${sessionName}" -F "#{pane_dead}"`,
293
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
294
+ );
295
+ return output.trim() === "0";
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+ async function checkHealth(health, sessionName) {
301
+ if (!health) {
302
+ return checkTmuxPane(sessionName);
303
+ }
304
+ switch (health.type) {
305
+ case "port":
306
+ return checkPort(health.port, health.host);
307
+ case "http":
308
+ return checkHttp(health.url, health.expectStatus);
309
+ }
310
+ }
311
+ function getHealthPort(health) {
312
+ if (!health) return void 0;
313
+ if (health.type === "port") return health.port;
314
+ if (health.type === "http") {
315
+ try {
316
+ const url = new URL(health.url);
317
+ return parseInt(url.port) || (url.protocol === "https:" ? 443 : 80);
318
+ } catch {
319
+ return void 0;
320
+ }
321
+ }
322
+ return void 0;
323
+ }
324
+
325
+ // src/core/service.ts
326
+ import { execSync as execSync4 } from "child_process";
327
+ async function ensureService(config, serviceName, options = {}, _dependencyStack = /* @__PURE__ */ new Set()) {
328
+ if (_dependencyStack.has(serviceName)) {
329
+ throw new Error(`Circular dependency detected: ${Array.from(_dependencyStack).join(" -> ")} -> ${serviceName}`);
330
+ }
331
+ const service = config.services[serviceName];
332
+ if (!service) {
333
+ throw new Error(`Unknown service: ${serviceName}`);
334
+ }
335
+ if (service.dependsOn && service.dependsOn.length > 0) {
336
+ _dependencyStack.add(serviceName);
337
+ try {
338
+ if (!options.quiet) console.log(`Checking dependencies for ${serviceName}...`);
339
+ for (const dep of service.dependsOn) {
340
+ await ensureService(config, dep, options, _dependencyStack);
341
+ }
342
+ } finally {
343
+ _dependencyStack.delete(serviceName);
344
+ }
345
+ }
346
+ const sessionName = getSessionName(config, serviceName);
347
+ const cwd = getServiceCwd(config, serviceName);
348
+ const timeout = options.timeout ?? config.defaults?.startupTimeoutSeconds ?? 30;
349
+ const log = options.quiet ? () => {
350
+ } : console.log;
351
+ const resolvedPort = getResolvedPort(config, serviceName);
352
+ const resolvedHealth = resolveHealthCheck(service.health, resolvedPort);
353
+ const env = buildServiceEnv(config, serviceName, service.env);
354
+ const isHealthy = await checkHealth(resolvedHealth, sessionName);
355
+ if (isHealthy) {
356
+ const hasTmux = hasSession(sessionName);
357
+ log(`\u2705 ${serviceName} already running`);
358
+ if (hasTmux) {
359
+ log(` \u2514\u2500 tmux session: ${sessionName}`);
360
+ } else {
361
+ log(` \u2514\u2500 (running outside tmux)`);
362
+ }
363
+ if (resolvedPort && config.instanceId) {
364
+ log(` \u2514\u2500 port: ${resolvedPort} (instance: ${config.instanceId})`);
365
+ }
366
+ return { serviceName, startedByUs: false, sessionName };
367
+ }
368
+ if (hasSession(sessionName)) {
369
+ log(`\u{1F504} Cleaning up stale session: ${sessionName}`);
370
+ killSession(sessionName);
371
+ }
372
+ log(`\u{1F680} Starting ${serviceName} in tmux session: ${sessionName}`);
373
+ if (resolvedPort && config.instanceId) {
374
+ log(` \u2514\u2500 port: ${resolvedPort} (instance: ${config.instanceId})`);
375
+ }
376
+ newSession(sessionName, cwd, service.command, env);
377
+ const remainOnExit = config.defaults?.remainOnExit ?? true;
378
+ setRemainOnExit(sessionName, remainOnExit);
379
+ log(`\u23F3 Waiting for ${serviceName} to be ready...`);
380
+ for (let i = 0; i < timeout; i++) {
381
+ if (await checkHealth(resolvedHealth, sessionName)) {
382
+ log(`\u2705 ${serviceName} ready`);
383
+ log(` \u2514\u2500 tmux session: ${sessionName}`);
384
+ return { serviceName, startedByUs: true, sessionName };
385
+ }
386
+ await sleep(1e3);
387
+ }
388
+ throw new Error(`${serviceName} failed to start within ${timeout}s`);
389
+ }
390
+ async function getStatus(config, serviceName) {
391
+ const service = config.services[serviceName];
392
+ if (!service) {
393
+ throw new Error(`Unknown service: ${serviceName}`);
394
+ }
395
+ const sessionName = getSessionName(config, serviceName);
396
+ const resolvedPort = getResolvedPort(config, serviceName);
397
+ const resolvedHealth = resolveHealthCheck(service.health, resolvedPort);
398
+ const healthy = await checkHealth(resolvedHealth, sessionName);
399
+ const hasTmux = hasSession(sessionName);
400
+ return {
401
+ name: serviceName,
402
+ healthy,
403
+ tmuxSession: hasTmux ? sessionName : null,
404
+ port: getHealthPort(service.health),
405
+ resolvedPort,
406
+ managedByDevmux: hasTmux,
407
+ instanceId: config.instanceId || void 0
408
+ };
409
+ }
410
+ async function getAllStatus(config) {
411
+ const statuses = [];
412
+ for (const serviceName of Object.keys(config.services)) {
413
+ statuses.push(await getStatus(config, serviceName));
414
+ }
415
+ return statuses;
416
+ }
417
+ function stopService(config, serviceName, options = {}) {
418
+ const service = config.services[serviceName];
419
+ if (!service) {
420
+ throw new Error(`Unknown service: ${serviceName}`);
421
+ }
422
+ const sessionName = getSessionName(config, serviceName);
423
+ const log = options.quiet ? () => {
424
+ } : console.log;
425
+ log(`\u{1F6D1} Stopping ${serviceName}...`);
426
+ if (hasSession(sessionName)) {
427
+ killSession(sessionName);
428
+ log(` \u2514\u2500 Killed tmux session: ${sessionName}`);
429
+ }
430
+ if (options.killPorts) {
431
+ const resolvedPort = getResolvedPort(config, serviceName);
432
+ const ports = [];
433
+ if (resolvedPort) ports.push(resolvedPort);
434
+ if (service.stopPorts) {
435
+ for (const p of service.stopPorts) {
436
+ ports.push(p);
437
+ }
438
+ }
439
+ for (const port of [...new Set(ports)]) {
440
+ try {
441
+ const pids = execSync4(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
442
+ if (pids) {
443
+ execSync4(`kill -9 ${pids.split("\n").join(" ")}`, { stdio: "pipe" });
444
+ log(` \u2514\u2500 Killed process(es) on port ${port}`);
445
+ }
446
+ } catch {
447
+ }
448
+ }
449
+ }
450
+ log(`\u2705 ${serviceName} stopped`);
451
+ }
452
+ function stopAllServices(config, options = {}) {
453
+ for (const serviceName of Object.keys(config.services)) {
454
+ stopService(config, serviceName, options);
455
+ }
456
+ }
457
+ async function restartService(config, serviceName, options = {}) {
458
+ stopService(config, serviceName, { killPorts: options.killPorts, quiet: options.quiet });
459
+ return ensureService(config, serviceName, { timeout: options.timeout, quiet: options.quiet });
460
+ }
461
+ function attachService(config, serviceName) {
462
+ const service = config.services[serviceName];
463
+ if (!service) {
464
+ throw new Error(`Unknown service: ${serviceName}`);
465
+ }
466
+ const sessionName = getSessionName(config, serviceName);
467
+ if (!hasSession(sessionName)) {
468
+ throw new Error(`No tmux session for ${serviceName}. Service may not be running or was started outside tmux.`);
469
+ }
470
+ console.log(`\u{1F4CE} Attaching to ${sessionName}...`);
471
+ console.log(` (detach with Ctrl+B, then D)`);
472
+ attachSession(sessionName);
473
+ }
474
+ function sleep(ms) {
475
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
476
+ }
477
+ function resolveHealthCheck(health, resolvedPort) {
478
+ if (!health) return void 0;
479
+ if (resolvedPort === void 0) return health;
480
+ if (health.type === "port") {
481
+ return { ...health, port: resolvedPort };
482
+ }
483
+ if (health.type === "http") {
484
+ try {
485
+ const url = new URL(health.url);
486
+ url.port = String(resolvedPort);
487
+ return { ...health, url: url.toString() };
488
+ } catch {
489
+ return health;
490
+ }
491
+ }
492
+ return health;
493
+ }
494
+ function buildServiceEnv(config, serviceName, userEnv) {
495
+ const resolvedPort = getResolvedPort(config, serviceName);
496
+ const env = {};
497
+ if (resolvedPort !== void 0) {
498
+ env.PORT = String(resolvedPort);
499
+ env.DEVMUX_PORT = String(resolvedPort);
500
+ }
501
+ if (config.instanceId) {
502
+ env.DEVMUX_INSTANCE_ID = config.instanceId;
503
+ }
504
+ env.DEVMUX_SERVICE = serviceName;
505
+ env.DEVMUX_PROJECT = config.project;
506
+ if (userEnv) {
507
+ for (const [key, value] of Object.entries(userEnv)) {
508
+ env[key] = value.replace(/\{\{PORT\}\}/g, String(resolvedPort ?? "")).replace(/\{\{INSTANCE\}\}/g, config.instanceId).replace(/\{\{SERVICE\}\}/g, serviceName).replace(/\{\{PROJECT\}\}/g, config.project);
509
+ }
510
+ }
511
+ return env;
512
+ }
513
+
514
+ // src/core/run.ts
515
+ import { spawn as spawn2 } from "child_process";
516
+ async function runWithServices(config, command, options) {
517
+ const { services, stopOnExit = true, quiet = false } = options;
518
+ const log = quiet ? () => {
519
+ } : console.log;
520
+ const startedByUs = [];
521
+ for (const serviceName of services) {
522
+ const service = config.services[serviceName];
523
+ if (!service) {
524
+ console.error(`\u274C Unknown service: ${serviceName}`);
525
+ process.exit(1);
526
+ }
527
+ const sessionName = getSessionName(config, serviceName);
528
+ const wasHealthy = await checkHealth(service.health, sessionName);
529
+ if (wasHealthy) {
530
+ log(`\u2705 ${serviceName} already running (will keep on exit)`);
531
+ } else {
532
+ const result = await ensureService(config, serviceName, { quiet });
533
+ if (result.startedByUs) {
534
+ startedByUs.push(result);
535
+ log(` (will stop on Ctrl+C)`);
536
+ }
537
+ }
538
+ }
539
+ log("");
540
+ const cleanup = () => {
541
+ if (stopOnExit && startedByUs.length > 0) {
542
+ log("");
543
+ log("\u{1F9F9} Cleaning up services we started...");
544
+ for (const result of startedByUs) {
545
+ stopService(config, result.serviceName, { killPorts: true, quiet: true });
546
+ log(` \u2514\u2500 Stopped ${result.serviceName}`);
547
+ }
548
+ }
549
+ };
550
+ process.on("SIGINT", () => {
551
+ cleanup();
552
+ process.exit(130);
553
+ });
554
+ process.on("SIGTERM", () => {
555
+ cleanup();
556
+ process.exit(143);
557
+ });
558
+ process.on("exit", cleanup);
559
+ const [cmd, ...args] = command;
560
+ const child = spawn2(cmd, args, {
561
+ stdio: "inherit",
562
+ shell: true
563
+ });
564
+ return new Promise((resolve3) => {
565
+ child.on("close", (code) => {
566
+ resolve3(code ?? 0);
567
+ });
568
+ child.on("error", (err) => {
569
+ console.error(`Failed to run command: ${err.message}`);
570
+ resolve3(1);
571
+ });
572
+ });
573
+ }
574
+
575
+ // src/discovery/turbo.ts
576
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
577
+ import { resolve as resolve2, relative } from "path";
578
+ function loadTurboConfig(root) {
579
+ const turboPath = resolve2(root, "turbo.json");
580
+ if (!existsSync2(turboPath)) return null;
581
+ try {
582
+ return JSON.parse(readFileSync2(turboPath, "utf-8"));
583
+ } catch {
584
+ return null;
585
+ }
586
+ }
587
+ function getPersistentTasks(turbo) {
588
+ const tasks = turbo.tasks ?? turbo.pipeline ?? {};
589
+ return Object.entries(tasks).filter(([_, task]) => task.persistent).map(([name]) => name.replace(/^\/\/#/, ""));
590
+ }
591
+ function findWorkspacePackages(root) {
592
+ const packages = [];
593
+ const rootPkg = resolve2(root, "package.json");
594
+ if (!existsSync2(rootPkg)) return packages;
595
+ try {
596
+ const pkg = JSON.parse(
597
+ readFileSync2(rootPkg, "utf-8")
598
+ );
599
+ const workspaces = pkg.workspaces ?? [];
600
+ for (const pattern of workspaces) {
601
+ const cleanPattern = pattern.replace(/\/\*$/, "");
602
+ const pkgPath = resolve2(root, cleanPattern, "package.json");
603
+ if (existsSync2(pkgPath)) {
604
+ const subPkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
605
+ packages.push({
606
+ name: subPkg.name ?? cleanPattern,
607
+ path: relative(root, resolve2(root, cleanPattern)) || ".",
608
+ scripts: Object.keys(subPkg.scripts ?? {})
609
+ });
610
+ }
611
+ }
612
+ for (const subdir of ["app", "api", "web", "packages", "apps"]) {
613
+ const pkgPath = resolve2(root, subdir, "package.json");
614
+ if (existsSync2(pkgPath) && !packages.some((p) => p.path === subdir)) {
615
+ const subPkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
616
+ packages.push({
617
+ name: subPkg.name ?? subdir,
618
+ path: subdir,
619
+ scripts: Object.keys(subPkg.scripts ?? {})
620
+ });
621
+ }
622
+ }
623
+ } catch {
624
+ }
625
+ return packages;
626
+ }
627
+ function discoverFromTurbo(root) {
628
+ const turbo = loadTurboConfig(root);
629
+ if (!turbo) return null;
630
+ const persistentTasks = getPersistentTasks(turbo);
631
+ if (persistentTasks.length === 0) return null;
632
+ const packages = findWorkspacePackages(root);
633
+ const services = {};
634
+ for (const pkg of packages) {
635
+ for (const task of persistentTasks) {
636
+ if (pkg.scripts.includes(task)) {
637
+ const serviceName = pkg.path === "." ? task : `${pkg.path.replace(/\//g, "-")}-${task}`;
638
+ services[serviceName] = {
639
+ cwd: pkg.path,
640
+ command: `pnpm ${task}`
641
+ };
642
+ }
643
+ }
644
+ }
645
+ if (Object.keys(services).length === 0) return null;
646
+ return {
647
+ version: 1,
648
+ project: "my-project",
649
+ services
650
+ };
651
+ }
652
+ function formatDiscoveredConfig(config) {
653
+ const lines = [
654
+ "# Discovered from turbo.json",
655
+ "# Review and update:",
656
+ "# 1. Set 'project' name",
657
+ "# 2. Add health checks (port or http) for each service",
658
+ "# 3. Remove services you don't want to manage",
659
+ "",
660
+ JSON.stringify(config, null, 2)
661
+ ];
662
+ return lines.join("\n");
663
+ }
664
+
665
+ // src/watch/manager.ts
666
+ import { execSync as execSync5 } from "child_process";
667
+ import { dirname as dirname2, join as join2 } from "path";
668
+ import { fileURLToPath } from "url";
669
+ function getWatcherCliPath() {
670
+ const thisFileDir = dirname2(fileURLToPath(import.meta.url));
671
+ if (thisFileDir.endsWith("watch")) {
672
+ return join2(thisFileDir, "watcher-cli.js");
673
+ }
674
+ return join2(thisFileDir, "watch", "watcher-cli.js");
675
+ }
676
+ function getWatchConfig(config) {
677
+ return config.watch;
678
+ }
679
+ function getServiceWatchConfig(config, serviceName) {
680
+ return config.services[serviceName]?.watch;
681
+ }
682
+ function isWatchEnabled(config, serviceName) {
683
+ const globalWatch = getWatchConfig(config);
684
+ const serviceWatch = getServiceWatchConfig(config, serviceName);
685
+ if (serviceWatch?.enabled !== void 0) {
686
+ return serviceWatch.enabled;
687
+ }
688
+ return globalWatch?.enabled ?? false;
689
+ }
690
+ function isPipeActive(sessionName) {
691
+ try {
692
+ const output = execSync5(`tmux show-options -t "${sessionName}" -p pipe-command 2>/dev/null || true`, {
693
+ encoding: "utf-8"
694
+ });
695
+ return output.includes("watcher-cli");
696
+ } catch {
697
+ return false;
698
+ }
699
+ }
700
+ function getWatcherStatus(config, serviceName) {
701
+ const sessionName = getSessionName(config, serviceName);
702
+ const hasSession2 = hasSession(sessionName);
703
+ return {
704
+ service: serviceName,
705
+ sessionName,
706
+ pipeActive: hasSession2 && isPipeActive(sessionName)
707
+ };
708
+ }
709
+ function getAllWatcherStatuses(config) {
710
+ return Object.keys(config.services).map((serviceName) => getWatcherStatus(config, serviceName));
711
+ }
712
+ function startWatcher2(config, serviceName, options = {}) {
713
+ const sessionName = getSessionName(config, serviceName);
714
+ const log = options.quiet ? () => {
715
+ } : console.log;
716
+ if (!hasSession(sessionName)) {
717
+ log(`\u274C Service ${serviceName} is not running (no tmux session: ${sessionName})`);
718
+ return false;
719
+ }
720
+ if (isPipeActive(sessionName)) {
721
+ log(`\u2705 Watcher already active for ${serviceName}`);
722
+ return true;
723
+ }
724
+ const globalWatch = getWatchConfig(config);
725
+ const serviceWatch = getServiceWatchConfig(config, serviceName);
726
+ const patterns = resolvePatterns(globalWatch, serviceWatch);
727
+ if (patterns.length === 0) {
728
+ log(`\u26A0\uFE0F No patterns configured for ${serviceName}`);
729
+ return false;
730
+ }
731
+ const outputDir = globalWatch?.outputDir ?? `${process.env.HOME}/.opencode/triggers`;
732
+ const dedupeWindowMs = globalWatch?.dedupeWindowMs ?? 5e3;
733
+ const contextLines = globalWatch?.contextLines ?? 20;
734
+ const patternsJson = JSON.stringify(patterns).replace(/"/g, '\\"');
735
+ const watcherCliPath = getWatcherCliPath();
736
+ const cmd = [
737
+ `node "${watcherCliPath}"`,
738
+ `--service=${serviceName}`,
739
+ `--project=${config.project}`,
740
+ `--session=${sessionName}`,
741
+ `--output=${outputDir}`,
742
+ `--dedupe=${dedupeWindowMs}`,
743
+ `--context=${contextLines}`,
744
+ `--patterns="${patternsJson}"`
745
+ ].join(" ");
746
+ try {
747
+ execSync5(`tmux pipe-pane -t "${sessionName}" '${cmd}'`, { stdio: "pipe" });
748
+ log(`\u{1F441}\uFE0F Started watching ${serviceName}`);
749
+ return true;
750
+ } catch (e) {
751
+ log(`\u274C Failed to start watcher for ${serviceName}: ${e}`);
752
+ return false;
753
+ }
754
+ }
755
+ function stopWatcher(config, serviceName, options = {}) {
756
+ const sessionName = getSessionName(config, serviceName);
757
+ const log = options.quiet ? () => {
758
+ } : console.log;
759
+ if (!hasSession(sessionName)) {
760
+ log(`\u26A0\uFE0F Service ${serviceName} is not running`);
761
+ return false;
762
+ }
763
+ if (!isPipeActive(sessionName)) {
764
+ log(`\u26A0\uFE0F Watcher not active for ${serviceName}`);
765
+ return false;
766
+ }
767
+ try {
768
+ execSync5(`tmux pipe-pane -t "${sessionName}"`, { stdio: "pipe" });
769
+ log(`\u{1F6D1} Stopped watching ${serviceName}`);
770
+ return true;
771
+ } catch (e) {
772
+ log(`\u274C Failed to stop watcher for ${serviceName}: ${e}`);
773
+ return false;
774
+ }
775
+ }
776
+ function startAllWatchers(config, options = {}) {
777
+ for (const serviceName of Object.keys(config.services)) {
778
+ if (isWatchEnabled(config, serviceName)) {
779
+ startWatcher2(config, serviceName, options);
780
+ }
781
+ }
782
+ }
783
+ function stopAllWatchers(config, options = {}) {
784
+ for (const serviceName of Object.keys(config.services)) {
785
+ const status = getWatcherStatus(config, serviceName);
786
+ if (status.pipeActive) {
787
+ stopWatcher(config, serviceName, options);
788
+ }
789
+ }
790
+ }
791
+
792
+ // src/watch/index.ts
793
+ var watch_exports = {};
794
+ __export(watch_exports, {
795
+ BUILTIN_PATTERN_SETS: () => BUILTIN_PATTERN_SETS,
796
+ DedupeCache: () => DedupeCache,
797
+ clearQueue: () => clearQueue,
798
+ computeContentHash: () => computeContentHash,
799
+ createRingBuffer: () => createRingBuffer,
800
+ ensureOutputDir: () => ensureOutputDir,
801
+ getAllWatcherStatuses: () => getAllWatcherStatuses,
802
+ getPendingEvents: () => getPendingEvents,
803
+ getQueuePath: () => getQueuePath,
804
+ getWatcherStatus: () => getWatcherStatus,
805
+ isStackTraceLine: () => isStackTraceLine,
806
+ matchPatterns: () => matchPatterns,
807
+ readQueue: () => readQueue,
808
+ resolvePatterns: () => resolvePatterns,
809
+ startAllWatchers: () => startAllWatchers,
810
+ startServiceWatcher: () => startWatcher2,
811
+ startWatcher: () => startWatcher,
812
+ stopAllWatchers: () => stopAllWatchers,
813
+ stopServiceWatcher: () => stopWatcher,
814
+ updateEventStatus: () => updateEventStatus,
815
+ writeEvent: () => writeEvent
816
+ });
817
+
818
+ export {
819
+ loadConfig,
820
+ getSessionName,
821
+ getServiceCwd,
822
+ driver_exports,
823
+ checkers_exports,
824
+ ensureService,
825
+ getStatus,
826
+ getAllStatus,
827
+ stopService,
828
+ stopAllServices,
829
+ restartService,
830
+ attachService,
831
+ runWithServices,
832
+ discoverFromTurbo,
833
+ formatDiscoveredConfig,
834
+ getAllWatcherStatuses,
835
+ startWatcher2 as startWatcher,
836
+ stopWatcher,
837
+ startAllWatchers,
838
+ stopAllWatchers,
839
+ watch_exports
840
+ };