@lukoweb/apitogo 0.1.50 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cli/cli.js +1339 -922
  2. package/dist/declarations/config/apitogo-config-core.d.ts +25 -0
  3. package/dist/declarations/config/apitogo-config-loader.d.ts +2 -0
  4. package/dist/declarations/config/apitogo-config-loader.node.d.ts +4 -0
  5. package/dist/declarations/config/build-apitogo-config.d.ts +8 -0
  6. package/dist/declarations/config/loader.d.ts +4 -0
  7. package/dist/declarations/config/local-manifest.d.ts +16 -9
  8. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +24 -24
  9. package/dist/declarations/config/validators/ModulesSchema.d.ts +38 -0
  10. package/dist/declarations/config/validators/ZudokuConfig.d.ts +18 -9
  11. package/dist/declarations/index.d.ts +2 -1
  12. package/dist/declarations/lib/authentication/auth-header-nav.d.ts +10 -0
  13. package/dist/declarations/lib/core/plugins.d.ts +1 -0
  14. package/dist/declarations/lib/ui/Button.d.ts +1 -1
  15. package/dist/declarations/lib/ui/Item.d.ts +1 -1
  16. package/dist/flat-config.d.ts +36 -23
  17. package/docs/configuration/authentication.md +11 -8
  18. package/docs/configuration/manifest.md +83 -93
  19. package/package.json +17 -1
  20. package/src/config/apitogo-config-core.ts +485 -0
  21. package/src/config/apitogo-config-loader.node.ts +68 -0
  22. package/src/config/apitogo-config-loader.ts +6 -0
  23. package/src/config/build-apitogo-config.ts +52 -0
  24. package/src/config/loader.ts +61 -1
  25. package/src/config/local-manifest.ts +72 -52
  26. package/src/config/resolve-modules.ts +16 -13
  27. package/src/config/validators/ModulesSchema.ts +17 -0
  28. package/src/index.ts +9 -1
  29. package/src/lib/authentication/auth-header-nav.ts +56 -0
  30. package/src/lib/authentication/providers/dev-portal-utils.ts +7 -2
  31. package/src/lib/components/Header.tsx +48 -60
  32. package/src/lib/components/MobileTopNavigation.tsx +29 -40
  33. package/src/lib/core/plugins.ts +1 -0
  34. package/src/vite/dev-portal-env.ts +12 -4
  35. package/src/vite/plugin-config.ts +20 -3
  36. package/src/vite/plugin-local-manifest.ts +15 -0
