@gachlab/devup 0.7.1 → 0.8.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ All notable changes to `@gachlab/devup` are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.1] — 2026-05-22
9
+
10
+ Patch focused on two real-world footguns surfaced as soon as 0.8.0 hit users.
11
+
12
+ ### Fixed
13
+ - **Spawner port pre-flight was unreliable.** The check used `checkPort` — a connect-based test designed for health checks — which can miss bindings (e.g. a server bound but not yet accepting connections, or listening on a different address family). When the check missed, the service crashed with a raw `EADDRINUSE` Node stack trace dumped to the logs panel. Replaced with `isPortBindable()` — a bind-based test using `net.createServer().listen()` — which catches every case that would actually conflict.
14
+ - **Pre-flight failures now record a crashed state.** Previously the spawner returned silently when the port was occupied, leaving no entry in the services list. Now the failed service appears in the stats panel as `crashed` with the `⚠ port N already in use` line in its log, matching the behaviour of other pre-flight failures.
15
+
16
+ ### Added
17
+ - **TUI refuses to start when a daemon is already running for the project.** Running `devup` (TUI) on top of `devup up -d` would race for the same ports — every API would fail with EADDRINUSE. The TUI now detects the PID file before mounting Ink and exits with a clear pointer to `devup down` and the `devup ctl` family.
18
+
19
+ ### Internals
20
+ - New public `isPortBindable(port, host?)` in `src/process/health.ts`. `checkPort` keeps its connect-based semantics for health checks and `waitForPort`. Three new tests cover free / occupied / bound-but-not-accepting cases.
21
+
22
+ ## [0.8.0] — 2026-05-22
23
+
24
+ **Headless devup.** The control plane grows up: streaming events, a CLI client that speaks it end-to-end, and the long-requested daemon mode so the stack can be left running while you keep working in the same terminal — `docker compose up -d` for Node monorepos.
25
+
26
+ ### Added
27
+ - **Streaming control plane** (#46). Two new RPC methods over the existing Unix socket:
28
+ - `logs.follow { svc, tail? }` — ack, then a replay of the last N tail lines (default 50), then live newline-delimited frames until the socket closes.
29
+ - `status.follow` — ack, then a full snapshot, then deltas as services transition.
30
+ Subscriptions auto-clean on socket close; no explicit unfollow needed. Backed by a small typed `Broadcaster<T>` pub-sub fed from `ProcessManagerEvents.onLog` / `onStateChange`.
31
+ - **`devup ctl <method>` CLI client** (#47). Lightweight reference client that exercises every control-plane method, doubles as a useful tool on its own:
32
+ - `devup ctl ping` — liveness check
33
+ - `devup ctl status [--follow]` — snapshot or live stream
34
+ - `devup ctl logs <svc> [--follow]` — tail (last 100) or follow live stream
35
+ - `devup ctl restart <svc>` / `devup ctl stop <svc>` — write operations
36
+ Friendly error when the daemon isn't running. Streaming variants abort on SIGINT.
37
+ - **Daemon mode** (#54). `devup up -d` (also `--detach`) boots the stack and returns the terminal immediately, leaving services running until explicitly stopped:
38
+ - Double-forks via `spawn({ detached: true, stdio: 'ignore' })` so the daemon survives terminal close. The parent waits up to 90s for the child to write `~/.devup/<project>.pid` (success signal) or `~/.devup/<project>.boot-error` (failure).
39
+ - Headless: no Ink/TUI mounted in the daemon process. **Feature parity with the TUI**: same `ProcessManager`, `LogSink`, control plane, lazy proxies, externals, proxy-config sync, and `--watch-config` hot-reload — minus the React layer.
40
+ - `devup down` reads the PID file, sends SIGTERM with a 10-second grace window, falls back to SIGKILL, and cleans up the PID + socket files. Reports stale PID files clearly.
41
+ - One daemon per project; trying `devup up -d` while one is running prints the existing PID and exits 1.
42
+ - Not yet supported on Windows; clear error directs users to the TUI.
43
+
44
+ ### Internals
45
+ - New `src/orchestrator/config-watcher.ts` extracts the diff/apply logic from `useHotReload` into a pure `applyConfigChange()` + an fs.watch wrapper `watchConfig()`. Both the TUI hook and the daemon now share this code path.
46
+
47
+ ### Notes
48
+ - Test suite: 338 → 369 (+31). New: 3 streaming, 8 `ctl`, 10 daemon unit, 3 daemon E2E (boot + clean shutdown, hot-reload, "already running" guard), 6 config-watcher tests, plus 1 fix to existing config tests.
49
+
8
50
  ## [0.7.1] — 2026-05-22
9
51
 
10
52
  Internals cleanup. No user-facing changes; safe drop-in upgrade from 0.7.0.
package/README.md CHANGED
@@ -40,9 +40,11 @@ Built with TypeScript 6, Ink (React for terminals), and zero test dependencies (
40
40
  - **Persistent logs** — every line streamed to `~/.devup/logs/<project>/<svc>.log` with rotation on each launch.
41
41
  - **Subcommands** — `devup logs <svc> [--follow]`, `devup install`, `devup status`, `devup help` work without launching the TUI.
42
42
  - **CI-ready** — `--dry-run` prints the resolved boot plan; `--once` boots, waits for readiness, exits `0/1` without a TUI.
43
+ - **Daemon mode** — `devup up -d` boots the stack detached (like `docker compose up -d`) so you can keep using the same terminal. `devup down` stops it. Linux + macOS.
44
+ - **`devup ctl`** — CLI client for the control plane: `ping`, `status [--follow]`, `logs <svc> [--follow]`, `restart`, `stop`.
43
45
  - **Reverse-proxy config** — generate Traefik, Nginx, or Caddy config from running services; health-aware.
44
- - **Unix-socket control plane** — local JSON-RPC at `~/.devup/sock-<project>.sock` (chmod 0600); `status`, `restart`, `stop`, `logs.tail`, `ping`.
45
- - **Cross-platform** — Linux, macOS, and Windows. Platform-specific process management, stats collection, and browser opening.
46
+ - **Unix-socket control plane** — local JSON-RPC at `~/.devup/sock-<project>.sock` (chmod 0600); `status`, `restart`, `stop`, `logs.tail`, `logs.follow`, `status.follow`, `ping`.
47
+ - **Cross-platform** — Linux, macOS, and Windows for the TUI; daemon mode is Linux + macOS only.
46
48
 
47
49
  ## Quick start
48
50
 
@@ -0,0 +1,17 @@
1
+ import { defaultSocketPath } from './socket-server.js';
2
+ export { defaultSocketPath };
3
+ /** Resolve the socket path, preferring an explicit override. */
4
+ export declare function resolveSocket(projectName: string, overridePath?: string): string;
5
+ /** Throw a friendly error if the socket doesn't exist (devup not running). */
6
+ export declare function assertSocketExists(socketPath: string, projectName: string): void;
7
+ /** Send a single RPC request and return the result, or throw on error. */
8
+ export declare function sendRpc(socketPath: string, method: string, params?: Record<string, unknown>): Promise<unknown>;
9
+ export interface StreamFrame {
10
+ event: string;
11
+ data: unknown;
12
+ svc?: string;
13
+ }
14
+ /** Open a streaming RPC (logs.follow / status.follow).
15
+ * Returns an abort function. The stream runs until abort() is called or the socket closes. */
16
+ export declare function openStream(socketPath: string, method: string, params: Record<string, unknown>, onFrame: (frame: StreamFrame) => void, onError?: (err: Error) => void): () => void;
17
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/control-plane/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAMhF;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CACrB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,OAAO,CAAC,CAiBlB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;+FAC+F;AAC/F,wBAAgB,UAAU,CACxB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,EACrC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAC7B,MAAM,IAAI,CAqBZ"}
@@ -3,6 +3,7 @@ import type { ProcessState } from '../process/types.js';
3
3
  /** Minimal JSON-RPC-like protocol over a local Unix socket.
4
4
  * Request ─► { id?, method, params? } newline-terminated JSON
5
5
  * Response ─► { id?, result | error } newline-terminated JSON
6
+ * Stream ─► { id, event, data } pushed until socket closes
6
7
  *
7
8
  * Auth model: unix socket created with `chmod 0600`. Anyone with read access
8
9
  * to the socket file already has the same uid as the devup process — no
@@ -17,6 +18,11 @@ export interface RpcContext {
17
18
  stop(name: string): void;
18
19
  /** Tail N most recent log lines for the given service (from the persistent log file). */
19
20
  tailLogs(svcName: string, lines: number): Promise<string[]>;
21
+ /** Subscribe to live log lines. Pass null to receive logs from all services.
22
+ * Returns an unsubscribe function. */
23
+ watchLogs(svcName: string | null, onLine: (svc: string, line: string) => void): () => void;
24
+ /** Subscribe to service-state changes. Returns an unsubscribe function. */
25
+ watchStatus(onUpdate: (name: string, state: ProcessState) => void): () => void;
20
26
  }
21
27
  export interface SocketServerHandle {
22
28
  server: Server;
@@ -1 +1 @@
1
- {"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../../src/control-plane/socket-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAe,MAAM,UAAU,CAAC;AAMlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;oBAOoB;AAEpB,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,iCAAiC;IACjC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,8BAA8B;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC7D;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,UAAU,EACf,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC1D,OAAO,CAAC,kBAAkB,CAAC,CAiC7B"}
1
+ {"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../../src/control-plane/socket-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAe,MAAM,UAAU,CAAC;AAMlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;oBAQoB;AAEpB,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,iCAAiC;IACjC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,8BAA8B;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5D;2CACuC;IACvC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC3F,2EAA2E;IAC3E,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAChF;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,UAAU,EACf,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC1D,OAAO,CAAC,kBAAkB,CAAC,CAiC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChG,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChG,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC"}