everything-dev 0.2.1 → 0.3.1
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.
- package/package.json +80 -79
- package/src/cli.ts +1491 -1198
- package/src/components/monitor-view.tsx +423 -419
- package/src/config.ts +529 -241
- package/src/contract.ts +381 -364
- package/src/lib/env.ts +83 -65
- package/src/lib/nova.ts +207 -195
- package/src/lib/orchestrator.ts +232 -199
- package/src/lib/process-registry.ts +141 -132
- package/src/lib/process.ts +499 -409
- package/src/lib/resource-monitor/diff.ts +27 -9
- package/src/lib/resource-monitor/platform/darwin.ts +31 -18
- package/src/lib/resource-monitor/snapshot.ts +164 -151
- package/src/plugin.ts +2281 -1841
- package/src/types.ts +182 -83
- package/src/ui/head.ts +37 -26
- package/src/utils/banner.ts +7 -9
- package/src/utils/run.ts +27 -16
- package/src/lib/secrets.ts +0 -29
package/src/config.ts
CHANGED
|
@@ -1,285 +1,573 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { access, readFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { Effect } from "every-plugin/effect";
|
|
5
|
+
import { Graph } from "near-social-js";
|
|
4
6
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
AppConfig,
|
|
8
|
+
BosConfig,
|
|
9
|
+
BosConfigInput,
|
|
10
|
+
PortConfig,
|
|
11
|
+
RemoteConfig,
|
|
12
|
+
RuntimeConfig,
|
|
13
|
+
SourceMode,
|
|
14
|
+
} from "./types";
|
|
15
|
+
import {
|
|
16
|
+
BosConfigInputSchema,
|
|
17
|
+
BosConfigSchema,
|
|
18
|
+
ConfigCircularExtendsError,
|
|
19
|
+
ConfigFetchError,
|
|
20
|
+
ConfigResolutionError,
|
|
13
21
|
} from "./types";
|
|
14
22
|
|
|
15
|
-
export
|
|
23
|
+
// Re-export types
|
|
24
|
+
export type {
|
|
25
|
+
AppConfig,
|
|
26
|
+
BosConfig,
|
|
27
|
+
PortConfig,
|
|
28
|
+
RemoteConfig,
|
|
29
|
+
RuntimeConfig,
|
|
30
|
+
SourceMode,
|
|
31
|
+
};
|
|
16
32
|
|
|
33
|
+
// Constants
|
|
17
34
|
export const DEFAULT_DEV_CONFIG: AppConfig = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
host: "local",
|
|
36
|
+
ui: "local",
|
|
37
|
+
api: "local",
|
|
21
38
|
};
|
|
22
39
|
|
|
40
|
+
// Global state (for caching)
|
|
41
|
+
const configCache = new Map<string, { config: BosConfig; timestamp: number }>();
|
|
23
42
|
let cachedConfig: BosConfig | null = null;
|
|
24
|
-
let
|
|
43
|
+
let projectRoot: string | null = null;
|
|
25
44
|
let configLoaded = false;
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
function findConfigPathSync(startDir: string): string | null {
|
|
40
|
-
let dir = startDir;
|
|
41
|
-
while (dir !== "/") {
|
|
42
|
-
const configPath = join(dir, "bos.config.json");
|
|
43
|
-
if (existsSync(configPath) && statSync(configPath).size > 0) {
|
|
44
|
-
return configPath;
|
|
45
|
-
}
|
|
46
|
-
dir = dirname(dir);
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function loadConfig(cwd?: string): BosConfig | null {
|
|
52
|
-
if (configLoaded) return cachedConfig;
|
|
53
|
-
|
|
54
|
-
const startDir = cwd ?? process.cwd();
|
|
55
|
-
const configPath = findConfigPathSync(startDir);
|
|
56
|
-
|
|
57
|
-
if (!configPath) {
|
|
58
|
-
configLoaded = true;
|
|
59
|
-
configDir = startDir;
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
configDir = dirname(configPath);
|
|
64
|
-
const content = require(configPath);
|
|
65
|
-
cachedConfig = content as BosConfig;
|
|
66
|
-
configLoaded = true;
|
|
67
|
-
return cachedConfig;
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// PUBLIC API (6 methods)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear the config cache. Used by --force flag.
|
|
52
|
+
*/
|
|
53
|
+
export function clearConfigCache(): void {
|
|
54
|
+
configCache.clear();
|
|
55
|
+
cachedConfig = null;
|
|
56
|
+
projectRoot = null;
|
|
57
|
+
configLoaded = false;
|
|
68
58
|
}
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Find the path to bos.config.json by walking up the directory tree.
|
|
62
|
+
*/
|
|
63
|
+
export function findConfigPath(cwd?: string): string | null {
|
|
64
|
+
let dir = cwd ?? process.cwd();
|
|
65
|
+
while (dir !== "/") {
|
|
66
|
+
const configPath = join(dir, "bos.config.json");
|
|
67
|
+
if (existsSync(configPath) && statSync(configPath).size > 0) {
|
|
68
|
+
return configPath;
|
|
69
|
+
}
|
|
70
|
+
dir = dirname(dir);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Get the cached config. Returns null if not loaded yet.
|
|
77
|
+
*/
|
|
78
|
+
export function getConfig(): BosConfig | null {
|
|
79
|
+
return cachedConfig;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Get the project root directory (where bos.config.json is located).
|
|
84
|
+
* Throws if config hasn't been loaded.
|
|
85
|
+
*/
|
|
86
|
+
export function getProjectRoot(): string {
|
|
87
|
+
if (!configLoaded || !projectRoot) {
|
|
88
|
+
throw new Error("Config not loaded. Call loadConfig() first.");
|
|
89
|
+
}
|
|
90
|
+
return projectRoot;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Result of loading config - includes everything needed by callers.
|
|
95
|
+
*/
|
|
96
|
+
export interface ConfigResult {
|
|
97
|
+
/** The validated BosConfig */
|
|
98
|
+
config: BosConfig;
|
|
99
|
+
/** Environment-specific runtime config */
|
|
100
|
+
runtime: RuntimeConfig;
|
|
101
|
+
/** Information about where/how the config was loaded */
|
|
102
|
+
source: {
|
|
103
|
+
path: string;
|
|
104
|
+
extended?: string[];
|
|
105
|
+
remote?: boolean;
|
|
106
|
+
};
|
|
107
|
+
/** Package information */
|
|
108
|
+
packages: {
|
|
109
|
+
all: string[];
|
|
110
|
+
resolved: PackageResolution[];
|
|
111
|
+
};
|
|
93
112
|
}
|
|
94
113
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Resolution info for a single package.
|
|
116
|
+
*/
|
|
117
|
+
export interface PackageResolution {
|
|
118
|
+
name: string;
|
|
119
|
+
mode: SourceMode;
|
|
120
|
+
exists: boolean;
|
|
121
|
+
port: number;
|
|
122
|
+
url: string;
|
|
103
123
|
}
|
|
104
124
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Primary config loader - handles everything.
|
|
127
|
+
* - Local config loading
|
|
128
|
+
* - Config inheritance (extends)
|
|
129
|
+
* - BOS URL fetching
|
|
130
|
+
* - Caching
|
|
131
|
+
* - Runtime config generation
|
|
132
|
+
*/
|
|
133
|
+
export async function loadConfig(options?: {
|
|
134
|
+
cwd?: string;
|
|
135
|
+
path?: string;
|
|
136
|
+
force?: boolean;
|
|
137
|
+
env?: "development" | "production";
|
|
138
|
+
}): Promise<ConfigResult | null> {
|
|
139
|
+
// Return cached result if available and not forcing reload
|
|
140
|
+
if (configLoaded && cachedConfig && !options?.force) {
|
|
141
|
+
const runtime = buildRuntimeConfig(
|
|
142
|
+
cachedConfig,
|
|
143
|
+
options?.env ?? "development",
|
|
144
|
+
);
|
|
145
|
+
return {
|
|
146
|
+
config: cachedConfig,
|
|
147
|
+
runtime,
|
|
148
|
+
source: { path: join(projectRoot!, "bos.config.json") },
|
|
149
|
+
packages: {
|
|
150
|
+
all: Object.keys(cachedConfig.app),
|
|
151
|
+
resolved: [], // Will be populated by resolvePackages
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find or use explicit config path
|
|
157
|
+
const configPath = options?.path ?? findConfigPath(options?.cwd);
|
|
158
|
+
if (!configPath) {
|
|
159
|
+
configLoaded = true;
|
|
160
|
+
projectRoot = options?.cwd ?? process.cwd();
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const baseDir = dirname(configPath);
|
|
165
|
+
|
|
166
|
+
// Check cache for this specific file
|
|
167
|
+
const cached = configCache.get(configPath);
|
|
168
|
+
if (cached && !options?.force) {
|
|
169
|
+
projectRoot = baseDir;
|
|
170
|
+
cachedConfig = cached.config;
|
|
171
|
+
configLoaded = true;
|
|
172
|
+
const runtime = buildRuntimeConfig(
|
|
173
|
+
cached.config,
|
|
174
|
+
options?.env ?? "development",
|
|
175
|
+
);
|
|
176
|
+
return {
|
|
177
|
+
config: cached.config,
|
|
178
|
+
runtime,
|
|
179
|
+
source: { path: configPath },
|
|
180
|
+
packages: {
|
|
181
|
+
all: Object.keys(cached.config.app),
|
|
182
|
+
resolved: [],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Resolve config with extends
|
|
189
|
+
const extendedChain: string[] = [];
|
|
190
|
+
const rawConfig = await resolveConfigWithExtends(
|
|
191
|
+
configPath,
|
|
192
|
+
baseDir,
|
|
193
|
+
new Set(),
|
|
194
|
+
extendedChain,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Validate with strict schema
|
|
198
|
+
const validated = BosConfigSchema.parse(rawConfig);
|
|
199
|
+
|
|
200
|
+
// Cache result
|
|
201
|
+
configCache.set(configPath, { config: validated, timestamp: Date.now() });
|
|
202
|
+
projectRoot = baseDir;
|
|
203
|
+
cachedConfig = validated;
|
|
204
|
+
configLoaded = true;
|
|
205
|
+
|
|
206
|
+
// Build runtime config
|
|
207
|
+
const runtime = buildRuntimeConfig(
|
|
208
|
+
validated,
|
|
209
|
+
options?.env ?? "development",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
config: validated,
|
|
214
|
+
runtime,
|
|
215
|
+
source: {
|
|
216
|
+
path: configPath,
|
|
217
|
+
extended: extendedChain.length > 0 ? extendedChain : undefined,
|
|
218
|
+
remote: extendedChain.some((e) => e.startsWith("bos://")),
|
|
219
|
+
},
|
|
220
|
+
packages: {
|
|
221
|
+
all: Object.keys(validated.app),
|
|
222
|
+
resolved: [],
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error instanceof ConfigCircularExtendsError) {
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
if (error instanceof ConfigFetchError) {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
if (error instanceof ConfigResolutionError) {
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
throw new ConfigResolutionError(
|
|
236
|
+
`Failed to load config from ${configPath}: ${error}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
111
239
|
}
|
|
112
240
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Resolve packages with modes, existence checks, and auto-remote detection.
|
|
243
|
+
* Merges functionality of resolvePackageModes() and getExistingPackages().
|
|
244
|
+
*/
|
|
245
|
+
export async function resolvePackages(
|
|
246
|
+
packages: string[],
|
|
247
|
+
requestedModes: Record<string, SourceMode>,
|
|
248
|
+
): Promise<{
|
|
249
|
+
resolved: Record<string, PackageResolution>;
|
|
250
|
+
autoRemote: string[];
|
|
251
|
+
}> {
|
|
252
|
+
const dir = getProjectRoot();
|
|
253
|
+
const resolved: Record<string, PackageResolution> = {};
|
|
254
|
+
const autoRemote: string[] = [];
|
|
255
|
+
|
|
256
|
+
// Get config for port calculations
|
|
257
|
+
const config = getConfig();
|
|
258
|
+
|
|
259
|
+
for (const pkg of packages) {
|
|
260
|
+
const exists = await fileExists(`${dir}/${pkg}/package.json`);
|
|
261
|
+
const requestedMode = requestedModes[pkg] ?? "local";
|
|
262
|
+
|
|
263
|
+
// Auto-switch to remote if package doesn't exist locally
|
|
264
|
+
const mode =
|
|
265
|
+
!exists && requestedMode === "local" ? "remote" : requestedMode;
|
|
266
|
+
if (!exists && requestedMode === "local") {
|
|
267
|
+
autoRemote.push(pkg);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Calculate port and URL
|
|
271
|
+
let port = 0;
|
|
272
|
+
let url = "";
|
|
273
|
+
|
|
274
|
+
if (config && pkg in config.app) {
|
|
275
|
+
const pkgConfig = config.app[pkg];
|
|
276
|
+
if (pkg === "host") {
|
|
277
|
+
port = parsePort(pkgConfig.development);
|
|
278
|
+
url = mode === "remote" ? pkgConfig.production : pkgConfig.development;
|
|
279
|
+
} else {
|
|
280
|
+
// For ui/api and other packages, access via index signature
|
|
281
|
+
const devUrl = (pkgConfig as { development?: string }).development;
|
|
282
|
+
const prodUrl = (pkgConfig as { production?: string }).production;
|
|
283
|
+
if (devUrl) {
|
|
284
|
+
port = parsePort(devUrl);
|
|
285
|
+
url = mode === "remote" ? (prodUrl ?? devUrl) : devUrl;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
resolved[pkg] = { name: pkg, mode, exists, port, url };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { resolved, autoRemote };
|
|
122
294
|
}
|
|
123
295
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new Error("No bos.config.json found");
|
|
128
|
-
}
|
|
129
|
-
return config.account;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function getComponentUrl(
|
|
133
|
-
component: "host" | "ui" | "api",
|
|
134
|
-
source: SourceMode
|
|
135
|
-
): string {
|
|
136
|
-
const config = loadConfig();
|
|
137
|
-
if (!config) {
|
|
138
|
-
throw new Error("No bos.config.json found");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (component === "host") {
|
|
142
|
-
return source === "remote" ? config.app.host.production : config.app.host.development;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const componentConfig = config.app[component];
|
|
146
|
-
if (!componentConfig || !("name" in componentConfig)) {
|
|
147
|
-
throw new Error(`Component ${component} not found in bos.config.json`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return source === "remote" ? componentConfig.production : componentConfig.development;
|
|
151
|
-
}
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// INTERNAL HELPERS
|
|
298
|
+
// ============================================================================
|
|
152
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Parse port from URL string.
|
|
302
|
+
*/
|
|
153
303
|
export function parsePort(url: string): number {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!config) {
|
|
165
|
-
return { host: 3000, ui: 3002, api: 3014 };
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
host: parsePort(config.app.host.development),
|
|
169
|
-
ui: config.app.ui ? parsePort((config.app.ui as RemoteConfig).development) : 3002,
|
|
170
|
-
api: config.app.api ? parsePort((config.app.api as RemoteConfig).development) : 3014,
|
|
171
|
-
};
|
|
304
|
+
try {
|
|
305
|
+
const parsed = new URL(url);
|
|
306
|
+
return parsed.port
|
|
307
|
+
? parseInt(parsed.port, 10)
|
|
308
|
+
: parsed.protocol === "https:"
|
|
309
|
+
? 443
|
|
310
|
+
: 80;
|
|
311
|
+
} catch {
|
|
312
|
+
return 3000;
|
|
313
|
+
}
|
|
172
314
|
}
|
|
173
315
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
export function getHostRemoteUrl(): string | undefined {
|
|
182
|
-
const config = loadConfig();
|
|
183
|
-
if (!config) return undefined;
|
|
184
|
-
return config.app.host.production || undefined;
|
|
316
|
+
/**
|
|
317
|
+
* Check if a file exists.
|
|
318
|
+
*/
|
|
319
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
320
|
+
return access(path)
|
|
321
|
+
.then(() => true)
|
|
322
|
+
.catch(() => false);
|
|
185
323
|
}
|
|
186
324
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Resolve BOS URL to Graph path.
|
|
327
|
+
* bos://{account}/{gateway} → {account}/bos/gateways/{gateway}/bos.config.json
|
|
328
|
+
*/
|
|
329
|
+
function resolveBosUrl(bosUrl: string): string {
|
|
330
|
+
const match = bosUrl.match(/^bos:\/\/([^/]+)\/(.+)$/);
|
|
331
|
+
if (!match) {
|
|
332
|
+
throw new ConfigResolutionError(
|
|
333
|
+
`Invalid BOS URL format: ${bosUrl}. Expected: bos://{account}/{gateway}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const [, account, gateway] = match;
|
|
337
|
+
return `${account}/bos/gateways/${gateway}/bos.config.json`;
|
|
193
338
|
}
|
|
194
339
|
|
|
195
|
-
|
|
196
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Fetch config from NEAR Social (BOS URL) with 10s timeout.
|
|
342
|
+
*/
|
|
343
|
+
async function fetchBosConfig(bosUrl: string): Promise<BosConfigInput> {
|
|
344
|
+
const configPath = resolveBosUrl(bosUrl);
|
|
345
|
+
const graph = new Graph();
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const data = await Effect.runPromise(
|
|
349
|
+
Effect.tryPromise({
|
|
350
|
+
try: () =>
|
|
351
|
+
Promise.race([
|
|
352
|
+
graph.get({ keys: [configPath] }),
|
|
353
|
+
new Promise<never>((_, reject) =>
|
|
354
|
+
setTimeout(() => reject(new Error("Timeout after 10s")), 10000),
|
|
355
|
+
),
|
|
356
|
+
]),
|
|
357
|
+
catch: (e) => new ConfigFetchError(bosUrl, e),
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (!data) {
|
|
362
|
+
throw new ConfigFetchError(bosUrl, "No data returned");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Navigate nested structure
|
|
366
|
+
const parts = configPath.split("/");
|
|
367
|
+
let current: unknown = data;
|
|
368
|
+
for (const part of parts) {
|
|
369
|
+
if (current && typeof current === "object" && part in current) {
|
|
370
|
+
current = (current as Record<string, unknown>)[part];
|
|
371
|
+
} else {
|
|
372
|
+
throw new ConfigFetchError(bosUrl, `Path not found: ${configPath}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (typeof current !== "string") {
|
|
377
|
+
throw new ConfigFetchError(bosUrl, "Config is not a string");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return JSON.parse(current) as BosConfigInput;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (error instanceof ConfigFetchError) {
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
throw new ConfigFetchError(bosUrl, error);
|
|
386
|
+
}
|
|
197
387
|
}
|
|
198
388
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Load a single config file (local or BOS URL).
|
|
391
|
+
*/
|
|
392
|
+
async function loadConfigFile(
|
|
393
|
+
configPath: string,
|
|
394
|
+
baseDir?: string,
|
|
395
|
+
): Promise<BosConfigInput> {
|
|
396
|
+
// BOS URL
|
|
397
|
+
if (configPath.startsWith("bos://")) {
|
|
398
|
+
return fetchBosConfig(configPath);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Local file
|
|
402
|
+
const resolvedPath = isAbsolute(configPath)
|
|
403
|
+
? configPath
|
|
404
|
+
: baseDir
|
|
405
|
+
? resolve(baseDir, configPath)
|
|
406
|
+
: resolve(process.cwd(), configPath);
|
|
407
|
+
|
|
408
|
+
const text = await readFile(resolvedPath, "utf-8");
|
|
409
|
+
return JSON.parse(text) as BosConfigInput;
|
|
202
410
|
}
|
|
203
411
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Deep merge configs with inheritance.
|
|
414
|
+
* - Objects: recursive merge
|
|
415
|
+
* - Arrays: child replaces parent
|
|
416
|
+
* - Primitives: child wins
|
|
417
|
+
*/
|
|
418
|
+
function mergeConfigs(
|
|
419
|
+
parent: BosConfigInput,
|
|
420
|
+
child: BosConfigInput,
|
|
421
|
+
): BosConfigInput {
|
|
422
|
+
const merged: BosConfigInput = { ...parent, ...child };
|
|
423
|
+
|
|
424
|
+
// Merge app configs deeply
|
|
425
|
+
if (parent.app || child.app) {
|
|
426
|
+
merged.app = { ...parent.app, ...child.app };
|
|
427
|
+
const parentApps = parent.app || {};
|
|
428
|
+
const childApps = child.app || {};
|
|
429
|
+
|
|
430
|
+
for (const key of new Set([
|
|
431
|
+
...Object.keys(parentApps),
|
|
432
|
+
...Object.keys(childApps),
|
|
433
|
+
])) {
|
|
434
|
+
const parentApp = parentApps[key as keyof typeof parentApps];
|
|
435
|
+
const childApp = childApps[key as keyof typeof childApps];
|
|
436
|
+
|
|
437
|
+
if (parentApp && childApp) {
|
|
438
|
+
(merged.app as Record<string, unknown>)[key] = {
|
|
439
|
+
...parentApp,
|
|
440
|
+
...childApp,
|
|
441
|
+
};
|
|
442
|
+
} else if (childApp) {
|
|
443
|
+
(merged.app as Record<string, unknown>)[key] = childApp;
|
|
444
|
+
} else if (parentApp) {
|
|
445
|
+
(merged.app as Record<string, unknown>)[key] = parentApp;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Merge shared deps deeply
|
|
451
|
+
if (parent.shared || child.shared) {
|
|
452
|
+
merged.shared = { ...parent.shared, ...child.shared };
|
|
453
|
+
const parentShared = parent.shared || {};
|
|
454
|
+
const childShared = child.shared || {};
|
|
455
|
+
|
|
456
|
+
// Deep merge each shared category (ui, api, etc.)
|
|
457
|
+
for (const category of new Set([
|
|
458
|
+
...Object.keys(parentShared),
|
|
459
|
+
...Object.keys(childShared),
|
|
460
|
+
])) {
|
|
461
|
+
const parentCategory =
|
|
462
|
+
parentShared[category as keyof typeof parentShared];
|
|
463
|
+
const childCategory = childShared[category as keyof typeof childShared];
|
|
464
|
+
|
|
465
|
+
if (parentCategory && childCategory) {
|
|
466
|
+
(merged.shared as Record<string, unknown>)[category] = {
|
|
467
|
+
...parentCategory,
|
|
468
|
+
...childCategory,
|
|
469
|
+
};
|
|
470
|
+
} else if (childCategory) {
|
|
471
|
+
(merged.shared as Record<string, unknown>)[category] = childCategory;
|
|
472
|
+
} else if (parentCategory) {
|
|
473
|
+
(merged.shared as Record<string, unknown>)[category] = parentCategory;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return merged;
|
|
225
479
|
}
|
|
226
480
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Recursively resolve config with extends inheritance.
|
|
483
|
+
*/
|
|
484
|
+
async function resolveConfigWithExtends(
|
|
485
|
+
configPath: string,
|
|
486
|
+
baseDir: string,
|
|
487
|
+
visited: Set<string>,
|
|
488
|
+
chain: string[],
|
|
489
|
+
): Promise<BosConfigInput> {
|
|
490
|
+
// Check for circular dependencies
|
|
491
|
+
if (visited.has(configPath)) {
|
|
492
|
+
throw new ConfigCircularExtendsError([...visited, configPath]);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Load current config
|
|
496
|
+
const config = await loadConfigFile(configPath, baseDir);
|
|
497
|
+
|
|
498
|
+
// Track in chain if it's a BOS URL
|
|
499
|
+
if (configPath.startsWith("bos://")) {
|
|
500
|
+
chain.push(configPath);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// If no extends, return as-is
|
|
504
|
+
if (!config.extends) {
|
|
505
|
+
return config;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Mark as visited
|
|
509
|
+
const newVisited = new Set(visited);
|
|
510
|
+
newVisited.add(configPath);
|
|
511
|
+
|
|
512
|
+
// Resolve parent
|
|
513
|
+
const parentPath = config.extends;
|
|
514
|
+
const parentBaseDir = parentPath.startsWith("bos://")
|
|
515
|
+
? baseDir
|
|
516
|
+
: isAbsolute(parentPath)
|
|
517
|
+
? dirname(parentPath)
|
|
518
|
+
: baseDir;
|
|
519
|
+
|
|
520
|
+
const parentConfig = await resolveConfigWithExtends(
|
|
521
|
+
parentPath,
|
|
522
|
+
parentBaseDir,
|
|
523
|
+
newVisited,
|
|
524
|
+
chain,
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Merge and return
|
|
528
|
+
return mergeConfigs(parentConfig, config);
|
|
242
529
|
}
|
|
243
530
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Build runtime config from BosConfig.
|
|
533
|
+
*/
|
|
534
|
+
function buildRuntimeConfig(
|
|
535
|
+
config: BosConfig,
|
|
536
|
+
env: "development" | "production",
|
|
537
|
+
): RuntimeConfig {
|
|
538
|
+
const uiConfig = config.app.ui as RemoteConfig | undefined;
|
|
539
|
+
const apiConfig = config.app.api as RemoteConfig | undefined;
|
|
540
|
+
|
|
541
|
+
// Extract properties from api config
|
|
542
|
+
const apiProxy = apiConfig?.proxy;
|
|
543
|
+
const apiVariables = apiConfig?.variables;
|
|
544
|
+
const apiSecrets = apiConfig?.secrets;
|
|
545
|
+
|
|
546
|
+
// Env var overrides
|
|
547
|
+
const uiUrlOverride = process.env.BOS_UI_URL;
|
|
548
|
+
const uiSsrUrlOverride = process.env.BOS_UI_SSR_URL;
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
env,
|
|
552
|
+
account: config.account,
|
|
553
|
+
title: config.account,
|
|
554
|
+
hostUrl: config.app.host[env],
|
|
555
|
+
shared: config.shared,
|
|
556
|
+
ui: {
|
|
557
|
+
name: uiConfig?.name ?? "ui",
|
|
558
|
+
url: uiUrlOverride ?? uiConfig?.[env] ?? "",
|
|
559
|
+
entry: `${uiUrlOverride ?? uiConfig?.[env] ?? ""}/remoteEntry.js`,
|
|
560
|
+
ssrUrl: uiSsrUrlOverride ?? uiConfig?.ssr,
|
|
561
|
+
source: env === "development" ? "local" : "remote",
|
|
562
|
+
},
|
|
563
|
+
api: {
|
|
564
|
+
name: apiConfig?.name ?? "api",
|
|
565
|
+
url: apiConfig?.[env] ?? "",
|
|
566
|
+
entry: `${apiConfig?.[env] ?? ""}/remoteEntry.js`,
|
|
567
|
+
source: env === "development" ? "local" : "remote",
|
|
568
|
+
proxy: apiProxy,
|
|
569
|
+
variables: apiVariables,
|
|
570
|
+
secrets: apiSecrets,
|
|
571
|
+
},
|
|
572
|
+
};
|
|
285
573
|
}
|