bgproc 0.0.0 → 0.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.
- package/README.md +54 -4
- package/dist/cli.mjs +175 -78
- package/dist/cli.mjs.map +1 -1
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Manage background processes like dev servers from the command line. Designed to
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install -g bgproc
|
|
11
|
+
# or using npx
|
|
12
|
+
npx bgproc start -n myserver -- npm run dev
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
## Usage
|
|
@@ -16,6 +18,13 @@ npm install -g bgproc
|
|
|
16
18
|
# Start a process
|
|
17
19
|
bgproc start -n myserver -- npm run dev
|
|
18
20
|
|
|
21
|
+
# Start and wait for port to be detected (great for dev servers)
|
|
22
|
+
bgproc start -n myserver -w -- npm run dev
|
|
23
|
+
# Streams logs to stderr, prints JSON with port to stdout when ready
|
|
24
|
+
|
|
25
|
+
# Force restart (kills existing process with same name)
|
|
26
|
+
bgproc start -n myserver -f -w -- npm run dev
|
|
27
|
+
|
|
19
28
|
# Check status (returns JSON with port detection)
|
|
20
29
|
bgproc status myserver
|
|
21
30
|
# {"name":"myserver","pid":12345,"running":true,"port":3000,...}
|
|
@@ -43,8 +52,10 @@ bgproc clean --all
|
|
|
43
52
|
## Features
|
|
44
53
|
|
|
45
54
|
- **JSON output**: All commands output JSON to stdout, errors to stderr
|
|
46
|
-
- **Port detection**: Automatically detects listening ports via `lsof`
|
|
47
|
-
- **
|
|
55
|
+
- **Port detection**: Automatically detects listening ports via `lsof` (checks child processes too)
|
|
56
|
+
- **Wait for port**: `--wait-for-port` blocks until port is detected, streaming logs
|
|
57
|
+
- **Force restart**: `--force` kills existing process with same name before starting
|
|
58
|
+
- **Duplicate prevention**: Prevents starting multiple processes with the same name
|
|
48
59
|
- **Log management**: Stdout/stderr captured, capped at 1MB
|
|
49
60
|
- **Timeout support**: `--timeout 60` kills after N seconds
|
|
50
61
|
- **Auto-cleanup**: Starting a process with the same name as a dead one auto-cleans it
|
|
@@ -55,8 +66,11 @@ bgproc clean --all
|
|
|
55
66
|
### `start`
|
|
56
67
|
|
|
57
68
|
```
|
|
58
|
-
-n, --name
|
|
59
|
-
-
|
|
69
|
+
-n, --name Process name (required)
|
|
70
|
+
-f, --force Kill existing process with same name before starting
|
|
71
|
+
-t, --timeout Kill after N seconds
|
|
72
|
+
-w, --wait-for-port Wait for port detection (optional: timeout in seconds)
|
|
73
|
+
--keep Keep process running on wait timeout (default: kill)
|
|
60
74
|
```
|
|
61
75
|
|
|
62
76
|
### `status`, `stop`, `logs`, `clean`
|
|
@@ -99,6 +113,42 @@ bgproc status -n myserver # equivalent
|
|
|
99
113
|
|
|
100
114
|
- `BGPROC_DATA_DIR`: Override data directory (default: `~/.local/share/bgproc`)
|
|
101
115
|
|
|
116
|
+
## Usage with AI Agents
|
|
117
|
+
|
|
118
|
+
### Just ask the agent
|
|
119
|
+
|
|
120
|
+
The simplest approach - just tell your agent to use it:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Use bgproc to start and manage the dev server. Run bgproc --help to see available commands.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### AI Coding Assistants
|
|
127
|
+
|
|
128
|
+
Add the skill to your AI coding assistant for richer context:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx skills add ascorbic/bgproc
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This works with Claude Code, Cursor, Codex, and other AI coding tools.
|
|
135
|
+
|
|
136
|
+
### AGENTS.md / CLAUDE.md
|
|
137
|
+
|
|
138
|
+
For more consistent results, add to your project instructions:
|
|
139
|
+
|
|
140
|
+
```markdown
|
|
141
|
+
## Background Processes
|
|
142
|
+
|
|
143
|
+
Use `bgproc` to manage dev servers and background processes. All commands output JSON.
|
|
144
|
+
|
|
145
|
+
Workflow:
|
|
146
|
+
1. `bgproc start -n devserver -- npm run dev` - Start a process
|
|
147
|
+
2. `bgproc status devserver` - Check if running, get port
|
|
148
|
+
3. `bgproc logs devserver` - View output if something's wrong
|
|
149
|
+
4. `bgproc stop devserver` - Stop when done
|
|
150
|
+
```
|
|
151
|
+
|
|
102
152
|
## Platform Support
|
|
103
153
|
|
|
104
154
|
macOS and Linux only. Windows is not supported.
|
package/dist/cli.mjs
CHANGED
|
@@ -6,6 +6,14 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import path, { join } from "node:path";
|
|
7
7
|
|
|
8
8
|
//#region src/registry.ts
|
|
9
|
+
/**
|
|
10
|
+
* Validate process name to prevent path traversal and code injection
|
|
11
|
+
*/
|
|
12
|
+
function validateName(name) {
|
|
13
|
+
if (!name) throw new Error("Process name required");
|
|
14
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new Error("Process name must contain only alphanumeric characters, hyphens, and underscores");
|
|
15
|
+
if (name.length > 64) throw new Error("Process name must be 64 characters or less");
|
|
16
|
+
}
|
|
9
17
|
function getDataDir() {
|
|
10
18
|
return process.env.BGPROC_DATA_DIR || join(homedir(), ".local", "share", "bgproc");
|
|
11
19
|
}
|
|
@@ -38,7 +46,7 @@ function addProcess(name, entry) {
|
|
|
38
46
|
const registry = readRegistry();
|
|
39
47
|
const existing = registry[name];
|
|
40
48
|
if (existing) {
|
|
41
|
-
if (isProcessRunning(existing.pid)) throw new Error(`Process '${name}' is already running (PID ${existing.pid}). Use
|
|
49
|
+
if (isProcessRunning(existing.pid)) throw new Error(`Process '${name}' is already running (PID ${existing.pid}). Use --force to restart.`);
|
|
42
50
|
const logPaths = getLogPaths(name);
|
|
43
51
|
try {
|
|
44
52
|
if (existsSync(logPaths.stdout)) unlinkSync(logPaths.stdout);
|
|
@@ -71,12 +79,48 @@ function getLogPaths(name) {
|
|
|
71
79
|
};
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/ports.ts
|
|
84
|
+
/**
|
|
85
|
+
* Get all descendant PIDs of a process (children, grandchildren, etc.)
|
|
86
|
+
*/
|
|
87
|
+
function getDescendantPids(pid) {
|
|
88
|
+
const descendants = [];
|
|
89
|
+
try {
|
|
90
|
+
const output = execSync(`pgrep -P ${pid} 2>/dev/null`, { encoding: "utf-8" });
|
|
91
|
+
for (const line of output.trim().split("\n")) {
|
|
92
|
+
const childPid = parseInt(line, 10);
|
|
93
|
+
if (!isNaN(childPid)) {
|
|
94
|
+
descendants.push(childPid);
|
|
95
|
+
descendants.push(...getDescendantPids(childPid));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
return descendants;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Detect listening ports for a given PID and all its descendants using lsof
|
|
103
|
+
*/
|
|
104
|
+
function detectPorts(pid) {
|
|
105
|
+
try {
|
|
106
|
+
const output = execSync(`lsof -p ${[pid, ...getDescendantPids(pid)].join(",")} -P -n 2>/dev/null | grep LISTEN`, { encoding: "utf-8" });
|
|
107
|
+
const ports = [];
|
|
108
|
+
for (const line of output.split("\n")) {
|
|
109
|
+
const match = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
110
|
+
if (match) ports.push(parseInt(match[1], 10));
|
|
111
|
+
}
|
|
112
|
+
return [...new Set(ports)];
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
74
118
|
//#endregion
|
|
75
119
|
//#region src/commands/start.ts
|
|
76
120
|
const startCommand = defineCommand({
|
|
77
121
|
meta: {
|
|
78
122
|
name: "start",
|
|
79
|
-
description: "Start a background process\n\nUsage: bgproc start -n <name> [-t <seconds>] -- <command...>"
|
|
123
|
+
description: "Start a background process\n\nUsage: bgproc start -n <name> [-f] [-t <seconds>] [-w [<seconds>]] [--keep] -- <command...>"
|
|
80
124
|
},
|
|
81
125
|
args: {
|
|
82
126
|
name: {
|
|
@@ -89,10 +133,43 @@ const startCommand = defineCommand({
|
|
|
89
133
|
type: "string",
|
|
90
134
|
alias: "t",
|
|
91
135
|
description: "Kill after N seconds"
|
|
136
|
+
},
|
|
137
|
+
waitForPort: {
|
|
138
|
+
type: "string",
|
|
139
|
+
alias: "w",
|
|
140
|
+
description: "Wait for port to be detected (optional: timeout in seconds)"
|
|
141
|
+
},
|
|
142
|
+
keep: {
|
|
143
|
+
type: "boolean",
|
|
144
|
+
description: "Keep process running on timeout (only with --wait-for-port)"
|
|
145
|
+
},
|
|
146
|
+
force: {
|
|
147
|
+
type: "boolean",
|
|
148
|
+
alias: "f",
|
|
149
|
+
description: "Kill existing process with same name before starting"
|
|
92
150
|
}
|
|
93
151
|
},
|
|
94
|
-
run({ args, rawArgs }) {
|
|
152
|
+
async run({ args, rawArgs }) {
|
|
95
153
|
const name = args.name;
|
|
154
|
+
try {
|
|
155
|
+
validateName(name);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(`Error: ${err.message}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
if (args.keep && args.waitForPort === void 0) {
|
|
161
|
+
console.error("Error: --keep requires --wait-for-port");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (args.force) {
|
|
165
|
+
const existing = getProcess(name);
|
|
166
|
+
if (existing && isProcessRunning(existing.pid)) {
|
|
167
|
+
try {
|
|
168
|
+
process.kill(existing.pid, "SIGTERM");
|
|
169
|
+
} catch {}
|
|
170
|
+
removeProcess(name);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
96
173
|
const timeout = args.timeout ? parseInt(args.timeout, 10) : void 0;
|
|
97
174
|
const dashDashIdx = rawArgs.indexOf("--");
|
|
98
175
|
const command = dashDashIdx >= 0 ? rawArgs.slice(dashDashIdx + 1) : [];
|
|
@@ -135,16 +212,73 @@ const startCommand = defineCommand({
|
|
|
135
212
|
process.exit(1);
|
|
136
213
|
}
|
|
137
214
|
if (timeout && child.pid) scheduleKill(child.pid, timeout, name);
|
|
138
|
-
|
|
215
|
+
const baseStatus = {
|
|
139
216
|
name,
|
|
140
217
|
pid: child.pid,
|
|
141
218
|
cwd,
|
|
142
219
|
command: command.join(" "),
|
|
143
220
|
...timeout && { killAt: entry.killAt }
|
|
144
|
-
}
|
|
221
|
+
};
|
|
222
|
+
if (args.waitForPort !== void 0) {
|
|
223
|
+
const waitTimeout = args.waitForPort ? parseInt(args.waitForPort, 10) : void 0;
|
|
224
|
+
const killOnTimeout = !args.keep;
|
|
225
|
+
const result = await waitForPortDetection(child.pid, logPaths.stdout, waitTimeout, killOnTimeout);
|
|
226
|
+
if (result.error) {
|
|
227
|
+
console.error(result.error);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
console.log(JSON.stringify({
|
|
231
|
+
...baseStatus,
|
|
232
|
+
ports: result.ports,
|
|
233
|
+
port: result.ports[0]
|
|
234
|
+
}));
|
|
235
|
+
} else console.log(JSON.stringify(baseStatus));
|
|
145
236
|
}
|
|
146
237
|
});
|
|
147
238
|
/**
|
|
239
|
+
* Wait for a port to be detected on the process.
|
|
240
|
+
* Tails logs to stderr while waiting.
|
|
241
|
+
*/
|
|
242
|
+
function waitForPortDetection(pid, logPath, timeoutSeconds, killOnTimeout) {
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
const startTime = Date.now();
|
|
245
|
+
const tail = spawn("tail", ["-f", logPath], { stdio: [
|
|
246
|
+
"ignore",
|
|
247
|
+
"pipe",
|
|
248
|
+
"ignore"
|
|
249
|
+
] });
|
|
250
|
+
tail.stdout?.pipe(process.stderr);
|
|
251
|
+
const cleanup = (tailProcess, intervalId) => {
|
|
252
|
+
clearInterval(intervalId);
|
|
253
|
+
tailProcess.kill();
|
|
254
|
+
};
|
|
255
|
+
const pollInterval = setInterval(() => {
|
|
256
|
+
if (!isProcessRunning(pid)) {
|
|
257
|
+
cleanup(tail, pollInterval);
|
|
258
|
+
resolve({ error: `Process ${pid} died before a port was detected` });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (timeoutSeconds !== void 0) {
|
|
262
|
+
if ((Date.now() - startTime) / 1e3 >= timeoutSeconds) {
|
|
263
|
+
cleanup(tail, pollInterval);
|
|
264
|
+
if (killOnTimeout) {
|
|
265
|
+
try {
|
|
266
|
+
process.kill(pid, "SIGTERM");
|
|
267
|
+
} catch {}
|
|
268
|
+
resolve({ error: `Timeout: no port detected after ${timeoutSeconds}s (process killed)` });
|
|
269
|
+
} else resolve({ error: `Timeout: no port detected after ${timeoutSeconds}s (process still running)` });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const ports = detectPorts(pid);
|
|
274
|
+
if (ports.length > 0) {
|
|
275
|
+
cleanup(tail, pollInterval);
|
|
276
|
+
resolve({ ports });
|
|
277
|
+
}
|
|
278
|
+
}, 500);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
148
282
|
* Fork a small process to kill after timeout
|
|
149
283
|
* This survives the parent CLI exiting
|
|
150
284
|
*/
|
|
@@ -154,58 +288,27 @@ function scheduleKill(pid, seconds, name) {
|
|
|
154
288
|
try {
|
|
155
289
|
process.kill(${pid}, 0); // check if alive
|
|
156
290
|
process.kill(${pid}, 'SIGTERM');
|
|
157
|
-
console.error('bgproc:
|
|
291
|
+
console.error('bgproc: ' + process.env.BGPROC_NAME + ' killed after ${seconds}s timeout');
|
|
158
292
|
} catch {}
|
|
159
293
|
process.exit(0);
|
|
160
294
|
}, ${seconds * 1e3});
|
|
161
295
|
`], {
|
|
162
296
|
detached: true,
|
|
163
|
-
stdio: "ignore"
|
|
297
|
+
stdio: "ignore",
|
|
298
|
+
env: {
|
|
299
|
+
...process.env,
|
|
300
|
+
BGPROC_NAME: name
|
|
301
|
+
}
|
|
164
302
|
}).unref();
|
|
165
303
|
}
|
|
166
304
|
|
|
167
305
|
//#endregion
|
|
168
|
-
//#region src/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const output = execSync(`lsof -p ${pid} -P -n 2>/dev/null | grep LISTEN`, { encoding: "utf-8" });
|
|
175
|
-
const ports = [];
|
|
176
|
-
for (const line of output.split("\n")) {
|
|
177
|
-
const match = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
178
|
-
if (match) ports.push(parseInt(match[1], 10));
|
|
179
|
-
}
|
|
180
|
-
return [...new Set(ports)];
|
|
181
|
-
} catch {
|
|
182
|
-
return [];
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Try to detect port from log output (for when lsof doesn't show it yet)
|
|
187
|
-
*/
|
|
188
|
-
function detectPortFromLogs(logPath) {
|
|
189
|
-
if (!existsSync(logPath)) return null;
|
|
190
|
-
try {
|
|
191
|
-
const lines = readFileSync(logPath, "utf-8").split("\n").slice(-50);
|
|
192
|
-
const patterns = [
|
|
193
|
-
/localhost:(\d+)/i,
|
|
194
|
-
/127\.0\.0\.1:(\d+)/,
|
|
195
|
-
/0\.0\.0\.0:(\d+)/,
|
|
196
|
-
/port\s+(\d+)/i,
|
|
197
|
-
/listening\s+(?:on\s+)?(?:port\s+)?:?(\d+)/i,
|
|
198
|
-
/:\/\/[^:]+:(\d+)/
|
|
199
|
-
];
|
|
200
|
-
for (const line of lines.reverse()) for (const pattern of patterns) {
|
|
201
|
-
const match = line.match(pattern);
|
|
202
|
-
if (match) {
|
|
203
|
-
const port = parseInt(match[1], 10);
|
|
204
|
-
if (port > 0 && port < 65536) return port;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
} catch {}
|
|
208
|
-
return null;
|
|
306
|
+
//#region src/utils.ts
|
|
307
|
+
function formatUptime(startedAt) {
|
|
308
|
+
const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1e3);
|
|
309
|
+
if (seconds < 60) return `${seconds}s`;
|
|
310
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
311
|
+
return `${Math.floor(seconds / 3600)}h${Math.floor(seconds % 3600 / 60)}m`;
|
|
209
312
|
}
|
|
210
313
|
|
|
211
314
|
//#endregion
|
|
@@ -222,8 +325,10 @@ const statusCommand = defineCommand({
|
|
|
222
325
|
} },
|
|
223
326
|
run({ args, rawArgs }) {
|
|
224
327
|
const name = args.name ?? rawArgs[0];
|
|
225
|
-
|
|
226
|
-
|
|
328
|
+
try {
|
|
329
|
+
validateName(name);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.error(`Error: ${err.message}`);
|
|
227
332
|
process.exit(1);
|
|
228
333
|
}
|
|
229
334
|
const entry = getProcess(name);
|
|
@@ -232,15 +337,7 @@ const statusCommand = defineCommand({
|
|
|
232
337
|
process.exit(1);
|
|
233
338
|
}
|
|
234
339
|
const running = isProcessRunning(entry.pid);
|
|
235
|
-
const
|
|
236
|
-
let ports = [];
|
|
237
|
-
if (running) {
|
|
238
|
-
ports = detectPorts(entry.pid);
|
|
239
|
-
if (ports.length === 0) {
|
|
240
|
-
const logPort = detectPortFromLogs(logPaths.stdout);
|
|
241
|
-
if (logPort) ports = [logPort];
|
|
242
|
-
}
|
|
243
|
-
}
|
|
340
|
+
const ports = running ? detectPorts(entry.pid) : [];
|
|
244
341
|
const uptime = running ? formatUptime(new Date(entry.startedAt)) : null;
|
|
245
342
|
console.log(JSON.stringify({
|
|
246
343
|
name,
|
|
@@ -256,12 +353,6 @@ const statusCommand = defineCommand({
|
|
|
256
353
|
}));
|
|
257
354
|
}
|
|
258
355
|
});
|
|
259
|
-
function formatUptime(startedAt) {
|
|
260
|
-
const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1e3);
|
|
261
|
-
if (seconds < 60) return `${seconds}s`;
|
|
262
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
263
|
-
return `${Math.floor(seconds / 3600)}h${Math.floor(seconds % 3600 / 60)}m`;
|
|
264
|
-
}
|
|
265
356
|
|
|
266
357
|
//#endregion
|
|
267
358
|
//#region src/logs.ts
|
|
@@ -328,8 +419,10 @@ const logsCommand = defineCommand({
|
|
|
328
419
|
},
|
|
329
420
|
run({ args, rawArgs }) {
|
|
330
421
|
const name = args.name ?? rawArgs[0];
|
|
331
|
-
|
|
332
|
-
|
|
422
|
+
try {
|
|
423
|
+
validateName(name);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error(`Error: ${err.message}`);
|
|
333
426
|
process.exit(1);
|
|
334
427
|
}
|
|
335
428
|
if (!getProcess(name)) {
|
|
@@ -381,8 +474,10 @@ const stopCommand = defineCommand({
|
|
|
381
474
|
},
|
|
382
475
|
run({ args, rawArgs }) {
|
|
383
476
|
const name = args.name ?? rawArgs[0];
|
|
384
|
-
|
|
385
|
-
|
|
477
|
+
try {
|
|
478
|
+
validateName(name);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(`Error: ${err.message}`);
|
|
386
481
|
process.exit(1);
|
|
387
482
|
}
|
|
388
483
|
const entry = getProcess(name);
|
|
@@ -433,22 +528,18 @@ const listCommand = defineCommand({
|
|
|
433
528
|
return true;
|
|
434
529
|
}).map(([name, entry]) => {
|
|
435
530
|
const running = isProcessRunning(entry.pid);
|
|
436
|
-
|
|
437
|
-
if (running) {
|
|
438
|
-
ports = detectPorts(entry.pid);
|
|
439
|
-
if (ports.length === 0) {
|
|
440
|
-
const logPort = detectPortFromLogs(getLogPaths(name).stdout);
|
|
441
|
-
if (logPort) ports = [logPort];
|
|
442
|
-
}
|
|
443
|
-
}
|
|
531
|
+
const ports = running ? detectPorts(entry.pid) : [];
|
|
444
532
|
return {
|
|
445
533
|
name,
|
|
446
534
|
pid: entry.pid,
|
|
447
535
|
running,
|
|
536
|
+
ports,
|
|
448
537
|
port: ports[0] ?? null,
|
|
449
538
|
cwd: entry.cwd,
|
|
450
539
|
command: entry.command.join(" "),
|
|
451
|
-
startedAt: entry.startedAt
|
|
540
|
+
startedAt: entry.startedAt,
|
|
541
|
+
uptime: running ? formatUptime(new Date(entry.startedAt)) : null,
|
|
542
|
+
...entry.killAt && { killAt: entry.killAt }
|
|
452
543
|
};
|
|
453
544
|
});
|
|
454
545
|
console.log(JSON.stringify(entries, null, 2));
|
|
@@ -484,6 +575,12 @@ const cleanCommand = defineCommand({
|
|
|
484
575
|
cleaned.push(procName);
|
|
485
576
|
}
|
|
486
577
|
} else if (name) {
|
|
578
|
+
try {
|
|
579
|
+
validateName(name);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.error(`Error: ${err.message}`);
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
487
584
|
const entry = registry[name];
|
|
488
585
|
if (!entry) {
|
|
489
586
|
console.error(`Process '${name}' not found`);
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/registry.ts","../src/commands/start.ts","../src/ports.ts","../src/commands/status.ts","../src/logs.ts","../src/commands/logs.ts","../src/commands/stop.ts","../src/commands/list.ts","../src/commands/clean.ts","../src/cli.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface ProcessEntry {\n pid: number;\n command: string[];\n cwd: string;\n startedAt: string;\n timeout?: number; // seconds, if set\n killAt?: string; // ISO timestamp when to kill\n}\n\nexport interface Registry {\n [name: string]: ProcessEntry;\n}\n\nfunction getDataDir(): string {\n return (\n process.env.BGPROC_DATA_DIR || join(homedir(), \".local\", \"share\", \"bgproc\")\n );\n}\n\nfunction getRegistryPath(): string {\n return join(getDataDir(), \"registry.json\");\n}\n\nexport function getLogsDir(): string {\n return join(getDataDir(), \"logs\");\n}\n\nexport function ensureDataDir(): void {\n mkdirSync(getDataDir(), { recursive: true });\n mkdirSync(getLogsDir(), { recursive: true });\n}\n\nexport function readRegistry(): Registry {\n ensureDataDir();\n const registryPath = getRegistryPath();\n if (!existsSync(registryPath)) {\n return {};\n }\n try {\n const content = readFileSync(registryPath, \"utf-8\");\n return JSON.parse(content);\n } catch {\n return {};\n }\n}\n\nexport function writeRegistry(registry: Registry): void {\n ensureDataDir();\n writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2));\n}\n\nexport function addProcess(name: string, entry: ProcessEntry): void {\n const registry = readRegistry();\n const existing = registry[name];\n\n if (existing) {\n if (isProcessRunning(existing.pid)) {\n throw new Error(\n `Process '${name}' is already running (PID ${existing.pid}). Use 'bgproc stop ${name}' first.`,\n );\n }\n // Dead process - auto-clean old logs before starting fresh\n const logPaths = getLogPaths(name);\n try {\n if (existsSync(logPaths.stdout)) unlinkSync(logPaths.stdout);\n if (existsSync(logPaths.stderr)) unlinkSync(logPaths.stderr);\n } catch {\n // ignore\n }\n }\n\n registry[name] = entry;\n writeRegistry(registry);\n}\n\nexport function removeProcess(name: string): void {\n const registry = readRegistry();\n delete registry[name];\n writeRegistry(registry);\n}\n\nexport function getProcess(name: string): ProcessEntry | undefined {\n const registry = readRegistry();\n return registry[name];\n}\n\nexport function isProcessRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function getLogPaths(name: string): { stdout: string; stderr: string } {\n return {\n stdout: join(getLogsDir(), `${name}.stdout.log`),\n stderr: join(getLogsDir(), `${name}.stderr.log`),\n };\n}\n","import { defineCommand } from \"citty\";\nimport { spawn } from \"node:child_process\";\nimport { openSync } from \"node:fs\";\nimport { addProcess, getLogPaths, ensureDataDir } from \"../registry.js\";\n\nexport const startCommand = defineCommand({\n meta: {\n name: \"start\",\n description:\n \"Start a background process\\n\\nUsage: bgproc start -n <name> [-t <seconds>] -- <command...>\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Name for the process\",\n required: true,\n },\n timeout: {\n type: \"string\",\n alias: \"t\",\n description: \"Kill after N seconds\",\n },\n },\n run({ args, rawArgs }) {\n const name = args.name;\n const timeout = args.timeout ? parseInt(args.timeout, 10) : undefined;\n\n // Get command from rawArgs after \"--\"\n const dashDashIdx = rawArgs.indexOf(\"--\");\n const command = dashDashIdx >= 0 ? rawArgs.slice(dashDashIdx + 1) : [];\n\n if (command.length === 0) {\n console.error(\n \"Error: No command specified. Use: bgproc start -n <name> -- <command>\",\n );\n process.exit(1);\n }\n\n ensureDataDir();\n const logPaths = getLogPaths(name);\n const cwd = process.cwd();\n\n // Open log files\n const stdoutFd = openSync(logPaths.stdout, \"a\");\n const stderrFd = openSync(logPaths.stderr, \"a\");\n\n // Spawn detached process\n const child = spawn(command[0]!, command.slice(1), {\n cwd,\n detached: true,\n stdio: [\"ignore\", stdoutFd, stderrFd],\n });\n\n child.unref();\n\n const entry = {\n pid: child.pid!,\n command,\n cwd,\n startedAt: new Date().toISOString(),\n ...(timeout && {\n timeout,\n killAt: new Date(Date.now() + timeout * 1000).toISOString(),\n }),\n };\n\n try {\n addProcess(name, entry);\n } catch (err) {\n // Kill the process we just started since we can't register it\n try {\n process.kill(child.pid!, \"SIGTERM\");\n } catch {\n // ignore\n }\n console.error((err as Error).message);\n process.exit(1);\n }\n\n // If timeout specified, schedule kill\n if (timeout && child.pid) {\n scheduleKill(child.pid, timeout, name);\n }\n\n console.log(\n JSON.stringify({\n name,\n pid: child.pid,\n cwd,\n command: command.join(\" \"),\n ...(timeout && { killAt: entry.killAt }),\n }),\n );\n },\n});\n\n/**\n * Fork a small process to kill after timeout\n * This survives the parent CLI exiting\n */\nfunction scheduleKill(pid: number, seconds: number, name: string): void {\n const killer = spawn(\n process.execPath,\n [\n \"-e\",\n `\n setTimeout(() => {\n try {\n process.kill(${pid}, 0); // check if alive\n process.kill(${pid}, 'SIGTERM');\n console.error('bgproc: ${name} killed after ${seconds}s timeout');\n } catch {}\n process.exit(0);\n }, ${seconds * 1000});\n `,\n ],\n {\n detached: true,\n stdio: \"ignore\",\n },\n );\n killer.unref();\n}\n","import { execSync } from \"node:child_process\";\nimport { readFileSync, existsSync } from \"node:fs\";\n\n/**\n * Detect listening ports for a given PID using lsof\n */\nexport function detectPorts(pid: number): number[] {\n try {\n // Just filter by PID and look for LISTEN in the output\n // -P = show port numbers, -n = no DNS resolution\n const output = execSync(`lsof -p ${pid} -P -n 2>/dev/null | grep LISTEN`, {\n encoding: \"utf-8\",\n });\n const ports: number[] = [];\n for (const line of output.split(\"\\n\")) {\n // Format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n // NAME is like *:3000 or 127.0.0.1:3000 or [::1]:3000\n const match = line.match(/:(\\d+)\\s+\\(LISTEN\\)/);\n if (match) {\n ports.push(parseInt(match[1], 10));\n }\n }\n return [...new Set(ports)]; // dedupe\n } catch {\n return [];\n }\n}\n\n/**\n * Try to detect port from log output (for when lsof doesn't show it yet)\n */\nexport function detectPortFromLogs(logPath: string): number | null {\n if (!existsSync(logPath)) return null;\n\n try {\n const content = readFileSync(logPath, \"utf-8\");\n // Check last 50 lines for port announcements\n const lines = content.split(\"\\n\").slice(-50);\n\n const patterns = [\n /localhost:(\\d+)/i,\n /127\\.0\\.0\\.1:(\\d+)/,\n /0\\.0\\.0\\.0:(\\d+)/,\n /port\\s+(\\d+)/i,\n /listening\\s+(?:on\\s+)?(?:port\\s+)?:?(\\d+)/i,\n /:\\/\\/[^:]+:(\\d+)/,\n ];\n\n for (const line of lines.reverse()) {\n for (const pattern of patterns) {\n const match = line.match(pattern);\n if (match) {\n const port = parseInt(match[1], 10);\n if (port > 0 && port < 65536) {\n return port;\n }\n }\n }\n }\n } catch {\n // ignore\n }\n\n return null;\n}\n","import { defineCommand } from \"citty\";\nimport { getProcess, isProcessRunning, getLogPaths } from \"../registry.js\";\nimport { detectPorts, detectPortFromLogs } from \"../ports.js\";\n\nexport const statusCommand = defineCommand({\n meta: {\n name: \"status\",\n description: \"Get status of a background process, including pid and open ports\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n },\n run({ args, rawArgs }) {\n const name = args.name ?? rawArgs[0];\n if (!name) {\n console.error(\"Error: Process name required\");\n process.exit(1);\n }\n const entry = getProcess(name);\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n const running = isProcessRunning(entry.pid);\n const logPaths = getLogPaths(name);\n\n let ports: number[] = [];\n if (running) {\n ports = detectPorts(entry.pid);\n // Fallback to log parsing if lsof didn't find anything\n if (ports.length === 0) {\n const logPort = detectPortFromLogs(logPaths.stdout);\n if (logPort) ports = [logPort];\n }\n }\n\n const uptime = running ? formatUptime(new Date(entry.startedAt)) : null;\n\n console.log(\n JSON.stringify({\n name,\n pid: entry.pid,\n running,\n ports,\n port: ports[0] ?? null, // convenience: first port\n cwd: entry.cwd,\n command: entry.command.join(\" \"),\n startedAt: entry.startedAt,\n uptime,\n ...(entry.killAt && { killAt: entry.killAt }),\n }),\n );\n },\n});\n\nfunction formatUptime(startedAt: Date): string {\n const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1000);\n if (seconds < 60) return `${seconds}s`;\n if (seconds < 3600) return `${Math.floor(seconds / 60)}m${seconds % 60}s`;\n const hours = Math.floor(seconds / 3600);\n const mins = Math.floor((seconds % 3600) / 60);\n return `${hours}h${mins}m`;\n}\n","import { createWriteStream, statSync, readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport type { WriteStream } from \"node:fs\";\n\nconst MAX_LOG_SIZE = 1 * 1024 * 1024; // 1MB\nconst KEEP_SIZE = 512 * 1024; // Keep last 512KB when truncating\n\n/**\n * Create a write stream that caps file size at 1MB\n */\nexport function createCappedWriteStream(path: string): WriteStream {\n const stream = createWriteStream(path, { flags: \"a\" });\n\n // Check and truncate periodically\n let bytesWritten = 0;\n const originalWrite = stream.write.bind(stream);\n\n stream.write = function (chunk: any, ...args: any[]): boolean {\n bytesWritten += Buffer.byteLength(chunk);\n\n // Every ~100KB written, check total size\n if (bytesWritten > 100 * 1024) {\n bytesWritten = 0;\n try {\n const stats = statSync(path);\n if (stats.size > MAX_LOG_SIZE) {\n // Truncate asynchronously - next writes will be to truncated file\n truncateLogFile(path);\n }\n } catch {\n // ignore\n }\n }\n\n return originalWrite(chunk, ...args);\n } as typeof stream.write;\n\n return stream;\n}\n\n/**\n * Truncate log file to keep only the last KEEP_SIZE bytes\n */\nfunction truncateLogFile(path: string): void {\n try {\n const content = readFileSync(path);\n if (content.length > MAX_LOG_SIZE) {\n const kept = content.slice(-KEEP_SIZE);\n // Find first newline to avoid cutting mid-line\n const newlineIdx = kept.indexOf(10); // \\n\n const trimmed = newlineIdx > 0 ? kept.slice(newlineIdx + 1) : kept;\n writeFileSync(path, trimmed);\n }\n } catch {\n // ignore\n }\n}\n\n/**\n * Read the last N lines from a log file\n */\nexport function readLastLines(path: string, n: number): string[] {\n if (!existsSync(path)) return [];\n\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\");\n // Remove trailing empty line if present\n if (lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n return lines.slice(-n);\n } catch {\n return [];\n }\n}\n\n/**\n * Read entire log file\n */\nexport function readLog(path: string): string {\n if (!existsSync(path)) return \"\";\n try {\n return readFileSync(path, \"utf-8\");\n } catch {\n return \"\";\n }\n}\n","import { defineCommand } from \"citty\";\nimport { spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { getProcess, getLogPaths } from \"../registry.js\";\nimport { readLastLines, readLog } from \"../logs.js\";\n\nexport const logsCommand = defineCommand({\n meta: {\n name: \"logs\",\n description: \"View logs for a background process\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n tail: {\n type: \"string\",\n alias: \"t\",\n description: \"Number of lines to show (default: 100)\",\n },\n follow: {\n type: \"boolean\",\n alias: \"f\",\n description: \"Follow log output (tail -f)\",\n },\n errors: {\n type: \"boolean\",\n alias: \"e\",\n description: \"Show only stderr\",\n },\n all: {\n type: \"boolean\",\n alias: \"a\",\n description: \"Show all logs (no line limit)\",\n },\n },\n run({ args, rawArgs }) {\n const name = args.name ?? rawArgs[0];\n if (!name) {\n console.error(\"Error: Process name required\");\n process.exit(1);\n }\n const entry = getProcess(name);\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n const logPaths = getLogPaths(name);\n const logPath = args.errors ? logPaths.stderr : logPaths.stdout;\n\n if (!existsSync(logPath)) {\n console.error(`No logs found for '${name}'`);\n process.exit(1);\n }\n\n if (args.follow) {\n // Use tail -f for follow mode\n const tail = spawn(\"tail\", [\"-f\", logPath], {\n stdio: \"inherit\",\n });\n\n process.on(\"SIGINT\", () => {\n tail.kill();\n process.exit(0);\n });\n\n return;\n }\n\n if (args.all) {\n const content = readLog(logPath);\n process.stdout.write(content);\n return;\n }\n\n const lines = parseInt(args.tail ?? \"100\", 10);\n const output = readLastLines(logPath, lines);\n console.log(output.join(\"\\n\"));\n },\n});\n","import { defineCommand } from \"citty\";\nimport { getProcess, removeProcess, isProcessRunning } from \"../registry.js\";\n\nexport const stopCommand = defineCommand({\n meta: {\n name: \"stop\",\n description: \"Stop a background process\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n force: {\n type: \"boolean\",\n alias: \"f\",\n description: \"Force kill (SIGKILL instead of SIGTERM)\",\n },\n },\n run({ args, rawArgs }) {\n const name = args.name ?? rawArgs[0];\n if (!name) {\n console.error(\"Error: Process name required\");\n process.exit(1);\n }\n const entry = getProcess(name);\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n const wasRunning = isProcessRunning(entry.pid);\n\n if (!wasRunning) {\n process.stderr.write(\n `Warning: Process '${name}' (PID ${entry.pid}) was already dead\\n`,\n );\n } else {\n const signal = args.force ? \"SIGKILL\" : \"SIGTERM\";\n try {\n process.kill(entry.pid, signal);\n } catch (err) {\n console.error(`Failed to kill process: ${(err as Error).message}`);\n process.exit(1);\n }\n }\n\n removeProcess(name);\n\n console.log(\n JSON.stringify({\n name,\n pid: entry.pid,\n stopped: true,\n wasRunning,\n signal: wasRunning ? (args.force ? \"SIGKILL\" : \"SIGTERM\") : null,\n }),\n );\n },\n});\n","import path from \"node:path\";\nimport { defineCommand } from \"citty\";\nimport { readRegistry, isProcessRunning, getLogPaths } from \"../registry.js\";\nimport { detectPorts, detectPortFromLogs } from \"../ports.js\";\n\nexport const listCommand = defineCommand({\n meta: {\n name: \"list\",\n description: \"List all background processes\",\n },\n args: {\n cwd: {\n type: \"string\",\n alias: \"c\",\n description: \"Filter by cwd (no arg = current directory)\",\n },\n },\n run({ args }) {\n const registry = readRegistry();\n\n // Handle --cwd with no value: use current directory\n // citty will set it to \"\" if flag present with no value\n let cwdFilter: string | undefined;\n if (args.cwd !== undefined) {\n cwdFilter = args.cwd === \"\" ? process.cwd() : path.resolve(args.cwd);\n }\n\n const entries = Object.entries(registry)\n .filter(([_, entry]) => {\n if (cwdFilter && entry.cwd !== cwdFilter) {\n return false;\n }\n return true;\n })\n .map(([name, entry]) => {\n const running = isProcessRunning(entry.pid);\n let ports: number[] = [];\n\n if (running) {\n ports = detectPorts(entry.pid);\n if (ports.length === 0) {\n const logPaths = getLogPaths(name);\n const logPort = detectPortFromLogs(logPaths.stdout);\n if (logPort) ports = [logPort];\n }\n }\n\n return {\n name,\n pid: entry.pid,\n running,\n port: ports[0] ?? null,\n cwd: entry.cwd,\n command: entry.command.join(\" \"),\n startedAt: entry.startedAt,\n };\n });\n\n console.log(JSON.stringify(entries, null, 2));\n },\n});\n","import { defineCommand } from \"citty\";\nimport { unlinkSync, existsSync } from \"node:fs\";\nimport {\n readRegistry,\n removeProcess,\n isProcessRunning,\n getLogPaths,\n} from \"../registry.js\";\n\nexport const cleanCommand = defineCommand({\n meta: {\n name: \"clean\",\n description: \"Remove dead processes and their logs\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n all: {\n type: \"boolean\",\n alias: \"a\",\n description: \"Clean all dead processes\",\n },\n },\n run({ args, rawArgs }) {\n const registry = readRegistry();\n const cleaned: string[] = [];\n const name = args.name ?? rawArgs[0];\n\n if (args.all) {\n // Clean all dead processes\n for (const [procName, entry] of Object.entries(registry)) {\n if (!isProcessRunning(entry.pid)) {\n cleanProcess(procName);\n cleaned.push(procName);\n }\n }\n } else if (name) {\n const entry = registry[name];\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n if (isProcessRunning(entry.pid)) {\n console.error(\n `Process '${name}' is still running. Use 'bgproc stop ${name}' first.`,\n );\n process.exit(1);\n }\n\n cleanProcess(name);\n cleaned.push(name);\n } else {\n console.error(\"Specify a process name or use --all\");\n process.exit(1);\n }\n\n console.log(\n JSON.stringify({\n cleaned,\n count: cleaned.length,\n }),\n );\n },\n});\n\nfunction cleanProcess(name: string): void {\n removeProcess(name);\n\n const logPaths = getLogPaths(name);\n try {\n if (existsSync(logPaths.stdout)) unlinkSync(logPaths.stdout);\n if (existsSync(logPaths.stderr)) unlinkSync(logPaths.stderr);\n } catch {\n // ignore\n }\n}\n","import { defineCommand, runMain } from \"citty\";\nimport { startCommand } from \"./commands/start.js\";\nimport { statusCommand } from \"./commands/status.js\";\nimport { logsCommand } from \"./commands/logs.js\";\nimport { stopCommand } from \"./commands/stop.js\";\nimport { listCommand } from \"./commands/list.js\";\nimport { cleanCommand } from \"./commands/clean.js\";\n\nconst main = defineCommand({\n meta: {\n name: \"bgproc\",\n version: \"0.1.0\",\n description: \"Simple process manager for agents. All commands output JSON to stdout.\\nExample: bgproc start -n myserver -- npm run dev\",\n },\n subCommands: {\n start: startCommand,\n status: statusCommand,\n logs: logsCommand,\n stop: stopCommand,\n list: listCommand,\n clean: cleanCommand,\n },\n});\n\nawait runMain(main);\n"],"mappings":";;;;;;;;AAiBA,SAAS,aAAqB;AAC5B,QACE,QAAQ,IAAI,mBAAmB,KAAK,SAAS,EAAE,UAAU,SAAS,SAAS;;AAI/E,SAAS,kBAA0B;AACjC,QAAO,KAAK,YAAY,EAAE,gBAAgB;;AAG5C,SAAgB,aAAqB;AACnC,QAAO,KAAK,YAAY,EAAE,OAAO;;AAGnC,SAAgB,gBAAsB;AACpC,WAAU,YAAY,EAAE,EAAE,WAAW,MAAM,CAAC;AAC5C,WAAU,YAAY,EAAE,EAAE,WAAW,MAAM,CAAC;;AAG9C,SAAgB,eAAyB;AACvC,gBAAe;CACf,MAAM,eAAe,iBAAiB;AACtC,KAAI,CAAC,WAAW,aAAa,CAC3B,QAAO,EAAE;AAEX,KAAI;EACF,MAAM,UAAU,aAAa,cAAc,QAAQ;AACnD,SAAO,KAAK,MAAM,QAAQ;SACpB;AACN,SAAO,EAAE;;;AAIb,SAAgB,cAAc,UAA0B;AACtD,gBAAe;AACf,eAAc,iBAAiB,EAAE,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;;AAGrE,SAAgB,WAAW,MAAc,OAA2B;CAClE,MAAM,WAAW,cAAc;CAC/B,MAAM,WAAW,SAAS;AAE1B,KAAI,UAAU;AACZ,MAAI,iBAAiB,SAAS,IAAI,CAChC,OAAM,IAAI,MACR,YAAY,KAAK,4BAA4B,SAAS,IAAI,sBAAsB,KAAK,UACtF;EAGH,MAAM,WAAW,YAAY,KAAK;AAClC,MAAI;AACF,OAAI,WAAW,SAAS,OAAO,CAAE,YAAW,SAAS,OAAO;AAC5D,OAAI,WAAW,SAAS,OAAO,CAAE,YAAW,SAAS,OAAO;UACtD;;AAKV,UAAS,QAAQ;AACjB,eAAc,SAAS;;AAGzB,SAAgB,cAAc,MAAoB;CAChD,MAAM,WAAW,cAAc;AAC/B,QAAO,SAAS;AAChB,eAAc,SAAS;;AAGzB,SAAgB,WAAW,MAAwC;AAEjE,QADiB,cAAc,CACf;;AAGlB,SAAgB,iBAAiB,KAAsB;AACrD,KAAI;AACF,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;SACD;AACN,SAAO;;;AAIX,SAAgB,YAAY,MAAkD;AAC5E,QAAO;EACL,QAAQ,KAAK,YAAY,EAAE,GAAG,KAAK,aAAa;EAChD,QAAQ,KAAK,YAAY,EAAE,GAAG,KAAK,aAAa;EACjD;;;;;AClGH,MAAa,eAAe,cAAc;CACxC,MAAM;EACJ,MAAM;EACN,aACE;EACH;CACD,MAAM;EACJ,MAAM;GACJ,MAAM;GACN,OAAO;GACP,aAAa;GACb,UAAU;GACX;EACD,SAAS;GACP,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACF;CACD,IAAI,EAAE,MAAM,WAAW;EACrB,MAAM,OAAO,KAAK;EAClB,MAAM,UAAU,KAAK,UAAU,SAAS,KAAK,SAAS,GAAG,GAAG;EAG5D,MAAM,cAAc,QAAQ,QAAQ,KAAK;EACzC,MAAM,UAAU,eAAe,IAAI,QAAQ,MAAM,cAAc,EAAE,GAAG,EAAE;AAEtE,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAQ,MACN,wEACD;AACD,WAAQ,KAAK,EAAE;;AAGjB,iBAAe;EACf,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,MAAM,QAAQ,KAAK;EAGzB,MAAM,WAAW,SAAS,SAAS,QAAQ,IAAI;EAC/C,MAAM,WAAW,SAAS,SAAS,QAAQ,IAAI;EAG/C,MAAM,QAAQ,MAAM,QAAQ,IAAK,QAAQ,MAAM,EAAE,EAAE;GACjD;GACA,UAAU;GACV,OAAO;IAAC;IAAU;IAAU;IAAS;GACtC,CAAC;AAEF,QAAM,OAAO;EAEb,MAAM,QAAQ;GACZ,KAAK,MAAM;GACX;GACA;GACA,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,GAAI,WAAW;IACb;IACA,QAAQ,IAAI,KAAK,KAAK,KAAK,GAAG,UAAU,IAAK,CAAC,aAAa;IAC5D;GACF;AAED,MAAI;AACF,cAAW,MAAM,MAAM;WAChB,KAAK;AAEZ,OAAI;AACF,YAAQ,KAAK,MAAM,KAAM,UAAU;WAC7B;AAGR,WAAQ,MAAO,IAAc,QAAQ;AACrC,WAAQ,KAAK,EAAE;;AAIjB,MAAI,WAAW,MAAM,IACnB,cAAa,MAAM,KAAK,SAAS,KAAK;AAGxC,UAAQ,IACN,KAAK,UAAU;GACb;GACA,KAAK,MAAM;GACX;GACA,SAAS,QAAQ,KAAK,IAAI;GAC1B,GAAI,WAAW,EAAE,QAAQ,MAAM,QAAQ;GACxC,CAAC,CACH;;CAEJ,CAAC;;;;;AAMF,SAAS,aAAa,KAAa,SAAiB,MAAoB;AAqBtE,CApBe,MACb,QAAQ,UACR,CACE,MACA;;;yBAGmB,IAAI;yBACJ,IAAI;mCACM,KAAK,gBAAgB,QAAQ;;;WAGrD,UAAU,IAAK;QAErB,EACD;EACE,UAAU;EACV,OAAO;EACR,CACF,CACM,OAAO;;;;;;;;ACpHhB,SAAgB,YAAY,KAAuB;AACjD,KAAI;EAGF,MAAM,SAAS,SAAS,WAAW,IAAI,mCAAmC,EACxE,UAAU,SACX,CAAC;EACF,MAAM,QAAkB,EAAE;AAC1B,OAAK,MAAM,QAAQ,OAAO,MAAM,KAAK,EAAE;GAGrC,MAAM,QAAQ,KAAK,MAAM,sBAAsB;AAC/C,OAAI,MACF,OAAM,KAAK,SAAS,MAAM,IAAI,GAAG,CAAC;;AAGtC,SAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;SACpB;AACN,SAAO,EAAE;;;;;;AAOb,SAAgB,mBAAmB,SAAgC;AACjE,KAAI,CAAC,WAAW,QAAQ,CAAE,QAAO;AAEjC,KAAI;EAGF,MAAM,QAFU,aAAa,SAAS,QAAQ,CAExB,MAAM,KAAK,CAAC,MAAM,IAAI;EAE5C,MAAM,WAAW;GACf;GACA;GACA;GACA;GACA;GACA;GACD;AAED,OAAK,MAAM,QAAQ,MAAM,SAAS,CAChC,MAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,QAAQ,KAAK,MAAM,QAAQ;AACjC,OAAI,OAAO;IACT,MAAM,OAAO,SAAS,MAAM,IAAI,GAAG;AACnC,QAAI,OAAO,KAAK,OAAO,MACrB,QAAO;;;SAKT;AAIR,QAAO;;;;;AC3DT,MAAa,gBAAgB,cAAc;CACzC,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,EACJ,MAAM;EACJ,MAAM;EACN,OAAO;EACP,aAAa;EACd,EACF;CACD,IAAI,EAAE,MAAM,WAAW;EACrB,MAAM,OAAO,KAAK,QAAQ,QAAQ;AAClC,MAAI,CAAC,MAAM;AACT,WAAQ,MAAM,+BAA+B;AAC7C,WAAQ,KAAK,EAAE;;EAEjB,MAAM,QAAQ,WAAW,KAAK;AAE9B,MAAI,CAAC,OAAO;AACV,WAAQ,MAAM,YAAY,KAAK,aAAa;AAC5C,WAAQ,KAAK,EAAE;;EAGjB,MAAM,UAAU,iBAAiB,MAAM,IAAI;EAC3C,MAAM,WAAW,YAAY,KAAK;EAElC,IAAI,QAAkB,EAAE;AACxB,MAAI,SAAS;AACX,WAAQ,YAAY,MAAM,IAAI;AAE9B,OAAI,MAAM,WAAW,GAAG;IACtB,MAAM,UAAU,mBAAmB,SAAS,OAAO;AACnD,QAAI,QAAS,SAAQ,CAAC,QAAQ;;;EAIlC,MAAM,SAAS,UAAU,aAAa,IAAI,KAAK,MAAM,UAAU,CAAC,GAAG;AAEnE,UAAQ,IACN,KAAK,UAAU;GACb;GACA,KAAK,MAAM;GACX;GACA;GACA,MAAM,MAAM,MAAM;GAClB,KAAK,MAAM;GACX,SAAS,MAAM,QAAQ,KAAK,IAAI;GAChC,WAAW,MAAM;GACjB;GACA,GAAI,MAAM,UAAU,EAAE,QAAQ,MAAM,QAAQ;GAC7C,CAAC,CACH;;CAEJ,CAAC;AAEF,SAAS,aAAa,WAAyB;CAC7C,MAAM,UAAU,KAAK,OAAO,KAAK,KAAK,GAAG,UAAU,SAAS,IAAI,IAAK;AACrE,KAAI,UAAU,GAAI,QAAO,GAAG,QAAQ;AACpC,KAAI,UAAU,KAAM,QAAO,GAAG,KAAK,MAAM,UAAU,GAAG,CAAC,GAAG,UAAU,GAAG;AAGvE,QAAO,GAFO,KAAK,MAAM,UAAU,KAAK,CAExB,GADH,KAAK,MAAO,UAAU,OAAQ,GAAG,CACtB;;;;;AChE1B,MAAM,eAAe,IAAI,OAAO;AAChC,MAAM,YAAY,MAAM;;;;AAwDxB,SAAgB,cAAc,MAAc,GAAqB;AAC/D,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO,EAAE;AAEhC,KAAI;EAEF,MAAM,QADU,aAAa,MAAM,QAAQ,CACrB,MAAM,KAAK;AAEjC,MAAI,MAAM,MAAM,SAAS,OAAO,GAC9B,OAAM,KAAK;AAEb,SAAO,MAAM,MAAM,CAAC,EAAE;SAChB;AACN,SAAO,EAAE;;;;;;AAOb,SAAgB,QAAQ,MAAsB;AAC5C,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO;AAC9B,KAAI;AACF,SAAO,aAAa,MAAM,QAAQ;SAC5B;AACN,SAAO;;;;;;AC9EX,MAAa,cAAc,cAAc;CACvC,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM;EACJ,MAAM;GACJ,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACD,MAAM;GACJ,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACD,QAAQ;GACN,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACD,QAAQ;GACN,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACD,KAAK;GACH,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACF;CACD,IAAI,EAAE,MAAM,WAAW;EACrB,MAAM,OAAO,KAAK,QAAQ,QAAQ;AAClC,MAAI,CAAC,MAAM;AACT,WAAQ,MAAM,+BAA+B;AAC7C,WAAQ,KAAK,EAAE;;AAIjB,MAAI,CAFU,WAAW,KAAK,EAElB;AACV,WAAQ,MAAM,YAAY,KAAK,aAAa;AAC5C,WAAQ,KAAK,EAAE;;EAGjB,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,UAAU,KAAK,SAAS,SAAS,SAAS,SAAS;AAEzD,MAAI,CAAC,WAAW,QAAQ,EAAE;AACxB,WAAQ,MAAM,sBAAsB,KAAK,GAAG;AAC5C,WAAQ,KAAK,EAAE;;AAGjB,MAAI,KAAK,QAAQ;GAEf,MAAM,OAAO,MAAM,QAAQ,CAAC,MAAM,QAAQ,EAAE,EAC1C,OAAO,WACR,CAAC;AAEF,WAAQ,GAAG,gBAAgB;AACzB,SAAK,MAAM;AACX,YAAQ,KAAK,EAAE;KACf;AAEF;;AAGF,MAAI,KAAK,KAAK;GACZ,MAAM,UAAU,QAAQ,QAAQ;AAChC,WAAQ,OAAO,MAAM,QAAQ;AAC7B;;EAIF,MAAM,SAAS,cAAc,SADf,SAAS,KAAK,QAAQ,OAAO,GAAG,CACF;AAC5C,UAAQ,IAAI,OAAO,KAAK,KAAK,CAAC;;CAEjC,CAAC;;;;AChFF,MAAa,cAAc,cAAc;CACvC,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM;EACJ,MAAM;GACJ,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACD,OAAO;GACL,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACF;CACD,IAAI,EAAE,MAAM,WAAW;EACrB,MAAM,OAAO,KAAK,QAAQ,QAAQ;AAClC,MAAI,CAAC,MAAM;AACT,WAAQ,MAAM,+BAA+B;AAC7C,WAAQ,KAAK,EAAE;;EAEjB,MAAM,QAAQ,WAAW,KAAK;AAE9B,MAAI,CAAC,OAAO;AACV,WAAQ,MAAM,YAAY,KAAK,aAAa;AAC5C,WAAQ,KAAK,EAAE;;EAGjB,MAAM,aAAa,iBAAiB,MAAM,IAAI;AAE9C,MAAI,CAAC,WACH,SAAQ,OAAO,MACb,qBAAqB,KAAK,SAAS,MAAM,IAAI,sBAC9C;OACI;GACL,MAAM,SAAS,KAAK,QAAQ,YAAY;AACxC,OAAI;AACF,YAAQ,KAAK,MAAM,KAAK,OAAO;YACxB,KAAK;AACZ,YAAQ,MAAM,2BAA4B,IAAc,UAAU;AAClE,YAAQ,KAAK,EAAE;;;AAInB,gBAAc,KAAK;AAEnB,UAAQ,IACN,KAAK,UAAU;GACb;GACA,KAAK,MAAM;GACX,SAAS;GACT;GACA,QAAQ,aAAc,KAAK,QAAQ,YAAY,YAAa;GAC7D,CAAC,CACH;;CAEJ,CAAC;;;;ACxDF,MAAa,cAAc,cAAc;CACvC,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,EACJ,KAAK;EACH,MAAM;EACN,OAAO;EACP,aAAa;EACd,EACF;CACD,IAAI,EAAE,QAAQ;EACZ,MAAM,WAAW,cAAc;EAI/B,IAAI;AACJ,MAAI,KAAK,QAAQ,OACf,aAAY,KAAK,QAAQ,KAAK,QAAQ,KAAK,GAAG,KAAK,QAAQ,KAAK,IAAI;EAGtE,MAAM,UAAU,OAAO,QAAQ,SAAS,CACrC,QAAQ,CAAC,GAAG,WAAW;AACtB,OAAI,aAAa,MAAM,QAAQ,UAC7B,QAAO;AAET,UAAO;IACP,CACD,KAAK,CAAC,MAAM,WAAW;GACtB,MAAM,UAAU,iBAAiB,MAAM,IAAI;GAC3C,IAAI,QAAkB,EAAE;AAExB,OAAI,SAAS;AACX,YAAQ,YAAY,MAAM,IAAI;AAC9B,QAAI,MAAM,WAAW,GAAG;KAEtB,MAAM,UAAU,mBADC,YAAY,KAAK,CACU,OAAO;AACnD,SAAI,QAAS,SAAQ,CAAC,QAAQ;;;AAIlC,UAAO;IACL;IACA,KAAK,MAAM;IACX;IACA,MAAM,MAAM,MAAM;IAClB,KAAK,MAAM;IACX,SAAS,MAAM,QAAQ,KAAK,IAAI;IAChC,WAAW,MAAM;IAClB;IACD;AAEJ,UAAQ,IAAI,KAAK,UAAU,SAAS,MAAM,EAAE,CAAC;;CAEhD,CAAC;;;;ACnDF,MAAa,eAAe,cAAc;CACxC,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM;EACJ,MAAM;GACJ,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACD,KAAK;GACH,MAAM;GACN,OAAO;GACP,aAAa;GACd;EACF;CACD,IAAI,EAAE,MAAM,WAAW;EACrB,MAAM,WAAW,cAAc;EAC/B,MAAM,UAAoB,EAAE;EAC5B,MAAM,OAAO,KAAK,QAAQ,QAAQ;AAElC,MAAI,KAAK,KAEP;QAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,SAAS,CACtD,KAAI,CAAC,iBAAiB,MAAM,IAAI,EAAE;AAChC,iBAAa,SAAS;AACtB,YAAQ,KAAK,SAAS;;aAGjB,MAAM;GACf,MAAM,QAAQ,SAAS;AAEvB,OAAI,CAAC,OAAO;AACV,YAAQ,MAAM,YAAY,KAAK,aAAa;AAC5C,YAAQ,KAAK,EAAE;;AAGjB,OAAI,iBAAiB,MAAM,IAAI,EAAE;AAC/B,YAAQ,MACN,YAAY,KAAK,uCAAuC,KAAK,UAC9D;AACD,YAAQ,KAAK,EAAE;;AAGjB,gBAAa,KAAK;AAClB,WAAQ,KAAK,KAAK;SACb;AACL,WAAQ,MAAM,sCAAsC;AACpD,WAAQ,KAAK,EAAE;;AAGjB,UAAQ,IACN,KAAK,UAAU;GACb;GACA,OAAO,QAAQ;GAChB,CAAC,CACH;;CAEJ,CAAC;AAEF,SAAS,aAAa,MAAoB;AACxC,eAAc,KAAK;CAEnB,MAAM,WAAW,YAAY,KAAK;AAClC,KAAI;AACF,MAAI,WAAW,SAAS,OAAO,CAAE,YAAW,SAAS,OAAO;AAC5D,MAAI,WAAW,SAAS,OAAO,CAAE,YAAW,SAAS,OAAO;SACtD;;;;;ACrDV,MAAM,QAhBO,cAAc;CACzB,MAAM;EACJ,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD,aAAa;EACX,OAAO;EACP,QAAQ;EACR,MAAM;EACN,MAAM;EACN,MAAM;EACN,OAAO;EACR;CACF,CAAC,CAEiB"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/registry.ts","../src/ports.ts","../src/commands/start.ts","../src/utils.ts","../src/commands/status.ts","../src/logs.ts","../src/commands/logs.ts","../src/commands/stop.ts","../src/commands/list.ts","../src/commands/clean.ts","../src/cli.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Validate process name to prevent path traversal and code injection\n */\nexport function validateName(name: string): void {\n if (!name) {\n throw new Error(\"Process name required\");\n }\n if (!/^[a-zA-Z0-9_-]+$/.test(name)) {\n throw new Error(\n \"Process name must contain only alphanumeric characters, hyphens, and underscores\",\n );\n }\n if (name.length > 64) {\n throw new Error(\"Process name must be 64 characters or less\");\n }\n}\n\nexport interface ProcessEntry {\n pid: number;\n command: string[];\n cwd: string;\n startedAt: string;\n timeout?: number; // seconds, if set\n killAt?: string; // ISO timestamp when to kill\n}\n\nexport interface Registry {\n [name: string]: ProcessEntry;\n}\n\nfunction getDataDir(): string {\n return (\n process.env.BGPROC_DATA_DIR || join(homedir(), \".local\", \"share\", \"bgproc\")\n );\n}\n\nfunction getRegistryPath(): string {\n return join(getDataDir(), \"registry.json\");\n}\n\nexport function getLogsDir(): string {\n return join(getDataDir(), \"logs\");\n}\n\nexport function ensureDataDir(): void {\n mkdirSync(getDataDir(), { recursive: true });\n mkdirSync(getLogsDir(), { recursive: true });\n}\n\nexport function readRegistry(): Registry {\n ensureDataDir();\n const registryPath = getRegistryPath();\n if (!existsSync(registryPath)) {\n return {};\n }\n try {\n const content = readFileSync(registryPath, \"utf-8\");\n return JSON.parse(content);\n } catch {\n return {};\n }\n}\n\nexport function writeRegistry(registry: Registry): void {\n ensureDataDir();\n writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2));\n}\n\nexport function addProcess(name: string, entry: ProcessEntry): void {\n const registry = readRegistry();\n const existing = registry[name];\n\n if (existing) {\n if (isProcessRunning(existing.pid)) {\n throw new Error(\n `Process '${name}' is already running (PID ${existing.pid}). Use --force to restart.`,\n );\n }\n // Dead process - auto-clean old logs before starting fresh\n const logPaths = getLogPaths(name);\n try {\n if (existsSync(logPaths.stdout)) unlinkSync(logPaths.stdout);\n if (existsSync(logPaths.stderr)) unlinkSync(logPaths.stderr);\n } catch {\n // ignore\n }\n }\n\n registry[name] = entry;\n writeRegistry(registry);\n}\n\nexport function removeProcess(name: string): void {\n const registry = readRegistry();\n delete registry[name];\n writeRegistry(registry);\n}\n\nexport function getProcess(name: string): ProcessEntry | undefined {\n const registry = readRegistry();\n return registry[name];\n}\n\nexport function isProcessRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function getLogPaths(name: string): { stdout: string; stderr: string } {\n return {\n stdout: join(getLogsDir(), `${name}.stdout.log`),\n stderr: join(getLogsDir(), `${name}.stderr.log`),\n };\n}\n","import { execSync } from \"node:child_process\";\n\n/**\n * Get all descendant PIDs of a process (children, grandchildren, etc.)\n */\nfunction getDescendantPids(pid: number): number[] {\n const descendants: number[] = [];\n try {\n // Get direct children using pgrep\n const output = execSync(`pgrep -P ${pid} 2>/dev/null`, {\n encoding: \"utf-8\",\n });\n for (const line of output.trim().split(\"\\n\")) {\n const childPid = parseInt(line, 10);\n if (!isNaN(childPid)) {\n descendants.push(childPid);\n // Recursively get grandchildren\n descendants.push(...getDescendantPids(childPid));\n }\n }\n } catch {\n // No children or pgrep failed\n }\n return descendants;\n}\n\n/**\n * Detect listening ports for a given PID and all its descendants using lsof\n */\nexport function detectPorts(pid: number): number[] {\n try {\n // Get all PIDs to check (the process itself plus all descendants)\n const allPids = [pid, ...getDescendantPids(pid)];\n const pidList = allPids.join(\",\");\n\n // Check all PIDs at once - lsof accepts comma-separated PIDs\n // -P = show port numbers, -n = no DNS resolution\n const output = execSync(\n `lsof -p ${pidList} -P -n 2>/dev/null | grep LISTEN`,\n {\n encoding: \"utf-8\",\n }\n );\n const ports: number[] = [];\n for (const line of output.split(\"\\n\")) {\n // Format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n // NAME is like *:3000 or 127.0.0.1:3000 or [::1]:3000\n const match = line.match(/:(\\d+)\\s+\\(LISTEN\\)/);\n if (match) {\n ports.push(parseInt(match[1], 10));\n }\n }\n return [...new Set(ports)]; // dedupe\n } catch {\n return [];\n }\n}\n","import { defineCommand } from \"citty\";\nimport { spawn, ChildProcess } from \"node:child_process\";\nimport { openSync } from \"node:fs\";\nimport { addProcess, getProcess, removeProcess, getLogPaths, ensureDataDir, validateName, isProcessRunning } from \"../registry.js\";\nimport { detectPorts } from \"../ports.js\";\n\nexport const startCommand = defineCommand({\n meta: {\n name: \"start\",\n description:\n \"Start a background process\\n\\nUsage: bgproc start -n <name> [-f] [-t <seconds>] [-w [<seconds>]] [--keep] -- <command...>\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Name for the process\",\n required: true,\n },\n timeout: {\n type: \"string\",\n alias: \"t\",\n description: \"Kill after N seconds\",\n },\n waitForPort: {\n type: \"string\",\n alias: \"w\",\n description: \"Wait for port to be detected (optional: timeout in seconds)\",\n },\n keep: {\n type: \"boolean\",\n description: \"Keep process running on timeout (only with --wait-for-port)\",\n },\n force: {\n type: \"boolean\",\n alias: \"f\",\n description: \"Kill existing process with same name before starting\",\n },\n },\n async run({ args, rawArgs }) {\n const name = args.name;\n\n try {\n validateName(name);\n } catch (err) {\n console.error(`Error: ${(err as Error).message}`);\n process.exit(1);\n }\n\n if (args.keep && args.waitForPort === undefined) {\n console.error(\"Error: --keep requires --wait-for-port\");\n process.exit(1);\n }\n\n // If --force, kill any existing process with this name\n if (args.force) {\n const existing = getProcess(name);\n if (existing && isProcessRunning(existing.pid)) {\n try {\n process.kill(existing.pid, \"SIGTERM\");\n } catch {\n // ignore - process may have just died\n }\n removeProcess(name);\n }\n }\n\n const timeout = args.timeout ? parseInt(args.timeout, 10) : undefined;\n\n // Get command from rawArgs after \"--\"\n const dashDashIdx = rawArgs.indexOf(\"--\");\n const command = dashDashIdx >= 0 ? rawArgs.slice(dashDashIdx + 1) : [];\n\n if (command.length === 0) {\n console.error(\n \"Error: No command specified. Use: bgproc start -n <name> -- <command>\",\n );\n process.exit(1);\n }\n\n ensureDataDir();\n const logPaths = getLogPaths(name);\n const cwd = process.cwd();\n\n // Open log files\n const stdoutFd = openSync(logPaths.stdout, \"a\");\n const stderrFd = openSync(logPaths.stderr, \"a\");\n\n // Spawn detached process\n const child = spawn(command[0]!, command.slice(1), {\n cwd,\n detached: true,\n stdio: [\"ignore\", stdoutFd, stderrFd],\n });\n\n child.unref();\n\n const entry = {\n pid: child.pid!,\n command,\n cwd,\n startedAt: new Date().toISOString(),\n ...(timeout && {\n timeout,\n killAt: new Date(Date.now() + timeout * 1000).toISOString(),\n }),\n };\n\n try {\n addProcess(name, entry);\n } catch (err) {\n // Kill the process we just started since we can't register it\n try {\n process.kill(child.pid!, \"SIGTERM\");\n } catch {\n // ignore\n }\n console.error((err as Error).message);\n process.exit(1);\n }\n\n // If timeout specified, schedule kill\n if (timeout && child.pid) {\n scheduleKill(child.pid, timeout, name);\n }\n\n const baseStatus = {\n name,\n pid: child.pid,\n cwd,\n command: command.join(\" \"),\n ...(timeout && { killAt: entry.killAt }),\n };\n\n // If --wait-for-port, wait for port detection before printing final status\n if (args.waitForPort !== undefined) {\n const waitTimeout = args.waitForPort ? parseInt(args.waitForPort, 10) : undefined;\n const killOnTimeout = !args.keep;\n const result = await waitForPortDetection(child.pid!, logPaths.stdout, waitTimeout, killOnTimeout);\n\n if (result.error) {\n console.error(result.error);\n process.exit(1);\n }\n\n console.log(\n JSON.stringify({\n ...baseStatus,\n ports: result.ports,\n port: result.ports![0],\n }),\n );\n } else {\n console.log(JSON.stringify(baseStatus));\n }\n },\n});\n\ninterface WaitResult {\n ports?: number[];\n error?: string;\n}\n\n/**\n * Wait for a port to be detected on the process.\n * Tails logs to stderr while waiting.\n */\nfunction waitForPortDetection(\n pid: number,\n logPath: string,\n timeoutSeconds?: number,\n killOnTimeout?: boolean\n): Promise<WaitResult> {\n return new Promise((resolve) => {\n const startTime = Date.now();\n\n // Tail the log file to stderr so user sees output\n const tail = spawn(\"tail\", [\"-f\", logPath], {\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n });\n tail.stdout?.pipe(process.stderr);\n\n const cleanup = (tailProcess: ChildProcess, intervalId: NodeJS.Timeout) => {\n clearInterval(intervalId);\n tailProcess.kill();\n };\n\n const pollInterval = setInterval(() => {\n // Check if process is still running\n if (!isProcessRunning(pid)) {\n cleanup(tail, pollInterval);\n resolve({ error: `Process ${pid} died before a port was detected` });\n return;\n }\n\n // Check for timeout\n if (timeoutSeconds !== undefined) {\n const elapsed = (Date.now() - startTime) / 1000;\n if (elapsed >= timeoutSeconds) {\n cleanup(tail, pollInterval);\n if (killOnTimeout) {\n try {\n process.kill(pid, \"SIGTERM\");\n } catch {\n // ignore - process may have just died\n }\n resolve({ error: `Timeout: no port detected after ${timeoutSeconds}s (process killed)` });\n } else {\n resolve({ error: `Timeout: no port detected after ${timeoutSeconds}s (process still running)` });\n }\n return;\n }\n }\n\n // Check for ports\n const ports = detectPorts(pid);\n if (ports.length > 0) {\n cleanup(tail, pollInterval);\n resolve({ ports });\n }\n }, 500);\n });\n}\n\n/**\n * Fork a small process to kill after timeout\n * This survives the parent CLI exiting\n */\nfunction scheduleKill(pid: number, seconds: number, name: string): void {\n const killer = spawn(\n process.execPath,\n [\n \"-e\",\n `\n setTimeout(() => {\n try {\n process.kill(${pid}, 0); // check if alive\n process.kill(${pid}, 'SIGTERM');\n console.error('bgproc: ' + process.env.BGPROC_NAME + ' killed after ${seconds}s timeout');\n } catch {}\n process.exit(0);\n }, ${seconds * 1000});\n `,\n ],\n {\n detached: true,\n stdio: \"ignore\",\n env: { ...process.env, BGPROC_NAME: name },\n },\n );\n killer.unref();\n}\n","export function formatUptime(startedAt: Date): string {\n const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1000);\n if (seconds < 60) return `${seconds}s`;\n if (seconds < 3600) return `${Math.floor(seconds / 60)}m${seconds % 60}s`;\n const hours = Math.floor(seconds / 3600);\n const mins = Math.floor((seconds % 3600) / 60);\n return `${hours}h${mins}m`;\n}\n","import { defineCommand } from \"citty\";\nimport { getProcess, isProcessRunning, validateName } from \"../registry.js\";\nimport { detectPorts } from \"../ports.js\";\nimport { formatUptime } from \"../utils.js\";\n\nexport const statusCommand = defineCommand({\n meta: {\n name: \"status\",\n description: \"Get status of a background process, including pid and open ports\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n },\n run({ args, rawArgs }) {\n const name = args.name ?? rawArgs[0];\n\n try {\n validateName(name);\n } catch (err) {\n console.error(`Error: ${(err as Error).message}`);\n process.exit(1);\n }\n\n const entry = getProcess(name);\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n const running = isProcessRunning(entry.pid);\n const ports = running ? detectPorts(entry.pid) : [];\n\n const uptime = running ? formatUptime(new Date(entry.startedAt)) : null;\n\n console.log(\n JSON.stringify({\n name,\n pid: entry.pid,\n running,\n ports,\n port: ports[0] ?? null, // convenience: first port\n cwd: entry.cwd,\n command: entry.command.join(\" \"),\n startedAt: entry.startedAt,\n uptime,\n ...(entry.killAt && { killAt: entry.killAt }),\n }),\n );\n },\n});\n","import { createWriteStream, statSync, readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport type { WriteStream } from \"node:fs\";\n\nconst MAX_LOG_SIZE = 1 * 1024 * 1024; // 1MB\nconst KEEP_SIZE = 512 * 1024; // Keep last 512KB when truncating\n\n/**\n * Create a write stream that caps file size at 1MB\n */\nexport function createCappedWriteStream(path: string): WriteStream {\n const stream = createWriteStream(path, { flags: \"a\" });\n\n // Check and truncate periodically\n let bytesWritten = 0;\n const originalWrite = stream.write.bind(stream);\n\n stream.write = function (chunk: any, ...args: any[]): boolean {\n bytesWritten += Buffer.byteLength(chunk);\n\n // Every ~100KB written, check total size\n if (bytesWritten > 100 * 1024) {\n bytesWritten = 0;\n try {\n const stats = statSync(path);\n if (stats.size > MAX_LOG_SIZE) {\n // Truncate asynchronously - next writes will be to truncated file\n truncateLogFile(path);\n }\n } catch {\n // ignore\n }\n }\n\n return originalWrite(chunk, ...args);\n } as typeof stream.write;\n\n return stream;\n}\n\n/**\n * Truncate log file to keep only the last KEEP_SIZE bytes\n */\nfunction truncateLogFile(path: string): void {\n try {\n const content = readFileSync(path);\n if (content.length > MAX_LOG_SIZE) {\n const kept = content.slice(-KEEP_SIZE);\n // Find first newline to avoid cutting mid-line\n const newlineIdx = kept.indexOf(10); // \\n\n const trimmed = newlineIdx > 0 ? kept.slice(newlineIdx + 1) : kept;\n writeFileSync(path, trimmed);\n }\n } catch {\n // ignore\n }\n}\n\n/**\n * Read the last N lines from a log file\n */\nexport function readLastLines(path: string, n: number): string[] {\n if (!existsSync(path)) return [];\n\n try {\n const content = readFileSync(path, \"utf-8\");\n const lines = content.split(\"\\n\");\n // Remove trailing empty line if present\n if (lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n return lines.slice(-n);\n } catch {\n return [];\n }\n}\n\n/**\n * Read entire log file\n */\nexport function readLog(path: string): string {\n if (!existsSync(path)) return \"\";\n try {\n return readFileSync(path, \"utf-8\");\n } catch {\n return \"\";\n }\n}\n","import { defineCommand } from \"citty\";\nimport { spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { getProcess, getLogPaths, validateName } from \"../registry.js\";\nimport { readLastLines, readLog } from \"../logs.js\";\n\nexport const logsCommand = defineCommand({\n meta: {\n name: \"logs\",\n description: \"View logs for a background process\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n tail: {\n type: \"string\",\n alias: \"t\",\n description: \"Number of lines to show (default: 100)\",\n },\n follow: {\n type: \"boolean\",\n alias: \"f\",\n description: \"Follow log output (tail -f)\",\n },\n errors: {\n type: \"boolean\",\n alias: \"e\",\n description: \"Show only stderr\",\n },\n all: {\n type: \"boolean\",\n alias: \"a\",\n description: \"Show all logs (no line limit)\",\n },\n },\n run({ args, rawArgs }) {\n const name = args.name ?? rawArgs[0];\n\n try {\n validateName(name);\n } catch (err) {\n console.error(`Error: ${(err as Error).message}`);\n process.exit(1);\n }\n\n const entry = getProcess(name);\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n const logPaths = getLogPaths(name);\n const logPath = args.errors ? logPaths.stderr : logPaths.stdout;\n\n if (!existsSync(logPath)) {\n console.error(`No logs found for '${name}'`);\n process.exit(1);\n }\n\n if (args.follow) {\n // Use tail -f for follow mode\n const tail = spawn(\"tail\", [\"-f\", logPath], {\n stdio: \"inherit\",\n });\n\n process.on(\"SIGINT\", () => {\n tail.kill();\n process.exit(0);\n });\n\n return;\n }\n\n if (args.all) {\n const content = readLog(logPath);\n process.stdout.write(content);\n return;\n }\n\n const lines = parseInt(args.tail ?? \"100\", 10);\n const output = readLastLines(logPath, lines);\n console.log(output.join(\"\\n\"));\n },\n});\n","import { defineCommand } from \"citty\";\nimport { getProcess, removeProcess, isProcessRunning, validateName } from \"../registry.js\";\n\nexport const stopCommand = defineCommand({\n meta: {\n name: \"stop\",\n description: \"Stop a background process\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n force: {\n type: \"boolean\",\n alias: \"f\",\n description: \"Force kill (SIGKILL instead of SIGTERM)\",\n },\n },\n run({ args, rawArgs }) {\n const name = args.name ?? rawArgs[0];\n\n try {\n validateName(name);\n } catch (err) {\n console.error(`Error: ${(err as Error).message}`);\n process.exit(1);\n }\n\n const entry = getProcess(name);\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n const wasRunning = isProcessRunning(entry.pid);\n\n if (!wasRunning) {\n process.stderr.write(\n `Warning: Process '${name}' (PID ${entry.pid}) was already dead\\n`,\n );\n } else {\n const signal = args.force ? \"SIGKILL\" : \"SIGTERM\";\n try {\n process.kill(entry.pid, signal);\n } catch (err) {\n console.error(`Failed to kill process: ${(err as Error).message}`);\n process.exit(1);\n }\n }\n\n removeProcess(name);\n\n console.log(\n JSON.stringify({\n name,\n pid: entry.pid,\n stopped: true,\n wasRunning,\n signal: wasRunning ? (args.force ? \"SIGKILL\" : \"SIGTERM\") : null,\n }),\n );\n },\n});\n","import path from \"node:path\";\nimport { defineCommand } from \"citty\";\nimport { readRegistry, isProcessRunning } from \"../registry.js\";\nimport { detectPorts } from \"../ports.js\";\nimport { formatUptime } from \"../utils.js\";\n\nexport const listCommand = defineCommand({\n meta: {\n name: \"list\",\n description: \"List all background processes\",\n },\n args: {\n cwd: {\n type: \"string\",\n alias: \"c\",\n description: \"Filter by cwd (no arg = current directory)\",\n },\n },\n run({ args }) {\n const registry = readRegistry();\n\n // Handle --cwd with no value: use current directory\n // citty will set it to \"\" if flag present with no value\n let cwdFilter: string | undefined;\n if (args.cwd !== undefined) {\n cwdFilter = args.cwd === \"\" ? process.cwd() : path.resolve(args.cwd);\n }\n\n const entries = Object.entries(registry)\n .filter(([_, entry]) => {\n if (cwdFilter && entry.cwd !== cwdFilter) {\n return false;\n }\n return true;\n })\n .map(([name, entry]) => {\n const running = isProcessRunning(entry.pid);\n const ports = running ? detectPorts(entry.pid) : [];\n\n return {\n name,\n pid: entry.pid,\n running,\n ports,\n port: ports[0] ?? null,\n cwd: entry.cwd,\n command: entry.command.join(\" \"),\n startedAt: entry.startedAt,\n uptime: running ? formatUptime(new Date(entry.startedAt)) : null,\n ...(entry.killAt && { killAt: entry.killAt }),\n };\n });\n\n console.log(JSON.stringify(entries, null, 2));\n },\n});\n","import { defineCommand } from \"citty\";\nimport { unlinkSync, existsSync } from \"node:fs\";\nimport {\n readRegistry,\n removeProcess,\n isProcessRunning,\n getLogPaths,\n validateName,\n} from \"../registry.js\";\n\nexport const cleanCommand = defineCommand({\n meta: {\n name: \"clean\",\n description: \"Remove dead processes and their logs\",\n },\n args: {\n name: {\n type: \"string\",\n alias: \"n\",\n description: \"Process name\",\n },\n all: {\n type: \"boolean\",\n alias: \"a\",\n description: \"Clean all dead processes\",\n },\n },\n run({ args, rawArgs }) {\n const registry = readRegistry();\n const cleaned: string[] = [];\n const name = args.name ?? rawArgs[0];\n\n if (args.all) {\n // Clean all dead processes\n for (const [procName, entry] of Object.entries(registry)) {\n if (!isProcessRunning(entry.pid)) {\n cleanProcess(procName);\n cleaned.push(procName);\n }\n }\n } else if (name) {\n try {\n validateName(name);\n } catch (err) {\n console.error(`Error: ${(err as Error).message}`);\n process.exit(1);\n }\n\n const entry = registry[name];\n\n if (!entry) {\n console.error(`Process '${name}' not found`);\n process.exit(1);\n }\n\n if (isProcessRunning(entry.pid)) {\n console.error(\n `Process '${name}' is still running. Use 'bgproc stop ${name}' first.`,\n );\n process.exit(1);\n }\n\n cleanProcess(name);\n cleaned.push(name);\n } else {\n console.error(\"Specify a process name or use --all\");\n process.exit(1);\n }\n\n console.log(\n JSON.stringify({\n cleaned,\n count: cleaned.length,\n }),\n );\n },\n});\n\nfunction cleanProcess(name: string): void {\n removeProcess(name);\n\n const logPaths = getLogPaths(name);\n try {\n if (existsSync(logPaths.stdout)) unlinkSync(logPaths.stdout);\n if (existsSync(logPaths.stderr)) unlinkSync(logPaths.stderr);\n } catch {\n // ignore\n }\n}\n","import { defineCommand, runMain } from \"citty\";\nimport { startCommand } from \"./commands/start.js\";\nimport { statusCommand } from \"./commands/status.js\";\nimport { logsCommand } from \"./commands/logs.js\";\nimport { stopCommand } from \"./commands/stop.js\";\nimport { listCommand } from \"./commands/list.js\";\nimport { cleanCommand } from \"./commands/clean.js\";\n\nconst main = defineCommand({\n meta: {\n name: \"bgproc\",\n version: \"0.1.0\",\n description: \"Simple process manager for agents. All commands output JSON to stdout.\\nExample: bgproc start -n myserver -- npm run dev\",\n },\n subCommands: {\n start: startCommand,\n status: statusCommand,\n logs: logsCommand,\n stop: stopCommand,\n list: listCommand,\n clean: cleanCommand,\n },\n});\n\nawait runMain(main);\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bgproc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Simple process manager for agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"background",
|
|
@@ -25,17 +25,6 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"type": "module",
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "tsdown",
|
|
30
|
-
"dev": "tsdown --watch",
|
|
31
|
-
"format": "oxfmt",
|
|
32
|
-
"lint": "oxlint",
|
|
33
|
-
"lint:fix": "oxlint --fix",
|
|
34
|
-
"start": "node dist/cli.mjs",
|
|
35
|
-
"test": "vitest",
|
|
36
|
-
"changeset": "changeset",
|
|
37
|
-
"release": "changeset publish"
|
|
38
|
-
},
|
|
39
28
|
"dependencies": {
|
|
40
29
|
"citty": "^0.1.6"
|
|
41
30
|
},
|
|
@@ -49,5 +38,16 @@
|
|
|
49
38
|
"tsdown": "^0.20.1",
|
|
50
39
|
"typescript": "^5.9.3",
|
|
51
40
|
"vitest": "^4.0.18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"dev": "tsdown --watch",
|
|
45
|
+
"format": "oxfmt",
|
|
46
|
+
"lint": "oxlint",
|
|
47
|
+
"lint:fix": "oxlint --fix",
|
|
48
|
+
"start": "node dist/cli.mjs",
|
|
49
|
+
"test": "vitest",
|
|
50
|
+
"changeset": "changeset",
|
|
51
|
+
"release": "changeset publish"
|
|
52
52
|
}
|
|
53
|
-
}
|
|
53
|
+
}
|