Skip to content

Commit aa7c889

Browse files
grypezclaude
andcommitted
feat(cli): integrate daemon commands
Wire daemon commands into the CLI via a new `kernel` command group. `daemon-process.ts` manages the daemon child process, and `kernel/index.ts` dispatches sub-commands (start, stop, status, etc.) through the IPC client. Adds e2e test scaffolding for daemon lifecycle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bb268c4 commit aa7c889

File tree

9 files changed

+432
-4
lines changed

9 files changed

+432
-4
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"test:integration": "yarn workspaces foreach --all run test:integration",
3838
"test:verbose": "yarn test --reporter verbose",
3939
"test:watch": "vitest",
40-
"why:batch": "./scripts/why-batch.sh"
40+
"why:batch": "./scripts/why-batch.sh",
41+
"root": "./scripts/echo-root.sh"
4142
},
4243
"simple-git-hooks": {
4344
"pre-commit": "./scripts/pre-commit.sh",

packages/cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"test:verbose": "yarn test --reporter verbose",
3232
"test:watch": "vitest --config vitest.config.ts",
3333
"test:integration": "vitest run --config vitest.integration.config.ts",
34+
"test:e2e": "vitest run --config vitest.config.e2e.ts",
3435
"test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent"
3536
},
3637
"dependencies": {
@@ -49,6 +50,7 @@
4950
"@metamask/kernel-utils": "workspace:^",
5051
"@metamask/logger": "workspace:^",
5152
"@metamask/utils": "^11.9.0",
53+
"@ocap/kernel-daemon": "workspace:^",
5254
"@types/node": "^22.13.1",
5355
"acorn": "^8.15.0",
5456
"chokidar": "^4.0.1",
@@ -82,6 +84,7 @@
8284
"eslint-plugin-n": "^17.17.0",
8385
"eslint-plugin-prettier": "^5.2.6",
8486
"eslint-plugin-promise": "^7.2.1",
87+
"execa": "^9.5.2",
8588
"jsdom": "^27.4.0",
8689
"prettier": "^3.5.3",
8790
"rimraf": "^6.0.1",

packages/cli/src/app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import yargs from 'yargs';
66
import { hideBin } from 'yargs/helpers';
77

88
import { bundleSource } from './commands/bundle.ts';
9+
import { registerKernelCommands } from './commands/kernel/index.ts';
910
import { getServer } from './commands/serve.ts';
1011
import { watchDir } from './commands/watch.ts';
1112
import { defaultConfig } from './config.ts';
@@ -175,4 +176,6 @@ const yargsInstance = yargs(hideBin(process.argv))
175176
},
176177
);
177178

