Skip to content

Commit eaed4de

Browse files
sohail2721claude
andcommitted
feat(quickstart): only offer SDK languages included in the plan
The quickstart language prompts now list only the SDK languages the account's subscription includes. Languages not on the plan are omitted from the choices, and a note listing them with an upgrade link is shown before the language selection — instead of failing after the fact. Both the portal (multi-select) and SDK (single-select) flows fetch account info before the language step and share a LANGUAGE_CHOICES list. Also defer the 'API Copilot is enabled' caution until after the build file is pruned, and only print it when Copilot survived the prune, so it is no longer shown for accounts whose plan doesn't include Copilot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7043449 commit eaed4de

5 files changed

Lines changed: 97 additions & 61 deletions

File tree

src/actions/portal/quickstart.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { FeaturesToRemove, ValidationService } from '../../infrastructure/servic
1919
import { FileName } from '../../types/file/fileName.js';
2020
import { ApiService } from '../../infrastructure/services/api-service.js';
2121
import { DEFAULT_COPILOT_WELCOME_MESSAGE } from './copilot.js';
22-
import { Language, mapLanguages } from '../../types/sdk/generate.js';
22+
import { mapLanguages } from '../../types/sdk/generate.js';
2323

2424
const defaultPort: number = 23513 as const;
2525
const defaultBaseUrl = new UrlPath(`http://localhost:${defaultPort}`);
@@ -130,9 +130,19 @@ export class PortalQuickstartAction {
130130
}
131131
}
132132

