Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 

338 satır
8.5 KiB

  1. import yargs, { Argv } from 'yargs';
  2. import * as clack from '@clack/prompts';
  3. import { DummyWriteStream } from './write-stream';
  4. import pino, {LogFn} from 'pino';
  5. import * as util from 'util';
  6. export interface Logger extends pino.BaseLogger {}
  7. export interface CommandHandlerArgs {
  8. self: any;
  9. interactive: boolean;
  10. send: (message: number) => void;
  11. logger: Logger;
  12. args: any;
  13. }
  14. export type CommandHandler = (args: CommandHandlerArgs) => void | number | Promise<number> | Promise<void>;
  15. export interface CommandArgs {
  16. aliases?: string[];
  17. command: string;
  18. parameters: string[];
  19. describe: string;
  20. handler: CommandHandler;
  21. interactiveMode?: boolean;
  22. }
  23. type PromptValueArgs = [Record<string, string>, ...never[]]
  24. | [string, string]
  25. | [[string, string], ...never[]]
  26. | [[string, string][], ...never[]];
  27. export interface TestModeResult {
  28. exitCode?: number;
  29. stdout: DummyWriteStream;
  30. stderr: DummyWriteStream;
  31. }
  32. interface InteractiveModeOptions {
  33. option: string;
  34. alias: string;
  35. intro: string;
  36. cancelled: string;
  37. outro: string;
  38. describe: string;
  39. }
  40. export interface CliOptions {
  41. name: string;
  42. interactiveMode?: Partial<InteractiveModeOptions>;
  43. logger?: Logger | boolean;
  44. }
  45. export class Cli {
  46. private testMode = false;
  47. private promptValues = {} as Record<string, unknown>;
  48. private testModeResult: TestModeResult = {
  49. exitCode: undefined as number | undefined,
  50. stdout: new DummyWriteStream(),
  51. stderr: new DummyWriteStream(),
  52. };
  53. private readonly cli: Argv;
  54. constructor(private readonly options: CliOptions) {
  55. this.cli = yargs()
  56. .scriptName(options.name)
  57. .help()
  58. .fail(false)
  59. .strict(false);
  60. }
  61. private exit(code: number) {
  62. if (this.testMode) {
  63. this.testModeResult.exitCode = code;
  64. return;
  65. }
  66. process.exit(code);
  67. }
  68. private async prompt(messages: Record<string, string>, interactiveModeOptions: InteractiveModeOptions) {
  69. const {
  70. intro,
  71. cancelled,
  72. outro,
  73. } = interactiveModeOptions;
  74. const messagesEntries = Object.entries(messages);
  75. if (messagesEntries.length > 0) {
  76. clack.intro(intro);
  77. const values = await clack.group(
  78. Object.fromEntries(
  79. messagesEntries.map(([key, value]) => [key, () => clack.text({ message: value })])
  80. ),
  81. {
  82. onCancel: () => {
  83. clack.cancel(cancelled);
  84. this.exit(1);
  85. }
  86. }
  87. );
  88. clack.outro(outro);
  89. return values;
  90. }
  91. return {};
  92. }
  93. private async generateCommandArgs(
  94. context: any,
  95. originalCommandArgs: Record<string, unknown>,
  96. interactive: boolean,
  97. interactiveModeOptions: InteractiveModeOptions,
  98. ) {
  99. if (!interactive) {
  100. return originalCommandArgs;
  101. }
  102. // TODO properly filter attributes
  103. const clackGroup = context.optional
  104. .filter((optional: { cmd: string[] }) => {
  105. const {
  106. '$0': _$0,
  107. i: _i,
  108. interactive: _interactive,
  109. test: _test,
  110. _: __,
  111. ...rest
  112. } = originalCommandArgs;
  113. return !Object.keys(rest).includes(optional.cmd[0]);
  114. })
  115. .reduce(
  116. (group: Record<string, string>, optional: { cmd: string[], describe: string }) => ({
  117. ...group,
  118. [optional.cmd[0]]: optional.describe ?? optional.cmd[0],
  119. }),
  120. {} as Record<string, string>,
  121. );
  122. const promptedArgs = this.testMode
  123. ? this.promptValues
  124. : await this.prompt(clackGroup, interactiveModeOptions);
  125. return {
  126. ...originalCommandArgs,
  127. ...promptedArgs,
  128. };
  129. }
  130. private buildHandler(handlerFn: Function, interactiveModeOptions: InteractiveModeOptions) {
  131. const thisCli = this;
  132. return async function handler(this: any, commandArgs: Record<string, unknown>) {
  133. const self = this;
  134. const { option, alias } = interactiveModeOptions;
  135. const interactiveLong = option in commandArgs ? Boolean(commandArgs.interactive) : false;
  136. const interactiveShort = alias in commandArgs ? Boolean(commandArgs.i) : false;
  137. const interactive = interactiveLong || interactiveShort;
  138. const buildArgs = await thisCli.generateCommandArgs(
  139. self,
  140. commandArgs,
  141. interactive,
  142. interactiveModeOptions,
  143. );
  144. const stdoutLogFn: LogFn = (...args: unknown[]) => {
  145. const stream = thisCli.testMode ? thisCli.testModeResult.stdout : process.stdout;
  146. const [arg0, arg1, ...etcArgs] = args;
  147. if (typeof arg0 === 'string' || typeof arg0 === 'number') {
  148. if (arg1) {
  149. if (etcArgs.length > 0) {
  150. stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs));
  151. } else {
  152. stream.write(util.format(`${arg0}\n`, arg1));
  153. }
  154. } else {
  155. stream.write(util.format(`${arg0}\n`));
  156. }
  157. } else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) {
  158. if (etcArgs.length > 0) {
  159. stream.write(util.format(`${arg1}\n`, ...etcArgs));
  160. } else {
  161. stream.write(util.format(`${arg1}\n`));
  162. }
  163. }
  164. };
  165. const stderrLogFn: LogFn = (...args: unknown[]) => {
  166. const stream = thisCli.testMode ? thisCli.testModeResult.stderr : process.stderr;
  167. const [arg0, arg1, ...etcArgs] = args;
  168. if (typeof arg0 === 'string' || typeof arg0 === 'number') {
  169. if (arg1) {
  170. if (etcArgs.length > 0) {
  171. stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs));
  172. } else {
  173. stream.write(util.format(`${arg0}\n`, arg1));
  174. }
  175. } else {
  176. stream.write(util.format(`${arg0}\n`));
  177. }
  178. } else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) {
  179. if (etcArgs.length > 0) {
  180. stream.write(util.format(`${arg1}\n`, ...etcArgs));
  181. } else {
  182. stream.write(util.format(`${arg1}\n`));
  183. }
  184. }
  185. };
  186. const loggerFn = thisCli.options.logger;
  187. const defaultLogger = {
  188. level: 'info',
  189. enabled: typeof loggerFn === 'boolean' ? loggerFn : true,
  190. debug: stdoutLogFn,
  191. info: stdoutLogFn,
  192. warn: stdoutLogFn,
  193. error: stderrLogFn,
  194. fatal: stderrLogFn,
  195. trace: stdoutLogFn,
  196. silent: () => {},
  197. } as Logger;
  198. const loggerBooleanFn = typeof loggerFn === 'boolean' && loggerFn ? defaultLogger : {
  199. level: 'silent',
  200. debug: () => {},
  201. info: () => {},
  202. warn: () => {},
  203. error: () => {},
  204. fatal: () => {},
  205. trace: () => {},
  206. silent: () => {},
  207. } as Logger;
  208. const logger = typeof loggerFn === 'undefined' ? defaultLogger : (typeof loggerFn === 'function' ? loggerFn : loggerBooleanFn);
  209. let exited = false;
  210. const returnCode = await handlerFn({
  211. self,
  212. interactive,
  213. logger,
  214. send: (code: number) => {
  215. exited = true;
  216. thisCli.exit(code);
  217. },
  218. args: buildArgs,
  219. });
  220. if (!exited) {
  221. thisCli.exit(returnCode ? returnCode : 0);
  222. }
  223. };
  224. }
  225. command({ parameters, command, interactiveMode = true, handler, ...args }: CommandArgs): Cli {
  226. // TODO type-safe declaration of positional/optional arguments
  227. const {
  228. option = 'interactive',
  229. alias = 'i',
  230. describe = 'Interactive mode',
  231. intro = 'Please provide the following values:',
  232. cancelled = 'Operation cancelled.',
  233. outro = 'Thank you!',
  234. } = this.options?.interactiveMode ?? {};
  235. const commandArgs = {
  236. ...args,
  237. command: `${command} ${parameters.join(' ').replace(/</g, '[').replace(/>/g, ']')}`,
  238. usage: parameters.map((p) => `${command} ${p}`).join('\n'),
  239. handler: this.buildHandler(handler, {
  240. option,
  241. alias,
  242. intro,
  243. cancelled,
  244. outro,
  245. describe,
  246. }),
  247. } as Record<string, unknown>
  248. if (interactiveMode) {
  249. commandArgs.options = {
  250. ...(commandArgs.options ?? {}),
  251. [option]: {
  252. alias,
  253. describe,
  254. type: 'boolean',
  255. default: false,
  256. hidden: true,
  257. },
  258. };
  259. }
  260. this.cli.command(commandArgs as unknown as Parameters<typeof this.cli.command>[0]);
  261. return this;
  262. }
  263. async run(args: string[]): Promise<void> {
  264. if (args.length > 0) {
  265. await this.cli.parse(args);
  266. return;
  267. }
  268. this.cli.showHelp();
  269. }
  270. test() {
  271. this.testMode = true;
  272. this.promptValues = {};
  273. const thisCli = this;
  274. return {
  275. promptValue(...args: PromptValueArgs) {
  276. const [testArg] = args;
  277. if (typeof testArg === 'string') {
  278. const [key, value] = args as [string, unknown];
  279. thisCli.promptValues[key] = thisCli.promptValues[key] ?? value;
  280. }
  281. if (Array.isArray(testArg)) {
  282. const [key, value] = testArg as [string, unknown];
  283. thisCli.promptValues[key] = thisCli.promptValues[key] ?? value;
  284. }
  285. if (typeof testArg === 'object' && testArg !== null) {
  286. Object.entries(testArg).forEach(([key, value]) => {
  287. thisCli.promptValues[key] = thisCli.promptValues[key] ?? value;
  288. });
  289. }
  290. return this;
  291. },
  292. async run(args: string[]): Promise<TestModeResult> {
  293. await thisCli.cli.parse(args);
  294. return thisCli.testModeResult;
  295. },
  296. };
  297. }
  298. }