diff --git a/package-lock.json b/package-lock.json index 42177d56..4d16e907 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@apimatic/cli", - "version": "1.1.0-alpha.10", + "version": "1.1.0-alpha.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apimatic/cli", - "version": "1.1.0-alpha.10", + "version": "1.1.0-alpha.13", "license": "MIT", "dependencies": { - "@apimatic/sdk": "^0.2.0-alpha.1", + "@apimatic/sdk": "^0.2.0-alpha.2", "@clack/prompts": "1.0.0-alpha.1", "@oclif/core": "^4.2.8", "@oclif/plugin-autocomplete": "^3.2.24", @@ -34,6 +34,7 @@ "treeify": "^1.1.0", "tslib": "^2.8.1", "unzipper": "^0.12.3", + "which": "^5.0.0", "yaml": "^2.8.0" }, "bin": { @@ -58,6 +59,7 @@ "@types/sinon": "^17.0.4", "@types/treeify": "^1.0.3", "@types/unzipper": "^0.10.4", + "@types/which": "^3.0.4", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "chai": "^4.5.0", @@ -250,9 +252,9 @@ } }, "node_modules/@apimatic/sdk": { - "version": "0.2.0-alpha.1", - "resolved": "https://registry.npmjs.org/@apimatic/sdk/-/sdk-0.2.0-alpha.1.tgz", - "integrity": "sha512-PDO6vN+DhiWcoUmDzND8Fwro/CNAg+Jbr2jBqXbGssVDw4P2BikKRqnhZoZPlPNYwEUaUr7/SIQwsP7Qx/1zMQ==", + "version": "0.2.0-alpha.2", + "resolved": "https://registry.npmjs.org/@apimatic/sdk/-/sdk-0.2.0-alpha.2.tgz", + "integrity": "sha512-Ii5UQGMuChXYzqJXKlEujqelJ0JSRVVk68Zgqnt0aZJZRClr0vcg8s9Yb7oeZjvEJNJod1uRqTak73pK3JrJfg==", "license": "MIT", "dependencies": { "@apimatic/authentication-adapters": "^0.5.4", @@ -5493,6 +5495,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/which": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.4.tgz", + "integrity": "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.14", "dev": true, @@ -7227,6 +7236,27 @@ "node": ">= 8" } }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dargs": { "version": "7.0.0", "dev": true, @@ -9838,8 +9868,13 @@ "license": "MIT" }, "node_modules/isexe": { - "version": "2.0.0", - "license": "ISC" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -12778,6 +12813,29 @@ "node": ">=8.0.0" } }, + "node_modules/spawn-wrap/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/spawn-wrap/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "dev": true, @@ -13782,16 +13840,18 @@ } }, "node_modules/which": { - "version": "2.0.2", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { - "node-which": "bin/node-which" + "node-which": "bin/which.js" }, "engines": { - "node": ">= 8" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/which-boxed-primitive": { diff --git a/package.json b/package.json index ee3f3332..1fde49ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@apimatic/cli", "description": "The official CLI for APIMatic.", - "version": "1.1.0-alpha.10", + "version": "1.1.0-alpha.13", "author": "APIMatic", "bin": { "apimatic": "./bin/run.js" @@ -44,7 +44,7 @@ "test": "tsx node_modules/mocha/bin/_mocha --forbid-only \"test/**/*.test.ts\" --timeout 99999" }, "dependencies": { - "@apimatic/sdk": "^0.2.0-alpha.1", + "@apimatic/sdk": "^0.2.0-alpha.2", "@clack/prompts": "1.0.0-alpha.1", "@oclif/core": "^4.2.8", "@oclif/plugin-autocomplete": "^3.2.24", @@ -69,6 +69,7 @@ "treeify": "^1.1.0", "tslib": "^2.8.1", "unzipper": "^0.12.3", + "which": "^5.0.0", "yaml": "^2.8.0" }, "devDependencies": { @@ -90,6 +91,7 @@ "@types/sinon": "^17.0.4", "@types/treeify": "^1.0.3", "@types/unzipper": "^0.10.4", + "@types/which": "^3.0.4", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "chai": "^4.5.0", diff --git a/src/actions/portal/recipe/new-recipe.ts b/src/actions/portal/recipe/new-recipe.ts index 59a7e1cd..94287fe7 100644 --- a/src/actions/portal/recipe/new-recipe.ts +++ b/src/actions/portal/recipe/new-recipe.ts @@ -1,6 +1,7 @@ import * as path from "path"; import fs from "fs"; import fsExtra from "fs-extra"; +import which from "which"; import { parse } from "yaml"; import { TreeObject } from "treeify"; import { tmpdir } from "os"; @@ -175,22 +176,24 @@ export class PortalRecipeAction { stepName: string ): Promise> { this.prompts.displayContentStepInfo(); - this.prompts.startProgressIndicatorWithMessage("Waiting for you to close the text editor"); let editor = process.env.EDITOR; let editorArgs: string[] = []; + const tempFilePath = path.join(tmpdir(), `recipe-markdown-content-${Date.now()}.md`); + const template = `# The Heading Goes Here\n\nThis is placeholder text for your API Recipe content step. Feel free to edit this. Save your changes and then close the file once you're done.`; + await fsExtra.writeFile(tempFilePath, template); try { - const tempFilePath = path.join(tmpdir(), `recipe-markdown-content-${Date.now()}.md`); - const template = `# The Heading Goes Here\n\nThis is placeholder text for your API Recipe content step. Feel free to edit this. Save your changes and then close the file once you're done.`; - - await fsExtra.writeFile(tempFilePath, template); - if (!editor) { if (process.platform === "win32") { await execa("cmd", ["/c", "start", "/wait", "notepad", tempFilePath], { stdio: "ignore" }); - } else { - editor = "nano"; - await execa(editor, [tempFilePath], { stdio: "ignore" }); + } else if (process.platform === "darwin" || process.platform === "linux") { + editor = "vim"; + try { + await execa(editor, [tempFilePath], { stdio: "inherit" }); + } + catch (error) { + // User exiting vim can throw a non-zero exit code leading to exception, ignore it. + } } } else { if (editor === "code" || editor.endsWith("code.cmd") || editor.endsWith("code.exe")) { @@ -200,17 +203,17 @@ export class PortalRecipeAction { await execa(editor, editorArgs, { stdio: "ignore" }); } - this.prompts.stopProgressIndicatorWithMessage("✅ Text editor closed."); const fileContent = await fsExtra.readFile(tempFilePath, "utf-8"); - - await fsExtra.unlink(tempFilePath); - recipe.addContentStep(stepName, stepName, fileContent); + this.prompts.displayStepAddedSuccessfullyMessage(); return Result.success("Added content step successfully."); } catch (error) { return Result.failure(`Unable to add content step. Please try again later.`); } + finally { + await fsExtra.unlink(tempFilePath); + } } private async promptUserAndAddEndpointStepToRecipe( diff --git a/src/application/portal/recipe/recipe-generator.ts b/src/application/portal/recipe/recipe-generator.ts index c193fc51..4edf57c5 100644 --- a/src/application/portal/recipe/recipe-generator.ts +++ b/src/application/portal/recipe/recipe-generator.ts @@ -80,7 +80,7 @@ export class PortalRecipeGenerator { recipesConfig.workflows = []; } const existingIndex = recipesConfig.workflows.findIndex( - (workflow: any) => workflow.name === recipeName + (workflow: any) => workflow.permalink === `page:recipes/${recipeFileName}` ); const newWorkflow = { diff --git a/src/commands/api/validate.ts b/src/commands/api/validate.ts index 21a820e1..524928b6 100644 --- a/src/commands/api/validate.ts +++ b/src/commands/api/validate.ts @@ -1,7 +1,7 @@ import fsExtra from "fs-extra"; import { ux, Flags, Command } from "@oclif/core"; -import { ApiError, ApiValidationExternalApIsController, ApiValidationSummary, Client } from "@apimatic/sdk"; +import { ApiError, ApiValidationExternalApisController, ApiValidationSummary, Client } from "@apimatic/sdk"; import { AuthenticationError, loggers } from "../../types/utils.js"; import { SDKClient } from "../../client-utils/sdk-client.js"; @@ -42,7 +42,7 @@ Specification file provided is valid const overrideAuthKey = flags["auth-key"] ? flags["auth-key"] : null; const client: Client = await SDKClient.getInstance().getClient(overrideAuthKey, this.config.configDir); - const apiValidationController: ApiValidationExternalApIsController = new ApiValidationExternalApIsController( + const apiValidationController: ApiValidationExternalApisController = new ApiValidationExternalApisController( client ); diff --git a/src/commands/portal/quickstart.ts b/src/commands/portal/quickstart.ts index 7918b2c1..cdbfceed 100644 --- a/src/commands/portal/quickstart.ts +++ b/src/commands/portal/quickstart.ts @@ -1,5 +1,5 @@ import { Command } from "@oclif/core"; -import { ApiValidationExternalApIsController, ApiValidationSummary, Client } from "@apimatic/sdk"; +import { ApiValidationExternalApisController, ApiValidationSummary, Client } from "@apimatic/sdk"; import { SDKClient } from "../../client-utils/sdk-client.js"; import { PortalQuickstartPrompts } from "../../prompts/portal/quickstart.js"; import { PortalQuickstartController } from "../../controllers/portal/quickstart.js"; @@ -28,7 +28,7 @@ export default class PortalQuickstart extends Command { prompts: PortalQuickstartPrompts, controller: PortalQuickstartController, specFile: SpecFile, - apiValidationController: ApiValidationExternalApIsController + apiValidationController: ApiValidationExternalApisController ): Promise { const apiValidationSummary = await controller.getSpecValidationSummary(prompts, specFile, apiValidationController); @@ -99,7 +99,7 @@ export default class PortalQuickstart extends Command { } const client: Client = await SDKClient.getInstance().getClient(null, this.config.configDir); - const apiValidationController: ApiValidationExternalApIsController = new ApiValidationExternalApIsController( + const apiValidationController: ApiValidationExternalApisController = new ApiValidationExternalApisController( client ); diff --git a/src/commands/sdk/generate.ts b/src/commands/sdk/generate.ts index ecee9ebe..1cbde0ab 100644 --- a/src/commands/sdk/generate.ts +++ b/src/commands/sdk/generate.ts @@ -3,7 +3,7 @@ import fsExtra from "fs-extra"; import { Command, Flags } from "@oclif/core"; import { SDKClient } from "../../client-utils/sdk-client.js"; -import { ApiError, Client, CodeGenerationExternalApIsController } from "@apimatic/sdk"; +import { ApiError, Client, CodeGenerationExternalApisController } from "@apimatic/sdk"; import { replaceHTML, isJSONParsable, getFileNameFromPath } from "../../utils/utils.js"; import { getSDKGenerationId, downloadGeneratedSDK } from "../../controllers/sdk/generate.js"; @@ -83,7 +83,7 @@ Success! Your SDK is located at swagger_sdk_csharp const overrideAuthKey = flags["auth-key"] ? flags["auth-key"] : null; const client: Client = await SDKClient.getInstance().getClient(overrideAuthKey, this.config.configDir); - const sdkGenerationController: CodeGenerationExternalApIsController = new CodeGenerationExternalApIsController( + const sdkGenerationController: CodeGenerationExternalApisController = new CodeGenerationExternalApisController( client ); diff --git a/src/controllers/api/validate.ts b/src/controllers/api/validate.ts index 2b8544cf..6351ed06 100644 --- a/src/controllers/api/validate.ts +++ b/src/controllers/api/validate.ts @@ -1,11 +1,11 @@ import fsExtra from "fs-extra"; -import { ApiResponse, ApiValidationExternalApIsController, ApiValidationSummary, ContentType, FileWrapper } from "@apimatic/sdk"; +import { ApiResponse, ApiValidationExternalApisController, ApiValidationSummary, ContentType, FileWrapper } from "@apimatic/sdk"; import { GetValidationParams } from "../../types/api/validate.js"; import { createTempDirectory, deleteFile, zipDirectory } from "../../utils/utils.js"; export const getValidationSummary = async ( { file, url }: GetValidationParams, - apiValidationController: ApiValidationExternalApIsController + apiValidationController: ApiValidationExternalApisController ): Promise => { let validation: ApiResponse; diff --git a/src/controllers/portal/quickstart.ts b/src/controllers/portal/quickstart.ts index a49538ae..26926da6 100644 --- a/src/controllers/portal/quickstart.ts +++ b/src/controllers/portal/quickstart.ts @@ -6,7 +6,7 @@ import fs from "fs"; import fsExtra from "fs-extra"; import { readdir } from "fs/promises"; import { getAuthInfo } from "../../client-utils/auth-manager.js"; -import { ApiError, ApiValidationExternalApIsController, ApiValidationSummary } from "@apimatic/sdk"; +import { ApiError, ApiValidationExternalApisController, ApiValidationSummary } from "@apimatic/sdk"; import { LoginCredentials, SpecFile } from "../../types/portal/quickstart.js"; import { SDKClient } from "../../client-utils/sdk-client.js"; import { @@ -28,7 +28,7 @@ import { AuthenticationError } from "../../types/utils.js"; export class PortalQuickstartController { private readonly specUrl = - "https://github.com/apimatic/static-portal-workflow/blob/master/spec/Apimatic-Calculator.json"; + "https://raw.githubusercontent.com/apimatic/static-portal-workflow/refs/heads/master/spec/Apimatic-Calculator.json"; async isUserAuthenticated(configDir: string): Promise { const storedAuth = await getAuthInfo(configDir); @@ -43,7 +43,6 @@ export class PortalQuickstartController { } async getSpecFile(spec: string): Promise { - let filePath = ""; const tempSpecDir = await createTempDirectory(); if (spec) { @@ -63,7 +62,7 @@ export class PortalQuickstartController { const specFile = await axios.get(specPath, { responseType: "arraybuffer" }); const fileName = path.basename(specPath); - filePath = path.join(tempSpecDir, fileName); + const filePath = path.join(tempSpecDir, fileName); await fsExtra.writeFile(filePath, specFile.data); } catch (error) { if (axios.isAxiosError(error)) { @@ -115,7 +114,6 @@ export class PortalQuickstartController { } else { specPath = path.normalize(specPath); const fileType = await filetype.fromFile(specPath); - filePath = tempSpecDir; if (fileType?.ext === "zip") { await unzipFile(fs.createReadStream(specPath), tempSpecDir); @@ -126,13 +124,13 @@ export class PortalQuickstartController { } } - return { localPath: filePath, url: this.specUrl }; + return { localPath: tempSpecDir, url: this.specUrl }; } async getSpecValidationSummary( prompts: PortalQuickstartPrompts, specFile: SpecFile, - apiValidationController: ApiValidationExternalApIsController + apiValidationController: ApiValidationExternalApisController ): Promise { const validationFlags: GetValidationParams = { file: specFile.localPath, @@ -222,6 +220,8 @@ export class PortalQuickstartController { } }); + fsExtra.emptyDirSync(targetFolder); + try { await git.clone(staticPortalRepoUrl, targetFolder); } catch (error) { diff --git a/src/controllers/portal/serve.ts b/src/controllers/portal/serve.ts index 657db34d..e4026f27 100644 --- a/src/controllers/portal/serve.ts +++ b/src/controllers/portal/serve.ts @@ -54,7 +54,7 @@ export const watchAndRegeneratePortal = async ( ignoredPaths: string[] = [] ) => { // Convert ignoredPaths to absolute paths for consistent comparison - const generatedFilesPaths = getGeneratedFilesPaths(sourceDir, portalDir); + const generatedFilesPaths = getGeneratedFilesPaths(sourceDir, portalDir); const absoluteIgnoredPaths = [ ...ignoredPaths.filter((ignoredPath) => ignoredPath.trim() !== ""), ...generatedFilesPaths @@ -144,7 +144,11 @@ async function handleFileChange( ) ); } else if (error.response.status === 403) { - console.error(getMessageInRedColor(`Access denied. It looks like you don't have access to APIMatic's Docs as Code offering. Check your subscription details and contact our team at support@apimatic.io if you believe this is a mistake.`)); + console.error( + getMessageInRedColor( + `Access denied. It looks like you don't have access to APIMatic's Docs as Code offering. Check your subscription details and contact our team at support@apimatic.io if you believe this is a mistake.` + ) + ); } else if (error.response.status === 422) { console.error( getMessageInRedColor( @@ -153,7 +157,9 @@ async function handleFileChange( ); } else if (error.response.status === 500) { console.error( - getMessageInRedColor(`Failed to regenerate the portal. Please ensure that the provided build directory follows the correct structure and contains valid API definition and build files. If the issue persists, reach out to our team at support@apimatic.io`) + getMessageInRedColor( + `Failed to regenerate the portal. Please ensure that the provided build directory follows the correct structure and contains valid API definition and build files. If the issue persists, reach out to our team at support@apimatic.io` + ) ); } else { console.error( @@ -172,11 +178,7 @@ async function handleFileChange( } else if (error.code === "ENOTFOUND" || error.code === "ERR_NETWORK") { throw new Error(getMessageInRedColor(`Network error. Please check your internet connection and try again.`)); } else { - console.error( - getMessageInRedColor( - `No response received from the server. Please try again later.` - ) - ); + console.error(getMessageInRedColor(`No response received from the server. Please try again later.`)); } } else { console.error(getMessageInRedColor(`Failed to regenerate portal: ${error.message}`)); diff --git a/src/controllers/sdk/generate.ts b/src/controllers/sdk/generate.ts index a1506de7..43067f7d 100644 --- a/src/controllers/sdk/generate.ts +++ b/src/controllers/sdk/generate.ts @@ -2,7 +2,7 @@ import fsExtra from "fs-extra"; import { ux } from "@oclif/core"; import { ApiResponse, - CodeGenerationExternalApIsController, + CodeGenerationExternalApisController, UserCodeGeneration, Platforms, GenerateSdkViaUrlRequest, @@ -13,7 +13,7 @@ import { unzipFile, writeFileUsingReadableStream } from "../../utils/utils.js"; export const getSDKGenerationId = async ( { file, url, platform }: GenerationIdParams, - sdkGenerationController: CodeGenerationExternalApIsController + sdkGenerationController: CodeGenerationExternalApisController ): Promise => { ux.action.start("Generating SDK"); @@ -51,7 +51,7 @@ const getSDKPlatform = (platform: string): Platforms | SimplePlatforms => { // Download Platform export const downloadGeneratedSDK = async ( { codeGenId, zippedSDKPath, sdkFolderPath, zip }: DownloadSDKParams, - sdkGenerationController: CodeGenerationExternalApIsController + sdkGenerationController: CodeGenerationExternalApisController ): Promise => { ux.action.start("Downloading SDK"); const { result }: ApiResponse = await sdkGenerationController.downloadSdk(codeGenId); diff --git a/src/infrastructure/services/portal-service.ts b/src/infrastructure/services/portal-service.ts index e7e554f4..7419b953 100644 --- a/src/infrastructure/services/portal-service.ts +++ b/src/infrastructure/services/portal-service.ts @@ -21,6 +21,7 @@ import { Result } from "../../types/common/result.js"; import { getMessageInRedColor, parseStreamBodyToJson, extractZipFile, deleteFile } from "../../utils/utils.js"; import { TransformationData } from "../../types/api/transform.js"; import { Sdl } from "../../types/sdl/sdl.js"; +import * as os from "os"; export class PortalService { private readonly CONTENT_TYPE = ContentType.EnumMultipartformdata; @@ -93,11 +94,20 @@ export class PortalService { return `X-Auth-Key ${key ?? ""}`; }; + private getUserAgent(): string { + const osInfo = `${os.platform()} ${os.release()}`; + const engine = "Node.js"; + const engineVersion = process.version; + + return `APIMATIC CLI - [OS: ${osInfo}, Engine: ${engine}/${engineVersion}]`; + } + private createApiClient = (authorizationHeader: string): Client => { return new Client({ customHeaderAuthenticationCredentials: { Authorization: authorizationHeader }, + userAgent: this.getUserAgent(), timeout: this.TIMEOUT }); }; diff --git a/src/prompts/portal/quickstart.ts b/src/prompts/portal/quickstart.ts index 6680688a..2fc8203c 100644 --- a/src/prompts/portal/quickstart.ts +++ b/src/prompts/portal/quickstart.ts @@ -92,7 +92,7 @@ export class PortalQuickstartPrompts { const spec = await text({ message: `Provide a local path or a public URL for your OpenAPI Definition file:`, placeholder: "Press Enter to use a sample OpenAPI file for APIMatic", - defaultValue: "", + defaultValue: "https://raw.githubusercontent.com/apimatic/static-portal-workflow/refs/heads/master/spec/Apimatic-Calculator.json", validate: (input) => { if (!input) return;