bgproc 0.0.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 +108 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +537 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# bgproc
|
|
2
|
+
|
|
3
|
+
Simple process manager for agents.
|
|
4
|
+
|
|
5
|
+
Manage background processes like dev servers from the command line. Designed to be agent-friendly with JSON output and easy status checking.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g bgproc
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Start a process
|
|
17
|
+
bgproc start -n myserver -- npm run dev
|
|
18
|
+
|
|
19
|
+
# Check status (returns JSON with port detection)
|
|
20
|
+
bgproc status myserver
|
|
21
|
+
# {"name":"myserver","pid":12345,"running":true,"port":3000,...}
|
|
22
|
+
|
|
23
|
+
# View logs
|
|
24
|
+
bgproc logs myserver
|
|
25
|
+
bgproc logs myserver --tail 50
|
|
26
|
+
bgproc logs myserver --follow
|
|
27
|
+
bgproc logs myserver --errors # stderr only
|
|
28
|
+
|
|
29
|
+
# List all processes
|
|
30
|
+
bgproc list
|
|
31
|
+
bgproc list --cwd # filter to current directory
|
|
32
|
+
bgproc list --cwd /path/to/dir # filter to specific directory
|
|
33
|
+
|
|
34
|
+
# Stop a process
|
|
35
|
+
bgproc stop myserver
|
|
36
|
+
bgproc stop myserver --force # SIGKILL
|
|
37
|
+
|
|
38
|
+
# Clean up dead processes
|
|
39
|
+
bgproc clean myserver
|
|
40
|
+
bgproc clean --all
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **JSON output**: All commands output JSON to stdout, errors to stderr
|
|
46
|
+
- **Port detection**: Automatically detects listening ports via `lsof`
|
|
47
|
+
- **Duplicate prevention**: Prevent starting multiple processes with the same name
|
|
48
|
+
- **Log management**: Stdout/stderr captured, capped at 1MB
|
|
49
|
+
- **Timeout support**: `--timeout 60` kills after N seconds
|
|
50
|
+
- **Auto-cleanup**: Starting a process with the same name as a dead one auto-cleans it
|
|
51
|
+
- **CWD filtering**: Filter process list by working directory
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
### `start`
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
-n, --name Process name (required)
|
|
59
|
+
-t, --timeout Kill after N seconds
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `status`, `stop`, `logs`, `clean`
|
|
63
|
+
|
|
64
|
+
All accept process name as positional arg or `-n`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bgproc status myserver
|
|
68
|
+
bgproc status -n myserver # equivalent
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `logs`
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
-t, --tail Number of lines (default: 100)
|
|
75
|
+
-f, --follow Tail the log
|
|
76
|
+
-e, --errors Show stderr only
|
|
77
|
+
-a, --all Show all logs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `stop`
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
-f, --force Use SIGKILL instead of SIGTERM
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `list`
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
-c, --cwd Filter by directory (no arg = current dir)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `clean`
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
-a, --all Clean all dead processes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Environment
|
|
99
|
+
|
|
100
|
+
- `BGPROC_DATA_DIR`: Override data directory (default: `~/.local/share/bgproc`)
|
|
101
|
+
|
|
102
|
+
## Platform Support
|
|
103
|
+
|
|
104
|
+
macOS and Linux only. Windows is not supported.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT © Matt Kane 2026
|
package/dist/cli.d.mts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { execSync, spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import path, { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
//#region src/registry.ts
|
|
9
|
+
function getDataDir() {
|
|
10
|
+
return process.env.BGPROC_DATA_DIR || join(homedir(), ".local", "share", "bgproc");
|
|
11
|
+
}
|
|
12
|
+
function getRegistryPath() {
|
|
13
|
+
return join(getDataDir(), "registry.json");
|
|
14
|
+
}
|
|
15
|
+
function getLogsDir() {
|
|
16
|
+
return join(getDataDir(), "logs");
|
|
17
|
+
}
|
|
18
|
+
function ensureDataDir() {
|
|
19
|
+
mkdirSync(getDataDir(), { recursive: true });
|
|
20
|
+
mkdirSync(getLogsDir(), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
function readRegistry() {
|
|
23
|
+
ensureDataDir();
|
|
24
|
+
const registryPath = getRegistryPath();
|
|
25
|
+
if (!existsSync(registryPath)) return {};
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(registryPath, "utf-8");
|
|
28
|
+
return JSON.parse(content);
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function writeRegistry(registry) {
|
|
34
|
+
ensureDataDir();
|
|
35
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2));
|
|
36
|
+
}
|
|
37
|
+
function addProcess(name, entry) {
|
|
38
|
+
const registry = readRegistry();
|
|
39
|
+
const existing = registry[name];
|
|
40
|
+
if (existing) {
|
|
41
|
+
if (isProcessRunning(existing.pid)) throw new Error(`Process '${name}' is already running (PID ${existing.pid}). Use 'bgproc stop ${name}' first.`);
|
|
42
|
+
const logPaths = getLogPaths(name);
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(logPaths.stdout)) unlinkSync(logPaths.stdout);
|
|
45
|
+
if (existsSync(logPaths.stderr)) unlinkSync(logPaths.stderr);
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
registry[name] = entry;
|
|
49
|
+
writeRegistry(registry);
|
|
50
|
+
}
|
|
51
|
+
function removeProcess(name) {
|
|
52
|
+
const registry = readRegistry();
|
|
53
|
+
delete registry[name];
|
|
54
|
+
writeRegistry(registry);
|
|
55
|
+
}
|
|
56
|
+
function getProcess(name) {
|
|
57
|
+
return readRegistry()[name];
|
|
58
|
+
}
|
|
59
|
+
function isProcessRunning(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function getLogPaths(name) {
|
|
68
|
+
return {
|
|
69
|
+
stdout: join(getLogsDir(), `${name}.stdout.log`),
|
|
70
|
+
stderr: join(getLogsDir(), `${name}.stderr.log`)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/commands/start.ts
|
|
76
|
+
const startCommand = defineCommand({
|
|
77
|
+
meta: {
|
|
78
|
+
name: "start",
|
|
79
|
+
description: "Start a background process\n\nUsage: bgproc start -n <name> [-t <seconds>] -- <command...>"
|
|
80
|
+
},
|
|
81
|
+
args: {
|
|
82
|
+
name: {
|
|
83
|
+
type: "string",
|
|
84
|
+
alias: "n",
|
|
85
|
+
description: "Name for the process",
|
|
86
|
+
required: true
|
|
87
|
+
},
|
|
88
|
+
timeout: {
|
|
89
|
+
type: "string",
|
|
90
|
+
alias: "t",
|
|
91
|
+
description: "Kill after N seconds"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
run({ args, rawArgs }) {
|
|
95
|
+
const name = args.name;
|
|
96
|
+
const timeout = args.timeout ? parseInt(args.timeout, 10) : void 0;
|
|
97
|
+
const dashDashIdx = rawArgs.indexOf("--");
|
|
98
|
+
const command = dashDashIdx >= 0 ? rawArgs.slice(dashDashIdx + 1) : [];
|
|
99
|
+
if (command.length === 0) {
|
|
100
|
+
console.error("Error: No command specified. Use: bgproc start -n <name> -- <command>");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
ensureDataDir();
|
|
104
|
+
const logPaths = getLogPaths(name);
|
|
105
|
+
const cwd = process.cwd();
|
|
106
|
+
const stdoutFd = openSync(logPaths.stdout, "a");
|
|
107
|
+
const stderrFd = openSync(logPaths.stderr, "a");
|
|
108
|
+
const child = spawn(command[0], command.slice(1), {
|
|
109
|
+
cwd,
|
|
110
|
+
detached: true,
|
|
111
|
+
stdio: [
|
|
112
|
+
"ignore",
|
|
113
|
+
stdoutFd,
|
|
114
|
+
stderrFd
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
child.unref();
|
|
118
|
+
const entry = {
|
|
119
|
+
pid: child.pid,
|
|
120
|
+
command,
|
|
121
|
+
cwd,
|
|
122
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
123
|
+
...timeout && {
|
|
124
|
+
timeout,
|
|
125
|
+
killAt: new Date(Date.now() + timeout * 1e3).toISOString()
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
addProcess(name, entry);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
try {
|
|
132
|
+
process.kill(child.pid, "SIGTERM");
|
|
133
|
+
} catch {}
|
|
134
|
+
console.error(err.message);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
if (timeout && child.pid) scheduleKill(child.pid, timeout, name);
|
|
138
|
+
console.log(JSON.stringify({
|
|
139
|
+
name,
|
|
140
|
+
pid: child.pid,
|
|
141
|
+
cwd,
|
|
142
|
+
command: command.join(" "),
|
|
143
|
+
...timeout && { killAt: entry.killAt }
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* Fork a small process to kill after timeout
|
|
149
|
+
* This survives the parent CLI exiting
|
|
150
|
+
*/
|
|
151
|
+
function scheduleKill(pid, seconds, name) {
|
|
152
|
+
spawn(process.execPath, ["-e", `
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
try {
|
|
155
|
+
process.kill(${pid}, 0); // check if alive
|
|
156
|
+
process.kill(${pid}, 'SIGTERM');
|
|
157
|
+
console.error('bgproc: ${name} killed after ${seconds}s timeout');
|
|
158
|
+
} catch {}
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}, ${seconds * 1e3});
|
|
161
|
+
`], {
|
|
162
|
+
detached: true,
|
|
163
|
+
stdio: "ignore"
|
|
164
|
+
}).unref();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/ports.ts
|
|
169
|
+
/**
|
|
170
|
+
* Detect listening ports for a given PID using lsof
|
|
171
|
+
*/
|
|
172
|
+
function detectPorts(pid) {
|
|
173
|
+
try {
|
|
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;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/commands/status.ts
|
|
213
|
+
const statusCommand = defineCommand({
|
|
214
|
+
meta: {
|
|
215
|
+
name: "status",
|
|
216
|
+
description: "Get status of a background process, including pid and open ports"
|
|
217
|
+
},
|
|
218
|
+
args: { name: {
|
|
219
|
+
type: "string",
|
|
220
|
+
alias: "n",
|
|
221
|
+
description: "Process name"
|
|
222
|
+
} },
|
|
223
|
+
run({ args, rawArgs }) {
|
|
224
|
+
const name = args.name ?? rawArgs[0];
|
|
225
|
+
if (!name) {
|
|
226
|
+
console.error("Error: Process name required");
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const entry = getProcess(name);
|
|
230
|
+
if (!entry) {
|
|
231
|
+
console.error(`Process '${name}' not found`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
const running = isProcessRunning(entry.pid);
|
|
235
|
+
const logPaths = getLogPaths(name);
|
|
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
|
+
}
|
|
244
|
+
const uptime = running ? formatUptime(new Date(entry.startedAt)) : null;
|
|
245
|
+
console.log(JSON.stringify({
|
|
246
|
+
name,
|
|
247
|
+
pid: entry.pid,
|
|
248
|
+
running,
|
|
249
|
+
ports,
|
|
250
|
+
port: ports[0] ?? null,
|
|
251
|
+
cwd: entry.cwd,
|
|
252
|
+
command: entry.command.join(" "),
|
|
253
|
+
startedAt: entry.startedAt,
|
|
254
|
+
uptime,
|
|
255
|
+
...entry.killAt && { killAt: entry.killAt }
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
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
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/logs.ts
|
|
268
|
+
const MAX_LOG_SIZE = 1 * 1024 * 1024;
|
|
269
|
+
const KEEP_SIZE = 512 * 1024;
|
|
270
|
+
/**
|
|
271
|
+
* Read the last N lines from a log file
|
|
272
|
+
*/
|
|
273
|
+
function readLastLines(path, n) {
|
|
274
|
+
if (!existsSync(path)) return [];
|
|
275
|
+
try {
|
|
276
|
+
const lines = readFileSync(path, "utf-8").split("\n");
|
|
277
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
278
|
+
return lines.slice(-n);
|
|
279
|
+
} catch {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Read entire log file
|
|
285
|
+
*/
|
|
286
|
+
function readLog(path) {
|
|
287
|
+
if (!existsSync(path)) return "";
|
|
288
|
+
try {
|
|
289
|
+
return readFileSync(path, "utf-8");
|
|
290
|
+
} catch {
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/commands/logs.ts
|
|
297
|
+
const logsCommand = defineCommand({
|
|
298
|
+
meta: {
|
|
299
|
+
name: "logs",
|
|
300
|
+
description: "View logs for a background process"
|
|
301
|
+
},
|
|
302
|
+
args: {
|
|
303
|
+
name: {
|
|
304
|
+
type: "string",
|
|
305
|
+
alias: "n",
|
|
306
|
+
description: "Process name"
|
|
307
|
+
},
|
|
308
|
+
tail: {
|
|
309
|
+
type: "string",
|
|
310
|
+
alias: "t",
|
|
311
|
+
description: "Number of lines to show (default: 100)"
|
|
312
|
+
},
|
|
313
|
+
follow: {
|
|
314
|
+
type: "boolean",
|
|
315
|
+
alias: "f",
|
|
316
|
+
description: "Follow log output (tail -f)"
|
|
317
|
+
},
|
|
318
|
+
errors: {
|
|
319
|
+
type: "boolean",
|
|
320
|
+
alias: "e",
|
|
321
|
+
description: "Show only stderr"
|
|
322
|
+
},
|
|
323
|
+
all: {
|
|
324
|
+
type: "boolean",
|
|
325
|
+
alias: "a",
|
|
326
|
+
description: "Show all logs (no line limit)"
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
run({ args, rawArgs }) {
|
|
330
|
+
const name = args.name ?? rawArgs[0];
|
|
331
|
+
if (!name) {
|
|
332
|
+
console.error("Error: Process name required");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
if (!getProcess(name)) {
|
|
336
|
+
console.error(`Process '${name}' not found`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
const logPaths = getLogPaths(name);
|
|
340
|
+
const logPath = args.errors ? logPaths.stderr : logPaths.stdout;
|
|
341
|
+
if (!existsSync(logPath)) {
|
|
342
|
+
console.error(`No logs found for '${name}'`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
if (args.follow) {
|
|
346
|
+
const tail = spawn("tail", ["-f", logPath], { stdio: "inherit" });
|
|
347
|
+
process.on("SIGINT", () => {
|
|
348
|
+
tail.kill();
|
|
349
|
+
process.exit(0);
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (args.all) {
|
|
354
|
+
const content = readLog(logPath);
|
|
355
|
+
process.stdout.write(content);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const output = readLastLines(logPath, parseInt(args.tail ?? "100", 10));
|
|
359
|
+
console.log(output.join("\n"));
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/commands/stop.ts
|
|
365
|
+
const stopCommand = defineCommand({
|
|
366
|
+
meta: {
|
|
367
|
+
name: "stop",
|
|
368
|
+
description: "Stop a background process"
|
|
369
|
+
},
|
|
370
|
+
args: {
|
|
371
|
+
name: {
|
|
372
|
+
type: "string",
|
|
373
|
+
alias: "n",
|
|
374
|
+
description: "Process name"
|
|
375
|
+
},
|
|
376
|
+
force: {
|
|
377
|
+
type: "boolean",
|
|
378
|
+
alias: "f",
|
|
379
|
+
description: "Force kill (SIGKILL instead of SIGTERM)"
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
run({ args, rawArgs }) {
|
|
383
|
+
const name = args.name ?? rawArgs[0];
|
|
384
|
+
if (!name) {
|
|
385
|
+
console.error("Error: Process name required");
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
const entry = getProcess(name);
|
|
389
|
+
if (!entry) {
|
|
390
|
+
console.error(`Process '${name}' not found`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
const wasRunning = isProcessRunning(entry.pid);
|
|
394
|
+
if (!wasRunning) process.stderr.write(`Warning: Process '${name}' (PID ${entry.pid}) was already dead\n`);
|
|
395
|
+
else {
|
|
396
|
+
const signal = args.force ? "SIGKILL" : "SIGTERM";
|
|
397
|
+
try {
|
|
398
|
+
process.kill(entry.pid, signal);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
console.error(`Failed to kill process: ${err.message}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
removeProcess(name);
|
|
405
|
+
console.log(JSON.stringify({
|
|
406
|
+
name,
|
|
407
|
+
pid: entry.pid,
|
|
408
|
+
stopped: true,
|
|
409
|
+
wasRunning,
|
|
410
|
+
signal: wasRunning ? args.force ? "SIGKILL" : "SIGTERM" : null
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/commands/list.ts
|
|
417
|
+
const listCommand = defineCommand({
|
|
418
|
+
meta: {
|
|
419
|
+
name: "list",
|
|
420
|
+
description: "List all background processes"
|
|
421
|
+
},
|
|
422
|
+
args: { cwd: {
|
|
423
|
+
type: "string",
|
|
424
|
+
alias: "c",
|
|
425
|
+
description: "Filter by cwd (no arg = current directory)"
|
|
426
|
+
} },
|
|
427
|
+
run({ args }) {
|
|
428
|
+
const registry = readRegistry();
|
|
429
|
+
let cwdFilter;
|
|
430
|
+
if (args.cwd !== void 0) cwdFilter = args.cwd === "" ? process.cwd() : path.resolve(args.cwd);
|
|
431
|
+
const entries = Object.entries(registry).filter(([_, entry]) => {
|
|
432
|
+
if (cwdFilter && entry.cwd !== cwdFilter) return false;
|
|
433
|
+
return true;
|
|
434
|
+
}).map(([name, entry]) => {
|
|
435
|
+
const running = isProcessRunning(entry.pid);
|
|
436
|
+
let ports = [];
|
|
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
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
name,
|
|
446
|
+
pid: entry.pid,
|
|
447
|
+
running,
|
|
448
|
+
port: ports[0] ?? null,
|
|
449
|
+
cwd: entry.cwd,
|
|
450
|
+
command: entry.command.join(" "),
|
|
451
|
+
startedAt: entry.startedAt
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/commands/clean.ts
|
|
460
|
+
const cleanCommand = defineCommand({
|
|
461
|
+
meta: {
|
|
462
|
+
name: "clean",
|
|
463
|
+
description: "Remove dead processes and their logs"
|
|
464
|
+
},
|
|
465
|
+
args: {
|
|
466
|
+
name: {
|
|
467
|
+
type: "string",
|
|
468
|
+
alias: "n",
|
|
469
|
+
description: "Process name"
|
|
470
|
+
},
|
|
471
|
+
all: {
|
|
472
|
+
type: "boolean",
|
|
473
|
+
alias: "a",
|
|
474
|
+
description: "Clean all dead processes"
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
run({ args, rawArgs }) {
|
|
478
|
+
const registry = readRegistry();
|
|
479
|
+
const cleaned = [];
|
|
480
|
+
const name = args.name ?? rawArgs[0];
|
|
481
|
+
if (args.all) {
|
|
482
|
+
for (const [procName, entry] of Object.entries(registry)) if (!isProcessRunning(entry.pid)) {
|
|
483
|
+
cleanProcess(procName);
|
|
484
|
+
cleaned.push(procName);
|
|
485
|
+
}
|
|
486
|
+
} else if (name) {
|
|
487
|
+
const entry = registry[name];
|
|
488
|
+
if (!entry) {
|
|
489
|
+
console.error(`Process '${name}' not found`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
if (isProcessRunning(entry.pid)) {
|
|
493
|
+
console.error(`Process '${name}' is still running. Use 'bgproc stop ${name}' first.`);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
cleanProcess(name);
|
|
497
|
+
cleaned.push(name);
|
|
498
|
+
} else {
|
|
499
|
+
console.error("Specify a process name or use --all");
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
console.log(JSON.stringify({
|
|
503
|
+
cleaned,
|
|
504
|
+
count: cleaned.length
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
function cleanProcess(name) {
|
|
509
|
+
removeProcess(name);
|
|
510
|
+
const logPaths = getLogPaths(name);
|
|
511
|
+
try {
|
|
512
|
+
if (existsSync(logPaths.stdout)) unlinkSync(logPaths.stdout);
|
|
513
|
+
if (existsSync(logPaths.stderr)) unlinkSync(logPaths.stderr);
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/cli.ts
|
|
519
|
+
await runMain(defineCommand({
|
|
520
|
+
meta: {
|
|
521
|
+
name: "bgproc",
|
|
522
|
+
version: "0.1.0",
|
|
523
|
+
description: "Simple process manager for agents. All commands output JSON to stdout.\nExample: bgproc start -n myserver -- npm run dev"
|
|
524
|
+
},
|
|
525
|
+
subCommands: {
|
|
526
|
+
start: startCommand,
|
|
527
|
+
status: statusCommand,
|
|
528
|
+
logs: logsCommand,
|
|
529
|
+
stop: stopCommand,
|
|
530
|
+
list: listCommand,
|
|
531
|
+
clean: cleanCommand
|
|
532
|
+
}
|
|
533
|
+
}));
|
|
534
|
+
|
|
535
|
+
//#endregion
|
|
536
|
+
export { };
|
|
537
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +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":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bgproc",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Simple process manager for agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"background",
|
|
7
|
+
"cli",
|
|
8
|
+
"dev-server",
|
|
9
|
+
"process"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Matt Kane",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/ascorbic/bgproc.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/ascorbic/bgproc#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/ascorbic/bgproc/issues"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"bgproc": "dist/cli.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
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
|
+
"dependencies": {
|
|
40
|
+
"citty": "^0.1.6"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
44
|
+
"@changesets/cli": "^2.29.8",
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"oxfmt": "^0.27.0",
|
|
47
|
+
"oxlint": "^1.0.0",
|
|
48
|
+
"oxlint-tsgolint": "^0.11.3",
|
|
49
|
+
"tsdown": "^0.20.1",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vitest": "^4.0.18"
|
|
52
|
+
}
|
|
53
|
+
}
|