133+
// Fetch account info up front so the language step can offer only the SDK
134+
// languages the plan allows, and so the API Copilot key (below) is resolved
135+
// from the same lookup. A lookup failure is fatal.
136+
const accountInfo = await this.apiService.getAccountInfo(this.configDir, this.commandMetadata.shell, null);
137+
if (accountInfo.isErr()) {
138+
this.prompts.accountInfoFetchFailed(accountInfo.error);
139+
return ActionResult.failed();
140+
}
141+
const allowedLanguages = mapLanguages(accountInfo.value.allowedLanguages);
142+
133143
// Step 3/4
134144
this.prompts.selectLanguagesStep();
135-
const languages = await this.prompts.selectLanguagesPrompt();
145+
const languages = await this.prompts.selectLanguagesPrompt(allowedLanguages);
136146
if (!languages) {
137147
this.prompts.noLanguagesSelected();
138148
return ActionResult.cancelled();
@@ -162,25 +172,10 @@ export class PortalQuickstartAction {
162172
}
163173

164174
// Resolve the API Copilot key to enable, if any, before setting up the source
165-
// directory so the user decides on Copilot up front. The lookup failing is fatal;
166-
// an account with no key continues silently (no Copilot); cancelling the multi-key
167-
// selection aborts quickstart.
168-
const accountInfo = await this.apiService.getAccountInfo(this.configDir, this.commandMetadata.shell, null);
169-
if (accountInfo.isErr()) {
170-
this.prompts.accountInfoFetchFailed(accountInfo.error);
171-
return ActionResult.failed();
172-
}
173-
174-
// Fail fast if a selected SDK language isn't on the plan (http docs are always allowed).
175-
const allowedLanguages = mapLanguages(accountInfo.value.allowedLanguages);
176-
const disallowedLanguages = languages.filter(
177-
(language) => language !== 'http' && !allowedLanguages.includes(language as Language)
178-
);
179-
if (disallowedLanguages.length > 0) {
180-
this.prompts.languagesNotAllowed(disallowedLanguages);
181-
return ActionResult.failed();
182-
}
183-
175+
// directory. An account with no key continues silently (no Copilot); cancelling
176+
// the multi-key selection aborts quickstart. (Account info was already fetched
177+
// above for the language step.) Whether Copilot is actually on the plan is only
178+
// known after the prune below, so the "enabled" caution is deferred until then.
184179
let copilotKey: string | undefined;
185180
const copilotKeys = accountInfo.value.ApiCopilotKeys ?? [];
186181
if (copilotKeys.length === 1) {
@@ -192,9 +187,6 @@ export class PortalQuickstartAction {
192187
return ActionResult.cancelled();
193188
}
194189
}
195-
if (copilotKey) {
196-
this.prompts.copilotEnabled(copilotKey);
197-
}
198190

199191
const masterBuildFile = await this.prompts.downloadBuildDirectory(
200192
this.fileDownloadService.downloadFile(this.buildFileUrl)
@@ -240,6 +232,12 @@ export class PortalQuickstartAction {
240232
);
241233
this.prompts.buildFilePruned(pruneResult.value.report);
242234

235+
// Only surface the Copilot caution if Copilot survived the prune — i.e. it's
236+
// actually on the plan. If it was stripped, buildFilePruned already reported it.
237+
if (copilotKey && !pruneResult.value.report.removedApiCopilot) {
238+
this.prompts.copilotEnabled(copilotKey);
239+
}
240+
243241
const specDirectory = sourceDirectory.join('spec');
244242
const specContext = new SpecContext(specDirectory);
245243
await specContext.replaceDefaultSpec(specPath);

src/actions/sdk/quickstart.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,20 @@ import { ValidateAction } from '../api/validate.js';
1212
import { FileDownloadService } from '../../infrastructure/services/file-download-service.js';
1313
import { FileService } from '../../infrastructure/file-service.js';
1414
import { GenerateAction } from './generate.js';
15-
import { CodeGenerationVersion, Language, Stability } from '../../types/sdk/generate.js';
15+
import { CodeGenerationVersion, Language, mapLanguages, Stability } from '../../types/sdk/generate.js';
1616
import { LauncherService } from '../../infrastructure/launcher-service.js';
1717
import { ZipService } from '../../infrastructure/zip-service.js';
1818
import { FileName } from '../../types/file/fileName.js';
1919
import { FeaturesToRemove, ValidationService } from '../../infrastructure/services/validation-service.js';
20+
import { ApiService } from '../../infrastructure/services/api-service.js';
2021

2122
export class SdkQuickstartAction {
2223
private readonly prompts = new SdkQuickstartPrompts();
2324
private readonly fileDownloadService = new FileDownloadService();
2425
private readonly fileService = new FileService();
2526
private readonly launcherService = new LauncherService();
2627
private readonly zipService = new ZipService();
28+
private readonly apiService = new ApiService();
2729
private readonly validationService = new ValidationService(this.configDir);
2830
private readonly metadataFileUrl = new UrlPath(
2931
`https://raw.githubusercontent.com/apimatic/sample-docs-as-code-portal/refs/heads/master/src/spec/APIMATIC-META.json`
@@ -118,10 +120,19 @@ export class SdkQuickstartAction {
118120
}
119121
}
120122

123+
// Fetch account info so the language step only offers SDK languages the plan
124+
// allows. A lookup failure is fatal.
125+
const accountInfo = await this.apiService.getAccountInfo(this.configDir, this.commandMetadata.shell, null);
126+
if (accountInfo.isErr()) {
127+
this.prompts.accountInfoFetchFailed(accountInfo.error);
128+
return ActionResult.failed();
129+
}
130+
const allowedLanguages = mapLanguages(accountInfo.value.allowedLanguages);
131+
121132
// Step 3/4
122133
this.prompts.selectLanguageStep();
123134

124-
const language = await this.prompts.selectLanguagePrompt();
135+
const language = await this.prompts.selectLanguagePrompt(allowedLanguages);
125136
if (!language) {
126137
this.prompts.noLanguageSelected();
127138
return ActionResult.cancelled();

src/prompts/portal/quickstart.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Result } from "neverthrow";
22
import { isCancel, log, multiselect, select, text } from "@clack/prompts";
3+
import { Language, LANGUAGE_CHOICES } from "../../types/sdk/generate.js";
34
import { UrlPath } from "../../types/file/urlPath.js";
45
import { format as f, getTree } from "../format.js";
56
import { DirectoryPath } from "../../types/file/directoryPath.js";
@@ -123,22 +124,22 @@ export class PortalQuickstartPrompts {
123124
log.info(`Step 3 of 4: Select programming languages`);
124125
}
125126

126-
public async selectLanguagesPrompt(): Promise<string[] | undefined> {
127+
public async selectLanguagesPrompt(allowedLanguages: Language[]): Promise<string[] | undefined> {
128+
const allowed = new Set(allowedLanguages);
129+
const available = LANGUAGE_CHOICES.filter(({ value }) => allowed.has(value));
130+
const excluded = LANGUAGE_CHOICES.filter(({ value }) => !allowed.has(value));
131+
132+
if (excluded.length > 0) {
133+
log.info(this.languagesNotOnPlanNote(excluded.map(({ label }) => label)));
134+
}
135+
127136
const languages = (await multiselect({
128137
message:
129138
"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:",
130-
options: [
131-
{ label: "Typescript", value: "typescript" },
132-
{ label: "Ruby", value: "ruby" },
133-
{ label: "Python", value: "python" },
134-
{ label: "Java", value: "java" },
135-
{ label: "C#", value: "csharp" },
136-
{ label: "PHP", value: "php" },
137-
{ label: "Go", value: "go" }
138-
],
139-
initialValues: ["typescript", "ruby", "python", "java", "csharp", "php", "go"],
139+
options: available.map(({ label, value }) => ({ label, value })),
140+
initialValues: available.map(({ value }) => value),
140141
required: false
141-
})) as string[];
142+
})) as Language[];
142143

143144
if (isCancel(languages)) {
144145
return undefined;
@@ -147,20 +148,17 @@ export class PortalQuickstartPrompts {
147148
return ["http", ...languages];
148149
}
149150

150-
public noLanguagesSelected() {
151-
log.error("No programming languages were selected.");
152-
}
153-
154-
public languagesNotAllowed(languages: string[]): void {
155-
const languagesList = languages.map((language) => ` • ${language}`).join("\n");
156-
const message = [
157-
"The following SDK languages are not available on your current subscription plan:",
151+
private languagesNotOnPlanNote(languages: string[]): string {
152+
return [
153+
`The following languages aren't included in your current subscription plan, so they aren't available to select:`,
154+
...languages.map((language) => ` • ${language}`),
158155
"",
159-
languagesList,
160-
"",
161-
"Select only the languages your plan includes, or upgrade your subscription to unlock more: https://www.apimatic.io/pricing"
156+
"Upgrade your subscription to unlock them: https://www.apimatic.io/pricing"
162157
].join("\n");
163-
log.error(message);
158+
}
159+
160+
public noLanguagesSelected() {
161+
log.error("No programming languages were selected.");
164162
}
165163

166164
public buildFilePruned(report: BuildFilePruneReport): void {

src/prompts/sdk/quickstart.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ServiceError } from "../../infrastructure/service-error.js";
99
import { DirectoryPath } from "../../types/file/directoryPath.js";
1010
import { removeQuotes } from "../../utils/string-utils.js";
1111
import { Directory } from "../../types/file/directory.js";
12-
import { Language } from "../../types/sdk/generate.js";
12+
import { Language, LANGUAGE_CHOICES } from "../../types/sdk/generate.js";
1313
import { UnallowedFeaturesResponse } from "../../infrastructure/services/validation-service.js";
1414

1515
const vscodeExtensionUrl =
@@ -143,18 +143,18 @@ export class SdkQuickstartPrompts {
143143
log.info(`Step 3 of 4: Select programming language`);
144144
}
145145

146-
public async selectLanguagePrompt(): Promise<Language | undefined> {
146+
public async selectLanguagePrompt(allowedLanguages: Language[]): Promise<Language | undefined> {
147+
const allowed = new Set(allowedLanguages);
148+
const available = LANGUAGE_CHOICES.filter(({ value }) => allowed.has(value));
149+
const excluded = LANGUAGE_CHOICES.filter(({ value }) => !allowed.has(value));
150+
151+
if (excluded.length > 0) {
152+
log.info(this.languagesNotOnPlanNote(excluded.map(({ label }) => label)));
153+
}
154+
147155
const language = await select({
148156
message: "Choose the programming language for your SDK:",
149-
options: [
150-
{ label: "Typescript", value: Language.TYPESCRIPT },
151-
{ label: "Ruby", value: Language.RUBY },
152-
{ label: "Python", value: Language.PYTHON },
153-
{ label: "Java", value: Language.JAVA },
154-
{ label: "C#", value: Language.CSHARP },
155-
{ label: "PHP", value: Language.PHP },
156-
{ label: "Go", value: Language.GO }
157-
]
157+
options: available.map(({ label, value }) => ({ label, value }))
158158
});
159159

160160
if (isCancel(language)) {
@@ -164,10 +164,23 @@ export class SdkQuickstartPrompts {
164164
return language;
165165
}
166166

167+
private languagesNotOnPlanNote(languages: string[]): string {
168+
return [
169+
`The following languages aren't included in your current subscription plan, so they aren't available to select:`,
170+
...languages.map((language) => ` • ${language}`),
171+
"",
172+
"Upgrade your subscription to unlock them: https://www.apimatic.io/pricing"
173+
].join("\n");
174+
}
175+
167176
public noLanguageSelected() {
168177
log.error("No programming language was selected.");
169178
}
170179

180+
public accountInfoFetchFailed(serviceError: ServiceError) {
181+
log.error(`Failed to fetch your account information. ${serviceError.errorMessage}`);
182+
}
183+
171184
public selectInputDirectoryStep() {
172185
log.info(`Step 4 of 4: Setup directory for SDK Generation`);
173186
}

src/types/sdk/generate.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,19 @@ export function mapLanguages(languageFlag: number): Language[] {
3333
.filter(([flag]) => (languageFlag & parseInt(flag)) !== 0)
3434
.map(([, language]) => language);
3535
}
36+
37+
/**
38+
* The languages offered in the quickstart prompts, in display order.
39+
* Shared by the portal (multi-select) and SDK (single-select) flows so both
40+
* present the same list; the subscription's allowed languages decide which
41+
* are selectable.
42+
*/
43+
export const LANGUAGE_CHOICES: ReadonlyArray<{ label: string; value: Language }> = [
44+
{ label: "Typescript", value: Language.TYPESCRIPT },
45+
{ label: "Ruby", value: Language.RUBY },
46+
{ label: "Python", value: Language.PYTHON },
47+
{ label: "Java", value: Language.JAVA },
48+
{ label: "C#", value: Language.CSHARP },
49+
{ label: "PHP", value: Language.PHP },
50+
{ label: "Go", value: Language.GO }
51+
];

0 commit comments

Comments
 (0)