Skip to content

Commit 1af63a7

Browse files
committed
refactor
1 parent b78ed3f commit 1af63a7

4 files changed

Lines changed: 50 additions & 65 deletions

File tree

src/actions/portal/quickstart.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class PortalQuickstartAction {
166166
// selection aborts quickstart.
167167
const accountInfo = await this.apiService.getAccountInfo(this.configDir, this.commandMetadata.shell, null);
168168
if (accountInfo.isErr()) {
169-
this.prompts.copilotKeyFetchFailed(accountInfo.error);
169+
this.prompts.accountInfoFetchFailed(accountInfo.error);
170170
return ActionResult.failed();
171171
}
172172

src/actions/portal/serve.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,7 @@ export class PortalServeAction {
7070

7171
const liveReloadPort = await this.networkService.getServerPort([35729, 35730, 35731, 35732]);
7272
const liveReloadServer = createLiveReloadServer({ port: liveReloadPort });
73-
74-
// get-port only checks availability; a port can be taken between that check and the
75-
// actual bind, so wait for each server to bind and fail cleanly instead of letting an
76-
// unhandled "error" event crash the CLI. livereload surfaces its bind error only on
77-
// its internal HTTP server (config.server), which isn't part of its public type.
73+
// get error if the live reload server fails to start (e.g. EADDRINUSE)
7874
const liveReloadHttpServer = (liveReloadServer as unknown as { config: { server: Server } }).config.server;
7975
if ((await this.waitForServerListening(liveReloadHttpServer)).isErr()) {
8076
this.prompts.serverStartFailed(liveReloadPort);

src/prompts/portal/quickstart.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,14 @@ ${f.link(referenceDocumentationUrl)}`;
230230
log.error("No API Copilot key was selected.");
231231
}
232232

233-
public copilotKeyFetchFailed(serviceError: ServiceError) {
234-
log.error(`Failed to fetch your API Copilot key. ${serviceError.errorMessage}`);
233+
public accountInfoFetchFailed(serviceError: ServiceError) {
234+
log.error(`Failed to fetch your account information. ${serviceError.errorMessage}`);
235235
}
236236

237237
public copilotEnabled(key: string) {
238238
const message =
239239
`API Copilot is enabled with key ${f.var(key)}. ` +
240-
`Any existing training data associated with this key will be overwritten when the portal is generated.`;
240+
`Any existing AI context associated with this key will be overwritten when the portal is generated.`;
241241
log.warn(message);
242242
}
243243

src/types/build/build.ts

Lines changed: 45 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -90,30 +90,25 @@ const CODEGEN_TEMPLATE_ID_BY_LANGUAGE: Readonly<Record<string, string>> = {
9090
// languageSettings entry for http must exist or the portal widget fails to render.
9191
const HTTP_TEMPLATE_ID = "http_curl_v1" as const;
9292

93-
// Deep clone used for copy-on-write transforms. The config is plain JSON, so a JSON
94-
// round-trip is an exact, dependency-free copy.
95-
function clone<T>(value: T): T {
96-
return JSON.parse(JSON.stringify(value)) as T;
97-
}
98-
9993
// Immutable per-language portal setting. Build new values via the static factory and
10094
// the with* transforms; the wrapped data is never mutated after construction.
10195
export class LanguageSetting {
10296
private constructor(private readonly data: LanguageSettingData) {}
10397

10498
public static from(data: LanguageSettingData = {}): LanguageSetting {
105-
return new LanguageSetting(clone(data));
99+
return new LanguageSetting(data);
106100
}
107101

108102
/** Returns a copy with all AI editor integrations (Cursor/Claude Code/VS Code) enabled. */
109103
public withAiIntegrationsEnabled(): LanguageSetting {
110-
const data = clone(this.data);
111-
data.aiIntegration = {
112-
cursor: { isEnabled: true },
113-
claudeCode: { isEnabled: true },
114-
vscode: { isEnabled: true }
115-
};
116-
return new LanguageSetting(data);
104+
return new LanguageSetting({
105+
...this.data,
106+
aiIntegration: {
107+
cursor: { isEnabled: true },
108+
claudeCode: { isEnabled: true },
109+
vscode: { isEnabled: true }
110+
}
111+
});
117112
}
118113

119114
public toJSON(): LanguageSettingData {
@@ -127,22 +122,19 @@ export class PortalSettings {
127122
private constructor(private readonly data: PortalSettingsData) {}
128123

129124
public static from(data: PortalSettingsData = {}): PortalSettings {
130-
return new PortalSettings(clone(data));
125+
return new PortalSettings(data);
131126
}
132127

133128
/** Returns a copy with the API-call base URL set. */
134129
public withBaseUrl(baseUrl: UrlPath): PortalSettings {
135-
const data = clone(this.data);
136-
data.baseUrl = baseUrl.toString();
137-
return new PortalSettings(data);
130+
return new PortalSettings({ ...this.data, baseUrl: baseUrl.toString() });
138131
}
139132

140133
// Returns a copy with AI editor integrations enabled for the given SDK languages
141134
// (friendly ids). Always adds the http entry the portal needs to render and opens the
142135
// portal on the first SDK language. Languages without a codegen template id are skipped.
143136
public withAiIntegrations(languages: string[]): PortalSettings {
144-
const data = clone(this.data);
145-
const languageSettings: { [language: string]: LanguageSettingData } = { ...data.languageSettings };
137+
const languageSettings: { [language: string]: LanguageSettingData } = { ...this.data.languageSettings };
146138
languageSettings[HTTP_TEMPLATE_ID] = languageSettings[HTTP_TEMPLATE_ID] ?? {};
147139

148140
let firstSdkTemplateId: string | undefined;
@@ -155,11 +147,11 @@ export class PortalSettings {
155147
languageSettings[templateId] = LanguageSetting.from(languageSettings[templateId]).withAiIntegrationsEnabled().toJSON();
156148
}
157149

158-
data.languageSettings = languageSettings;
159-
if (firstSdkTemplateId) {
160-
data.initialPlatform = firstSdkTemplateId;
161-
}
162-
return new PortalSettings(data);
150+
return new PortalSettings({
151+
...this.data,
152+
languageSettings,
153+
...(firstSdkTemplateId ? { initialPlatform: firstSdkTemplateId } : {})
154+
});
163155
}
164156

165157
public toJSON(): PortalSettingsData {
@@ -205,30 +197,34 @@ export class BuildConfig {
205197

206198
/** Returns a copy with the portal's languageConfig set from the selected friendly language ids. */
207199
public withPortalLanguages(languages: string[]): BuildConfig {
208-
const data = clone(this.data);
209-
data.generatePortal!.languageConfig = getLanguagesConfig(languages);
210-
return new BuildConfig(data);
200+
const portal = this.data.generatePortal!;
201+
return new BuildConfig({
202+
...this.data,
203+
generatePortal: { ...portal, languageConfig: getLanguagesConfig(languages) }
204+
});
211205
}
212206

213207
/** Returns a copy with the API Copilot configuration set (or overwritten). */
214208
public withApiCopilotConfig(config: CopilotConfig): BuildConfig {
215-
const data = clone(this.data);
216-
data.apiCopilotConfig = { ...config };
217-
return new BuildConfig(data);
209+
return new BuildConfig({ ...this.data, apiCopilotConfig: { ...config } });
218210
}
219211

220212
// Returns a copy with API Copilot enabled for a locally-served portal: stores the
221213
// Copilot config, points the portal base URL at the local serve URL, and turns on AI
222214
// editor integrations for the configured SDK languages.
223215
public withApiCopilotForPortal(key: string, welcomeMessage: string, baseUrl: string): BuildConfig {
224-
const data = clone(this.data);
225-
const portal = data.generatePortal!;
226-
data.apiCopilotConfig = { isEnabled: true, key, welcomeMessage };
227-
portal.baseUrl = baseUrl;
228-
portal.portalSettings = PortalSettings.from(portal.portalSettings)
229-
.withAiIntegrations(Object.keys(portal.languageConfig))
230-
.toJSON();
231-
return new BuildConfig(data);
216+
const portal = this.data.generatePortal!;
217+
return new BuildConfig({
218+
...this.data,
219+
apiCopilotConfig: { isEnabled: true, key, welcomeMessage },
220+
generatePortal: {
221+
...portal,
222+
baseUrl,
223+
portalSettings: PortalSettings.from(portal.portalSettings)
224+
.withAiIntegrations(Object.keys(portal.languageConfig))
225+
.toJSON()
226+
}
227+
});
232228
}
233229

234230
public updateBuildConfigBaseUrl(serveUrl: UrlPath): Result<BuildConfig, "unchanged"> {
@@ -245,29 +241,22 @@ export class BuildConfig {
245241
return err("unchanged");
246242
}
247243

248-
const data = clone(this.data);
249-
const portal = data.generatePortal!;
250-
if (portal.portalSettings?.baseUrl) {
251-
portal.portalSettings = PortalSettings.from(portal.portalSettings).withBaseUrl(serveUrl).toJSON();
252-
} else {
253-
portal.baseUrl = serveUrl.toString();
254-
}
255-
return ok(new BuildConfig(data));
244+
const portal = this.data.generatePortal!;
245+
const updatedPortal: PortalConfig = portal.portalSettings?.baseUrl
246+
? { ...portal, portalSettings: PortalSettings.from(portal.portalSettings).withBaseUrl(serveUrl).toJSON() }
247+
: { ...portal, baseUrl: serveUrl.toString() };
248+
return ok(new BuildConfig({ ...this.data, generatePortal: updatedPortal }));
256249
}
257250

258251
// Returns a copy with a recipe workflow added (or replaced, matched by permalink).
259252
public withRecipeWorkflow(name: string, functionName: string, scriptPath: string): BuildConfig {
260-
const data = clone(this.data);
261-
const recipes = (data.recipes ??= {});
262-
const workflows = (recipes.workflows ??= []);
253+
const recipes = this.data.recipes ?? {};
254+
const workflows = recipes.workflows ?? [];
263255
const permalink = `page:recipes/${functionName}`;
264256
const workflow: RecipeWorkflow = { name, permalink, functionName, scriptPath };
265257
const existingIndex = workflows.findIndex((w) => w.permalink === permalink);
266-
if (existingIndex === -1) {
267-
workflows.push(workflow);
268-
} else {
269-
workflows[existingIndex] = workflow;
270-
}
271-
return new BuildConfig(data);
258+
const updatedWorkflows =
259+
existingIndex === -1 ? [...workflows, workflow] : workflows.map((w, index) => (index === existingIndex ? workflow : w));
260+
return new BuildConfig({ ...this.data, recipes: { ...recipes, workflows: updatedWorkflows } });
272261
}
273262
}

0 commit comments

Comments
 (0)