@kb-labs/shared-daemon 2.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @kb-labs/shared-daemon
|
|
2
|
+
|
|
3
|
+
Universal process-lifecycle bootstrapper for KB Labs daemon services.
|
|
4
|
+
|
|
5
|
+
`runDaemon()` owns the boilerplate every daemon repeats — repo-root
|
|
6
|
+
resolution, `.env` loading, platform bootstrap, port/host resolution from
|
|
7
|
+
env, and graceful `SIGTERM`/`SIGINT` shutdown — so each service only writes
|
|
8
|
+
its own `setup()`.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { runDaemon } from '@kb-labs/shared-daemon';
|
|
14
|
+
import { createServiceBootstrap } from '@kb-labs/core-runtime';
|
|
15
|
+
import { makeAssemblyHook } from '@kb-labs/plugin-runtime';
|
|
16
|
+
import { getListenOptions } from '@kb-labs/shared-http';
|
|
17
|
+
|
|
18
|
+
await runDaemon(
|
|
19
|
+
{
|
|
20
|
+
appId: 'my-service',
|
|
21
|
+
defaultPort: 5070,
|
|
22
|
+
portEnvVar: 'MY_SERVICE_PORT',
|
|
23
|
+
hostEnvVar: 'MY_SERVICE_HOST',
|
|
24
|
+
async setup({ platform, port, host, repoRoot }) {
|
|
25
|
+
const server = await createServer({ platform, repoRoot });
|
|
26
|
+
await server.listen(getListenOptions(port, host));
|
|
27
|
+
return () => server.close(); // teardown — runs before platform.shutdown()
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
// DI: keeps makeAssemblyHook (Layer 1) out of this package
|
|
31
|
+
(appId, repoRoot) =>
|
|
32
|
+
createServiceBootstrap({ appId, repoRoot, assemblyHook: makeAssemblyHook() }),
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What it handles
|
|
37
|
+
|
|
38
|
+
- **Env + platform** — resolves `repoRoot` via `findRepoRoot`, loads `.env`,
|
|
39
|
+
bootstraps the platform through the injected factory.
|
|
40
|
+
- **Socket isolation** — seeds `KB_SOCKET_HASH` (from `KB_PROJECT_ROOT ?? repoRoot`)
|
|
41
|
+
before config interpolation so `${KB_SOCKET_HASH}` socket paths resolve, matching
|
|
42
|
+
kb-dev's per-project hash.
|
|
43
|
+
- **Port/host** — read from the configured env vars with defaults.
|
|
44
|
+
- **Shutdown** — a single re-entrant handler runs `setup()`'s teardown, then
|
|
45
|
+
`platform.shutdown()`, then exits; a second signal is ignored.
|
|
46
|
+
|
|
47
|
+
Pairs with `createDaemonServer()` from `@kb-labs/shared-http`, which owns the
|
|
48
|
+
HTTP-layer boilerplate (Fastify, request correlation, observability routes).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PlatformContainer } from '@kb-labs/core-runtime';
|
|
2
|
+
import { ILogger } from '@kb-labs/core-platform';
|
|
3
|
+
|
|
4
|
+
interface DaemonContext {
|
|
5
|
+
platform: PlatformContainer;
|
|
6
|
+
logger: ILogger;
|
|
7
|
+
port: number;
|
|
8
|
+
host: string;
|
|
9
|
+
/**
|
|
10
|
+
* Repository root resolved via findRepoRoot(process.cwd()). Services should
|
|
11
|
+
* use this (not process.cwd()) for plugin/workflow discovery so they behave
|
|
12
|
+
* correctly when launched from a subdirectory.
|
|
13
|
+
*/
|
|
14
|
+
repoRoot: string;
|
|
15
|
+
}
|
|
16
|
+
interface DaemonConfig {
|
|
17
|
+
appId: string;
|
|
18
|
+
/**
|
|
19
|
+
* serviceId in the transport map (declarative network). Defaults to appId.
|
|
20
|
+
* Set explicitly when they differ. The daemon binds the port the transport
|
|
21
|
+
* publishes for this serviceId — keeping bind and route consistent (incl.
|
|
22
|
+
* any KB_NET_OFFSET shift).
|
|
23
|
+
*/
|
|
24
|
+
serviceId?: string;
|
|
25
|
+
defaultPort: number;
|
|
26
|
+
portEnvVar: string;
|
|
27
|
+
defaultHost?: string;
|
|
28
|
+
hostEnvVar?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Called after platform is ready. Returns a teardown callback.
|
|
31
|
+
* The callback is invoked before platform.shutdown() on SIGTERM/SIGINT.
|
|
32
|
+
*/
|
|
33
|
+
setup(ctx: DaemonContext): Promise<() => Promise<void>>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Universal process-lifecycle runner for KB Labs daemon services.
|
|
37
|
+
*
|
|
38
|
+
* Handles: env loading, platform init, port/host resolution,
|
|
39
|
+
* setup invocation, and graceful SIGTERM/SIGINT shutdown.
|
|
40
|
+
*
|
|
41
|
+
* @param config - Service-specific configuration
|
|
42
|
+
* @param platformBootstrap - DI factory for platform (keeps makeAssemblyHook out of this package)
|
|
43
|
+
* Example: (appId, repoRoot) => createServiceBootstrap({ appId, repoRoot, assemblyHook: makeAssemblyHook() })
|
|
44
|
+
*/
|
|
45
|
+
declare function runDaemon(config: DaemonConfig, platformBootstrap: (appId: string, repoRoot: string) => Promise<PlatformContainer>): Promise<void>;
|
|
46
|
+
|
|
47
|
+
export { type DaemonConfig, type DaemonContext, runDaemon };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { findRepoRoot } from '@kb-labs/core-sys';
|
|
3
|
+
import { loadEnvFromRoot } from '@kb-labs/core-runtime';
|
|
4
|
+
|
|
5
|
+
// src/daemon.ts
|
|
6
|
+
async function runDaemon(config, platformBootstrap) {
|
|
7
|
+
const repoRoot = await findRepoRoot(process.cwd());
|
|
8
|
+
loadEnvFromRoot(repoRoot);
|
|
9
|
+
if (!process.env.KB_SOCKET_HASH) {
|
|
10
|
+
const hashRoot = process.env.KB_PROJECT_ROOT ?? repoRoot;
|
|
11
|
+
process.env.KB_SOCKET_HASH = createHash("md5").update(hashRoot).digest("hex").slice(0, 8);
|
|
12
|
+
}
|
|
13
|
+
const platform = await platformBootstrap(config.appId, repoRoot);
|
|
14
|
+
const logger = platform.logger.child({
|
|
15
|
+
serviceId: config.appId,
|
|
16
|
+
service: "bootstrap"
|
|
17
|
+
});
|
|
18
|
+
const serviceId = config.serviceId ?? config.appId;
|
|
19
|
+
const transport = platform.getAdapter("serviceTransport");
|
|
20
|
+
const addr = transport?.listenAddress?.(serviceId);
|
|
21
|
+
const netOffset = Number(process.env.KB_NET_OFFSET) || 0;
|
|
22
|
+
const port = addr && "port" in addr ? addr.port : (process.env[config.portEnvVar] ? parseInt(process.env[config.portEnvVar], 10) : config.defaultPort) + netOffset;
|
|
23
|
+
const addrHost = addr && "host" in addr ? addr.host : void 0;
|
|
24
|
+
const host = config.hostEnvVar && process.env[config.hostEnvVar] ? process.env[config.hostEnvVar] : addrHost ?? config.defaultHost ?? "0.0.0.0";
|
|
25
|
+
logger.info(`${config.appId}: starting`, { port, host });
|
|
26
|
+
const teardown = await config.setup({ platform, logger, port, host, repoRoot });
|
|
27
|
+
let shuttingDown = false;
|
|
28
|
+
const shutdown = async (signal) => {
|
|
29
|
+
if (shuttingDown) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
shuttingDown = true;
|
|
33
|
+
logger.warn(`${config.appId}: received ${signal}`);
|
|
34
|
+
await teardown();
|
|
35
|
+
await platform.shutdown();
|
|
36
|
+
logger.info(`${config.appId}: stopped`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
};
|
|
39
|
+
process.on("SIGTERM", async () => {
|
|
40
|
+
await shutdown("SIGTERM");
|
|
41
|
+
});
|
|
42
|
+
process.on("SIGINT", async () => {
|
|
43
|
+
await shutdown("SIGINT");
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { runDaemon };
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daemon.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { createHash } from 'node:crypto';\nimport { findRepoRoot } from '@kb-labs/core-sys';\nimport { loadEnvFromRoot } from '@kb-labs/core-runtime';\nimport type { PlatformContainer } from '@kb-labs/core-runtime';\nimport type { ILogger, IServiceTransport } from '@kb-labs/core-platform';\n\nexport interface DaemonContext {\n platform: PlatformContainer;\n logger: ILogger;\n port: number;\n host: string;\n /**\n * Repository root resolved via findRepoRoot(process.cwd()). Services should\n * use this (not process.cwd()) for plugin/workflow discovery so they behave\n * correctly when launched from a subdirectory.\n */\n repoRoot: string;\n}\n\nexport interface DaemonConfig {\n appId: string;\n /**\n * serviceId in the transport map (declarative network). Defaults to appId.\n * Set explicitly when they differ. The daemon binds the port the transport\n * publishes for this serviceId — keeping bind and route consistent (incl.\n * any KB_NET_OFFSET shift).\n */\n serviceId?: string;\n defaultPort: number;\n portEnvVar: string;\n defaultHost?: string;\n hostEnvVar?: string;\n /**\n * Called after platform is ready. Returns a teardown callback.\n * The callback is invoked before platform.shutdown() on SIGTERM/SIGINT.\n */\n setup(ctx: DaemonContext): Promise<() => Promise<void>>;\n}\n\n/**\n * Universal process-lifecycle runner for KB Labs daemon services.\n *\n * Handles: env loading, platform init, port/host resolution,\n * setup invocation, and graceful SIGTERM/SIGINT shutdown.\n *\n * @param config - Service-specific configuration\n * @param platformBootstrap - DI factory for platform (keeps makeAssemblyHook out of this package)\n * Example: (appId, repoRoot) => createServiceBootstrap({ appId, repoRoot, assemblyHook: makeAssemblyHook() })\n */\nexport async function runDaemon(\n config: DaemonConfig,\n platformBootstrap: (appId: string, repoRoot: string) => Promise<PlatformContainer>,\n): Promise<void> {\n const repoRoot = await findRepoRoot(process.cwd());\n loadEnvFromRoot(repoRoot);\n\n // Ensure KB_SOCKET_HASH is set before interpolateConfig() runs inside platformBootstrap.\n // kb-dev sets it via spawnEnv() for all managed services (including gateway, rest-api).\n // For manual starts (dev scripts, local testing), derive it the same way kb-dev does —\n // from the project dir (KB_PROJECT_ROOT), falling back to repoRoot — so a manually\n // started service lands in the same /tmp/kb-<hash>/ dir as kb-dev-managed peers.\n if (!process.env.KB_SOCKET_HASH) {\n const hashRoot = process.env.KB_PROJECT_ROOT ?? repoRoot;\n process.env.KB_SOCKET_HASH = createHash('md5').update(hashRoot).digest('hex').slice(0, 8);\n }\n\n const platform = await platformBootstrap(config.appId, repoRoot);\n const logger = platform.logger.child({\n serviceId: config.appId,\n service: 'bootstrap',\n });\n\n // Bind port from the transport (the single declarative network source): the\n // daemon listens on exactly the port the transport publishes for its\n // serviceId, so bind and route stay consistent — including any KB_NET_OFFSET\n // shift. Socket services resolve their bind via KB_SOCKET_PATH (setup →\n // getListenOptions), so listenAddress returns socketPath and we keep the\n // fallback port. Services NOT in the transport map (e.g. state-daemon — not\n // gateway-routed) are treated as edges: the fallback applies KB_NET_OFFSET\n // directly, so the one offset knob still shifts their bind. Host stays the\n // daemon's own concern (offset never affects host).\n const serviceId = config.serviceId ?? config.appId;\n const transport = platform.getAdapter<IServiceTransport>('serviceTransport');\n const addr = transport?.listenAddress?.(serviceId);\n const netOffset = Number(process.env.KB_NET_OFFSET) || 0;\n const port = addr && 'port' in addr\n ? addr.port\n : (process.env[config.portEnvVar]\n ? parseInt(process.env[config.portEnvVar]!, 10)\n : config.defaultPort) + netOffset;\n\n // Host precedence: explicit env override > transport's advisory host (set only\n // by adapters that own the bind host, e.g. k8s) > daemon default.\n const addrHost = addr && 'host' in addr ? addr.host : undefined;\n const host = (config.hostEnvVar && process.env[config.hostEnvVar])\n ? process.env[config.hostEnvVar]!\n : (addrHost ?? config.defaultHost ?? '0.0.0.0');\n\n logger.info(`${config.appId}: starting`, { port, host } as Record<string, unknown>);\n\n const teardown = await config.setup({ platform, logger, port, host, repoRoot });\n\n // Guard against a second signal (e.g. SIGTERM then Ctrl-C) re-running teardown\n // against already-closed resources.\n let shuttingDown = false;\n const shutdown = async (signal: string) => {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n logger.warn(`${config.appId}: received ${signal}`);\n await teardown();\n await platform.shutdown();\n logger.info(`${config.appId}: stopped`);\n process.exit(0);\n };\n\n process.on('SIGTERM', async () => { await shutdown('SIGTERM'); });\n process.on('SIGINT', async () => { await shutdown('SIGINT'); });\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/shared-daemon",
|
|
3
|
+
"description": "Universal process-lifecycle bootstrapper for KB Labs daemon services",
|
|
4
|
+
"version": "2.96.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@kb-labs/core-platform": "2.96.0",
|
|
20
|
+
"@kb-labs/core-runtime": "2.96.0",
|
|
21
|
+
"@kb-labs/core-sys": "2.96.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.3.3",
|
|
25
|
+
"rimraf": "^6.0.1",
|
|
26
|
+
"tsup": "^8.5.0",
|
|
27
|
+
"typescript": "^5.6.3",
|
|
28
|
+
"vitest": "^3.2.6",
|
|
29
|
+
"@kb-labs/devkit": "2.96.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.0.0",
|
|
33
|
+
"pnpm": ">=9.0.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup --config tsup.config.ts",
|
|
41
|
+
"clean": "rimraf dist",
|
|
42
|
+
"dev": "tsup --config tsup.config.ts --watch",
|
|
43
|
+
"lint": "eslint src",
|
|
44
|
+
"lint:fix": "eslint src --fix",
|
|
45
|
+
"test": "vitest run --passWithNoTests",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"type-check": "tsc --noEmit"
|
|
48
|
+
}
|
|
49
|
+
}
|