Skip to content

Commit

Permalink
Merge pull request #165 from bluecadet/feat/cli
Browse files Browse the repository at this point in the history
add CLI package
  • Loading branch information
claytercek authored Oct 30, 2024
2 parents 3d40d3c + 205157e commit aec362a
Show file tree
Hide file tree
Showing 43 changed files with 2,601 additions and 2,288 deletions.
10 changes: 10 additions & 0 deletions .changeset/young-queens-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@bluecadet/launchpad": major
"@bluecadet/launchpad-scaffold": major
"@bluecadet/launchpad-content": major
"@bluecadet/launchpad-monitor": major
"@bluecadet/launchpad-utils": major
"@bluecadet/launchpad-cli": major
---

Move CLI to separate package
3,097 changes: 1,987 additions & 1,110 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@
"engines": {
"node": ">=18"
}
}
}
48 changes: 48 additions & 0 deletions packages/cli/lib/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env node

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
export { defineConfig } from './launchpad-options.js';

/**
* @typedef LaunchpadArgv
* @property {string} [config]
* @property {(string | number)[]} [env]
* @property {string} [envCascade]
*/

yargs(hideBin(process.argv))
.parserConfiguration({
// See https://github.com/yargs/yargs-parser#camel-case-expansion
'camel-case-expansion': false
})
.option('config', { alias: 'c', describe: 'Path to your JS config file', type: 'string' })
.option('env', { alias: 'e', describe: 'Path(s) to your .env file(s)', type: 'array' })
.option('env-cascade', { alias: 'E', describe: 'cascade env variables from `.env`, `.env.<arg>`, `.env.local`, `.env.<arg>.local` in launchpad root dir', type: 'string' })
.command('start', 'Starts launchpad by updating content and starting apps.', async ({ argv }) => {
const resolvedArgv = await argv;
const { start } = await import('./commands/start.js');
await start(resolvedArgv);
})
.command('stop', 'Stops launchpad by stopping apps and killing any existing PM2 instance.', async ({ argv }) => {
const resolvedArgv = await argv;
const { stop } = await import('./commands/stop.js');
await stop(resolvedArgv);
})
.command('content', 'Only download content.', async ({ argv }) => {
const resolvedArgv = await argv;
const { content } = await import('./commands/content.js');
await content(resolvedArgv);
})
.command('monitor', 'Only start apps.', async ({ argv }) => {
const resolvedArgv = await argv;
const { monitor } = await import('./commands/monitor.js');
await monitor(resolvedArgv);
})
.command('scaffold', 'Configures the current PC for exhibit environments (with admin prompt).', async ({ argv }) => {
const resolvedArgv = await argv;
const { scaffold } = await import('./commands/scaffold.js');
await scaffold(resolvedArgv);
})
.help()
.parse();
26 changes: 26 additions & 0 deletions packages/cli/lib/commands/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ResultAsync } from 'neverthrow';
import { ImportError } from '../errors.js';
import { loadConfigAndEnv } from '../utils/load-config-and-env.js';

/**
* @param {import("../cli.js").LaunchpadArgv} argv
*/
export function content(argv) {
return loadConfigAndEnv(argv).andThen(config => {
return importLaunchpadContent().map(({ LaunchpadContent }) => {
const contentInstance = new LaunchpadContent(config.content);
return contentInstance.download();
});
}).mapErr(error => {
console.error('Content failed to download.');
console.error(error.message);
process.exit(1);
});
}

export function importLaunchpadContent() {
return ResultAsync.fromPromise(
import('@bluecadet/launchpad-content'),
() => new ImportError('Could not find module "@bluecadet/launchpad-content". Make sure you have installed it.')
);
}
32 changes: 32 additions & 0 deletions packages/cli/lib/commands/monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ResultAsync } from 'neverthrow';
import { ImportError, MonitorError } from '../errors.js';
import { loadConfigAndEnv } from '../utils/load-config-and-env.js';

