Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default [
ecmaVersion: 2021,
sourceType: "module",
globals: {
process: "readonly",
NodeJS: true,
},
},
Expand All @@ -27,4 +28,4 @@ export default [
"node_modules"
]
},
];
];
File renamed without changes.
63 changes: 63 additions & 0 deletions src/actions/portal/copilot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ApiService } from "../../infrastructure/services/api-service.js";
import { PortalCopilotPrompts } from "../../prompts/portal/copilot.js";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { ActionResult } from "../action-result.js";
import { SubscriptionInfo } from "../../types/api/account.js";
import { BuildContext } from "../../types/build-context.js";

export class CopilotAction {
private readonly apiService = new ApiService();
private readonly prompts = new PortalCopilotPrompts();
private readonly configDir: DirectoryPath;
private readonly authKey: string | null;

constructor(configDir: DirectoryPath, authKey: string | null = null) {
this.configDir = configDir;
this.authKey = authKey;
}

public async execute(buildDirectory: DirectoryPath, welcomeMessage: string, enable: boolean): Promise<ActionResult> {
const buildContext = new BuildContext(buildDirectory);

if (!(await buildContext.validate())) {
return ActionResult.error("build directory is empty or not valid.");
}

const buildJson = await buildContext.getBuildFileContents();

if (buildJson.apiCopilotConfig != null && !(await this.prompts.confirmOverwrite()))
return ActionResult.error("Exiting without making any change.");

const response = await this.apiService.getAccountInfo(this.configDir, this.authKey);
if (!response.isSuccess()) {
return ActionResult.error(response.error!);
}
const apiCopilotKey = await this.selectCopilotKey(response.value);
if (apiCopilotKey === null) {
return ActionResult.error("No copilot key found for the current subscription. Please contact support at [email protected].");
}

buildJson.apiCopilotConfig = {
isEnabled: enable,
key: apiCopilotKey,
welcomeMessage: welcomeMessage
};

await buildContext.updateBuildFileContents(buildJson);

this.prompts.copilotConfigured(apiCopilotKey);
return ActionResult.success();
}

private async selectCopilotKey(subscription: SubscriptionInfo | undefined): Promise<string | null> {
if (
subscription === undefined ||
subscription.ApiCopilotKeys === undefined ||
subscription.ApiCopilotKeys.length === 0
) {
return null;
}

return await this.prompts.selectCopilotKey(subscription.ApiCopilotKeys);
}
}
89 changes: 89 additions & 0 deletions src/actions/portal/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { PortalGeneratePrompts } from "../../prompts/portal/generate.js";
import { ZipService } from "../../infrastructure/zip-service.js";
import { FileService } from "../../infrastructure/file-service.js";
import { PortalService } from "../../infrastructure/services/portal-service.js";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { FilePath } from "../../types/file/filePath.js";
import { FileName } from "../../types/file/fileName.js";
import { ActionResult } from "../action-result.js";
import { BuildContext } from "../../types/build-context.js";
import { PortalContext } from "../../types/portal-context.js";
import { withDirPath } from "../../infrastructure/tmp-extensions.js";


export class GenerateAction {
private readonly prompts: PortalGeneratePrompts = new PortalGeneratePrompts();
private readonly zipArchiver: ZipService = new ZipService();
private readonly fileService: FileService = new FileService();
private readonly portalService: PortalService = new PortalService();
private readonly configDir: DirectoryPath;
private readonly authKey: string | null;

constructor(configDir: DirectoryPath, authKey: string | null = null) {
this.configDir = configDir;
this.authKey = authKey;
}

public readonly execute = async (
buildDirectory: DirectoryPath,
portalDirectory: DirectoryPath,
force: boolean,
zipPortal: boolean
): Promise<ActionResult> => {

if (buildDirectory.isEqual(portalDirectory)) {
return ActionResult.error("build directory and portal directory cannot be the same.");
}

const buildContext = new BuildContext(buildDirectory);
if (!await buildContext.validate()) {
return ActionResult.error("build directory is empty or not valid");
}

const portalContext = new PortalContext(portalDirectory);
if (!force && (await portalContext.exists()) && !(await this.prompts.overwritePortal(portalDirectory))) {
return ActionResult.error(
"Please enter a different destination folder or remove the existing files and try again."
);
}

return await withDirPath(async (tempDirectory) => {
this.prompts.displayPortalGenerationMessage();

const buildZipPath = new FilePath(tempDirectory, new FileName("build.zip"));
await this.zipArchiver.archive(buildDirectory, buildZipPath);

const response = await this.portalService.generatePortal(buildZipPath, this.configDir, this.authKey);

if (!response.isSuccess()) {
this.prompts.displayPortalGenerationErrorMessage();
return ActionResult.error(await this.parseError(response.error!, portalDirectory, tempDirectory));
}

this.prompts.displayPortalGenerationSuccessMessage();

const tempPortalFilePath = new FilePath(tempDirectory, new FileName("portal.zip"));
await this.fileService.writeFile(tempPortalFilePath, <NodeJS.ReadableStream>response.value);

await portalContext.save(tempPortalFilePath, zipPortal);

return ActionResult.success();
});
}

private async parseError(error: string | NodeJS.ReadableStream, portalDirectory: DirectoryPath, tempDirectory: DirectoryPath): Promise<string> {
if (typeof error === 'string') {
return error;
}

const tempErrorFilePath = new FilePath(tempDirectory, new FileName("error.zip"));
await this.fileService.writeFile(tempErrorFilePath, <NodeJS.ReadableStream>error);

await this.fileService.cleanDirectory(portalDirectory);
await this.zipArchiver.unArchive(tempErrorFilePath, portalDirectory);

const errorReportPath = portalDirectory.join("apimatic-debug");
return "An error occurred during portal generation due to an issue with the input. An error report has been written at the destination path: " +
errorReportPath;
}
}
102 changes: 0 additions & 102 deletions src/actions/portal/generatePortalAction.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/actions/portal/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ServeHandler } from "../../application/portal/serve/serve-handler.js";
import { Result } from "../../types/common/result.js";
import { PortalService } from "../../infrastructure/services/portal-service.js";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { ActionResult } from "../actionResult.js";
import { ActionResult } from "../action-result.js";

