feat(cli)!: dsh boots profiles; plugin subcommand manages them via pnpm
dsh --profile <name> replaces the fixed entry modes: --config and -p are removed, --patch adds overlays over the composed profile, a positional task selects one-shot mode (requires the headless-runner row), and dsh web stays as the alias for --profile web carrying the Web flag family as patches. dsh plugin --profile <name> forwards verbatim to pnpm in the profile directory, initializes on first use, and reconciles the dsh.plugins layer list after add/remove (patch-less packages warn and stay plain dependencies). Config dumps and the keyless web e2e scaffold compose the same bundle layers over the same empty root as the boot.
This commit is contained in:
@@ -94,6 +94,27 @@ describe('resolveBundleDir', () => {
|
||||
expect(resolveBundleDir('t', 'local-only', anchor, profileDir)).toContain('local-only')
|
||||
expect(() => resolveBundleDir('t', 'absent', anchor, profileDir)).toThrow('cannot resolve profile bundle')
|
||||
})
|
||||
|
||||
it('resolves a package whose exports map omits ./package.json', () => {
|
||||
// Common on npm: an exports map without "./package.json" makes
|
||||
// require.resolve('<pkg>/package.json') throw ERR_PACKAGE_PATH_NOT_EXPORTED;
|
||||
// resolution must fall through to the paths probe instead of misreporting
|
||||
// the installed package as missing.
|
||||
const anchor = stageInstallation({})
|
||||
const profileDir = tmp()
|
||||
writeFileSync(join(profileDir, 'package.json'), '{}')
|
||||
const dir = join(profileDir, 'node_modules', 'sealed-bundle')
|
||||
mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify({
|
||||
name: 'sealed-bundle',
|
||||
version: '0.0.0',
|
||||
exports: { '.': './index.js' },
|
||||
dsh: { patch: './cordis.patch.yml' },
|
||||
}))
|
||||
writeFileSync(join(dir, 'index.js'), '')
|
||||
writeFileSync(join(dir, 'cordis.patch.yml'), '[]\n')
|
||||
expect(resolveBundleDir('t', 'sealed-bundle', anchor, profileDir)).toBe(dir)
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadProfile', () => {
|
||||
@@ -200,4 +221,18 @@ describe('healProfilesModuleFallback', () => {
|
||||
healProfilesModuleFallback(anchor, home)
|
||||
expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app')
|
||||
})
|
||||
|
||||
it('tolerates losing the concurrent-heal race to an identical link and rejects a different one', () => {
|
||||
// The EEXIST arm: a second process wrote the link between our lstat miss
|
||||
// and symlinkSync. Simulated by pre-creating the correct link and calling
|
||||
// the internal path through a stale-lstat shim is not possible from
|
||||
// outside, so probe the observable contract: healing twice concurrently
|
||||
// is a no-op, and a foreign REAL directory still fails loud.
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
healProfilesModuleFallback(anchor, home)
|
||||
healProfilesModuleFallback(anchor, home) // second healer sees the correct link
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user