@@ -10,8 +10,21 @@ import {
1010} from './config.js' ;
1111import { runAuth , requestComicApi } from './auth.js' ;
1212import { CliError } from './errors.js' ;
13+ import { VERSION } from './version.js' ;
1314
14- export const VERSION = '0.1.0-beta.1' ;
15+ export { VERSION } from './version.js' ;
16+
17+ const SUPPORTED_COMIC_SIZES = new Set ( [
18+ '1024x1024' ,
19+ '512x768' ,
20+ '512x1024' ,
21+ '576x1024' ,
22+ '768x1024' ,
23+ '1024x768' ,
24+ '768x512' ,
25+ '1024x576' ,
26+ '1024x512' ,
27+ ] ) ;
1528
1629export async function main (
1730 argv = process . argv . slice ( 2 ) ,
@@ -30,10 +43,10 @@ export async function main(
3043 io . stdout . write ( `llamagen ${ VERSION } \n` ) ;
3144 return 0 ;
3245 }
33- if ( command === 'auth' ) return runAuth ( args , globals , env , io , runtime ) ;
46+ if ( command === 'auth' ) return await runAuth ( args , globals , env , io , runtime ) ;
3447 if ( command === 'config' ) return runConfig ( args , env , io ) ;
35- if ( command === 'comic' ) return runComic ( args , globals , env , io ) ;
36- if ( command === 'animation' ) return runAnimation ( args , globals , env , io ) ;
48+ if ( command === 'comic' ) return await runComic ( args , globals , env , io ) ;
49+ if ( command === 'animation' ) return await runAnimation ( args , globals , env , io ) ;
3750 throw new CliError ( `Unknown command: ${ command } ` , 2 ) ;
3851 } catch ( error ) {
3952 const code = error instanceof CliError ? error . code : 1 ;
@@ -50,42 +63,77 @@ async function runComic(args, globals, env, io) {
5063 }
5164 if ( subcommand === 'create' ) {
5265 const options = parseOptions ( args ) ;
66+ assertAllowedOptions ( options , [
67+ 'prompt' , 'promptUrl' , 'size' , 'preset' , 'style' , 'language' , 'fixPanelNum' , 'wait' ,
68+ ] , 'comic create' ) ;
5369 requireOption ( options , 'prompt' ) ;
70+ const size = options . size || '1024x1024' ;
71+ if ( ! SUPPORTED_COMIC_SIZES . has ( size ) ) {
72+ throw new CliError ( `Unsupported comic size: ${ size } ` , 2 ) ;
73+ }
5474 const body = compact ( {
5575 prompt : options . prompt ,
5676 promptUrl : options . promptUrl ,
57- size : options . size ,
58- preset : options . preset ,
77+ size,
78+ preset : options . preset || 'neutral' ,
5979 style : options . style ,
6080 language : options . language ,
61- fixPanelNum : numberOption ( options . fixPanelNum ) ,
81+ fixPanelNum : integerOption ( options . fixPanelNum , '--fix-panel-num' , 1 , 20 ) ,
6282 } ) ;
6383 const job = await apiRequest ( 'POST' , '/v1/comics/generations' , body , globals , env ) ;
64- if ( options . wait ) writeJson ( io , await waitForComic ( job . id , globals , env ) ) ;
65- else writeJson ( io , job ) ;
84+ if ( options . wait ) {
85+ if ( typeof job . id !== 'string' || ! job . id ) {
86+ throw new CliError ( 'Comic API did not return a generation ID.' , 1 ) ;
87+ }
88+ const completed = await waitForComic ( job . id , globals , env ) ;
89+ writeJson ( io , completed ) ;
90+ if ( isFailureStatus ( completed . status ) ) {
91+ io . stderr . write ( `Comic generation ${ job . id } ended with status ${ completed . status } .\n` ) ;
92+ return 1 ;
93+ }
94+ } else writeJson ( io , job ) ;
6695 return 0 ;
6796 }
6897 if ( subcommand === 'get' ) {
6998 const id = args . shift ( ) ;
7099 if ( ! id ) throw new CliError ( 'Usage: llamagen comic get <generation_id>' , 2 ) ;
71- writeJson ( io , await apiRequest ( 'GET' , `/v1/comics/generations/${ encodeURIComponent ( id ) } ` , undefined , globals , env ) ) ;
100+ const options = parseOptions ( args ) ;
101+ assertAllowedOptions ( options , [ 'page' , 'panel' ] , 'comic get' ) ;
102+ const query = new URLSearchParams ( ) ;
103+ const page = integerOption ( options . page , '--page' , 0 ) ;
104+ const panel = integerOption ( options . panel , '--panel' , 0 ) ;
105+ if ( page !== undefined ) query . set ( 'page' , String ( page ) ) ;
106+ if ( panel !== undefined ) query . set ( 'panel' , String ( panel ) ) ;
107+ const suffix = query . size ? `?${ query . toString ( ) } ` : '' ;
108+ writeJson ( io , await apiRequest ( 'GET' , `/v1/comics/generations/${ encodeURIComponent ( id ) } ${ suffix } ` , undefined , globals , env ) ) ;
72109 return 0 ;
73110 }
74111 if ( subcommand === 'continue' ) {
75112 const id = args . shift ( ) ;
76113 if ( ! id ) throw new CliError ( 'Usage: llamagen comic continue <generation_id> --prompt <prompt>' , 2 ) ;
77114 const options = parseOptions ( args ) ;
115+ assertAllowedOptions ( options , [ 'prompt' , 'fixPanelNum' ] , 'comic continue' ) ;
78116 requireOption ( options , 'prompt' ) ;
79- writeJson ( io , await apiRequest ( 'PATCH' , `/v1/comics/generations/${ encodeURIComponent ( id ) } ` , { prompt : options . prompt , action : 'continueWrite' } , globals , env ) ) ;
117+ writeJson ( io , await apiRequest ( 'PATCH' , `/v1/comics/generations/${ encodeURIComponent ( id ) } ` , compact ( {
118+ prompt : options . prompt ,
119+ fixPanelNum : integerOption ( options . fixPanelNum , '--fix-panel-num' , 1 , 20 ) ,
120+ action : 'continueWrite' ,
121+ } ) , globals , env ) ) ;
80122 return 0 ;
81123 }
82124 if ( subcommand === 'update-panel' ) {
83125 const id = args . shift ( ) ;
84126 if ( ! id ) throw new CliError ( 'Usage: llamagen comic update-panel <generation_id> --page <n> --panel <n> --prompt <prompt>' , 2 ) ;
85127 const options = parseOptions ( args ) ;
128+ assertAllowedOptions ( options , [ 'page' , 'panel' , 'prompt' , 'panelPrompt' ] , 'comic update-panel' ) ;
129+ const panel = integerOption ( options . panel , '--panel' , 0 ) ;
130+ if ( panel === undefined ) throw new CliError ( 'Missing required option --panel' , 2 ) ;
131+ if ( ! hasNonEmptyString ( options . prompt ) && ! hasNonEmptyString ( options . panelPrompt ) ) {
132+ throw new CliError ( 'Missing required option --prompt or --panel-prompt' , 2 ) ;
133+ }
86134 const body = compact ( {
87- page : numberOption ( options . page ) ,
88- panel : numberOption ( options . panel ) ,
135+ page : integerOption ( options . page , '--page' , 0 ) ,
136+ panel,
89137 prompt : options . prompt ,
90138 panelPrompt : options . panelPrompt ,
91139 action : 'regeneratePanel' ,
@@ -108,6 +156,7 @@ async function runAnimation(args, globals, env, io) {
108156 }
109157 if ( subcommand === 'create' ) {
110158 const options = parseOptions ( args ) ;
159+ assertAllowedOptions ( options , [ 'prompt' ] , 'animation create' ) ;
111160 requireOption ( options , 'prompt' ) ;
112161 writeJson ( io , await apiRequest ( 'POST' , '/v1/artworks/generations' , { prompt : options . prompt } , globals , env ) ) ;
113162 return 0 ;
@@ -222,8 +271,8 @@ function parseGlobals(argv) {
222271 else if ( arg === '--site-url' ) globals . siteUrl = requiredGlobalValue ( argv , ++ index , arg ) ;
223272 else if ( arg === '--api-url' ) globals . apiUrl = requiredGlobalValue ( argv , ++ index , arg ) ;
224273 else if ( arg === '--base-url' ) globals . baseUrl = requiredGlobalValue ( argv , ++ index , arg ) ;
225- else if ( arg === '--timeout-ms' ) globals . timeoutMs = requiredGlobalValue ( argv , ++ index , arg ) ;
226- else if ( arg === '--poll-interval-ms' ) globals . pollIntervalMs = requiredGlobalValue ( argv , ++ index , arg ) ;
274+ else if ( arg === '--timeout-ms' ) globals . timeoutMs = integerOption ( requiredGlobalValue ( argv , ++ index , arg ) , arg , 1 ) ;
275+ else if ( arg === '--poll-interval-ms' ) globals . pollIntervalMs = integerOption ( requiredGlobalValue ( argv , ++ index , arg ) , arg , 1 ) ;
227276 else args . push ( arg ) ;
228277 }
229278 return { args, globals } ;
@@ -257,13 +306,35 @@ function requireOption(options, key) {
257306 }
258307}
259308
260- function numberOption ( value ) {
261- if ( value === undefined || value === true ) return undefined ;
309+ function integerOption ( value , flag , min , max = Number . MAX_SAFE_INTEGER ) {
310+ if ( value === undefined ) return undefined ;
311+ if ( value === true ) {
312+ throw new CliError ( `${ flag } requires a value.` , 2 ) ;
313+ }
262314 const parsed = Number ( value ) ;
263- if ( ! Number . isFinite ( parsed ) ) throw new CliError ( `Expected numeric value, got ${ value } ` , 2 ) ;
315+ if ( ! Number . isSafeInteger ( parsed ) || parsed < min || parsed > max ) {
316+ const range = max === Number . MAX_SAFE_INTEGER ? `${ min } or greater` : `${ min } to ${ max } ` ;
317+ throw new CliError ( `${ flag } must be an integer from ${ range } .` , 2 ) ;
318+ }
264319 return parsed ;
265320}
266321
322+ function assertAllowedOptions ( options , allowed , command ) {
323+ const allowedSet = new Set ( allowed ) ;
324+ const unknown = Object . keys ( options ) . find ( ( key ) => ! allowedSet . has ( key ) ) ;
325+ if ( unknown ) {
326+ throw new CliError ( `Unknown option for ${ command } : --${ kebabCase ( unknown ) } ` , 2 ) ;
327+ }
328+ }
329+
330+ function hasNonEmptyString ( value ) {
331+ return typeof value === 'string' && value . trim ( ) . length > 0 ;
332+ }
333+
334+ function isFailureStatus ( status ) {
335+ return [ 'failed' , 'canceled' , 'cancelled' ] . includes ( String ( status ) . toLowerCase ( ) ) ;
336+ }
337+
267338function compact ( value ) {
268339 return Object . fromEntries (
269340 Object . entries ( value ) . filter ( ( [ , item ] ) => item !== undefined && item !== null && item !== false ) ,
@@ -287,7 +358,7 @@ function helpText() {
287358}
288359
289360function comicHelpText ( ) {
290- return `Usage:\n llamagen comic create --prompt <prompt> [--prompt-url <url>] [--size 1024x1024] [--wait]\n llamagen comic get <generation_id>\n llamagen comic continue <generation_id> --prompt <prompt>\n llamagen comic update-panel <generation_id> --page <n> --panel <n> --prompt <prompt>\n llamagen comic usage\n` ;
361+ return `Usage:\n llamagen comic create --prompt <prompt> [--prompt-url <url>] [--size 1024x1024] [--fix-panel-num <1-20>] [-- wait]\n llamagen comic get <generation_id> [--page <n>] [--panel <n>] \n llamagen comic continue <generation_id> --prompt <prompt> [--fix-panel-num <1-20>] \n llamagen comic update-panel <generation_id> [ --page <n>] --panel <n> ( --prompt <prompt> | --panel-prompt <prompt>) \n llamagen comic usage\n` ;
291362}
292363
293364function animationHelpText ( ) {
0 commit comments