mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(cli): dsh --dump-config / --dump-default-config print the composed tree
dsh --dump-config and dsh web --dump-config compose the shipped base, the surface overlay, and the --config or personal overlay — exactly the layers that surface boots — and print the entry list as YAML without booting; --dump-default-config stops at the surface overlay so the two outputs diff to precisely the user layer's effect. The dump shares the mounting code: the vendored include exports its patch algorithm as applyEntryPatches() and its !!js dialect as entryListSchema (logged in vendor/README.md), dsh-app-boot's renderConfigDump() composes and renders through both (and now imports the dialect instead of duplicating it), and the CLI adds a thin dump-config mode. !!js expressions print verbatim; unmatched patches warn on stderr; boot-only flags are rejected alongside the dump flags. (cherry picked from commit 1fdbebfa8a5dc7df840d53666320064a7e3dae59)
This commit is contained in:
@@ -23,6 +23,22 @@ interface TuiInvocation {
|
||||
resume?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the composed config tree and exit, without booting: `--dump-config`
|
||||
* composes the shipped base, the surface overlay, and the `--config` or
|
||||
* personal overlay — exactly the layers that surface would boot;
|
||||
* `--dump-default-config` stops at the surface overlay (the shipped tree, no
|
||||
* user layer).
|
||||
*/
|
||||
interface DumpConfigInvocation {
|
||||
mode: 'dump-config'
|
||||
surface: 'tui' | 'web'
|
||||
/** Omit the `--config`/personal layer and print only the shipped composition. */
|
||||
defaultOnly: boolean
|
||||
/** The `--config` overlay to compose instead of the personal one. */
|
||||
config?: string
|
||||
}
|
||||
|
||||
/** Headless one-shot: `dsh -p "task"`. */
|
||||
interface HeadlessInvocation {
|
||||
mode: 'headless'
|
||||
@@ -69,6 +85,7 @@ interface WebInvocation {
|
||||
/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
|
||||
export type DshInvocation =
|
||||
| TuiInvocation
|
||||
| DumpConfigInvocation
|
||||
| HeadlessInvocation
|
||||
| MetaInvocation
|
||||
| SkillSessionInvocation
|
||||
@@ -82,6 +99,34 @@ interface WebOptions {
|
||||
dev?: boolean
|
||||
workspaceRoot?: string
|
||||
trustedHost?: string[]
|
||||
dumpConfig?: boolean
|
||||
dumpDefaultConfig?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the two dump flags for one surface, or return `undefined` when
|
||||
* neither was passed. Both flags together are contradictory (one includes the
|
||||
* user layer, the other excludes it) and fail loud through `error`.
|
||||
*/
|
||||
function resolveDump(
|
||||
surface: 'tui' | 'web',
|
||||
options: { config?: string; dumpConfig?: boolean; dumpDefaultConfig?: boolean },
|
||||
error: (message: string) => never,
|
||||
): DumpConfigInvocation | undefined {
|
||||
if (options.dumpConfig !== true && options.dumpDefaultConfig !== true) return undefined
|
||||
if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
|
||||
error('error: --dump-config and --dump-default-config are mutually exclusive')
|
||||
}
|
||||
const defaultOnly = options.dumpDefaultConfig === true
|
||||
if (defaultOnly && options.config !== undefined) {
|
||||
error('error: --dump-default-config prints the shipped tree and takes no --config')
|
||||
}
|
||||
return {
|
||||
mode: 'dump-config',
|
||||
surface,
|
||||
defaultOnly,
|
||||
...options.config !== undefined && { config: options.config },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +180,26 @@ Examples:
|
||||
.option('--resume <id>', 'continue a past session by id')
|
||||
.option('--config <path>', 'apply this overlay of loader patches instead of the personal one')
|
||||
.option('--config-replace <path>', 'boot this file as the entire tree, ignoring the shipped and personal configuration')
|
||||
.action((options: { config?: string; configReplace?: string; prompt?: string; resume?: string }) => {
|
||||
.option('--dump-config', 'print the composed config tree (base + surface + --config/personal overlay) and exit')
|
||||
.option('--dump-default-config', 'print the shipped config tree (base + surface overlay, no user layer) and exit')
|
||||
.action((options: {
|
||||
config?: string
|
||||
configReplace?: string
|
||||
prompt?: string
|
||||
resume?: string
|
||||
dumpConfig?: boolean
|
||||
dumpDefaultConfig?: boolean
|
||||
}) => {
|
||||
const dump = resolveDump('tui', options, message => program.error(message))
|
||||
if (dump !== undefined) {
|
||||
// The dump prints composition; a boot-only flag alongside it would be
|
||||
// silently ignored, so reject the mix loud.
|
||||
if (options.prompt !== undefined || options.resume !== undefined || options.configReplace !== undefined) {
|
||||
program.error('error: --dump-config/--dump-default-config take none of -p/--prompt, --resume, or --config-replace')
|
||||
}
|
||||
resolved = dump
|
||||
return
|
||||
}
|
||||
if (options.prompt !== undefined) {
|
||||
// A headless prompt owns the invocation; an empty task has nothing to
|
||||
// run, and --config/--resume are TUI inputs that must not silently
|
||||
@@ -168,10 +232,18 @@ Examples:
|
||||
// a leaked config/prompt/resume option is a mistyped invocation that must fail
|
||||
// loud rather than silently run and drop the input.
|
||||
const rejectParentOptions = (command: string): void => {
|
||||
const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
|
||||
const parent = program.opts<{
|
||||
config?: string
|
||||
configReplace?: string
|
||||
prompt?: string
|
||||
resume?: string
|
||||
dumpConfig?: boolean
|
||||
dumpDefaultConfig?: boolean
|
||||
}>()
|
||||
if (parent.config !== undefined || parent.configReplace !== undefined
|
||||
|| parent.prompt !== undefined || parent.resume !== undefined) {
|
||||
program.error(`error: ${command} takes none of --config, --config-replace, -p/--prompt, or --resume`)
|
||||
|| parent.prompt !== undefined || parent.resume !== undefined
|
||||
|| parent.dumpConfig !== undefined || parent.dumpDefaultConfig !== undefined) {
|
||||
program.error(`error: ${command} takes none of --config, --config-replace, -p/--prompt, --resume, --dump-config, or --dump-default-config`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +270,15 @@ Examples:
|
||||
.option('--dev', 'developer mode: hot-reload the browser client')
|
||||
.option('--workspace-root <path>', 'parent directory for workspaces created from the browser UI')
|
||||
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
|
||||
.option('--dump-config', 'print the composed config tree (base + web + --config/personal overlay) and exit')
|
||||
.option('--dump-default-config', 'print the shipped config tree (base + web overlay, no user layer) and exit')
|
||||
.action((options: WebOptions) => {
|
||||
rejectParentOptions('web')
|
||||
const dump = resolveDump('web', options, message => program.error(message))
|
||||
if (dump !== undefined) {
|
||||
resolved = dump
|
||||
return
|
||||
}
|
||||
resolved = resolveWeb(options)
|
||||
})
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ switch (invocation.mode) {
|
||||
await runTui(invocation.config, invocation.resume, undefined, undefined, invocation.configReplace)
|
||||
break
|
||||
}
|
||||
case 'dump-config': {
|
||||
const { runDumpConfig } = await import('./dump-config.ts')
|
||||
runDumpConfig(invocation.surface, invocation.defaultOnly, invocation.config)
|
||||
break
|
||||
}
|
||||
case 'meta': {
|
||||
const { runMeta } = await import('./tui.ts')
|
||||
await runMeta()
|
||||
|
||||
61
apps/cli/src/dump-config.ts
Normal file
61
apps/cli/src/dump-config.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* `dsh --dump-config` / `dsh web --dump-config` — print the composed config
|
||||
* tree without booting: the shipped base, the surface overlay, and (unless
|
||||
* `--dump-default-config`) the `--config` or personal overlay, composed
|
||||
* through the include's own patch algorithm so the printed tree is exactly
|
||||
* what that surface would mount. `!!js` expressions print verbatim,
|
||||
* unevaluated — the dump shows composition, not one process's environment.
|
||||
* Launcher-provided boot-context values (session identity, CLI-flag patches)
|
||||
* are per-invocation facts outside the config tree and do not appear.
|
||||
* @module @deepseek-ai/dsh/dump-config
|
||||
*/
|
||||
|
||||
import { basename, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
loadOverlayPatches,
|
||||
loadPersonalPatches,
|
||||
PERSONAL_CONFIG_FILENAME,
|
||||
renderConfigDump,
|
||||
type ConfigDumpLayer,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
const BASE_CONFIG = fileURLToPath(new URL('../config/base.cordis.yml', import.meta.url))
|
||||
const SURFACE_OVERLAYS = {
|
||||
tui: fileURLToPath(new URL('../config/tui.cordis.yml', import.meta.url)),
|
||||
web: fileURLToPath(new URL('../config/web.cordis.yml', import.meta.url)),
|
||||
} as const
|
||||
|
||||
/* v8 ignore start -- composition over the unit-tested renderConfigDump; the
|
||||
built-bin e2e drives this path end to end */
|
||||
/**
|
||||
* Print one surface's composed config tree to stdout, with a comment
|
||||
* separator naming the file each section of rows comes from (and the layers
|
||||
* that patched it).
|
||||
* @param surface - which surface overlay to compose over the shared base.
|
||||
* @param defaultOnly - stop at the surface overlay (no `--config`/personal layer).
|
||||
* @param config - the `--config` overlay path composed instead of the personal
|
||||
* one, or `undefined` to use `$DSH_HOME/config.yaml`.
|
||||
*/
|
||||
export function runDumpConfig(surface: 'tui' | 'web', defaultOnly: boolean, config?: string): void {
|
||||
const overlay = SURFACE_OVERLAYS[surface]
|
||||
const layers: ConfigDumpLayer[] = [
|
||||
{ label: basename(overlay), patches: loadOverlayPatches(NAME, overlay) },
|
||||
]
|
||||
if (!defaultOnly) {
|
||||
if (config === undefined) {
|
||||
const personal = loadPersonalPatches(NAME)
|
||||
// The personal file may be absent; the shipped layers still print.
|
||||
if (personal !== undefined) {
|
||||
layers.push({ label: join(resolveDshHome(), PERSONAL_CONFIG_FILENAME), patches: personal })
|
||||
}
|
||||
} else {
|
||||
layers.push({ label: config, patches: loadOverlayPatches(NAME, config) })
|
||||
}
|
||||
}
|
||||
process.stdout.write(renderConfigDump(NAME, BASE_CONFIG, layers))
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
Reference in New Issue
Block a user