bm2 1.0.19 → 1.0.22

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 CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  # ⚡ BM2
2
3
 
3
4
  **A blazing-fast, full-featured process manager built entirely on Bun native APIs.**
@@ -47,6 +48,20 @@ The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.
47
48
  - [WebSocket API](#websocket-api)
48
49
  - [Prometheus and Grafana Integration](#prometheus-and-grafana-integration)
49
50
  - [Programmatic API](#programmatic-api)
51
+ - [Quick Start](#programmatic-quick-start)
52
+ - [Connection Lifecycle](#connection-lifecycle)
53
+ - [Process Management](#programmatic-process-management)
54
+ - [Introspection](#introspection)
55
+ - [Logs](#logs)
56
+ - [Monitoring and Metrics](#programmatic-monitoring-and-metrics)
57
+ - [Persistence](#persistence)
58
+ - [Dashboard Control](#dashboard-control)
59
+ - [Module Management](#module-management)
60
+ - [Daemon Lifecycle](#daemon-lifecycle)
61
+ - [Low-Level Transport](#low-level-transport)
62
+ - [Events](#events)
63
+ - [Error Handling](#error-handling)
64
+ - [Direct ProcessManager Usage](#direct-processmanager-usage)
50
65
  - [Architecture](#architecture)
51
66
  - [Comparison with PM2](#comparison-with-pm2)
52
67
  - [Recipes and Examples](#recipes-and-examples)
@@ -1258,10 +1273,475 @@ groups:
1258
1273
 
1259
1274
  ## Programmatic API
1260
1275
 
1261
- BM2 can be used as a library in your own Bun applications:
1276
+ BM2 exposes two levels of programmatic access. The `BM2` client class communicates with the daemon over its Unix socket, giving you the same capabilities as the CLI from within any Bun application. For in-process usage without a daemon, you can use the `ProcessManager` class directly.
1277
+
1278
+ ### Programmatic Quick Start
1279
+
1280
+ ```ts
1281
+ import BM2 from "bm2";
1282
+
1283
+ const bm2 = new BM2();
1284
+ await bm2.connect();
1285
+
1286
+ // Start a clustered application
1287
+ await bm2.start({
1288
+ script: "./server.ts",
1289
+ name: "api",
1290
+ instances: 4,
1291
+ execMode: "cluster",
1292
+ port: 3000,
1293
+ env: { NODE_ENV: "production" },
1294
+ });
1295
+
1296
+ // List all processes
1297
+ const processes = await bm2.list();
1298
+ console.log(processes);
1299
+
1300
+ // Stream metrics every 2 seconds
1301
+ bm2.on("metrics", (snapshot) => {
1302
+ console.log(`CPU: ${snapshot.system.cpu}% Memory: ${snapshot.system.memory}%`);
1303
+ });
1304
+ bm2.startPolling(2000);
1305
+
1306
+ // Graceful shutdown
1307
+ bm2.stopPolling();
1308
+ await bm2.disconnect();
1309
+ ```
1310
+
1311
+ ---
1312
+
1313
+ ### Connection Lifecycle
1314
+
1315
+ #### `bm2.connect(): Promise<BM2>`
1316
+
1317
+ Connect to the BM2 daemon. If the daemon is not running, it is spawned automatically and the method waits up to 5 seconds for it to become responsive. Returns the `BM2` instance for chaining.
1318
+
1319
+ ```ts
1320
+ const bm2 = new BM2();
1321
+ await bm2.connect();
1322
+ console.log(`Connected to daemon PID ${bm2.daemonPid}`);
1323
+ ```
1324
+
1325
+ #### `bm2.disconnect(): Promise<void>`
1326
+
1327
+ Disconnect from the daemon. This stops any internal polling timers but does not kill the daemon — all managed processes continue running.
1328
+
1329
+ ```ts
1330
+ await bm2.disconnect();
1331
+ console.log(bm2.connected); // false
1332
+ ```
1333
+
1334
+ #### `bm2.connected: boolean`
1335
+
1336
+ Read-only property indicating whether the client believes the daemon is reachable.
1337
+
1338
+ #### `bm2.daemonPid: number | null`
1339
+
1340
+ Read-only property containing the PID of the daemon process, or `null` if unknown.
1341
+
1342
+ ---
1343
+
1344
+ ### Programmatic Process Management
1345
+
1346
+ #### `bm2.start(options: StartOptions): Promise<ProcessState[]>`
1347
+
1348
+ Start a new process or process group. The `script` path is automatically resolved to an absolute path. Returns the array of `ProcessState` objects for the started instances.
1349
+
1350
+ ```ts
1351
+ const procs = await bm2.start({
1352
+ script: "./worker.ts",
1353
+ name: "worker",
1354
+ instances: 2,
1355
+ env: { QUEUE: "emails" },
1356
+ maxMemoryRestart: "256M",
1357
+ });
1358
+ console.log(`Started ${procs.length} instances`);
1359
+ ```
1360
+
1361
+ The `StartOptions` object accepts all the same fields documented in the [Process Options](#process-options) configuration reference.
1362
+
1363
+ #### `bm2.startEcosystem(config: EcosystemConfig): Promise<ProcessState[]>`
1364
+
1365
+ Start an entire ecosystem configuration. All script paths within the config are resolved to absolute paths before being sent to the daemon.
1366
+
1367
+ ```ts
1368
+ const procs = await bm2.startEcosystem({
1369
+ apps: [
1370
+ { script: "./api.ts", name: "api", instances: 4, port: 3000 },
1371
+ { script: "./worker.ts", name: "worker", instances: 2 },
1372
+ ],
1373
+ });
1374
+ ```
1375
+
1376
+ #### `bm2.stop(target?: string | number): Promise<ProcessState[]>`
1377
+
1378
+ Stop one or more processes. The `target` can be a process name, numeric ID, namespace, or `"all"`. Defaults to `"all"` when omitted.
1379
+
1380
+ ```ts
1381
+ await bm2.stop("api"); // Stop by name
1382
+ await bm2.stop(0); // Stop by ID
1383
+ await bm2.stop(); // Stop all
1384
+ ```
1385
+
1386
+ #### `bm2.restart(target?: string | number): Promise<ProcessState[]>`
1387
+
1388
+ Hard restart one or more processes. The process is fully stopped and then re-spawned.
1389
+
1390
+ ```ts
1391
+ await bm2.restart("api");
1392
+ await bm2.restart(); // Restart all
1393
+ ```
1394
+
1395
+ #### `bm2.reload(target?: string | number): Promise<ProcessState[]>`
1396
+
1397
+ Graceful zero-downtime reload. New instances are started before old ones are stopped, ensuring no dropped requests. Ideal for deploying new code.
1398
+
1399
+ ```ts
1400
+ await bm2.reload("api");
1401
+ await bm2.reload(); // Reload all
1402
+ ```
1403
+
1404
+ #### `bm2.delete(target?: string | number): Promise<ProcessState[]>`
1405
+
1406
+ Stop and remove one or more processes from BM2's management entirely.
1407
+
1408
+ ```ts
1409
+ await bm2.delete("api");
1410
+ await bm2.delete(); // Delete all
1411
+ ```
1412
+
1413
+ #### `bm2.scale(target: string | number, count: number): Promise<ProcessState[]>`
1414
+
1415
+ Scale a process group to the specified number of instances. When scaling up, new instances inherit the configuration of existing ones. When scaling down, the highest-numbered instances are removed first.
1416
+
1417
+ ```ts
1418
+ await bm2.scale("api", 8); // Scale up to 8 instances
1419
+ await bm2.scale("api", 2); // Scale down to 2 instances
1420
+ ```
1421
+
1422
+ #### `bm2.sendSignal(target: string | number, signal: string): Promise<void>`
1423
+
1424
+ Send an OS signal to a managed process.
1425
+
1426
+ ```ts
1427
+ await bm2.sendSignal("api", "SIGUSR2");
1428
+ await bm2.sendSignal(0, "SIGHUP");
1429
+ ```
1430
+
1431
+ #### `bm2.reset(target?: string | number): Promise<ProcessState[]>`
1432
+
1433
+ Reset the restart counter for one or more processes. Defaults to `"all"`.
1434
+
1435
+ ```ts
1436
+ await bm2.reset("api");
1437
+ await bm2.reset(); // Reset all
1438
+ ```
1439
+
1440
+ ---
1441
+
1442
+ ### Introspection
1443
+
1444
+ #### `bm2.list(): Promise<ProcessState[]>`
1445
+
1446
+ List all managed processes with their current state.
1447
+
1448
+ ```ts
1449
+ const processes = await bm2.list();
1450
+ for (const proc of processes) {
1451
+ console.log(`${proc.name} [${proc.status}] PID=${proc.pid} CPU=${proc.cpu}%`);
1452
+ }
1453
+ ```
1454
+
1455
+ #### `bm2.describe(target: string | number): Promise<ProcessState[]>`
1456
+
1457
+ Get detailed information about a specific process or process group.
1458
+
1459
+ ```ts
1460
+ const details = await bm2.describe("api");
1461
+ console.log(details[0]);
1462
+ ```
1463
+
1464
+ ---
1262
1465
 
1466
+ ### Logs
1467
+
1468
+ #### `bm2.logs(target?: string | number, lines?: number): Promise<Array<{ name: string; id: number; out: string; err: string }>>`
1469
+
1470
+ Retrieve recent log lines for one or all processes. Defaults to `"all"` with `20` lines.
1471
+
1472
+ ```ts
1473
+ const logs = await bm2.logs("api", 100);
1474
+ for (const entry of logs) {
1475
+ console.log(`[${entry.name}] stdout:\n${entry.out}`);
1476
+ if (entry.err) console.error(`[${entry.name}] stderr:\n${entry.err}`);
1477
+ }
1478
+ ```
1479
+
1480
+ #### `bm2.flush(target?: string | number): Promise<void>`
1481
+
1482
+ Truncate log files for one or all processes.
1483
+
1484
+ ```ts
1485
+ await bm2.flush("api"); // Flush logs for "api"
1486
+ await bm2.flush(); // Flush all logs
1487
+ ```
1488
+
1489
+ ---
1490
+
1491
+ ### Programmatic Monitoring and Metrics
1492
+
1493
+ #### `bm2.metrics(): Promise<MetricSnapshot>`
1494
+
1495
+ Take a single metrics snapshot containing process-level and system-level telemetry.
1496
+
1497
+ ```ts
1498
+ const snapshot = await bm2.metrics();
1499
+ console.log(`System CPU: ${snapshot.system.cpu}%`);
1500
+ for (const proc of snapshot.processes) {
1501
+ console.log(` ${proc.name}: ${proc.memory} bytes, ${proc.cpu}% CPU`);
1502
+ }
1503
+ ```
1504
+
1505
+ #### `bm2.metricsHistory(seconds?: number): Promise<MetricSnapshot[]>`
1506
+
1507
+ Retrieve historical metric snapshots from the daemon's in-memory ring buffer. The `seconds` parameter controls the look-back window and defaults to `300` (5 minutes). The daemon retains up to 1 hour of per-second snapshots.
1508
+
1509
+ ```ts
1510
+ const history = await bm2.metricsHistory(600); // Last 10 minutes
1511
+ console.log(`Got ${history.length} snapshots`);
1512
+ ```
1513
+
1514
+ #### `bm2.prometheus(): Promise<string>`
1515
+
1516
+ Get the current metrics formatted as a Prometheus exposition text string.
1517
+
1518
+ ```ts
1519
+ const text = await bm2.prometheus();
1520
+ console.log(text);
1521
+ // # HELP bm2_process_cpu CPU usage percentage
1522
+ // # TYPE bm2_process_cpu gauge
1523
+ // bm2_process_cpu{name="api-0",id="0"} 1.2
1524
+ // ...
1525
+ ```
1526
+
1527
+ #### `bm2.startPolling(intervalMs?: number): void`
1528
+
1529
+ Start polling the daemon for metrics at a fixed interval and emitting `"metrics"` events. Defaults to `2000` ms. Calling this again replaces the existing polling timer.
1530
+
1531
+ ```ts
1532
+ bm2.on("metrics", (snapshot) => {
1533
+ console.log(`${snapshot.processes.length} processes running`);
1534
+ });
1535
+ bm2.startPolling(1000); // Poll every second
1263
1536
  ```
1264
- // app.ts
1537
+
1538
+ #### `bm2.stopPolling(): void`
1539
+
1540
+ Stop the metrics polling loop.
1541
+
1542
+ ```ts
1543
+ bm2.stopPolling();
1544
+ ```
1545
+
1546
+ ---
1547
+
1548
+ ### Persistence
1549
+
1550
+ #### `bm2.save(): Promise<void>`
1551
+
1552
+ Persist the current process list to `~/.bm2/dump.json` so it can be restored later.
1553
+
1554
+ ```ts
1555
+ await bm2.save();
1556
+ ```
1557
+
1558
+ #### `bm2.resurrect(): Promise<ProcessState[]>`
1559
+
1560
+ Restore previously saved processes from `~/.bm2/dump.json`.
1561
+
1562
+ ```ts
1563
+ const restored = await bm2.resurrect();
1564
+ console.log(`Restored ${restored.length} processes`);
1565
+ ```
1566
+
1567
+ ---
1568
+
1569
+ ### Dashboard Control
1570
+
1571
+ #### `bm2.dashboard(port?: number, metricsPort?: number): Promise<{ port: number; metricsPort: number }>`
1572
+
1573
+ Start the web dashboard. Defaults to port `9615` for the dashboard and `9616` for the Prometheus metrics endpoint.
1574
+
1575
+ ```ts
1576
+ const { port, metricsPort } = await bm2.dashboard(8080, 8081);
1577
+ console.log(`Dashboard: http://localhost:${port}`);
1578
+ console.log(`Metrics: http://localhost:${metricsPort}/metrics`);
1579
+ ```
1580
+
1581
+ #### `bm2.dashboardStop(): Promise<void>`
1582
+
1583
+ Stop the web dashboard.
1584
+
1585
+ ```ts
1586
+ await bm2.dashboardStop();
1587
+ ```
1588
+
1589
+ ---
1590
+
1591
+ ### Module Management
1592
+
1593
+ #### `bm2.moduleInstall(nameOrPath: string): Promise<{ path: string }>`
1594
+
1595
+ Install a BM2 module from a git URL, local path, or npm package name.
1596
+
1597
+ ```ts
1598
+ const result = await bm2.moduleInstall("bm2-prometheus-pushgateway");
1599
+ console.log(`Installed to ${result.path}`);
1600
+ ```
1601
+
1602
+ #### `bm2.moduleUninstall(name: string): Promise<void>`
1603
+
1604
+ Uninstall a BM2 module.
1605
+
1606
+ ```ts
1607
+ await bm2.moduleUninstall("bm2-prometheus-pushgateway");
1608
+ ```
1609
+
1610
+ #### `bm2.moduleList(): Promise<Array<{ name: string; version: string }>>`
1611
+
1612
+ List all installed modules.
1613
+
1614
+ ```ts
1615
+ const modules = await bm2.moduleList();
1616
+ for (const mod of modules) {
1617
+ console.log(`${mod.name}@${mod.version}`);
1618
+ }
1619
+ ```
1620
+
1621
+ ---
1622
+
1623
+ ### Daemon Lifecycle
1624
+
1625
+ #### `bm2.ping(): Promise<{ pid: number; uptime: number }>`
1626
+
1627
+ Ping the daemon and return its PID and uptime in milliseconds.
1628
+
1629
+ ```ts
1630
+ const info = await bm2.ping();
1631
+ console.log(`Daemon PID ${info.pid}, up for ${Math.round(info.uptime / 1000)}s`);
1632
+ ```
1633
+
1634
+ #### `bm2.kill(): Promise<void>`
1635
+
1636
+ Kill the daemon and all managed processes. Cleans up the socket and PID files. The daemon connection will not respond after this call, which is expected.
1637
+
1638
+ ```ts
1639
+ await bm2.kill();
1640
+ console.log(bm2.connected); // false
1641
+ ```
1642
+
1643
+ #### `bm2.daemonReload(): Promise<string>`
1644
+
1645
+ Reload the daemon server itself without killing managed processes.
1646
+
1647
+ ```ts
1648
+ const result = await bm2.daemonReload();
1649
+ console.log(result);
1650
+ ```
1651
+
1652
+ ---
1653
+
1654
+ ### Low-Level Transport
1655
+
1656
+ #### `bm2.send(message: DaemonMessage): Promise<DaemonResponse>`
1657
+
1658
+ Send an arbitrary message to the daemon over the Unix socket and return the raw response. This is useful for custom command types, future extensions, or direct daemon interaction.
1659
+
1660
+ ```ts
1661
+ const response = await bm2.send({ type: "ping" });
1662
+ console.log(response);
1663
+ // { success: true, data: { pid: 12345, uptime: 60000 }, id: "abc123" }
1664
+ ```
1665
+
1666
+ Messages are JSON objects with a `type` field for routing and an optional `id` field for request-response correlation (auto-generated if omitted). The `data` field carries command-specific payload.
1667
+
1668
+ ---
1669
+
1670
+ ### Events
1671
+
1672
+ The `BM2` class extends `EventEmitter` and emits the following typed events:
1673
+
1674
+ | Event | Payload | Description |
1675
+ |---|---|---|
1676
+ | `daemon:connected` | — | Daemon connection established |
1677
+ | `daemon:disconnected` | — | Client disconnected from daemon |
1678
+ | `daemon:launched` | `pid: number` | Daemon was spawned by this client |
1679
+ | `daemon:killed` | — | Daemon was killed via `kill()` |
1680
+ | `error` | `error: Error` | Transport or polling error |
1681
+ | `process:start` | `processes: ProcessState[]` | Process(es) started |
1682
+ | `process:stop` | `processes: ProcessState[]` | Process(es) stopped |
1683
+ | `process:restart` | `processes: ProcessState[]` | Process(es) restarted |
1684
+ | `process:reload` | `processes: ProcessState[]` | Process(es) reloaded |
1685
+ | `process:delete` | `processes: ProcessState[]` | Process(es) deleted |
1686
+ | `process:scale` | `processes: ProcessState[]` | Process group scaled |
1687
+ | `metrics` | `snapshot: MetricSnapshot` | Metrics snapshot received |
1688
+ | `log:data` | `logs: Array<{ name, id, out, err }>` | Log data retrieved |
1689
+
1690
+ ```ts
1691
+ const bm2 = new BM2();
1692
+
1693
+ bm2.on("daemon:connected", () => console.log("Connected!"));
1694
+ bm2.on("daemon:disconnected", () => console.log("Disconnected"));
1695
+ bm2.on("process:start", (procs) => {
1696
+ console.log("Started:", procs.map((p) => p.name).join(", "));
1697
+ });
1698
+ bm2.on("process:stop", (procs) => {
1699
+ console.log("Stopped:", procs.map((p) => p.name).join(", "));
1700
+ });
1701
+ bm2.on("error", (err) => console.error("BM2 error:", err.message));
1702
+ bm2.on("metrics", (snapshot) => {
1703
+ console.log(`${snapshot.processes.length} processes, system CPU ${snapshot.system.cpu}%`);
1704
+ });
1705
+
1706
+ await bm2.connect();
1707
+ ```
1708
+
1709
+ ---
1710
+
1711
+ ### Error Handling
1712
+
1713
+ All methods that communicate with the daemon throw a `BM2Error` when the daemon returns a failure response. The error includes the command that failed and the full daemon response for inspection.
1714
+
1715
+ ```ts
1716
+ import { BM2Error } from "bm2";
1717
+
1718
+ try {
1719
+ await bm2.describe("nonexistent");
1720
+ } catch (err) {
1721
+ if (err instanceof BM2Error) {
1722
+ console.error(`Command "${err.command}" failed: ${err.message}`);
1723
+ console.error("Full response:", err.response);
1724
+ }
1725
+ }
1726
+ ```
1727
+
1728
+ Transport-level errors (daemon unreachable, socket closed) throw standard `Error` instances.
1729
+
1730
+ #### `BM2Error` Properties
1731
+
1732
+ | Property | Type | Description |
1733
+ |---|---|---|
1734
+ | `message` | `string` | Human-readable error message |
1735
+ | `command` | `string` | The daemon command type that failed |
1736
+ | `response` | `DaemonResponse` | The full response object from the daemon |
1737
+
1738
+ ---
1739
+
1740
+ ### Direct ProcessManager Usage
1741
+
1742
+ For in-process usage without a running daemon, you can use the `ProcessManager` class directly. This is useful for embedding BM2 into your own application or for custom tooling.
1743
+
1744
+ ```ts
1265
1745
  import { ProcessManager } from "bm2/process-manager";
1266
1746
  import { Dashboard } from "bm2/dashboard";
1267
1747
 
@@ -1308,6 +1788,8 @@ await pm.resurrect();
1308
1788
  await pm.stopAll();
1309
1789
  ```
1310
1790
 
1791
+ The `ProcessManager` provides the same process management capabilities but runs in-process rather than communicating with a daemon. Use the `BM2` client class for the standard daemon-based workflow, and `ProcessManager` when you need direct, embedded control.
1792
+
1311
1793
  ---
1312
1794
 
1313
1795
  ## Architecture
@@ -1476,6 +1958,64 @@ bun install
1476
1958
  bm2 reload all
1477
1959
  ```
1478
1960
 
1961
+ ### Programmatic Monitoring Service
1962
+
1963
+ ```ts
1964
+ import BM2 from "bm2";
1965
+
1966
+ const bm2 = new BM2();
1967
+ await bm2.connect();
1968
+
1969
+ // Alert when any process uses more than 512 MB
1970
+ bm2.on("metrics", (snapshot) => {
1971
+ for (const proc of snapshot.processes) {
1972
+ if (proc.memory > 512 * 1024 * 1024) {
1973
+ console.warn(`⚠️ ${proc.name} using ${Math.round(proc.memory / 1024 / 1024)} MB`);
1974
+ }
1975
+ }
1976
+ });
1977
+
1978
+ bm2.startPolling(5000);
1979
+
1980
+ // Keep running
1981
+ process.on("SIGINT", async () => {
1982
+ bm2.stopPolling();
1983
+ await bm2.disconnect();
1984
+ process.exit(0);
1985
+ });
1986
+ ```
1987
+
1988
+ ### Programmatic Deploy Pipeline
1989
+
1990
+ ```ts
1991
+ import BM2 from "bm2";
1992
+
1993
+ const bm2 = new BM2();
1994
+ await bm2.connect();
1995
+
1996
+ // Deploy new code, then reload
1997
+ console.log("Reloading all processes...");
1998
+ const reloaded = await bm2.reload("all");
1999
+ console.log(`Reloaded ${reloaded.length} processes`);
2000
+
2001
+ // Verify everything is healthy
2002
+ const processes = await bm2.list();
2003
+ const allOnline = processes.every((p) => p.status === "online");
2004
+
2005
+ if (allOnline) {
2006
+ console.log("✅ All processes online");
2007
+ await bm2.save();
2008
+ } else {
2009
+ console.error("❌ Some processes failed to come online");
2010
+ const failed = processes.filter((p) => p.status !== "online");
2011
+ for (const p of failed) {
2012
+ console.error(` ${p.name}: ${p.status}`);
2013
+ }
2014
+ }
2015
+
2016
+ await bm2.disconnect();
2017
+ ```
2018
+
1479
2019
  ---
1480
2020
 
1481
2021
  ## Troubleshooting
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "bm2",
3
- "version": "1.0.19",
3
+ "version": "1.0.22",
4
4
  "description": "A blazing-fast, full-featured process manager built entirely on Bun native APIs. The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.",
5
- "main": "src/index.ts",
6
- "module": "src/index.ts",
7
- "types": "src/index.ts",
5
+ "main": "src/api.ts",
6
+ "module": "src/api.ts",
7
+ "types": "src/api.ts",
8
+ "exports": {
9
+ ".": "./src/api.ts",
10
+ "./cli": "./src/index.ts"
11
+ },
8
12
  "bin": {
9
13
  "bm2": "./src/index.ts"
10
14
  },
@@ -42,7 +46,7 @@
42
46
  "email": "hello@maxxpainn.com",
43
47
  "url": "https://maxxpainn.com"
44
48
  },
45
- "license": "MIT",
49
+ "license": "GPL-3.0",
46
50
  "repository": {
47
51
  "type": "git",
48
52
  "url": "git+https://github.com/bun-bm2/bm2.git"
package/src/api.ts ADDED
@@ -0,0 +1,598 @@
1
+ /**
2
+ * BM2 — Bun Process Manager
3
+ * Programmatic API
4
+ *
5
+ * Usage:
6
+ * import BM2 from "bm2";
7
+ * const bm2 = new BM2();
8
+ * await bm2.connect();
9
+ * const list = await bm2.list();
10
+ * await bm2.start({ script: "./app.ts", name: "my-app" });
11
+ * await bm2.disconnect();
12
+ *
13
+ * License: GPL-3.0-only
14
+ * Author: Zak <zak@maxxpainn.com>
15
+ */
16
+
17
+ import { EventEmitter } from "events";
18
+ import { existsSync, readFileSync, unlinkSync } from "fs";
19
+ import { join } from "path";
20
+ import { resolve } from "path";
21
+ import {
22
+ DAEMON_SOCKET,
23
+ DAEMON_PID_FILE,
24
+ BM2_HOME,
25
+ DASHBOARD_PORT,
26
+ METRICS_PORT,
27
+ DAEMON_OUT_LOG_FILE,
28
+ DAEMON_ERR_LOG_FILE,
29
+ } from "./constants";
30
+ import { ensureDirs, generateId } from "./utils";
31
+ import type {
32
+ DaemonMessage,
33
+ DaemonResponse,
34
+ StartOptions,
35
+ EcosystemConfig,
36
+ ProcessState,
37
+ MetricSnapshot,
38
+ ProcessStatus,
39
+ } from "./types";
40
+
41
+ // ────────────────────────────────────────────────────────────────────────────
42
+ // Bus event types emitted by BM2
43
+ // ────────────────────────────────────────────────────────────────────────────
44
+
45
+ export interface BM2Events {
46
+ /** Daemon successfully connected */
47
+ "daemon:connected": [];
48
+ /** Daemon connection lost */
49
+ "daemon:disconnected": [];
50
+ /** Daemon launched by this client */
51
+ "daemon:launched": [pid: number];
52
+ /** Daemon killed */
53
+ "daemon:killed": [];
54
+ /** Error on the transport layer */
55
+ "error": [error: Error];
56
+ /** Process started */
57
+ "process:start": [processes: ProcessState[]];
58
+ /** Process stopped */
59
+ "process:stop": [processes: ProcessState[]];
60
+ /** Process restarted */
61
+ "process:restart": [processes: ProcessState[]];
62
+ /** Process reloaded */
63
+ "process:reload": [processes: ProcessState[]];
64
+ /** Process deleted */
65
+ "process:delete": [processes: ProcessState[]];
66
+ /** Process scaled */
67
+ "process:scale": [processes: ProcessState[]];
68
+ /** Metrics snapshot received */
69
+ "metrics": [snapshot: MetricSnapshot];
70
+ /** Log data received */
71
+ "log:data": [logs: Array<{ name: string; id: number; out: string; err: string }>];
72
+ }
73
+
74
+ // ────────────────────────────────────────────────────────────────────────────
75
+ // Main API class
76
+ // ────────────────────────────────────────────────────────────────────────────
77
+
78
+ export class BM2 extends EventEmitter<BM2Events> {
79
+ private _connected: boolean = false;
80
+ private _daemonPid: number | null = null;
81
+ private _pollTimer: ReturnType<typeof setInterval> | null = null;
82
+
83
+ /** Whether the client believes the daemon is reachable. */
84
+ get connected(): boolean {
85
+ return this._connected;
86
+ }
87
+
88
+ /** PID of the daemon process (if known). */
89
+ get daemonPid(): number | null {
90
+ return this._daemonPid;
91
+ }
92
+
93
+ // ──────────────────────────── lifecycle ────────────────────────────────
94
+
95
+ /**
96
+ * Connect to the BM2 daemon.
97
+ * If the daemon is not running it will be spawned automatically
98
+ * (same behaviour as the CLI).
99
+ */
100
+ async connect(): Promise<this> {
101
+ ensureDirs();
102
+
103
+ if (!(await this.isDaemonAlive())) {
104
+ await this.launchDaemon();
105
+ }
106
+
107
+ // Verify connectivity
108
+ const pong = await this.send({ type: "ping" });
109
+ if (!pong.success) {
110
+ throw new Error("Failed to connect to BM2 daemon");
111
+ }
112
+
113
+ this._connected = true;
114
+ this._daemonPid = pong.data?.pid ?? null;
115
+ this.emit("daemon:connected");
116
+
117
+ return this;
118
+ }
119
+
120
+ /**
121
+ * Disconnect from the daemon. Stops any internal polling but does **not**
122
+ * kill the daemon — processes keep running.
123
+ */
124
+ async disconnect(): Promise<void> {
125
+ this.stopPolling();
126
+ this._connected = false;
127
+ this.emit("daemon:disconnected");
128
+ }
129
+
130
+ // ─────────────────────── process management ───────────────────────────
131
+
132
+ /**
133
+ * Start a new process (or ecosystem).
134
+ *
135
+ * ```ts
136
+ * await bm2.start({ script: "./server.ts", name: "api", instances: 4 });
137
+ * ```
138
+ */
139
+ async start(options: StartOptions): Promise<ProcessState[]> {
140
+ if (options.script) {
141
+ options.script = resolve(options.script);
142
+ }
143
+ const res = await this.sendOrThrow({ type: "start", data: options });
144
+ this.emit("process:start", res.data);
145
+ return res.data;
146
+ }
147
+
148
+ /**
149
+ * Start an ecosystem configuration object.
150
+ *
151
+ * ```ts
152
+ * await bm2.startEcosystem({ apps: [{ script: "./a.ts" }, { script: "./b.ts" }] });
153
+ * ```
154
+ */
155
+ async startEcosystem(config: EcosystemConfig): Promise<ProcessState[]> {
156
+ // Resolve scripts to absolute paths
157
+ for (const app of config.apps) {
158
+ if (app.script) app.script = resolve(app.script);
159
+ }
160
+ const res = await this.sendOrThrow({ type: "ecosystem", data: config });
161
+ this.emit("process:start", res.data);
162
+ return res.data;
163
+ }
164
+
165
+ /**
166
+ * Stop one or more processes.
167
+ * @param target Process id, name, namespace, or `"all"`.
168
+ */
169
+ async stop(target: string | number = "all"): Promise<ProcessState[]> {
170
+ const type = target === "all" ? "stopAll" : "stop";
171
+ const data = target === "all" ? undefined : { target: String(target) };
172
+ const res = await this.sendOrThrow({ type, data });
173
+ this.emit("process:stop", res.data);
174
+ return res.data;
175
+ }
176
+
177
+ /**
178
+ * Restart one or more processes (hard restart).
179
+ */
180
+ async restart(target: string | number = "all"): Promise<ProcessState[]> {
181
+ const type = target === "all" ? "restartAll" : "restart";
182
+ const data = target === "all" ? undefined : { target: String(target) };
183
+ const res = await this.sendOrThrow({ type, data });
184
+ this.emit("process:restart", res.data);
185
+ return res.data;
186
+ }
187
+
188
+ /**
189
+ * Graceful zero-downtime reload.
190
+ */
191
+ async reload(target: string | number = "all"): Promise<ProcessState[]> {
192
+ const type = target === "all" ? "reloadAll" : "reload";
193
+ const data = target === "all" ? undefined : { target: String(target) };
194
+ const res = await this.sendOrThrow({ type, data });
195
+ this.emit("process:reload", res.data);
196
+ return res.data;
197
+ }
198
+
199
+ /**
200
+ * Stop and remove one or more processes from BM2's list.
201
+ */
202
+ async delete(target: string | number = "all"): Promise<ProcessState[]> {
203
+ const type = target === "all" ? "deleteAll" : "delete";
204
+ const data = target === "all" ? undefined : { target: String(target) };
205
+ const res = await this.sendOrThrow({ type, data });
206
+ this.emit("process:delete", res.data);
207
+ return res.data;
208
+ }
209
+
210
+ /**
211
+ * Scale a process group to `count` instances.
212
+ */
213
+ async scale(target: string | number, count: number): Promise<ProcessState[]> {
214
+ const res = await this.sendOrThrow({
215
+ type: "scale",
216
+ data: { target: String(target), count },
217
+ });
218
+ this.emit("process:scale", res.data);
219
+ return res.data;
220
+ }
221
+
222
+ /**
223
+ * Send an OS signal to a process.
224
+ */
225
+ async sendSignal(target: string | number, signal: string): Promise<void> {
226
+ await this.sendOrThrow({
227
+ type: "signal",
228
+ data: { target: String(target), signal },
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Reset restart counters for one or more processes.
234
+ */
235
+ async reset(target: string | number = "all"): Promise<ProcessState[]> {
236
+ const res = await this.sendOrThrow({
237
+ type: "reset",
238
+ data: { target: String(target) },
239
+ });
240
+ return res.data;
241
+ }
242
+
243
+ // ───────────────────────── introspection ──────────────────────────────
244
+
245
+ /**
246
+ * List all managed processes.
247
+ */
248
+ async list(): Promise<ProcessState[]> {
249
+ const res = await this.sendOrThrow({ type: "list" });
250
+ return res.data;
251
+ }
252
+
253
+ /**
254
+ * Get detailed description(s) of a process.
255
+ */
256
+ async describe(target: string | number): Promise<ProcessState[]> {
257
+ const res = await this.sendOrThrow({
258
+ type: "describe",
259
+ data: { target: String(target) },
260
+ });
261
+ return res.data;
262
+ }
263
+
264
+ // ────────────────────────── logs ───────────────────────────────────────
265
+
266
+ /**
267
+ * Retrieve recent log lines.
268
+ */
269
+ async logs(
270
+ target: string | number = "all",
271
+ lines: number = 20
272
+ ): Promise<Array<{ name: string; id: number; out: string; err: string }>> {
273
+ const res = await this.sendOrThrow({
274
+ type: "logs",
275
+ data: { target: String(target), lines },
276
+ });
277
+ this.emit("log:data", res.data);
278
+ return res.data;
279
+ }
280
+
281
+ /**
282
+ * Flush (truncate) log files for one or all processes.
283
+ */
284
+ async flush(target?: string | number): Promise<void> {
285
+ await this.sendOrThrow({
286
+ type: "flush",
287
+ data: target !== undefined ? { target: String(target) } : undefined,
288
+ });
289
+ }
290
+
291
+ // ──────────────────────── monitoring ───────────────────────────────────
292
+
293
+ /**
294
+ * Take a single metrics snapshot.
295
+ */
296
+ async metrics(): Promise<MetricSnapshot> {
297
+ const res = await this.sendOrThrow({ type: "metrics" });
298
+ this.emit("metrics", res.data);
299
+ return res.data;
300
+ }
301
+
302
+ /**
303
+ * Get historical metric snapshots.
304
+ * @param seconds Look-back window (default 300 = 5 min).
305
+ */
306
+ async metricsHistory(seconds: number = 300): Promise<MetricSnapshot[]> {
307
+ const res = await this.sendOrThrow({
308
+ type: "metricsHistory",
309
+ data: { seconds },
310
+ });
311
+ return res.data;
312
+ }
313
+
314
+ /**
315
+ * Get Prometheus-formatted metrics string.
316
+ */
317
+ async prometheus(): Promise<string> {
318
+ const res = await this.sendOrThrow({ type: "prometheus" });
319
+ return res.data;
320
+ }
321
+
322
+ /**
323
+ * Start polling metrics at a fixed interval and emitting `"metrics"` events.
324
+ *
325
+ * ```ts
326
+ * bm2.on("metrics", (snapshot) => console.log(snapshot));
327
+ * bm2.startPolling(2000);
328
+ * ```
329
+ */
330
+ startPolling(intervalMs: number = 2000): void {
331
+ this.stopPolling();
332
+ this._pollTimer = setInterval(async () => {
333
+ try {
334
+ await this.metrics();
335
+ } catch (err) {
336
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
337
+ }
338
+ }, intervalMs);
339
+ }
340
+
341
+ /** Stop the metrics polling loop started by `startPolling()`. */
342
+ stopPolling(): void {
343
+ if (this._pollTimer) {
344
+ clearInterval(this._pollTimer);
345
+ this._pollTimer = null;
346
+ }
347
+ }
348
+
349
+ // ────────────────────── persistence ───────────────────────────────────
350
+
351
+ /**
352
+ * Persist the current process list to disk so it can be restored later.
353
+ */
354
+ async save(): Promise<void> {
355
+ await this.sendOrThrow({ type: "save" });
356
+ }
357
+
358
+ /**
359
+ * Restore previously saved processes.
360
+ */
361
+ async resurrect(): Promise<ProcessState[]> {
362
+ const res = await this.sendOrThrow({ type: "resurrect" });
363
+ return res.data;
364
+ }
365
+
366
+ // ────────────────────── dashboard ─────────────────────────────────────
367
+
368
+ /**
369
+ * Start the web dashboard.
370
+ */
371
+ async dashboard(
372
+ port: number = DASHBOARD_PORT,
373
+ metricsPort: number = METRICS_PORT
374
+ ): Promise<{ port: number; metricsPort: number }> {
375
+ const res = await this.sendOrThrow({
376
+ type: "dashboard",
377
+ data: { port, metricsPort },
378
+ });
379
+ return res.data;
380
+ }
381
+
382
+ /**
383
+ * Stop the web dashboard.
384
+ */
385
+ async dashboardStop(): Promise<void> {
386
+ await this.sendOrThrow({ type: "dashboardStop" });
387
+ }
388
+
389
+ // ────────────────────── modules ───────────────────────────────────────
390
+
391
+ /**
392
+ * Install a BM2 module.
393
+ */
394
+ async moduleInstall(nameOrPath: string): Promise<{ path: string }> {
395
+ const res = await this.sendOrThrow({
396
+ type: "moduleInstall",
397
+ data: { module: nameOrPath },
398
+ });
399
+ return res.data;
400
+ }
401
+
402
+ /**
403
+ * Uninstall a BM2 module.
404
+ */
405
+ async moduleUninstall(name: string): Promise<void> {
406
+ await this.sendOrThrow({
407
+ type: "moduleUninstall",
408
+ data: { module: name },
409
+ });
410
+ }
411
+
412
+ /**
413
+ * List installed modules.
414
+ */
415
+ async moduleList(): Promise<Array<{ name: string; version: string }>> {
416
+ const res = await this.sendOrThrow({ type: "moduleList" });
417
+ return res.data;
418
+ }
419
+
420
+ // ────────────────────── daemon lifecycle ──────────────────────────────
421
+
422
+ /**
423
+ * Ping the daemon. Returns daemon PID and uptime.
424
+ */
425
+ async ping(): Promise<{ pid: number; uptime: number }> {
426
+ const res = await this.sendOrThrow({ type: "ping" });
427
+ return res.data;
428
+ }
429
+
430
+ /**
431
+ * Kill the daemon and all managed processes.
432
+ */
433
+ async kill(): Promise<void> {
434
+ try {
435
+ await this.send({ type: "kill" });
436
+ } catch {
437
+ // Expected — daemon exits before responding
438
+ }
439
+
440
+ // Clean up leftover files
441
+ try { if (existsSync(DAEMON_SOCKET)) unlinkSync(DAEMON_SOCKET); } catch {}
442
+ try { if (existsSync(DAEMON_PID_FILE)) unlinkSync(DAEMON_PID_FILE); } catch {}
443
+
444
+ this._connected = false;
445
+ this._daemonPid = null;
446
+ this.stopPolling();
447
+ this.emit("daemon:killed");
448
+ }
449
+
450
+ /**
451
+ * Reload the daemon server itself.
452
+ */
453
+ async daemonReload(): Promise<string> {
454
+ const res = await this.sendOrThrow({ type: "daemonReload" });
455
+ return res.data;
456
+ }
457
+
458
+ // ────────────────────── internal transport ────────────────────────────
459
+
460
+ /**
461
+ * Low-level: send an arbitrary message to the daemon and return the
462
+ * raw response. Useful for custom or future command types.
463
+ */
464
+ async send(message: DaemonMessage): Promise<DaemonResponse> {
465
+
466
+ if (!message.id) {
467
+ message.id = generateId();
468
+ }
469
+
470
+ const body = JSON.stringify(message);
471
+
472
+ // Bun supports fetching over Unix sockets with the `unix` option
473
+ const response = await fetch(`http://localhost/`, {
474
+ method: "POST",
475
+ headers: { "Content-Type": "application/json" },
476
+ body,
477
+ unix: DAEMON_SOCKET,
478
+ });
479
+
480
+ if (!response.ok) {
481
+ const text = await response.text();
482
+ throw new Error(`Daemon HTTP ${response.status}: ${text}`);
483
+ }
484
+
485
+ return (await response.json()) as DaemonResponse;
486
+ }
487
+
488
+ // ────────────────────── private helpers ───────────────────────────────
489
+
490
+ /** Send and throw a friendly error if `success` is false. */
491
+ private async sendOrThrow(message: DaemonMessage): Promise<DaemonResponse> {
492
+ const res = await this.send(message);
493
+ if (!res.success) {
494
+ throw new BM2Error(
495
+ res.error || `Command "${message.type}" failed`,
496
+ message.type,
497
+ res
498
+ );
499
+ }
500
+ return res;
501
+ }
502
+
503
+ /** Check whether the daemon is running and reachable. */
504
+ private async isDaemonAlive(): Promise<boolean> {
505
+ // Quick PID-file check first
506
+ if (existsSync(DAEMON_PID_FILE)) {
507
+ try {
508
+ const pid = parseInt(readFileSync(DAEMON_PID_FILE, "utf-8").trim());
509
+ process.kill(pid, 0); // throws if process doesn't exist
510
+ } catch {
511
+ // Stale PID file
512
+ return false;
513
+ }
514
+ } else {
515
+ return false;
516
+ }
517
+
518
+ // Verify the socket is responsive
519
+ if (!existsSync(DAEMON_SOCKET)) return false;
520
+
521
+ try {
522
+ const res = await this.send({ type: "ping" });
523
+ return res.success;
524
+ } catch {
525
+ return false;
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Launch the daemon as a detached background process.
531
+ * Waits up to 5 seconds for it to become responsive.
532
+ */
533
+ private async launchDaemon(): Promise<void> {
534
+ const daemonScript = join(import.meta.dir, "daemon.ts");
535
+ const bunPath = Bun.which("bun") || "bun";
536
+
537
+ // Open log files for daemon stdout/stderr
538
+ const outLog = Bun.file(DAEMON_OUT_LOG_FILE);
539
+ const errLog = Bun.file(DAEMON_ERR_LOG_FILE);
540
+
541
+ const proc = Bun.spawn([bunPath, "run", daemonScript], {
542
+ stdio: ["ignore", "ignore", "ignore"],
543
+ env: { ...process.env as Record<string, string> },
544
+ });
545
+
546
+ // Detach — we don't want to keep a handle
547
+ proc.unref();
548
+
549
+ // Poll until daemon is responsive (up to 5 s)
550
+ const deadline = Date.now() + 5000;
551
+ let alive = false;
552
+
553
+ while (Date.now() < deadline) {
554
+ await Bun.sleep(200);
555
+ try {
556
+ if (existsSync(DAEMON_SOCKET)) {
557
+ const res = await this.send({ type: "ping" });
558
+ if (res.success) {
559
+ alive = true;
560
+ this._daemonPid = res.data?.pid ?? proc.pid;
561
+ break;
562
+ }
563
+ }
564
+ } catch {
565
+ // Not ready yet — keep waiting
566
+ }
567
+ }
568
+
569
+ if (!alive) {
570
+ throw new Error(
571
+ "Timed out waiting for BM2 daemon to start. " +
572
+ `Check ${DAEMON_ERR_LOG_FILE} for details.`
573
+ );
574
+ }
575
+
576
+ this.emit("daemon:launched", this._daemonPid!);
577
+ }
578
+ }
579
+
580
+ // ────────────────────────── error class ─────────────────────────────────
581
+
582
+ export class BM2Error extends Error {
583
+ /** The daemon command type that failed. */
584
+ public readonly command: string;
585
+ /** The full daemon response. */
586
+ public readonly response: DaemonResponse;
587
+
588
+ constructor(message: string, command: string, response: DaemonResponse) {
589
+ super(message);
590
+ this.name = "BM2Error";
591
+ this.command = command;
592
+ this.response = response;
593
+ }
594
+ }
595
+
596
+ // ────────────────────────── default export ──────────────────────────────
597
+
598
+ export default BM2;
package/src/index.ts CHANGED
@@ -39,7 +39,6 @@ import type {
39
39
  EcosystemConfig,
40
40
  ProcessState,
41
41
  } from "./types";
42
- import Table from "cli-table3";
43
42
  import { statusColor } from "./colors";
44
43
  import { liveWatchProcess, printProcessTable } from "./process-table";
45
44
 
@@ -84,6 +84,7 @@ function minimalBorders() {
84
84
  // ---------- Table Printer ----------
85
85
 
86
86
  export function printProcessTable(processes: ProcessState[]) {
87
+
87
88
  console.log("");
88
89
  console.log(color("BM2 — Bun Process Manager", "bold"));
89
90
  console.log(color("─────────────────────────────────────────────", "dim"));
@@ -133,7 +134,7 @@ export function printProcessTable(processes: ProcessState[]) {
133
134
  }
134
135
 
135
136
 
136
- export function liveWatchProcess(processes: ProcessState[], interval = 1000) {
137
+ export function liveWatchProcess(processes: ProcessState[], interval = 5_000) {
137
138
  let sortBy: "cpu" | "mem" | "uptime" | "default" = "default";
138
139
 
139
140
  // Clear console helper
@@ -34,39 +34,41 @@
34
34
  }
35
35
 
36
36
  private generateSystemd(bunPath: string, bm2Path: string, daemonPath: string): string {
37
+
37
38
  const unit = `[Unit]
38
- Description=BM2 Process Manager
39
- Documentation=https://github.com/bm2
40
- After=network.target
41
-
42
- [Service]
43
- Type=forking
44
- User=${process.env.USER || "root"}
45
- LimitNOFILE=infinity
46
- LimitNPROC=infinity
47
- LimitCORE=infinity
48
- Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${join(bunPath, "..")}
49
- Environment=BM2_HOME=${join(process.env.HOME || "/root", ".bm2")}
50
- PIDFile=${join(process.env.HOME || "/root", ".bm2", "daemon.pid")}
51
- Restart=on-failure
52
-
53
- ExecStart=${bunPath} run ${daemonPath}
54
- ExecReload=${bunPath} run ${bm2Path} reload all
55
- ExecStop=${bunPath} run ${bm2Path} kill
56
-
57
- [Install]
58
- WantedBy=multi-user.target`;
59
-
39
+ Description=BM2 Process Manager
40
+ Documentation=https://github.com/bm2
41
+ After=network.target
42
+
43
+ [Service]
44
+ Type=simple
45
+ User=${process.env.USER || "root"}
46
+ LimitNOFILE=infinity
47
+ LimitNPROC=infinity
48
+ LimitCORE=infinity
49
+ Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${join(bunPath, "..")}
50
+ Environment=BM2_HOME=${join(process.env.HOME || "/root", ".bm2")}
51
+ Restart=on-failure
52
+
53
+ ExecStart=${bunPath} run ${daemonPath}
54
+ ExecStartPost=${bunPath} run ${bm2Path} resurrect
55
+ ExecReload=${bunPath} run ${bm2Path} reload all
56
+ ExecStop=${bunPath} run ${bm2Path} kill
57
+
58
+ [Install]
59
+ WantedBy=multi-user.target
60
+ `;
61
+
60
62
  const servicePath = "/etc/systemd/system/bm2.service";
61
- return `# BM2 Systemd Service
62
- # Save to: ${servicePath}
63
- # Then run:
64
- # sudo systemctl daemon-reload
65
- # sudo systemctl enable bm2
66
- # sudo systemctl start bm2
67
-
68
- ${unit}`;
69
- }
63
+ return `# BM2 Systemd Service
64
+ # Save to: ${servicePath}
65
+ # Then run:
66
+ # sudo systemctl daemon-reload
67
+ # sudo systemctl enable bm2
68
+ # sudo systemctl start bm2
69
+
70
+ ${unit}`;
71
+ }
70
72
 
71
73
  private generateLaunchd(bunPath: string, bm2Path: string, daemonPath: string): string {
72
74
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -115,11 +117,11 @@
115
117
  if (os === "linux") {
116
118
  const servicePath = "/etc/systemd/system/bm2.service";
117
119
  // Extract just the unit content
118
- const unitContent = content.split("\n\n").slice(1).join("\n\n");
119
- await Bun.write(servicePath, unitContent);
120
+ //const unitContent = content.split("\n\n").slice(1).join("\n\n");
121
+ await Bun.write(servicePath, content);
120
122
 
121
- Bun.spawn(["sudo", "systemctl", "daemon-reload"], { stdout: "inherit" });
122
- Bun.spawn(["sudo", "systemctl", "enable", "bm2"], { stdout: "inherit" });
123
+ Bun.spawn(["sudo", "systemctl", "daemon-reload"], { stdout: "inherit" }).exited;
124
+ Bun.spawn(["sudo", "systemctl", "enable", "bm2"], { stdout: "inherit" }).exited;
123
125
 
124
126
  return `Service installed at ${servicePath}\nRun: sudo systemctl start bm2`;
125
127
  } else if (os === "darwin") {
package/src/hello.js DELETED
@@ -1,7 +0,0 @@
1
-
2
- let counter = 0;
3
-
4
- setInterval(() => {
5
- console.log(`Hello World ${counter++}`)
6
-
7
- })