Merge remote-tracking branch 'origin/master' into worktree/drop-create-by-name
# Conflicts: # .agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.i18n.yaml # .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml # .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md # .agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.i18n.yaml # .agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md # .agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.zh.md # apps/cli/reference/README.i18n.yaml # docs/config-catalog.md # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/api-proxy-approval.spec.ts # packages/host/apiproxy/tests/api-proxy-blank.spec.ts # packages/host/apiproxy/tests/api-proxy-cold.spec.ts # packages/host/apiproxy/tests/api-proxy-commands.spec.ts # packages/host/apiproxy/tests/api-proxy-config.spec.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/host/apiproxy/tests/api-proxy-projections.spec.ts # packages/host/apiproxy/tests/api-proxy-question.spec.ts # packages/host/apiproxy/tests/api-proxy-rename.spec.ts # packages/host/apiproxy/tests/api-proxy-search.spec.ts # packages/host/apiproxy/tests/api-proxy-subagents.spec.ts # packages/host/apiproxy/tests/api-proxy-view.spec.ts # packages/host/apiproxy/tests/api-proxy-workspace.spec.ts # packages/todo/tool-todo/tests/projection.spec.ts # scripts/hero-composer-dom-continuity.mjs
This commit is contained in:
4118 files changed
+128795
-32407
No files matched your search
+41
-17
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Commander adapter for the `dsh` command-line entry. The default command
|
||||
* boots a named profile (`--profile <name>`), optionally with extra `--patch`
|
||||
* overlays and a positional task (one-shot mode for profiles mounting the
|
||||
* headless runner). `web` is a hardcoded alias for `--profile web` that adds
|
||||
* the Web flag family; `plugin` manages a profile's plugin dependencies by
|
||||
* forwarding to pnpm. Commander owns help, version, and parse errors.
|
||||
* overlays. `run` owns one-shot task execution, defaulting to the headless
|
||||
* profile; `web` is a hardcoded alias for `--profile web` that adds the Web
|
||||
* flag family; `plugin` manages a profile's plugin dependencies by forwarding
|
||||
* to pnpm. Commander owns help, version, and parse errors.
|
||||
* @module @deepseek-ai/dsh/args
|
||||
*/
|
||||
|
||||
@@ -16,8 +16,16 @@ interface ProfileInvocation {
|
||||
profile: string
|
||||
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
|
||||
patches: string[]
|
||||
/** Positional task text joined by spaces; non-empty only for one-shot runs. */
|
||||
task?: string
|
||||
}
|
||||
|
||||
/** Run one task through a profile mounting the headless runner. */
|
||||
interface RunInvocation {
|
||||
mode: 'run'
|
||||
profile: string
|
||||
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
|
||||
patches: string[]
|
||||
/** Non-blank task text joined from the variadic positional arguments. */
|
||||
task: string
|
||||
}
|
||||
|
||||
/** Print a composed profile tree and exit without booting. */
|
||||
@@ -53,7 +61,7 @@ interface PluginInvocation {
|
||||
}
|
||||
|
||||
/** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */
|
||||
export type DshInvocation = ProfileInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation
|
||||
export type DshInvocation = ProfileInvocation | RunInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation
|
||||
|
||||
/** Raw web-subcommand options straight from Commander. */
|
||||
interface WebOptions {
|
||||
@@ -66,6 +74,12 @@ interface WebOptions {
|
||||
dumpDefaultConfig?: boolean
|
||||
}
|
||||
|
||||
/** Raw run-subcommand options straight from Commander. */
|
||||
interface RunOptions {
|
||||
profile: string
|
||||
patch?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never
|
||||
* variadic — a variadic `--patch` would swallow a following positional task.
|
||||
@@ -88,19 +102,19 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
|
||||
.addHelpText('after', `
|
||||
Examples:
|
||||
dsh --profile web boot the web profile (same as: dsh web)
|
||||
dsh --profile headless "run the tests" answer one task, print the result, and exit
|
||||
dsh run "run the tests" answer one task, print the result, and exit
|
||||
dsh run --profile custom "run the tests" run one task through a custom one-shot profile
|
||||
dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay
|
||||
dsh plugin --profile tui add <package> install a plugin into the tui profile
|
||||
dsh web --port 8080 the web alias with its flag family
|
||||
`)
|
||||
.exitOverride()
|
||||
.enablePositionalOptions()
|
||||
.argument('[task...]', 'one-shot task text for profiles mounting the headless runner')
|
||||
.option('--profile <name>', 'the profile under $DSH_HOME/profiles to boot')
|
||||
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
|
||||
.option('--dump-config', 'print the composed profile tree and exit')
|
||||
.option('--dump-default-config', 'print the profile tree without its user layer or --patch overlays and exit')
|
||||
.action((task: string[], options: {
|
||||
.action((options: {
|
||||
profile?: string
|
||||
patch?: string[]
|
||||
dumpConfig?: boolean
|
||||
@@ -114,7 +128,6 @@ Examples:
|
||||
if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
|
||||
program.error('error: --dump-config and --dump-default-config are mutually exclusive')
|
||||
}
|
||||
if (task.length > 0) program.error('error: --dump-config/--dump-default-config take no task')
|
||||
const defaultOnly = options.dumpDefaultConfig === true
|
||||
if (defaultOnly && patches.length > 0) {
|
||||
program.error('error: --dump-default-config prints the bundle layers and takes no --patch')
|
||||
@@ -122,12 +135,7 @@ Examples:
|
||||
resolved = { mode: 'dump-config', profile, defaultOnly, patches }
|
||||
return
|
||||
}
|
||||
resolved = {
|
||||
mode: 'profile',
|
||||
profile,
|
||||
patches,
|
||||
...task.length > 0 ? { task: task.join(' ') } : {},
|
||||
}
|
||||
resolved = { mode: 'profile', profile, patches }
|
||||
})
|
||||
|
||||
/** Reject parent options that crossed a subcommand boundary. */
|
||||
@@ -144,6 +152,22 @@ Examples:
|
||||
}
|
||||
}
|
||||
|
||||
const run = program.command('run').description('run one task through a profile mounting the headless runner')
|
||||
run
|
||||
.option('--profile <name>', 'one-shot profile under $DSH_HOME/profiles', 'headless')
|
||||
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
|
||||
.argument('<task...>', 'task text')
|
||||
.action((task: string[], options: RunOptions) => {
|
||||
rejectParentOptions('run')
|
||||
const profile = options.profile
|
||||
if (profile === '') program.error('error: --profile needs a name')
|
||||
const patches = options.patch ?? []
|
||||
if (patches.includes('')) program.error('error: --patch needs a path')
|
||||
const joined = task.join(' ')
|
||||
if (joined.trim() === '') program.error('error: run needs a non-blank task')
|
||||
resolved = { mode: 'run', profile, patches, task: joined }
|
||||
})
|
||||
|
||||
const web = program.command('web').description('serve the browser UI (alias of --profile web) on the configured host and port')
|
||||
web
|
||||
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
|
||||
|
||||
+13
-4
@@ -10,7 +10,7 @@
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { loadEnv } from '@deepseek-ai/dsh-app-boot'
|
||||
import { loadLayeredEnv } from '@deepseek-ai/dsh-app-boot'
|
||||
import { parseDshArgs } from './args.ts'
|
||||
|
||||
// Both the source tree (apps/cli/src) and the bundled bin (apps/cli/lib) sit
|
||||
@@ -24,22 +24,31 @@ function readVersion(): string {
|
||||
return typeof manifest.version === 'string' ? manifest.version : '0.0.0'
|
||||
}
|
||||
|
||||
loadEnv('dsh')
|
||||
const invocation = parseDshArgs(process.argv.slice(2), readVersion())
|
||||
|
||||
switch (invocation.mode) {
|
||||
case 'profile': {
|
||||
const { runProfile } = await import('./profile-boot.ts')
|
||||
await runProfile({
|
||||
environment: loadLayeredEnv('dsh'),
|
||||
profile: invocation.profile,
|
||||
patchFiles: invocation.patches,
|
||||
...invocation.task !== undefined && { task: invocation.task },
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'run': {
|
||||
const { runProfile } = await import('./profile-boot.ts')
|
||||
await runProfile({
|
||||
environment: loadLayeredEnv('dsh'),
|
||||
profile: invocation.profile,
|
||||
patchFiles: invocation.patches,
|
||||
task: invocation.task,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'web': {
|
||||
const { runWeb } = await import('./web.ts')
|
||||
await runWeb(invocation)
|
||||
await runWeb(invocation, loadLayeredEnv('dsh'))
|
||||
break
|
||||
}
|
||||
case 'plugin': {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Config-dump entry for `dsh --profile <name> --dump-config`: compose the
|
||||
* profile's patch layers through the include plugin's patch algorithm without
|
||||
* booting or evaluating `!!js`, with one provenance layer per bundle, the
|
||||
* booting or evaluating `!!js`, with one source layer per bundle, the
|
||||
* profile's own patch file, and each `--patch` overlay.
|
||||
* @module @deepseek-ai/dsh/dump-config
|
||||
*/
|
||||
@@ -15,12 +15,13 @@ import {
|
||||
type ConfigDumpLayer,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { homePatchPath, prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
/* v8 ignore start -- built-bin acceptance drives this boot-free dispatch */
|
||||
/**
|
||||
* Print a profile composition with provenance comments.
|
||||
* Print a profile composition with comments naming each source file and patch layer.
|
||||
* @param profile - the profile name.
|
||||
* @param defaultOnly - omit the profile's user layer and `--patch` overlays
|
||||
* (the recovery diagnostic for a broken `cordis.patch.yml`, which is then
|
||||
@@ -33,6 +34,12 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re
|
||||
label: layer.packageName,
|
||||
patches: layer.patches,
|
||||
}))
|
||||
// The win32 shell platform layer rides between bundles and user layers,
|
||||
// exactly where the boot applies it.
|
||||
const windowsShellLayer = resolveWindowsShellLayer(process.platform, loaded.layers, NAME)
|
||||
if (windowsShellLayer !== undefined) {
|
||||
layers.push({ label: windowsShellLayer.label, patches: windowsShellLayer.patches })
|
||||
}
|
||||
if (!defaultOnly) {
|
||||
if (existsSync(loaded.patchPath)) {
|
||||
layers.push({ label: loaded.patchPath, patches: loaded.patches })
|
||||
|
||||
@@ -5,54 +5,73 @@ export const PROCESS_SHUTDOWN_TIMEOUT_MS = 5_000
|
||||
|
||||
/** Process-exit controller shared by normal completion and Unix signal handlers. */
|
||||
export interface ProcessShutdown {
|
||||
/** Start or join graceful disposal before exiting with `code`. */
|
||||
/** Start or join graceful disposal before allowing natural completion with `code`. */
|
||||
shutdown(code: number): Promise<void>
|
||||
/** Start graceful disposal, or force exit when a shutdown is already running. */
|
||||
/** Start graceful disposal followed by exit, or force exit when shutdown is already running. */
|
||||
interrupt(code: number): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one process-exit controller around an application disposer.
|
||||
* @param dispose - Whole-application teardown that resolves at quiescence.
|
||||
* @param exit - Process exit boundary, replaceable by tests.
|
||||
* @param forceExit - Forced process exit boundary, replaceable by tests.
|
||||
* @param complete - Natural process completion boundary, replaceable by tests.
|
||||
* @param timeoutMs - Grace before forced exit, replaceable by tests.
|
||||
* @returns A controller whose normal calls coalesce and whose repeated signal call escalates.
|
||||
*/
|
||||
export function createProcessShutdown(
|
||||
dispose: () => Promise<void>,
|
||||
exit: (code: number) => void = (code) => { process.exit(code) },
|
||||
forceExit: (code: number) => void = (code) => { process.exit(code) },
|
||||
complete: (code: number) => void = (code) => { process.exitCode = code },
|
||||
timeoutMs = PROCESS_SHUTDOWN_TIMEOUT_MS,
|
||||
): ProcessShutdown {
|
||||
let pending: Promise<void> | undefined
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
let exited = false
|
||||
let completed = false
|
||||
let forceExited = false
|
||||
|
||||
const exitOnce = (code: number): void => {
|
||||
if (exited) return
|
||||
exited = true
|
||||
const clearExitTimeout = (): void => {
|
||||
/* v8 ignore else -- shutdown() arms the timer before any asynchronous exit path can run. */
|
||||
if (timeout !== undefined) clearTimeout(timeout)
|
||||
exit(code)
|
||||
}
|
||||
|
||||
const shutdown = (code: number): Promise<void> => {
|
||||
const forceExitOnce = (code: number): void => {
|
||||
if (forceExited) return
|
||||
forceExited = true
|
||||
clearExitTimeout()
|
||||
forceExit(code)
|
||||
}
|
||||
|
||||
const completeOnce = (code: number): void => {
|
||||
if (completed || forceExited) return
|
||||
completed = true
|
||||
clearExitTimeout()
|
||||
complete(code)
|
||||
}
|
||||
|
||||
const start = (code: number, forceAfterDispose: boolean): Promise<void> => {
|
||||
if (pending !== undefined) return pending
|
||||
timeout = setTimeout(() => { exitOnce(code) }, timeoutMs)
|
||||
timeout = setTimeout(() => { forceExitOnce(code) }, timeoutMs)
|
||||
pending = Promise.resolve().then(dispose).then(
|
||||
() => { exitOnce(code) },
|
||||
() => { exitOnce(code) },
|
||||
() => {
|
||||
if (forceAfterDispose) forceExitOnce(code)
|
||||
else completeOnce(code)
|
||||
},
|
||||
() => { forceExitOnce(code) },
|
||||
)
|
||||
return pending
|
||||
}
|
||||
|
||||
return {
|
||||
shutdown,
|
||||
shutdown(code) {
|
||||
return start(code, false)
|
||||
},
|
||||
interrupt(code) {
|
||||
if (pending !== undefined) {
|
||||
exitOnce(code)
|
||||
forceExitOnce(code)
|
||||
return
|
||||
}
|
||||
void shutdown(code)
|
||||
void start(code, true)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,9 @@
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from 'cordis'
|
||||
import { FiberState, type Context } from 'cordis'
|
||||
import type { PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-paths'
|
||||
import {
|
||||
boot,
|
||||
composeEntries,
|
||||
@@ -25,8 +26,16 @@ import {
|
||||
type Profile,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
|
||||
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
|
||||
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url))
|
||||
|
||||
/** Harness-home directory holding locally authored agent presets. */
|
||||
const USER_PRESET_DIR = '.agent-presets'
|
||||
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import type { HeadlessIo } from '@deepseek-ai/dsh-headless'
|
||||
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -46,7 +55,7 @@ export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.me
|
||||
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
|
||||
const TELEMETRY_ROW_ID = 'telemetry-otel'
|
||||
|
||||
/** The one-shot runner row a positional task requires and configures. */
|
||||
/** The one-shot runner row a `dsh run` task requires and configures. */
|
||||
const HEADLESS_ROW_ID = 'headless-runner'
|
||||
|
||||
/** The empty root entry list every profile tree patches over. */
|
||||
@@ -103,6 +112,8 @@ interface ComposedProfile {
|
||||
profile: Profile
|
||||
/** Bundle layers concatenated — the part below the user layers on a live reload. */
|
||||
bundlePatches: PatchOptions[]
|
||||
/** The win32 shell platform layer (the base bundle's `windows.cordis.patch.yml`), between bundles and user layers. */
|
||||
windowsShellPatches: PatchOptions[]
|
||||
/** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
|
||||
homePatches: PatchOptions[]
|
||||
/** Layers above the user layers on a live reload: --patch overlays, flag patches, the telemetry switch. */
|
||||
@@ -117,12 +128,19 @@ interface ComposedProfile {
|
||||
|
||||
/** The full patch stack of one composed profile, in application order. */
|
||||
function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
return [...composed.bundlePatches, ...composed.profile.patches, ...composed.homePatches, ...composed.overlayAndFlags]
|
||||
return [
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...composed.profile.patches,
|
||||
...composed.homePatches,
|
||||
...composed.overlayAndFlags,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Load `name` and compose its effective patch stack: bundle layers in
|
||||
* `dsh.profile.bundles` order, the profile's user layer, the home-level user layer
|
||||
* `dsh.profile.bundles` order, the win32 shell platform layer (when the host
|
||||
* is Windows), the profile's user layer, the home-level user layer
|
||||
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
|
||||
* every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* then flag patches derived from the composed rows, then the telemetry
|
||||
@@ -141,14 +159,33 @@ function composeProfile(
|
||||
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? []
|
||||
const rows = new Map<string, { name?: string; config?: unknown }>()
|
||||
for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
|
||||
for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
}
|
||||
const overlayAndFlags = [...overlays, ...deriveFlagPatches(rows)]
|
||||
// The agent-preset roots are an assembly fact of every dsh launcher, not a
|
||||
// patch author's choice: the shipped set sits beside this app's config and
|
||||
// the user's own under the Harness home. Resolved per boot ($DSH_HOME may
|
||||
// differ per run) and only patched when the composed tree actually mounts
|
||||
// the roster — a one-shot `dsh run` composes agents from the same roster
|
||||
// `dsh web` offers.
|
||||
if (rows.has('agent-presets')) {
|
||||
overlayAndFlags.push({
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
...(rows.get('agent-presets')?.config ?? {}) as Record<string, unknown>,
|
||||
roots: [
|
||||
{ path: SHIPPED_PRESET_ROOT, trust: 'system' },
|
||||
{ path: dshHomePath(USER_PRESET_DIR), trust: 'user' },
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
|
||||
if (telemetryPatch !== undefined) overlayAndFlags.push(telemetryPatch)
|
||||
return { profile, bundlePatches, homePatches, overlayAndFlags, rows }
|
||||
return { profile, bundlePatches, windowsShellPatches, homePatches, overlayAndFlags, rows }
|
||||
}
|
||||
|
||||
/** Options for {@link runProfile}. */
|
||||
@@ -159,10 +196,17 @@ export interface RunProfileOptions {
|
||||
patchFiles: readonly string[]
|
||||
/** Launcher hook turning the pre-flag composed rows into flag patches (the web alias's flag family). */
|
||||
deriveFlagPatches?: (rows: ProfileRows) => PatchOptions[]
|
||||
/** One-shot task text; requires the composition to mount the headless runner row. */
|
||||
/** `dsh run` task text; requires the composition to mount the headless runner row. */
|
||||
task?: string
|
||||
/** Surface setup registered after Loader installation and before any config-tree entry mounts. */
|
||||
prepare?: (ctx: Context, rows: ProfileRows) => Promise<void> | void
|
||||
/** This run's frozen environment snapshot, provided to the tree before any entry mounts. */
|
||||
environment: EnvironmentSnapshot
|
||||
}
|
||||
|
||||
/** Re-throw setup failures unless this invocation's signal already owns shutdown. */
|
||||
function suppressSignalShutdownError(signal: AbortSignal, error: unknown): void {
|
||||
if (!signal.aborted) throw error
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,17 +231,22 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// error naming no fix.
|
||||
throw new Error(
|
||||
`dsh: profile ${JSON.stringify(options.profile)} mounts the one-shot runner and needs a task: `
|
||||
+ `dsh --profile ${options.profile} "<task>"`,
|
||||
+ `dsh run --profile ${options.profile} "<task>"`,
|
||||
)
|
||||
}
|
||||
|
||||
const app: { current?: Context } = {}
|
||||
const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })
|
||||
const signalShutdown = new AbortController()
|
||||
const interrupt = (code: number): void => {
|
||||
signalShutdown.abort()
|
||||
shutdown.interrupt(code)
|
||||
}
|
||||
// Signals own teardown throughout the startup window, not only after boot()
|
||||
// settles: an inserted front door can publish readiness before sibling rows
|
||||
// settles: an inserted entry point can publish readiness before sibling rows
|
||||
// finish mounting.
|
||||
process.on('SIGTERM', () => { shutdown.interrupt(options.task === undefined ? 0 : 143) })
|
||||
process.on('SIGINT', () => { shutdown.interrupt(130) })
|
||||
process.on('SIGTERM', () => { interrupt(options.task === undefined ? 0 : 143) })
|
||||
process.on('SIGINT', () => { interrupt(130) })
|
||||
installFailLoud(NAME, process, async () => {
|
||||
await app.current?.fiber.dispose()
|
||||
})
|
||||
@@ -215,6 +264,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// removing the override could never revert the row to the bundle default.
|
||||
const composeLive = (): PatchOptions[] => structuredClone([
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
|
||||
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
|
||||
...composed.overlayAndFlags,
|
||||
@@ -226,6 +276,9 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// application must not mutate the objects later reloads recompose from.
|
||||
const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), async (hostCtx) => {
|
||||
app.current = hostCtx
|
||||
// Before any config-tree entry mounts, so a plugin that resolves a
|
||||
// user-facing value at construction already sees this run's layers.
|
||||
hostCtx.provide(DSH_ENVIRONMENT_KEY, options.environment)
|
||||
if (options.task !== undefined) {
|
||||
const io: HeadlessIo = {
|
||||
stdout: process.stdout,
|
||||
@@ -237,33 +290,41 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
await options.prepare?.(hostCtx, composed.rows)
|
||||
})
|
||||
app.current = ctx
|
||||
// A surface can dispose the whole tree while startup was still in flight
|
||||
// (early SIGTERM); the Loader service goes with it and there is nothing to
|
||||
// keep live.
|
||||
if (watchProfilePatch && ctx.get('loader') !== undefined) {
|
||||
// Config-only HMR for the live profile patch layer: the web bundle
|
||||
// disables the shared module-reload `hmr` row (its reload lifecycle is
|
||||
// untested), so when the composition leaves no HMR service, mount a
|
||||
// watch-only instance with no module roots — cordis.patch.yml edits stay
|
||||
// live on every long-lived surface. A silent skip would break the
|
||||
// documented hot-reload contract. HMR injects the timer service, which a
|
||||
// bare custom profile may not mount either.
|
||||
if (ctx.get('hmr') === undefined) {
|
||||
if (ctx.get('timer') === undefined) {
|
||||
await ctx.loader.create({ name: '@cordisjs/plugin-timer' })
|
||||
// A surface can dispose the whole tree while startup or this post-boot
|
||||
// watcher setup is still in flight. Loader presence and fiber state own
|
||||
// liveness; the local signal fact distinguishes that expected exit race
|
||||
// from a real HMR error.
|
||||
if (watchProfilePatch
|
||||
&& !signalShutdown.signal.aborted
|
||||
&& ctx.fiber.state === FiberState.ACTIVE
|
||||
&& ctx.get('loader') !== undefined) {
|
||||
try {
|
||||
// Config-only HMR for the live profile patch layer: the web bundle
|
||||
// disables the shared module-reload `hmr` row (its reload lifecycle is
|
||||
// untested), so when the composition leaves no HMR service, mount a
|
||||
// watch-only instance with no module roots — cordis.patch.yml edits stay
|
||||
// live on every long-lived surface. A silent skip would break the
|
||||
// documented hot-reload contract. HMR injects the timer service, which a
|
||||
// bare custom profile may not mount either.
|
||||
if (ctx.get('hmr') === undefined) {
|
||||
if (ctx.get('timer') === undefined) {
|
||||
await ctx.loader.create({ name: '@cordisjs/plugin-timer' })
|
||||
}
|
||||
await ctx.loader.create({ name: '@cordisjs/plugin-hmr', config: { root: [] } })
|
||||
}
|
||||
await ctx.loader.create({ name: '@cordisjs/plugin-hmr', config: { root: [] } })
|
||||
await watchUserPatches(ctx, {
|
||||
binName: NAME,
|
||||
filename: composed.profile.patchPath,
|
||||
compose: composeLive,
|
||||
})
|
||||
await watchUserPatches(ctx, {
|
||||
binName: NAME,
|
||||
filename: homePatchPath(),
|
||||
compose: composeLive,
|
||||
})
|
||||
} catch (error) {
|
||||
suppressSignalShutdownError(signalShutdown.signal, error)
|
||||
}
|
||||
await watchUserPatches(ctx, {
|
||||
binName: NAME,
|
||||
filename: composed.profile.patchPath,
|
||||
compose: composeLive,
|
||||
})
|
||||
await watchUserPatches(ctx, {
|
||||
binName: NAME,
|
||||
filename: homePatchPath(),
|
||||
compose: composeLive,
|
||||
})
|
||||
}
|
||||
return { ctx, shutdown }
|
||||
}
|
||||
+7
-1
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from 'cordis'
|
||||
import type { PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { runProfile, type ProfileRows } from './profile-boot.ts'
|
||||
|
||||
const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
@@ -94,6 +95,9 @@ function deriveWebFlagPatches(
|
||||
// inserts the client-hmr row), never pass-throughs of composed values.
|
||||
put('web-runtime', 'mode', flags.dev ? 'development' : 'production')
|
||||
put('web-runtime', 'lanAddresses', lanAddresses)
|
||||
// The agent-preset roots are patched by the shared profile boot: they are
|
||||
// an assembly fact of every dsh launcher, and `dsh run` composes agents
|
||||
// from the same roster this alias offers.
|
||||
const patches = [...overrides.entries()].map(([id, bag]): PatchOptions => {
|
||||
const composed = rows.get(id)
|
||||
if (composed === undefined) throw new Error(`dsh: patch target row "${id}" not found in the web profile composition`)
|
||||
@@ -122,9 +126,11 @@ export function webSurfaceContextEnabled(rows: ProfileRows): boolean {
|
||||
* every boot. The URL line is printed by the web-app bundle's runtime row
|
||||
* after Loader settlement.
|
||||
* @param flags - the parsed `dsh web` flag family.
|
||||
* @param environment - this run's frozen environment snapshot.
|
||||
*/
|
||||
export async function runWeb(flags: WebFlags): Promise<void> {
|
||||
export async function runWeb(flags: WebFlags, environment: EnvironmentSnapshot): Promise<void> {
|
||||
await runProfile({
|
||||
environment,
|
||||
profile: 'web',
|
||||
patchFiles: flags.patches,
|
||||
deriveFlagPatches: rows => deriveWebFlagPatches(rows, flags),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* The Windows shell platform layer: on win32 hosts the shipped profile
|
||||
* compositions swap the POSIX-only bash stack for the sandbox-confined
|
||||
* PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox` +
|
||||
* `@deepseek-ai/dsh-tool-pwsh`). The layer is the base bundle's
|
||||
* `windows.cordis.patch.yml`, injected by the launcher between the bundle
|
||||
* layers and the user layers so a user patch can still override it — the
|
||||
* only override channel is composition config, like every other roster
|
||||
* decision. POSIX hosts never receive the layer.
|
||||
* @module @deepseek-ai/dsh/windows-shell
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import type { PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
|
||||
/** The base bundle whose package carries the Windows shell patch. */
|
||||
export const BASE_BUNDLE = '@deepseek-ai/dsh-base'
|
||||
|
||||
/** The Windows shell patch filename inside the base bundle package. */
|
||||
export const WINDOWS_SHELL_PATCH_FILENAME = 'windows.cordis.patch.yml'
|
||||
|
||||
/** One Windows shell platform layer: its patch file and parsed patches. */
|
||||
export interface WindowsShellLayer {
|
||||
/** The patch file path, used as the config-dump provenance label. */
|
||||
label: string
|
||||
/** The parsed patch entries, applied after the bundle layers. */
|
||||
patches: PatchOptions[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Windows shell platform layer for a profile composition.
|
||||
* @param platform - the host platform (`process.platform` at call sites).
|
||||
* @param layers - the profile's bundle layers, in application order.
|
||||
* @param binName - the diagnostic prefix on thrown errors (`dsh`).
|
||||
* @returns the pwsh layer on win32, else `undefined`. A custom profile that
|
||||
* mounts no base bundle is skipped (it owns its shell stack); a base
|
||||
* bundle whose Windows shell patch is missing fails loud in
|
||||
* {@link loadOverlayPatches} — the shipped package always carries it, so
|
||||
* a miss is a broken installation.
|
||||
*/
|
||||
export function resolveWindowsShellLayer(
|
||||
platform: NodeJS.Platform,
|
||||
layers: readonly ProfileLayer[],
|
||||
binName: string,
|
||||
): WindowsShellLayer | undefined {
|
||||
if (platform !== 'win32') return undefined
|
||||
const base = layers.find(layer => layer.packageName === BASE_BUNDLE)
|
||||
if (base === undefined) return undefined
|
||||
const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME)
|
||||
return { label, patches: loadOverlayPatches(binName, label) }
|
||||
}
|
||||
Reference in New Issue
Block a user