feat(cli)!: complete --config on every surface and delete the personal overlay
$DSH_HOME/config.yaml was an implicit composition layer: if the file existed, every launch applied an arbitrary Loader patch graph over the shipped tree, kept live by a dedicated HMR watcher. Three costs came from the implicitness, not the capability. A patch replaces its target row's whole config, so a file written months ago pins that row to the field set it knew and every default the shipped tree later adds silently stops applying. It competed with the typed settings namespaces llm-deepseek and llm-pi-ai already register, so which one wins was a function of layer order rather than meaning. And the explicit escape hatch it was supposedly redundant with did not exist on every surface: dsh -p, dsh meta, and dsh upgrade all rejected --config, so for them the implicit file was the only composition route at all. Complete the explicit layer first: --config and --config-replace now work on every booting surface. A headless --config-replace tree must still mount a webserver row, because that surface reaches its own agent over the same HTTP gateway the browser uses; AppCLIEntry names that contract in the failure instead of reporting a bare missing service. Then delete the implicit one. PERSONAL_CONFIG_FILENAME, loadPersonalPatches, watchPersonalPatches, and the config-only HMR row mounted for it are gone; a file left at that path is inert, and --dump-config no longer reads the Harness home. --config therefore stops *replacing* the personal overlay and simply *is* the user overlay. No migration: a user who wants the old behavior names the same file (dsh --config ~/.dsh/config.yaml), which a shell alias makes permanent.
This commit is contained in:
@@ -30,6 +30,17 @@ describe('parseDshArgs', () => {
|
||||
expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
|
||||
expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
|
||||
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
|
||||
// Every booting surface takes the composition flags: with the personal
|
||||
// overlay gone, naming a tree is the only way to compose one, so a
|
||||
// surface that could not name one would have no composition path at all.
|
||||
expect(parse(['-p', 'task', '--config', 'c.yml']))
|
||||
.toEqual({ mode: 'headless', prompt: 'task', config: 'c.yml' })
|
||||
expect(parse(['-p', 'task', '--config-replace', 'tree.yml']))
|
||||
.toEqual({ mode: 'headless', prompt: 'task', configReplace: 'tree.yml' })
|
||||
expect(parse(['meta', '--experimental', '--config', 'c.yml']))
|
||||
.toEqual({ mode: 'meta', config: 'c.yml' })
|
||||
expect(parse(['upgrade', '--experimental', '--config-replace', 'tree.yml']))
|
||||
.toEqual({ mode: 'upgrade', configReplace: 'tree.yml' })
|
||||
// Experimental subcommands run under the per-invocation flag or the env opt-in.
|
||||
expect(parse(['meta', '--experimental'])).toEqual({ mode: 'meta' })
|
||||
expect(parse(['meta'], true)).toEqual({ mode: 'meta' })
|
||||
@@ -77,9 +88,8 @@ describe('parseDshArgs', () => {
|
||||
// schema at boot, not here.)
|
||||
expect(exitCode(['--resume='])).toBe(1)
|
||||
expect(exitCode(['-p', ''])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['--config', 'c.yml', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--config', 'c.yml', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['--bogus'])).toBe(1)
|
||||
expect(exitCode(['bogus-positional'])).toBe(1)
|
||||
@@ -91,16 +101,14 @@ describe('parseDshArgs', () => {
|
||||
expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
|
||||
// Same rule for each subcommand that shares no option with the default
|
||||
// surface, so a leaked flag is a typo, not something to ignore.
|
||||
// `meta` fixes its own config tree and always starts fresh,
|
||||
// so every default-surface option is rejected.
|
||||
// `meta` always starts fresh, so the session options are rejected; the
|
||||
// composition flags are its own and only their combination is rejected.
|
||||
expect(exitCode(['meta', '--experimental', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['meta', '--experimental', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['meta', '--experimental', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['meta', '--experimental', '-p', 'task'])).toBe(1)
|
||||
// `upgrade` takes no options beyond the gate: any leaked default-surface
|
||||
// flag is a mistyped invocation, not a silently-dropped input.
|
||||
expect(exitCode(['meta', '--experimental', '--config', 'c.yml', '--config-replace', 't.yml'])).toBe(1)
|
||||
// `upgrade` always mints a fresh session, so `--resume` and a leaked
|
||||
// parent flag are mistyped invocations; its own composition flags are not.
|
||||
expect(exitCode(['upgrade', '--experimental', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['upgrade', '--experimental', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'task', 'upgrade', '--experimental'])).toBe(1)
|
||||
// The pre-release command names have no compatibility aliases.
|
||||
expect(exitCode(['experimental-meta'])).toBe(1)
|
||||
|
||||
Reference in New Issue
Block a user