cleanup(cli): remove the profile-json config entry

`./.dsh-tmp-profile/config.json` was the web config-tree boot's user-config
plane, but never gained a writer: no production code created or edited it, no
test exercised it, and no user documentation named it. The fields it mapped
have owners elsewhere — provider/model are the api-gateway's default route and
persistenceRoot is an assembly fact, while typed user preferences live in
$DSH_HOME/settings.yaml.

Delete PROFILE_DIR, PROFILE_FILE, ProfileMapping, PROFILE_MAPPINGS, and
readProfile() with the patch source that consumed them. AppCLIEntry now
composes patches from CLI flags and the resolved frontend distIndex only; the
surrounding layers are unchanged. A file on disk is ignored completely — no
migration, replacement format, or deprecation diagnostic, per the pre-release
stance.
This commit is contained in:
Yichen Jiang
2026-08-04 14:11:38 +08:00
parent 012bc40f04
commit 88c035c98e
11 changed files with 94 additions and 72 deletions

View File

@@ -1,6 +1,6 @@
# `dsh web` — the browser surface, as a patch list over `base.cordis.yml`.
# The launcher includes the base and applies this file, then any `--config`
# overlay, then AppCLIEntry's profile-json and CLI-flag patches, as sibling patch
# overlay, then AppCLIEntry's CLI-flag patches, as sibling patch
# lists at ONE include level: patches never cross an include boundary, so
# stacking overlays as nested includes would silently stop reaching base rows.
#
@@ -81,8 +81,8 @@
name: '@deepseek-ai/dsh-host-directory-picker-auto'
# The API gateway: the transport-agnostic dispatch face every client shape
# shares. provider/model are the host default routing — the profile json's
# mapping target (user config overrides these engineering defaults).
# shares. provider/model are the host default route for created and resumed
# agents; a session's own picker overrides it per agent.
- id: api-gateway
name: '@deepseek-ai/dsh-host-apiproxy'
config:

View File

