console-app/lib/InteractiveOptions.ts

250 lines
6.1 KiB
TypeScript
Raw Normal View History

2020-05-07 12:19:19 +02:00
/* eslint-disable no-process-exit */
/* eslint-disable no-console */
2020-05-05 13:32:40 +02:00
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of console-app which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, May 2020
*/
2020-05-05 13:19:17 +02:00
/* eslint-disable max-lines-per-function */
2020-05-05 12:48:36 +02:00
/* eslint-disable complexity */
/* eslint-disable max-statements */
/* eslint-disable no-process-env */
2020-05-06 08:34:21 +02:00
import { Persistent } from '@sapphirecode/modelling';
2020-05-05 12:48:36 +02:00
import fs from 'fs-extra';
2020-05-05 13:28:01 +02:00
import yargs, { Options } from 'yargs';
2020-05-05 13:19:17 +02:00
import { Confirm, Input } from 'enquirer';
2020-05-05 11:56:36 +02:00
2020-05-05 15:15:24 +02:00
type OptionType =
'string'
| 'number'
| 'boolean'
| 'file'
| 'folder'
| 'path';
2020-05-05 11:56:36 +02:00
interface Option {
name: string;
type: OptionType;
required?: boolean;
2020-05-05 12:48:36 +02:00
default?: unknown;
alias?: string;
env?: string;
2020-05-05 13:19:17 +02:00
description?: string;
message?: string;
2020-05-05 12:17:48 +02:00
}
interface OptionProcess extends Option {
filled: boolean;
2020-05-05 12:48:36 +02:00
value?: unknown;
2020-05-05 11:56:36 +02:00
}
2020-05-07 12:19:19 +02:00
function get_string_type (type: OptionType): 'string'|'number'|'boolean' {
if ([
'string',
'number',
'boolean'
].includes (type))
return type as ('string'|'number'|'boolean');
if ([
'file',
'folder',
'path'
].includes (type))
return 'string';
throw new Error (`unknown option type ${type}`);
}
2020-05-05 11:56:36 +02:00
export class InteractiveOptions extends Persistent {
2020-05-05 12:17:48 +02:00
protected options: Array<OptionProcess>;
2020-05-07 12:19:19 +02:00
protected quiet = false;
2020-05-05 12:17:48 +02:00
2020-05-05 11:56:36 +02:00
public constructor (options: Array<Option>) {
super ();
2020-05-05 12:17:48 +02:00
this.options = options
.map ((v) => ({ filled: false, ...v } as OptionProcess));
for (const option of this.options) {
2020-05-07 12:19:19 +02:00
if (
typeof option.default !== 'undefined'
&& typeof option.default !== get_string_type (option.type)
) {
throw new Error (
`default does not match option type on ${option.name}`
);
}
2020-05-05 11:56:36 +02:00
this.properties[option.name] = option.type;
}
}
2020-05-05 12:17:48 +02:00
2020-05-07 13:16:41 +02:00
public async parse (): Promise<Record<string, unknown>> {
2020-05-05 13:19:17 +02:00
await this.get_env_options ();
await this.get_args_options ();
2020-05-05 12:17:48 +02:00
await this.get_interactive_options ();
2020-05-07 13:16:41 +02:00
return this.to_object ();
2020-05-05 12:17:48 +02:00
}
2020-05-05 12:48:36 +02:00
private async assign_arg (opt: OptionProcess, value: unknown): Promise<void> {
2020-05-05 15:15:24 +02:00
if (opt.type === 'string') {
2020-05-05 13:19:17 +02:00
opt.value = String (value);
2020-05-05 12:48:36 +02:00
opt.filled = true;
return;
}
2020-05-05 15:15:24 +02:00
if (opt.type === 'number') {
2020-05-05 13:19:17 +02:00
if (![
'string',
'number'
].includes (typeof value))
return;
2020-05-05 13:28:01 +02:00
const as_num = parseInt (String (value));
2020-05-05 12:48:36 +02:00
const is_num = !isNaN (as_num);
if (is_num) {
opt.value = as_num;
opt.filled = true;
}
return;
}
2020-05-05 15:15:24 +02:00
if (opt.type === 'boolean') {
2020-05-05 13:19:17 +02:00
if (![
'string',
'boolean',
'number'
].includes (typeof value))
return;
const is_bool = [
0,
1
2020-05-05 13:28:01 +02:00
].includes (parseInt (String (value)))
|| (/^(?:true|false)$/ui).test (value as string);
2020-05-05 13:19:17 +02:00
if (is_bool) {
const as_bool = value === 1 || (/true/ui).test (value as string);
opt.value = as_bool;
2020-05-05 12:48:36 +02:00
opt.filled = true;
}
return;
}
if (
2020-05-05 15:15:24 +02:00
opt.type === 'path'
|| opt.type === 'file'
|| opt.type === 'folder'
2020-05-05 12:48:36 +02:00
) {
2020-05-05 13:19:17 +02:00
if (typeof value !== 'string' || !await fs.pathExists (value))
2020-05-05 12:48:36 +02:00
return;
2020-05-05 15:15:24 +02:00
if (opt.type === 'path') {
2020-05-05 12:48:36 +02:00
opt.value = value;
opt.filled = true;
return;
}
const stat = await fs.stat (value);
2020-05-05 15:15:24 +02:00
if (stat.isDirectory () === (opt.type === 'folder')) {
2020-05-05 12:48:36 +02:00
opt.value = value;
opt.filled = true;
}
}
}
2020-05-05 12:17:48 +02:00
2020-05-05 13:19:17 +02:00
private async get_env_options (): Promise<void> {
2020-05-05 12:48:36 +02:00
await Promise.all (this.options.map ((opt) => {
if (
typeof opt.env !== 'undefined'
&& typeof process.env[opt.env] !== 'undefined'
)
return this.assign_arg (opt, process.env[opt.env]);
return Promise.resolve ();
}));
2020-05-05 12:17:48 +02:00
}
2020-05-05 13:19:17 +02:00
private async get_args_options (): Promise<void> {
2020-05-05 13:28:01 +02:00
const yargs_config: Record<string, Options> = {
2020-05-05 13:19:17 +02:00
quiet: {
alias: 'q',
default: false,
type: 'boolean',
describe: 'do not ask for options interactively'
2020-05-07 12:19:19 +02:00
},
help: {
alias: 'h',
default: false,
type: 'boolean',
describe: ''
2020-05-05 13:19:17 +02:00
}
};
for (const opt of this.options) {
yargs_config[opt.name] = {
alias: opt.alias,
default: opt.default,
2020-05-07 12:19:19 +02:00
type: get_string_type (opt.type),
2020-05-05 13:19:17 +02:00
describe: opt.description
};
}
2020-05-05 13:28:01 +02:00
const argv = yargs.options (yargs_config)
.parse ();
2020-05-07 12:19:19 +02:00
if (argv.help) {
yargs.options (yargs_config)
.showHelp ();
process.exit (0);
}
this.quiet = argv.quiet as boolean;
2020-05-05 13:19:17 +02:00
await Promise.all (this.options.map ((opt) => {
if (typeof argv[opt.name] !== 'undefined')
return this.assign_arg (opt, argv[opt.name]);
return Promise.resolve ();
}));
}
2020-05-05 12:17:48 +02:00
2020-05-05 13:19:17 +02:00
private async prompt (opt: OptionProcess): Promise<void> {
if (opt.filled)
return;
if (
2020-05-05 15:15:24 +02:00
opt.type === 'string'
|| opt.type === 'file'
|| opt.type === 'folder'
|| opt.type === 'path'
|| opt.type === 'number'
2020-05-05 13:19:17 +02:00
) {
const value = await new Input ({
message: opt.message,
default: opt.default
})
.run ();
await this.assign_arg (opt, value);
return;
}
if (
2020-05-05 15:15:24 +02:00
opt.type === 'boolean'
2020-05-05 13:19:17 +02:00
) {
const value = await new Confirm ({
message: opt.message,
default: opt.default
})
.run ();
await this.assign_arg (opt, value);
}
2020-05-05 12:17:48 +02:00
}
private async get_interactive_options (): Promise<void> {
2020-05-07 12:19:19 +02:00
if (this.quiet) {
const missing = this.options.filter ((o) => !o.filled && o.required)
.map ((o) => o.name);
if (missing.length > 0) {
console.error ('missing arguments:');
console.error (missing.join (', '));
process.exit (0);
}
}
let invalid = false;
for (const opt of this.options) {
2020-05-05 13:19:17 +02:00
// eslint-disable-next-line no-await-in-loop
await this.prompt (opt);
2020-05-07 12:19:19 +02:00
if (opt.filled === false)
invalid = true;
}
if (invalid)
await this.get_interactive_options ();
2020-05-05 12:17:48 +02:00
}
2020-05-05 11:56:36 +02:00
}