diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 00000000..6b7f785c --- /dev/null +++ b/src/cli.ts @@ -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(); + } +} diff --git a/src/command/build/unity-build-command.ts b/src/command/build/unity-build-command.ts index 2a7090a4..f4e3b077 100644 --- a/src/command/build/unity-build-command.ts +++ b/src/command/build/unity-build-command.ts @@ -5,33 +5,37 @@ import MacBuilder from '../../model/mac-builder.ts'; import { CommandBase } from '../command-base.ts'; export class UnityBuildCommand extends CommandBase implements CommandInterface { - public async validate() { - await super.validate(); - } - - public async execute(): Promise { + public async execute(options): Promise { try { - const { workspace, actionFolder } = Action; - const { parameters, env } = this.options; - - Action.checkCompatibility(); - Cache.verify(); - - const baseImage = new ImageTag(parameters); - log.debug('baseImage', baseImage); - - await PlatformSetup.setup(parameters, actionFolder); - if (env.getOS() === 'darwin') { - MacBuilder.run(actionFolder, workspace, parameters); - } else { - await Docker.run(baseImage, { workspace, actionFolder, ...parameters }); - } - - // Set output - await Output.setBuildVersion(parameters.buildVersion); + // Todo - rework this without needing this.options, use parameters from cli instead. + // const { workspace, actionFolder } = Action; + // const { parameters, env } = this.options; + // + // Action.checkCompatibility(); + // Cache.verify(); + // + // const baseImage = new ImageTag(options); + // log.debug('baseImage', baseImage); + // + // await PlatformSetup.setup(parameters, actionFolder); + // if (env.getOS() === 'darwin') { + // MacBuilder.run(actionFolder, workspace, parameters); + // } else { + // await Docker.run(baseImage, { workspace, actionFolder, ...parameters }); + // } + // + // // Set output + // await Output.setBuildVersion(parameters.buildVersion); } catch (error) { log.error(error); Deno.exit(1); } } + + public async configureOptions(instance): Promise { + instance.option('buildName', { + description: 'Name of the build', + type: 'string', + }); + } } diff --git a/src/command/command-base.ts b/src/command/command-base.ts index 841201bb..00c95ce6 100644 --- a/src/command/command-base.ts +++ b/src/command/command-base.ts @@ -1,8 +1,9 @@ import { Options } from '../config/options.ts'; import { Input } from '../model/index.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; private options: Options; @@ -10,20 +11,6 @@ export class CommandBase { this.name = name; } - public configure(options: Options): this { - this.options = options; - - return this; - } - - public async validate(): Promise { - return this; - } - - public async parseParameters(input: Input, parameters: Parameters): Promise> { - return {}; - } - public async execute(): Promise { throw new Error('Method not implemented.'); } diff --git a/src/command/command-factory.ts b/src/command/command-factory.ts index 846c32d2..643b078d 100644 --- a/src/command/command-factory.ts +++ b/src/command/command-factory.ts @@ -2,6 +2,7 @@ import { NonExistentCommand } from './null/non-existent-command.ts'; import { UnityBuildCommand } from './build/unity-build-command.ts'; import { CommandInterface } from './command-interface.ts'; import { UnityRemoteBuildCommand } from './remote/unity-remote-build-command.ts'; +import { Engine } from '../model/engine.ts'; export class CommandFactory { constructor() {} @@ -13,9 +14,12 @@ export class CommandFactory { return this; } - public createCommand(commandName: string): CommandInterface { + public createCommand(command: string[]): CommandInterface { + // Structure looks like: _: [ "build" ], + const commandName = command[0]; + switch (this.engine) { - case 'unity': + case Engine.unity: return this.createUnityCommand(commandName); default: throw new Error(`Engine ${this.engine} is not yet supported.`); @@ -26,10 +30,11 @@ export class CommandFactory { switch (commandName) { case 'build': return new UnityBuildCommand(commandName); - case 'build-remote': - return new UnityRemoteBuildCommand(commandName); - default: - return new NonExistentCommand(commandName); + + // case 'remote-build': + // return new UnityRemoteBuildCommand(commandName); + // default: + // return new NonExistentCommand(commandName); } } } diff --git a/src/command/command-interface.ts b/src/command/command-interface.ts index 3edf98fa..fb8094a4 100644 --- a/src/command/command-interface.ts +++ b/src/command/command-interface.ts @@ -1,11 +1,8 @@ import { Options } from '../config/options.ts'; -import Parameters from '../model/parameters.ts'; -import { Input } from '../model/index.ts'; +import { yargs } from '../dependencies.ts'; export interface CommandInterface { name: string; execute: (options: Options) => Promise; - parseParameters: (input: Input, parameters: Parameters) => Promise>; - configure(options: Options): this; - validate(): Promise; + configureOptions: (instance: yargs.Argv) => Promise; } diff --git a/src/dependencies.ts b/src/dependencies.ts index e68c5bbd..57c757ba 100644 --- a/src/dependencies.ts +++ b/src/dependencies.ts @@ -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 { 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 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 import waitUntil from './module/wait-until.ts'; @@ -71,4 +73,6 @@ export { waitUntil, Writable, yaml, + yargs, + yargsTypes, }; diff --git a/src/index.ts b/src/index.ts index ee645e24..35b3346f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,41 +1,13 @@ -import { configSync } from './dependencies.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'; +import { Cli } from './cli.ts'; -export class GameCI { - private readonly env: Environment; - - constructor() { - this.env = new Environment(Deno.env, configSync()); - this.args = Deno.args; - } - - public async run() { +class GameCI { + public static async run() { try { - // Infallible configuration - const { commandName, subCommands, args, verbosity } = new ArgumentsParser().parse(this.args); - await configureLogger(verbosity); + const { command, options } = await new Cli().validateAndParseArguments(); - // Todo - set default values for parameters to restore functionality. - // Todo - investigate how fitting a CLI lib will be for us - // (now that things are starting to be more separated) + const success = await command.execute(options); - // Determine which command to run - 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.`); + if (!success) throw new Error(`${command.name} failed.`); } catch (error) { log.error(error); Deno.exit(1); @@ -43,4 +15,4 @@ export class GameCI { } } -await new GameCI().run(); +await GameCI.run(); diff --git a/src/core/engine/engine-detector.ts b/src/middleware/engine-detection/engine-detector.ts similarity index 100% rename from src/core/engine/engine-detector.ts rename to src/middleware/engine-detection/engine-detector.ts diff --git a/src/middleware/engine-detection/index.ts b/src/middleware/engine-detection/index.ts new file mode 100644 index 00000000..8384cf8b --- /dev/null +++ b/src/middleware/engine-detection/index.ts @@ -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; +}; diff --git a/src/middleware/logger-verbosity/index.ts b/src/middleware/logger-verbosity/index.ts new file mode 100644 index 00000000..c7a5081c --- /dev/null +++ b/src/middleware/logger-verbosity/index.ts @@ -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); +}; diff --git a/src/model/engine.ts b/src/model/engine.ts new file mode 100644 index 00000000..1d082d8b --- /dev/null +++ b/src/model/engine.ts @@ -0,0 +1,3 @@ +export class Engine { + public static unity = 'unity'; +}