@@ -16,16 +16,17 @@ import { cursorAdapter } from './cursor.ts';
1616import { readStandardNativeHookCommands , type TargetHookContract } from './hook-contract.ts' ;
1717import { portableAdapter } from './portable.ts' ;
1818import { pluginAdapter } from './plugin.ts' ;
19- import type {
20- TargetAdapter ,
21- TargetArtifactDocumentContract ,
22- TargetArtifactDocumentValidator ,
23- TargetArtifactLayout ,
24- TargetArtifactOutputLayout ,
25- TargetArtifactSchemaContract ,
26- TargetArtifactValidationContract ,
27- TargetAdapterMetadata ,
28- TargetSchemaDescriptor ,
19+ import {
20+ routedCliBinLayout ,
21+ type TargetAdapter ,
22+ type TargetArtifactDocumentContract ,
23+ type TargetArtifactDocumentValidator ,
24+ type TargetArtifactLayout ,
25+ type TargetArtifactOutputLayout ,
26+ type TargetArtifactSchemaContract ,
27+ type TargetArtifactValidationContract ,
28+ type TargetAdapterMetadata ,
29+ type TargetSchemaDescriptor ,
2930} from './types.ts' ;
3031import type { TargetMcpRuntimeContract } from '../services/mcp-runtime.ts' ;
3132import { deepFreeze } from '../core/freeze.ts' ;
@@ -181,12 +182,31 @@ const snapshotArtifactLayout = (
181182 hookContract : TargetHookContract | undefined ,
182183 mcpRuntime : TargetMcpRuntimeContract | undefined ,
183184) : TargetArtifactLayout => {
185+ // A supported `cli` capability promises a home for the compiled routed CLI,
186+ // and the compiler emits it at exactly one place (`bin/<name>.mjs`), so the
187+ // promise is checked before any early return and against that fixed layout.
188+ const cliSupported = capabilityIsSupported ( adapter . capabilities [ cliBinCapability ] ) ;
189+ const missingCliBinLayout = ( ) : Error =>
190+ new Error ( `Target adapter "${ adapter . name } " declares a supported ${ cliBinCapability } capability without a routed CLI bin layout.` ) ;
184191 const declaredLayout = adapter . artifactLayout ;
185- if ( declaredLayout === undefined ) return emptyArtifactLayout ;
192+ if ( declaredLayout === undefined ) {
193+ if ( cliSupported ) throw missingCliBinLayout ( ) ;
194+ return emptyArtifactLayout ;
195+ }
186196 const layout = record ( declaredLayout ) ;
187197 if ( layout === undefined ) throw new Error ( 'Target adapter artifact layout must be a record.' ) ;
188198
189199 const cliBin = layout . cliBin === undefined ? undefined : snapshotOutputLayout ( layout . cliBin , 'routed CLI bin' ) ;
200+ if ( cliBin === undefined && cliSupported ) throw missingCliBinLayout ( ) ;
201+ if (
202+ cliBin !== undefined &&
203+ ( cliBin . directory !== routedCliBinLayout . directory ||
204+ ! routedCliBinLayout . allowedSuffixes . every ( ( suffix ) => cliBin . allowedSuffixes . includes ( suffix ) ) )
205+ ) {
206+ throw new Error (
207+ `Target adapter "${ adapter . name } " routed CLI bin layout must use directory ${ JSON . stringify ( routedCliBinLayout . directory ) } and admit ${ routedCliBinLayout . allowedSuffixes . map ( ( suffix ) => JSON . stringify ( suffix ) ) . join ( ', ' ) } ; the compiler emits the routed CLI only there.` ,
208+ ) ;
209+ }
190210 const commands = layout . commands === undefined ? undefined : snapshotOutputLayout ( layout . commands , 'commands' ) ;
191211 const hookWrappers = layout . hookWrappers === undefined
192212 ? undefined
@@ -235,9 +255,6 @@ const snapshotArtifactLayout = (
235255 if ( skills !== undefined && ! capabilityIsSupported ( adapter . capabilities . skills ) ) {
236256 throw new Error ( `Target adapter "${ adapter . name } " declares Skill layout without skills capability.` ) ;
237257 }
238- if ( cliBin === undefined && capabilityIsSupported ( adapter . capabilities [ cliBinCapability ] ) ) {
239- throw new Error ( `Target adapter "${ adapter . name } " declares a supported ${ cliBinCapability } capability without a routed CLI bin layout.` ) ;
240- }
241258 return Object . freeze ( {
242259 ...( assets === undefined ? { } : { assets } ) ,
243260 ...( bin === undefined ? { } : { bin } ) ,
0 commit comments