fix(cli): close shared config review gaps

This commit is contained in:
Turtle
2026-07-30 14:56:39 +08:00
parent 5a490553bc
commit a51143bded
51 changed files with 277 additions and 171 deletions
+3 -35
View File
@@ -12,7 +12,6 @@ import { createRequire } from 'node:module'
import { networkInterfaces } from 'node:os'
import { join, resolve } from 'node:path'
import { Context } from 'cordis'
import type { FiberState } from 'cordis'
import type { PatchOptions } from '@cordisjs/plugin-include'
import yaml from 'js-yaml'
import { boot, installFailLoud, loadEnv, loadOverlayPatches, loadPersonalPatches } from '@deepseek-ai/dsh-app-boot'
@@ -88,14 +87,6 @@ const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
})
const includeYamlSchema = yaml.JSON_SCHEMA.extend(jsExprType)
/**
* Value mirror of cordis's `FiberState` const enum members the sweep needs
* (a const enum has no runtime object to import; same rationale as the
* client-side mirror in dsh-client-web).
*/
const FIBER_ACTIVE = 2 as FiberState.ACTIVE
const FIBER_PENDING = 0 as FiberState.PENDING
/** Constructor facts for one dsh invocation over the shared composition (argv already parsed by the surface bin). */
export interface AppCLIEntryOptions {
/** Absolute path of the shared base config the Loader includes. */
@@ -174,10 +165,8 @@ export class AppCLIEntry {
}
/**
* Compose the patch set from the non-yml config sources: computed
* engineering defaults (the global session root), profile json (user
* config, overriding those defaults), CLI flags, and the resolved frontend
* dist. Patches replace a row's config wholesale, so each patched row's yml
* Compose the patch set from profile json, CLI flags, and the resolved
* frontend dist. Patches replace a row's config wholesale, so each patched row's yml
* static values are re-read here (bypass parse) and merged under the overrides.
*/
private composePatches(): void {
@@ -189,7 +178,6 @@ export class AppCLIEntry {
overrides.set(entryId, bag)
}
// Source 1: profile json (missing file = empty; unmapped key = loud).
for (const [key, value] of Object.entries(this.readProfile())) {
const mapping = PROFILE_MAPPINGS.find(m => m.jsonPath === key)
@@ -215,7 +203,6 @@ export class AppCLIEntry {
// user config. Workspace knowledge stays here.
put('webserver', 'distIndex', this.resolveDistIndex())
this.patches = [...overrides.entries()].map(([id, bag]) => {
const yml = rows.get(id)
if (yml === undefined) throw new Error(`dsh: patch target row "${id}" not found in ${this.options.configPath}`)
@@ -241,28 +228,9 @@ export class AppCLIEntry {
})
}
/**
* Shared boot catches import failures, installFailLoud catches late apply
* rejections, and the all-ACTIVE sweep
* below catches PENDING fibers (cordis inject waiting has no timeout).
*/
/** Install the diagnostic for plugin rejections that happen after settled boot. */
private assertBoot(): void {
installFailLoud('dsh')
const failures: string[] = []
for (const entry of this.ctx.loader.entries()) {
if (entry.fiber === undefined || entry.disabled) continue
const state = entry.fiber.state
if (state === FIBER_ACTIVE) continue
if (state === FIBER_PENDING) {
const missing = Object.keys(entry.fiber.inject).filter(service => this.ctx.get(service) === undefined)
failures.push(`${entry.options.name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
} else {
failures.push(`${entry.options.name}: fiber state ${String(state)}`)
}
}
if (failures.length > 0) {
throw new Error(`dsh: ${String(failures.length)} entr${failures.length === 1 ? 'y' : 'ies'} did not activate\n${failures.join('\n')}`)
}
}
/**
+3
View File
@@ -126,6 +126,9 @@ Examples:
dsh --resume <id> continue a past session
`)
.exitOverride()
// Stop parent options at a subcommand boundary so `web --config` belongs to
// Web while `--config ... web` remains a leaked default-surface option.
.enablePositionalOptions()
// Default surface: option-only (no positional), so `web` can be a real
// subcommand without a positional collision.
.option('-p, --prompt <task>', 'answer this task without the interactive UI, then exit')
+8 -8
View File
@@ -10,8 +10,8 @@
* workspace, and an in-place resume enters the selected session's own directory.
* `dsh meta`
* ({@link runMeta}) is the one exception — it makes this harness checkout the
* workspace. `dsh upgrade` ({@link runSkillSession}) are fresh
* sessions whose first turn auto-invokes a bundled skill. After boot, the
* workspace. `dsh upgrade` ({@link runSkillSession}) is a fresh
* session whose first turn auto-invokes a bundled skill. After boot, the
* agent's system prompt is told the path to this harness checkout so it can
* find its own source.
* @module @deepseek-ai/dsh/tui
@@ -146,7 +146,7 @@ export async function runTui(
const app: { current?: Context } = {}
// Resume always enters the default surface because meta rejects parent
// options, including `--resume`. The resumed session already persists its cwd.
const resumeArgs = (sessionId: string, _targetCwd?: string): string[] => [
const resumeArgs = (sessionId: string): string[] => [
`--resume=${sessionId}`,
// Both config flags must survive the handoff: resuming into a different
// tree than the session was created in would silently change the agent.
@@ -167,7 +167,7 @@ export async function runTui(
process.execPath,
...process.execArgv,
entry,
...resumeArgs(sessionId, cwd),
...resumeArgs(sessionId),
]
// `execve` inherits the cwd, and the target session may belong to another
// workspace. Enter it BEFORE teardown commits: an unreachable directory
@@ -199,14 +199,14 @@ export async function runTui(
const replaceTree = configReplace !== undefined
const patches = replaceTree ? [] : [
...loadOverlayPatches(NAME, TUI_OVERLAY),
...config === undefined
...resolvedConfig === undefined
? loadPersonalPatches(NAME) ?? []
: loadOverlayPatches(NAME, resolveConfigPath(resolve(config), undefined)),
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
]
const queryIndexPath = join(tmpdir(), SESSION_QUERY_DB)
const ctx = await boot(
NAME,
replaceTree ? resolveConfigPath(resolve(configReplace), undefined) : BASE_CONFIG,
resolvedConfigReplace === undefined ? BASE_CONFIG : resolveConfigPath(resolvedConfigReplace, undefined),
patches,
(hostCtx) => {
// The launcher owns session identity and the exit line: a config-mounted
@@ -230,7 +230,7 @@ export async function runTui(
rm(`${queryIndexPath}-wal`, { force: true }),
rm(`${queryIndexPath}-shm`, { force: true }),
])
}, 'launcherSessionQueryPath.cleanup')
}, `${SESSION_QUERY_SQLITE_PATH_KEY}.cleanup`)
if (resumeHost !== undefined) hostCtx.provide('tuiResumeHost', resumeHost)
// Seed the first turn only for a fresh session, so resuming never
// re-invokes the skill.