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 files changed
+203 -156

No files matched your search

+23 -22
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)