diff --git a/packages/cli/src/commands/docs.ts b/packages/cli/src/commands/docs.ts index 39ee43df6..388d6dcfe 100644 --- a/packages/cli/src/commands/docs.ts +++ b/packages/cli/src/commands/docs.ts @@ -1,7 +1,8 @@ import * as p from '@clack/prompts'; import { validateInstallationOptions } from '@/utils/installation/codegen'; +import { isRendererValidForUseCase } from '@/utils/installation/detect-renderer'; import { RENDERER_LABELS } from '@/utils/installation/renderer-options'; -import type { InstallMethod, Renderer, UseCase } from '@/utils/installation/types'; +import { type InstallMethod, type Renderer, type UseCase, VALID_RENDERERS } from '@/utils/installation/types'; import type { Framework } from '../utils/config.js'; import { getConfigValue } from '../utils/config.js'; import { docExistsInAnyFramework, readBundledDoc, readLlmsTxt } from '../utils/docs.js'; @@ -59,6 +60,12 @@ function mapPresetToUseCase(preset: string): UseCase { return result; } +const PRESET_NAMES: Record = { + 'default-video': 'video', + 'default-audio': 'audio', + 'background-video': 'background-video', +}; + const ALL_RENDERERS = Object.keys(RENDERER_LABELS) as Renderer[]; function validateMedia(media: string): Renderer { @@ -184,6 +191,17 @@ export async function handleDocs(flags: ParsedFlags, positionals: string[]): Pro process.exit(1); } + // The interactive prompt builds its renderer list from VALID_RENDERERS[useCase], + // so an invalid renderer/use-case pairing can only arrive via the explicit + // `--media` flag. Reject it here to keep the CLI at parity with the UI. + if (!isRendererValidForUseCase(opts.renderer, opts.useCase)) { + const validMedia = VALID_RENDERERS[opts.useCase].join(', '); + console.error( + `Error: media type "${opts.renderer}" is not valid for the "${PRESET_NAMES[opts.useCase]}" preset. Valid options: ${validMedia}` + ); + process.exit(1); + } + // The interactive prompt hides CDN for renderers without a CDN build; guard // the non-interactive flag path so a `--install-method cdn` request for one // can't emit a broken snippet. diff --git a/packages/cli/src/commands/tests/docs.test.ts b/packages/cli/src/commands/tests/docs.test.ts index fcbcc62f7..ff91bc700 100644 --- a/packages/cli/src/commands/tests/docs.test.ts +++ b/packages/cli/src/commands/tests/docs.test.ts @@ -300,6 +300,22 @@ describe('handleDocs', () => { ).rejects.toThrow(ExitError); expect(errors()).toContain('no CDN build'); }); + + it('errors when --media is not valid for the chosen --preset (dash + audio)', async () => { + await expect( + handleDocs(htmlFlags({ preset: 'audio', skin: 'default', media: 'dash' }), ['how-to/installation']) + ).rejects.toThrow(ExitError); + const err = errors(); + expect(err).toContain('media type "dash" is not valid for the "audio" preset'); + expect(err).toContain('html5-audio'); + }); + + it('errors when --media is an audio-only renderer for the video preset (mux-audio + video)', async () => { + await expect( + handleDocs(htmlFlags({ preset: 'video', skin: 'default', media: 'mux-audio' }), ['how-to/installation']) + ).rejects.toThrow(ExitError); + expect(errors()).toContain('media type "mux-audio" is not valid for the "video" preset'); + }); }); describe('React framework', () => { diff --git a/packages/cli/src/site-modules.d.ts b/packages/cli/src/site-modules.d.ts index 613bae79f..da638001d 100644 --- a/packages/cli/src/site-modules.d.ts +++ b/packages/cli/src/site-modules.d.ts @@ -67,6 +67,7 @@ declare module '@/utils/installation/detect-renderer' { } export function detectRenderer(url: string, useCase: UseCase): DetectionResult | null; + export function isRendererValidForUseCase(renderer: Renderer, useCase: UseCase): boolean; } declare module '@/utils/installation/cdn-code' {