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.
90 lines
4.8 KiB
TypeScript
90 lines
4.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { parseDshArgs } from '../src/args.ts'
|
|
|
|
const parse = (argv: string[]) => parseDshArgs(argv, '1.2.3')
|
|
|
|
/** Capture the process exit code while muting Commander's output. */
|
|
function exitCode(argv: string[]): number {
|
|
const exit = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') })
|
|
vi.spyOn(process.stdout, 'write').mockReturnValue(true)
|
|
vi.spyOn(process.stderr, 'write').mockReturnValue(true)
|
|
try {
|
|
parse(argv)
|
|
throw new Error(`expected ${JSON.stringify(argv)} to exit`)
|
|
} catch {
|
|
return exit.mock.calls.at(-1)?.[0] as number
|
|
} finally {
|
|
vi.restoreAllMocks()
|
|
}
|
|
}
|
|
|
|
afterEach(() => { vi.restoreAllMocks() })
|
|
|
|
describe('parseDshArgs', () => {
|
|
it('routes profile boots, one-shot tasks, and the web alias', () => {
|
|
expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [] })
|
|
expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
|
|
.toEqual({ mode: 'profile', profile: 'headless', patches: [], task: 'run the tests' })
|
|
expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--patch', 'b.yml']))
|
|
.toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'] })
|
|
expect(parse(['web'])).toEqual({ mode: 'web', dev: false, patches: [] })
|
|
expect(parse(['web', '--patch', 'web.yml'])).toEqual({ mode: 'web', dev: false, patches: ['web.yml'] })
|
|
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
|
|
.toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, workspaceRoot: '/w', patches: [] })
|
|
expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9']))
|
|
.toEqual({ mode: 'web', dev: false, patches: [], trustedHosts: ['harness.internal:3080', 'lab.internal', '10.0.0.9'] })
|
|
})
|
|
|
|
it('routes the plugin pnpm forwarder', () => {
|
|
expect(parse(['plugin', '--profile', 'tui', 'add', 'turtle-ui']))
|
|
.toEqual({ mode: 'plugin', profile: 'tui', args: ['add', 'turtle-ui'] })
|
|
expect(parse(['plugin', '--profile', 'tui', 'remove', 'turtle-ui']))
|
|
.toEqual({ mode: 'plugin', profile: 'tui', args: ['remove', 'turtle-ui'] })
|
|
expect(parse(['plugin', '--profile', 'tui', 'why', 'cordis']))
|
|
.toEqual({ mode: 'plugin', profile: 'tui', args: ['why', 'cordis'] })
|
|
// Unknown pnpm flags forward verbatim.
|
|
expect(parse(['plugin', '--profile', 'tui', 'add', '--save-dev', 'x']))
|
|
.toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '--save-dev', 'x'] })
|
|
})
|
|
|
|
it('routes profile and web config dumps', () => {
|
|
expect(parse(['--profile', 'web', '--dump-config']))
|
|
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: false, patches: [] })
|
|
expect(parse(['--profile', 'web', '--dump-default-config']))
|
|
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: true, patches: [] })
|
|
expect(parse(['--profile', 'tui', '--dump-config', '--patch', 'x.yml']))
|
|
.toEqual({ mode: 'dump-config', profile: 'tui', defaultOnly: false, patches: ['x.yml'] })
|
|
expect(parse(['web', '--dump-config']))
|
|
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: false, patches: [] })
|
|
expect(parse(['web', '--dump-default-config']))
|
|
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: true, patches: [] })
|
|
})
|
|
|
|
it('rejects missing profile, removed flags, and contradictory inputs', () => {
|
|
expect(exitCode([])).toBe(1)
|
|
expect(exitCode(['tui'])).toBe(1) // a bare word is a task without --profile
|
|
expect(exitCode(['--config', 'c.yml'])).toBe(1) // removed
|
|
expect(exitCode(['-p', 'task'])).toBe(1) // removed
|
|
expect(exitCode(['--profile', ''])).toBe(1)
|
|
expect(exitCode(['--profile', 'x', '--patch='])).toBe(1)
|
|
expect(exitCode(['--dump-config'])).toBe(1)
|
|
expect(exitCode(['--profile', 'x', '--dump-config', '--dump-default-config'])).toBe(1)
|
|
expect(exitCode(['--profile', 'x', '--dump-default-config', '--patch', 'p.yml'])).toBe(1)
|
|
expect(exitCode(['--profile', 'x', '--dump-config', 'task'])).toBe(1)
|
|
expect(exitCode(['--bogus'])).toBe(1)
|
|
expect(exitCode(['--profile', 'x', 'web'])).toBe(1)
|
|
expect(exitCode(['web', '--dump-config', '--dump-default-config'])).toBe(1)
|
|
expect(exitCode(['web', '--dump-default-config', '--patch', 'w.yml'])).toBe(1)
|
|
expect(exitCode(['web', '--patch='])).toBe(1)
|
|
expect(exitCode(['plugin', 'add', 'x'])).toBe(1) // --profile required
|
|
expect(exitCode(['plugin', '--profile', 'tui'])).toBe(1) // nothing to forward
|
|
expect(exitCode(['plugin', '--profile', ''])).toBe(1)
|
|
expect(exitCode(['--profile', 'x', 'plugin', 'add', 'y'])).toBe(1)
|
|
})
|
|
|
|
it('exits 0 for help and version', () => {
|
|
expect(exitCode(['--help'])).toBe(0)
|
|
expect(exitCode(['--version'])).toBe(0)
|
|
})
|
|
})
|