/**
* @param {import("../cli.js").LaunchpadArgv} argv
*/
export function monitor(argv) {
return loadConfigAndEnv(argv).andThen(config => {
return importLaunchpadMonitor()
.map(({ LaunchpadMonitor }) => {
return new LaunchpadMonitor(config.monitor);
})
.andThrough((monitorInstance) => {
return ResultAsync.fromPromise(monitorInstance.connect(), () => new MonitorError('Failed to connect to monitor'));
})
.andThrough((monitorInstance) => {
return ResultAsync.fromPromise(monitorInstance.start(), () => new MonitorError('Failed to start monitor'));
});
}).mapErr(error => {
console.error('Monitor failed to start.');
console.error(error.message);
process.exit(1);
});
}

export function importLaunchpadMonitor() {
return ResultAsync.fromPromise(
import('@bluecadet/launchpad-monitor'),
() => new ImportError('Could not find module "@bluecadet/launchpad-monitor". Make sure you have installed it.')
);
}
8 changes: 8 additions & 0 deletions packages/cli/lib/commands/scaffold.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { launchScaffold } from '@bluecadet/launchpad-scaffold';

/**
* @param {import("../cli.js").LaunchpadArgv} argv
*/
export async function scaffold(argv) {
await launchScaffold();
}
30 changes: 30 additions & 0 deletions packages/cli/lib/commands/start.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { loadConfigAndEnv } from '../utils/load-config-and-env.js';
import { importLaunchpadMonitor } from './monitor.js';
import { importLaunchpadContent } from './content.js';
import { ResultAsync } from 'neverthrow';
import { MonitorError } from '../errors.js';

/**
* @param {import("../cli.js").LaunchpadArgv} argv
*/
export async function start(argv) {
return loadConfigAndEnv(argv).andThen(config => {
return importLaunchpadContent().map(({ LaunchpadContent }) => {
const contentInstance = new LaunchpadContent(config.content);
return contentInstance.start();
}).andThen(() => importLaunchpadMonitor())
.map(({ LaunchpadMonitor }) => {
return new LaunchpadMonitor(config.monitor);
})
.andThrough((monitorInstance) => {
return ResultAsync.fromPromise(monitorInstance.connect(), () => new MonitorError('Failed to connect to monitor'));
})
.andThrough((monitorInstance) => {
return ResultAsync.fromPromise(monitorInstance.start(), () => new MonitorError('Failed to start monitor'));
});
}).mapErr(error => {
console.error('Launchpad failed to start.');
console.error(error.message);
process.exit(1);
});
}
10 changes: 10 additions & 0 deletions packages/cli/lib/commands/stop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import LaunchpadMonitor from '@bluecadet/launchpad-monitor';
import { LogManager } from '@bluecadet/launchpad-utils';

/**
* @param {import("../cli.js").LaunchpadArgv} argv
*/
export async function stop(argv) {
LogManager.configureRootLogger();
await LaunchpadMonitor.kill();
}
39 changes: 39 additions & 0 deletions packages/cli/lib/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export class LaunchpadCLIError extends Error {
/**
* @param {string} message
*/
constructor(message) {
super(message);
this.name = 'LaunchpadCLIError';
}
}

export class ImportError extends LaunchpadCLIError {
/**
* @param {string} message
*/
constructor(message) {
super(message);
this.name = 'ImportError';
}
}

export class ConfigError extends LaunchpadCLIError {
/**
* @param {string} message
*/
constructor(message) {
super(message);
this.name = 'ConfigError';
}
}

export class MonitorError extends LaunchpadCLIError {
/**
* @param {string} message
*/
constructor(message) {
super(message);
this.name = 'MonitorError';
}
}
1 change: 1 addition & 0 deletions packages/cli/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { defineConfig } from './launchpad-options.js';
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,21 @@
* @module launchpad-options
*/

/**
* @typedef {import("@bluecadet/launchpad-content/lib/content-plugin-driver.js").ContentHooks & import("@bluecadet/launchpad-monitor/lib/monitor-plugin-driver.js").MonitorHooks} AllHooks
*/