@@ -0,0 +1,485 @@
1
+ export const APITOGO_CONFIG_FILENAME = "apitogo.config.json";
2
+
3
+ const ENV_PREFIX = "APITOGO_CONFIG_";
4
+
5
+ /** Derived at read time for platform/MCP — not stored in user JSON. */
6
+ const DERIVED_MANIFEST_KEYS = [
7
+ "siteName",
8
+ "branding",
9
+ "pricing",
10
+ "authentication",
11
+ "plans",
12
+ "backend",
13
+ "gateway",
14
+ "projectId",
15
+ ] as const;
16
+
17
+ const SITE_CONFIG_KEYS = [
18
+ "site",
19
+ "metadata",
20
+ "modules",
21
+ "navigation",
22
+ "apis",
23
+ "redirects",
24
+ "theme",
25
+ "search",
26
+ "llms",
27
+ "protectedRoutes",
28
+ "slots",
29
+ "mdx",
30
+ "catalogs",
31
+ "versions",
32
+ "options",
33
+ ] as const;
34
+
35
+ export type ApitogoJsonConfig = Record<string, unknown>;
36
+
37
+ export type AuthenticationSecrets = {
38
+ oidc?: {
39
+ authority?: string;
40
+ clientId?: string;
41
+ clientSecret?: string;
42
+ };
43
+ stripe?: {
44
+ secretKey?: string;
45
+ webhookSecret?: string;
46
+ };
47
+ };
48
+
49
+ function parseEnvValue(raw: string | undefined): unknown {
50
+ if (raw === undefined) {
51
+ return raw;
52
+ }
53
+
54
+ const trimmed = raw.trim();
55
+ if (trimmed === "true") {
56
+ return true;
57
+ }
58
+ if (trimmed === "false") {
59
+ return false;
60
+ }
61
+ if (trimmed === "null") {
62
+ return null;
63
+ }
64
+
65
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
66
+ return Number(trimmed);
67
+ }
68
+
69
+ if (
70
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
71
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
72
+ ) {
73
+ try {
74
+ return JSON.parse(trimmed) as unknown;
75
+ } catch {
76
+ return trimmed;
77
+ }
78
+ }
79
+
80
+ return trimmed;
81
+ }
82
+
83
+ function setNestedValue(
84
+ target: Record<string, unknown>,
85
+ pathParts: string[],
86
+ value: unknown,
87
+ ): void {
88
+ if (pathParts.length === 0) {
89
+ return;
90
+ }
91
+
92
+ let current: Record<string, unknown> = target;
93
+ for (let index = 0; index < pathParts.length - 1; index += 1) {
94
+ const key = pathParts[index];
95
+ if (!key) {
96
+ return;
97
+ }
98
+ const next = current[key];
99
+ if (
100
+ next === undefined ||
101
+ next === null ||
102
+ typeof next !== "object" ||
103
+ Array.isArray(next)
104
+ ) {
105
+ current[key] = {};
106
+ }
107
+ current = current[key] as Record<string, unknown>;
108
+ }
109
+
110
+ const leafKey = pathParts[pathParts.length - 1];
111
+ if (!leafKey) {
112
+ return;
113
+ }
114
+
115
+ current[leafKey] = value;
116
+ }
117
+
118
+ function stripRuntimeConfigFields(
119
+ config: ApitogoJsonConfig,
120
+ ): ApitogoJsonConfig {
121
+ const { plugins: _plugins, ...jsonOnly } = config as ApitogoJsonConfig & {
122
+ plugins?: unknown;
123
+ };
124
+ return jsonOnly;
125
+ }
126
+
127
+ export function applyEnvOverrides(
128
+ config: ApitogoJsonConfig,
129
+ env: NodeJS.ProcessEnv = process.env,
130
+ ): ApitogoJsonConfig {
131
+ const result = structuredClone(stripRuntimeConfigFields(config));
132
+
133
+ for (const [key, value] of Object.entries(env)) {
134
+ if (!key.startsWith(ENV_PREFIX) || value === undefined) {
135
+ continue;
136
+ }
137
+
138
+ const pathParts = key.slice(ENV_PREFIX.length).split("__").filter(Boolean);
139
+
140
+ if (pathParts.length === 0) {
141
+ continue;
142
+ }
143
+
144
+ setNestedValue(result, pathParts, parseEnvValue(value));
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
151
+ return value !== undefined &&
152
+ value !== null &&
153
+ typeof value === "object" &&
154
+ !Array.isArray(value)
155
+ ? (value as Record<string, unknown>)
156
+ : undefined;
157
+ }
158
+
159
+ function readSiteTitle(config: ApitogoJsonConfig): string | undefined {
160
+ const site = asRecord(config.site);
161
+ const branding = asRecord(config.branding);
162
+ const siteTitle = site?.title;
163
+ const brandingTitle = branding?.title;
164
+ const legacySiteName = config.siteName;
165
+
166
+ if (typeof siteTitle === "string" && siteTitle.trim()) {
167
+ return siteTitle.trim();
168
+ }
169
+ if (typeof brandingTitle === "string" && brandingTitle.trim()) {
170
+ return brandingTitle.trim();
171
+ }
172
+ if (typeof legacySiteName === "string" && legacySiteName.trim()) {
173
+ return legacySiteName.trim();
174
+ }
175
+
176
+ return undefined;
177
+ }
178
+
179
+ function readLogoUrl(config: ApitogoJsonConfig): string | undefined {
180
+ const site = asRecord(config.site);
181
+ const logo = asRecord(site?.logo);
182
+ const src = asRecord(logo?.src);
183
+ const branding = asRecord(config.branding);
184
+
185
+ const fromSite =
186
+ (typeof src?.light === "string" && src.light) ||
187
+ (typeof src?.dark === "string" && src.dark) ||
188
+ (typeof logo?.logoUrl === "string" && logo.logoUrl) ||
189
+ (typeof site?.logoUrl === "string" && site.logoUrl);
190
+
191
+ if (fromSite) {
192
+ return fromSite;
193
+ }
194
+
195
+ return typeof branding?.logoUrl === "string" ? branding.logoUrl : undefined;
196
+ }
197
+
198
+ function firstApiInput(config: ApitogoJsonConfig): string | undefined {
199
+ const apis = config.apis;
200
+ const list = Array.isArray(apis) ? apis : apis ? [apis] : [];
201
+ for (const entry of list) {
202
+ const api = asRecord(entry);
203
+ if (typeof api?.input === "string" && api.input.trim()) {
204
+ return api.input.trim();
205
+ }
206
+ }
207
+ return undefined;
208
+ }
209
+
210
+ function mergeAuthenticationGateway(
211
+ config: ApitogoJsonConfig,
212
+ ): Record<string, unknown> | undefined {
213
+ const authentication = asRecord(config.authentication);
214
+ if (!authentication) {
215
+ return undefined;
216
+ }
217
+
218
+ const authGateway = asRecord(authentication.gateway);
219
+ const rootGateway = asRecord(config.gateway);
220
+
221
+ if (!authGateway && !rootGateway) {
222
+ return authentication;
223
+ }
224
+
225
+ return {
226
+ ...authentication,
227
+ gateway: {
228
+ ...rootGateway,
229
+ ...authGateway,
230
+ },
231
+ };
232
+ }
233
+
234
+ function ensureModules(config: ApitogoJsonConfig): Record<string, unknown> {
235
+ const modules = asRecord(config.modules) ?? {};
236
+ config.modules = modules;
237
+ return modules;
238
+ }
239
+
240
+ type PlanCatalogItem = Record<string, unknown> & { sku?: string };
241
+
242
+ function mergePlansBySku(
243
+ existing: PlanCatalogItem[],
244
+ patch: PlanCatalogItem[],
245
+ ): PlanCatalogItem[] {
246
+ const bySku = new Map<string, PlanCatalogItem>();
247
+ for (const plan of existing) {
248
+ if (typeof plan.sku === "string") {
249
+ bySku.set(plan.sku, plan);
250
+ }
251
+ }
252
+ for (const plan of patch) {
253
+ if (typeof plan.sku !== "string") {
254
+ continue;
255
+ }
256
+ const prior = bySku.get(plan.sku);
257
+ bySku.set(plan.sku, prior ? { ...prior, ...plan } : plan);
258
+ }
259
+ return Array.from(bySku.values());
260
+ }
261
+
262
+ /** Gateway plan catalog stored under `modules.userPanel.plans.items`. */
263
+ export function readPlanCatalogItems(
264
+ config: ApitogoJsonConfig,
265
+ ): PlanCatalogItem[] {
266
+ const normalized = normalizeApitogoConfig(config);
267
+ const items = asRecord(
268
+ asRecord(asRecord(normalized.modules)?.userPanel)?.plans,
269
+ )?.items;
270
+ return Array.isArray(items) ? (items as PlanCatalogItem[]) : [];
271
+ }
272
+
273
+ function migratePlanCatalog(normalized: ApitogoJsonConfig): void {
274
+ const modules = ensureModules(normalized);
275
+ const userPanel = asRecord(modules.userPanel) ?? {};
276
+ const plansModule = asRecord(userPanel.plans) ?? {};
277
+ const rootList = Array.isArray(normalized.plans)
278
+ ? (normalized.plans as PlanCatalogItem[])
279
+ : [];
280
+ const existingItems = Array.isArray(plansModule.items)
281
+ ? (plansModule.items as PlanCatalogItem[])
282
+ : [];
283
+
284
+ if (rootList.length) {
285
+ plansModule.items = mergePlansBySku(existingItems, rootList);
286
+ if (plansModule.enabled === undefined) {
287
+ plansModule.enabled = true;
288
+ }
289
+ userPanel.plans = plansModule;
290
+ modules.userPanel = userPanel;
291
+ }
292
+
293
+ delete normalized.plans;
294
+ }
295
+
296
+ /** Collapse legacy duplicate fields into canonical shape (mutates copy). */
297
+ export function normalizeApitogoConfig(
298
+ config: ApitogoJsonConfig,
299
+ ): ApitogoJsonConfig {
300
+ const normalized = structuredClone(stripRuntimeConfigFields(config));
301
+ const title = readSiteTitle(normalized);
302
+ const logoUrl = readLogoUrl(normalized);
303
+
304
+ const site = asRecord(normalized.site) ?? {};
305
+ if (title) {
306
+ site.title = title;
307
+ }
308
+ if (logoUrl && !asRecord(site.logo)?.src) {
309
+ site.logo = {
310
+ ...(asRecord(site.logo) ?? {}),
311
+ src: { light: logoUrl, dark: logoUrl },
312
+ };
313
+ }
314
+ normalized.site = site;
315
+
316
+ const authentication = mergeAuthenticationGateway(normalized);
317
+ if (authentication) {
318
+ normalized.authentication = authentication;
319
+ }
320
+ delete normalized.gateway;
321
+
322
+ const modules = ensureModules(normalized);
323
+ const legacyDocs = asRecord(normalized.docs);
324
+ const moduleDocs = asRecord(modules.docs) ?? {};
325
+ if (legacyDocs) {
326
+ modules.docs = { ...legacyDocs, ...moduleDocs };
327
+ }
328
+ delete normalized.docs;
329
+
330
+ const legacyApiKeys = asRecord(normalized.apiKeys);
331
+ const userPanel = asRecord(modules.userPanel) ?? {};
332
+ const apiKeys = asRecord(userPanel.apiKeys) ?? {};
333
+ if (legacyApiKeys?.enabled === true && apiKeys.enabled === undefined) {
334
+ userPanel.apiKeys = { ...apiKeys, enabled: true };
335
+ modules.userPanel = userPanel;
336
+ }
337
+ delete normalized.apiKeys;
338
+
339
+ const apiInput = firstApiInput(normalized);
340
+ if (apiInput) {
341
+ const backend = asRecord(normalized.backend) ?? {};
342
+ if (!backend.openApiPath) {
343
+ backend.openApiPath = apiInput;
344
+ }
345
+ normalized.backend = backend;
346
+ }
347
+
348
+ delete normalized.siteName;
349
+ delete normalized.branding;
350
+ delete normalized.projectName;
351
+
352
+ migratePlanCatalog(normalized);
353
+ syncBillingEnabled(normalized);
354
+
355
+ return normalized;
356
+ }
357
+
358
+ /** Billing is controlled by `modules.userPanel.plans.enabled` (legacy `pricing.enabled` is migrated). */
359
+ export function isBillingEnabled(config: ApitogoJsonConfig): boolean {
360
+ const normalized = normalizeApitogoConfig(config);
361
+ const plans = asRecord(
362
+ asRecord(asRecord(normalized.modules)?.userPanel)?.plans,
363
+ );
364
+ return plans?.enabled === true;
365
+ }
366
+
367
+ function syncBillingEnabled(normalized: ApitogoJsonConfig): void {
368
+ const legacyPricing = asRecord(normalized.pricing);
369
+ const modules = ensureModules(normalized);
370
+ const userPanel = asRecord(modules.userPanel) ?? {};
371
+ const plans = asRecord(userPanel.plans) ?? {};
372
+
373
+ const enabled = plans.enabled === true || legacyPricing?.enabled === true;
374
+
375
+ if (enabled) {
376
+ userPanel.plans = { ...plans, enabled: true };
377
+ modules.userPanel = userPanel;
378
+ }
379
+
380
+ delete normalized.pricing;
381
+ }
382
+
383
+ export function deriveManifestFields(
384
+ config: ApitogoJsonConfig,
385
+ ): ApitogoJsonConfig {
386
+ const normalized = normalizeApitogoConfig(config);
387
+ const manifest: ApitogoJsonConfig = {};
388
+
389
+ const title = readSiteTitle(normalized);
390
+ if (title) {
391
+ manifest.siteName = title;
392
+ const logoUrl = readLogoUrl(normalized);
393
+ manifest.branding = logoUrl ? { title, logoUrl } : { title };
394
+ }
395
+
396
+ for (const key of DERIVED_MANIFEST_KEYS) {
397
+ if (key === "siteName" || key === "branding") {
398
+ continue;
399
+ }
400
+ if (key === "authentication") {
401
+ const authentication = asRecord(normalized.authentication);
402
+ if (authentication) {
403
+ const {
404
+ gateway: _gateway,
405
+ oidc: _oidc,
406
+ stripe: _stripe,
407
+ ...authPublic
408
+ } = authentication;
409
+ manifest.authentication = authPublic;
410
+ }
411
+ continue;
412
+ }
413
+ if (key === "gateway") {
414
+ continue;
415
+ }
416
+ if (key === "pricing") {
417
+ if (isBillingEnabled(config)) {
418
+ manifest.pricing = { enabled: true };
419
+ }
420
+ continue;
421
+ }
422
+ if (key === "plans") {
423
+ const items = readPlanCatalogItems(normalized);
424
+ if (items.length) {
425
+ manifest.plans = items;
426
+ }
427
+ continue;
428
+ }
429
+ if (normalized[key] !== undefined) {
430
+ manifest[key] = normalized[key];
431
+ }
432
+ }
433
+
434
+ const authentication = asRecord(normalized.authentication);
435
+ if (authentication?.gateway) {
436
+ manifest.gateway = authentication.gateway;
437
+ }
438
+
439
+ return manifest;
440
+ }
441
+
442
+ export function extractManifest(
443
+ config: ApitogoJsonConfig | null | undefined,
444
+ ): ApitogoJsonConfig | null {
445
+ if (!config || typeof config !== "object") {
446
+ return null;
447
+ }
448
+
449
+ const manifest = deriveManifestFields(config);
450
+ return Object.keys(manifest).length > 0 ? manifest : null;
451
+ }
452
+
453
+ export function extractSiteConfig(
454
+ config: ApitogoJsonConfig | null | undefined,
455
+ ): ApitogoJsonConfig {
456
+ if (!config || typeof config !== "object") {
457
+ return {};
458
+ }
459
+
460
+ const normalized = normalizeApitogoConfig(config);
461
+ const siteConfig: ApitogoJsonConfig = {};
462
+
463
+ for (const key of SITE_CONFIG_KEYS) {
464
+ if (normalized[key] !== undefined) {
465
+ siteConfig[key] = normalized[key];
466
+ }
467
+ }
468
+
469
+ if (normalized.authentication !== undefined) {
470
+ siteConfig.authentication = normalized.authentication;
471
+ }
472
+
473
+ const modulesDocs = asRecord(asRecord(normalized.modules)?.docs);
474
+ if (modulesDocs) {
475
+ const { enabled: _enabled, ...docsConfig } = modulesDocs;
476
+ siteConfig.docs = docsConfig;
477
+ }
478
+
479
+ return siteConfig;
480
+ }
481
+
482
+ /** @deprecated Use {@link readSiteTitle} via normalized config — kept for MCP bootstrap naming. */
483
+ export function resolveProjectDisplayName(config: ApitogoJsonConfig): string {
484
+ return readSiteTitle(config) ?? "Developer Portal";
485
+ }
@@ -0,0 +1,68 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ APITOGO_CONFIG_FILENAME,
5
+ applyEnvOverrides,
6
+ type ApitogoJsonConfig,
7
+ type AuthenticationSecrets,
8
+ normalizeApitogoConfig,
9
+ } from "./apitogo-config-core.js";
10
+
11
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
12
+ return value !== undefined &&
13
+ value !== null &&
14
+ typeof value === "object" &&
15
+ !Array.isArray(value)
16
+ ? (value as Record<string, unknown>)
17
+ : undefined;
18
+ }
19
+
20
+ export function loadApitogoConfig(rootDir = process.cwd()): ApitogoJsonConfig {
21
+ const configPath = path.join(path.resolve(rootDir), APITOGO_CONFIG_FILENAME);
22
+
23
+ if (!existsSync(configPath)) {
24
+ throw new Error(
25
+ `Missing ${APITOGO_CONFIG_FILENAME} in ${path.resolve(rootDir)}`,
26
+ );
27
+ }
28
+
29
+ const raw = readFileSync(configPath, "utf8");
30
+ const parsed = JSON.parse(raw) as ApitogoJsonConfig;
31
+ return normalizeApitogoConfig(applyEnvOverrides(parsed, process.env));
32
+ }
33
+
34
+ export function readAuthenticationSecrets(
35
+ env: NodeJS.ProcessEnv = process.env,
36
+ ): AuthenticationSecrets {
37
+ const overrides = applyEnvOverrides({}, env);
38
+ const authentication = asRecord(overrides.authentication);
39
+ const oidc = asRecord(authentication?.oidc);
40
+ const stripe = asRecord(authentication?.stripe);
41
+
42
+ const secrets: AuthenticationSecrets = {};
43
+ if (oidc) {
44
+ secrets.oidc = {
45
+ authority:
46
+ typeof oidc.authority === "string" ? oidc.authority : undefined,
47
+ clientId: typeof oidc.clientId === "string" ? oidc.clientId : undefined,
48
+ clientSecret:
49
+ typeof oidc.clientSecret === "string" ? oidc.clientSecret : undefined,
50
+ };
51
+ }
52
+ if (stripe) {
53
+ secrets.stripe = {
54
+ secretKey:
55
+ typeof stripe.secretKey === "string" ? stripe.secretKey : undefined,
56
+ webhookSecret:
57
+ typeof stripe.webhookSecret === "string"
58
+ ? stripe.webhookSecret
59
+ : undefined,
60
+ };
61
+ }
62
+
63
+ return secrets;
64
+ }
65
+
66
+ export function apitogoConfigPath(rootDir: string): string {
67
+ return path.join(path.resolve(rootDir), APITOGO_CONFIG_FILENAME);
68
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./apitogo-config-core.js";
2
+ export {
3
+ apitogoConfigPath,
4
+ loadApitogoConfig,
5
+ readAuthenticationSecrets,
6
+ } from "./apitogo-config-loader.node.js";
@@ -0,0 +1,52 @@
1
+ import type { ApitogoPlugin } from "../lib/core/plugins.js";
2
+ import {
3
+ extractSiteConfig,
4
+ normalizeApitogoConfig,
5
+ type ApitogoJsonConfig,
6
+ } from "./apitogo-config-core.js";
7
+ import type { ApitogoConfig } from "./config.js";
8
+
9
+ type BuildApitogoConfigOptions = Partial<ApitogoConfig> &
10
+ ApitogoJsonConfig & {
11
+ plugins?: ApitogoPlugin[];
12
+ };
13
+
14
+ export function buildApitogoConfig({
15
+ plugins,
16
+ plans: _legacyPlans,
17
+ ...jsonOverrides
18
+ }: BuildApitogoConfigOptions = {}): ApitogoConfig {
19
+ const jsonConfig = normalizeApitogoConfig({
20
+ ...jsonOverrides,
21
+ ...(_legacyPlans !== undefined ? { plans: _legacyPlans } : {}),
22
+ } as ApitogoJsonConfig);
23
+ const siteConfig = extractSiteConfig(jsonConfig);
24
+ const { plans: _rootPlans, ...restOverrides } =
25
+ jsonOverrides as ApitogoJsonConfig;
26
+
27
+ return {
28
+ ...siteConfig,
29
+ ...restOverrides,
30
+ plugins,
31
+ authentication: {
32
+ ...(siteConfig.authentication as ApitogoConfig["authentication"]),
33
+ ...jsonOverrides.authentication,
34
+ },
35
+ site: {
36
+ ...(siteConfig.site as ApitogoConfig["site"]),
37
+ ...jsonOverrides.site,
38
+ },
39
+ metadata: {
40
+ ...(siteConfig.metadata as ApitogoConfig["metadata"]),
41
+ ...jsonOverrides.metadata,
42
+ },
43
+ docs: {
44
+ ...(siteConfig.docs as ApitogoConfig["docs"]),
45
+ ...jsonOverrides.docs,
46
+ },
47
+ modules: {
48
+ ...(jsonConfig.modules as ApitogoConfig["modules"]),
49
+ ...jsonOverrides.modules,
50
+ },
51
+ } as ApitogoConfig;
52
+ }