feat: introduce yargs architecture
parent
bb844bcb46
commit
6b32132ec0
|
|
@ -0,0 +1,122 @@
|
||||||
|
import yargs from 'https://deno.land/x/yargs@v17.5.1-deno/deno.ts';
|
||||||
|
import { default as getHomeDir } from 'https://deno.land/x/dir@1.5.1/home_dir/mod.ts';
|
||||||
|
import { engineDetection } from './middleware/engine-detection/index.ts';
|
||||||
|
import { CommandInterface } from './command/command-interface.ts';
|
||||||
|
import { configureLogger } from './middleware/logger-verbosity/index.ts';
|
||||||
|
import { CommandFactory } from './command/command-factory.ts';
|
||||||
|
|
||||||
|
export class Cli {
|
||||||
|
private readonly yargs: yargs.Argv;
|
||||||
|
private readonly cliStorageAbsolutePath: string;
|
||||||
|
private readonly cliStorageCanonicalPath: string;
|
||||||
|
private readonly configFileName: string;
|
||||||
|
private command: CommandInterface;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cliStorageAbsolutePath = `${getHomeDir()}/.game-ci`;
|
||||||
|
this.cliStorageCanonicalPath = '~/.game-ci';
|
||||||
|
this.configFileName = 'config.json';
|
||||||
|
this.yargs = yargs(Deno.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validateAndParseArguments() {
|
||||||
|
this.globalSettings();
|
||||||
|
this.configureLogger();
|
||||||
|
this.globalOptions();
|
||||||
|
|
||||||
|
await this.registerBuildCommand();
|
||||||
|
|
||||||
|
await this.parse();
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: this.command,
|
||||||
|
options: this.options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private globalSettings() {
|
||||||
|
const defaultCanonicalPath = `${this.cliStorageCanonicalPath}/${this.configFileName}`;
|
||||||
|
const defaultAbsolutePath = `${this.cliStorageAbsolutePath}/${this.configFileName}`;
|
||||||
|
|
||||||
|
this.yargs
|
||||||
|
.config('config', `default: ${defaultCanonicalPath}`, async (override) => {
|
||||||
|
const configPath = override || defaultAbsolutePath;
|
||||||
|
|
||||||
|
return JSON.parse(await Deno.readTextFile(configPath));
|
||||||
|
})
|
||||||
|
.parserConfiguration({
|
||||||
|
'dot-notation': false,
|
||||||
|
'duplicate-arguments-array': false,
|
||||||
|
'negation-prefix': false,
|
||||||
|
'strip-aliased': true,
|
||||||
|
'strip-dashed': true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Todo - enable `.env()` after this is merged: https://github.com/yargs/yargs/pull/2231
|
||||||
|
// this.yargs.env();
|
||||||
|
}
|
||||||
|
|
||||||
|
private configureLogger() {
|
||||||
|
this.yargs
|
||||||
|
.options('quiet', {
|
||||||
|
alias: 'q',
|
||||||
|
default: false,
|
||||||
|
description: 'Suppress all output',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
.options('verbose', {
|
||||||
|
alias: 'v',
|
||||||
|
default: false,
|
||||||
|
description: 'Enable verbose logging',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
.options('veryVerbose', {
|
||||||
|
alias: 'vv',
|
||||||
|
default: false,
|
||||||
|
description: 'Enable very verbose logging',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
.options('maxVerbose', {
|
||||||
|
alias: 'vvv',
|
||||||
|
default: false,
|
||||||
|
description: 'Enable debug logging',
|
||||||
|
})
|
||||||
|
.middleware([configureLogger], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private globalOptions() {
|
||||||
|
this.yargs
|
||||||
|
.epilogue('for more information, find our manual at https://game.ci/docs/cli')
|
||||||
|
.middleware([])
|
||||||
|
.showHelpOnFail(true)
|
||||||
|
.strict(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async registerBuildCommand() {
|
||||||
|
this.yargs.command('build [projectPath]', 'Builds a project that you want to build', async (yargs) => {
|
||||||
|
yargs
|
||||||
|
.positional('projectPath', {
|
||||||
|
describe: 'Path to the project',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
.middleware([
|
||||||
|
engineDetection, // Command is engine specific
|
||||||
|
async (args) => {
|
||||||
|
await this.registerCommand(args, yargs);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async registerCommand(args: yargs.Arguments, yargs) {
|
||||||
|
const { engine, engineVersion, _: command } = args;
|
||||||
|
|
||||||
|
this.command = new CommandFactory().selectEngine(engine, engineVersion).createCommand(command);
|
||||||
|
|
||||||
|
await this.command.configureOptions(yargs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parse() {
|
||||||
|
this.options = await this.yargs.parseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,33 +5,37 @@ import MacBuilder from '../../model/mac-builder.ts';
|
||||||
import { CommandBase } from '../command-base.ts';
|
import { CommandBase } from '../command-base.ts';
|
||||||
|
|
||||||
export class UnityBuildCommand extends CommandBase implements CommandInterface {
|
export class UnityBuildCommand extends CommandBase implements CommandInterface {
|
||||||
public async validate() {
|
public async execute(options): Promise<boolean> {
|
||||||
await super.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async execute(): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const { workspace, actionFolder } = Action;
|
// Todo - rework this without needing this.options, use parameters from cli instead.
|
||||||
const { parameters, env } = this.options;
|
// const { workspace, actionFolder } = Action;
|
||||||
|
// const { parameters, env } = this.options;
|
||||||
Action.checkCompatibility();
|
//
|
||||||
Cache.verify();
|
// Action.checkCompatibility();
|
||||||
|
// Cache.verify();
|
||||||
const baseImage = new ImageTag(parameters);
|
//
|
||||||
log.debug('baseImage', baseImage);
|
// const baseImage = new ImageTag(options);
|
||||||
|
// log.debug('baseImage', baseImage);
|
||||||
await PlatformSetup.setup(parameters, actionFolder);
|
//
|
||||||
if (env.getOS() === 'darwin') {
|
// await PlatformSetup.setup(parameters, actionFolder);
|
||||||
MacBuilder.run(actionFolder, workspace, parameters);
|
// if (env.getOS() === 'darwin') {
|
||||||
} else {
|
// MacBuilder.run(actionFolder, workspace, parameters);
|
||||||
await Docker.run(baseImage, { workspace, actionFolder, ...parameters });
|
// } else {
|
||||||
}
|
// await Docker.run(baseImage, { workspace, actionFolder, ...parameters });
|
||||||
|
// }
|
||||||
// Set output
|
//
|
||||||
await Output.setBuildVersion(parameters.buildVersion);
|
// // Set output
|
||||||
|
// await Output.setBuildVersion(parameters.buildVersion);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async configureOptions(instance): Promise<void> {
|
||||||
|
instance.option('buildName', {
|
||||||
|
description: 'Name of the build',
|
||||||
|
type: 'string',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Options } from '../config/options.ts';
|
import { Options } from '../config/options.ts';
|
||||||
import { Input } from '../model/index.ts';
|
import { Input } from '../model/index.ts';
|
||||||
import Parameters from '../model/parameters.ts';
|
import Parameters from '../model/parameters.ts';
|
||||||
|
import { CommandInterface } from './command-interface.ts';
|
||||||
|
|
||||||
export class CommandBase {
|
export class CommandBase implements CommandInterface {
|
||||||
public readonly name: string;
|
public readonly name: string;
|
||||||
private options: Options;
|
private options: Options;
|
||||||
|
|
||||||
|
|
@ -10,20 +11,6 @@ export class CommandBase {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public configure(options: Options): this {
|
|
||||||
this.options = options;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async validate(): Promise<this> {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async parseParameters(input: Input, parameters: Parameters): Promise<Partial<Parameters>> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async execute(): Promise<boolean> {
|
public async execute(): Promise<boolean> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { NonExistentCommand } from './null/non-existent-command.ts';
|
||||||
import { UnityBuildCommand } from './build/unity-build-command.ts';
|
import { UnityBuildCommand } from './build/unity-build-command.ts';
|
||||||
import { CommandInterface } from './command-interface.ts';
|
import { CommandInterface } from './command-interface.ts';
|
||||||
import { UnityRemoteBuildCommand } from './remote/unity-remote-build-command.ts';
|
import { UnityRemoteBuildCommand } from './remote/unity-remote-build-command.ts';
|
||||||
|
import { Engine } from '../model/engine.ts';
|
||||||
|
|
||||||
export class CommandFactory {
|
export class CommandFactory {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
@ -13,9 +14,12 @@ export class CommandFactory {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public createCommand(commandName: string): CommandInterface {
|
public createCommand(command: string[]): CommandInterface {
|
||||||
|
// Structure looks like: _: [ "build" ],
|
||||||
|
const commandName = command[0];
|
||||||
|
|
||||||
switch (this.engine) {
|
switch (this.engine) {
|
||||||
case 'unity':
|
case Engine.unity:
|
||||||
return this.createUnityCommand(commandName);
|
return this.createUnityCommand(commandName);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Engine ${this.engine} is not yet supported.`);
|
throw new Error(`Engine ${this.engine} is not yet supported.`);
|
||||||
|
|
@ -26,10 +30,11 @@ export class CommandFactory {
|
||||||
switch (commandName) {
|
switch (commandName) {
|
||||||
case 'build':
|
case 'build':
|
||||||
return new UnityBuildCommand(commandName);
|
return new UnityBuildCommand(commandName);
|
||||||
case 'build-remote':
|
|
||||||
return new UnityRemoteBuildCommand(commandName);
|
// case 'remote-build':
|
||||||
default:
|
// return new UnityRemoteBuildCommand(commandName);
|
||||||
return new NonExistentCommand(commandName);
|
// default:
|
||||||
|
// return new NonExistentCommand(commandName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { Options } from '../config/options.ts';
|
import { Options } from '../config/options.ts';
|
||||||
import Parameters from '../model/parameters.ts';
|
import { yargs } from '../dependencies.ts';
|
||||||
import { Input } from '../model/index.ts';
|
|
||||||
|
|
||||||
export interface CommandInterface {
|
export interface CommandInterface {
|
||||||
name: string;
|
name: string;
|
||||||
execute: (options: Options) => Promise<boolean>;
|
execute: (options: Options) => Promise<boolean>;
|
||||||
parseParameters: (input: Input, parameters: Parameters) => Promise<Partial<Parameters>>;
|
configureOptions: (instance: yargs.Argv) => Promise<void>;
|
||||||
configure(options: Options): this;
|
|
||||||
validate(): Promise<this>;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import { Command } from 'https://deno.land/x/cmd@v1.2.0/commander/index.ts';
|
||||||
import { getUnityChangeset as getUnityChangeSet } from 'https://deno.land/x/unity_changeset@2.0.0/src/index.ts';
|
import { getUnityChangeset as getUnityChangeSet } from 'https://deno.land/x/unity_changeset@2.0.0/src/index.ts';
|
||||||
import { Buffer } from 'https://deno.land/std@0.151.0/io/buffer.ts';
|
import { Buffer } from 'https://deno.land/std@0.151.0/io/buffer.ts';
|
||||||
import { config, configSync } from 'https://deno.land/std@0.151.0/dotenv/mod.ts';
|
import { config, configSync } from 'https://deno.land/std@0.151.0/dotenv/mod.ts';
|
||||||
|
import yargs from 'https://deno.land/x/yargs@v17.4.0-deno/deno.ts';
|
||||||
|
import * as yargsTypes from 'https://deno.land/x/yargs@v17.4.0-deno/deno-types.ts';
|
||||||
|
|
||||||
// Internally managed packages
|
// Internally managed packages
|
||||||
import waitUntil from './module/wait-until.ts';
|
import waitUntil from './module/wait-until.ts';
|
||||||
|
|
@ -71,4 +73,6 @@ export {
|
||||||
waitUntil,
|
waitUntil,
|
||||||
Writable,
|
Writable,
|
||||||
yaml,
|
yaml,
|
||||||
|
yargs,
|
||||||
|
yargsTypes,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
42
src/index.ts
42
src/index.ts
|
|
@ -1,41 +1,13 @@
|
||||||
import { configSync } from './dependencies.ts';
|
import { Cli } from './cli.ts';
|
||||||
import { configureLogger } from './core/logger/index.ts';
|
|
||||||
import { Options } from './config/options.ts';
|
|
||||||
import { CommandFactory } from './command/command-factory.ts';
|
|
||||||
import { ArgumentsParser } from './core/cli/arguments-parser.ts';
|
|
||||||
import { Environment } from './core/env/environment.ts';
|
|
||||||
import { EngineDetector } from './core/engine/engine-detector.ts';
|
|
||||||
|
|
||||||
export class GameCI {
|
class GameCI {
|
||||||
private readonly env: Environment;
|
public static async run() {
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.env = new Environment(Deno.env, configSync());
|
|
||||||
this.args = Deno.args;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run() {
|
|
||||||
try {
|
try {
|
||||||
// Infallible configuration
|
const { command, options } = await new Cli().validateAndParseArguments();
|
||||||
const { commandName, subCommands, args, verbosity } = new ArgumentsParser().parse(this.args);
|
|
||||||
await configureLogger(verbosity);
|
|
||||||
|
|
||||||
// Todo - set default values for parameters to restore functionality.
|
const success = await command.execute(options);
|
||||||
// Todo - investigate how fitting a CLI lib will be for us
|
|
||||||
// (now that things are starting to be more separated)
|
|
||||||
|
|
||||||
// Determine which command to run
|
if (!success) throw new Error(`${command.name} failed.`);
|
||||||
const { engine, engineVersion } = await new EngineDetector(subCommands, args).detect();
|
|
||||||
const command = new CommandFactory().selectEngine(engine, engineVersion).createCommand(commandName, subCommands);
|
|
||||||
|
|
||||||
// Provide the command with options
|
|
||||||
const options = await new Options(command, this.env).generateParameters(args);
|
|
||||||
await command.configure(options).validate();
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
if (log.isVerbose) log.info('Executing', command.name);
|
|
||||||
const success = await command.execute();
|
|
||||||
if (!success) log.info(`Command ${command.name} failed.`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
|
|
@ -43,4 +15,4 @@ export class GameCI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await new GameCI().run();
|
await GameCI.run();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { EngineDetector } from './engine-detector.ts';
|
||||||
|
|
||||||
|
export const engineDetection = async (argv) => {
|
||||||
|
const { projectPath } = argv;
|
||||||
|
|
||||||
|
if (!projectPath) throw new Error('Unable to detect engine. No project path provided.');
|
||||||
|
|
||||||
|
const { engine, engineVersion } = await new EngineDetector(projectPath).detect();
|
||||||
|
|
||||||
|
argv.engine = engine;
|
||||||
|
argv.engineVersion = engineVersion;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { configureLogger as createLoggerAndSetVerbosity } from '../../core/logger/index.ts';
|
||||||
|
|
||||||
|
export const configureLogger = async (argv) => {
|
||||||
|
const { quiet, verbose, veryVerbose, maxVerbose } = argv;
|
||||||
|
|
||||||
|
let verbosity;
|
||||||
|
if (maxVerbose) {
|
||||||
|
verbosity = 3;
|
||||||
|
} else if (veryVerbose) {
|
||||||
|
verbosity = 2;
|
||||||
|
} else if (verbose) {
|
||||||
|
verbosity = 1;
|
||||||
|
} else if (quiet) {
|
||||||
|
verbosity = -1;
|
||||||
|
} else {
|
||||||
|
verbosity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createLoggerAndSetVerbosity(verbosity);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class Engine {
|
||||||
|
public static unity = 'unity';
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue