1- import { statSync } from 'node:fs'
21import { Command , Option } from 'clipanion'
2+ import { z } from 'zod'
33
44import type { InterpolatableRobotAiChatInstructionsWithHiddenFieldsInput } from '../../alphalib/types/robots/ai-chat.ts'
55import type { InterpolatableRobotImageDescribeInstructionsWithHiddenFieldsInput } from '../../alphalib/types/robots/image-describe.ts'
6- import {
7- concurrencyOption ,
8- countProvidedInputs ,
9- deleteAfterProcessingOption ,
10- inputPathsOption ,
11- recursiveOption ,
12- reprocessStaleOption ,
13- validateSharedFileProcessingOptions ,
14- watchOption ,
15- } from '../fileProcessingOptions.ts'
16- import { prepareIntentInputs } from '../intentRuntime.ts'
17- import type { AssembliesCreateOptions } from './assemblies.ts'
6+ import type { IntentFileCommandDefinition , PreparedIntentInputs } from '../intentRuntime.ts'
7+ import { GeneratedWatchableFileIntentCommand } from '../intentRuntime.ts'
188import * as assembliesCommands from './assemblies.ts'
19- import { AuthenticatedCommand } from './BaseCommand.ts'
209
2110const imageDescribeFields = [ 'labels' , 'altText' , 'title' , 'caption' , 'description' ] as const
2211
@@ -31,15 +20,6 @@ const wordpressDescribeFields = [
3120
3221const defaultDescribeModel = 'anthropic/claude-sonnet-4-5'
3322
34- function isHttpUrl ( value : string ) : boolean {
35- try {
36- const url = new URL ( value )
37- return url . protocol === 'http:' || url . protocol === 'https:'
38- } catch {
39- return false
40- }
41- }
42-
4323function parseFields ( value : string [ ] | undefined ) : ImageDescribeField [ ] {
4424 const rawFields = ( value ?? [ ] )
4525 . flatMap ( ( part ) => part . split ( ',' ) )
@@ -259,7 +239,27 @@ function buildDescribeStep({
259239 return buildAiChatStep ( { fields, model, profile } )
260240}
261241
262- export class ImageDescribeCommand extends AuthenticatedCommand {
242+ const imageDescribeBaseDefinition = {
243+ commandLabel : 'image describe' ,
244+ execution : {
245+ kind : 'single-step' ,
246+ fields : [ ] ,
247+ fixedValues : { } ,
248+ resultStepName : 'describe' ,
249+ schema : z . object ( { } ) ,
250+ } ,
251+ inputPolicy : {
252+ kind : 'required' ,
253+ } ,
254+ outputDescription : 'Write the JSON result to this path or directory' ,
255+ } satisfies IntentFileCommandDefinition
256+
257+ type ResolvedDescribeRequest = {
258+ profile : 'wordpress' | null
259+ requestedFields : ImageDescribeField [ ]
260+ }
261+
262+ export class ImageDescribeCommand extends GeneratedWatchableFileIntentCommand {
263263 static override paths = [ [ 'image' , 'describe' ] ]
264264
265265 static override usage = Command . Usage ( {
@@ -283,17 +283,6 @@ export class ImageDescribeCommand extends AuthenticatedCommand {
283283 ] ,
284284 } )
285285
286- outputPath = Option . String ( '--out,-o' , {
287- description : 'Write the JSON result to this path or directory' ,
288- required : true ,
289- } )
290-
291- inputs = inputPathsOption ( 'Provide an input path, directory, URL, or - for stdin' )
292-
293- inputBase64 = Option . Array ( '--input-base64' , {
294- description : 'Provide base64-encoded input content directly' ,
295- } )
296-
297286 fields = Option . Array ( '--fields' , {
298287 description :
299288 'Describe output fields to generate, for example labels or altText,title,caption,description' ,
@@ -307,102 +296,65 @@ export class ImageDescribeCommand extends AuthenticatedCommand {
307296 description : `Model to use for generated text fields (default: ${ defaultDescribeModel } )` ,
308297 } )
309298
310- recursive = recursiveOption ( )
311-
312- deleteAfterProcessing = deleteAfterProcessingOption ( )
313-
314- reprocessStale = reprocessStaleOption ( )
315-
316- watch = watchOption ( )
317-
318- concurrency = concurrencyOption ( )
319-
320- private getProvidedInputCount ( ) : number {
321- return countProvidedInputs ( {
322- inputs : this . inputs ,
323- inputBase64 : this . inputBase64 ,
324- } )
325- }
326-
327- private hasTransientInputSources ( ) : boolean {
328- return (
329- ( this . inputs ?. some ( ( input ) => isHttpUrl ( input ) ) ?? false ) ||
330- ( this . inputBase64 ?. length ?? 0 ) > 0
331- )
299+ protected override getIntentDefinition ( ) : IntentFileCommandDefinition {
300+ return imageDescribeBaseDefinition
332301 }
333302
334- private isDirectoryOutputTarget ( ) : boolean {
335- try {
336- return statSync ( this . outputPath ) . isDirectory ( )
337- } catch {
338- return false
303+ protected override getIntentRawValues ( ) : Record < string , unknown > {
304+ return {
305+ fields : this . fields ,
306+ forProfile : this . forProfile ,
307+ model : this . model ,
339308 }
340309 }
341310
342- protected override async run ( ) : Promise < number | undefined > {
343- if ( this . getProvidedInputCount ( ) === 0 ) {
344- this . output . error ( 'image describe requires --input or --input-base64' )
345- return 1
346- }
347-
348- const sharedValidationError = validateSharedFileProcessingOptions ( {
349- explicitInputCount : this . getProvidedInputCount ( ) ,
350- singleAssembly : false ,
351- watch : this . watch ,
352- watchRequiresInputsMessage : 'image describe --watch requires --input or --input-base64' ,
353- } )
354- if ( sharedValidationError != null ) {
355- this . output . error ( sharedValidationError )
356- return 1
357- }
358-
359- if ( this . watch && this . hasTransientInputSources ( ) ) {
360- this . output . error ( '--watch is only supported for filesystem inputs' )
361- return 1
362- }
363-
364- const explicitFields = parseFields ( this . fields )
365- const profile = resolveProfile ( this . forProfile )
311+ private resolveDescribeRequest ( rawValues : Record < string , unknown > ) : ResolvedDescribeRequest {
312+ const explicitFields = parseFields ( rawValues . fields as string [ ] | undefined )
313+ const profile = resolveProfile ( rawValues . forProfile as string | undefined )
366314 const requestedFields = resolveRequestedFields ( { explicitFields, profile } )
367315 validateRequestedFields ( {
368316 explicitFields,
369317 fields : requestedFields ,
370- model : this . model ,
318+ model : rawValues . model as string ,
371319 profile,
372320 } )
373321
374- const preparedInputs = await prepareIntentInputs ( {
375- inputValues : this . inputs ?? [ ] ,
376- inputBase64Values : this . inputBase64 ?? [ ] ,
377- } )
322+ return {
323+ profile,
324+ requestedFields,
325+ }
326+ }
378327
379- try {
380- if ( this . watch && preparedInputs . hasTransientInputs ) {
381- this . output . error ( '--watch is only supported for filesystem inputs' )
382- return 1
383- }
384-
385- const { hasFailures } = await assembliesCommands . create ( this . output , this . client , {
386- del : this . deleteAfterProcessing ,
387- inputs : preparedInputs . inputs ,
388- recursive : this . recursive ,
389- reprocessStale : this . reprocessStale ,
390- watch : this . watch ,
391- concurrency : this . concurrency ,
392- output : this . outputPath ,
393- outputMode : this . isDirectoryOutputTarget ( ) ? 'directory' : 'file' ,
394- stepsData : {
395- describe : buildDescribeStep ( {
396- fields : requestedFields ,
397- model : this . model ,
398- profile,
399- } ) ,
400- } satisfies AssembliesCreateOptions [ 'stepsData' ] ,
401- } )
402-
403- return hasFailures ? 1 : undefined
404- } finally {
405- await Promise . all ( preparedInputs . cleanup . map ( ( cleanup ) => cleanup ( ) ) )
328+ protected override validateBeforePreparingInputs (
329+ rawValues : Record < string , unknown > ,
330+ ) : number | undefined {
331+ const validationError = super . validateBeforePreparingInputs ( rawValues )
332+ if ( validationError != null ) {
333+ return validationError
406334 }
335+
336+ this . resolveDescribeRequest ( rawValues )
337+ return undefined
338+ }
339+
340+ protected override async executePreparedInputs (
341+ rawValues : Record < string , unknown > ,
342+ preparedInputs : PreparedIntentInputs ,
343+ ) : Promise < number | undefined > {
344+ const { profile, requestedFields } = this . resolveDescribeRequest ( rawValues )
345+ const { hasFailures } = await assembliesCommands . create ( this . output , this . client , {
346+ ...this . getCreateOptions ( preparedInputs . inputs ) ,
347+ output : this . outputPath ,
348+ outputMode : this . resolveOutputMode ( ) ,
349+ stepsData : {
350+ describe : buildDescribeStep ( {
351+ fields : requestedFields ,
352+ model : rawValues . model as string ,
353+ profile,
354+ } ) ,
355+ } ,
356+ } )
357+
358+ return hasFailures ? 1 : undefined
407359 }
408360}
0 commit comments