Merge latest origin/master into feature/tui-first-run-welcome
# Conflicts: # apps/cli/README.i18n.yaml
This commit is contained in:
513 files changed
+81331
-5057
No files matched your search
+83
-4
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
+9
-4
@@ -43,14 +43,19 @@ 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()
|
||||
const { runTui, SOURCE_ROOT } = await import('./tui.ts')
|
||||
await runTui(undefined, undefined, SOURCE_ROOT)
|
||||
break
|
||||
}
|
||||
case 'upgrade': {
|
||||
const { runSkillSession } = await import('./tui.ts')
|
||||
await runSkillSession(`dsh-${invocation.mode}`)
|
||||
const { runTui } = await import('./tui.ts')
|
||||
await runTui(undefined, undefined, undefined, `dsh-${invocation.mode}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -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 */
|
||||
+8
-30
@@ -8,12 +8,10 @@
|
||||
* from it, so `dsh` acts on whatever project it is launched in. Session storage
|
||||
* is the exception — it lives under the Harness home so `/resume` reaches every
|
||||
* 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}) 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.
|
||||
* `dsh meta` is the one exception — it makes this harness checkout the
|
||||
* workspace. `dsh upgrade` 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
|
||||
*/
|
||||
|
||||
@@ -72,30 +70,11 @@ const SESSION_QUERY_DB = `session-query-${String(process.pid)}-${randomUUID()}.d
|
||||
// The harness checkout root: three hops up from apps/cli/{src,lib}, resolved
|
||||
// from this bin's location so it holds however `dsh` is launched (a PATH
|
||||
// symlink, an arbitrary cwd). The agent is told where its own source lives.
|
||||
const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
/** The harness checkout used as the `dsh meta` workspace and source prompt path. */
|
||||
export const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
/* v8 ignore start -- composition over the unit-tested dsh-app-boot helpers;
|
||||
the CLI PTY smoke drives this path end to end, personal overlay included */
|
||||
/**
|
||||
* Run the interactive TUI with this harness checkout as the workspace
|
||||
* (`dsh meta`), whatever directory it was launched from.
|
||||
*/
|
||||
export async function runMeta(): Promise<void> {
|
||||
return runTui(undefined, undefined, SOURCE_ROOT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the interactive TUI as a guided fresh session whose first turn invokes a
|
||||
* bundled skill (`dsh upgrade` → `dsh-upgrade`).
|
||||
* Always mints a fresh session in the invoking directory; the skill is seeded
|
||||
* only on this first launch, so a later `--resume` of the session is an ordinary
|
||||
* TUI session with no re-injection.
|
||||
* @param skill - the bundled skill name to auto-invoke as the first turn.
|
||||
*/
|
||||
export async function runSkillSession(skill: string): Promise<void> {
|
||||
return runTui(undefined, undefined, undefined, skill)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the interactive TUI from the invoking directory.
|
||||
* @param config - an overlay patch list applied over the shared base and the
|
||||
@@ -110,9 +89,8 @@ export async function runSkillSession(skill: string): Promise<void> {
|
||||
* @param workspace - a directory to make the workspace instead of the invoking
|
||||
* one, or `undefined` to keep the cwd. Only `dsh meta` passes it.
|
||||
* @param initialSkill - a bundled skill to auto-invoke as a fresh session's
|
||||
* first turn, or `undefined`. Set only by {@link runSkillSession} and ignored
|
||||
* on a resume, so it never re-fires; reaches the app through
|
||||
* {@link INITIAL_SKILL_KEY}.
|
||||
* first turn, or `undefined`. Set only by `dsh upgrade` and ignored on a resume,
|
||||
* so it never re-fires; reaches the app through {@link INITIAL_SKILL_KEY}.
|
||||
* @param configReplace - a config path to boot as the ENTIRE tree, bypassing the
|
||||
* shared base, the TUI overlay, and the personal overlay alike, or `undefined`
|
||||
* to compose them; already parsed from `--config-replace`.
|
||||
|
||||
Reference in New Issue
Block a user