198 lines
4.9 KiB
TypeScript
198 lines
4.9 KiB
TypeScript
/*
|
|
* 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
|
|
*/
|
|
|
|
/* eslint-disable max-lines-per-function */
|
|
/* eslint-disable complexity */
|
|
/* eslint-disable max-statements */
|
|
/* eslint-disable no-process-env */
|
|
import { Persistent } from '@scode/modelling';
|
|
import fs from 'fs-extra';
|
|
import yargs, { Options } from 'yargs';
|
|
import { Confirm, Input } from 'enquirer';
|
|
|
|
type OptionType =
|
|
'string'
|
|
| 'number'
|
|
| 'boolean'
|
|
| 'file'
|
|
| 'folder'
|
|
| 'path';
|
|
|
|
interface Option {
|
|
name: string;
|
|
type: OptionType;
|
|
required?: boolean;
|
|
default?: unknown;
|
|
alias?: string;
|
|
env?: string;
|
|
description?: string;
|
|
message?: string;
|
|
}
|
|
|
|
interface OptionProcess extends Option {
|
|
filled: boolean;
|
|
value?: unknown;
|
|
}
|
|
|
|
export class InteractiveOptions extends Persistent {
|
|
protected options: Array<OptionProcess>;
|
|
|
|
public constructor (options: Array<Option>) {
|
|
super ();
|
|
this.options = options
|
|
.map ((v) => ({ filled: false, ...v } as OptionProcess));
|
|
for (const option of this.options) {
|
|
if (typeof option.default !== option.type) {
|
|
throw new Error (
|
|
`default does not match option type on ${option.name}`
|
|
);
|
|
}
|
|
this.properties[option.name] = option.type;
|
|
}
|
|
}
|
|
|
|
public async parse (): Promise<void> {
|
|
await this.get_env_options ();
|
|
await this.get_args_options ();
|
|
await this.get_interactive_options ();
|
|
}
|
|
|
|
private async assign_arg (opt: OptionProcess, value: unknown): Promise<void> {
|
|
if (opt.type === 'string') {
|
|
opt.value = String (value);
|
|
opt.filled = true;
|
|
return;
|
|
}
|
|
if (opt.type === 'number') {
|
|
if (![
|
|
'string',
|
|
'number'
|
|
].includes (typeof value))
|
|
return;
|
|
|
|
const as_num = parseInt (String (value));
|
|
const is_num = !isNaN (as_num);
|
|
if (is_num) {
|
|
opt.value = as_num;
|
|
opt.filled = true;
|
|
}
|
|
return;
|
|
}
|
|
if (opt.type === 'boolean') {
|
|
if (![
|
|
'string',
|
|
'boolean',
|
|
'number'
|
|
].includes (typeof value))
|
|
return;
|
|
|
|
const is_bool = [
|
|
0,
|
|
1
|
|
].includes (parseInt (String (value)))
|
|
|| (/^(?:true|false)$/ui).test (value as string);
|
|
if (is_bool) {
|
|
const as_bool = value === 1 || (/true/ui).test (value as string);
|
|
opt.value = as_bool;
|
|
opt.filled = true;
|
|
}
|
|
return;
|
|
}
|
|
if (
|
|
opt.type === 'path'
|
|
|| opt.type === 'file'
|
|
|| opt.type === 'folder'
|
|
) {
|
|
if (typeof value !== 'string' || !await fs.pathExists (value))
|
|
return;
|
|
if (opt.type === 'path') {
|
|
opt.value = value;
|
|
opt.filled = true;
|
|
return;
|
|
}
|
|
const stat = await fs.stat (value);
|
|
if (stat.isDirectory () === (opt.type === 'folder')) {
|
|
opt.value = value;
|
|
opt.filled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async get_env_options (): Promise<void> {
|
|
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 ();
|
|
}));
|
|
}
|
|
|
|
private async get_args_options (): Promise<void> {
|
|
const yargs_config: Record<string, Options> = {
|
|
quiet: {
|
|
alias: 'q',
|
|
default: false,
|
|
type: 'boolean',
|
|
describe: 'do not ask for options interactively'
|
|
}
|
|
};
|
|
for (const opt of this.options) {
|
|
yargs_config[opt.name] = {
|
|
alias: opt.alias,
|
|
default: opt.default,
|
|
type: opt.type === 'boolean' ? 'boolean' : 'string',
|
|
describe: opt.description
|
|
};
|
|
}
|
|
const argv = yargs.options (yargs_config)
|
|
.parse ();
|
|
await Promise.all (this.options.map ((opt) => {
|
|
if (typeof argv[opt.name] !== 'undefined')
|
|
return this.assign_arg (opt, argv[opt.name]);
|
|
return Promise.resolve ();
|
|
}));
|
|
}
|
|
|
|
private async prompt (opt: OptionProcess): Promise<void> {
|
|
if (opt.filled)
|
|
return;
|
|
if (
|
|
opt.type === 'string'
|
|
|| opt.type === 'file'
|
|
|| opt.type === 'folder'
|
|
|| opt.type === 'path'
|
|
|| opt.type === 'number'
|
|
) {
|
|
const value = await new Input ({
|
|
message: opt.message,
|
|
default: opt.default
|
|
})
|
|
.run ();
|
|
await this.assign_arg (opt, value);
|
|
return;
|
|
}
|
|
if (
|
|
opt.type === 'boolean'
|
|
) {
|
|
const value = await new Confirm ({
|
|
message: opt.message,
|
|
default: opt.default
|
|
})
|
|
.run ();
|
|
await this.assign_arg (opt, value);
|
|
}
|
|
}
|
|
|
|
private async get_interactive_options (): Promise<void> {
|
|
for (const opt of this.options)
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.prompt (opt);
|
|
}
|
|
}
|