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

@@ -37,7 +37,7 @@ function stageInstallation(bundles: Record<string, { patch?: string; deps?: Reco
name,
version: '0.0.0',
dependencies: spec.deps ?? {},
...spec.patch === undefined ? {} : { dsh: { patch: './cordis.patch.yml' } },
...spec.patch === undefined ? {} : { dsh: { bundle: { patch: './cordis.patch.yml' } } },
}))
if (spec.patch !== undefined) writeFileSync(join(dir, 'cordis.patch.yml'), spec.patch)
}
@@ -61,13 +61,13 @@ describe('initProfile', () => {
const dir = resolveProfileDir('tui', home)
initProfile(dir, ['@deepseek-ai/dsh-base'])
const manifest = readProfileManifest('t', dir)
expect(manifest.dsh?.plugins).toEqual(['@deepseek-ai/dsh-base'])
expect(manifest.dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base'])
expect(readFileSync(join(dir, PROFILE_PATCH_FILENAME), 'utf8')).toContain('[]')
expect(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8')).toContain('nodeLinker: hoisted')
// Re-init keeps user edits.
writeFileSync(join(dir, PROFILE_PATCH_FILENAME), '- id: x\n config: {}\n')
initProfile(dir, ['other'])
expect(readProfileManifest('t', dir).dsh?.plugins).toEqual(['@deepseek-ai/dsh-base'])
expect(readProfileManifest('t', dir).dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base'])
expect(readFileSync(join(dir, PROFILE_PATCH_FILENAME), 'utf8')).toContain('- id: x')
})
})
@@ -75,8 +75,8 @@ describe('initProfile', () => {
describe('manifest round-trip', () => {
it('writes and reads back, and fails loud on a broken manifest', () => {
const dir = tmp()
writeProfileManifest(dir, { name: 'p', dsh: { plugins: ['a'] } })
expect(readProfileManifest('t', dir).dsh?.plugins).toEqual(['a'])
writeProfileManifest(dir, { name: 'p', dsh: { profile: { bundles: ['a'] } } })
expect(readProfileManifest('t', dir).dsh?.profile?.bundles).toEqual(['a'])
writeFileSync(join(dir, 'package.json'), '[]')
expect(() => readProfileManifest('t', dir)).toThrow('must hold a JSON object')
expect(() => readProfileManifest('t', join(dir, 'nope'))).toThrow('failed to read profile manifest')
@@ -109,7 +109,7 @@ describe('resolveBundleDir', () => {
name: 'sealed-bundle',
version: '0.0.0',
exports: { '.': './index.js' },
dsh: { patch: './cordis.patch.yml' },
dsh: { bundle: { patch: './cordis.patch.yml' } },
}))
writeFileSync(join(dir, 'index.js'), '')
writeFileSync(join(dir, 'cordis.patch.yml'), '[]\n')
@@ -118,7 +118,7 @@ describe('resolveBundleDir', () => {
})
describe('loadProfile', () => {
it('resolves each dsh.plugins bundle to its patch layer in order, plus the user layer', () => {
it('resolves each dsh.profile.bundles entry to its patch layer in order, plus the user layer', () => {
const anchor = stageInstallation({
'bundle-a': { patch: '- insert:\n - id: a\n name: pkg-a\n' },
'bundle-b': { patch: '- id: a\n config:\n v: 2\n' },
@@ -157,16 +157,16 @@ describe('loadProfile', () => {
} catch {
// Resolution failure is the plain-Node outcome for this empty anchor.
}
expect(readProfileManifest('t', resolveProfileDir('web', home)).dsh?.plugins)
expect(readProfileManifest('t', resolveProfileDir('web', home)).dsh?.profile?.bundles)
.toEqual([...PROFILE_TEMPLATES.web ?? []])
})
it('fails loud when a listed bundle declares no dsh.patch', () => {
it('fails loud when a listed bundle declares no dsh.bundle', () => {
const anchor = stageInstallation({ 'not-a-bundle': {} })
const home = tmp()
const dir = resolveProfileDir('demo', home)
initProfile(dir, ['not-a-bundle'])
expect(() => loadProfile('t', 'demo', anchor, home)).toThrow('declares no dsh.patch')
expect(() => loadProfile('t', 'demo', anchor, home)).toThrow('declares no dsh.bundle')
})
})