179+
registerKernelCommands(yargsInstance, logger);
180+
178181
await yargsInstance.help('help').parse();
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Daemon process entry point.
3+
* This file is forked as a detached child process by `startDaemon`.
4+
* It creates a kernel, starts the RPC server, and writes the PID file.
5+
*/
6+
import '@metamask/kernel-shims/endoify-node';
7+
8+
// These packages are used at runtime in the daemon process but cannot be
9+
// listed as direct dependencies of @ocap/cli due to Turbo cyclic dependency
10+
// constraints (they depend on @metamask/ocap-kernel).
11+
/* eslint-disable import-x/no-extraneous-dependencies */
12+
import { rpcHandlers } from '@metamask/kernel-browser-runtime';
13+
import { RpcService } from '@metamask/kernel-rpc-methods';
14+
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
15+
import { Logger } from '@metamask/logger';
16+
import { Kernel } from '@metamask/ocap-kernel';
17+
import {
18+
DB_FILE,
19+
PID_FILE,
20+
SOCK_FILE,
21+
LOG_FILE,
22+
createDaemonServer,
23+
} from '@ocap/kernel-daemon';
24+
import { NodejsPlatformServices } from '@ocap/nodejs';
25+
/* eslint-enable import-x/no-extraneous-dependencies */
26+
import { appendFile, writeFile, access, unlink } from 'node:fs/promises';
27+
import type { Server } from 'node:net';
28+
29+
const logger = new Logger('kernel-daemon');
30+
31+
let server: Server | undefined;
32+
let kernel: Kernel | undefined;
33+
34+
/**
35+
* Redirect logger output to log file.
36+
*
37+
* @param message - The message to log.
38+
* @param args - Additional arguments.
39+
*/
40+
async function logToFile(message: string, ...args: unknown[]): Promise<void> {
41+
const timestamp = new Date().toISOString();
42+
const line = `[${timestamp}] ${message} ${args.map(String).join(' ')}\n`;
43+
await appendFile(LOG_FILE, line);
44+
}
45+
46+
/**
47+
* Check whether a file exists at the given path.
48+
*
49+
* @param filePath - The path to check.
50+
* @returns True if the file exists.
51+
*/
52+
async function fileExists(filePath: string): Promise<boolean> {
53+
try {
54+
await access(filePath);
55+
return true;
56+
} catch {
57+
return false;
58+
}
59+
}
60+
61+
/**
62+
* Perform graceful shutdown: stop kernel, close server, clean up files.
63+
*/
64+
async function shutdown(): Promise<void> {
65+
await logToFile('Shutting down daemon...');
66+
67+
if (kernel) {
68+
try {
69+
await kernel.stop();
70+
} catch (error) {
71+
await logToFile('Error stopping kernel:', String(error));
72+
}
73+
}
74+
75+
if (server) {
76+
server.close();
77+
}
78+
79+
if (await fileExists(PID_FILE)) {
80+
await unlink(PID_FILE);
81+
}
82+
if (await fileExists(SOCK_FILE)) {
83+
await unlink(SOCK_FILE);
84+
}
85+
86+
// eslint-disable-next-line n/no-process-exit
87+
process.exit(0);
88+
}
89+
90+
/**
91+
*
92+
*/
93+
async function main(): Promise<void> {
94+
await logToFile('Starting daemon process...');
95+
96+
// Write PID file
97+
await writeFile(PID_FILE, String(process.pid));
98+
await logToFile(`PID ${process.pid} written to ${PID_FILE}`);
99+
100+
// Create platform services
101+
const platformServices = new NodejsPlatformServices({
102+
logger: logger.subLogger({ tags: ['platform-services'] }),
103+
});
104+
105+
// Create kernel database with persistent storage
106+
const kernelDatabase = await makeSQLKernelDatabase({
107+
dbFilename: DB_FILE,
108+
});
109+
110+
// Create kernel
111+
kernel = await Kernel.make(platformServices, kernelDatabase, {
112+
logger: logger.subLogger({ tags: ['kernel'] }),
113+
});
114+
115+
await logToFile('Kernel created successfully');
116+
117+
// Initialize kernel identity (peer ID, crypto) for OCAP URL operations
118+
await kernel.initIdentity();
119+
await logToFile('Kernel identity initialized');
120+
121+
// Build the RPC dispatcher from the standard kernel handlers
122+
const rpcService = new RpcService(rpcHandlers, {
123+
kernel,
124+
executeDBQuery: (sql: string) => kernelDatabase.executeQuery(sql),
125+
});
126+
127+
// Start RPC server
128+
server = createDaemonServer({
129+
rpcDispatcher: rpcService,
130+
logger: logger.subLogger({ tags: ['rpc-server'] }),
131+
onShutdown: shutdown,
132+
});
133+
134+
await logToFile('Daemon server started');
135+
136+
// Register signal handlers for graceful shutdown
137+
process.on('SIGINT', () => {
138+
shutdown().catch(async (error) =>
139+
logToFile('Error during shutdown:', String(error)),
140+
);
141+
});
142+
process.on('SIGTERM', () => {
143+
shutdown().catch(async (error) =>
144+
logToFile('Error during shutdown:', String(error)),
145+
);
146+
});
147+
}
148+
149+
main().catch(async (error) => {
150+
await logToFile('Fatal error starting daemon:', String(error));
151+
// eslint-disable-next-line n/no-process-exit
152+
process.exit(1);
153+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Logger, makeTaglessConsoleTransport } from '@metamask/logger';
2+
import { makeFileTransport } from '@metamask/logger/file-transport';
3+
import { handleDaemonStart, registerDaemonCommands } from '@ocap/kernel-daemon';
4+
import { homedir } from 'node:os';
5+
import { join } from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
import type { Argv } from 'yargs';
8+
9+
const DAEMON_LOG_FILE = join(homedir(), '.ocap-kernel-daemon', 'daemon.log');
10+
11+
/**
12+
* Create a logger for daemon CLI commands that writes to console (without tags)
13+
* and to the daemon log file (with tags).
14+
*
15+
* @returns A Logger configured with console and file transports.
16+
*/
17+
function makeDaemonLogger(): Logger {
18+
return new Logger({
19+
tags: ['daemon'],
20+
transports: [
21+
makeTaglessConsoleTransport(),
22+
makeFileTransport(DAEMON_LOG_FILE),
23+
],
24+
});
25+
}
26+
27+
/**
28+
* Register the `kernel` command group on the given yargs instance.
29+
*
30+
* @param yargs - The yargs instance to extend.
31+
* @param _logger - Logger for command output.
32+
* @returns The extended yargs instance.
33+
*/
34+
export function registerKernelCommands(yargs: Argv, _logger: Logger): Argv {
35+
return yargs.command(
36+
'kernel [command]',
37+
'Manage the ocap kernel daemon',
38+
(_yargs) =>
39+
_yargs.showHelpOnFail(false).command(
40+
'daemon [command]',
41+
'Manage the background kernel daemon',
42+
(yg) => {
43+
const daemonLogger = makeDaemonLogger();
44+
const daemonProcessPath = fileURLToPath(
45+
new URL('./daemon-process.mjs', import.meta.url),
46+
);
47+
const getMethodSpecs = async (): Promise<
48+
Record<string, { method: string }>
49+
> => {
50+
// eslint-disable-next-line import-x/no-extraneous-dependencies
51+
const { rpcMethodSpecs } = await import(
52+
'@metamask/kernel-browser-runtime/rpc-handlers'
53+
);
54+
return rpcMethodSpecs;
55+
};
56+
return registerDaemonCommands(yg, {
57+
logger: daemonLogger,
58+
getMethodSpecs,
59+
daemonProcessPath,
60+
});
61+
},
62+
async (args) => {
63+
if (!args.command) {
64+
const daemonLogger = makeDaemonLogger();
65+
const daemonProcessPath = fileURLToPath(
66+
new URL('./daemon-process.mjs', import.meta.url),
67+
);
68+
try {
69+
await handleDaemonStart(daemonProcessPath, daemonLogger);
70+
} catch (error) {
71+
if (
72+
error instanceof Error &&
73+
error.message.startsWith('Daemon already running')
74+
) {
75+
daemonLogger.info(error.message);
76+
} else {
77+
throw error;
78+
}
79+
}
80+
// eslint-disable-next-line n/no-process-exit
81+
process.exit(0);
82+
}
83+
},
84+
),
85+
(_args) => {
86+
// no-op: bare `kernel` shows help via demandCommand
87+
},
88+
);
89+
}

0 commit comments

Comments
 (0)