UNPKG

@buddy-js/cli

Version:

A IaC tool to create your [Buddy CI] pipelines programmatically via JS/TS.

103 lines (102 loc) 5.79 kB
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)); }