@buddy-js/cli
Version:
A IaC tool to create your [Buddy CI] pipelines programmatically via JS/TS.
103 lines (102 loc) • 5.79 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getRegisteredPipelines, isFixed } from '@buddy-js/core';
import Schema from '@buddy-js/types';
import { Spinner, StatusMessage } from '@inkjs/ui';
import { Args, Flags } from '@oclif/core';
import Ajv from 'ajv-draft-04';
import addFormats from 'ajv-formats';
import { glob } from 'glob';
import sanitizeFilename from 'sanitize-filename';
import YAML from 'yaml';
import { getLoader } from '../utils/loader.js';
import { Box, Text } from 'ink';
import { BaseCommand } from '../utils/base-command.js';
function Switch(props) {
if (props.value != null && props.value in props && props[props.value]) {
return props[props.value]();
}
return props._(props.value);
}
export default class Generate extends BaseCommand {
static enableJsonFlag = true;
static aliases = ['gen', 'g'];
static description = 'Generates YAML files for Buddy CI pipeline definitions';
static examples = ['<%= config.bin %> <%= command.id %>'];
static args = {
input: Args.string({ char: 'i', description: 'input file', default: '.buddy/buddy.{ts,mts,cts,js,mjs,cjs}' })
};
static flags = {
output: Flags.string({ char: 'o', description: 'output directory', default: '.buddy' }),
clear: Flags.boolean({
description: '[default: true] Remove all YAML files from output directory before generating',
default: true,
allowNo: true
}),
cwd: Flags.string({ default: '.' }),
indent: Flags.integer({ description: 'Indentation depth for generated YAML files', default: 2, helpGroup: 'YAML format' }),
lineWidth: Flags.integer({ description: 'Max line width for generated YAML files', default: 80, helpGroup: 'YAML format' })
};
view = ({ load, validate, clear, emit, result }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: JSON.stringify({ load, validate, clear, emit }, null, 2) }), _jsx(Switch, { value: load, running: () => _jsx(Spinner, { label: "Loading..." }), done: () => _jsx(StatusMessage, { variant: "success", children: "Loaded" }), _: () => _jsx(Text, { color: "gray", children: "Load" }) }), _jsx(Switch, { value: validate, running: () => _jsx(Spinner, { label: "Validating..." }), done: () => _jsx(StatusMessage, { variant: "success", children: "Validated" }), _: () => _jsx(Text, { color: "gray", children: "Validate" }) }), _jsx(Switch, { value: clear, running: () => _jsx(Spinner, { label: "Clearing..." }), done: () => _jsx(StatusMessage, { variant: "success", children: "Cleared" }), skipped: () => (_jsx(Text, { color: "gray", strikethrough: true, children: "Clear (Skipped)" })), _: () => _jsx(Text, { color: "gray", children: "Clear" }) }), _jsx(Switch, { value: emit, running: () => _jsx(Spinner, { label: "Emitting..." }), done: () => _jsx(StatusMessage, { variant: "success", children: "Emitted" }), _: () => _jsx(Text, { color: "gray", children: "Emit" }) }), result && (_jsx(Box, { marginTop: 1, children: _jsx(StatusMessage, { variant: "success", children: result }) }))] }));
initialState = {};
async *handle() {
if (!this.flags.clear) {
yield { clear: 'skipped' };
}
const inputFile = await this.findInputFile();
const extension = path.extname(inputFile);
const loader = getLoader(extension);
yield { load: 'running' };
await loader.load(path.resolve(this.flags.cwd, inputFile));
yield { load: 'done' };
const ajv = new Ajv({
loadSchema: async () => ({})
});
addFormats(ajv);
const pipelines = [...getRegisteredPipelines()];
yield { validate: 'running' };
await sleep(16);
const fn = await ajv.compileAsync(Schema);
if (!fn(pipelines)) {
throw new Error(fn.errors.map(error => error.message).join('\n'));
}
yield { validate: 'done' };
if (this.flags.clear) {
yield { clear: 'running' };
const cwd = path.resolve(this.flags.cwd, this.flags.output);
const files = await glob('*.yml', { cwd, absolute: true });
await Promise.all(files.map(file => fs.unlink(file)));
yield { clear: 'done' };
}
yield { emit: 'running' };
const schemaFile = fileURLToPath(import.meta.resolve('@buddy-js/types/schema.json'));
for (const pipeline of pipelines) {
const filename = `${sanitizeFilename(pipeline.pipeline).replace(/ +/g, '-') + (isFixed(pipeline) ? '.fixed' : '')}.yml`;
const yaml = YAML.stringify([pipeline], { indent: this.flags.indent, lineWidth: this.flags.lineWidth });
const file = path.resolve(this.flags.cwd, this.flags.output, filename);
await fs.mkdir(path.dirname(file), { recursive: true });
if (!(await fs.access(file).then(() => true, () => false))) {
await fs.writeFile(file, `# yaml-language-server: $schema=${path.relative(path.dirname(file), schemaFile)}\n`);
}
await fs.appendFile(file, yaml);
}
yield { emit: 'done' };
const result = `Created ${pipelines.length} Pipeline(s)`;
yield { result };
return {
result
};
}
async findInputFile() {
const files = await glob(this.args.input, { cwd: this.flags.cwd, absolute: true });
if (!files[0]) {
throw new Error(`Cannot find input file: "${this.args.input}" in ${this.flags.cwd}`);
}
return files[0];
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}