refactor(cmdline)!: an app's entrypoint provides values its rows read

Replaces the patch round trip. An app's entrypoint resolves the command
line into a service, and the rows it configures read that service from
their own config — port: !!js ctx.get('webStartup')?.port ?? 3080 — so the
resolved value beats the value written beside it and nothing is written
back into a row or handed to the launcher.

A bundle names the entrypoint row in its manifest (dsh.bundle.entrypoint),
which is what lets the boot mount in two passes: entrypoints alone, then
the whole composition. That ordering is required, not cosmetic — a row's
config expressions are evaluated when the include applies the row, and a
strict ctx.get only answers for a service whose providing fiber is already
active.

What this removes: ctx.appPatches and the launcher-owned patch layer, the
disable/re-enable recycle and its in-flight-mount barrier, overrideConfig,
and the reload hazard they existed for. A live config edit now re-applies
the second pass against services that are still up, so a served port
survives by construction.

What it adds: ctx.appReady, because Loader settlement no longer means the
app is up — a row mounted in the second pass can observe a settled tree
while that pass is still running, or already rolling back. The web URL line
waits for it, so a boot that fails in the second pass announces nothing.
This commit is contained in:
Turtle
2026-08-07 11:58:03 +08:00
parent f749e04881
commit 1f0a0440f3
23 changed files with 720 additions and 593 deletions

View File

@@ -13,6 +13,7 @@
import { createRequire } from 'node:module'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { enableRow } from '@deepseek-ai/dsh-cmdline'
import * as FrontendStatic from '@deepseek-ai/dsh-frontend-static'
import type {} from '@deepseek-ai/cordis-plugin-loader'
import type {} from '@deepseek-ai/dsh-host-webserver'
@@ -22,6 +23,9 @@ import type {} from '@deepseek-ai/dsh-bash-env'
/** Stable Cordis plugin name. */
export const name = 'web-app'
/** The client-plugin reload chain row this bundle ships disabled, for `--dev`. */
const HMR_ROW_ID = 'client-hmr'
/** Services required before the web runtime can mount. */
export const inject = ['httpServer']
@@ -112,6 +116,11 @@ export const internals: { resolveDistIndex: () => string } = { resolveDistIndex
*/
export function apply(ctx: Context, config: Config): void {
ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() })
// The client-plugin reload chain is a row this bundle ships off, because it
// exists only in development. Turning it on belongs here rather than in the
// entrypoint: it needs the host rows this phase of the boot mounts, and the
// entrypoint runs before them.
if (config.mode === 'development') void enableRow(ctx, HMR_ROW_ID)
if (config.surfaceContext) {
ctx.inject(['systemPrompt'], (promptCtx) => {
promptCtx.systemPrompt.section({
@@ -143,15 +152,20 @@ export function apply(ctx: Context, config: Config): void {
const port = ctx.httpServer.port
console.log(`dsh web: ${localWebUrl(ctx)}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate}:${String(port)})`}`)
}
const loader = ctx.get('loader')
if (loader === undefined) printUrl()
// A launcher that mounts in phases tells this row when the whole
// composition is up; Loader settlement alone would let the line print
// between phases, announcing a server whose boot can still fail. A
// hand-built tree has neither and prints at once.
const settled = ctx.get('appReady') ?? ctx.get('loader')?.await()
if (settled === undefined) printUrl()
else {
void loader.await().then(() => {
// The tree can be disposed while settlement was in flight (early
void settled.then(() => {
// The tree can be disposed while the boot was in flight (early
// SIGTERM); a URL line for a dead server would only mislead, and
// reading the torn-down port would turn a clean shutdown into a crash.
if (ctx.get('httpServer') !== undefined) printUrl()
})
// A failed boot is reported by the launcher; this row only stays quiet.
}, () => {})
}
}
}