export class PortalServeAction {
protected readonly prompts: PortalServePrompts;
Expand Down
2 changes: 1 addition & 1 deletion src/application/portal/serve/portal-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Mutex } from "async-mutex";
import { ServePaths } from "../../../types/portal/serve.js";
import { WatcherHandler } from "./watcher-handler.js";
import { DirectoryPath } from "../../../types/file/directoryPath.js";
import { ActionResult } from "../../../actions/actionResult.js";
import { ActionResult } from "../../../actions/action-result.js";

export class PortalWatcher {
public async watchAndRegeneratePortalOnChange(
Expand Down
2 changes: 1 addition & 1 deletion src/application/portal/serve/serve-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Result } from "../../../types/common/result.js";
import { ServeFlags, ServePaths } from "../../../types/portal/serve.js";
import { PortalWatcher } from "./portal-watcher.js";
import { DirectoryPath } from "../../../types/file/directoryPath.js";
import { ActionResult } from "../../../actions/actionResult.js";
import { ActionResult } from "../../../actions/action-result.js";

export class ServeHandler {
private server!: Server;
Expand Down
44 changes: 44 additions & 0 deletions src/commands/portal/copilot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Command, Flags } from "@oclif/core";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { PortalCopilotPrompts } from "../../prompts/portal/copilot.js";
import { FlagsProvider } from "../../types/flags-provider.js";
import { CopilotAction } from "../../actions/portal/copilot.js";

const DEFAULT_WORKING_DIRECTORY = "./";

export default class PortalCopilotEnable extends Command {
static description = "adds the API Copilot configuration in APIMATIC-BUILD.json";

static flags = {
...FlagsProvider.folder,
"welcome-message": Flags.string({
char: "m",
default: "",
description: "welcome message for the API copilot"
}),
disable : Flags.boolean({
default: false,
description: "marks the API Copilot as disabled in the configuration"
}),
...FlagsProvider["auth-key"]
};

static examples = [
`$ apimatic portal:copilot --folder="./portal/" --welcome-message="Welcome to our API!"`,
`$ apimatic portal:copilot --folder="./portal/"`
];

private readonly prompts = new PortalCopilotPrompts();

async run(): Promise<void> {
const {
flags: { folder, "auth-key": authKey, disable, 'welcome-message': welcomeMessage }
} = await this.parse(PortalCopilotEnable);

const workingDirectory = new DirectoryPath(folder ?? DEFAULT_WORKING_DIRECTORY);
const buildDirectory = folder ? new DirectoryPath(folder, "build") : workingDirectory.join("build");
const copilotConfigAction = new CopilotAction(new DirectoryPath(this.config.configDir), authKey);
const result = await copilotConfigAction.execute(buildDirectory, welcomeMessage, !disable);
result.map((message) => this.prompts.logError(message));
}
}
24 changes: 7 additions & 17 deletions src/commands/portal/generate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Command, Config, Flags } from "@oclif/core";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { GeneratePortalAction } from "../../actions/portal/generatePortalAction.js";
import { GenerateAction } from "../../actions/portal/generate.js";
import { PortalGeneratePrompts } from "../../prompts/portal/generate.js";
import { FlagsProvider } from "../../types/flags-provider.js";

const DEFAULT_WORKING_DIRECTORY = "./";

Expand All @@ -10,25 +11,14 @@ export class PortalGenerate extends Command {
"Generate and download a static API Documentation portal. Requires an input directory containing API specifications, a config file and optionally, markdown guides. For details, refer to the [documentation](https://docs.apimatic.io/platform-api/#/http/guides/generating-on-prem-api-portal/build-file-reference)";

static flags = {
folder: Flags.string({
description:
"[default: ./] path to the parent directory containing the 'build' folder, which includes API specifications and configuration files."
}),
destination: Flags.string({
description: "[default: <folder>/portal] path where the portal will be generated."
}),
force: Flags.boolean({
char: "f",
default: false,
description: "overwrite if a portal exists in the destination"
}),
...FlagsProvider.folder,
...FlagsProvider.destination,
...FlagsProvider.force,
zip: Flags.boolean({
default: false,
description: "download the generated portal as a .zip archive"
}),
"auth-key": Flags.string({
description: "override current authentication state with an authentication key"
})
...FlagsProvider["auth-key"]
};

static examples = [`$ apimatic portal:generate`, `$ apimatic portal:generate --folder="./" --destination="./portal"`];
Expand All @@ -49,7 +39,7 @@ export class PortalGenerate extends Command {
const buildDirectory = folder ? new DirectoryPath(folder, "build") : workingDirectory.join("build");
const portalDirectory = destination ? new DirectoryPath(destination) : workingDirectory.join("portal");

const action = new GeneratePortalAction(this.getConfigDir(), authKey);
const action = new GenerateAction(this.getConfigDir(), authKey);
const result = await action.execute(buildDirectory, portalDirectory, force, zipPortal);
result.mapAll(
() => this.prompts.displayOutroMessage(portalDirectory.toString()),
Expand Down
Loading