@launchfile/macos-dev 0.1.4 → 0.3.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.
Files changed (124) hide show
  1. package/dist/bootstrap.d.ts +58 -0
  2. package/dist/bootstrap.js +224 -0
  3. package/dist/env-writer.d.ts +19 -13
  4. package/dist/env-writer.js +44 -8
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +1 -0
  7. package/dist/process-manager.d.ts +13 -0
  8. package/dist/process-manager.js +57 -2
  9. package/dist/process-stopper.d.ts +99 -0
  10. package/dist/process-stopper.js +165 -0
  11. package/dist/provider.d.ts +18 -0
  12. package/dist/provider.js +176 -70
  13. package/dist/shell.js +1 -1
  14. package/dist/state.d.ts +28 -0
  15. package/dist/storage.d.ts +16 -3
  16. package/dist/storage.js +23 -6
  17. package/package.json +9 -4
  18. package/CLAUDE.md +0 -37
  19. package/dist/__tests__/dry-run.test.d.ts +0 -2
  20. package/dist/__tests__/dry-run.test.d.ts.map +0 -1
  21. package/dist/__tests__/dry-run.test.js +0 -129
  22. package/dist/__tests__/dry-run.test.js.map +0 -1
  23. package/dist/__tests__/env-writer.test.d.ts +0 -2
  24. package/dist/__tests__/env-writer.test.d.ts.map +0 -1
  25. package/dist/__tests__/env-writer.test.js +0 -127
  26. package/dist/__tests__/env-writer.test.js.map +0 -1
  27. package/dist/__tests__/lockfile-detect.test.d.ts +0 -2
  28. package/dist/__tests__/lockfile-detect.test.d.ts.map +0 -1
  29. package/dist/__tests__/lockfile-detect.test.js +0 -75
  30. package/dist/__tests__/lockfile-detect.test.js.map +0 -1
  31. package/dist/__tests__/port-allocator.test.d.ts +0 -2
  32. package/dist/__tests__/port-allocator.test.d.ts.map +0 -1
  33. package/dist/__tests__/port-allocator.test.js +0 -53
  34. package/dist/__tests__/port-allocator.test.js.map +0 -1
  35. package/dist/__tests__/secret-generator.test.d.ts +0 -2
  36. package/dist/__tests__/secret-generator.test.d.ts.map +0 -1
  37. package/dist/__tests__/secret-generator.test.js +0 -26
  38. package/dist/__tests__/secret-generator.test.js.map +0 -1
  39. package/dist/__tests__/state.test.d.ts +0 -2
  40. package/dist/__tests__/state.test.d.ts.map +0 -1
  41. package/dist/__tests__/state.test.js +0 -30
  42. package/dist/__tests__/state.test.js.map +0 -1
  43. package/dist/cli.d.ts.map +0 -1
  44. package/dist/cli.js.map +0 -1
  45. package/dist/env-writer.d.ts.map +0 -1
  46. package/dist/env-writer.js.map +0 -1
  47. package/dist/health.d.ts.map +0 -1
  48. package/dist/health.js.map +0 -1
  49. package/dist/index.d.ts.map +0 -1
  50. package/dist/index.js.map +0 -1
  51. package/dist/lockfile-detect.d.ts.map +0 -1
  52. package/dist/lockfile-detect.js.map +0 -1
  53. package/dist/port-allocator.d.ts.map +0 -1
  54. package/dist/port-allocator.js.map +0 -1
  55. package/dist/prereqs.d.ts.map +0 -1
  56. package/dist/prereqs.js.map +0 -1
  57. package/dist/process-manager.d.ts.map +0 -1
  58. package/dist/process-manager.js.map +0 -1
  59. package/dist/provider.d.ts.map +0 -1
  60. package/dist/provider.js.map +0 -1
  61. package/dist/resources/index.d.ts.map +0 -1
  62. package/dist/resources/index.js.map +0 -1
  63. package/dist/resources/mysql.d.ts.map +0 -1
  64. package/dist/resources/mysql.js.map +0 -1
  65. package/dist/resources/postgres.d.ts.map +0 -1
  66. package/dist/resources/postgres.js.map +0 -1
  67. package/dist/resources/redis.d.ts.map +0 -1
  68. package/dist/resources/redis.js.map +0 -1
  69. package/dist/resources/sqlite.d.ts.map +0 -1
  70. package/dist/resources/sqlite.js.map +0 -1
  71. package/dist/resources/types.d.ts.map +0 -1
  72. package/dist/resources/types.js.map +0 -1
  73. package/dist/runtimes/bun.d.ts.map +0 -1
  74. package/dist/runtimes/bun.js.map +0 -1
  75. package/dist/runtimes/index.d.ts.map +0 -1
  76. package/dist/runtimes/index.js.map +0 -1
  77. package/dist/runtimes/node.d.ts.map +0 -1
  78. package/dist/runtimes/node.js.map +0 -1
  79. package/dist/runtimes/python.d.ts.map +0 -1
  80. package/dist/runtimes/python.js.map +0 -1
  81. package/dist/runtimes/ruby.d.ts.map +0 -1
  82. package/dist/runtimes/ruby.js.map +0 -1
  83. package/dist/runtimes/types.d.ts.map +0 -1
  84. package/dist/runtimes/types.js.map +0 -1
  85. package/dist/secret-generator.d.ts.map +0 -1
  86. package/dist/secret-generator.js.map +0 -1
  87. package/dist/shell.d.ts.map +0 -1
  88. package/dist/shell.js.map +0 -1
  89. package/dist/state.d.ts.map +0 -1
  90. package/dist/state.js.map +0 -1
  91. package/dist/storage.d.ts.map +0 -1
  92. package/dist/storage.js.map +0 -1
  93. package/src/__tests__/dry-run.test.ts +0 -157
  94. package/src/__tests__/env-writer.test.ts +0 -156
  95. package/src/__tests__/lockfile-detect.test.ts +0 -74
  96. package/src/__tests__/port-allocator.test.ts +0 -59
  97. package/src/__tests__/secret-generator.test.ts +0 -31
  98. package/src/__tests__/state.test.ts +0 -33
  99. package/src/cli.ts +0 -78
  100. package/src/env-writer.ts +0 -231
  101. package/src/health.ts +0 -80
  102. package/src/index.ts +0 -9
  103. package/src/lockfile-detect.ts +0 -48
  104. package/src/port-allocator.ts +0 -97
  105. package/src/prereqs.ts +0 -32
  106. package/src/process-manager.ts +0 -242
  107. package/src/provider.ts +0 -415
  108. package/src/resources/index.ts +0 -29
  109. package/src/resources/mysql.ts +0 -98
  110. package/src/resources/postgres.ts +0 -133
  111. package/src/resources/redis.ts +0 -58
  112. package/src/resources/sqlite.ts +0 -52
  113. package/src/resources/types.ts +0 -47
  114. package/src/runtimes/bun.ts +0 -25
  115. package/src/runtimes/index.ts +0 -24
  116. package/src/runtimes/node.ts +0 -71
  117. package/src/runtimes/python.ts +0 -42
  118. package/src/runtimes/ruby.ts +0 -55
  119. package/src/runtimes/types.ts +0 -16
  120. package/src/secret-generator.ts +0 -32
  121. package/src/shell.ts +0 -70
  122. package/src/state.ts +0 -92
  123. package/src/storage.ts +0 -67
  124. package/tsconfig.json +0 -18
