buncargo 1.0.11 → 1.0.13

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.
package/core/process.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  type SpawnOptions,
5
5
  spawn,
6
6
  } from "node:child_process";
7
+ import { platform } from "node:os";
7
8
  import { resolve } from "node:path";
8
9
  import type { AppConfig, DevServerPids, ExecOptions } from "../types";
9
10
 
@@ -93,19 +94,41 @@ export interface SpawnDevServerOptions {
93
94
  verbose?: boolean;
94
95
  detached?: boolean;
95
96
  isCI?: boolean;
97
+ /** Kill any existing process using the port before starting. Default: true */
98
+ killExisting?: boolean;
99
+ /** The port this server will use (required if killExisting is true) */
100
+ port?: number;
96
101
  }
97
102
 
98
103
  /**
99
104
  * Spawn a dev server as a detached process.
105
+ * If killExisting is true and port is provided, kills any existing process on that port first.
100
106
  */
101
- export function spawnDevServer(
107
+ export async function spawnDevServer(
102
108
  command: string,
103
109
  root: string,
104
110
  appCwd: string | undefined,
105
111
  envVars: Record<string, string>,
106
112
  options: SpawnDevServerOptions = {},
107
- ): ChildProcess {
108
- const { verbose = false, detached = true, isCI = false } = options;
113
+ ): Promise<ChildProcess> {
114
+ const {
115
+ verbose = false,
116
+ detached = true,
117
+ isCI = false,
118
+ killExisting = true,
119
+ port,
120
+ } = options;
121
+
122
+ // Kill existing process on the port if requested
123
+ if (killExisting && port !== undefined) {
124
+ const existingPid = getProcessOnPort(port);
125
+ if (existingPid !== null) {
126
+ if (verbose) {
127
+ console.log(` ⚠️ Port ${port} is in use by process ${existingPid}`);
128
+ }
129
+ await killProcessOnPortAndWait(port, { verbose });
130
+ }
131
+ }
109
132
 
110
133
  // Parse command into parts
111
134
  const parts = command.split(" ");
@@ -134,20 +157,31 @@ export function spawnDevServer(
134
157
  return proc;
135
158
  }
136
159
 
160
+ export interface StartDevServersOptions {
161
+ verbose?: boolean;
162
+ productionBuild?: boolean;
163
+ isCI?: boolean;
164
+ /** Kill any existing process using the port before starting. Default: true */
165
+ killExisting?: boolean;
166
+ }
167
+
137
168
  /**
138
169
  * Start all configured dev servers.
170
+ * If killExisting is true (default), any process already using a port will be killed first.
139
171
  */
140
- export function startDevServers(
172
+ export async function startDevServers(
141
173
  apps: Record<string, AppConfig>,
142
174
  root: string,
143
175
  envVars: Record<string, string>,
144
- options: {
145
- verbose?: boolean;
146
- productionBuild?: boolean;
147
- isCI?: boolean;
148
- } = {},
149
- ): DevServerPids {
150
- const { verbose = true, productionBuild = false, isCI = false } = options;
176
+ ports: Record<string, number>,
177
+ options: StartDevServersOptions = {},
178
+ ): Promise<DevServerPids> {
179
+ const {
180
+ verbose = true,
181
+ productionBuild = false,
182
+ isCI = false,
183
+ killExisting = true,
184
+ } = options;
151
185
  const pids: DevServerPids = {};
152
186
 
153
187
  if (verbose) {
@@ -163,9 +197,13 @@ export function startDevServers(
163
197
  ? (config.prodCommand ?? config.devCommand)
164
198
  : config.devCommand;
165
199
 
166
- const proc = spawnDevServer(command, root, config.cwd, envVars, {
200
+ const port = ports[name];
201
+
202
+ const proc = await spawnDevServer(command, root, config.cwd, envVars, {
167
203
  verbose,
168
204
  isCI,
205
+ killExisting,
206
+ port,
169
207
  });
170
208
 
171
209
  if (proc.pid) {
@@ -179,6 +217,158 @@ export function startDevServers(
179
217
  return pids;
180
218
  }
181
219
 
220
+ // ═══════════════════════════════════════════════════════════════════════════
221
+ // Port Process Management
222
+ // ═══════════════════════════════════════════════════════════════════════════
223
+
224
+ /**
225
+ * Get the PID of the process using a specific port.
226
+ * Returns null if no process is using the port.
227
+ */
228
+ export function getProcessOnPort(port: number): number | null {
229
+ try {
230
+ const os = platform();
231
+ let output: string;
232
+
233
+ if (os === "win32") {
234
+ // Windows: use netstat
235
+ output = execSync(`netstat -ano | findstr :${port}`, {
236
+ encoding: "utf-8",
237
+ stdio: ["pipe", "pipe", "pipe"],
238
+ });
239
+ // Parse Windows netstat output: TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 12345
240
+ const lines = output.trim().split("\n");
241
+ for (const line of lines) {
242
+ // Only match LISTENING state
243
+ if (line.includes("LISTENING")) {
244
+ const parts = line.trim().split(/\s+/);
245
+ const pid = Number.parseInt(parts[parts.length - 1], 10);
246
+ if (!Number.isNaN(pid) && pid > 0) {
247
+ return pid;
248
+ }
249
+ }
250
+ }
251
+ } else {
252
+ // macOS/Linux: use lsof
253
+ output = execSync(`lsof -ti :${port}`, {
254
+ encoding: "utf-8",
255
+ stdio: ["pipe", "pipe", "pipe"],
256
+ });
257
+ const pid = Number.parseInt(output.trim().split("\n")[0], 10);
258
+ if (!Number.isNaN(pid) && pid > 0) {
259
+ return pid;
260
+ }
261
+ }
262
+
263
+ return null;
264
+ } catch {
265
+ // No process found on port (command exits with error)
266
+ return null;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Check if a port is currently in use.
272
+ */
273
+ export function isPortInUse(port: number): boolean {
274
+ return getProcessOnPort(port) !== null;
275
+ }
276
+
277
+ /**
278
+ * Kill the process using a specific port.
279
+ * Returns true if a process was killed, false if no process was using the port.
280
+ */
281
+ export function killProcessOnPort(
282
+ port: number,
283
+ options: { verbose?: boolean; signal?: NodeJS.Signals } = {},
284
+ ): boolean {
285
+ const { verbose = false, signal = "SIGTERM" } = options;
286
+
287
+ const pid = getProcessOnPort(port);
288
+ if (pid === null) {
289
+ return false;
290
+ }
291
+
292
+ try {
293
+ if (verbose) {
294
+ console.log(` Killing process ${pid} on port ${port}`);
295
+ }
296
+ process.kill(pid, signal);
297
+ return true;
298
+ } catch {
299
+ // Process may have already exited
300
+ return false;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Kill the process on port and wait for it to fully release the port.
306
+ * Uses SIGTERM first, then SIGKILL if the process doesn't exit.
307
+ */
308
+ export async function killProcessOnPortAndWait(
309
+ port: number,
310
+ options: { verbose?: boolean; timeout?: number } = {},
311
+ ): Promise<boolean> {
312
+ const { verbose = false, timeout = 5000 } = options;
313
+
314
+ const pid = getProcessOnPort(port);
315
+ if (pid === null) {
316
+ return false;
317
+ }
318
+
319
+ if (verbose) {
320
+ console.log(` Killing process ${pid} on port ${port}...`);
321
+ }
322
+
323
+ // First try SIGTERM
324
+ try {
325
+ process.kill(pid, "SIGTERM");
326
+ } catch {
327
+ // Process may have already exited
328
+ return false;
329
+ }
330
+
331
+ // Wait for port to be released
332
+ const startTime = Date.now();
333
+ const checkInterval = 100;
334
+
335
+ while (Date.now() - startTime < timeout) {
336
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
337
+
338
+ if (!isPortInUse(port)) {
339
+ if (verbose) {
340
+ console.log(` ✓ Port ${port} released`);
341
+ }
342
+ return true;
343
+ }
344
+ }
345
+
346
+ // If still running, try SIGKILL
347
+ if (verbose) {
348
+ console.log(` Process ${pid} didn't exit, sending SIGKILL...`);
349
+ }
350
+
351
+ try {
352
+ process.kill(pid, "SIGKILL");
353
+ } catch {
354
+ // Process may have already exited
355
+ }
356
+
357
+ // Wait a bit more for SIGKILL
358
+ await new Promise((resolve) => setTimeout(resolve, 500));
359
+
360
+ const released = !isPortInUse(port);
361
+ if (verbose) {
362
+ if (released) {
363
+ console.log(` ✓ Port ${port} released after SIGKILL`);
364
+ } else {
365
+ console.log(` ⚠ Port ${port} still in use`);
366
+ }
367
+ }
368
+
369
+ return released;
370
+ }
371
+
182
372
  // ═══════════════════════════════════════════════════════════════════════════
183
373
  // Process Management
184
374
  // ═══════════════════════════════════════════════════════════════════════════
package/dist/bin.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  runCli
4
- } from "./index-twc5zwja.js";
4
+ } from "./index-6srpc523.js";
5
5
  import {
6
6
  loadDevEnv
7
- } from "./index-habjkmf5.js";
8
- import"./index-hb4y6f08.js";
9
- import"./index-8gd8fkwx.js";
10
- import"./index-6qd90dkp.js";
7
+ } from "./index-75y4cg2z.js";
8
+ import"./index-ndnmnsej.js";
9
+ import"./index-vhs88xhe.js";
10
+ import"./index-cty0bcry.js";
11
11
  import"./index-tjqw9vtj.js";
12
12
  import"./index-5hka0tff.js";
13
13
  import"./index-2fr3g85b.js";
@@ -21,7 +21,7 @@ import {
21
21
  var require_package = __commonJS((exports, module) => {
22
22
  module.exports = {
23
23
  name: "buncargo",
24
- version: "1.0.11",
24
+ version: "1.0.13",
25
25
  description: "A Bun-powered development environment CLI for managing Docker Compose services, dev servers, and environment variables",
26
26
  type: "module",
27
27
  module: "./dist/index.js",
package/dist/cli.js CHANGED
@@ -2,9 +2,9 @@ import {
2
2
  getFlagValue,
3
3
  hasFlag,
4
4
  runCli
5
- } from "./index-twc5zwja.js";
6
- import"./index-8gd8fkwx.js";
7
- import"./index-6qd90dkp.js";
5
+ } from "./index-6srpc523.js";
6
+ import"./index-vhs88xhe.js";
7
+ import"./index-cty0bcry.js";
8
8
  import"./index-qnx9j3qa.js";
9
9
  export {
10
10
  runCli,
@@ -9,17 +9,21 @@ import {
9
9
  startHeartbeat,
10
10
  stopHeartbeat,
11
11
  stopWatchdog
12
- } from "../index-8gd8fkwx.js";
12
+ } from "../index-vhs88xhe.js";
13
13
  import {
14
14
  buildApps,
15
15
  exec,
16
16
  execAsync,
17
+ getProcessOnPort,
18
+ isPortInUse,
17
19
  isProcessAlive,
20
+ killProcessOnPort,
21
+ killProcessOnPortAndWait,
18
22
  spawnDevServer,
19
23
  startDevServers,
20
24
  stopAllProcesses,
21
25
  stopProcess
22
- } from "../index-6qd90dkp.js";
26
+ } from "../index-cty0bcry.js";
23
27
  import {
24
28
  MAX_ATTEMPTS,
25
29
  POLL_INTERVAL,
@@ -78,9 +82,12 @@ export {
78
82
  logFrontendPort,
79
83
  logExpoApiUrl,
80
84
  logApiUrl,
85
+ killProcessOnPortAndWait,
86
+ killProcessOnPort,
81
87
  isWorktree,
82
88
  isWatchdogRunning,
83
89
  isProcessAlive,
90
+ isPortInUse,
84
91
  isPortAvailable,
85
92
  isContainerRunning,
86
93
  isCI,
@@ -88,6 +95,7 @@ export {
88
95
  getWatchdogPidFile,
89
96
  getWatchdogPid,
90
97
  getProjectName,
98
+ getProcessOnPort,
91
99
  getLocalIp,
92
100
  getHeartbeatFile,
93
101
  getEnvVar,
@@ -17,19 +17,53 @@ export interface SpawnDevServerOptions {
17
17
  verbose?: boolean;
18
18
  detached?: boolean;
19
19
  isCI?: boolean;
20
+ /** Kill any existing process using the port before starting. Default: true */
21
+ killExisting?: boolean;
22
+ /** The port this server will use (required if killExisting is true) */
23
+ port?: number;
20
24
  }
21
25
  /**
22
26
  * Spawn a dev server as a detached process.
27
+ * If killExisting is true and port is provided, kills any existing process on that port first.
23
28
  */
24
- export declare function spawnDevServer(command: string, root: string, appCwd: string | undefined, envVars: Record<string, string>, options?: SpawnDevServerOptions): ChildProcess;
29
+ export declare function spawnDevServer(command: string, root: string, appCwd: string | undefined, envVars: Record<string, string>, options?: SpawnDevServerOptions): Promise<ChildProcess>;
30
+ export interface StartDevServersOptions {
31
+ verbose?: boolean;
32
+ productionBuild?: boolean;
33
+ isCI?: boolean;
34
+ /** Kill any existing process using the port before starting. Default: true */
35
+ killExisting?: boolean;
36
+ }
25
37
  /**
26
38
  * Start all configured dev servers.
39
+ * If killExisting is true (default), any process already using a port will be killed first.
40
+ */
41
+ export declare function startDevServers(apps: Record<string, AppConfig>, root: string, envVars: Record<string, string>, ports: Record<string, number>, options?: StartDevServersOptions): Promise<DevServerPids>;
42
+ /**
43
+ * Get the PID of the process using a specific port.
44
+ * Returns null if no process is using the port.
45
+ */
46
+ export declare function getProcessOnPort(port: number): number | null;
47
+ /**
48
+ * Check if a port is currently in use.
27
49
  */
28
- export declare function startDevServers(apps: Record<string, AppConfig>, root: string, envVars: Record<string, string>, options?: {
50
+ export declare function isPortInUse(port: number): boolean;
51
+ /**
52
+ * Kill the process using a specific port.
53
+ * Returns true if a process was killed, false if no process was using the port.
54
+ */
55
+ export declare function killProcessOnPort(port: number, options?: {
29
56
  verbose?: boolean;
30
- productionBuild?: boolean;
31
- isCI?: boolean;
32
- }): DevServerPids;
57
+ signal?: NodeJS.Signals;
58
+ }): boolean;
59
+ /**
60
+ * Kill the process on port and wait for it to fully release the port.
61
+ * Uses SIGTERM first, then SIGKILL if the process doesn't exit.
62
+ */
63
+ export declare function killProcessOnPortAndWait(port: number, options?: {
64
+ verbose?: boolean;
65
+ timeout?: number;
66
+ }): Promise<boolean>;
33
67
  /**
34
68
  * Stop a process by PID.
35
69
  */
@@ -2,19 +2,27 @@ import {
2
2
  buildApps,
3
3
  exec,
4
4
  execAsync,
5
+ getProcessOnPort,
6
+ isPortInUse,
5
7
  isProcessAlive,
8
+ killProcessOnPort,
9
+ killProcessOnPortAndWait,
6
10
  spawnDevServer,
7
11
  startDevServers,
8
12
  stopAllProcesses,
9
13
  stopProcess
10
- } from "../index-6qd90dkp.js";
14
+ } from "../index-cty0bcry.js";
11
15
  import"../index-qnx9j3qa.js";
12
16
  export {
13
17
  stopProcess,
14
18
  stopAllProcesses,
15
19
  startDevServers,
16
20
  spawnDevServer,
21
+ killProcessOnPortAndWait,
22
+ killProcessOnPort,
17
23
  isProcessAlive,
24
+ isPortInUse,
25
+ getProcessOnPort,
18
26
  execAsync,
19
27
  exec,
20
28
  buildApps
@@ -9,8 +9,8 @@ import {
9
9
  startHeartbeat,
10
10
  stopHeartbeat,
11
11
  stopWatchdog
12
- } from "../index-8gd8fkwx.js";
13
- import"../index-6qd90dkp.js";
12
+ } from "../index-vhs88xhe.js";
13
+ import"../index-cty0bcry.js";
14
14
  import"../index-qnx9j3qa.js";
15
15
  export {
16
16
  stopWatchdog,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createDevEnvironment
3
- } from "./index-hb4y6f08.js";
4
- import"./index-8gd8fkwx.js";
5
- import"./index-6qd90dkp.js";
3
+ } from "./index-ndnmnsej.js";
4
+ import"./index-vhs88xhe.js";
5
+ import"./index-cty0bcry.js";
6
6
  import"./index-tjqw9vtj.js";
7
7
  import"./index-5hka0tff.js";
8
8
  import"./index-2fr3g85b.js";