diff --git a/package-lock.json b/package-lock.json index 181ed47..396e52d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4079,7 +4079,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dev": true, "dependencies": { "restore-cursor": "^4.0.0" }, @@ -4090,6 +4089,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -6091,6 +6101,17 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -6938,6 +6959,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -9295,6 +9327,111 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.0.1.tgz", + "integrity": "sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz", + "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -10311,7 +10448,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dev": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -10327,7 +10463,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "engines": { "node": ">=6" } @@ -10336,7 +10471,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -10350,8 +10484,7 @@ "node_modules/restore-cursor/node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/ret": { "version": "0.2.2", @@ -10921,6 +11054,17 @@ "node": ">= 0.8" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", @@ -12321,6 +12465,7 @@ "chalk": "^5.1.2", "dotenv": "^16.4.5", "mime-types": "^2.1.35", + "ora": "^8.0.1", "pino": "^9.3.1", "pino-pretty": "^11.2.1" }, diff --git a/packages/cli/cli.js b/packages/cli/cli.js new file mode 100644 index 0000000..0f37181 --- /dev/null +++ b/packages/cli/cli.js @@ -0,0 +1,4 @@ +#!/usr/bin/env -S node --no-warnings +/* eslint-disable */ +process.removeAllListeners("warning"); +import "./dist/index.js"; diff --git a/packages/cli/package.json b/packages/cli/package.json index 1255c7f..9615d25 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,6 +21,9 @@ "engines": { "node": ">=18.0.0" }, + "bin": { + "ratemyopenapi": "cli.js" + }, "devDependencies": { "@types/mime-types": "^2.1.4", "@types/node": "^18.18.10", @@ -38,6 +41,7 @@ "chalk": "^5.1.2", "dotenv": "^16.4.5", "mime-types": "^2.1.35", + "ora": "^8.0.1", "pino": "^9.3.1", "pino-pretty": "^11.2.1" }, diff --git a/packages/cli/src/cmds/report.ts b/packages/cli/src/cmds/report.ts index e5045cb..5ae5bca 100644 --- a/packages/cli/src/cmds/report.ts +++ b/packages/cli/src/cmds/report.ts @@ -1,4 +1,5 @@ -import { syncReport, SyncReportArguments } from "../sync-report/handler.js"; +import { SyncReportArguments } from "sync-report/interfaces.js"; +import { syncReport } from "../sync-report/handler.js"; import { Argv } from "yargs"; export default { diff --git a/packages/cli/src/common/output.ts b/packages/cli/src/common/output.ts index 83ea2bf..a81f3c0 100644 --- a/packages/cli/src/common/output.ts +++ b/packages/cli/src/common/output.ts @@ -41,3 +41,29 @@ export async function printTableToConsoleAndExitGracefully(table: any) { printTableToConsole(table); process.exit(0); } + +export function printScoreResult( + message: string, + score: number, + options?: { + overrideGreen?: number; + overrideYellow?: number; + overrideRed?: number; + }, +) { + const greenScore = + options && options.overrideGreen ? options.overrideGreen : 80; + const yellowScore = + options && options.overrideYellow ? options.overrideYellow : 60; + const redScore = options && options.overrideRed ? options.overrideRed : 59; + + if (score >= greenScore) { + console.log(`${message} ${chalk.bold.green(score)}`); + } else if (score >= yellowScore && score < greenScore) { + console.log(`${message} ${chalk.bold.yellow(score)}`); + } else if (score <= redScore) { + console.log(`${message} ${chalk.bold.red(score)}`); + } else { + console.log(`${message} ${score}`); + } +} diff --git a/packages/cli/src/sync-report/handler.ts b/packages/cli/src/sync-report/handler.ts index 388f6b3..1c7f80f 100644 --- a/packages/cli/src/sync-report/handler.ts +++ b/packages/cli/src/sync-report/handler.ts @@ -1,29 +1,49 @@ import { printCriticalFailureToConsoleAndExit, printDiagnosticsToConsole, - printResultToConsole, + printResultToConsoleAndExitGracefully, + printScoreResult, } from "../common/output.js"; import { existsSync } from "node:fs"; import { join, relative, resolve } from "node:path"; import { ApiError } from "@zuplo/errors"; import { readFile } from "node:fs/promises"; import { lookup } from "mime-types"; +import ora from "ora"; +import chalk from "chalk"; +import { APIResponse, SyncReportArguments } from "./interfaces.js"; -export interface SyncReportArguments { - dir: string; - "api-key": string; - filename: string; -} +const okMark = "\x1b[32m✔\x1b[0m"; +const failMark = "\x1b[31m✖\x1b[0m"; export async function syncReport(argv: SyncReportArguments) { + printDiagnosticsToConsole(`Rate Open API file ${argv.filename}`); + printDiagnosticsToConsole(`Press Ctrl+C to cancel.\n`); + const spinner = ora("Loading file for processing").start(); + + process.on("SIGTERM", () => { + spinner.stop(); + printResultToConsoleAndExitGracefully("\nProcess has been canceled\n"); + }); + process.on("SIGINT", () => { + spinner.stop(); + printResultToConsoleAndExitGracefully("\nProcess has been canceled\n"); + }); + + // @TODO - remove this in favor of bin configs + process.env.NODE_NO_WARNINGS = "1"; + process.removeAllListeners("warning"); + const sourceDirectory = resolve(join(relative(process.cwd(), argv.dir))); const openApiFilePath = join(sourceDirectory, argv.filename); if (!existsSync(openApiFilePath)) { + spinner.stopAndPersist({ symbol: failMark }); printCriticalFailureToConsoleAndExit( `The Open API file path provided does not exist: ${argv.filename}. Please specify an existing Open API file and try again.`, ); } + spinner.stopAndPersist({ symbol: okMark }); // Read the file as a buffer const data = await readFile(openApiFilePath, "utf-8"); @@ -36,7 +56,8 @@ export async function syncReport(argv: SyncReportArguments) { const formData = new FormData(); formData.set("apiFile", file, argv.filename); - printDiagnosticsToConsole(`Processing file ${argv.filename}`); + spinner.start(); + spinner.text = "Analizing file"; try { const fileUploadResults = await fetch( @@ -51,16 +72,34 @@ export async function syncReport(argv: SyncReportArguments) { ); if (fileUploadResults.status !== 200) { + spinner.fail("Analizing file\n"); const error = (await fileUploadResults.json()) as ApiError; printCriticalFailureToConsoleAndExit(`${error.detail ?? error.message}`); } else { - // @TODO - show a nice table - const res = await fileUploadResults.json(); - printResultToConsole( - `File upload success: ${JSON.stringify(res, null, 2)}`, + spinner.succeed("Analizing file\n"); + const res = (await fileUploadResults.json()) as APIResponse; + + const simpleJSON = { + ...res.results.simpleReport, + ...{ reportId: res.reportId, reportUrl: res.reportUrl }, + }; + + // @TODO support more output modes e.g. JSON, GH Actions, etc + + console.log(`${chalk.bold.blue("==>")} ${chalk.bold("Results")}\n`); + printScoreResult("Overall", simpleJSON.score); + console.log("======"); + printScoreResult("- Docs", simpleJSON.docsScore); + printScoreResult("- Completeness", simpleJSON.completenessScore); + printScoreResult("- SDK Generation", simpleJSON.sdkGenerationScore); + printScoreResult("- Security", simpleJSON.securityScore); + console.log("======\n"); + console.log( + `View details of your report at ${chalk.magenta(simpleJSON.reportUrl)}\n`, ); } } catch (err) { + spinner.fail("Analizing file\n"); // @TODO - show a nice useful error printCriticalFailureToConsoleAndExit( `Error on file upload: ${JSON.stringify(err)}`, diff --git a/packages/cli/src/sync-report/interfaces.ts b/packages/cli/src/sync-report/interfaces.ts new file mode 100644 index 0000000..2fbdf80 --- /dev/null +++ b/packages/cli/src/sync-report/interfaces.ts @@ -0,0 +1,43 @@ +export interface SyncReportArguments { + dir: string; + "api-key": string; + filename: string; +} + +export interface APIResponse { + results: { + simpleReport: { + version: string; + title: string; + fileExtension: "json" | "yaml"; + docsScore: number; + completenessScore: number; + score: number; + securityScore: number; + sdkGenerationScore: number; + shortSummary: string; + longSummary: string; + }; + }; + fullReport: { + issues: { + code: string; + message: string; + path: string[]; + severity: number; + source: string; + range: { + start: { + line: number; + character: number; + }; + end: { + line: number; + character: number; + }; + }; + }[]; + }; + reportId: string; + reportUrl: string; +}