refactor(cli)!: namespace the profile and bundle manifests under dsh.profile and dsh.bundle

A profile manifest and a bundle manifest are different kinds and shared one
flat `dsh` section: `dsh.plugins` listed bundles (not plugins) and `dsh.patch`
declared a bundle's layer. Each kind now names its role — a bundle declares
`dsh.bundle.patch`, a profile declares `dsh.profile.bundles` — so a
package.json states which role it plays and the list name matches its contents.

`DEFAULT_PROFILE_PLUGINS` becomes `DEFAULT_PROFILE_BUNDLES`, and
`DshManifestSection` splits into `DshBundleManifest`/`DshProfileManifest`.
Pre-release: no compatibility shim; turtle-ui moved with it (bd5ff10).
This commit is contained in:
Turtle
2026-08-06 17:28:30 +08:00
parent 52d7515936
commit 62d0f26fd6
36 changed files with 203 additions and 156 deletions

View File

@@ -1,12 +1,12 @@
/**
* `dsh plugin --profile <name> <args...>` — profile plugin management as a
* thin pnpm forwarder: initialize the profile on first use, run
* `pnpm <args...>` in the profile directory, then reconcile the `dsh.plugins`
* bundle-layer list against the installed state (a dependency resolving to a
* package that declares `dsh.patch` joins the layer stack; a removed or
* patch-less dependency leaves it). Reconciling by installed state, not by
* dependency diff, means `update` activates a package that gained its
* `dsh.patch` in a newer version.
* `pnpm <args...>` in the profile directory, then reconcile the
* `dsh.profile.bundles` layer list against the installed state (a dependency
* resolving to a package that declares `dsh.bundle` joins the layer stack; a
* removed or bundle-less dependency leaves it). Reconciling by installed
* state, not by dependency diff, means `update` activates a package that
* gained its `dsh.bundle` declaration in a newer version.
* @module @deepseek-ai/dsh/plugin
*/
@@ -14,7 +14,7 @@ import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import {
DEFAULT_PROFILE_PLUGINS,
DEFAULT_PROFILE_BUNDLES,
initProfile,
PROFILE_TEMPLATES,
readProfileManifest,
@@ -31,7 +31,7 @@ const NAME = 'dsh'
* Whether a resolved dependency exports a profile patch, i.e. is a bundle.
* @param packageName - the dependency's package name.
* @param profileDir - the profile directory (resolution anchor).
* @returns true when the package manifest declares `dsh.patch`.
* @returns true when the package manifest declares `dsh.bundle`.
*/
function exportsPatch(packageName: string, profileDir: string): boolean {
let dir: string
@@ -41,25 +41,26 @@ function exportsPatch(packageName: string, profileDir: string): boolean {
return false // pnpm reported success yet the package is unresolvable — treat as plain
}
const manifest = readProfileManifest(NAME, dir)
return manifest.dsh?.patch !== undefined
return manifest.dsh?.bundle?.patch !== undefined
}
/**
* Reconcile `dsh.plugins` against the installed state: pnpm has already
* written the real installed names (so a git/path/tarball/alias spec on the
* command line reconciles by its true package name) and materialized the
* packages. A dependency that resolves to a `dsh.patch`-declaring package
* joins the layer stack (appended in dependency order); a dependency-listed
* name that no longer does — removed, or the installed version dropped the
* declaration — leaves it. In-box bundles from the profile template are not
* dependencies and are never touched. Warns once per newly-added patch-less
* dependency (a plain library is fine; the warning is orientation).
* Reconcile `dsh.profile.bundles` against the installed state: pnpm has
* already written the real installed names (so a git/path/tarball/alias spec
* on the command line reconciles by its true package name) and materialized
* the packages. A dependency that resolves to a `dsh.bundle`-declaring
* package joins the layer stack (appended in dependency order); a
* dependency-listed name that no longer does — removed, or the installed
* version dropped the declaration — leaves it. In-box bundles from the
* profile template are not dependencies and are never touched. Warns once
* per newly-added bundle-less dependency (a plain library is fine; the
* warning is orientation).
*/
function reconcilePlugins(before: ProfileManifest, profileDir: string): void {
const after = readProfileManifest(NAME, profileDir)
const beforeDeps = new Set(Object.keys(before.dependencies ?? {}))
const dependencies = Object.keys(after.dependencies ?? {})
const plugins = after.dsh?.plugins ?? []
const plugins = after.dsh?.profile?.bundles ?? []
let changed = false
for (const packageName of dependencies) {
const isBundle = exportsPatch(packageName, profileDir)
@@ -68,7 +69,7 @@ function reconcilePlugins(before: ProfileManifest, profileDir: string): void {
changed = true
} else if (!isBundle && !beforeDeps.has(packageName)) {
process.stderr.write(
`${NAME}: warning: ${packageName} declares no dsh.patch — installed as a plain dependency, not a profile layer `
`${NAME}: warning: ${packageName} declares no dsh.bundle — installed as a plain dependency, not a profile layer `
+ '(a later update that gains one activates it automatically)\n',
)
}
@@ -85,7 +86,7 @@ function reconcilePlugins(before: ProfileManifest, profileDir: string): void {
}
}
if (!changed) return
after.dsh = { ...after.dsh, plugins }
after.dsh = { ...after.dsh, profile: { ...after.dsh?.profile, bundles: plugins } }
writeProfileManifest(profileDir, after)
}
@@ -119,7 +120,7 @@ function anchorPathSpec(argument: string, cwd: string): string {
export function runPlugin(profile: string, args: readonly string[]): number {
const dir = resolveProfileDir(profile)
if (!existsSync(join(dir, 'package.json'))) {
initProfile(dir, PROFILE_TEMPLATES[profile] ?? DEFAULT_PROFILE_PLUGINS)
initProfile(dir, PROFILE_TEMPLATES[profile] ?? DEFAULT_PROFILE_BUNDLES)
process.stderr.write(`${NAME}: initialized profile ${profile} at ${dir}\n`)
}
const before = readProfileManifest(NAME, dir)

View File

@@ -1,6 +1,6 @@
/**
* Shared profile boot for every `dsh` surface: resolve the profile, stack its
* patch layers (bundle layers in `dsh.plugins` order, the profile's own
* patch layers (bundle layers in `dsh.profile.bundles` order, the profile's own
* `cordis.patch.yml`, `--patch` overlays, flag-derived patches, the telemetry
* switch), mount the tree over the profile's empty root config, keep the
* profile patch layer live, and wire fail-loud plus bounded shutdown.
@@ -21,7 +21,7 @@ import {
loadOverlayPatches,
loadProfile,
PROFILE_PATCH_FILENAME,
watchPersonalPatches,
watchUserPatches,
type Profile,
} from '@deepseek-ai/dsh-app-boot'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
@@ -51,7 +51,7 @@ const HEADLESS_ROW_ID = 'headless-runner'
/** The empty root entry list every profile tree patches over. */
const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches:
# each bundle in package.json's dsh.plugins, then cordis.patch.yml, then any
# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any
# --patch overlays. Edit cordis.patch.yml, not this file.
[]
`
@@ -119,7 +119,7 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
/**
* Load `name` and compose its effective patch stack: bundle layers in
* `dsh.plugins` order, the profile's user layer, the home-level user layer
* `dsh.profile.bundles` order, the profile's user layer, the home-level user layer
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
* every profile, so it outranks the per-profile layer), `--patch` overlays,
* then flag patches derived from the composed rows, then the telemetry
@@ -251,12 +251,12 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
}
await ctx.loader.create({ name: '@cordisjs/plugin-hmr', config: { root: [] } })
}
await watchPersonalPatches(ctx, {
await watchUserPatches(ctx, {
binName: NAME,
filename: composed.profile.patchPath,
compose: composeLive,
})
await watchPersonalPatches(ctx, {
await watchUserPatches(ctx, {
binName: NAME,
filename: homePatchPath(),
compose: composeLive,