diff --git a/src/actions/portal/copilot.ts b/src/actions/portal/copilot.ts index b46d6406..36d2836e 100644 --- a/src/actions/portal/copilot.ts +++ b/src/actions/portal/copilot.ts @@ -21,7 +21,7 @@ export const DEFAULT_COPILOT_WELCOME_MESSAGE = "Ask me anything about this API or try one of these example prompts:\n" + "\n" + "- `What authentication methods does this API support?`\n" + - "- `What endpoints are available in this API?`\n" ; + "- `What is this API about?`\n" ; export class CopilotAction { private readonly apiService = new ApiService(); diff --git a/src/actions/portal/quickstart.ts b/src/actions/portal/quickstart.ts index 090366d1..dd519002 100644 --- a/src/actions/portal/quickstart.ts +++ b/src/actions/portal/quickstart.ts @@ -19,6 +19,7 @@ import { FeaturesToRemove, ValidationService } from '../../infrastructure/servic import { FileName } from '../../types/file/fileName.js'; import { ApiService } from '../../infrastructure/services/api-service.js'; import { DEFAULT_COPILOT_WELCOME_MESSAGE } from './copilot.js'; +import { mapLanguages } from '../../types/sdk/generate.js'; const defaultPort: number = 23513 as const; const defaultBaseUrl = new UrlPath(`http://localhost:${defaultPort}`); @@ -56,6 +57,22 @@ export class PortalQuickstartAction { } return await withDirPath(async (tempDirectory: DirectoryPath): Promise => { + // Fetch account info before anything else so the plan is known up front: it + // gates the on-prem generation exit below, feeds the language step the allowed + // SDK languages, and resolves the API Copilot key later. A lookup failure is fatal. + const accountInfo = await this.apiService.getAccountInfo(this.configDir, this.commandMetadata.shell, null); + if (accountInfo.isErr()) { + this.prompts.accountInfoFetchFailed(accountInfo.error); + return ActionResult.failed(); + } + // Quickstart generates the portal locally (on-prem); a plan that doesn't allow + // on-prem generation can't run it, so stop before importing or pruning a spec. + if (!accountInfo.value.isOnPremGenerationAllowed) { + this.prompts.onPremGenerationNotAllowedOnPlan(); + return ActionResult.cancelled(); + } + const allowedLanguages = mapLanguages(accountInfo.value.allowedLanguages); + // Step 1/4 this.prompts.importSpecStep(); @@ -131,10 +148,19 @@ export class PortalQuickstartAction { // Step 3/4 this.prompts.selectLanguagesStep(); - const languages = await this.prompts.selectLanguagesPrompt(); - if (!languages) { - this.prompts.noLanguagesSelected(); - return ActionResult.cancelled(); + let languages: string[]; + if (allowedLanguages.length === 0) { + // With no SDK languages on the plan there's nothing to select, so skip the + // menu and build the portal with HTTP documentation only. + this.prompts.httpOnlyPortalOnPlan(); + languages = ['http']; + } else { + const selectedLanguages = await this.prompts.selectLanguagesPrompt(allowedLanguages); + if (!selectedLanguages) { + this.prompts.noLanguagesSelected(); + return ActionResult.cancelled(); + } + languages = selectedLanguages; } // Step 4/4 @@ -161,15 +187,10 @@ export class PortalQuickstartAction { } // Resolve the API Copilot key to enable, if any, before setting up the source - // directory so the user decides on Copilot up front. The lookup failing is fatal; - // an account with no key continues silently (no Copilot); cancelling the multi-key - // selection aborts quickstart. - const accountInfo = await this.apiService.getAccountInfo(this.configDir, this.commandMetadata.shell, null); - if (accountInfo.isErr()) { - this.prompts.accountInfoFetchFailed(accountInfo.error); - return ActionResult.failed(); - } - + // directory. An account with no key continues silently (no Copilot); cancelling + // the multi-key selection aborts quickstart. (Account info was already fetched + // above for the language step.) Whether Copilot is actually on the plan is only + // known after the prune below, so the "enabled" caution is deferred until then. let copilotKey: string | undefined; const copilotKeys = accountInfo.value.ApiCopilotKeys ?? []; if (copilotKeys.length === 1) { @@ -181,9 +202,6 @@ export class PortalQuickstartAction { return ActionResult.cancelled(); } } - if (copilotKey) { - this.prompts.copilotEnabled(copilotKey); - } const masterBuildFile = await this.prompts.downloadBuildDirectory( this.fileDownloadService.downloadFile(this.buildFileUrl) @@ -214,6 +232,24 @@ export class PortalQuickstartAction { : baseConfig; await buildContext.updateBuildFileContents(buildConfig); + // Prune the build file to what the plan allows (SDK languages + AI features) + // before serving. Fail closed: a prune failure aborts rather than serving a + // build the plan can't generate. + const pruneResult = await this.validationService.pruneBuildFile(buildContext.buildConfigFilePath()); + if (pruneResult.isErr()) { + this.prompts.serviceError(pruneResult.error); + return ActionResult.failed(); + } + const { buildFile: prunedConfig, report } = pruneResult.value; + await buildContext.updateBuildFileContents(prunedConfig); + this.prompts.buildFilePruned(report); + + // Only surface the Copilot caution if Copilot survived the prune — i.e. it's + // actually on the plan. If it was stripped, buildFilePruned already reported it. + if (prunedConfig.hasApiCopilot() && copilotKey) { + this.prompts.copilotEnabled(copilotKey); + } + const specDirectory = sourceDirectory.join('spec'); const specContext = new SpecContext(specDirectory); await specContext.replaceDefaultSpec(specPath); diff --git a/src/actions/sdk/quickstart.ts b/src/actions/sdk/quickstart.ts index 037f7898..7628dd07 100644 --- a/src/actions/sdk/quickstart.ts +++ b/src/actions/sdk/quickstart.ts @@ -12,11 +12,12 @@ import { ValidateAction } from '../api/validate.js'; import { FileDownloadService } from '../../infrastructure/services/file-download-service.js'; import { FileService } from '../../infrastructure/file-service.js'; import { GenerateAction } from './generate.js'; -import { CodeGenerationVersion, Language, Stability } from '../../types/sdk/generate.js'; +import { CodeGenerationVersion, Language, mapLanguages, Stability } from '../../types/sdk/generate.js'; import { LauncherService } from '../../infrastructure/launcher-service.js'; import { ZipService } from '../../infrastructure/zip-service.js'; import { FileName } from '../../types/file/fileName.js'; import { FeaturesToRemove, ValidationService } from '../../infrastructure/services/validation-service.js'; +import { ApiService } from '../../infrastructure/services/api-service.js'; export class SdkQuickstartAction { private readonly prompts = new SdkQuickstartPrompts(); @@ -24,6 +25,7 @@ export class SdkQuickstartAction { private readonly fileService = new FileService(); private readonly launcherService = new LauncherService(); private readonly zipService = new ZipService(); + private readonly apiService = new ApiService(); private readonly validationService = new ValidationService(this.configDir); private readonly metadataFileUrl = new UrlPath( `https://raw.githubusercontent.com/apimatic/sample-docs-as-code-portal/refs/heads/master/src/spec/APIMATIC-META.json` @@ -44,6 +46,20 @@ export class SdkQuickstartAction { } return await withDirPath(async (tempDirectory: DirectoryPath): Promise => { + // Fetch account info before anything else so the plan is known up front: it + // gates the free-plan exit below and feeds the language step the allowed SDK + // languages. A lookup failure is fatal. + const accountInfo = await this.apiService.getAccountInfo(this.configDir, this.commandMetadata.shell, null); + if (accountInfo.isErr()) { + this.prompts.accountInfoFetchFailed(accountInfo.error); + return ActionResult.failed(); + } + // An SDK needs a language; with none on the plan (e.g. the free plan) there's + // nothing to generate, so stop before importing or pruning a spec. + if (mapLanguages(accountInfo.value.allowedLanguages).length === 0) { + this.prompts.noLanguagesAvailableOnPlan(); + return ActionResult.cancelled(); + } // Step 1/4 this.prompts.importSpecStep(); @@ -120,8 +136,7 @@ export class SdkQuickstartAction { // Step 3/4 this.prompts.selectLanguageStep(); - - const language = await this.prompts.selectLanguagePrompt(); + const language = await this.prompts.selectLanguagePrompt(mapLanguages(accountInfo.value.allowedLanguages)); if (!language) { this.prompts.noLanguageSelected(); return ActionResult.cancelled(); diff --git a/src/infrastructure/services/validation-service.ts b/src/infrastructure/services/validation-service.ts index 298b4f8f..8a20d753 100644 --- a/src/infrastructure/services/validation-service.ts +++ b/src/infrastructure/services/validation-service.ts @@ -14,12 +14,17 @@ import { AuthInfo, getAuthInfo } from "../../client-utils/auth-manager.js"; import { apiClientFactory } from "./api-client-factory.js"; import { err, ok, Result } from "neverthrow"; import { FilePath } from "../../types/file/filePath.js"; +import { FileName } from "../../types/file/fileName.js"; import { CommandMetadata } from "../../types/common/command-metadata.js"; import FormData from "form-data"; +import { ZipService } from "../zip-service.js"; +import { FileService } from "../file-service.js"; +import { withDirPath } from "../tmp-extensions.js"; import { handleServiceError, ServiceError } from "../service-error.js"; -import axios, { AxiosResponse } from "axios"; +import axios from "axios"; import { envInfo } from "../env-info.js"; import { Buffer } from "node:buffer"; +import { BuildConfig } from "../../types/build/build.js"; export enum RemovableFeature { Merging = 'Merging', @@ -53,8 +58,21 @@ export interface ValidateApiResponse { unallowedFeatures: UnallowedFeaturesResponse | null; } +export interface BuildFilePruneReport { + removedLanguages: string[]; + removedApiCopilot: boolean; + removedAiIntegration: boolean; +} + +export interface PruneBuildFileResponse { + buildFile: BuildConfig; + report: BuildFilePruneReport; +} + export class ValidationService { private readonly apiBaseUrl = "https://api.apimatic.io" as const; + private readonly zipService = new ZipService(); + private readonly fileService = new FileService(); constructor(private readonly configDir: DirectoryPath) {} @@ -132,6 +150,90 @@ export class ValidationService { } } + /** + * Prunes a build file down to what the user's subscription allows (SDK languages + + * AI features) via the platform, returning the pruned build file and a report of + * what was removed. The platform is the entitlement authority, so the build we + * submit for generation is never rejected for a build-file feature the plan lacks. + */ + public async pruneBuildFile( + buildConfigFilePath: FilePath, + authKey?: string | null + ): Promise> { + const authInfo: AuthInfo | null = await getAuthInfo(this.configDir.toString()); + const authorizationHeader = this.createAuthorizationHeader(authInfo, authKey ?? null); + + const formData = new FormData(); + formData.append("file", createReadStream(buildConfigFilePath.toString()), { + filename: "APIMATIC-BUILD.json", + contentType: "application/json" + }); + + const baseURL = envInfo.getBaseUrl() ?? this.apiBaseUrl; + + try { + const response = await axios({ + method: "POST", + url: `${baseURL}/build-features/prune`, + data: formData, + headers: { + ...formData.getHeaders(), + Authorization: authorizationHeader + }, + // The endpoint returns a zip (pruned APIMATIC-BUILD.json + report.json), + // streamed so it can be written straight to disk and unarchived. + responseType: "stream", + validateStatus: () => true + }); + + if (response.status >= 400) { + return err(await this.parsePruneErrorResponse(response)); + } + + // Persist the zip to a temp dir and extract it via ZipService, then read the + // two entries back off disk — no in-memory zip handling. + return await withDirPath>(async (tempDir) => { + const zipPath = new FilePath(tempDir, new FileName("prune-response.zip")); + await this.fileService.writeFile(zipPath, response.data); + + const extractDir = tempDir.join("prune"); + await this.zipService.unArchive(zipPath, extractDir); + + const prunedBuildConfigFile = new FilePath(extractDir, new FileName("APIMATIC-BUILD.json")); + const reportFile = new FilePath(extractDir, new FileName("report.json")); + if (!(await this.fileService.fileExists(prunedBuildConfigFile)) || !(await this.fileService.fileExists(reportFile))) { + return err(ServiceError.ServerError); + } + + const buildConfigFile = BuildConfig.parse(await this.fileService.getContents(prunedBuildConfigFile)); + const report = JSON.parse(await this.fileService.getContents(reportFile)) as BuildFilePruneReport; + return ok({ buildFile: buildConfigFile, report }); + }); + } catch (error: unknown) { + return err(handleServiceError(error)); + } + } + + /** Decodes a streamed error body into a ServiceError with a prune-specific fallback message. */ + private async parsePruneErrorResponse( + response: { status: number; data: AsyncIterable } + ): Promise { + const chunks: Buffer[] = []; + for await (const chunk of response.data) { + chunks.push(Buffer.from(chunk)); + } + const errorBody = Buffer.concat(chunks).toString("utf-8"); + + let message = `Error ${response.status}: Failed to prune the build file for your subscription.`; + try { + const body = JSON.parse(errorBody) as { errors?: { summary?: string[] }; message?: string; title?: string }; + message = body?.errors?.summary?.[0] ?? body?.message ?? body?.title ?? message; + } catch { + // Non-JSON / undecodable body — keep the default message. + } + return ServiceError.badRequest(message, {}); + } + private createAuthorizationHeader(authInfo: AuthInfo | null, overrideAuthKey: string | null): string { const key = overrideAuthKey || authInfo?.authKey; return `X-Auth-Key ${key ?? ""}`; @@ -158,7 +260,9 @@ export class ValidationService { return "Unexpected error occurred while validating API specification."; } - private async parseErrorResponse(response: AxiosResponse): Promise { + private async parseErrorResponse( + response: { status: number; data: AsyncIterable } + ): Promise { const chunks: Buffer[] = []; for await (const chunk of response.data) { chunks.push(Buffer.from(chunk)); diff --git a/src/prompts/portal/quickstart.ts b/src/prompts/portal/quickstart.ts index 7d46f41c..2f04cf63 100644 --- a/src/prompts/portal/quickstart.ts +++ b/src/prompts/portal/quickstart.ts @@ -1,5 +1,6 @@ import { Result } from "neverthrow"; import { isCancel, log, multiselect, select, text } from "@clack/prompts"; +import { Language, LANGUAGE_CHOICES } from "../../types/sdk/generate.js"; import { UrlPath } from "../../types/file/urlPath.js"; import { format as f, getTree } from "../format.js"; import { DirectoryPath } from "../../types/file/directoryPath.js"; @@ -9,11 +10,15 @@ import { Directory } from "../../types/file/directory.js"; import { createResourceInputFromInput, ResourceInput } from "../../types/file/resource-input.js"; import { FileDownloadResponse } from "../../infrastructure/services/file-download-service.js"; import { noteWrapped, withSpinner } from "../prompt.js"; -import { UnallowedFeaturesResponse } from "../../infrastructure/services/validation-service.js"; +import { + BuildFilePruneReport, + UnallowedFeaturesResponse +} from "../../infrastructure/services/validation-service.js"; const vscodeExtensionUrl = "https://marketplace.visualstudio.com/items?itemName=apimatic-developers.apimatic-for-vscode"; const referenceDocumentationUrl = "https://docs.apimatic.io/cli-getting-started/advanced-portal-setup"; +const pricingUrl = "https://www.apimatic.io/pricing"; export class PortalQuickstartPrompts { public importSpecStep() { @@ -88,7 +93,7 @@ export class PortalQuickstartPrompts { "To continue:", "- Remove these components from your API Specification and re-run this command.", "- Combine your split API Specification files into a single file. We can automatically remove unsupported components from single-file specs.", - "- Upgrade your subscription to unlock additional features: https://www.apimatic.io/pricing" + `- Upgrade your subscription to unlock additional features: ${f.link(pricingUrl)}` ].join("\n"); log.info(message); @@ -110,7 +115,7 @@ export class PortalQuickstartPrompts { endpointMessage, "", "You won't see these components in the generated SDKs or documentation.", - "Want to keep them? Upgrade your subscription to unlock additional features: https://www.apimatic.io/pricing" + `Want to keep them? Upgrade your subscription to unlock additional features: ${f.link(pricingUrl)}` ].join("\n"); log.info(message); @@ -120,22 +125,22 @@ export class PortalQuickstartPrompts { log.info(`Step 3 of 4: Select programming languages`); } - public async selectLanguagesPrompt(): Promise { + public async selectLanguagesPrompt(allowedLanguages: Language[]): Promise { + const allowed = new Set(allowedLanguages); + const available = LANGUAGE_CHOICES.filter(({ value }) => allowed.has(value)); + const excluded = LANGUAGE_CHOICES.filter(({ value }) => !allowed.has(value)); + + if (excluded.length > 0) { + log.info(this.languagesNotOnPlanNote(excluded.map(({ label }) => label))); + } + const languages = (await multiselect({ message: "Your API Portal will contain SDKs and SDK Documentation in the following Languages (HTTP is enabled by default). Press enter to continue with all languages, or use the arrow keys and space to customize your selection:", - options: [ - { label: "Typescript", value: "typescript" }, - { label: "Ruby", value: "ruby" }, - { label: "Python", value: "python" }, - { label: "Java", value: "java" }, - { label: "C#", value: "csharp" }, - { label: "PHP", value: "php" }, - { label: "Go", value: "go" } - ], - initialValues: ["typescript", "ruby", "python", "java", "csharp", "php", "go"], + options: available.map(({ label, value }) => ({ label, value })), + initialValues: available.map(({ value }) => value), required: false - })) as string[]; + })) as Language[]; if (isCancel(languages)) { return undefined; @@ -144,10 +149,60 @@ export class PortalQuickstartPrompts { return ["http", ...languages]; } + public httpOnlyPortalOnPlan(): void { + const message = [ + "Your current subscription plan doesn't include any SDK languages, so the portal will contain HTTP documentation only.", + `Upgrade your subscription to unlock SDK languages: ${f.link(pricingUrl)}` + ].join("\n"); + log.warn(message); + } + + public onPremGenerationNotAllowedOnPlan(): void { + const message = [ + "Your current subscription plan doesn't include on-prem generation.", + `Upgrade your subscription to get started: ${f.link(pricingUrl)}` + ].join("\n"); + log.warn(message); + } + + private languagesNotOnPlanNote(languages: string[]): string { + return [ + `The following languages aren't included in your current subscription plan`, + ...languages.map((language) => ` • ${language}`), + "", + `Upgrade your subscription to unlock them: ${f.link(pricingUrl)}` + ].join("\n"); + } + public noLanguagesSelected() { log.error("No programming languages were selected."); } + public buildFilePruned(report: BuildFilePruneReport): void { + const removed: string[] = []; + if (report.removedLanguages.length > 0) { + removed.push(`SDK languages: ${report.removedLanguages.join(", ")}`); + } + if (report.removedApiCopilot) { + removed.push("API Copilot"); + } + if (report.removedAiIntegration) { + removed.push("AI context plugins (Cursor / VS Code / Claude Code)"); + } + if (removed.length === 0) { + return; + } + + const message = [ + "Some portal features aren't on your current subscription plan and were removed before generation:", + "", + ...removed.map((item) => ` • ${item}`), + "", + `Upgrade your subscription to unlock these: ${f.link(pricingUrl)}` + ].join("\n"); + log.warn(message); + } + public selectInputDirectoryStep() { log.info(`Step 4 of 4: Generate source files for Docs as Code`); } diff --git a/src/prompts/sdk/quickstart.ts b/src/prompts/sdk/quickstart.ts index 8b682089..790d15fd 100644 --- a/src/prompts/sdk/quickstart.ts +++ b/src/prompts/sdk/quickstart.ts @@ -9,12 +9,13 @@ import { ServiceError } from "../../infrastructure/service-error.js"; import { DirectoryPath } from "../../types/file/directoryPath.js"; import { removeQuotes } from "../../utils/string-utils.js"; import { Directory } from "../../types/file/directory.js"; -import { Language } from "../../types/sdk/generate.js"; +import { Language, LANGUAGE_CHOICES } from "../../types/sdk/generate.js"; import { UnallowedFeaturesResponse } from "../../infrastructure/services/validation-service.js"; const vscodeExtensionUrl = "https://marketplace.visualstudio.com/items?itemName=apimatic-developers.apimatic-for-vscode"; const sdkCustomizationUrl = "https://docs.apimatic.io/generate-sdks/codegen-settings/codegen-settings-overview/"; +const pricingUrl = "https://www.apimatic.io/pricing"; export class SdkQuickstartPrompts { public importSpecStep() { @@ -58,7 +59,7 @@ export class SdkQuickstartPrompts { "To continue:", "- Remove these components from your API Specification and re-run this command.", "- Combine your split API Specification files into a single file. We can automatically remove unsupported components from single-file specs.", - "- Upgrade your subscription to unlock additional features: https://www.apimatic.io/pricing" + `- Upgrade your subscription to unlock additional features: ${f.link(pricingUrl)}` ].join("\n"); log.info(message); @@ -80,7 +81,7 @@ export class SdkQuickstartPrompts { endpointMessage, "", "You won't see these components in the generated SDKs or documentation.", - "Want to keep them? Upgrade your subscription to unlock additional features: https://www.apimatic.io/pricing" + `Want to keep them? Upgrade your subscription to unlock additional features: ${f.link(pricingUrl)}` ].join("\n"); log.info(message); @@ -143,18 +144,18 @@ export class SdkQuickstartPrompts { log.info(`Step 3 of 4: Select programming language`); } - public async selectLanguagePrompt(): Promise { + public async selectLanguagePrompt(allowedLanguages: Language[]): Promise { + const allowed = new Set(allowedLanguages); + const available = LANGUAGE_CHOICES.filter(({ value }) => allowed.has(value)); + const excluded = LANGUAGE_CHOICES.filter(({ value }) => !allowed.has(value)); + + if (excluded.length > 0) { + log.info(this.languagesNotOnPlanNote(excluded.map(({ label }) => label))); + } + const language = await select({ message: "Choose the programming language for your SDK:", - options: [ - { label: "Typescript", value: Language.TYPESCRIPT }, - { label: "Ruby", value: Language.RUBY }, - { label: "Python", value: Language.PYTHON }, - { label: "Java", value: Language.JAVA }, - { label: "C#", value: Language.CSHARP }, - { label: "PHP", value: Language.PHP }, - { label: "Go", value: Language.GO } - ] + options: available.map(({ label, value }) => ({ label, value })) }); if (isCancel(language)) { @@ -164,10 +165,31 @@ export class SdkQuickstartPrompts { return language; } + public noLanguagesAvailableOnPlan(): void { + const message = [ + "You're on the Free plan.", + `Upgrade your subscription to get started: ${f.link(pricingUrl)}` + ].join("\n"); + log.warn(message); + } + + private languagesNotOnPlanNote(languages: string[]): string { + return [ + `The following languages aren't included in your current subscription plan:`, + ...languages.map((language) => ` • ${language}`), + "", + `Upgrade your subscription to unlock them: ${f.link(pricingUrl)}` + ].join("\n"); + } + public noLanguageSelected() { log.error("No programming language was selected."); } + public accountInfoFetchFailed(serviceError: ServiceError) { + log.error(`Failed to fetch your account information. ${serviceError.errorMessage}`); + } + public selectInputDirectoryStep() { log.info(`Step 4 of 4: Setup directory for SDK Generation`); } diff --git a/src/types/api/account.ts b/src/types/api/account.ts index 1c33c5ab..96fa6cbc 100644 --- a/src/types/api/account.ts +++ b/src/types/api/account.ts @@ -6,5 +6,6 @@ export interface SubscriptionInfo { tenantId: string; allowedLanguages: number; // might be a comma-separated string or code — clarify if needed isPackagePublishingAllowed: boolean; + isOnPremGenerationAllowed: boolean; ApiCopilotKeys: string[]; } diff --git a/src/types/build-context.ts b/src/types/build-context.ts index cd9a4c78..a46f1bcc 100644 --- a/src/types/build-context.ts +++ b/src/types/build-context.ts @@ -19,6 +19,11 @@ export class BuildContext { return new FilePath(this.buildDirectory, new FileName("APIMATIC-BUILD.json")); } + /** The APIMATIC-BUILD.json path within this build directory, for callers that hand it to a service. */ + public buildConfigFilePath(): FilePath { + return this.buildFile; + } + public async validate(): Promise { // TODO: add more checks here if (!(await this.fileService.directoryExists(this.buildDirectory))) return false; diff --git a/src/types/build/build.ts b/src/types/build/build.ts index dd7ddd27..dac4f12c 100644 --- a/src/types/build/build.ts +++ b/src/types/build/build.ts @@ -166,7 +166,11 @@ export class BuildConfig { private constructor(private readonly data: BuildConfigData) {} public static parse(json: string): BuildConfig { - return new BuildConfig(JSON.parse(json) as BuildConfigData); + return BuildConfig.from(JSON.parse(json) as BuildConfigData); + } + + public static from(data: BuildConfigData): BuildConfig { + return new BuildConfig(data); } // Called implicitly by JSON.stringify when the config is written back to disk. diff --git a/src/types/sdk/generate.ts b/src/types/sdk/generate.ts index 67c3d4d8..106932c2 100644 --- a/src/types/sdk/generate.ts +++ b/src/types/sdk/generate.ts @@ -33,3 +33,19 @@ export function mapLanguages(languageFlag: number): Language[] { .filter(([flag]) => (languageFlag & parseInt(flag)) !== 0) .map(([, language]) => language); } + +/** + * The languages offered in the quickstart prompts, in display order. + * Shared by the portal (multi-select) and SDK (single-select) flows so both + * present the same list; the subscription's allowed languages decide which + * are selectable. + */ +export const LANGUAGE_CHOICES: ReadonlyArray<{ label: string; value: Language }> = [ + { label: "Typescript", value: Language.TYPESCRIPT }, + { label: "Ruby", value: Language.RUBY }, + { label: "Python", value: Language.PYTHON }, + { label: "Java", value: Language.JAVA }, + { label: "C#", value: Language.CSHARP }, + { label: "PHP", value: Language.PHP }, + { label: "Go", value: Language.GO } +];