fix(runtime): separate packaged JSON-RPC resolution

This commit is contained in:
Yichen Jiang
2026-08-10 21:39:47 +08:00
parent 7175ba3d5e
commit 523764a582
18 changed files with 141 additions and 85 deletions

View File

@@ -1,54 +1,11 @@
#!/usr/bin/env node
/**
* Boots an external `cordis.yml`; its `@deepseek-ai/dsh-jsonrpc` entry serves
* newline-delimited JSON-RPC on stdio. `$DSH_CORDIS_CONFIG` wins over `argv[2]`;
* empty or missing paths exit 1, with no default config or `DSH_SNAPSHOT` mode.
* App-boot owns env loading, Loader guards, and settled-tree startup.
* stdin EOF and SIGTERM dispose the root context and exit 0; SIGINT exits 130.
* Protocol `shutdown` belongs to the server plugin. Stdout is reserved for frames.
* Generic JSON-RPC agent bin. External configurations own their bare plugin
* packages; the packaged runtime uses `packaged-bin.ts` instead.
*
* @module @deepseek-ai/dsh-jsonrpc-demo/bin
*/
import { existsSync } from 'node:fs'
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runJsonrpcAgent } from './runner.ts'
const NAME = 'dsh-jsonrpc-agent'
/* v8 ignore start -- composition over tested app-boot/jsonrpc and executable acceptance paths */
installFailLoud(NAME)
loadEnv(NAME)
// Env wins over argv; empty values are absent. External config defines the deployment.
const fromEnv = process.env['DSH_CORDIS_CONFIG']
const fromArgv = process.argv[2]
const requested = fromEnv !== undefined && fromEnv !== ''
? fromEnv
: fromArgv !== undefined && fromArgv !== '' ? fromArgv : undefined
const configPath = requested === undefined ? undefined : resolveConfigPath(requested, undefined)
if (configPath === undefined || !existsSync(configPath)) {
process.stderr.write(
`usage: ${NAME} <path/to/cordis.yml> (or set DSH_CORDIS_CONFIG=<path>, which wins); the config is required — there is no built-in fallback\n`,
)
process.exit(1)
}
// The executable owns a closed plugin set; config-adjacent node_modules must
// not shadow the packages embedded beside this bin in the VFS.
const ctx = await boot(NAME, configPath, undefined, undefined, import.meta.url)
let exiting = false
async function disposeAndExit(code: number): Promise<void> {
if (exiting) return
exiting = true
try {
await ctx.fiber.dispose()
} finally {
process.exit(code)
}
}
process.stdin.on('end', () => { void disposeAndExit(0) })
process.on('SIGTERM', () => { void disposeAndExit(0) })
process.on('SIGINT', () => { void disposeAndExit(130) })
/* v8 ignore stop */
await runJsonrpcAgent()

View File

@@ -1,7 +1,8 @@
/**
* Bin-only app package: `bin.ts` discovers an external `cordis.yml` and owns
* process exit. This module exports no composition plugin; the config chooses
* whether to load the {@link @deepseek-ai/dsh-jsonrpc} serving plugin.
* Bin-only app package: its generic and packaged entries discover an external
* `cordis.yml` and own process exit. This module exports no composition plugin;
* the config chooses whether to load the
* {@link @deepseek-ai/dsh-jsonrpc} serving plugin.
*
* @module @deepseek-ai/dsh-jsonrpc-demo
*/

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env node
/**
* Closed-runtime JSON-RPC agent bin. Bare plugins resolve from the installed
* runtime closure while relative plugins remain configuration-relative.
*
* @module @deepseek-ai/dsh-jsonrpc-demo/packaged-bin
*/
import { runJsonrpcAgent } from './runner.ts'
await runJsonrpcAgent(import.meta.url)

View File

@@ -0,0 +1,55 @@
/**
* Shared process lifecycle for the generic and closed-runtime JSON-RPC bins.
*
* @module @deepseek-ai/dsh-jsonrpc-demo/runner
*/
import { existsSync } from 'node:fs'
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
const NAME = 'dsh-jsonrpc-agent'
/**
* Boot the explicitly selected external configuration and own process exit.
* @param bareModuleBaseUrl - optional installed-runtime base for bare plugins;
* omit it when the configuration project owns its plugin packages.
* @returns after process handlers are installed; process lifetime then belongs
* to stdin and signal events.
*/
export async function runJsonrpcAgent(bareModuleBaseUrl?: string): Promise<void> {
/* v8 ignore start -- composition over tested app-boot/jsonrpc and executable acceptance paths */
installFailLoud(NAME)
loadEnv(NAME)
// Env wins over argv; empty values are absent. External config defines the deployment.
const fromEnv = process.env['DSH_CORDIS_CONFIG']
const fromArgv = process.argv[2]
const requested = fromEnv !== undefined && fromEnv !== ''
? fromEnv
: fromArgv !== undefined && fromArgv !== '' ? fromArgv : undefined
const configPath = requested === undefined ? undefined : resolveConfigPath(requested, undefined)
if (configPath === undefined || !existsSync(configPath)) {
process.stderr.write(
`usage: ${NAME} <path/to/cordis.yml> (or set DSH_CORDIS_CONFIG=<path>, which wins); the config is required — there is no built-in fallback\n`,
)
process.exit(1)
}
const ctx = await boot(NAME, configPath, undefined, undefined, bareModuleBaseUrl)
let exiting = false
async function disposeAndExit(code: number): Promise<void> {
if (exiting) return
exiting = true
try {
await ctx.fiber.dispose()
} finally {
process.exit(code)
}
}
process.stdin.on('end', () => { void disposeAndExit(0) })
process.on('SIGTERM', () => { void disposeAndExit(0) })
process.on('SIGINT', () => { void disposeAndExit(130) })
/* v8 ignore stop */
}