@neat.is/core 0.4.29-dev.20260715 → 0.4.30-dev.20260716
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/dist/cli.cjs +3 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/neatd.cjs +1 -1
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +1 -1
- package/dist/neatd.js.map +1 -1
- package/package.json +2 -2
package/dist/neatd.js
CHANGED
|
@@ -466,7 +466,7 @@ async function main() {
|
|
|
466
466
|
process.exit(1);
|
|
467
467
|
}
|
|
468
468
|
var entry = process.argv[1] ?? "";
|
|
469
|
-
if (/[\\/]neatd\.(?:cjs|js)$/.test(entry)
|
|
469
|
+
if (/[\\/](?:neatd\.(?:cjs|js)|neatd)$/.test(entry)) {
|
|
470
470
|
main().catch((err) => {
|
|
471
471
|
console.error(err);
|
|
472
472
|
process.exit(1);
|
package/dist/neatd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/neatd.ts","../src/web-spawn.ts","../src/version-skew.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `neatd` — distribution-layer daemon CLI (ADR-049).\n *\n * Subcommands:\n * neatd start [--foreground] boot the daemon and watch the registry\n * neatd stop signal the running daemon to shut down\n * neatd reload signal the running daemon to re-read the registry\n * neatd status print PID + per-project last-seen timestamps\n *\n * MVP runs in foreground only. Backgrounding is the supervisor's job\n * (launchd / systemd / nohup) — `neatd start` blocks until SIGINT/SIGTERM.\n *\n * v0.2.10: also brings up the web UI on port 6328 by default (ADR-059).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport { startDaemon, reconcileDaemonRecordSync } from './daemon.js'\nimport { BindAuthorityError } from './auth.js'\nimport { listProjects, registryPath } from './registry.js'\nimport { spawnWebUI, DEFAULT_WEB_PORT, type WebHandle } from './web-spawn.js'\nimport { checkVersionSkew } from './version-skew.js'\n\n// Resolve the running @neat.is/core version from the bundled package.json.\n// Lockstep publishing (ADR-052) keeps this in step with the `neat.is` meta\n// package, which is what the operator installs globally. Tests stub by\n// setting NEAT_LOCAL_VERSION.\nfunction localVersion(): string {\n if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {\n return process.env.NEAT_LOCAL_VERSION\n }\n try {\n const req = createRequire(import.meta.url)\n const pkg = req('../package.json') as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : '0.0.0'\n } catch {\n return '0.0.0'\n }\n}\n\nfunction neatHome(): string {\n if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {\n return path.resolve(process.env.NEAT_HOME)\n }\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\nasync function readPid(): Promise<number | null> {\n try {\n const raw = await fs.readFile(path.join(neatHome(), 'neatd.pid'), 'utf8')\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) ? n : null\n } catch {\n return null\n }\n}\n\nfunction usage(): void {\n console.log('usage: neatd <start|stop|reload|status> [--foreground]')\n}\n\nfunction restPortFromEnv(): number {\n const raw = process.env.PORT\n return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080\n}\n\nfunction webPortFromEnv(): number | undefined {\n const raw = process.env.NEAT_WEB_PORT\n if (!raw || raw.length === 0) return undefined\n const n = Number.parseInt(raw, 10)\n return Number.isFinite(n) ? n : undefined\n}\n\n// Install the daemon's ingest fault-containment handlers (daemon contract\n// §Fault containment, ADR-112). A rejected promise that escapes the ingest\n// drain loop must not dark the whole OBSERVED layer, so `unhandledRejection`\n// is contained: logged loud (error + stack, never silent) and the daemon keeps\n// serving. An `uncaughtException` is a different class of fault — a synchronous\n// throw that reaches the top of the stack leaves the process in an undefined\n// state — so it stays fatal: logged loud, then exit non-zero and let\n// supervision restart clean rather than serve from corrupt state. `onFatal` is\n// injectable so tests can assert the disposition without ending the process.\nexport function installIngestFaultHandlers(\n onFatal: (code: number) => void = (code) => process.exit(code),\n): void {\n const logFault = (kind: string, err: unknown): void => {\n const e = err as Error\n console.error(`neatd: ${kind}. ${e?.stack ?? e?.message ?? String(err)}`)\n }\n process.on('unhandledRejection', (reason) =>\n logFault('unhandled rejection — daemon staying up', reason),\n )\n process.on('uncaughtException', (err) => {\n logFault('uncaught exception — exiting', err)\n onFatal(1)\n })\n}\n\nasync function cmdStart(): Promise<void> {\n // ADR-096 — when the orchestrator spawns a per-project daemon it passes the\n // project name + root + allocated ports through the environment. Forward them\n // into startDaemon so the daemon scopes itself to that one project and writes\n // its daemon.json self-description. A bare `neatd start` with none of these\n // set falls through to the legacy multi-project daemon. (startDaemon also\n // reads PORT / OTEL_PORT / NEAT_WEB_PORT itself; we thread the project bits\n // explicitly so the wiring is visible at the entrypoint.)\n const project = process.env.NEAT_PROJECT\n const projectPath = process.env.NEAT_PROJECT_PATH\n const startOpts =\n project && project.length > 0\n ? { project, projectPath, webPort: webPortFromEnv() }\n : {}\n\n let handle: Awaited<ReturnType<typeof startDaemon>>\n try {\n handle = await startDaemon(startOpts)\n } catch (err) {\n if (err instanceof BindAuthorityError) {\n // ADR-073 §3 — refuse to bind on a public interface without a token.\n // Clean stderr line, exit 1 — no stack trace; the operator wants the\n // single-line directive, not a Node.js Error dump.\n console.error(`neatd: ${err.message}`)\n process.exit(1)\n }\n throw err\n }\n // Ingest fault containment per ADR-112 — a rejected promise that escapes the\n // drain loop is contained (logged, keep serving); an uncaughtException stays\n // fatal. Installed after startDaemon resolves, so the bind paths keep their\n // own fatal handling above.\n installIngestFaultHandlers()\n\n // Reconcile daemon.json on any exit (project-daemon contract §2). The\n // graceful `stop()` path does this asynchronously first; this synchronous\n // backstop covers a crash or fatal signal so a dead daemon never leaves a\n // `running` record pointing clients at a port nothing is listening on.\n process.on('exit', () => {\n if (handle.daemonRecord) {\n reconcileDaemonRecordSync(handle.daemonRecord, handle.neatHome)\n }\n })\n\n console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`)\n console.log(`neatd: registry at ${registryPath()}`)\n // ADR-063 — surface the bound addresses so the operator can sanity-check\n // the documented happy path without grepping startup logs.\n if (handle.restAddress) console.log(`neatd: REST → ${handle.restAddress}`)\n if (handle.otlpAddress) console.log(`neatd: OTLP → ${handle.otlpAddress}`)\n\n // Version-skew advisory — non-fatal, fail-open. Surfaces the gap when the\n // operator's globally installed `neat.is` binary lags the published\n // version. Set NEAT_DISABLE_VERSION_CHECK=1 to silence the check (e.g. in\n // air-gapped environments).\n if (process.env.NEAT_DISABLE_VERSION_CHECK !== '1') {\n void checkVersionSkew({ localVersion: localVersion() }).catch(() => {\n // Fail-open: any thrown error from the helper resolves silently. The\n // helper already catches its own internals; this catches anything\n // exotic the Promise chain bubbles up.\n })\n }\n\n // ADR-059 — bring up the web UI alongside the daemon. Failure here aborts\n // start with the clear-error pattern from ADR-049 instead of silently\n // running headless.\n const skipWeb = process.env.NEAT_WEB_DISABLED === '1'\n let web: WebHandle | null = null\n if (!skipWeb) {\n try {\n web = await spawnWebUI(restPortFromEnv())\n } catch (err) {\n console.error((err as Error).message)\n await handle.stop().catch(() => {})\n process.exit(3)\n }\n } else {\n console.log('neatd: web UI disabled (NEAT_WEB_DISABLED=1)')\n }\n\n console.log('neatd: SIGHUP reloads, SIGTERM/SIGINT stops')\n\n let stopping = false\n const shutdown = (signal: NodeJS.Signals): void => {\n if (stopping) return\n stopping = true\n console.log(`neatd: ${signal} received, stopping…`)\n void Promise.allSettled([handle.stop(), web ? web.stop() : Promise.resolve()])\n .catch((err) => console.error(`neatd: shutdown error — ${(err as Error).message}`))\n .finally(() => process.exit(0))\n }\n process.on('SIGTERM', shutdown)\n process.on('SIGINT', shutdown)\n\n // Block forever — supervisors keep us in the foreground.\n await new Promise<void>(() => {})\n}\n\nasync function cmdStop(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGTERM')\n console.log(`neatd: SIGTERM sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdReload(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGHUP')\n console.log(`neatd: SIGHUP sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdStatus(): Promise<void> {\n const pid = await readPid()\n console.log(`pid: ${pid ?? '(not running)'}`)\n console.log(`registry: ${registryPath()}`)\n const webPort = process.env.NEAT_WEB_PORT\n ? Number.parseInt(process.env.NEAT_WEB_PORT, 10)\n : DEFAULT_WEB_PORT\n console.log(`web ui: http://localhost:${webPort}`)\n const projects = await listProjects().catch(() => [])\n if (projects.length === 0) {\n console.log('projects: (none)')\n return\n }\n console.log('projects:')\n for (const p of projects) {\n const seen = p.lastSeenAt ?? 'never'\n console.log(` ${p.name}\\t${p.status}\\t${p.path}\\tlast-seen=${seen}`)\n }\n}\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2]\n if (!cmd || cmd === '-h' || cmd === '--help') {\n usage()\n process.exit(cmd ? 0 : 2)\n }\n\n if (cmd === 'start') return cmdStart()\n if (cmd === 'stop') return cmdStop()\n if (cmd === 'reload') return cmdReload()\n if (cmd === 'status') return cmdStatus()\n\n console.error(`neatd: unknown command \"${cmd}\"`)\n usage()\n process.exit(1)\n}\n\nconst entry = process.argv[1] ?? ''\nif (/[\\\\/]neatd\\.(?:cjs|js)$/.test(entry) || entry.endsWith('/neatd')) {\n main().catch((err) => {\n console.error(err)\n process.exit(1)\n })\n}\n","/**\n * Web UI spawn helper (ADR-059, ADR-096).\n *\n * A project's daemon brings up the REST API + OTel receivers, then calls\n * `spawnWebUI(restPort)` to make its dashboard reachable. Lifecycle is\n * parent-tied: SIGTERM/SIGINT on the daemon cascades to the web child via\n * `stop()`.\n *\n * Per-project ports (ADR-096 §5). The dashboard binds its own port and points\n * at its own REST API, both read from the project's\n * `<projectRoot>/neat-out/daemon.json` — the single source of truth for \"where\n * is this project's daemon\" (project-daemon contract §2). The `restPort`\n * argument is tolerated as a fallback so callers that still pass it keep\n * working, but `daemon.json` wins when present. A missing or malformed\n * `daemon.json` falls back to the canonical `6328`/`8080`; the read never\n * throws.\n *\n * Lazy spawn (ADR-096 §7). The web port is bound up front by a thin listener,\n * but the heavyweight Next.js server is not started until someone actually\n * opens the dashboard. A daemon that nobody is looking at keeps no web process\n * resident — this matters once there's one daemon per project and the cost\n * would otherwise multiply by N.\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport { promises as fsp } from 'node:fs'\nimport net from 'node:net'\nimport path from 'node:path'\n\nexport const DEFAULT_WEB_PORT = 6328\nexport const DEFAULT_REST_PORT = 8080\n\nexport interface WebHandle {\n /** The Next.js child once it's been lazily spawned; null until first open. */\n readonly child: ChildProcess | null\n port: number\n /** True once the underlying Next.js server has actually been spawned. */\n started: () => boolean\n stop: () => Promise<void>\n}\n\n// The slice of daemon.json we read. Other fields exist (pid, status, …) but the\n// web UI only needs the ports, so we narrow to those and tolerate the rest.\ninterface DaemonPortsShape {\n rest?: unknown\n web?: unknown\n}\ninterface DaemonRecordShape {\n ports?: DaemonPortsShape\n}\n\nfunction asValidPort(value: unknown): number | null {\n const n = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10)\n return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null\n}\n\n/**\n * Resolve the project root whose `neat-out/daemon.json` describes this\n * daemon. `NEAT_SCAN_PATH` is the canonical project-root env (the REST server\n * reads the same name); `process.cwd()` is the fallback for a daemon launched\n * from inside the project.\n */\nfunction projectRoot(): string {\n const fromEnv = process.env.NEAT_SCAN_PATH\n return path.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd())\n}\n\n/**\n * Read `<projectRoot>/neat-out/daemon.json` and pull out the web + REST ports.\n * Bulletproof: a missing file, unreadable file, bad JSON, or a malformed port\n * all resolve to `null` for that field — never a throw. Authoritative when\n * present (project-daemon contract §2), so its ports take precedence over the\n * `restPort` argument and over `NEAT_WEB_PORT`.\n */\nexport async function readDaemonPorts(\n root: string,\n): Promise<{ web: number | null; rest: number | null }> {\n try {\n const raw = await fsp.readFile(path.join(root, 'neat-out', 'daemon.json'), 'utf8')\n const parsed = JSON.parse(raw) as DaemonRecordShape\n const ports = parsed?.ports ?? {}\n return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) }\n } catch {\n return { web: null, rest: null }\n }\n}\n\n/**\n * Pure port resolution, factored out so it's testable without binding a\n * socket. `daemon.json` is the source of truth (ADR-096 §2); `NEAT_WEB_PORT`\n * and the `restPort` argument are fallbacks for installs predating the file.\n */\nexport function resolveWebPorts(args: {\n daemonWeb: number | null\n daemonRest: number | null\n webPortEnv: string | undefined\n apiUrlEnv: string | undefined\n restPortArg: number\n}): { webPort: number; apiUrl: string } {\n const { daemonWeb, daemonRest, webPortEnv, apiUrlEnv, restPortArg } = args\n\n const portFromEnv = webPortEnv && webPortEnv.length > 0 ? Number.parseInt(webPortEnv, 10) : null\n if (portFromEnv !== null && (!Number.isFinite(portFromEnv) || portFromEnv <= 0 || portFromEnv > 65535)) {\n throw new Error(`neatd: invalid NEAT_WEB_PORT=\"${webPortEnv}\"`)\n }\n const webPort = daemonWeb ?? portFromEnv ?? DEFAULT_WEB_PORT\n\n const restPort = daemonRest ?? asValidPort(restPortArg) ?? DEFAULT_REST_PORT\n const apiUrl = apiUrlEnv ?? `http://localhost:${restPort}`\n\n return { webPort, apiUrl }\n}\n\n/**\n * Best-effort port collision check before binding. Binds, closes, returns. A\n * race between the check and the real bind is acceptable — the placeholder's\n * own `listen` will then fail loudly and the parent exits.\n */\nasync function assertPortFree(port: number): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const tester = net.createServer()\n tester.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(\n new Error(\n `neatd: web UI port ${port} in use; set NEAT_WEB_PORT to override or stop the conflicting process`,\n ),\n )\n } else {\n reject(err)\n }\n })\n tester.once('listening', () => {\n tester.close(() => resolve())\n })\n tester.listen(port, '127.0.0.1')\n })\n}\n\n/**\n * Resolve the web package directory. In a global install\n * (`npm install -g neat.is`) the package lives at\n * `node_modules/@neat.is/web/`; in the monorepo it's `packages/web/`.\n * `require.resolve` finds whichever one Node has on its search path.\n */\nfunction resolveWebPackageDir(): string {\n const req = (typeof require !== 'undefined'\n ? require\n : // ESM fallback — daemon CJS bundle has `require`, but typecheck wants this\n (eval('require') as NodeRequire))\n const pkgJsonPath = req.resolve('@neat.is/web/package.json')\n return path.dirname(pkgJsonPath)\n}\n\n/**\n * Locate the standalone server.js inside the web package. ADR-064 #2 — the\n * tarball ships `.next/standalone/packages/web/server.js` (path preserved\n * relative to the monorepo tracing root). Missing means the package was\n * published without `next build` having run, which the smoke-test gate\n * catches at publish time.\n */\nfunction resolveStandaloneServerEntry(webDir: string): string {\n return path.join(webDir, '.next/standalone/packages/web/server.js')\n}\n\n// Find a free loopback port for the lazily-spawned Next server to listen on,\n// behind the public web port. `listen(0)` lets the OS pick.\nasync function pickInternalPort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const srv = net.createServer()\n srv.once('error', reject)\n srv.listen(0, '127.0.0.1', () => {\n const addr = srv.address()\n if (addr && typeof addr === 'object') {\n const port = addr.port\n srv.close(() => resolve(port))\n } else {\n srv.close(() => reject(new Error('neatd: could not pick an internal web port')))\n }\n })\n })\n}\n\n/**\n * Test-only seams. Production callers pass `spawnWebUI(restPort)` untouched —\n * the orchestrator's call signature is unchanged (ADR-096 scope). Tests inject\n * an alternate server entry (a throwaway script) so they can exercise the lazy\n * spawn and the proxy handoff without a real Next build on disk.\n */\nexport interface SpawnWebUIOptions {\n serverEntry?: string\n skipBuildCheck?: boolean\n}\n\nexport async function spawnWebUI(\n restPort: number,\n opts: SpawnWebUIOptions = {},\n): Promise<WebHandle> {\n const root = projectRoot()\n const fromDaemon = await readDaemonPorts(root)\n\n // daemon.json is authoritative; NEAT_WEB_PORT / NEAT_API_URL / the restPort\n // argument are fallbacks for installs predating per-project daemon.json.\n const { webPort: port, apiUrl } = resolveWebPorts({\n daemonWeb: fromDaemon.web,\n daemonRest: fromDaemon.rest,\n webPortEnv: process.env.NEAT_WEB_PORT,\n apiUrlEnv: process.env.NEAT_API_URL,\n restPortArg: restPort,\n })\n\n await assertPortFree(port)\n\n const serverEntry =\n opts.serverEntry ?? resolveStandaloneServerEntry(resolveWebPackageDir())\n // ADR-064 — fail loudly if the standalone build is missing. v0.3.0 shipped\n // without it; the symptom on the user side was `next start` aborting with\n // `Could not find a production build in the '.next' directory`. We check at\n // bind time, before anyone opens the dashboard, so the operator learns about\n // a broken install at start rather than on first visit.\n if (!opts.skipBuildCheck) {\n try {\n require.resolve(serverEntry)\n } catch {\n throw new Error(\n `neatd: web UI standalone build missing at ${serverEntry}. ` +\n `The published @neat.is/web tarball should include it; if you're running from a ` +\n `monorepo checkout, run \\`npm run build --workspace @neat.is/web\\` first, or set ` +\n `NEAT_WEB_DISABLED=1 to skip the web UI.`,\n )\n }\n }\n\n let child: ChildProcess | null = null\n let internalPort = 0\n let starting: Promise<void> | null = null\n let stopped = false\n\n // ADR-096 §7 — bring up the real Next server the first time the dashboard is\n // opened. Idempotent: concurrent first connections share one spawn.\n function ensureStarted(): Promise<void> {\n if (starting) return starting\n starting = (async () => {\n internalPort = await pickInternalPort()\n // ADR-059 #6 — the child inherits NEAT_API_URL pointing at this project's\n // REST server, unless the operator pre-configured it.\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n PORT: String(internalPort),\n HOSTNAME: '127.0.0.1',\n NEAT_API_URL: apiUrl,\n }\n // The standalone bundle is self-contained — its own `node_modules` and\n // `package.json` sit alongside `server.js`. Spawn `node` against it\n // directly; no `next start` (which needs the source tree + build cache)\n // and no `npm exec` (which is monorepo-only in practice).\n // `detached: false` keeps the child in our process group so signals reach it.\n child = spawn(process.execPath, [serverEntry], {\n cwd: path.dirname(serverEntry),\n env,\n stdio: ['ignore', 'inherit', 'inherit'],\n detached: false,\n })\n child.on('error', (err) => {\n console.error(`neatd: web UI spawn error — ${err.message}`)\n })\n await waitForListening(internalPort)\n console.log(`neatd: web UI ready on http://localhost:${port}`)\n })()\n return starting\n }\n\n // The public listener on the web port. It costs nothing until visited; the\n // first connection triggers ensureStarted(), then every socket is piped\n // through to the internal Next server. A raw TCP proxy forwards SSE and any\n // other streaming response transparently.\n // Live front-side sockets, so stop() can force them down. A raw net.Server\n // has no http.Server-style closeAllConnections(), and its close() waits for\n // every open connection to end on its own — which a browser dashboard's\n // keep-alive / EventSource sockets never do (#518).\n const liveSockets = new Set<net.Socket>()\n const front = net.createServer((socket) => {\n liveSockets.add(socket)\n socket.on('close', () => liveSockets.delete(socket))\n socket.on('error', () => socket.destroy())\n ensureStarted()\n .then(() => {\n if (stopped) {\n socket.destroy()\n return\n }\n const upstream = net.connect(internalPort, '127.0.0.1')\n upstream.on('error', () => socket.destroy())\n socket.pipe(upstream)\n upstream.pipe(socket)\n socket.on('close', () => upstream.destroy())\n upstream.on('close', () => socket.destroy())\n })\n .catch((err) => {\n console.error(`neatd: web UI failed to start — ${(err as Error).message}`)\n socket.destroy()\n })\n })\n\n await new Promise<void>((resolve, reject) => {\n front.once('error', reject)\n front.listen(port, '127.0.0.1', () => resolve())\n })\n\n console.log(`neatd: web UI listening on http://localhost:${port} (starts on first open)`)\n\n async function stop(): Promise<void> {\n if (stopped) return\n stopped = true\n await new Promise<void>((resolve) => {\n front.close(() => resolve())\n // `front.close()` only fires its callback once every existing connection\n // has ended. The dashboard holds long-lived keep-alive / EventSource (SSE)\n // sockets piped through this raw TCP front, and a live browser tab never\n // drops them on its own — so destroy the live sockets here and the\n // callback resolves promptly. Graceful stop then completes whether or not\n // a dashboard tab is open.\n for (const socket of liveSockets) socket.destroy()\n liveSockets.clear()\n })\n if (!child || !child.pid) return\n try {\n child.kill('SIGTERM')\n } catch {\n /* already gone */\n }\n // Give the child up to 3s to exit gracefully, then SIGKILL.\n await new Promise<void>((resolve) => {\n const t = setTimeout(() => {\n try {\n child?.kill('SIGKILL')\n } catch {\n /* gone */\n }\n resolve()\n }, 3000)\n child?.once('exit', () => {\n clearTimeout(t)\n resolve()\n })\n })\n }\n\n return {\n get child() {\n return child\n },\n port,\n started: () => child !== null,\n stop,\n }\n}\n\n// Poll the internal port until the Next server accepts a connection, so the\n// first proxied socket doesn't race the child's bind. Bounded so a child that\n// never comes up doesn't hang the request forever.\nasync function waitForListening(port: number, timeoutMs = 15000): Promise<void> {\n const deadline = Date.now() + timeoutMs\n for (;;) {\n const ok = await new Promise<boolean>((resolve) => {\n const s = net.connect(port, '127.0.0.1')\n s.once('connect', () => {\n s.destroy()\n resolve(true)\n })\n s.once('error', () => {\n s.destroy()\n resolve(false)\n })\n })\n if (ok) return\n if (Date.now() > deadline) {\n throw new Error(`neatd: web UI did not start listening on :${port} within ${timeoutMs}ms`)\n }\n await new Promise((r) => setTimeout(r, 150))\n }\n}\n","/**\n * Version-skew check for `neatd start` (ADR-049 §Lifecycle — non-fatal\n * advisory).\n *\n * A globally installed `neat.is` binary may run an older version than the\n * `neat.is` available on npm — happens when the operator has not run\n * `npm install -g neat.is@latest` since the last publish. The six packages\n * move in lockstep (publish-system contract / ADR-052), so the registry\n * version of `neat.is` mirrors the local `@neat.is/core` version.\n *\n * On daemon start we compare the two and log a single advisory line when\n * the registry is ahead. The check is fail-open by design — registry\n * unreachable, slow, or returning an unexpected payload all resolve to\n * \"no warning, daemon proceeds normally.\"\n */\n\nimport { setTimeout as delay } from 'node:timers/promises'\n\nconst NPM_REGISTRY_URL = 'https://registry.npmjs.org/neat.is/latest'\nconst DEFAULT_TIMEOUT_MS = 2000\n\nexport interface VersionSkewCheckOptions {\n // The local version the running daemon reports. Production: read from the\n // bundled @neat.is/core package.json. Tests pass a literal.\n localVersion: string\n // Override the registry URL (tests point at a mock server).\n registryUrl?: string\n // Timeout for the registry fetch. Defaults to 2s.\n timeoutMs?: number\n // Injected so tests can simulate slow responses without real timers.\n fetchImpl?: typeof fetch\n // Sink for the advisory line. Defaults to console.warn.\n warn?: (message: string) => void\n}\n\nexport interface VersionSkewCheckResult {\n // The version the registry reported, or null on any fetch / parse failure.\n remoteVersion: string | null\n // Whether the local version is behind the registry's.\n skewed: boolean\n // Whether the advisory was emitted.\n warned: boolean\n}\n\n/**\n * Fail-open registry lookup. Returns the version string or null on any\n * non-success path (network, timeout, parse, missing field).\n */\nasync function fetchRegistryVersion(\n url: string,\n timeoutMs: number,\n fetchImpl: typeof fetch,\n): Promise<string | null> {\n const controller = new AbortController()\n let timedOut = false\n const timer = delay(timeoutMs).then(() => {\n timedOut = true\n controller.abort()\n })\n try {\n const res = await Promise.race([\n fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }),\n timer.then(() => null),\n ])\n if (timedOut || !res) return null\n if (!res.ok) return null\n const body = (await res.json()) as { version?: unknown }\n if (typeof body.version !== 'string' || body.version.length === 0) return null\n return body.version\n } catch {\n return null\n } finally {\n // Make sure the abort fires so the delay promise resolves even on\n // success — keeps the process from hanging on test teardown.\n if (!timedOut) controller.abort()\n }\n}\n\n/**\n * Compare two semver-shaped strings. Returns true when `local` is strictly\n * older than `remote`. We don't use a real semver library because the\n * advisory is intentionally coarse — any non-equal pair where the registry\n * looks newer is enough to suggest a re-install. The implementation does a\n * plain segment-by-segment integer compare and tolerates pre-release\n * suffixes by stripping them off.\n */\nexport function isLocalBehind(local: string, remote: string): boolean {\n if (local === remote) return false\n const parse = (v: string): number[] => {\n const core = v.split(/[-+]/)[0] ?? v\n return core.split('.').map((s) => {\n const n = Number.parseInt(s, 10)\n return Number.isFinite(n) ? n : 0\n })\n }\n const a = parse(local)\n const b = parse(remote)\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const av = a[i] ?? 0\n const bv = b[i] ?? 0\n if (av < bv) return true\n if (av > bv) return false\n }\n return false\n}\n\nexport async function checkVersionSkew(\n opts: VersionSkewCheckOptions,\n): Promise<VersionSkewCheckResult> {\n const url = opts.registryUrl ?? NPM_REGISTRY_URL\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const fetchImpl = opts.fetchImpl ?? fetch\n const warn = opts.warn ?? ((m) => console.warn(m))\n\n const remote = await fetchRegistryVersion(url, timeoutMs, fetchImpl)\n if (!remote) return { remoteVersion: null, skewed: false, warned: false }\n\n if (!isLocalBehind(opts.localVersion, remote)) {\n return { remoteVersion: remote, skewed: false, warned: false }\n }\n\n warn(\n `[neatd] running neat.is@${opts.localVersion} but neat.is@${remote} is on npm — run \\`npm install -g neat.is@latest\\` to update`,\n )\n return { remoteVersion: remote, skewed: true, warned: true }\n}\n"],"mappings":"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","names":["path","req","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/neatd.ts","../src/web-spawn.ts","../src/version-skew.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `neatd` — distribution-layer daemon CLI (ADR-049).\n *\n * Subcommands:\n * neatd start [--foreground] boot the daemon and watch the registry\n * neatd stop signal the running daemon to shut down\n * neatd reload signal the running daemon to re-read the registry\n * neatd status print PID + per-project last-seen timestamps\n *\n * MVP runs in foreground only. Backgrounding is the supervisor's job\n * (launchd / systemd / nohup) — `neatd start` blocks until SIGINT/SIGTERM.\n *\n * v0.2.10: also brings up the web UI on port 6328 by default (ADR-059).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport { startDaemon, reconcileDaemonRecordSync } from './daemon.js'\nimport { BindAuthorityError } from './auth.js'\nimport { listProjects, registryPath } from './registry.js'\nimport { spawnWebUI, DEFAULT_WEB_PORT, type WebHandle } from './web-spawn.js'\nimport { checkVersionSkew } from './version-skew.js'\n\n// Resolve the running @neat.is/core version from the bundled package.json.\n// Lockstep publishing (ADR-052) keeps this in step with the `neat.is` meta\n// package, which is what the operator installs globally. Tests stub by\n// setting NEAT_LOCAL_VERSION.\nfunction localVersion(): string {\n if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {\n return process.env.NEAT_LOCAL_VERSION\n }\n try {\n const req = createRequire(import.meta.url)\n const pkg = req('../package.json') as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : '0.0.0'\n } catch {\n return '0.0.0'\n }\n}\n\nfunction neatHome(): string {\n if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {\n return path.resolve(process.env.NEAT_HOME)\n }\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\nasync function readPid(): Promise<number | null> {\n try {\n const raw = await fs.readFile(path.join(neatHome(), 'neatd.pid'), 'utf8')\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) ? n : null\n } catch {\n return null\n }\n}\n\nfunction usage(): void {\n console.log('usage: neatd <start|stop|reload|status> [--foreground]')\n}\n\nfunction restPortFromEnv(): number {\n const raw = process.env.PORT\n return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080\n}\n\nfunction webPortFromEnv(): number | undefined {\n const raw = process.env.NEAT_WEB_PORT\n if (!raw || raw.length === 0) return undefined\n const n = Number.parseInt(raw, 10)\n return Number.isFinite(n) ? n : undefined\n}\n\n// Install the daemon's ingest fault-containment handlers (daemon contract\n// §Fault containment, ADR-112). A rejected promise that escapes the ingest\n// drain loop must not dark the whole OBSERVED layer, so `unhandledRejection`\n// is contained: logged loud (error + stack, never silent) and the daemon keeps\n// serving. An `uncaughtException` is a different class of fault — a synchronous\n// throw that reaches the top of the stack leaves the process in an undefined\n// state — so it stays fatal: logged loud, then exit non-zero and let\n// supervision restart clean rather than serve from corrupt state. `onFatal` is\n// injectable so tests can assert the disposition without ending the process.\nexport function installIngestFaultHandlers(\n onFatal: (code: number) => void = (code) => process.exit(code),\n): void {\n const logFault = (kind: string, err: unknown): void => {\n const e = err as Error\n console.error(`neatd: ${kind}. ${e?.stack ?? e?.message ?? String(err)}`)\n }\n process.on('unhandledRejection', (reason) =>\n logFault('unhandled rejection — daemon staying up', reason),\n )\n process.on('uncaughtException', (err) => {\n logFault('uncaught exception — exiting', err)\n onFatal(1)\n })\n}\n\nasync function cmdStart(): Promise<void> {\n // ADR-096 — when the orchestrator spawns a per-project daemon it passes the\n // project name + root + allocated ports through the environment. Forward them\n // into startDaemon so the daemon scopes itself to that one project and writes\n // its daemon.json self-description. A bare `neatd start` with none of these\n // set falls through to the legacy multi-project daemon. (startDaemon also\n // reads PORT / OTEL_PORT / NEAT_WEB_PORT itself; we thread the project bits\n // explicitly so the wiring is visible at the entrypoint.)\n const project = process.env.NEAT_PROJECT\n const projectPath = process.env.NEAT_PROJECT_PATH\n const startOpts =\n project && project.length > 0\n ? { project, projectPath, webPort: webPortFromEnv() }\n : {}\n\n let handle: Awaited<ReturnType<typeof startDaemon>>\n try {\n handle = await startDaemon(startOpts)\n } catch (err) {\n if (err instanceof BindAuthorityError) {\n // ADR-073 §3 — refuse to bind on a public interface without a token.\n // Clean stderr line, exit 1 — no stack trace; the operator wants the\n // single-line directive, not a Node.js Error dump.\n console.error(`neatd: ${err.message}`)\n process.exit(1)\n }\n throw err\n }\n // Ingest fault containment per ADR-112 — a rejected promise that escapes the\n // drain loop is contained (logged, keep serving); an uncaughtException stays\n // fatal. Installed after startDaemon resolves, so the bind paths keep their\n // own fatal handling above.\n installIngestFaultHandlers()\n\n // Reconcile daemon.json on any exit (project-daemon contract §2). The\n // graceful `stop()` path does this asynchronously first; this synchronous\n // backstop covers a crash or fatal signal so a dead daemon never leaves a\n // `running` record pointing clients at a port nothing is listening on.\n process.on('exit', () => {\n if (handle.daemonRecord) {\n reconcileDaemonRecordSync(handle.daemonRecord, handle.neatHome)\n }\n })\n\n console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`)\n console.log(`neatd: registry at ${registryPath()}`)\n // ADR-063 — surface the bound addresses so the operator can sanity-check\n // the documented happy path without grepping startup logs.\n if (handle.restAddress) console.log(`neatd: REST → ${handle.restAddress}`)\n if (handle.otlpAddress) console.log(`neatd: OTLP → ${handle.otlpAddress}`)\n\n // Version-skew advisory — non-fatal, fail-open. Surfaces the gap when the\n // operator's globally installed `neat.is` binary lags the published\n // version. Set NEAT_DISABLE_VERSION_CHECK=1 to silence the check (e.g. in\n // air-gapped environments).\n if (process.env.NEAT_DISABLE_VERSION_CHECK !== '1') {\n void checkVersionSkew({ localVersion: localVersion() }).catch(() => {\n // Fail-open: any thrown error from the helper resolves silently. The\n // helper already catches its own internals; this catches anything\n // exotic the Promise chain bubbles up.\n })\n }\n\n // ADR-059 — bring up the web UI alongside the daemon. Failure here aborts\n // start with the clear-error pattern from ADR-049 instead of silently\n // running headless.\n const skipWeb = process.env.NEAT_WEB_DISABLED === '1'\n let web: WebHandle | null = null\n if (!skipWeb) {\n try {\n web = await spawnWebUI(restPortFromEnv())\n } catch (err) {\n console.error((err as Error).message)\n await handle.stop().catch(() => {})\n process.exit(3)\n }\n } else {\n console.log('neatd: web UI disabled (NEAT_WEB_DISABLED=1)')\n }\n\n console.log('neatd: SIGHUP reloads, SIGTERM/SIGINT stops')\n\n let stopping = false\n const shutdown = (signal: NodeJS.Signals): void => {\n if (stopping) return\n stopping = true\n console.log(`neatd: ${signal} received, stopping…`)\n void Promise.allSettled([handle.stop(), web ? web.stop() : Promise.resolve()])\n .catch((err) => console.error(`neatd: shutdown error — ${(err as Error).message}`))\n .finally(() => process.exit(0))\n }\n process.on('SIGTERM', shutdown)\n process.on('SIGINT', shutdown)\n\n // Block forever — supervisors keep us in the foreground.\n await new Promise<void>(() => {})\n}\n\nasync function cmdStop(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGTERM')\n console.log(`neatd: SIGTERM sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdReload(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGHUP')\n console.log(`neatd: SIGHUP sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdStatus(): Promise<void> {\n const pid = await readPid()\n console.log(`pid: ${pid ?? '(not running)'}`)\n console.log(`registry: ${registryPath()}`)\n const webPort = process.env.NEAT_WEB_PORT\n ? Number.parseInt(process.env.NEAT_WEB_PORT, 10)\n : DEFAULT_WEB_PORT\n console.log(`web ui: http://localhost:${webPort}`)\n const projects = await listProjects().catch(() => [])\n if (projects.length === 0) {\n console.log('projects: (none)')\n return\n }\n console.log('projects:')\n for (const p of projects) {\n const seen = p.lastSeenAt ?? 'never'\n console.log(` ${p.name}\\t${p.status}\\t${p.path}\\tlast-seen=${seen}`)\n }\n}\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2]\n if (!cmd || cmd === '-h' || cmd === '--help') {\n usage()\n process.exit(cmd ? 0 : 2)\n }\n\n if (cmd === 'start') return cmdStart()\n if (cmd === 'stop') return cmdStop()\n if (cmd === 'reload') return cmdReload()\n if (cmd === 'status') return cmdStatus()\n\n console.error(`neatd: unknown command \"${cmd}\"`)\n usage()\n process.exit(1)\n}\n\nconst entry = process.argv[1] ?? ''\nif (/[\\\\/](?:neatd\\.(?:cjs|js)|neatd)$/.test(entry)) {\n main().catch((err) => {\n console.error(err)\n process.exit(1)\n })\n}\n","/**\n * Web UI spawn helper (ADR-059, ADR-096).\n *\n * A project's daemon brings up the REST API + OTel receivers, then calls\n * `spawnWebUI(restPort)` to make its dashboard reachable. Lifecycle is\n * parent-tied: SIGTERM/SIGINT on the daemon cascades to the web child via\n * `stop()`.\n *\n * Per-project ports (ADR-096 §5). The dashboard binds its own port and points\n * at its own REST API, both read from the project's\n * `<projectRoot>/neat-out/daemon.json` — the single source of truth for \"where\n * is this project's daemon\" (project-daemon contract §2). The `restPort`\n * argument is tolerated as a fallback so callers that still pass it keep\n * working, but `daemon.json` wins when present. A missing or malformed\n * `daemon.json` falls back to the canonical `6328`/`8080`; the read never\n * throws.\n *\n * Lazy spawn (ADR-096 §7). The web port is bound up front by a thin listener,\n * but the heavyweight Next.js server is not started until someone actually\n * opens the dashboard. A daemon that nobody is looking at keeps no web process\n * resident — this matters once there's one daemon per project and the cost\n * would otherwise multiply by N.\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport { promises as fsp } from 'node:fs'\nimport net from 'node:net'\nimport path from 'node:path'\n\nexport const DEFAULT_WEB_PORT = 6328\nexport const DEFAULT_REST_PORT = 8080\n\nexport interface WebHandle {\n /** The Next.js child once it's been lazily spawned; null until first open. */\n readonly child: ChildProcess | null\n port: number\n /** True once the underlying Next.js server has actually been spawned. */\n started: () => boolean\n stop: () => Promise<void>\n}\n\n// The slice of daemon.json we read. Other fields exist (pid, status, …) but the\n// web UI only needs the ports, so we narrow to those and tolerate the rest.\ninterface DaemonPortsShape {\n rest?: unknown\n web?: unknown\n}\ninterface DaemonRecordShape {\n ports?: DaemonPortsShape\n}\n\nfunction asValidPort(value: unknown): number | null {\n const n = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10)\n return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null\n}\n\n/**\n * Resolve the project root whose `neat-out/daemon.json` describes this\n * daemon. `NEAT_SCAN_PATH` is the canonical project-root env (the REST server\n * reads the same name); `process.cwd()` is the fallback for a daemon launched\n * from inside the project.\n */\nfunction projectRoot(): string {\n const fromEnv = process.env.NEAT_SCAN_PATH\n return path.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd())\n}\n\n/**\n * Read `<projectRoot>/neat-out/daemon.json` and pull out the web + REST ports.\n * Bulletproof: a missing file, unreadable file, bad JSON, or a malformed port\n * all resolve to `null` for that field — never a throw. Authoritative when\n * present (project-daemon contract §2), so its ports take precedence over the\n * `restPort` argument and over `NEAT_WEB_PORT`.\n */\nexport async function readDaemonPorts(\n root: string,\n): Promise<{ web: number | null; rest: number | null }> {\n try {\n const raw = await fsp.readFile(path.join(root, 'neat-out', 'daemon.json'), 'utf8')\n const parsed = JSON.parse(raw) as DaemonRecordShape\n const ports = parsed?.ports ?? {}\n return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) }\n } catch {\n return { web: null, rest: null }\n }\n}\n\n/**\n * Pure port resolution, factored out so it's testable without binding a\n * socket. `daemon.json` is the source of truth (ADR-096 §2); `NEAT_WEB_PORT`\n * and the `restPort` argument are fallbacks for installs predating the file.\n */\nexport function resolveWebPorts(args: {\n daemonWeb: number | null\n daemonRest: number | null\n webPortEnv: string | undefined\n apiUrlEnv: string | undefined\n restPortArg: number\n}): { webPort: number; apiUrl: string } {\n const { daemonWeb, daemonRest, webPortEnv, apiUrlEnv, restPortArg } = args\n\n const portFromEnv = webPortEnv && webPortEnv.length > 0 ? Number.parseInt(webPortEnv, 10) : null\n if (portFromEnv !== null && (!Number.isFinite(portFromEnv) || portFromEnv <= 0 || portFromEnv > 65535)) {\n throw new Error(`neatd: invalid NEAT_WEB_PORT=\"${webPortEnv}\"`)\n }\n const webPort = daemonWeb ?? portFromEnv ?? DEFAULT_WEB_PORT\n\n const restPort = daemonRest ?? asValidPort(restPortArg) ?? DEFAULT_REST_PORT\n const apiUrl = apiUrlEnv ?? `http://localhost:${restPort}`\n\n return { webPort, apiUrl }\n}\n\n/**\n * Best-effort port collision check before binding. Binds, closes, returns. A\n * race between the check and the real bind is acceptable — the placeholder's\n * own `listen` will then fail loudly and the parent exits.\n */\nasync function assertPortFree(port: number): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const tester = net.createServer()\n tester.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(\n new Error(\n `neatd: web UI port ${port} in use; set NEAT_WEB_PORT to override or stop the conflicting process`,\n ),\n )\n } else {\n reject(err)\n }\n })\n tester.once('listening', () => {\n tester.close(() => resolve())\n })\n tester.listen(port, '127.0.0.1')\n })\n}\n\n/**\n * Resolve the web package directory. In a global install\n * (`npm install -g neat.is`) the package lives at\n * `node_modules/@neat.is/web/`; in the monorepo it's `packages/web/`.\n * `require.resolve` finds whichever one Node has on its search path.\n */\nfunction resolveWebPackageDir(): string {\n const req = (typeof require !== 'undefined'\n ? require\n : // ESM fallback — daemon CJS bundle has `require`, but typecheck wants this\n (eval('require') as NodeRequire))\n const pkgJsonPath = req.resolve('@neat.is/web/package.json')\n return path.dirname(pkgJsonPath)\n}\n\n/**\n * Locate the standalone server.js inside the web package. ADR-064 #2 — the\n * tarball ships `.next/standalone/packages/web/server.js` (path preserved\n * relative to the monorepo tracing root). Missing means the package was\n * published without `next build` having run, which the smoke-test gate\n * catches at publish time.\n */\nfunction resolveStandaloneServerEntry(webDir: string): string {\n return path.join(webDir, '.next/standalone/packages/web/server.js')\n}\n\n// Find a free loopback port for the lazily-spawned Next server to listen on,\n// behind the public web port. `listen(0)` lets the OS pick.\nasync function pickInternalPort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const srv = net.createServer()\n srv.once('error', reject)\n srv.listen(0, '127.0.0.1', () => {\n const addr = srv.address()\n if (addr && typeof addr === 'object') {\n const port = addr.port\n srv.close(() => resolve(port))\n } else {\n srv.close(() => reject(new Error('neatd: could not pick an internal web port')))\n }\n })\n })\n}\n\n/**\n * Test-only seams. Production callers pass `spawnWebUI(restPort)` untouched —\n * the orchestrator's call signature is unchanged (ADR-096 scope). Tests inject\n * an alternate server entry (a throwaway script) so they can exercise the lazy\n * spawn and the proxy handoff without a real Next build on disk.\n */\nexport interface SpawnWebUIOptions {\n serverEntry?: string\n skipBuildCheck?: boolean\n}\n\nexport async function spawnWebUI(\n restPort: number,\n opts: SpawnWebUIOptions = {},\n): Promise<WebHandle> {\n const root = projectRoot()\n const fromDaemon = await readDaemonPorts(root)\n\n // daemon.json is authoritative; NEAT_WEB_PORT / NEAT_API_URL / the restPort\n // argument are fallbacks for installs predating per-project daemon.json.\n const { webPort: port, apiUrl } = resolveWebPorts({\n daemonWeb: fromDaemon.web,\n daemonRest: fromDaemon.rest,\n webPortEnv: process.env.NEAT_WEB_PORT,\n apiUrlEnv: process.env.NEAT_API_URL,\n restPortArg: restPort,\n })\n\n await assertPortFree(port)\n\n const serverEntry =\n opts.serverEntry ?? resolveStandaloneServerEntry(resolveWebPackageDir())\n // ADR-064 — fail loudly if the standalone build is missing. v0.3.0 shipped\n // without it; the symptom on the user side was `next start` aborting with\n // `Could not find a production build in the '.next' directory`. We check at\n // bind time, before anyone opens the dashboard, so the operator learns about\n // a broken install at start rather than on first visit.\n if (!opts.skipBuildCheck) {\n try {\n require.resolve(serverEntry)\n } catch {\n throw new Error(\n `neatd: web UI standalone build missing at ${serverEntry}. ` +\n `The published @neat.is/web tarball should include it; if you're running from a ` +\n `monorepo checkout, run \\`npm run build --workspace @neat.is/web\\` first, or set ` +\n `NEAT_WEB_DISABLED=1 to skip the web UI.`,\n )\n }\n }\n\n let child: ChildProcess | null = null\n let internalPort = 0\n let starting: Promise<void> | null = null\n let stopped = false\n\n // ADR-096 §7 — bring up the real Next server the first time the dashboard is\n // opened. Idempotent: concurrent first connections share one spawn.\n function ensureStarted(): Promise<void> {\n if (starting) return starting\n starting = (async () => {\n internalPort = await pickInternalPort()\n // ADR-059 #6 — the child inherits NEAT_API_URL pointing at this project's\n // REST server, unless the operator pre-configured it.\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n PORT: String(internalPort),\n HOSTNAME: '127.0.0.1',\n NEAT_API_URL: apiUrl,\n }\n // The standalone bundle is self-contained — its own `node_modules` and\n // `package.json` sit alongside `server.js`. Spawn `node` against it\n // directly; no `next start` (which needs the source tree + build cache)\n // and no `npm exec` (which is monorepo-only in practice).\n // `detached: false` keeps the child in our process group so signals reach it.\n child = spawn(process.execPath, [serverEntry], {\n cwd: path.dirname(serverEntry),\n env,\n stdio: ['ignore', 'inherit', 'inherit'],\n detached: false,\n })\n child.on('error', (err) => {\n console.error(`neatd: web UI spawn error — ${err.message}`)\n })\n await waitForListening(internalPort)\n console.log(`neatd: web UI ready on http://localhost:${port}`)\n })()\n return starting\n }\n\n // The public listener on the web port. It costs nothing until visited; the\n // first connection triggers ensureStarted(), then every socket is piped\n // through to the internal Next server. A raw TCP proxy forwards SSE and any\n // other streaming response transparently.\n // Live front-side sockets, so stop() can force them down. A raw net.Server\n // has no http.Server-style closeAllConnections(), and its close() waits for\n // every open connection to end on its own — which a browser dashboard's\n // keep-alive / EventSource sockets never do (#518).\n const liveSockets = new Set<net.Socket>()\n const front = net.createServer((socket) => {\n liveSockets.add(socket)\n socket.on('close', () => liveSockets.delete(socket))\n socket.on('error', () => socket.destroy())\n ensureStarted()\n .then(() => {\n if (stopped) {\n socket.destroy()\n return\n }\n const upstream = net.connect(internalPort, '127.0.0.1')\n upstream.on('error', () => socket.destroy())\n socket.pipe(upstream)\n upstream.pipe(socket)\n socket.on('close', () => upstream.destroy())\n upstream.on('close', () => socket.destroy())\n })\n .catch((err) => {\n console.error(`neatd: web UI failed to start — ${(err as Error).message}`)\n socket.destroy()\n })\n })\n\n await new Promise<void>((resolve, reject) => {\n front.once('error', reject)\n front.listen(port, '127.0.0.1', () => resolve())\n })\n\n console.log(`neatd: web UI listening on http://localhost:${port} (starts on first open)`)\n\n async function stop(): Promise<void> {\n if (stopped) return\n stopped = true\n await new Promise<void>((resolve) => {\n front.close(() => resolve())\n // `front.close()` only fires its callback once every existing connection\n // has ended. The dashboard holds long-lived keep-alive / EventSource (SSE)\n // sockets piped through this raw TCP front, and a live browser tab never\n // drops them on its own — so destroy the live sockets here and the\n // callback resolves promptly. Graceful stop then completes whether or not\n // a dashboard tab is open.\n for (const socket of liveSockets) socket.destroy()\n liveSockets.clear()\n })\n if (!child || !child.pid) return\n try {\n child.kill('SIGTERM')\n } catch {\n /* already gone */\n }\n // Give the child up to 3s to exit gracefully, then SIGKILL.\n await new Promise<void>((resolve) => {\n const t = setTimeout(() => {\n try {\n child?.kill('SIGKILL')\n } catch {\n /* gone */\n }\n resolve()\n }, 3000)\n child?.once('exit', () => {\n clearTimeout(t)\n resolve()\n })\n })\n }\n\n return {\n get child() {\n return child\n },\n port,\n started: () => child !== null,\n stop,\n }\n}\n\n// Poll the internal port until the Next server accepts a connection, so the\n// first proxied socket doesn't race the child's bind. Bounded so a child that\n// never comes up doesn't hang the request forever.\nasync function waitForListening(port: number, timeoutMs = 15000): Promise<void> {\n const deadline = Date.now() + timeoutMs\n for (;;) {\n const ok = await new Promise<boolean>((resolve) => {\n const s = net.connect(port, '127.0.0.1')\n s.once('connect', () => {\n s.destroy()\n resolve(true)\n })\n s.once('error', () => {\n s.destroy()\n resolve(false)\n })\n })\n if (ok) return\n if (Date.now() > deadline) {\n throw new Error(`neatd: web UI did not start listening on :${port} within ${timeoutMs}ms`)\n }\n await new Promise((r) => setTimeout(r, 150))\n }\n}\n","/**\n * Version-skew check for `neatd start` (ADR-049 §Lifecycle — non-fatal\n * advisory).\n *\n * A globally installed `neat.is` binary may run an older version than the\n * `neat.is` available on npm — happens when the operator has not run\n * `npm install -g neat.is@latest` since the last publish. The six packages\n * move in lockstep (publish-system contract / ADR-052), so the registry\n * version of `neat.is` mirrors the local `@neat.is/core` version.\n *\n * On daemon start we compare the two and log a single advisory line when\n * the registry is ahead. The check is fail-open by design — registry\n * unreachable, slow, or returning an unexpected payload all resolve to\n * \"no warning, daemon proceeds normally.\"\n */\n\nimport { setTimeout as delay } from 'node:timers/promises'\n\nconst NPM_REGISTRY_URL = 'https://registry.npmjs.org/neat.is/latest'\nconst DEFAULT_TIMEOUT_MS = 2000\n\nexport interface VersionSkewCheckOptions {\n // The local version the running daemon reports. Production: read from the\n // bundled @neat.is/core package.json. Tests pass a literal.\n localVersion: string\n // Override the registry URL (tests point at a mock server).\n registryUrl?: string\n // Timeout for the registry fetch. Defaults to 2s.\n timeoutMs?: number\n // Injected so tests can simulate slow responses without real timers.\n fetchImpl?: typeof fetch\n // Sink for the advisory line. Defaults to console.warn.\n warn?: (message: string) => void\n}\n\nexport interface VersionSkewCheckResult {\n // The version the registry reported, or null on any fetch / parse failure.\n remoteVersion: string | null\n // Whether the local version is behind the registry's.\n skewed: boolean\n // Whether the advisory was emitted.\n warned: boolean\n}\n\n/**\n * Fail-open registry lookup. Returns the version string or null on any\n * non-success path (network, timeout, parse, missing field).\n */\nasync function fetchRegistryVersion(\n url: string,\n timeoutMs: number,\n fetchImpl: typeof fetch,\n): Promise<string | null> {\n const controller = new AbortController()\n let timedOut = false\n const timer = delay(timeoutMs).then(() => {\n timedOut = true\n controller.abort()\n })\n try {\n const res = await Promise.race([\n fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }),\n timer.then(() => null),\n ])\n if (timedOut || !res) return null\n if (!res.ok) return null\n const body = (await res.json()) as { version?: unknown }\n if (typeof body.version !== 'string' || body.version.length === 0) return null\n return body.version\n } catch {\n return null\n } finally {\n // Make sure the abort fires so the delay promise resolves even on\n // success — keeps the process from hanging on test teardown.\n if (!timedOut) controller.abort()\n }\n}\n\n/**\n * Compare two semver-shaped strings. Returns true when `local` is strictly\n * older than `remote`. We don't use a real semver library because the\n * advisory is intentionally coarse — any non-equal pair where the registry\n * looks newer is enough to suggest a re-install. The implementation does a\n * plain segment-by-segment integer compare and tolerates pre-release\n * suffixes by stripping them off.\n */\nexport function isLocalBehind(local: string, remote: string): boolean {\n if (local === remote) return false\n const parse = (v: string): number[] => {\n const core = v.split(/[-+]/)[0] ?? v\n return core.split('.').map((s) => {\n const n = Number.parseInt(s, 10)\n return Number.isFinite(n) ? n : 0\n })\n }\n const a = parse(local)\n const b = parse(remote)\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const av = a[i] ?? 0\n const bv = b[i] ?? 0\n if (av < bv) return true\n if (av > bv) return false\n }\n return false\n}\n\nexport async function checkVersionSkew(\n opts: VersionSkewCheckOptions,\n): Promise<VersionSkewCheckResult> {\n const url = opts.registryUrl ?? NPM_REGISTRY_URL\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const fetchImpl = opts.fetchImpl ?? fetch\n const warn = opts.warn ?? ((m) => console.warn(m))\n\n const remote = await fetchRegistryVersion(url, timeoutMs, fetchImpl)\n if (!remote) return { remoteVersion: null, skewed: false, warned: false }\n\n if (!isLocalBehind(opts.localVersion, remote)) {\n return { remoteVersion: remote, skewed: false, warned: false }\n }\n\n warn(\n `[neatd] running neat.is@${opts.localVersion} but neat.is@${remote} is on npm — run \\`npm install -g neat.is@latest\\` to update`,\n )\n return { remoteVersion: remote, skewed: true, warned: true }\n}\n"],"mappings":"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","names":["path","req","path"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neat.is/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30-dev.20260716",
|
|
4
4
|
"description": "NEAT graph engine: tree-sitter extraction, OTel ingest, REST API",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"homepage": "https://neat.is",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@fastify/cors": "^10.0.0",
|
|
52
52
|
"@grpc/grpc-js": "^1.14.3",
|
|
53
53
|
"@grpc/proto-loader": "^0.8.0",
|
|
54
|
-
"@neat.is/types": "^0.4.
|
|
54
|
+
"@neat.is/types": "^0.4.30-dev.20260716",
|
|
55
55
|
"chokidar": "^4.0.3",
|
|
56
56
|
"fastify": "^5.0.0",
|
|
57
57
|
"graphology": "^0.25.4",
|