@chriscode/devmux 1.2.0 → 1.3.1

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.
@@ -1,27 +1,8 @@
1
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";
2
+ __esm,
3
+ __export,
4
+ init_esm_shims
5
+ } from "./chunk-66UOCF5R.js";
25
6
 
26
7
  // src/utils/worktree.ts
27
8
  import { execSync } from "child_process";
@@ -54,6 +35,12 @@ function resolveInstanceId() {
54
35
  }
55
36
  return "";
56
37
  }
38
+ var init_worktree = __esm({
39
+ "src/utils/worktree.ts"() {
40
+ "use strict";
41
+ init_esm_shims();
42
+ }
43
+ });
57
44
 
58
45
  // src/utils/port.ts
59
46
  function calculatePortOffset(instanceId) {
@@ -68,13 +55,24 @@ function calculatePortOffset(instanceId) {
68
55
  function resolvePort(basePort, instanceId) {
69
56
  return basePort + calculatePortOffset(instanceId);
70
57
  }
58
+ var init_port = __esm({
59
+ "src/utils/port.ts"() {
60
+ "use strict";
61
+ init_esm_shims();
62
+ }
63
+ });
71
64
 
72
65
  // src/config/loader.ts
73
- var CONFIG_NAMES = [
74
- "devmux.config.json",
75
- ".devmuxrc.json",
76
- ".devmuxrc"
77
- ];
66
+ var loader_exports = {};
67
+ __export(loader_exports, {
68
+ getBasePort: () => getBasePort,
69
+ getResolvedPort: () => getResolvedPort,
70
+ getServiceCwd: () => getServiceCwd,
71
+ getSessionName: () => getSessionName,
72
+ loadConfig: () => loadConfig
73
+ });
74
+ import { readFileSync, existsSync } from "fs";
75
+ import { resolve, dirname } from "path";
78
76
  function findConfigFile(startDir) {
79
77
  let dir = resolve(startDir);
80
78
  const root = dirname(dir);
@@ -174,6 +172,20 @@ function getResolvedPort(config, serviceName) {
174
172
  if (basePort === void 0) return void 0;
175
173
  return resolvePort(basePort, config.instanceId);
176
174
  }
175
+ var CONFIG_NAMES;
176
+ var init_loader = __esm({
177
+ "src/config/loader.ts"() {
178
+ "use strict";
179
+ init_esm_shims();
180
+ init_worktree();
181
+ init_port();
182
+ CONFIG_NAMES = [
183
+ "devmux.config.json",
184
+ ".devmuxrc.json",
185
+ ".devmuxrc"
186
+ ];
187
+ }
188
+ });
177
189
 
178
190
  // src/tmux/driver.ts
179
191
  var driver_exports = {};
@@ -185,6 +197,7 @@ __export(driver_exports, {
185
197
  newSession: () => newSession,
186
198
  setRemainOnExit: () => setRemainOnExit
187
199
  });
200
+ init_esm_shims();
188
201
  import { execSync as execSync2, spawn } from "child_process";
189
202
  function hasSession(sessionName) {
190
203
  try {
@@ -252,23 +265,24 @@ __export(checkers_exports, {
252
265
  checkTmuxPane: () => checkTmuxPane,
253
266
  getHealthPort: () => getHealthPort
254
267
  });
268
+ init_esm_shims();
255
269
  import { createConnection } from "net";
256
270
  import { execSync as execSync3 } from "child_process";
257
271
  function checkPort(port, host = "127.0.0.1") {
258
- return new Promise((resolve3) => {
272
+ return new Promise((resolve2) => {
259
273
  const socket = createConnection({ port, host });
260
274
  socket.setTimeout(1e3);
261
275
  socket.on("connect", () => {
262
276
  socket.destroy();
263
- resolve3(true);
277
+ resolve2(true);
264
278
  });
265
279
  socket.on("timeout", () => {
266
280
  socket.destroy();
267
- resolve3(false);
281
+ resolve2(false);
268
282
  });
269
283
  socket.on("error", () => {
270
284
  socket.destroy();
271
- resolve3(false);
285
+ resolve2(false);
272
286
  });
273
287
  });
274
288
  }
@@ -323,7 +337,95 @@ function getHealthPort(health) {
323
337
  }
324
338
 
325
339
  // src/core/service.ts
340
+ init_esm_shims();
341
+ init_loader();
342
+
343
+ // src/utils/process.ts
344
+ init_esm_shims();
345
+ import fkill from "fkill";
346
+ import psList from "ps-list";
326
347
  import { execSync as execSync4 } from "child_process";
348
+ async function getProcessOnPort(port) {
349
+ try {
350
+ if (process.platform === "win32") {
351
+ const output2 = execSync4(`netstat -ano | findstr :${port}`, { encoding: "utf-8" });
352
+ const lines = output2.trim().split("\n");
353
+ for (const line of lines) {
354
+ const parts = line.trim().split(/\s+/);
355
+ if (parts.length >= 5) {
356
+ const pid2 = parseInt(parts[4], 10);
357
+ if (!isNaN(pid2)) {
358
+ const proc = await getProcessInfo(pid2);
359
+ if (proc) return proc;
360
+ }
361
+ }
362
+ }
363
+ return null;
364
+ }
365
+ const output = execSync4(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
366
+ if (!output) return null;
367
+ const pid = parseInt(output.split("\n")[0], 10);
368
+ if (isNaN(pid)) return null;
369
+ return await getProcessInfo(pid);
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+ async function getProcessInfo(pid) {
375
+ try {
376
+ const processes = await psList();
377
+ const proc = processes.find((p) => p.pid === pid);
378
+ if (!proc) return null;
379
+ return {
380
+ pid: proc.pid,
381
+ name: proc.name,
382
+ cmd: proc.cmd
383
+ };
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+ async function killProcess(pid) {
389
+ try {
390
+ await fkill(pid, { force: true });
391
+ } catch {
392
+ }
393
+ }
394
+ async function getProcessesOnPort(port) {
395
+ const processes = [];
396
+ try {
397
+ if (process.platform === "win32") {
398
+ const output = execSync4(`netstat -ano | findstr :${port}`, { encoding: "utf-8" });
399
+ const lines = output.trim().split("\n");
400
+ const seenPids = /* @__PURE__ */ new Set();
401
+ for (const line of lines) {
402
+ const parts = line.trim().split(/\s+/);
403
+ if (parts.length >= 5) {
404
+ const pid = parseInt(parts[4], 10);
405
+ if (!isNaN(pid) && !seenPids.has(pid)) {
406
+ seenPids.add(pid);
407
+ const proc = await getProcessInfo(pid);
408
+ if (proc) processes.push(proc);
409
+ }
410
+ }
411
+ }
412
+ } else {
413
+ const output = execSync4(`lsof -ti :${port}`, { encoding: "utf-8" }).trim();
414
+ if (output) {
415
+ const pids = output.split("\n").map((p) => parseInt(p, 10)).filter((p) => !isNaN(p));
416
+ const uniquePids = [...new Set(pids)];
417
+ for (const pid of uniquePids) {
418
+ const proc = await getProcessInfo(pid);
419
+ if (proc) processes.push(proc);
420
+ }
421
+ }
422
+ }
423
+ } catch {
424
+ }
425
+ return processes;
426
+ }
427
+
428
+ // src/core/service.ts
327
429
  async function ensureService(config, serviceName, options = {}, _dependencyStack = /* @__PURE__ */ new Set()) {
328
430
  if (_dependencyStack.has(serviceName)) {
329
431
  throw new Error(`Circular dependency detected: ${Array.from(_dependencyStack).join(" -> ")} -> ${serviceName}`);
@@ -414,7 +516,7 @@ async function getAllStatus(config) {
414
516
  }
415
517
  return statuses;
416
518
  }
417
- function stopService(config, serviceName, options = {}) {
519
+ async function stopService(config, serviceName, options = {}) {
418
520
  const service = config.services[serviceName];
419
521
  if (!service) {
420
522
  throw new Error(`Unknown service: ${serviceName}`);
@@ -437,21 +539,18 @@ function stopService(config, serviceName, options = {}) {
437
539
  }
438
540
  }
439
541
  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 {
542
+ const processes = await getProcessesOnPort(port);
543
+ for (const proc of processes) {
544
+ await killProcess(proc.pid);
545
+ log(` \u2514\u2500 Killed process ${proc.name} (PID ${proc.pid}) on port ${port}`);
447
546
  }
448
547
  }
449
548
  }
450
549
  log(`\u2705 ${serviceName} stopped`);
451
550
  }
452
- function stopAllServices(config, options = {}) {
551
+ async function stopAllServices(config, options = {}) {
453
552
  for (const serviceName of Object.keys(config.services)) {
454
- stopService(config, serviceName, options);
553
+ await stopService(config, serviceName, options);
455
554
  }
456
555
  }
457
556
  async function restartService(config, serviceName, options = {}) {
@@ -472,7 +571,7 @@ function attachService(config, serviceName) {
472
571
  attachSession(sessionName);
473
572
  }
474
573
  function sleep(ms) {
475
- return new Promise((resolve3) => setTimeout(resolve3, ms));
574
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
476
575
  }
477
576
  function resolveHealthCheck(health, resolvedPort) {
478
577
  if (!health) return void 0;
@@ -511,330 +610,22 @@ function buildServiceEnv(config, serviceName, userEnv) {
511
610
  return env;
512
611
  }
513
612
 
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
613
  export {
819
614
  loadConfig,
820
615
  getSessionName,
821
616
  getServiceCwd,
617
+ loader_exports,
618
+ init_loader,
619
+ hasSession,
822
620
  driver_exports,
621
+ checkHealth,
823
622
  checkers_exports,
623
+ getProcessOnPort,
824
624
  ensureService,
825
625
  getStatus,
826
626
  getAllStatus,
827
627
  stopService,
828
628
  stopAllServices,
829
629
  restartService,
830
- attachService,
831
- runWithServices,
832
- discoverFromTurbo,
833
- formatDiscoveredConfig,
834
- getAllWatcherStatuses,
835
- startWatcher2 as startWatcher,
836
- stopWatcher,
837
- startAllWatchers,
838
- stopAllWatchers,
839
- watch_exports
630
+ attachService
840
631
  };