expensify-common 2.0.187 → 2.0.188

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CLI.d.ts ADDED
@@ -0,0 +1,160 @@
1
+ import type { NonEmptyObject, NonEmptyTuple } from 'type-fest';
2
+ /**
3
+ * A base CLI arg has only a description, which we will use in the help/usage message (built-in to any CLI).
4
+ */
5
+ type CLIArg = {
6
+ description: string;
7
+ };
8
+ /**
9
+ * A boolean arg is characterized only by its presence or absence so has no other fields,
10
+ * but we'll create a type alias to clearly distinguish it from other argument types.
11
+ */
12
+ type BooleanArg = CLIArg;
13
+ /**
14
+ * Any other argument is provided raw in process.argv as a string.
15
+ * It can remain a string, or can be transformed into another type by a custom `parse` function.
16
+ * It can be optional (by providing a default) or required (no default value).
17
+ * It can also supersede other named arguments when provided.
18
+ */
19
+ type StringArg<T = unknown> = CLIArg & {
20
+ default?: T;
21
+ parse?: (val: string) => T;
22
+ supersedes?: string[];
23
+ required?: boolean;
24
+ };
25
+ /**
26
+ * A positional argument is just a string arg, but also must be assigned a name which we will eventually expose the CLI consumer.
27
+ * If `variadic` is true, this must be the last positional arg and it collects all remaining positional args into a string[].
28
+ */
29
+ type PositionalArg<T = unknown> = StringArg<T> & {
30
+ name: string;
31
+ variadic?: true;
32
+ };
33
+ /**
34
+ * This type represents the config for a CLI.
35
+ * The last positional arg can be marked `variadic: true` to collect all remaining positional args into a string[].
36
+ */
37
+ type CLIConfig = NonEmptyObject<{
38
+ /**
39
+ * Record of named flags that are fully characterized by their presence or absence (present=true,absent=false).
40
+ * @example `--verbose`
41
+ */
42
+ flags?: Record<string, BooleanArg>;
43
+ /**
44
+ * Record of named arguments that are represented by a key and a value.
45
+ * @example `--threads=8`
46
+ * @example `--name Rory`
47
+ */
48
+ namedArgs?: Record<string, StringArg>;
49
+ /**
50
+ * Tuple of positional args.
51
+ * @example `myScript.ts arg1 arg2 arg3`
52
+ */
53
+ positionalArgs?: NonEmptyTuple<PositionalArg>;
54
+ }>;
55
+ /**
56
+ * Record of flags to boolean after parsing.
57
+ */
58
+ type ParsedFlags<Flags extends CLIConfig['flags']> = {
59
+ [K in keyof NonNullable<Flags>]: boolean;
60
+ };
61
+ /**
62
+ * Utility type to infer the final value of a string param. Either:
63
+ * - it's a plain string, or
64
+ * - it has a parse function and the final value is inferred from the return type of that function
65
+ */
66
+ type InferStringArgParsedValue<T extends StringArg> = T extends {
67
+ parse: (val: string) => infer R;
68
+ } ? R : string;
69
+ /**
70
+ * Record of named args after parsing.
71
+ */
72
+ type ParsedNamedArgs<NamedArgs extends CLIConfig['namedArgs']> = {
73
+ [K in keyof NonNullable<NamedArgs>]: InferStringArgParsedValue<NonNullable<NamedArgs>[K]>;
74
+ };
75
+ /**
76
+ * Record of positional args after parsing.
77
+ * Variadic args are parsed as string[]; all others use InferStringArgParsedValue.
78
+ */
79
+ type ParsedPositionalArgs<PositionalArgs extends CLIConfig['positionalArgs']> = {
80
+ [K in NonNullable<PositionalArgs>[number] as K['name']]: K extends {
81
+ variadic: true;
82
+ } ? string[] : InferStringArgParsedValue<K>;
83
+ };
84
+ /**
85
+ * Utility to parse command-line arguments to a script.
86
+ *
87
+ * @example
88
+ * ```
89
+ * const cli = new CLI({
90
+ * flags: {
91
+ * verbose: {
92
+ * description: 'Enable verbose logging',
93
+ * },
94
+ * },
95
+ * namedArgs: {
96
+ * time: {
97
+ * description: 'Time of day to greet (morning or evening)',
98
+ * default: 'morning',
99
+ * parse: (val) => {
100
+ * if (val !== 'morning' && val !== 'evening') {
101
+ * throw new Error('Must be "morning" or "evening"');
102
+ * }
103
+ * return val as 'morning' | 'evening';
104
+ * },
105
+ * },
106
+ * },
107
+ * positionalArgs: [
108
+ * {
109
+ * name: 'firstName'
110
+ * description: 'First name to greet',
111
+ * },
112
+ * {
113
+ * name: 'lastName',
114
+ * description: 'Last name to greet',
115
+ * default: '',
116
+ * },
117
+ * ],
118
+ * });
119
+ *
120
+ * let fullName = cli.positionalArgs.firstName;
121
+ * if (cli.flags.verbose) {
122
+ * fullName += cli.positionalArgs.lastName;
123
+ * }
124
+ * console.log(fullName);
125
+ * console.log(cli.namedArgs.time);
126
+ * ```
127
+ */
128
+ /**
129
+ * Built-in flags that are always available on any CLI.
130
+ */
131
+ type BuiltInFlags = {
132
+ yes: boolean;
133
+ no: boolean;
134
+ help: boolean;
135
+ };
136
+ declare class CLI<TConfig extends CLIConfig> {
137
+ private readonly config;
138
+ /**
139
+ * Flags after parsing (includes built-in flags like --yes, --no, and --help).
140
+ */
141
+ readonly flags: ParsedFlags<TConfig['flags']> & BuiltInFlags;
142
+ /**
143
+ * Named args after parsing.
144
+ */
145
+ readonly namedArgs: ParsedNamedArgs<TConfig['namedArgs']>;
146
+ /**
147
+ * Positional args after parsing, collected into a record keyed by the name of each arg.
148
+ */
149
+ readonly positionalArgs: ParsedPositionalArgs<TConfig['positionalArgs']>;
150
+ constructor(config: TConfig);
151
+ private printHelp;
152
+ private static parseStringArg;
153
+ /**
154
+ * Prompts the user for confirmation and returns true if they confirm (y/yes), false otherwise.
155
+ * If --yes flag was passed, returns true immediately without prompting.
156
+ * If --no flag was passed, returns false immediately without prompting.
157
+ */
158
+ promptUserConfirmation(message: string): Promise<boolean>;
159
+ }
160
+ export default CLI;
package/dist/CLI.js ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ /**
49
+ * This file contains a CLI utility class which can be used to declaratively implement a strongly-typed CLI.
50
+ * You provide a CLIConfig defining your arguments, then the class will handle parsing argv, type validation, error handling, and help messages.
51
+ */
52
+ const readline = __importStar(require("readline"));
53
+ const SafeString_js_1 = __importDefault(require("./SafeString.js"));
54
+ class CLI {
55
+ constructor(config) {
56
+ var _a, _b, _c, _d, _e, _f;
57
+ this.config = config;
58
+ const rawArgs = process.argv.slice(2);
59
+ // Initialize all flags to false by default (including built-in flags)
60
+ this.flags = Object.assign(Object.assign({}, Object.fromEntries(Object.keys((_a = config.flags) !== null && _a !== void 0 ? _a : {}).map((key) => [key, false]))), { yes: false, no: false, help: false });
61
+ try {
62
+ const parsedNamedArgs = {};
63
+ const parsedPositionalArgs = {};
64
+ const providedNamedArgs = new Set();
65
+ let positionalIndex = 0;
66
+ for (let i = 0; i < rawArgs.length; i++) {
67
+ const rawArg = rawArgs.at(i);
68
+ if (rawArg === undefined) {
69
+ continue;
70
+ }
71
+ if (rawArg.startsWith('--')) {
72
+ // Either a flag or a named param
73
+ const [rawArgName, rawArgValue] = rawArg.slice(2).split('=');
74
+ if (rawArgName in this.flags) {
75
+ // Arg is a flag
76
+ this.flags[rawArgName] = true;
77
+ }
78
+ else if (config.namedArgs && rawArgName in config.namedArgs) {
79
+ // Arg is a named arg
80
+ providedNamedArgs.add(rawArgName);
81
+ // Grab the value from the split token, otherwise go for the next token
82
+ let argValueBeforeParse = '';
83
+ if (rawArgValue) {
84
+ argValueBeforeParse = rawArgValue;
85
+ }
86
+ else {
87
+ argValueBeforeParse = (_b = rawArgs.at(++i)) !== null && _b !== void 0 ? _b : '';
88
+ if (!argValueBeforeParse || argValueBeforeParse.startsWith('--')) {
89
+ throw new Error(`Missing value for --${rawArgName}`);
90
+ }
91
+ }
92
+ const spec = config.namedArgs[rawArgName];
93
+ parsedNamedArgs[rawArgName] = CLI.parseStringArg(argValueBeforeParse, rawArgName, spec);
94
+ }
95
+ else {
96
+ console.error(`Unknown flag: --${rawArgName}`);
97
+ process.exit(1);
98
+ }
99
+ }
100
+ else {
101
+ // Arg is a positional arg
102
+ const spec = (_c = config.positionalArgs) === null || _c === void 0 ? void 0 : _c.at(positionalIndex);
103
+ if (spec === undefined) {
104
+ throw new Error(`Unexpected arg: ${rawArg}`);
105
+ }
106
+ if (spec.variadic) {
107
+ // Variadic: collect this and all remaining non-flag args into an array
108
+ const collected = [];
109
+ for (let j = i; j < rawArgs.length; j++) {
110
+ const remaining = rawArgs.at(j);
111
+ if (remaining === undefined || remaining.startsWith('--')) {
112
+ break;
113
+ }
114
+ collected.push(remaining);
115
+ }
116
+ parsedPositionalArgs[spec.name] = collected;
117
+ break;
118
+ }
119
+ parsedPositionalArgs[spec.name] = CLI.parseStringArg(rawArg, spec.name, spec);
120
+ positionalIndex++;
121
+ }
122
+ }
123
+ // Handle help command
124
+ if (this.flags.help) {
125
+ this.printHelp();
126
+ process.exit(0);
127
+ }
128
+ // Handle supersession logic
129
+ const supersededArgs = new Set();
130
+ for (const [name, spec] of Object.entries((_d = config.namedArgs) !== null && _d !== void 0 ? _d : {})) {
131
+ if (providedNamedArgs.has(name) && spec.supersedes) {
132
+ for (const supersededArg of spec.supersedes) {
133
+ supersededArgs.add(supersededArg);
134
+ if (providedNamedArgs.has(supersededArg)) {
135
+ console.warn(`⚠️ Warning: --${supersededArg} is superseded by --${name} and will be ignored.`);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ // Validate that all required args are present, assign defaults where values are not parsed
141
+ for (const [name, spec] of Object.entries((_e = config.namedArgs) !== null && _e !== void 0 ? _e : {})) {
142
+ if (name in parsedNamedArgs) {
143
+ if (supersededArgs.has(name)) {
144
+ parsedNamedArgs[name] = undefined;
145
+ }
146
+ }
147
+ else if (supersededArgs.has(name)) {
148
+ // This arg was superseded, so don't require it and don't assign a default
149
+ continue;
150
+ }
151
+ else if (spec.default !== undefined) {
152
+ parsedNamedArgs[name] = spec.default;
153
+ }
154
+ else if (spec.required === false) {
155
+ // Explicitly marked as optional, leave undefined
156
+ continue;
157
+ }
158
+ else {
159
+ // Arguments without defaults are required by default (unless explicitly marked as optional)
160
+ throw new Error(`Missing required named argument --${name}`);
161
+ }
162
+ }
163
+ for (const spec of (_f = config.positionalArgs) !== null && _f !== void 0 ? _f : []) {
164
+ if (!(spec.name in parsedPositionalArgs)) {
165
+ if (spec.default !== undefined) {
166
+ parsedPositionalArgs[spec.name] = spec.default;
167
+ }
168
+ else if (spec.variadic) {
169
+ parsedPositionalArgs[spec.name] = [];
170
+ }
171
+ else {
172
+ throw new Error(`Missing required positional argument --${spec.name}`);
173
+ }
174
+ }
175
+ }
176
+ this.namedArgs = parsedNamedArgs;
177
+ this.positionalArgs = parsedPositionalArgs;
178
+ }
179
+ catch (err) {
180
+ // If help flag was set, the error is from process.exit(0) in tests (where it's mocked to throw) - just rethrow it
181
+ if (this.flags.help) {
182
+ throw err;
183
+ }
184
+ if (err instanceof Error) {
185
+ console.error(err.message);
186
+ this.printHelp();
187
+ }
188
+ else {
189
+ console.error('An unexpected error occurred initializing the CLI.');
190
+ }
191
+ process.exit(1);
192
+ }
193
+ }
194
+ printHelp() {
195
+ var _a;
196
+ const { flags = {}, namedArgs = {}, positionalArgs = [] } = this.config;
197
+ const scriptName = (_a = process.argv.at(1)) !== null && _a !== void 0 ? _a : 'script.ts';
198
+ const positionalUsage = positionalArgs
199
+ .map((arg) => {
200
+ const label = arg.variadic ? `${arg.name}...` : arg.name;
201
+ return arg.default === undefined ? `<${label}>` : `[${label}]`;
202
+ })
203
+ .join(' ');
204
+ const namedArgUsage = Object.keys(namedArgs)
205
+ .map((key) => `[--${key} <value>]`)
206
+ .join(' ');
207
+ const flagUsage = [...Object.keys(flags), '--yes', '--no', '--help'].map((key) => `[${key.startsWith('--') ? key : `--${key}`}]`).join(' ');
208
+ console.log(`\nUsage: npx ts-node ${scriptName} ${flagUsage} ${namedArgUsage} ${positionalUsage}\n`);
209
+ console.log('Flags:');
210
+ for (const [name, spec] of Object.entries(flags)) {
211
+ console.log(` --${name.padEnd(20)} ${spec.description}`);
212
+ }
213
+ // Built-in flags
214
+ console.log(` --${'yes'.padEnd(20)} Automatically answer "yes" to all confirmation prompts.`);
215
+ console.log(` --${'no'.padEnd(20)} Automatically answer "no" to all confirmation prompts.`);
216
+ console.log(` --${'help'.padEnd(20)} Show this help message.`);
217
+ console.log('');
218
+ if (Object.keys(namedArgs).length > 0) {
219
+ console.log('Named Arguments:');
220
+ for (const [name, spec] of Object.entries(namedArgs)) {
221
+ const defaultLabel = spec.default !== undefined ? ` (default: ${(0, SafeString_js_1.default)(spec.default)})` : '';
222
+ const supersededLabel = spec.supersedes && spec.supersedes.length > 0 ? ` (supersedes: ${spec.supersedes.join(', ')})` : '';
223
+ console.log(` --${name.padEnd(20)} ${spec.description}${defaultLabel}${supersededLabel}`);
224
+ }
225
+ console.log('');
226
+ }
227
+ if (positionalArgs.length > 0) {
228
+ console.log('Positional Arguments:');
229
+ for (const arg of positionalArgs) {
230
+ const defaultLabel = arg.default !== undefined ? ` (default: ${(0, SafeString_js_1.default)(arg.default)})` : '';
231
+ console.log(` ${arg.name.padEnd(22)} ${arg.description}${defaultLabel}`);
232
+ }
233
+ console.log('');
234
+ }
235
+ }
236
+ static parseStringArg(rawString, paramName, spec) {
237
+ if ('parse' in spec && !!spec.parse) {
238
+ try {
239
+ return spec.parse(rawString);
240
+ }
241
+ catch (error) {
242
+ let errorMessage = '';
243
+ if (error instanceof Error) {
244
+ errorMessage = error.message;
245
+ }
246
+ console.error(`Invalid value for --${paramName}: ${errorMessage}`);
247
+ process.exit(1);
248
+ }
249
+ }
250
+ else {
251
+ return rawString;
252
+ }
253
+ }
254
+ /**
255
+ * Prompts the user for confirmation and returns true if they confirm (y/yes), false otherwise.
256
+ * If --yes flag was passed, returns true immediately without prompting.
257
+ * If --no flag was passed, returns false immediately without prompting.
258
+ */
259
+ promptUserConfirmation(message) {
260
+ return __awaiter(this, void 0, void 0, function* () {
261
+ // Check for built-in flags first
262
+ if (this.flags.yes) {
263
+ return true;
264
+ }
265
+ if (this.flags.no) {
266
+ return false;
267
+ }
268
+ const rl = readline.createInterface({
269
+ input: process.stdin,
270
+ output: process.stdout,
271
+ });
272
+ return new Promise((resolve) => {
273
+ rl.question(message, (answer) => {
274
+ rl.close();
275
+ const normalizedAnswer = answer.trim().toLowerCase();
276
+ resolve(normalizedAnswer === 'y' || normalizedAnswer === 'yes');
277
+ });
278
+ });
279
+ });
280
+ }
281
+ }
282
+ exports.default = CLI;
@@ -0,0 +1,160 @@
1
+ import type { NonEmptyObject, NonEmptyTuple } from 'type-fest';
2
+ /**
3
+ * A base CLI arg has only a description, which we will use in the help/usage message (built-in to any CLI).
4
+ */
5
+ type CLIArg = {
6
+ description: string;
7
+ };
8
+ /**
9
+ * A boolean arg is characterized only by its presence or absence so has no other fields,
10
+ * but we'll create a type alias to clearly distinguish it from other argument types.
11
+ */
12
+ type BooleanArg = CLIArg;
13
+ /**
14
+ * Any other argument is provided raw in process.argv as a string.
15
+ * It can remain a string, or can be transformed into another type by a custom `parse` function.
16
+ * It can be optional (by providing a default) or required (no default value).
17
+ * It can also supersede other named arguments when provided.
18
+ */
19
+ type StringArg<T = unknown> = CLIArg & {
20
+ default?: T;
21
+ parse?: (val: string) => T;
22
+ supersedes?: string[];
23
+ required?: boolean;
24
+ };
25
+ /**
26
+ * A positional argument is just a string arg, but also must be assigned a name which we will eventually expose the CLI consumer.
27
+ * If `variadic` is true, this must be the last positional arg and it collects all remaining positional args into a string[].
28
+ */
29
+ type PositionalArg<T = unknown> = StringArg<T> & {
30
+ name: string;
31
+ variadic?: true;
32
+ };
33
+ /**
34
+ * This type represents the config for a CLI.
35
+ * The last positional arg can be marked `variadic: true` to collect all remaining positional args into a string[].
36
+ */
37
+ type CLIConfig = NonEmptyObject<{
38
+ /**
39
+ * Record of named flags that are fully characterized by their presence or absence (present=true,absent=false).
40
+ * @example `--verbose`
41
+ */
42
+ flags?: Record<string, BooleanArg>;
43
+ /**
44
+ * Record of named arguments that are represented by a key and a value.
45
+ * @example `--threads=8`
46
+ * @example `--name Rory`
47
+ */
48
+ namedArgs?: Record<string, StringArg>;
49
+ /**
50
+ * Tuple of positional args.
51
+ * @example `myScript.ts arg1 arg2 arg3`
52
+ */
53
+ positionalArgs?: NonEmptyTuple<PositionalArg>;
54
+ }>;
55
+ /**
56
+ * Record of flags to boolean after parsing.
57
+ */
58
+ type ParsedFlags<Flags extends CLIConfig['flags']> = {
59
+ [K in keyof NonNullable<Flags>]: boolean;
60
+ };
61
+ /**
62
+ * Utility type to infer the final value of a string param. Either:
63
+ * - it's a plain string, or
64
+ * - it has a parse function and the final value is inferred from the return type of that function
65
+ */
66
+ type InferStringArgParsedValue<T extends StringArg> = T extends {
67
+ parse: (val: string) => infer R;
68
+ } ? R : string;
69
+ /**
70
+ * Record of named args after parsing.
71
+ */
72
+ type ParsedNamedArgs<NamedArgs extends CLIConfig['namedArgs']> = {
73
+ [K in keyof NonNullable<NamedArgs>]: InferStringArgParsedValue<NonNullable<NamedArgs>[K]>;
74
+ };
75
+ /**
76
+ * Record of positional args after parsing.
77
+ * Variadic args are parsed as string[]; all others use InferStringArgParsedValue.
78
+ */
79
+ type ParsedPositionalArgs<PositionalArgs extends CLIConfig['positionalArgs']> = {
80
+ [K in NonNullable<PositionalArgs>[number] as K['name']]: K extends {
81
+ variadic: true;
82
+ } ? string[] : InferStringArgParsedValue<K>;
83
+ };
84
+ /**
85
+ * Utility to parse command-line arguments to a script.
86
+ *
87
+ * @example
88
+ * ```
89
+ * const cli = new CLI({
90
+ * flags: {
91
+ * verbose: {
92
+ * description: 'Enable verbose logging',
93
+ * },
94
+ * },
95
+ * namedArgs: {
96
+ * time: {
97
+ * description: 'Time of day to greet (morning or evening)',
98
+ * default: 'morning',
99
+ * parse: (val) => {
100
+ * if (val !== 'morning' && val !== 'evening') {
101
+ * throw new Error('Must be "morning" or "evening"');
102
+ * }
103
+ * return val as 'morning' | 'evening';
104
+ * },
105
+ * },
106
+ * },
107
+ * positionalArgs: [
108
+ * {
109
+ * name: 'firstName'
110
+ * description: 'First name to greet',
111
+ * },
112
+ * {
113
+ * name: 'lastName',
114
+ * description: 'Last name to greet',
115
+ * default: '',
116
+ * },
117
+ * ],
118
+ * });
119
+ *
120
+ * let fullName = cli.positionalArgs.firstName;
121
+ * if (cli.flags.verbose) {
122
+ * fullName += cli.positionalArgs.lastName;
123
+ * }
124
+ * console.log(fullName);
125
+ * console.log(cli.namedArgs.time);
126
+ * ```
127
+ */
128
+ /**
129
+ * Built-in flags that are always available on any CLI.
130
+ */
131
+ type BuiltInFlags = {
132
+ yes: boolean;
133
+ no: boolean;
134
+ help: boolean;
135
+ };
136
+ declare class CLI<TConfig extends CLIConfig> {
137
+ private readonly config;
138
+ /**
139
+ * Flags after parsing (includes built-in flags like --yes, --no, and --help).
140
+ */
141
+ readonly flags: ParsedFlags<TConfig['flags']> & BuiltInFlags;
142
+ /**
143
+ * Named args after parsing.
144
+ */
145
+ readonly namedArgs: ParsedNamedArgs<TConfig['namedArgs']>;
146
+ /**
147
+ * Positional args after parsing, collected into a record keyed by the name of each arg.
148
+ */
149
+ readonly positionalArgs: ParsedPositionalArgs<TConfig['positionalArgs']>;
150
+ constructor(config: TConfig);
151
+ private printHelp;
152
+ private static parseStringArg;
153
+ /**
154
+ * Prompts the user for confirmation and returns true if they confirm (y/yes), false otherwise.
155
+ * If --yes flag was passed, returns true immediately without prompting.
156
+ * If --no flag was passed, returns false immediately without prompting.
157
+ */
158
+ promptUserConfirmation(message: string): Promise<boolean>;
159
+ }
160
+ export default CLI;
@@ -3,226 +3,58 @@
3
3
  * You provide a CLIConfig defining your arguments, then the class will handle parsing argv, type validation, error handling, and help messages.
4
4
  */
5
5
  import * as readline from 'readline';
6
- import type {NonEmptyObject, NonEmptyTuple, ValueOf, Writable} from 'type-fest';
7
- import SafeString from './SafeString';
8
-
9
- /**
10
- * A base CLI arg has only a description, which we will use in the help/usage message (built-in to any CLI).
11
- */
12
- type CLIArg = {
13
- description: string;
14
- };
15
-
16
- /**
17
- * A boolean arg is characterized only by its presence or absence so has no other fields,
18
- * but we'll create a type alias to clearly distinguish it from other argument types.
19
- */
20
- type BooleanArg = CLIArg;
21
-
22
- /**
23
- * Any other argument is provided raw in process.argv as a string.
24
- * It can remain a string, or can be transformed into another type by a custom `parse` function.
25
- * It can be optional (by providing a default) or required (no default value).
26
- * It can also supersede other named arguments when provided.
27
- */
28
- type StringArg<T = unknown> = CLIArg & {
29
- default?: T;
30
- parse?: (val: string) => T;
31
- supersedes?: string[];
32
- required?: boolean;
33
- };
34
-
35
- /**
36
- * A positional argument is just a string arg, but also must be assigned a name which we will eventually expose the CLI consumer.
37
- * If `variadic` is true, this must be the last positional arg and it collects all remaining positional args into a string[].
38
- */
39
- type PositionalArg<T = unknown> = StringArg<T> & {
40
- name: string;
41
- variadic?: true;
42
- };
43
-
44
- /**
45
- * This type represents the config for a CLI.
46
- * The last positional arg can be marked `variadic: true` to collect all remaining positional args into a string[].
47
- */
48
- type CLIConfig = NonEmptyObject<{
49
- /**
50
- * Record of named flags that are fully characterized by their presence or absence (present=true,absent=false).
51
- * @example `--verbose`
52
- */
53
- flags?: Record<string, BooleanArg>;
54
-
55
- /**
56
- * Record of named arguments that are represented by a key and a value.
57
- * @example `--threads=8`
58
- * @example `--name Rory`
59
- */
60
- namedArgs?: Record<string, StringArg>;
61
-
62
- /**
63
- * Tuple of positional args.
64
- * @example `myScript.ts arg1 arg2 arg3`
65
- */
66
- positionalArgs?: NonEmptyTuple<PositionalArg>;
67
- }>;
68
-
69
- /**
70
- * Record of flags to boolean after parsing.
71
- */
72
- type ParsedFlags<Flags extends CLIConfig['flags']> = {
73
- [K in keyof NonNullable<Flags>]: boolean;
74
- };
75
-
76
- /**
77
- * Utility type to infer the final value of a string param. Either:
78
- * - it's a plain string, or
79
- * - it has a parse function and the final value is inferred from the return type of that function
80
- */
81
- type InferStringArgParsedValue<T extends StringArg> = T extends {
82
- parse: (val: string) => infer R;
83
- }
84
- ? R
85
- : string;
86
-
87
- /**
88
- * Record of named args after parsing.
89
- */
90
- type ParsedNamedArgs<NamedArgs extends CLIConfig['namedArgs']> = {
91
- [K in keyof NonNullable<NamedArgs>]: InferStringArgParsedValue<NonNullable<NamedArgs>[K]>;
92
- };
93
-
94
- /**
95
- * Record of positional args after parsing.
96
- * Variadic args are parsed as string[]; all others use InferStringArgParsedValue.
97
- */
98
- type ParsedPositionalArgs<PositionalArgs extends CLIConfig['positionalArgs']> = {
99
- [K in NonNullable<PositionalArgs>[number] as K['name']]: K extends {
100
- variadic: true;
101
- }
102
- ? string[]
103
- : InferStringArgParsedValue<K>;
104
- };
105
-
106
- /**
107
- * Utility to parse command-line arguments to a script.
108
- *
109
- * @example
110
- * ```
111
- * const cli = new CLI({
112
- * flags: {
113
- * verbose: {
114
- * description: 'Enable verbose logging',
115
- * },
116
- * },
117
- * namedArgs: {
118
- * time: {
119
- * description: 'Time of day to greet (morning or evening)',
120
- * default: 'morning',
121
- * parse: (val) => {
122
- * if (val !== 'morning' && val !== 'evening') {
123
- * throw new Error('Must be "morning" or "evening"');
124
- * }
125
- * return val as 'morning' | 'evening';
126
- * },
127
- * },
128
- * },
129
- * positionalArgs: [
130
- * {
131
- * name: 'firstName'
132
- * description: 'First name to greet',
133
- * },
134
- * {
135
- * name: 'lastName',
136
- * description: 'Last name to greet',
137
- * default: '',
138
- * },
139
- * ],
140
- * });
141
- *
142
- * let fullName = cli.positionalArgs.firstName;
143
- * if (cli.flags.verbose) {
144
- * fullName += cli.positionalArgs.lastName;
145
- * }
146
- * console.log(fullName);
147
- * console.log(cli.namedArgs.time);
148
- * ```
149
- */
150
- /**
151
- * Built-in flags that are always available on any CLI.
152
- */
153
- type BuiltInFlags = {
154
- yes: boolean;
155
- no: boolean;
156
- help: boolean;
157
- };
158
-
159
- class CLI<TConfig extends CLIConfig> {
160
- /**
161
- * Flags after parsing (includes built-in flags like --yes, --no, and --help).
162
- */
163
- public readonly flags: ParsedFlags<TConfig['flags']> & BuiltInFlags;
164
-
165
- /**
166
- * Named args after parsing.
167
- */
168
- public readonly namedArgs: ParsedNamedArgs<TConfig['namedArgs']>;
169
-
170
- /**
171
- * Positional args after parsing, collected into a record keyed by the name of each arg.
172
- */
173
- public readonly positionalArgs: ParsedPositionalArgs<TConfig['positionalArgs']>;
174
-
175
- constructor(private readonly config: TConfig) {
6
+ import SafeString from './SafeString.js';
7
+ class CLI {
8
+ constructor(config) {
9
+ this.config = config;
176
10
  const rawArgs = process.argv.slice(2);
177
-
178
11
  // Initialize all flags to false by default (including built-in flags)
179
12
  this.flags = {
180
13
  ...Object.fromEntries(Object.keys(config.flags ?? {}).map((key) => [key, false])),
181
14
  yes: false,
182
15
  no: false,
183
16
  help: false,
184
- } as typeof this.flags;
185
-
17
+ };
186
18
  try {
187
- const parsedNamedArgs: Partial<Writable<typeof this.namedArgs>> = {};
188
- const parsedPositionalArgs: Partial<Writable<typeof this.positionalArgs>> = {};
189
- const providedNamedArgs = new Set<string>();
190
-
19
+ const parsedNamedArgs = {};
20
+ const parsedPositionalArgs = {};
21
+ const providedNamedArgs = new Set();
191
22
  let positionalIndex = 0;
192
23
  for (let i = 0; i < rawArgs.length; i++) {
193
24
  const rawArg = rawArgs.at(i);
194
25
  if (rawArg === undefined) {
195
26
  continue;
196
27
  }
197
-
198
28
  if (rawArg.startsWith('--')) {
199
29
  // Either a flag or a named param
200
30
  const [rawArgName, rawArgValue] = rawArg.slice(2).split('=');
201
31
  if (rawArgName in this.flags) {
202
32
  // Arg is a flag
203
- (this.flags as Record<string, boolean>)[rawArgName] = true;
204
- } else if (config.namedArgs && rawArgName in config.namedArgs) {
33
+ this.flags[rawArgName] = true;
34
+ }
35
+ else if (config.namedArgs && rawArgName in config.namedArgs) {
205
36
  // Arg is a named arg
206
37
  providedNamedArgs.add(rawArgName);
207
-
208
38
  // Grab the value from the split token, otherwise go for the next token
209
39
  let argValueBeforeParse = '';
210
40
  if (rawArgValue) {
211
41
  argValueBeforeParse = rawArgValue;
212
- } else {
42
+ }
43
+ else {
213
44
  argValueBeforeParse = rawArgs.at(++i) ?? '';
214
45
  if (!argValueBeforeParse || argValueBeforeParse.startsWith('--')) {
215
46
  throw new Error(`Missing value for --${rawArgName}`);
216
47
  }
217
48
  }
218
-
219
49
  const spec = config.namedArgs[rawArgName];
220
- parsedNamedArgs[rawArgName as keyof typeof parsedNamedArgs] = CLI.parseStringArg(argValueBeforeParse, rawArgName, spec) as ValueOf<typeof parsedNamedArgs>;
221
- } else {
50
+ parsedNamedArgs[rawArgName] = CLI.parseStringArg(argValueBeforeParse, rawArgName, spec);
51
+ }
52
+ else {
222
53
  console.error(`Unknown flag: --${rawArgName}`);
223
54
  process.exit(1);
224
55
  }
225
- } else {
56
+ }
57
+ else {
226
58
  // Arg is a positional arg
227
59
  const spec = config.positionalArgs?.at(positionalIndex);
228
60
  if (spec === undefined) {
@@ -230,7 +62,7 @@ class CLI<TConfig extends CLIConfig> {
230
62
  }
231
63
  if (spec.variadic) {
232
64
  // Variadic: collect this and all remaining non-flag args into an array
233
- const collected: string[] = [];
65
+ const collected = [];
234
66
  for (let j = i; j < rawArgs.length; j++) {
235
67
  const remaining = rawArgs.at(j);
236
68
  if (remaining === undefined || remaining.startsWith('--')) {
@@ -238,22 +70,20 @@ class CLI<TConfig extends CLIConfig> {
238
70
  }
239
71
  collected.push(remaining);
240
72
  }
241
- parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = collected as ValueOf<typeof parsedPositionalArgs>;
73
+ parsedPositionalArgs[spec.name] = collected;
242
74
  break;
243
75
  }
244
- parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = CLI.parseStringArg(rawArg, spec.name, spec) as ValueOf<typeof parsedPositionalArgs>;
76
+ parsedPositionalArgs[spec.name] = CLI.parseStringArg(rawArg, spec.name, spec);
245
77
  positionalIndex++;
246
78
  }
247
79
  }
248
-
249
80
  // Handle help command
250
81
  if (this.flags.help) {
251
82
  this.printHelp();
252
83
  process.exit(0);
253
84
  }
254
-
255
85
  // Handle supersession logic
256
- const supersededArgs = new Set<string>();
86
+ const supersededArgs = new Set();
257
87
  for (const [name, spec] of Object.entries(config.namedArgs ?? {})) {
258
88
  if (providedNamedArgs.has(name) && spec.supersedes) {
259
89
  for (const supersededArg of spec.supersedes) {
@@ -264,42 +94,46 @@ class CLI<TConfig extends CLIConfig> {
264
94
  }
265
95
  }
266
96
  }
267
-
268
97
  // Validate that all required args are present, assign defaults where values are not parsed
269
98
  for (const [name, spec] of Object.entries(config.namedArgs ?? {})) {
270
99
  if (name in parsedNamedArgs) {
271
100
  if (supersededArgs.has(name)) {
272
- parsedNamedArgs[name as keyof typeof parsedNamedArgs] = undefined as ValueOf<typeof parsedNamedArgs>;
101
+ parsedNamedArgs[name] = undefined;
273
102
  }
274
- } else if (supersededArgs.has(name)) {
103
+ }
104
+ else if (supersededArgs.has(name)) {
275
105
  // This arg was superseded, so don't require it and don't assign a default
276
106
  continue;
277
- } else if (spec.default !== undefined) {
278
- parsedNamedArgs[name as keyof typeof parsedNamedArgs] = spec.default as ValueOf<typeof parsedNamedArgs>;
279
- } else if (spec.required === false) {
107
+ }
108
+ else if (spec.default !== undefined) {
109
+ parsedNamedArgs[name] = spec.default;
110
+ }
111
+ else if (spec.required === false) {
280
112
  // Explicitly marked as optional, leave undefined
281
113
  continue;
282
- } else {
114
+ }
115
+ else {
283
116
  // Arguments without defaults are required by default (unless explicitly marked as optional)
284
117
  throw new Error(`Missing required named argument --${name}`);
285
118
  }
286
119
  }
287
-
288
120
  for (const spec of config.positionalArgs ?? []) {
289
121
  if (!(spec.name in parsedPositionalArgs)) {
290
122
  if (spec.default !== undefined) {
291
- parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = spec.default as ValueOf<typeof parsedPositionalArgs>;
292
- } else if (spec.variadic) {
293
- parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = [] as ValueOf<typeof parsedPositionalArgs>;
294
- } else {
123
+ parsedPositionalArgs[spec.name] = spec.default;
124
+ }
125
+ else if (spec.variadic) {
126
+ parsedPositionalArgs[spec.name] = [];
127
+ }
128
+ else {
295
129
  throw new Error(`Missing required positional argument --${spec.name}`);
296
130
  }
297
131
  }
298
132
  }
299
-
300
- this.namedArgs = parsedNamedArgs as typeof this.namedArgs;
301
- this.positionalArgs = parsedPositionalArgs as unknown as typeof this.positionalArgs;
302
- } catch (err) {
133
+ this.namedArgs = parsedNamedArgs;
134
+ this.positionalArgs = parsedPositionalArgs;
135
+ }
136
+ catch (err) {
303
137
  // If help flag was set, the error is from process.exit(0) in tests (where it's mocked to throw) - just rethrow it
304
138
  if (this.flags.help) {
305
139
  throw err;
@@ -307,29 +141,27 @@ class CLI<TConfig extends CLIConfig> {
307
141
  if (err instanceof Error) {
308
142
  console.error(err.message);
309
143
  this.printHelp();
310
- } else {
144
+ }
145
+ else {
311
146
  console.error('An unexpected error occurred initializing the CLI.');
312
147
  }
313
148
  process.exit(1);
314
149
  }
315
150
  }
316
-
317
- private printHelp(): void {
318
- const {flags = {}, namedArgs = {}, positionalArgs = []} = this.config;
151
+ printHelp() {
152
+ const { flags = {}, namedArgs = {}, positionalArgs = [] } = this.config;
319
153
  const scriptName = process.argv.at(1) ?? 'script.ts';
320
154
  const positionalUsage = positionalArgs
321
155
  .map((arg) => {
322
- const label = arg.variadic ? `${arg.name}...` : arg.name;
323
- return arg.default === undefined ? `<${label}>` : `[${label}]`;
324
- })
156
+ const label = arg.variadic ? `${arg.name}...` : arg.name;
157
+ return arg.default === undefined ? `<${label}>` : `[${label}]`;
158
+ })
325
159
  .join(' ');
326
160
  const namedArgUsage = Object.keys(namedArgs)
327
161
  .map((key) => `[--${key} <value>]`)
328
162
  .join(' ');
329
163
  const flagUsage = [...Object.keys(flags), '--yes', '--no', '--help'].map((key) => `[${key.startsWith('--') ? key : `--${key}`}]`).join(' ');
330
-
331
164
  console.log(`\nUsage: npx ts-node ${scriptName} ${flagUsage} ${namedArgUsage} ${positionalUsage}\n`);
332
-
333
165
  console.log('Flags:');
334
166
  for (const [name, spec] of Object.entries(flags)) {
335
167
  console.log(` --${name.padEnd(20)} ${spec.description}`);
@@ -339,7 +171,6 @@ class CLI<TConfig extends CLIConfig> {
339
171
  console.log(` --${'no'.padEnd(20)} Automatically answer "no" to all confirmation prompts.`);
340
172
  console.log(` --${'help'.padEnd(20)} Show this help message.`);
341
173
  console.log('');
342
-
343
174
  if (Object.keys(namedArgs).length > 0) {
344
175
  console.log('Named Arguments:');
345
176
  for (const [name, spec] of Object.entries(namedArgs)) {
@@ -349,7 +180,6 @@ class CLI<TConfig extends CLIConfig> {
349
180
  }
350
181
  console.log('');
351
182
  }
352
-
353
183
  if (positionalArgs.length > 0) {
354
184
  console.log('Positional Arguments:');
355
185
  for (const arg of positionalArgs) {
@@ -359,12 +189,12 @@ class CLI<TConfig extends CLIConfig> {
359
189
  console.log('');
360
190
  }
361
191
  }
362
-
363
- private static parseStringArg<T extends StringArg>(rawString: string, paramName: string, spec: T): InferStringArgParsedValue<T> {
192
+ static parseStringArg(rawString, paramName, spec) {
364
193
  if ('parse' in spec && !!spec.parse) {
365
194
  try {
366
- return spec.parse(rawString) as InferStringArgParsedValue<T>;
367
- } catch (error) {
195
+ return spec.parse(rawString);
196
+ }
197
+ catch (error) {
368
198
  let errorMessage = '';
369
199
  if (error instanceof Error) {
370
200
  errorMessage = error.message;
@@ -372,17 +202,17 @@ class CLI<TConfig extends CLIConfig> {
372
202
  console.error(`Invalid value for --${paramName}: ${errorMessage}`);
373
203
  process.exit(1);
374
204
  }
375
- } else {
376
- return rawString as InferStringArgParsedValue<T>;
205
+ }
206
+ else {
207
+ return rawString;
377
208
  }
378
209
  }
379
-
380
210
  /**
381
211
  * Prompts the user for confirmation and returns true if they confirm (y/yes), false otherwise.
382
212
  * If --yes flag was passed, returns true immediately without prompting.
383
213
  * If --no flag was passed, returns false immediately without prompting.
384
214
  */
385
- async promptUserConfirmation(message: string): Promise<boolean> {
215
+ async promptUserConfirmation(message) {
386
216
  // Check for built-in flags first
387
217
  if (this.flags.yes) {
388
218
  return true;
@@ -390,12 +220,10 @@ class CLI<TConfig extends CLIConfig> {
390
220
  if (this.flags.no) {
391
221
  return false;
392
222
  }
393
-
394
223
  const rl = readline.createInterface({
395
224
  input: process.stdin,
396
225
  output: process.stdout,
397
226
  });
398
-
399
227
  return new Promise((resolve) => {
400
228
  rl.question(message, (answer) => {
401
229
  rl.close();
@@ -405,5 +233,4 @@ class CLI<TConfig extends CLIConfig> {
405
233
  });
406
234
  }
407
235
  }
408
-
409
236
  export default CLI;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SafeString is a utility function that converts a value to a string.
3
+ * It handles the problematic case of plain objects by converting them to JSON.
4
+ * It helps with eslint rule https://typescript-eslint.io/rules/no-base-to-string
5
+ * @param value - The value to convert to a string.
6
+ * @returns The string representation of the value.
7
+ */
8
+ export default function SafeString(value: unknown): string;
@@ -5,50 +5,45 @@
5
5
  * @param value - The value to convert to a string.
6
6
  * @returns The string representation of the value.
7
7
  */
8
- export default function SafeString(value: unknown): string {
8
+ export default function SafeString(value) {
9
9
  if (value === undefined || value === null) {
10
10
  return '';
11
11
  }
12
-
13
12
  // Handle primitives explicitly so the final fallback never receives an object.
14
13
  const valueType = typeof value;
15
14
  if (valueType === 'string') {
16
- return value as string;
15
+ return value;
17
16
  }
18
17
  if (valueType === 'number' || valueType === 'boolean' || valueType === 'function' || valueType === 'bigint' || valueType === 'symbol') {
19
- const primitive = value as number | boolean | Function | bigint | symbol;
18
+ const primitive = value;
20
19
  return String(primitive);
21
20
  }
22
-
23
21
  if (valueType === 'object') {
24
22
  if (Array.isArray(value)) {
25
23
  try {
26
24
  return JSON.stringify(value);
27
- } catch {
25
+ }
26
+ catch {
28
27
  return '[object Array]';
29
28
  }
30
29
  }
31
-
32
- const obj = value as {toString: () => string};
30
+ const obj = value;
33
31
  const hasCustomToString = obj.toString && obj.toString !== Object.prototype.toString;
34
32
  if (hasCustomToString) {
35
33
  return obj.toString();
36
34
  }
37
-
38
35
  if (value instanceof Map) {
39
36
  return '[object Map]';
40
37
  }
41
-
42
38
  if (value instanceof Set) {
43
39
  return '[object Set]';
44
40
  }
45
-
46
41
  try {
47
42
  return JSON.stringify(obj);
48
- } catch {
43
+ }
44
+ catch {
49
45
  return '[object Object]';
50
46
  }
51
47
  }
52
-
53
48
  return '';
54
49
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expensify-common",
3
- "version": "2.0.187",
3
+ "version": "2.0.188",
4
4
  "author": "Expensify, Inc.",
5
5
  "description": "Expensify libraries and components shared across different repos",
6
6
  "homepage": "https://expensify.com",
@@ -14,14 +14,14 @@
14
14
  "default": "./dist/index.js"
15
15
  },
16
16
  "./CLI": {
17
- "types": "./lib/CLI.ts",
18
- "default": "./lib/CLI.ts"
17
+ "types": "./dist/CLI.d.ts",
18
+ "import": "./dist/esm/CLI.js",
19
+ "require": "./dist/CLI.js",
20
+ "default": "./dist/esm/CLI.js"
19
21
  }
20
22
  },
21
23
  "files": [
22
24
  "dist/**/*",
23
- "lib/CLI.ts",
24
- "lib/SafeString.ts",
25
25
  "API.md",
26
26
  "README.md",
27
27
  "LICENSE.md"
@@ -29,7 +29,7 @@
29
29
  "scripts": {
30
30
  "grunt": "grunt",
31
31
  "typecheck": "tsc --noEmit",
32
- "build": "tsc -p tsconfig.build.json && cp ./lib/*.d.ts ./dist",
32
+ "build": "tsc -p tsconfig.build.json && cp ./lib/*.d.ts ./dist && tsc -p tsconfig.cli.cjs.json && tsc -p tsconfig.cli.esm.json && cp ./lib/esm-package.json ./dist/esm/package.json",
33
33
  "test": "jest",
34
34
  "lint": "eslint lib/ __tests__/",
35
35
  "prettier": "prettier --write lib/ __tests__/",
@@ -101,6 +101,9 @@
101
101
  ]
102
102
  },
103
103
  "jest": {
104
- "testEnvironment": "jsdom"
104
+ "testEnvironment": "jsdom",
105
+ "moduleNameMapper": {
106
+ "^(\\.{1,2}/.*)\\.js$": "$1"
107
+ }
105
108
  }
106
109
  }