11import process from 'node:process' ;
22
3+ import chalk from 'chalk' ;
34import yargonaut from 'yargonaut' ;
45import yargs from 'yargs/yargs' ;
56
7+ import { camelCaseToKebabCase , commandRegistry , kebabCaseString } from '../lib/command-framework/apify-command.js' ;
8+ import type { FlagTag , TaggedFlagBuilder } from '../lib/command-framework/flags.js' ;
9+ import { renderMainHelpMenu , selectiveRenderHelpForCommand } from '../lib/command-framework/help.js' ;
610import { readStdin } from '../lib/commands/read-stdin.js' ;
7- import { version } from '../lib/consts.js' ;
11+ import { cliVersion } from '../lib/consts.js' ;
12+ import { error } from '../lib/outputs.js' ;
813
914yargonaut //
1015 . style ( 'blue' )
@@ -13,10 +18,8 @@ yargonaut //
1318 . errorsStyle ( 'red' ) ;
1419
1520export const cli = yargs ( )
16- . scriptName ( 'apify' )
1721 . version ( false )
1822 . help ( false )
19- // .wrap(Math.max(80, process.stdout.columns || 80))
2023 . parserConfiguration ( {
2124 // Disables the automatic conversion of `--foo-bar` to `fooBar` (we handle it manually)
2225 'camel-case-expansion' : false ,
@@ -25,14 +28,186 @@ export const cli = yargs()
2528 // We parse numbers manually
2629 'parse-numbers' : false ,
2730 'parse-positional-numbers' : false ,
31+ 'short-option-groups' : false ,
32+ } )
33+ . strict ( )
34+ . locale ( 'en' )
35+ . updateStrings ( {
36+ // Keys come from https://github.com/yargs/yargs/blob/main/locales/en.json
37+ 'Not enough arguments following: %s' : 'MISSING_ARGUMENT_INPUT %s' ,
38+ // @ts -expect-error @types/yargs is outdated -.-
39+ 'Unknown argument: %s' : {
40+ one : 'UNKNOWN_ARGUMENT_INPUT %s' ,
41+ other : 'UNKNOWN_ARGUMENTS_INPUT %s' ,
42+ } ,
43+ // @ts -expect-error @types/yargs is outdated -.-
44+ 'Not enough non-option arguments: got %s, need at least %s' : {
45+ one : 'NOT_ENOUGH_NON_OPTION_ARGUMENTS_INPUT {"got":%s,"need":%s}' ,
46+ other : 'NOT_ENOUGH_NON_OPTION_ARGUMENTS_INPUT {"got":%s,"need":%s}' ,
47+ } ,
48+ 'Arguments %s and %s are mutually exclusive' : 'ARGUMENTS_ARE_MUTUALLY_EXCLUSIVE_INPUT ["%s","%s"]' ,
49+ } )
50+ . option ( 'help' , {
51+ boolean : true ,
52+ describe : 'Shows this help message.' ,
53+ alias : 'h' ,
2854 } ) ;
2955
3056// @ts -expect-error @types/yargs is outdated -.-
3157cli . usageConfiguration ( { 'hide-types' : true } ) ;
3258
33- export function printCLIVersionAndExit ( ) : never {
34- console . log ( version ) ;
35- process . exit ( 0 ) ;
59+ export function printCLIVersionAndExitIfFlagUsed ( parsed : Awaited < ReturnType < typeof cli . parse > > ) {
60+ if ( parsed . v === true || parsed . version === true ) {
61+ console . log ( cliVersion ) ;
62+ process . exit ( 0 ) ;
63+ }
64+ }
65+
66+ export function printHelpAndExitIfFlagUsed ( parsed : Awaited < ReturnType < typeof cli . parse > > , entrypoint : string ) {
67+ if ( parsed . help === true || parsed . h === true || parsed . _ . length === 0 ) {
68+ console . log ( renderMainHelpMenu ( entrypoint ) ) ;
69+ process . exit ( 0 ) ;
70+ }
71+ }
72+
73+ export async function runCLI ( entrypoint : string ) {
74+ await cli . parse ( process . argv . slice ( 2 ) , { } , ( rawError , parsed ) => {
75+ if ( rawError ) {
76+ if ( process . env . CLI_DEBUG ) {
77+ console . error ( { type : 'parsed' , error : rawError ?. message , parsed } ) ;
78+ }
79+
80+ const errorMessageSplit = rawError . message . split ( ' ' ) ;
81+
82+ const possibleCommands = [
83+ //
84+ `${ parsed . _ [ 0 ] } ${ parsed . _ [ 1 ] } ` ,
85+ `${ parsed . _ [ 0 ] } ` ,
86+ ] ;
87+
88+ const command = commandRegistry . get ( possibleCommands . find ( ( cmd ) => commandRegistry . has ( cmd ) ) ?? '' ) ;
89+
90+ if ( ! command ) {
91+ error ( {
92+ message : `Command ${ parsed . _ [ 0 ] } not found` ,
93+ } ) ;
94+
95+ return ;
96+ }
97+
98+ const commandFlags = Object . entries ( command . flags ?? { } )
99+ . filter ( ( [ , flag ] ) => typeof flag !== 'string' )
100+ . map ( ( [ flagName , flag ] ) => {
101+ const castedFlag = flag as TaggedFlagBuilder < FlagTag , unknown , unknown , unknown > ;
102+
103+ const flagKey = kebabCaseString ( camelCaseToKebabCase ( flagName ) ) . toLowerCase ( ) ;
104+
105+ return {
106+ flagKey,
107+ char : castedFlag . char ,
108+ aliases : castedFlag . aliases ?. map ( ( alias ) =>
109+ kebabCaseString ( camelCaseToKebabCase ( alias ) ) . toLowerCase ( ) ,
110+ ) ,
111+ matches ( otherFlagKey : string ) {
112+ return (
113+ this . flagKey === otherFlagKey ||
114+ this . char === otherFlagKey ||
115+ this . aliases ?. some ( ( aliasedFlag ) => aliasedFlag === otherFlagKey )
116+ ) ;
117+ } ,
118+ } ;
119+ } ) ;
120+
121+ switch ( errorMessageSplit [ 0 ] ) {
122+ case 'MISSING_ARGUMENT_INPUT' : {
123+ for ( const flag of commandFlags ) {
124+ if ( flag . matches ( errorMessageSplit [ 1 ] ) ) {
125+ error ( {
126+ message : `Flag --${ flag . flagKey } expects a value` ,
127+ } ) ;
128+
129+ return ;
130+ }
131+ }
132+
133+ break ;
134+ }
135+
136+ case 'ARGUMENTS_ARE_MUTUALLY_EXCLUSIVE_INPUT' : {
137+ const args = JSON . parse ( errorMessageSplit [ 1 ] ) as string [ ] ;
138+
139+ error ( {
140+ message : [
141+ `The following errors occurred:` ,
142+ ...args
143+ . sort ( ( a , b ) => a . localeCompare ( b ) )
144+ . map ( ( arg ) => {
145+ const value = parsed [ arg ] ;
146+
147+ const isBoolean = typeof value === 'boolean' ;
148+
149+ const argRepresentation = isBoolean ? `--${ arg } ` : `--${ arg } =${ value } ` ;
150+
151+ return ` ${ chalk . red ( '>' ) } ${ chalk . gray (
152+ `${ argRepresentation } cannot also be provided when using ${ args
153+ . filter ( ( a ) => a !== arg )
154+ . map ( ( a ) => `--${ a } ` )
155+ . join ( ', ' ) } `,
156+ ) } `;
157+ } ) ,
158+ ` ${ chalk . red ( '>' ) } See more help with --help` ,
159+ ] . join ( '\n' ) ,
160+ } ) ;
161+
162+ break ;
163+ }
164+
165+ case 'UNKNOWN_ARGUMENT_INPUT' :
166+ case 'UNKNOWN_ARGUMENTS_INPUT' : {
167+ const nonexistentType = commandFlags . length ? 'flag' : 'subcommand' ;
168+ const nonexistentRepresentation = ( ( ) => {
169+ // Rudimentary as heck, we cannot infer if the flag is provided as `-f` or `-ff` or `--flag`, etc.
170+ if ( nonexistentType === 'flag' ) {
171+ return errorMessageSplit [ 1 ] . length === 1
172+ ? `-${ errorMessageSplit [ 1 ] } `
173+ : `--${ errorMessageSplit [ 1 ] } ` ;
174+ }
175+
176+ return errorMessageSplit [ 1 ] ;
177+ } ) ( ) ;
178+
179+ error ( {
180+ message : [
181+ `Nonexistent ${ nonexistentType } : ${ nonexistentRepresentation } ` ,
182+ ` ${ chalk . red ( '>' ) } See more help with --help` ,
183+ '' ,
184+ selectiveRenderHelpForCommand ( command , {
185+ showUsageString : true ,
186+ showSubcommands : true ,
187+ } ) ,
188+ ] . join ( '\n' ) ,
189+ } ) ;
190+
191+ break ;
192+ }
193+
194+ default : {
195+ console . error ( { type : 'unhandled' , error : rawError . message , parsed } ) ;
196+ }
197+ }
198+ } else {
199+ handleParseResults ( parsed , entrypoint ) ;
200+ }
201+ } ) ;
202+ }
203+
204+ export function handleParseResults ( parsed : Awaited < ReturnType < typeof cli . parse > > , entrypoint : string ) {
205+ if ( parsed . _ . length === 0 ) {
206+ printCLIVersionAndExitIfFlagUsed ( parsed ) ;
207+ printHelpAndExitIfFlagUsed ( parsed , entrypoint ) ;
208+ }
209+
210+ // console.log({ unhandledArgs: parsed });
36211}
37212
38213export const cachedStdinInput = await readStdin ( ) ;
0 commit comments