diff --git a/.gitignore b/.gitignore index d0be6e3..dd377e5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ lib/* dist-test/* sandbox/* .vscode/ +pnpm-lock.yaml +pnpm-workspace.yaml +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index d0c9fbb..d0da128 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Once installed, run godspeed from your terminal to see the available commands an | plugins | | manage eventsource and datasource plugins for godspeed. | | gen-crud-api | | scans your prisma datasources and generate CRUD APIs events and workflows | | gen-graphql-schema | | scans your graphql events and generate graphql schema . | + | tools | | Extra godspeed tools. | ## 📖 Documentation diff --git a/package.json b/package.json index 0c4b77a..796b241 100755 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "glob": "10.3.4", "inquirer": "^8.2.5", "js-yaml": "^4.1.0", + "json-schema": "^0.4.0", "lodash": "^4.17.21", "mocha": "^10.2.0", "path": "^0.12.7", @@ -43,7 +44,8 @@ "rimraf": "^6.0.1", "signale": "^1.4.0", "simple-git": "^3.7.1", - "sinon": "^16.0.0" + "sinon": "^16.0.0", + "yaml": "^2.8.1" }, "devDependencies": { "@types/chai": "^4.3.6", @@ -55,6 +57,7 @@ "@types/handlebars-helpers": "^0.5.3", "@types/inquirer": "^9.0.3", "@types/js-yaml": "^4.0.5", + "@types/json-schema": "^7.0.15", "@types/mocha": "^10.0.1", "@types/node": "^18.17.14", "@types/proxyquire": "^1.3.29", diff --git a/src/commands/create/index.ts b/src/commands/create/index.ts index cb1a58a..3d41b65 100644 --- a/src/commands/create/index.ts +++ b/src/commands/create/index.ts @@ -1,5 +1,5 @@ const fsExtras = require("fs-extra"); -import path from "path" +import path from "path"; // import interactiveMode from "../../utils/interactiveMode"; // import { @@ -9,7 +9,8 @@ import path from "path" // } from "../../utils/dockerUtility"; // import checkPrerequisite from "../../utils/checkPrerequisite"; import { - installDependencies, installPackage, + installDependencies, + installPackage, validateAndCreateProjectDirectory, } from "../../utils/index"; import { copyingLocalTemplate } from "../../utils"; @@ -77,30 +78,31 @@ export default async function create( const gitFilePath = path.join(process.cwd(), projectName, ".git"); fsExtras.removeSync(gitFilePath); - - -if(options.fromExample === 'mongo-as-prisma'){ - await installPackage(projectDirPath,'@godspeedsystems/plugins-prisma-as-datastore') -} + if (options.fromExample === "mongo-as-prisma") { + await installPackage( + projectDirPath, + "@godspeedsystems/plugins-prisma-as-datastore" + ); + } await installDependencies(projectDirPath, projectName); + // Delete .template folder in project folder. + const templateFilePath = path.join(process.cwd(), projectName, ".template"); + + if (fsExtras.existsSync(templateFilePath)) { + fsExtras.removeSync(templateFilePath); + } try { // the NEW flow [without containers] - - // const composeOptions = await getComposeOptions(); - // if (composeOptions.composeOptions) { // composeOptions.composeOptions.push(`${projectName}_devcontainer`); // } - // composeOptions.cwd = path.resolve(projectDirPath, ".devcontainer"); // composeOptions.log = process.env.DEBUG ? Boolean(process.env.DEBUG) : false; - // // check if there are already running resources // await prepareToStartContainers(projectName, composeOptions); - // await buildContainers( // projectName, // godspeedOptions, diff --git a/src/index.ts b/src/index.ts index 35f2116..55b4cf6 100755 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ process.env.SUPPRESS_NO_CONFIG_WARNING = "true"; import * as dotenv from "dotenv"; import chalk from "chalk"; -import { Command } from "commander"; +import { Command, Option } from "commander"; import create from "./commands/create/index"; // import update from "./commands/update/index"; import path from "path"; @@ -13,12 +13,13 @@ import devOpsPluginCommands from "./commands/devops-plugin"; import pluginCommands from "./commands/plugin"; import prismaCommands from "./commands/prisma"; import otelCommands from "./commands/otel"; -import {genGraphqlSchema} from "./utils/index"; +import { genGraphqlSchema } from "./utils/index"; const fsExtras = require("fs-extra"); import { cwd } from "process"; import fs, { readFileSync } from "fs"; import { homedir } from "node:os"; -import { readdir } from 'fs/promises'; +import { readdir } from "fs/promises"; +import { JSONSchema7 } from "json-schema"; import { globSync } from "glob"; import inquirer from "inquirer"; @@ -42,6 +43,97 @@ const detectOSType = () => { return "UNKNOWN"; } }; + +function fetchToolsInfo(): Tool[] { + const result = spawnSync.sync( + "npx", + ["@godspeedsystems/gs-tool", "list", "-m"], + { + shell: true, + } + ); + + try { + return JSON.parse(result.stdout?.toString() || "{}")?.data; + } catch { + return []; + } +} + +function extractJson(str: string) { + const json: object[] = []; + + if (!str) return json; + + // Step 1: Combine stdout + stderr for unified search + const text = str.trim(); + + // Step 2: Regex pattern to find possible JSON blocks + // It matches `{...}` or `[...]` at any nesting level + const jsonRegex = /(\{[\s\S]*\}|\[[\s\S]*\])/g; + + // Step 3: Iterate through all matches, and try JSON.parse + const matches = text.match(jsonRegex); + if (!matches) return json; + + for (const candidate of matches) { + try { + json.push(JSON.parse(candidate)); // return first valid JSON + } catch { + // not valid, continue + } + } + + return json; +} + +/** + * Convert JSON schema into Commander options + */ +function parseInputSchemaToCommander(schema: JSONSchema7, cmd: Command) { + if (!schema || schema.type !== "object" || !schema.properties) return; + + for (const [prop, config] of Object.entries(schema.properties)) { + if (typeof config === "boolean") continue; + + const desc = config.description || ""; + const def = (config as any).default; + + if (config.enum) { + const values = config.enum as string[]; + const opt = new Option(`--${prop} <${prop}>`, `${desc}`).choices(values); + + if (def !== undefined) opt.default(def); + cmd.addOption(opt); + + // opt.attributeName = () => prop; + continue; + } + + // Handle booleans + if (config.type === "boolean") { + cmd.option(`--${prop}`, desc, def); + continue; + } + + // Handle strings/numbers etc. + const flag = `--${prop} <${prop}>`; + + cmd.option(flag, desc, def); + } +} + +function getRawOpts(cmd: Command) { + const opts = cmd.optsWithGlobals(); + const result: Record = {}; + for (const opt of cmd.options) { + const key = opt.long?.replace(/^--/, ""); // e.g. foo-bar + if (key && opts[opt.attributeName()] !== undefined) + result[key] = opts[opt.attributeName()]; + } + return result; +} + export const isAGodspeedProject = () => { // verify .godspeed file, only then, it is a godspeed project try { @@ -96,19 +188,31 @@ const updateServicesJson = async (add = true) => { }; if (add) { - const exists = servicesData.services.some((service: any) => service.path === process.cwd()); + const exists = servicesData.services.some( + (service: any) => service.path === process.cwd() + ); if (!exists) servicesData.services.push(currentProject); } else { - servicesData.services = servicesData.services.filter((service: any) => service.path !== process.cwd()); + servicesData.services = servicesData.services.filter( + (service: any) => service.path !== process.cwd() + ); } - await fs.promises.writeFile(servicesFile, JSON.stringify(servicesData, null, 2), "utf-8"); + await fs.promises.writeFile( + servicesFile, + JSON.stringify(servicesData, null, 2), + "utf-8" + ); console.log(chalk.green("Project data updated successfully.")); } catch (error: any) { if (error.code === "EACCES") { const action = add ? "link" : "unlink"; - console.error("\x1b[31mPermission denied: Cannot write to services.json\x1b[0m"); - console.error(`\x1b[33mTry running: \x1b[1msudo godspeed ${action}\x1b[0m`); + console.error( + "\x1b[31mPermission denied: Cannot write to services.json\x1b[0m" + ); + console.error( + `\x1b[33mTry running: \x1b[1msudo godspeed ${action}\x1b[0m` + ); } else { console.error("\x1b[31mAn error occurred:\x1b[0m", error); } @@ -205,7 +309,6 @@ const updateServicesJson = async (add = true) => { // } // }); - program .command("dev") .description("run godspeed development server.") @@ -228,19 +331,21 @@ const updateServicesJson = async (add = true) => { } }); - program + program .command("link") - .description("Link a local Godspeed project to the global environment for development in godspeed-daemon.") + .description( + "Link a local Godspeed project to the global environment for development in godspeed-daemon." + ) .action(async () => { if (await isAGodspeedProject()) { updateServicesJson(true); } }); - + program .command("unlink") .description("Unlink a local Godspeed project from the global environment.") - .action(async() => { + .action(async () => { if (await isAGodspeedProject()) { updateServicesJson(false); } @@ -256,14 +361,12 @@ const updateServicesJson = async (add = true) => { spawnSync("pnpm", ["run", "gen-crud-api"], { stdio: "inherit" }); } }); - program + program .command("gen-graphql-schema") - .description( - "scans your graphql events and generate graphql schema" - ) + .description("scans your graphql events and generate graphql schema") .action(async () => { if (isAGodspeedProject()) { - await genGraphqlSchema() + await genGraphqlSchema(); } }); program @@ -296,28 +399,28 @@ const updateServicesJson = async (add = true) => { }); // fetch the list of installed devops-plugins - const pluginPath = path.resolve(homedir(), `.godspeed/devops-plugins/node_modules/@godspeedsystems/`); + const pluginPath = path.resolve( + homedir(), + `.godspeed/devops-plugins/node_modules/@godspeedsystems/` + ); + + const devopsPluginSubCommand = program + .command("devops-plugin") + .description(`manages godspeed devops-plugins.`); - const devopsPluginSubCommand = program.command('devops-plugin') - .description(`manages godspeed devops-plugins.`) + devopsPluginSubCommand.addCommand(devOpsPluginCommands.install); - devopsPluginSubCommand - .addCommand(devOpsPluginCommands.install); + devopsPluginSubCommand.addCommand(devOpsPluginCommands.list); - devopsPluginSubCommand - .addCommand(devOpsPluginCommands.list); - - devopsPluginSubCommand - .addCommand(devOpsPluginCommands.remove); - - devopsPluginSubCommand - .addCommand(devOpsPluginCommands.update); + devopsPluginSubCommand.addCommand(devOpsPluginCommands.remove); + + devopsPluginSubCommand.addCommand(devOpsPluginCommands.update); const devopsPluginHelp = ` To see help for any installed devops plugin, you can run: help `; - devopsPluginSubCommand.on('--help', () => { + devopsPluginSubCommand.on("--help", () => { console.log(devopsPluginHelp); }); @@ -330,19 +433,25 @@ const updateServicesJson = async (add = true) => { .description("installed godspeed devops plugin") .allowUnknownOption(true) .action(async () => { - const installedPluginPath = path.resolve(pluginPath, installedPluginName, "dist/index.js"); + const installedPluginPath = path.resolve( + pluginPath, + installedPluginName, + "dist/index.js" + ); // check if installedPluginPath exists. if (!fs.existsSync(installedPluginPath)) { - console.error(`${installedPluginName} is not installed properly. Please make sure ${installedPluginPath} exists.`); + console.error( + `${installedPluginName} is not installed properly. Please make sure ${installedPluginPath} exists.` + ); return; } const args = process.argv.slice(4); // Spawn the plugin with all arguments and options - spawnSync('node', [installedPluginPath, ...args], { - stdio: 'inherit', + spawnSync("node", [installedPluginPath, ...args], { + stdio: "inherit", }); }); } @@ -391,5 +500,69 @@ const updateServicesJson = async (add = true) => { .addCommand(otelCommands.disable) .description("enable/disable Observability in Godspeed."); + const toolsCmd = program + .command("tools") + .description("Extra godspeed tools") + .allowUnknownOption(true) + .showHelpAfterError() + .showSuggestionAfterError(true) + .allowUnknownOption() + .allowExcessArguments(); + + if (process.argv.includes("tools")) { + const toolsList = fetchToolsInfo(); + + toolsList.forEach((tool) => { + const cmd = new Command(tool.name) + .description(tool.summary) + .version(tool.version) + .option("--input-json ", "input json file or json string"); + + parseInputSchemaToCommander(tool.inputjson, cmd); + + cmd.action((_, thisCmd) => { + const options = getRawOpts(thisCmd); + + const inputJsonValue = options["input-json"] || JSON.stringify(options); + + const args = [ + "--input-json-base64", + Buffer.from(inputJsonValue, "utf-8").toString("base64"), + ]; + + const result = spawnSync.sync( + "npx", + ["@godspeedsystems/gs-tool", tool.name, ...args], + { + stdio: "pipe", + shell: true, + cwd: process.cwd(), + } + ); + + const error = extractJson(result.stderr?.toString() || "{}")[0] as any; + const output = extractJson(result.stdout?.toString() || "{}")[0] as any; + + if (error?.error?.message) { + console.error("\n" + chalk.red.bold(error.error.message)); + } + + if (output?.data && Object.keys(output?.data).length) { + console.log("\n" + JSON.stringify(output.data, null, 2)); + } + + if (error?.message) { + console.log("\n" + chalk.cyan(error.message)); + } + + if (output?.message) { + console.log("\n" + chalk.cyan(output.message)); + } + }); + + toolsCmd.addCommand(cmd); + }); + } + program.parse(); })(); diff --git a/src/utils/types.ts b/src/utils/types.ts index e13181d..f645cdb 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -47,3 +47,10 @@ interface GodspeedOptions { cliVersionWhileLastUpdate: string; }; } + +type Tool = { + name: string; + summary: string; + version: string; + inputjson: InputJSON; +};