@@ -2,8 +2,8 @@
* AppCLIEntry — the pre-cordis boot glue the config-tree dsh surfaces share
* (`dsh web` and `dsh -p`; the TUI composes dsh-app-boot directly).
* Everything here is what must exist before the Loader runs: the patch
* composition over the shipped base and surface overlay (profile json + CLI
* flags + the resolved frontend dist), and the fail-loud activation audit after the tree
* composition over the shipped base and surface overlay (CLI flags + the
* resolved frontend dist), and the fail-loud activation audit after the tree
* settles. The environment is what the bin already loaded (ambient plus the
* invoking directory's `.env`); `$DSH_HOME/.env` belongs to the credential
* provider and is never hoisted here.
@@ -12,7 +12,7 @@
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { networkInterfaces } from 'node:os'
import { join, resolve } from 'node:path'
import { resolve } from 'node:path'
import { Context } from 'cordis'
import type { PatchOptions } from '@cordisjs/plugin-include'
import yaml from 'js-yaml'
@@ -26,10 +26,6 @@ import {
// Empty type import carries the httpServer Context merge for the port read below.
import type {} from '@deepseek-ai/dsh-host-webserver'
/** Profile file under the invoking directory (read-only this round; never created — see the design's profile ruling). */
const PROFILE_DIR = '.dsh-tmp-profile'
const PROFILE_FILE = 'config.json'
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets (mounted in web.cordis.yml). */
const TELEMETRY_ROW_ID = 'telemetry-otel'
@@ -100,25 +96,6 @@ export function configHasTelemetryRow(file: string): boolean {
row.id === TELEMETRY_ROW_ID || (row.insert ?? []).some(inserted => inserted.id === TELEMETRY_ROW_ID))
}
/** One profile-json key mapped onto a yml row's config field. */
interface ProfileMapping {
jsonPath: string
entryId: string
configKey: string
}
/**
* The static profile→row mapping table. json is user config and wins over the
* yml engineering default per field; a json key absent from this table fails
* loud (a typo silently ignored would read as "setting has no effect").
* Developers extend deployments by adding rows here.
*/
const PROFILE_MAPPINGS: ProfileMapping[] = [
{ jsonPath: 'provider', entryId: 'api-gateway', configKey: 'provider' },
{ jsonPath: 'model', entryId: 'api-gateway', configKey: 'model' },
{ jsonPath: 'persistenceRoot', entryId: 'session-persistence-jsonl', configKey: 'root' },
]
// The include's YAML dialect: `!!js` scalars become expression nodes the
// Loader evaluates at entry activation. The bypass parse below must accept
// them (and passing one through a patch unchanged is legal).
@@ -135,14 +112,14 @@ export interface AppCLIEntryOptions {
configPath: string
/**
* Absolute path of this surface's overlay: a patch list applied over
* {@link configPath} before this entry's own profile/flag patches. Its rows
* {@link configPath} before this entry's own flag patches. Its rows
* are also merge inputs, so a flag override preserves the overlay's other
* fields on the same row.
*/
overlayPath: string
/**
* Optional explicit overlay applied after {@link overlayPath} and before
* this entry's own profile/flag patches. When absent, the personal
* this entry's own flag patches. When absent, the personal
* `$DSH_HOME/config.yaml` overlay is applied instead.
*/
extraOverlayPath?: string
@@ -205,8 +182,8 @@ export class AppCLIEntry {
}
/**
* 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
* Compose the patch set from 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 {
@@ -218,28 +195,19 @@ 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)
if (mapping === undefined) {
throw new Error(`dsh: profile key "${key}" has no mapping (known: ${PROFILE_MAPPINGS.map(m => m.jsonPath).join(', ')})`)
}
put(mapping.entryId, mapping.configKey, value)
}
// Source 2: CLI flags (field set disjoint from the json mappings).
// Source 1: CLI flags.
if (this.options.host !== undefined) put('webserver', 'host', this.options.host)
if (this.options.port !== undefined) put('webserver', 'port', this.options.port)
if (this.options.workspaceRoot !== undefined) put('api-gateway', 'workspaceRoot', this.options.workspaceRoot)
// Source 2b: authorities for the /api browser-trust fence (rationale on
// Source 1b: authorities for the /api browser-trust fence (rationale on
// resolveLanTrust).
const ymlHost = (rows.get('webserver')?.config as { host?: string } | undefined)?.host
const { lanAddresses, trustedHosts } = resolveLanTrust(this.options.host ?? ymlHost, this.options.trustedHosts ?? [])
this.lanAddresses = lanAddresses
if (trustedHosts.length > 0) put('connection', 'trustedHosts', trustedHosts)
// Source 3: the frontend dist — an assembly fact of this app, never yml
// Source 2: the frontend dist — an assembly fact of this app, never yml
// user config. Workspace knowledge stays here.
put('webserver', 'distIndex', this.resolveDistIndex())
@@ -262,7 +230,7 @@ export class AppCLIEntry {
// One include of the shared base with every overlay as a sibling patch
// list: patches never cross an include boundary, so nesting them would
// silently stop reaching base rows. The surface overlay applies first, then
// this entry's profile-json and CLI-flag patches, which therefore win.
// this entry's CLI-flag patches, which therefore win.
const compose = (overlay: PatchOptions[]): PatchOptions[] => [
...loadOverlayPatches('dsh', this.options.overlayPath),
...overlay,
@@ -327,22 +295,6 @@ export class AppCLIEntry {
return doc as { id?: string; config?: unknown; insert?: { id?: string; config?: unknown }[] }[]
}
/** Profile json under cwd; read-only — never created here, absent = no user config. */
private readProfile(): Record<string, unknown> {
let raw: string
try {
raw = readFileSync(join(process.cwd(), PROFILE_DIR, PROFILE_FILE), 'utf8')
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return {}
throw error
}
const parsed: unknown = JSON.parse(raw)
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`dsh: ${PROFILE_DIR}/${PROFILE_FILE} must hold a JSON object`)
}
return parsed as Record<string, unknown>
}
/** Dist location is workspace knowledge of this app: resolved through the frontend package exports, not configured. */
private resolveDistIndex(): string {
const require = createRequire(import.meta.url)