/**
* @typedef LaunchpadOptions Combined options to initialize Launchpad.
* @property {import("@bluecadet/launchpad-content").ContentOptions} [content]
* @property {import("@bluecadet/launchpad-monitor").MonitorOptions} [monitor]
* @property {import("./command-center.js").CommandOptions} [commands]
* @property {import("./command-hooks.js").HookMapping} [hooks]
* @property {import("@bluecadet/launchpad-utils/lib/plugin-driver.js").Plugin<AllHooks>[]} [plugins]
* @property {import("@bluecadet/launchpad-utils/lib/log-manager.js").LogOptions} [logging]
* @property {boolean} [shutdownOnExit] Will listen for exit events. Defaults to 'true'
*/

const LAUNCHPAD_OPTIONS_DEFAULTS = {
shutdownOnExit: true
};

/**
* Applies defaults to the provided launchpad config.
* @param {LaunchpadOptions} config
*/
export function resolveLaunchpadOptions(config) {
return {
...LAUNCHPAD_OPTIONS_DEFAULTS,
...config
};
// NOTE: at the moment, there are no defaults to apply
// so this function is just a passthrough
return config;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ import chalk from 'chalk';

const DEFAULT_CONFIG_PATHS = ['launchpad.config.js', 'launchpad.config.mjs', 'launchpad.json', 'config.json'];

/**
* @typedef BaseConfig
* @property {import('./log-manager.js').LogOptions} [logging]
*/

/**
*
* @param {ImportMeta?} importMeta
Expand All @@ -23,7 +18,7 @@ function getProcessDirname(importMeta) {
/**
* Imports a JS config from a set of paths. The JS files have to export
* its config as the default export. Will return the first config found.
* @template {BaseConfig} T config type
* @template T config type
* @param {Array<string>} paths
* @param {ImportMeta?} importMeta The import.meta property of the file at your base directory.
* @returns {Promise<Partial<T> | null>} The parsed config object or null if none can be found
Expand Down
File renamed without changes.
48 changes: 48 additions & 0 deletions packages/cli/lib/utils/load-config-and-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { err, errAsync, ok, ResultAsync } from 'neverthrow';
import { findConfig, loadConfigFromFile } from './config.js';
import { ConfigError } from '../errors.js';
import path from 'path';
import { resolveEnv } from './env.js';
import { resolveLaunchpadOptions } from '../launchpad-options.js';
import chalk from 'chalk';

/**
* @param {import("../cli.js").LaunchpadArgv} argv
* @returns {import('neverthrow').ResultAsync<import('../launchpad-options.js').ResolvedLaunchpadOptions, ConfigError>}
*/
export function loadConfigAndEnv(argv) {
const configPath = argv.config ?? findConfig();

if (!configPath) {
return errAsync(new ConfigError('No config file found.'));
}

const configDir = path.dirname(configPath);

if (argv.env) {
// if env arg is passed, resolve paths relative to the CWD
const rootDir = process.env.INIT_CWD ?? '';
resolveEnv(
argv.env.map(p => path.resolve(rootDir, p.toString()))
);
} else if (argv.envCascade) {
// if env-cascade arg is passed, resolve paths relative to the config file

// Load order: .env < .env.local < .env.[override] < .env.[override].local
resolveEnv([
path.resolve(configDir, '.env'),
path.resolve(configDir, '.env.local'),
path.resolve(configDir, `.env.${argv.envCascade}`),
path.resolve(configDir, `.env.${argv.envCascade}.local`)
]);
} else {
// default to loading .env and .env.local in the config dir
resolveEnv([
path.resolve(configDir, '.env'),
path.resolve(configDir, '.env.local')
]);
}

return ResultAsync.fromPromise(loadConfigFromFile(configPath), (e) => new ConfigError(`Failed to load config file at path: ${chalk.white(configPath)}`))
.map(config => resolveLaunchpadOptions(config));
}
Loading

0 comments on commit aec362a

Please sign in to comment.