package/src/health.ts DELETED
@@ -1,80 +0,0 @@
1
- /**
2
- * Health check polling for components.
3
- */
4
-
5
- import type { NormalizedHealth } from "@launchfile/sdk";
6
- import { shell } from "./shell.js";
7
-
8
- /** Parse a duration string like "30s", "1m", "500ms" to milliseconds */
9
- export function parseDuration(duration: string): number {
10
- const match = /^(\d+)(ms|s|m)$/.exec(duration);
11
- if (!match) return 0;
12
- const value = Number.parseInt(match[1]!, 10);
13
- switch (match[2]) {
14
- case "ms":
15
- return value;
16
- case "s":
17
- return value * 1000;
18
- case "m":
19
- return value * 60_000;
20
- default:
21
- return 0;
22
- }
23
- }
24
-
25
- /**
26
- * Wait for a component to become healthy.
27
- * Returns true if healthy, false if timed out.
28
- */
29
- export async function waitForHealthy(
30
- name: string,
31
- health: NormalizedHealth,
32
- port: number,
33
- overallTimeout: number = 60_000,
34
- ): Promise<boolean> {
35
- const startPeriod = parseDuration(health.start_period ?? "0s");
36
- const interval = parseDuration(health.interval ?? "3s");
37
- const checkTimeout = parseDuration(health.timeout ?? "5s");
38
-
39
- // Wait for start period
40
- if (startPeriod > 0) {
41
- console.log(` [${name}] Waiting ${health.start_period} start period...`);
42
- await sleep(startPeriod);
43
- }
44
-
45
- const deadline = Date.now() + overallTimeout;
46
-
47
- while (Date.now() < deadline) {
48
- try {
49
- if (health.path) {
50
- const resp = await fetch(`http://localhost:${port}${health.path}`, {
51
- signal: AbortSignal.timeout(checkTimeout),
52
- });
53
- if (resp.ok) {
54
- console.log(` [${name}] Healthy`);
55
- return true;
56
- }
57
- } else if (health.command) {
58
- await shell(health.command, { timeout: checkTimeout, silent: true });
59
- console.log(` [${name}] Healthy`);
60
- return true;
61
- } else {
62
- // No specific check — try connecting to the port
63
- const resp = await fetch(`http://localhost:${port}/`, {
64
- signal: AbortSignal.timeout(checkTimeout),
65
- }).catch(() => null);
66
- if (resp) return true;
67
- }
68
- } catch {
69
- // Expected — service not ready yet
70
- }
71
- await sleep(interval);
72
- }
73
-
74
- console.error(` [${name}] Health check timed out after ${overallTimeout}ms`);
75
- return false;
76
- }
77
-
78
- function sleep(ms: number): Promise<void> {
79
- return new Promise((resolve) => setTimeout(resolve, ms));
80
- }
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * macOS Dev Provider for Launchfile.
3
- *
4
- * Run apps locally on macOS with brew services for databases
5
- * and native runtimes for the app itself.
6
- */
7
-
8
- export { launchUp, launchDown, launchStatus, launchEnv } from "./provider.js";
9
- export type { LaunchUpOpts } from "./provider.js";
@@ -1,48 +0,0 @@
1
- /**
2
- * Detect the package manager from lockfile presence.
3
- */
4
-
5
- import { access } from "node:fs/promises";
6
- import { join } from "node:path";
7
-
8
- export interface PackageManager {
9
- name: string;
10
- installCommand: string;
11
- lockfile: string;
12
- }
13
-
14
- const LOCKFILE_ORDER: PackageManager[] = [
15
- { name: "bun", installCommand: "bun install", lockfile: "bun.lockb" },
16
- { name: "bun", installCommand: "bun install", lockfile: "bun.lock" },
17
- { name: "pnpm", installCommand: "pnpm install", lockfile: "pnpm-lock.yaml" },
18
- { name: "yarn", installCommand: "yarn install", lockfile: "yarn.lock" },
19
- { name: "npm", installCommand: "npm install", lockfile: "package-lock.json" },
20
- { name: "bundler", installCommand: "bundle install", lockfile: "Gemfile.lock" },
21
- { name: "go", installCommand: "go mod download", lockfile: "go.sum" },
22
- { name: "cargo", installCommand: "cargo build", lockfile: "Cargo.lock" },
23
- { name: "pip", installCommand: "pip install -r requirements.txt", lockfile: "requirements.txt" },
24
- { name: "poetry", installCommand: "poetry install", lockfile: "poetry.lock" },
25
- { name: "uv", installCommand: "uv sync", lockfile: "uv.lock" },
26
- ];
27
-
28
- async function fileExists(path: string): Promise<boolean> {
29
- try {
30
- await access(path);
31
- return true;
32
- } catch {
33
- return false;
34
- }
35
- }
36
-
37
- /**
38
- * Detect the package manager from lockfile presence in a directory.
39
- * Returns the first match in priority order, or null.
40
- */
41
- export async function detectPackageManager(projectDir: string): Promise<PackageManager | null> {
42
- for (const pm of LOCKFILE_ORDER) {
43
- if (await fileExists(join(projectDir, pm.lockfile))) {
44
- return pm;
45
- }
46
- }
47
- return null;
48
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Deterministic port allocation for Launchfile apps.
3
- *
4
- * Ports are derived from a hash of the app name so they're stable across runs
5
- * and machines. If a port is in use, we scan upward for the next free one.
6
- */
7
-
8
- import { createServer } from "node:net";
9
-
10
- /** Base range for allocated ports */
11
- const PORT_RANGE_START = 10_000;
12
- const PORT_RANGE_SIZE = 10_000;
13
-
14
- /** Simple string hash → number in a range */
15
- function hashToRange(input: string, rangeSize: number): number {
16
- let hash = 0;
17
- for (let i = 0; i < input.length; i++) {
18
- hash = (hash * 31 + input.charCodeAt(i)) | 0;
19
- }
20
- return Math.abs(hash) % rangeSize;
21
- }
22
-
23
- /** Check if a port is available by attempting to listen on it */
24
- async function isPortFree(port: number): Promise<boolean> {
25
- return new Promise((resolve) => {
26
- const server = createServer();
27
- server.once("error", () => resolve(false));
28
- server.once("listening", () => {
29
- server.close(() => resolve(true));
30
- });
31
- server.listen(port, "127.0.0.1");
32
- });
33
- }
34
-
35
- /**
36
- * Allocate a port for a given key (e.g., "myapp" or "myapp:backend").
37
- * Uses deterministic hashing for stability, with fallback scan.
38
- */
39
- export async function allocatePort(
40
- key: string,
41
- existingPorts: Set<number>,
42
- ): Promise<number> {
43
- const preferred = PORT_RANGE_START + hashToRange(key, PORT_RANGE_SIZE);
44
-
45
- // Try the preferred port first
46
- if (!existingPorts.has(preferred) && (await isPortFree(preferred))) {
47
- return preferred;
48
- }
49
-
50
- // Scan upward for the next free port
51
- for (let offset = 1; offset < PORT_RANGE_SIZE; offset++) {
52
- const candidate = PORT_RANGE_START + ((preferred - PORT_RANGE_START + offset) % PORT_RANGE_SIZE);
53
- if (!existingPorts.has(candidate) && (await isPortFree(candidate))) {
54
- return candidate;
55
- }
56
- }
57
-
58
- throw new Error(`No free port found in range ${PORT_RANGE_START}–${PORT_RANGE_START + PORT_RANGE_SIZE}`);
59
- }
60
-
61
- /**
62
- * Allocate ports for all components in a launch config.
63
- * Returns a map of component name → port.
64
- */
65
- export async function allocatePorts(
66
- components: Record<string, { provides?: Array<{ port: number }> }>,
67
- appName: string,
68
- savedPorts?: Record<string, number>,
69
- ): Promise<Record<string, number>> {
70
- const allocated = new Set<number>();
71
- const result: Record<string, number> = {};
72
-
73
- for (const [name, component] of Object.entries(components)) {
74
- // Reuse saved port if still free
75
- const saved = savedPorts?.[name];
76
- if (saved && (await isPortFree(saved))) {
77
- result[name] = saved;
78
- allocated.add(saved);
79
- continue;
80
- }
81
-
82
- // Use the component's declared port if free
83
- const declaredPort = component.provides?.[0]?.port;
84
- if (declaredPort && !allocated.has(declaredPort) && (await isPortFree(declaredPort))) {
85
- result[name] = declaredPort;
86
- allocated.add(declaredPort);
87
- continue;
88
- }
89
-
90
- // Fall back to deterministic allocation
91
- const port = await allocatePort(`${appName}:${name}`, allocated);
92
- result[name] = port;
93
- allocated.add(port);
94
- }
95
-
96
- return result;
97
- }
package/src/prereqs.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * Prerequisite checks for the macOS dev provider.
3
- */
4
-
5
- import { shellOk } from "./shell.js";
6
-
7
- export interface PrereqResult {
8
- ok: boolean;
9
- missing: string[];
10
- warnings: string[];
11
- }
12
-
13
- export async function checkPrereqs(): Promise<PrereqResult> {
14
- const missing: string[] = [];
15
- const warnings: string[] = [];
16
-
17
- // Homebrew is required
18
- if (!(await shellOk("which brew"))) {
19
- missing.push("Homebrew — install from https://brew.sh");
20
- }
21
-
22
- // Git is required (for cloning)
23
- if (!(await shellOk("which git"))) {
24
- missing.push("git — install via: brew install git");
25
- }
26
-
27
- return {
28
- ok: missing.length === 0,
29
- missing,
30
- warnings,
31
- };
32
- }
@@ -1,242 +0,0 @@
1
- /**
2
- * Lightweight process manager for multi-component Launchfile apps.
3
- *
4
- * Handles topological startup ordering, log multiplexing,
5
- * health check waits, and graceful shutdown.
6
- */
7
-
8
- import { spawn, type ChildProcess } from "node:child_process";
9
- import { createWriteStream, mkdirSync } from "node:fs";
10
- import { join } from "node:path";
11
- import type { NormalizedHealth, NormalizedDependsOnEntry } from "@launchfile/sdk";
12
- import { waitForHealthy } from "./health.js";
13
-
14
- // ANSI colors for log prefixing
15
- const COLORS = [
16
- "\x1b[36m", // cyan
17
- "\x1b[33m", // yellow
18
- "\x1b[32m", // green
19
- "\x1b[35m", // magenta
20
- "\x1b[34m", // blue
21
- "\x1b[31m", // red
22
- ];
23
- const RESET = "\x1b[0m";
24
-
25
- interface ManagedProcess {
26
- name: string;
27
- command: string;
28
- env: Record<string, string>;
29
- cwd: string;
30
- dependsOn: NormalizedDependsOnEntry[];
31
- health?: NormalizedHealth;
32
- port?: number;
33
- process?: ChildProcess;
34
- status: "pending" | "starting" | "running" | "healthy" | "failed" | "stopped";
35
- }
36
-
37
- export class ProcessManager {
38
- private processes = new Map<string, ManagedProcess>();
39
- private logDir: string;
40
-
41
- constructor(projectDir: string) {
42
- this.logDir = join(projectDir, ".launchfile", "logs");
43
- // Security: restrict permissions — logs may contain sensitive output
44
- mkdirSync(this.logDir, { recursive: true, mode: 0o700 });
45
- }
46
-
47
- register(
48
- name: string,
49
- config: {
50
- command: string;
51
- env: Record<string, string>;
52
- cwd: string;
53
- dependsOn?: NormalizedDependsOnEntry[];
54
- health?: NormalizedHealth;
55
- port?: number;
56
- },
57
- ): void {
58
- this.processes.set(name, {
59
- name,
60
- command: config.command,
61
- env: config.env,
62
- cwd: config.cwd,
63
- dependsOn: config.dependsOn ?? [],
64
- health: config.health,
65
- port: config.port,
66
- status: "pending",
67
- });
68
- }
69
-
70
- /**
71
- * Start all registered processes respecting dependency order.
72
- */
73
- async startAll(): Promise<void> {
74
- const batches = this.topologicalSort();
75
-
76
- for (const batch of batches) {
77
- // Start all processes in this batch concurrently
78
- await Promise.all(batch.map((name) => this.startOne(name)));
79
- }
80
-
81
- console.log("\n All components started.");
82
- }
83
-
84
- private async startOne(name: string): Promise<void> {
85
- const proc = this.processes.get(name);
86
- if (!proc) throw new Error(`Unknown component: ${name}`);
87
-
88
- // Wait for dependencies
89
- for (const dep of proc.dependsOn) {
90
- const depProc = this.processes.get(dep.component);
91
- if (!depProc) continue;
92
-
93
- if (dep.condition === "healthy") {
94
- console.log(` [${name}] Waiting for ${dep.component} to be healthy...`);
95
- if (depProc.health && depProc.port) {
96
- await waitForHealthy(dep.component, depProc.health, depProc.port);
97
- }
98
- }
99
- // For "started" condition, the process is already spawned by the time we get here
100
- }
101
-
102
- proc.status = "starting";
103
- console.log(` [${name}] Starting: ${proc.command}`);
104
-
105
- const logFile = createWriteStream(join(this.logDir, `${name}.log`), { flags: "a" });
106
- const colorIdx = [...this.processes.keys()].indexOf(name) % COLORS.length;
107
- const color = COLORS[colorIdx]!;
108
- const maxNameLen = Math.max(...[...this.processes.keys()].map((n) => n.length));
109
- const paddedName = name.padEnd(maxNameLen);
110
-
111
- proc.process = spawn("sh", ["-c", proc.command], {
112
- env: { ...process.env, ...proc.env },
113
- cwd: proc.cwd,
114
- stdio: ["ignore", "pipe", "pipe"],
115
- });
116
-
117
- // Pipe stdout with prefix
118
- proc.process.stdout?.on("data", (data: Buffer) => {
119
- const lines = data.toString().split("\n");
120
- for (const line of lines) {
121
- if (line) {
122
- process.stdout.write(`${color}[${paddedName}]${RESET} ${line}\n`);
123
- logFile.write(`${new Date().toISOString()} ${line}\n`);
124
- }
125
- }
126
- });
127
-
128
- // Pipe stderr with prefix
129
- proc.process.stderr?.on("data", (data: Buffer) => {
130
- const lines = data.toString().split("\n");
131
- for (const line of lines) {
132
- if (line) {
133
- process.stderr.write(`${color}[${paddedName}]${RESET} \x1b[2m${line}${RESET}\n`);
134
- logFile.write(`${new Date().toISOString()} ERR ${line}\n`);
135
- }
136
- }
137
- });
138
-
139
- proc.process.on("exit", (code) => {
140
- proc.status = code === 0 ? "stopped" : "failed";
141
- console.log(
142
- `${color}[${paddedName}]${RESET} Process exited with code ${code}`,
143
- );
144
- logFile.end();
145
- });
146
-
147
- proc.status = "running";
148
- }
149
-
150
- /**
151
- * Graceful shutdown in reverse dependency order.
152
- */
153
- async stopAll(): Promise<void> {
154
- const batches = this.topologicalSort().reverse();
155
-
156
- for (const batch of batches) {
157
- await Promise.all(
158
- batch.map((name) => {
159
- const proc = this.processes.get(name);
160
- if (!proc?.process || proc.status === "stopped") return Promise.resolve();
161
- return this.stopOne(proc);
162
- }),
163
- );
164
- }
165
- }
166
-
167
- private stopOne(proc: ManagedProcess): Promise<void> {
168
- return new Promise((resolve) => {
169
- if (!proc.process) {
170
- resolve();
171
- return;
172
- }
173
-
174
- const timeout = setTimeout(() => {
175
- proc.process?.kill("SIGKILL");
176
- }, 10_000);
177
-
178
- proc.process.once("exit", () => {
179
- clearTimeout(timeout);
180
- proc.status = "stopped";
181
- resolve();
182
- });
183
-
184
- proc.process.kill("SIGTERM");
185
- });
186
- }
187
-
188
- /**
189
- * Topological sort based on depends_on.
190
- * Returns batches of component names that can start concurrently.
191
- */
192
- private topologicalSort(): string[][] {
193
- const names = [...this.processes.keys()];
194
- const inDegree = new Map<string, number>();
195
- const dependents = new Map<string, string[]>();
196
-
197
- for (const name of names) {
198
- inDegree.set(name, 0);
199
- dependents.set(name, []);
200
- }
201
-
202
- for (const [name, proc] of this.processes) {
203
- for (const dep of proc.dependsOn) {
204
- if (this.processes.has(dep.component)) {
205
- inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
206
- dependents.get(dep.component)!.push(name);
207
- }
208
- }
209
- }
210
-
211
- const batches: string[][] = [];
212
- let remaining = new Set(names);
213
-
214
- while (remaining.size > 0) {
215
- const batch = [...remaining].filter((n) => (inDegree.get(n) ?? 0) === 0);
216
- if (batch.length === 0) {
217
- // Circular dependency — just add remaining
218
- batches.push([...remaining]);
219
- break;
220
- }
221
- batches.push(batch);
222
- for (const name of batch) {
223
- remaining.delete(name);
224
- for (const dependent of dependents.get(name) ?? []) {
225
- inDegree.set(dependent, (inDegree.get(dependent) ?? 0) - 1);
226
- }
227
- }
228
- }
229
-
230
- return batches;
231
- }
232
-
233
- /** Get status summary for all processes */
234
- getStatus(): Array<{ name: string; status: string; pid?: number; port?: number }> {
235
- return [...this.processes.entries()].map(([name, proc]) => ({
236
- name,
237
- status: proc.status,
238
- pid: proc.process?.pid,
239
- port: proc.port,
240
- }));
241
- }
242
- }