everything-dev 0.3.3 → 1.3.3

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 (313) hide show
  1. package/README.md +64 -0
  2. package/cli.js +10 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +29 -0
  4. package/dist/api-contract.cjs +172 -0
  5. package/dist/api-contract.cjs.map +1 -0
  6. package/dist/api-contract.mjs +171 -0
  7. package/dist/api-contract.mjs.map +1 -0
  8. package/dist/api.cjs +124 -0
  9. package/dist/api.cjs.map +1 -0
  10. package/dist/api.d.cts +36 -0
  11. package/dist/api.d.cts.map +1 -0
  12. package/dist/api.d.mts +36 -0
  13. package/dist/api.d.mts.map +1 -0
  14. package/dist/api.mjs +119 -0
  15. package/dist/api.mjs.map +1 -0
  16. package/dist/app.cjs +156 -0
  17. package/dist/app.cjs.map +1 -0
  18. package/dist/app.mjs +153 -0
  19. package/dist/app.mjs.map +1 -0
  20. package/dist/cli/catalog.cjs +30 -0
  21. package/dist/cli/catalog.cjs.map +1 -0
  22. package/dist/cli/catalog.mjs +29 -0
  23. package/dist/cli/catalog.mjs.map +1 -0
  24. package/dist/cli/help.cjs +16 -0
  25. package/dist/cli/help.cjs.map +1 -0
  26. package/dist/cli/help.mjs +16 -0
  27. package/dist/cli/help.mjs.map +1 -0
  28. package/dist/cli/init.cjs +287 -0
  29. package/dist/cli/init.cjs.map +1 -0
  30. package/dist/cli/init.d.cts +36 -0
  31. package/dist/cli/init.d.cts.map +1 -0
  32. package/dist/cli/init.d.mts +36 -0
  33. package/dist/cli/init.d.mts.map +1 -0
  34. package/dist/cli/init.mjs +279 -0
  35. package/dist/cli/init.mjs.map +1 -0
  36. package/dist/cli/parse.cjs +96 -0
  37. package/dist/cli/parse.cjs.map +1 -0
  38. package/dist/cli/parse.mjs +95 -0
  39. package/dist/cli/parse.mjs.map +1 -0
  40. package/dist/cli/prompts.cjs +42 -0
  41. package/dist/cli/prompts.cjs.map +1 -0
  42. package/dist/cli/prompts.mjs +41 -0
  43. package/dist/cli/prompts.mjs.map +1 -0
  44. package/dist/cli.cjs +167 -0
  45. package/dist/cli.cjs.map +1 -0
  46. package/dist/cli.d.cts +1 -0
  47. package/dist/cli.d.mts +1 -0
  48. package/dist/cli.mjs +166 -0
  49. package/dist/cli.mjs.map +1 -0
  50. package/dist/components/dev-view.cjs +307 -0
  51. package/dist/components/dev-view.cjs.map +1 -0
  52. package/dist/components/dev-view.mjs +306 -0
  53. package/dist/components/dev-view.mjs.map +1 -0
  54. package/dist/components/streaming-view.cjs +146 -0
  55. package/dist/components/streaming-view.cjs.map +1 -0
  56. package/dist/components/streaming-view.mjs +144 -0
  57. package/dist/components/streaming-view.mjs.map +1 -0
  58. package/dist/config.cjs +280 -0
  59. package/dist/config.cjs.map +1 -0
  60. package/dist/config.d.cts +35 -0
  61. package/dist/config.d.cts.map +1 -0
  62. package/dist/config.d.mts +35 -0
  63. package/dist/config.d.mts.map +1 -0
  64. package/dist/config.mjs +266 -0
  65. package/dist/config.mjs.map +1 -0
  66. package/dist/contract.cjs +209 -0
  67. package/dist/contract.cjs.map +1 -0
  68. package/dist/contract.d.cts +490 -0
  69. package/dist/contract.d.cts.map +1 -0
  70. package/dist/contract.d.mts +490 -0
  71. package/dist/contract.d.mts.map +1 -0
  72. package/dist/contract.meta.cjs +104 -0
  73. package/dist/contract.meta.cjs.map +1 -0
  74. package/dist/contract.meta.d.cts +141 -0
  75. package/dist/contract.meta.d.cts.map +1 -0
  76. package/dist/contract.meta.d.mts +141 -0
  77. package/dist/contract.meta.d.mts.map +1 -0
  78. package/dist/contract.meta.mjs +102 -0
  79. package/dist/contract.meta.mjs.map +1 -0
  80. package/dist/contract.mjs +186 -0
  81. package/dist/contract.mjs.map +1 -0
  82. package/dist/dev-logs.cjs +53 -0
  83. package/dist/dev-logs.cjs.map +1 -0
  84. package/dist/dev-logs.mjs +51 -0
  85. package/dist/dev-logs.mjs.map +1 -0
  86. package/dist/dev-session.cjs +195 -0
  87. package/dist/dev-session.cjs.map +1 -0
  88. package/dist/dev-session.mjs +194 -0
  89. package/dist/dev-session.mjs.map +1 -0
  90. package/dist/fastkv.cjs +89 -0
  91. package/dist/fastkv.cjs.map +1 -0
  92. package/dist/fastkv.d.cts +11 -0
  93. package/dist/fastkv.d.cts.map +1 -0
  94. package/dist/fastkv.d.mts +11 -0
  95. package/dist/fastkv.d.mts.map +1 -0
  96. package/dist/fastkv.mjs +82 -0
  97. package/dist/fastkv.mjs.map +1 -0
  98. package/dist/federation.server.cjs +27 -0
  99. package/dist/federation.server.cjs.map +1 -0
  100. package/dist/federation.server.mjs +27 -0
  101. package/dist/federation.server.mjs.map +1 -0
  102. package/dist/host.cjs +367 -0
  103. package/dist/host.cjs.map +1 -0
  104. package/dist/host.d.cts +22 -0
  105. package/dist/host.d.cts.map +1 -0
  106. package/dist/host.d.mts +22 -0
  107. package/dist/host.d.mts.map +1 -0
  108. package/dist/host.mjs +364 -0
  109. package/dist/host.mjs.map +1 -0
  110. package/dist/index.cjs +122 -0
  111. package/dist/index.d.cts +7 -0
  112. package/dist/index.d.mts +7 -0
  113. package/dist/index.mjs +9 -0
  114. package/dist/integrity.cjs +39 -0
  115. package/dist/integrity.cjs.map +1 -0
  116. package/dist/integrity.d.cts +7 -0
  117. package/dist/integrity.d.cts.map +1 -0
  118. package/dist/integrity.d.mts +7 -0
  119. package/dist/integrity.d.mts.map +1 -0
  120. package/dist/integrity.mjs +35 -0
  121. package/dist/integrity.mjs.map +1 -0
  122. package/dist/internal/manifest-normalizer.cjs +140 -0
  123. package/dist/internal/manifest-normalizer.cjs.map +1 -0
  124. package/dist/internal/manifest-normalizer.mjs +138 -0
  125. package/dist/internal/manifest-normalizer.mjs.map +1 -0
  126. package/dist/mf.cjs +77 -0
  127. package/dist/mf.cjs.map +1 -0
  128. package/dist/mf.d.cts +19 -0
  129. package/dist/mf.d.cts.map +1 -0
  130. package/dist/mf.d.mts +19 -0
  131. package/dist/mf.d.mts.map +1 -0
  132. package/dist/mf.mjs +71 -0
  133. package/dist/mf.mjs.map +1 -0
  134. package/dist/near-cli.cjs +196 -0
  135. package/dist/near-cli.cjs.map +1 -0
  136. package/dist/near-cli.mjs +193 -0
  137. package/dist/near-cli.mjs.map +1 -0
  138. package/dist/network.cjs +9 -0
  139. package/dist/network.cjs.map +1 -0
  140. package/dist/network.mjs +8 -0
  141. package/dist/network.mjs.map +1 -0
  142. package/dist/orchestrator.cjs +441 -0
  143. package/dist/orchestrator.cjs.map +1 -0
  144. package/dist/orchestrator.d.cts +40 -0
  145. package/dist/orchestrator.d.cts.map +1 -0
  146. package/dist/orchestrator.d.mts +40 -0
  147. package/dist/orchestrator.d.mts.map +1 -0
  148. package/dist/orchestrator.mjs +436 -0
  149. package/dist/orchestrator.mjs.map +1 -0
  150. package/dist/plugin.cjs +830 -0
  151. package/dist/plugin.cjs.map +1 -0
  152. package/dist/plugin.d.cts +347 -0
  153. package/dist/plugin.d.cts.map +1 -0
  154. package/dist/plugin.d.mts +348 -0
  155. package/dist/plugin.d.mts.map +1 -0
  156. package/dist/plugin.mjs +827 -0
  157. package/dist/plugin.mjs.map +1 -0
  158. package/dist/process-registry.cjs +120 -0
  159. package/dist/process-registry.cjs.map +1 -0
  160. package/dist/process-registry.d.cts +25 -0
  161. package/dist/process-registry.d.cts.map +1 -0
  162. package/dist/process-registry.d.mts +25 -0
  163. package/dist/process-registry.d.mts.map +1 -0
  164. package/dist/process-registry.mjs +119 -0
  165. package/dist/process-registry.mjs.map +1 -0
  166. package/dist/sdk.cjs +61 -0
  167. package/dist/sdk.d.cts +5 -0
  168. package/dist/sdk.d.mts +5 -0
  169. package/dist/sdk.mjs +6 -0
  170. package/dist/shared.cjs +143 -0
  171. package/dist/shared.cjs.map +1 -0
  172. package/dist/shared.d.cts +33 -0
  173. package/dist/shared.d.cts.map +1 -0
  174. package/dist/shared.d.mts +33 -0
  175. package/dist/shared.d.mts.map +1 -0
  176. package/dist/shared.mjs +140 -0
  177. package/dist/shared.mjs.map +1 -0
  178. package/dist/types.cjs +160 -0
  179. package/dist/types.cjs.map +1 -0
  180. package/dist/types.d.cts +269 -0
  181. package/dist/types.d.cts.map +1 -0
  182. package/dist/types.d.mts +269 -0
  183. package/dist/types.d.mts.map +1 -0
  184. package/dist/types.mjs +144 -0
  185. package/dist/types.mjs.map +1 -0
  186. package/dist/ui/head.cjs +67 -0
  187. package/dist/ui/head.cjs.map +1 -0
  188. package/dist/ui/head.d.cts +19 -0
  189. package/dist/ui/head.d.cts.map +1 -0
  190. package/dist/ui/head.d.mts +19 -0
  191. package/dist/ui/head.d.mts.map +1 -0
  192. package/dist/ui/head.mjs +61 -0
  193. package/dist/ui/head.mjs.map +1 -0
  194. package/dist/ui/index.cjs +32 -0
  195. package/dist/ui/index.d.cts +7 -0
  196. package/dist/ui/index.d.mts +7 -0
  197. package/dist/ui/index.mjs +6 -0
  198. package/dist/ui/metadata.cjs +106 -0
  199. package/dist/ui/metadata.cjs.map +1 -0
  200. package/dist/ui/metadata.d.cts +35 -0
  201. package/dist/ui/metadata.d.cts.map +1 -0
  202. package/dist/ui/metadata.d.mts +35 -0
  203. package/dist/ui/metadata.d.mts.map +1 -0
  204. package/dist/ui/metadata.mjs +100 -0
  205. package/dist/ui/metadata.mjs.map +1 -0
  206. package/dist/ui/router.cjs +56 -0
  207. package/dist/ui/router.cjs.map +1 -0
  208. package/dist/ui/router.d.cts +11 -0
  209. package/dist/ui/router.d.cts.map +1 -0
  210. package/dist/ui/router.d.mts +11 -0
  211. package/dist/ui/router.d.mts.map +1 -0
  212. package/dist/ui/router.mjs +51 -0
  213. package/dist/ui/router.mjs.map +1 -0
  214. package/dist/ui/runtime.cjs +65 -0
  215. package/dist/ui/runtime.cjs.map +1 -0
  216. package/dist/ui/runtime.d.cts +29 -0
  217. package/dist/ui/runtime.d.cts.map +1 -0
  218. package/dist/ui/runtime.d.mts +29 -0
  219. package/dist/ui/runtime.d.mts.map +1 -0
  220. package/dist/ui/runtime.mjs +53 -0
  221. package/dist/ui/runtime.mjs.map +1 -0
  222. package/dist/ui/types.cjs +0 -0
  223. package/dist/ui/types.d.cts +52 -0
  224. package/dist/ui/types.d.cts.map +1 -0
  225. package/dist/ui/types.d.mts +52 -0
  226. package/dist/ui/types.d.mts.map +1 -0
  227. package/dist/ui/types.mjs +1 -0
  228. package/dist/utils/banner.cjs +24 -0
  229. package/dist/utils/banner.cjs.map +1 -0
  230. package/dist/utils/banner.mjs +23 -0
  231. package/dist/utils/banner.mjs.map +1 -0
  232. package/dist/utils/linkify.cjs +15 -0
  233. package/dist/utils/linkify.cjs.map +1 -0
  234. package/dist/utils/linkify.mjs +14 -0
  235. package/dist/utils/linkify.mjs.map +1 -0
  236. package/dist/utils/run.cjs +40 -0
  237. package/dist/utils/run.cjs.map +1 -0
  238. package/dist/utils/run.mjs +39 -0
  239. package/dist/utils/run.mjs.map +1 -0
  240. package/dist/utils/theme.cjs +44 -0
  241. package/dist/utils/theme.cjs.map +1 -0
  242. package/dist/utils/theme.mjs +37 -0
  243. package/dist/utils/theme.mjs.map +1 -0
  244. package/package.json +269 -80
  245. package/src/api-contract.ts +309 -0
  246. package/src/api.ts +181 -0
  247. package/src/app.ts +346 -0
  248. package/src/cli/catalog.ts +49 -0
  249. package/src/cli/help.ts +13 -0
  250. package/src/cli/init.ts +386 -0
  251. package/src/cli/parse.ts +130 -0
  252. package/src/cli/prompts.ts +64 -0
  253. package/src/cli.ts +203 -1507
  254. package/src/components/dev-view.tsx +307 -255
  255. package/src/components/streaming-view.ts +164 -128
  256. package/src/config.ts +462 -532
  257. package/src/contract.meta.ts +96 -0
  258. package/src/contract.ts +164 -561
  259. package/src/dev-logs.ts +85 -0
  260. package/src/dev-session.ts +318 -0
  261. package/src/fastkv.ts +153 -0
  262. package/src/federation.server.ts +43 -0
  263. package/src/host.ts +526 -0
  264. package/src/index.ts +6 -3
  265. package/src/integrity.ts +54 -0
  266. package/src/internal/manifest-normalizer.ts +251 -0
  267. package/src/mf.ts +105 -0
  268. package/src/near-cli.ts +284 -0
  269. package/src/network.ts +3 -0
  270. package/src/orchestrator.ts +648 -0
  271. package/src/plugin.ts +1130 -2311
  272. package/src/process-registry.ts +154 -0
  273. package/src/scripts/sync-api-contract.ts +24 -0
  274. package/src/sdk.ts +14 -0
  275. package/src/shared.ts +206 -0
  276. package/src/types.ts +152 -206
  277. package/src/ui/head.ts +34 -27
  278. package/src/ui/index.ts +3 -3
  279. package/src/ui/metadata.ts +95 -0
  280. package/src/ui/router.ts +22 -6
  281. package/src/ui/runtime.ts +55 -6
  282. package/src/ui/types.ts +24 -11
  283. package/src/utils/banner.ts +10 -6
  284. package/src/utils/run.ts +26 -27
  285. package/src/utils/theme.ts +3 -66
  286. package/src/components/monitor-view.tsx +0 -475
  287. package/src/components/status-view.tsx +0 -173
  288. package/src/lib/env.ts +0 -109
  289. package/src/lib/near-cli.ts +0 -289
  290. package/src/lib/nova.ts +0 -266
  291. package/src/lib/orchestrator.ts +0 -276
  292. package/src/lib/process-registry.ts +0 -166
  293. package/src/lib/process.ts +0 -550
  294. package/src/lib/resource-monitor/assertions.ts +0 -234
  295. package/src/lib/resource-monitor/command.ts +0 -283
  296. package/src/lib/resource-monitor/diff.ts +0 -157
  297. package/src/lib/resource-monitor/errors.ts +0 -127
  298. package/src/lib/resource-monitor/index.ts +0 -305
  299. package/src/lib/resource-monitor/platform/darwin.ts +0 -306
  300. package/src/lib/resource-monitor/platform/index.ts +0 -35
  301. package/src/lib/resource-monitor/platform/linux.ts +0 -332
  302. package/src/lib/resource-monitor/platform/windows.ts +0 -298
  303. package/src/lib/resource-monitor/snapshot.ts +0 -217
  304. package/src/lib/resource-monitor/types.ts +0 -74
  305. package/src/lib/session-recorder/errors.ts +0 -102
  306. package/src/lib/session-recorder/flows/login.ts +0 -210
  307. package/src/lib/session-recorder/index.ts +0 -361
  308. package/src/lib/session-recorder/playwright.ts +0 -257
  309. package/src/lib/session-recorder/report.ts +0 -353
  310. package/src/lib/session-recorder/server.ts +0 -268
  311. package/src/lib/session-recorder/types.ts +0 -115
  312. package/src/lib/sync.ts +0 -1
  313. package/src/ui/files.ts +0 -134
package/src/config.ts CHANGED
@@ -1,573 +1,503 @@
1
- import { existsSync, statSync } from "node:fs";
2
- import { access, readFile } from "node:fs/promises";
1
+ import { existsSync, readFileSync } from "node:fs";
3
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
- import { Effect } from "every-plugin/effect";
5
- import { Graph } from "near-social-js";
6
- import type {
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,
21
- } from "./types";
22
-
23
- // Re-export types
24
- export type {
25
- AppConfig,
26
- BosConfig,
27
- PortConfig,
28
- RemoteConfig,
29
- RuntimeConfig,
30
- SourceMode,
31
- };
32
-
33
- // Constants
34
- export const DEFAULT_DEV_CONFIG: AppConfig = {
35
- host: "local",
36
- ui: "local",
37
- api: "local",
38
- };
39
-
40
- // Global state (for caching)
41
- const configCache = new Map<string, { config: BosConfig; timestamp: number }>();
3
+ import { fetchBosConfigFromFastKv } from "./fastkv";
4
+ import { getNetworkIdForAccount } from "./network";
5
+ import type { BosConfig, RuntimeConfig, RuntimePluginConfig } from "./types";
6
+ import { BosConfigSchema } from "./types";
7
+
8
+ interface BosConfigInput extends Record<string, unknown> {
9
+ extends?: string;
10
+ development?: string;
11
+ production?: string;
12
+ productionIntegrity?: string;
13
+ proxy?: string;
14
+ variables?: Record<string, string>;
15
+ secrets?: string[];
16
+ app?: Record<string, Record<string, unknown>>;
17
+ shared?: Record<string, Record<string, Record<string, unknown>>>;
18
+ plugins?: Record<string, BosConfigInput>;
19
+ }
20
+
21
+ const LOCAL_PREFIX = "local:";
22
+ const DEFAULT_HOST_PORT = 3000;
23
+
24
+ interface RuntimeTarget {
25
+ source: "local" | "remote";
26
+ url: string;
27
+ localPath?: string;
28
+ port?: number;
29
+ }
30
+
42
31
  let cachedConfig: BosConfig | null = null;
43
32
  let projectRoot: string | null = null;
44
- let configLoaded = false;
45
-
46
- // ============================================================================
47
- // PUBLIC API (6 methods)
48
- // ============================================================================
49
33
 
50
- /**
51
- * Clear the config cache. Used by --force flag.
52
- */
53
34
  export function clearConfigCache(): void {
54
- configCache.clear();
55
- cachedConfig = null;
56
- projectRoot = null;
57
- configLoaded = false;
35
+ cachedConfig = null;
36
+ projectRoot = null;
58
37
  }
59
38
 
60
- /**
61
- * Find the path to bos.config.json by walking up the directory tree.
62
- */
63
39
  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;
40
+ let dir = cwd ?? process.cwd();
41
+ while (dir !== "/") {
42
+ const configPath = join(dir, "bos.config.json");
43
+ if (existsSync(configPath)) {
44
+ return configPath;
45
+ }
46
+ dir = dirname(dir);
47
+ }
48
+ return null;
73
49
  }
74
50
 
75
- /**
76
- * Get the cached config. Returns null if not loaded yet.
77
- */
78
51
  export function getConfig(): BosConfig | null {
79
- return cachedConfig;
52
+ return cachedConfig;
80
53
  }
81
54
 
82
- /**
83
- * Get the project root directory (where bos.config.json is located).
84
- * Throws if config hasn't been loaded.
85
- */
86
55
  export function getProjectRoot(): string {
87
- if (!configLoaded || !projectRoot) {
88
- throw new Error("Config not loaded. Call loadConfig() first.");
89
- }
90
- return projectRoot;
56
+ if (!projectRoot) {
57
+ throw new Error("Config not loaded. Call loadConfig() first.");
58
+ }
59
+ return projectRoot;
91
60
  }
92
61
 
93
- /**
94
- * Result of loading config - includes everything needed by callers.
95
- */
96
62
  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
- };
63
+ config: BosConfig;
64
+ runtime: RuntimeConfig;
65
+ source: {
66
+ path: string;
67
+ extended?: string[];
68
+ remote?: boolean;
69
+ };
112
70
  }
113
71
 
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;
123
- }
124
-
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
72
  export async function loadConfig(options?: {
134
- cwd?: string;
135
- path?: string;
136
- force?: boolean;
137
- env?: "development" | "production";
73
+ cwd?: string;
74
+ path?: string;
75
+ env?: "development" | "production";
138
76
  }): 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
- }
77
+ const configPath = options?.path ?? findConfigPath(options?.cwd);
78
+ if (!configPath) {
79
+ projectRoot = options?.cwd ?? process.cwd();
80
+ return null;
81
+ }
82
+
83
+ const baseDir = dirname(configPath);
84
+
85
+ try {
86
+ const extendedChain: string[] = [];
87
+ const parsed = await resolveConfigWithExtends(configPath, baseDir, new Set(), extendedChain);
88
+ const config = BosConfigSchema.parse(parsed);
89
+
90
+ cachedConfig = config;
91
+ projectRoot = baseDir;
92
+
93
+ const pluginRuntime = await resolveRuntimePlugins(
94
+ config.plugins ?? {},
95
+ baseDir,
96
+ options?.env ?? "development",
97
+ );
98
+ const runtime = buildRuntimeConfig(config, baseDir, options?.env ?? "development", {
99
+ plugins: pluginRuntime,
100
+ });
101
+
102
+ return {
103
+ config,
104
+ runtime,
105
+ source: {
106
+ path: configPath,
107
+ extended: extendedChain.length > 0 ? extendedChain : undefined,
108
+ remote: extendedChain.some((entry) => entry.startsWith("bos://")),
109
+ },
110
+ };
111
+ } catch (error) {
112
+ throw new Error(`Failed to load config from ${configPath}: ${error}`);
113
+ }
239
114
  }
240
115
 
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 };
116
+ export async function loadBosConfig(options?: {
117
+ cwd?: string;
118
+ path?: string;
119
+ env?: "development" | "production";
120
+ }): Promise<RuntimeConfig> {
121
+ const result = await loadConfig(options);
122
+ if (!result) {
123
+ throw new Error("No bos.config.json found");
124
+ }
125
+
126
+ return result.runtime;
294
127
  }
295
128
 
296
- // ============================================================================
297
- // INTERNAL HELPERS
298
- // ============================================================================
129
+ export async function buildRuntimePluginsForConfig(
130
+ config: BosConfig,
131
+ baseDir: string,
132
+ env: "development" | "production",
133
+ ): Promise<Record<string, RuntimePluginConfig> | undefined> {
134
+ const plugins = await resolveRuntimePlugins(config.plugins ?? {}, baseDir, env);
135
+ return Object.keys(plugins).length > 0 ? plugins : undefined;
136
+ }
299
137
 
300
- /**
301
- * Parse port from URL string.
302
- */
303
- export function parsePort(url: string): number {
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
- }
138
+ function buildRuntimeConfig(
139
+ config: BosConfig,
140
+ baseDir: string,
141
+ env: "development" | "production",
142
+ options?: { plugins?: Record<string, RuntimePluginConfig> },
143
+ ): RuntimeConfig {
144
+ const uiConfig = config.app.ui;
145
+ const apiConfig = config.app.api;
146
+ const uiRuntime =
147
+ env === "development"
148
+ ? resolveRuntimeTarget(uiConfig.development, baseDir)
149
+ : resolveRuntimeTarget(uiConfig.production, baseDir, "remote");
150
+ const apiRuntime =
151
+ env === "development"
152
+ ? resolveRuntimeTarget(apiConfig.development, baseDir)
153
+ : resolveRuntimeTarget(apiConfig.production, baseDir, "remote");
154
+
155
+ return {
156
+ env,
157
+ account: config.account,
158
+ domain: config.domain,
159
+ networkId: getNetworkIdForAccount(config.account),
160
+ repository: config.repository,
161
+ hostUrl:
162
+ env === "development"
163
+ ? resolveDevelopmentHostUrl(config.app.host.development)
164
+ : config.app.host.production,
165
+ shared: config.shared,
166
+ ui: {
167
+ name: uiConfig.name,
168
+ url: uiRuntime.url,
169
+ entry: uiRuntime.url ? `${uiRuntime.url}/mf-manifest.json` : "/mf-manifest.json",
170
+ localPath: uiRuntime.localPath,
171
+ port: uiRuntime.port,
172
+ ssrUrl: uiConfig.ssr,
173
+ ssrIntegrity: env === "production" ? uiConfig.ssrIntegrity : undefined,
174
+ integrity: env === "production" ? uiConfig.productionIntegrity : undefined,
175
+ source: uiRuntime.source,
176
+ },
177
+ api: {
178
+ name: apiConfig.name,
179
+ url: apiRuntime.url,
180
+ entry: apiRuntime.url ? `${apiRuntime.url}/mf-manifest.json` : "/mf-manifest.json",
181
+ localPath: apiRuntime.localPath,
182
+ port: apiRuntime.port,
183
+ source: apiRuntime.source,
184
+ proxy: apiConfig.proxy,
185
+ variables: apiConfig.variables,
186
+ secrets: apiConfig.secrets,
187
+ integrity: env === "production" ? apiConfig.productionIntegrity : undefined,
188
+ },
189
+ plugins:
190
+ options?.plugins && Object.keys(options.plugins).length > 0 ? options.plugins : undefined,
191
+ };
314
192
  }
315
193
 
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);
194
+ async function loadConfigFile(configPath: string, baseDir: string): Promise<BosConfigInput> {
195
+ if (configPath.startsWith("bos://")) {
196
+ return fetchBosConfigFromFastKv<BosConfigInput>(configPath);
197
+ }
198
+
199
+ const resolvedPath = isAbsolute(configPath) ? configPath : resolve(baseDir, configPath);
200
+ return JSON.parse(readFileSync(resolvedPath, "utf-8")) as BosConfigInput;
323
201
  }
324
202
 
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`;
203
+ async function resolveConfigWithExtends(
204
+ configPath: string,
205
+ baseDir: string,
206
+ visited: Set<string>,
207
+ chain: string[],
208
+ ): Promise<BosConfigInput> {
209
+ if (visited.has(configPath)) {
210
+ throw new Error(`Circular extends detected: ${[...visited, configPath].join(" -> ")}`);
211
+ }
212
+
213
+ const config = await loadConfigFile(configPath, baseDir);
214
+ if (configPath.startsWith("bos://")) {
215
+ chain.push(configPath);
216
+ }
217
+
218
+ if (!config.extends) {
219
+ return config;
220
+ }
221
+
222
+ const nextVisited = new Set(visited);
223
+ nextVisited.add(configPath);
224
+ const parentPath = config.extends;
225
+ const parentBaseDir = parentPath.startsWith("bos://")
226
+ ? baseDir
227
+ : isAbsolute(parentPath)
228
+ ? dirname(parentPath)
229
+ : baseDir;
230
+ const parent = await resolveConfigWithExtends(parentPath, parentBaseDir, nextVisited, chain);
231
+
232
+ return mergeConfigs(parent, config);
338
233
  }
339
234
 
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
- }
235
+ function mergeConfigs(parent: BosConfigInput, child: BosConfigInput): BosConfigInput {
236
+ const result = mergeValues(parent, child) as BosConfigInput;
237
+ if (child.plugins !== undefined) {
238
+ result.plugins = child.plugins;
239
+ }
240
+ return result;
387
241
  }
388
242
 
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;
243
+ async function resolveRuntimePlugins(
244
+ plugins: Record<string, BosConfigInput>,
245
+ baseDir: string,
246
+ env: "development" | "production",
247
+ prefix: string[] = [],
248
+ ): Promise<Record<string, RuntimePluginConfig>> {
249
+ const out: Record<string, RuntimePluginConfig> = {};
250
+
251
+ for (const [pluginId, pluginInput] of Object.entries(plugins)) {
252
+ const runtimeKey = [...prefix, pluginId].join("/");
253
+ const { config: resolvedConfig, baseDir: pluginBaseDir } = await resolveBosConfigInput(
254
+ pluginInput,
255
+ baseDir,
256
+ new Set(),
257
+ [],
258
+ );
259
+
260
+ const pluginRuntime = buildRuntimePluginConfig(
261
+ runtimeKey,
262
+ resolvedConfig,
263
+ pluginBaseDir,
264
+ env,
265
+ pluginInput,
266
+ );
267
+ if (
268
+ pluginRuntime.source === "remote" &&
269
+ pluginRuntime.url &&
270
+ !pluginRuntime.localPath &&
271
+ typeof resolvedConfig.app?.api?.name !== "string"
272
+ ) {
273
+ pluginRuntime.name = await resolveRemotePluginRuntimeName(
274
+ pluginRuntime.url,
275
+ pluginRuntime.name,
276
+ );
277
+ }
278
+
279
+ const productionIntegrity = pluginInput.productionIntegrity;
280
+ if (env === "production" && productionIntegrity) {
281
+ pluginRuntime.integrity = productionIntegrity;
282
+ }
283
+
284
+ out[runtimeKey] = pluginRuntime;
285
+
286
+ if (resolvedConfig.plugins && Object.keys(resolvedConfig.plugins).length > 0) {
287
+ const nested = await resolveRuntimePlugins(resolvedConfig.plugins, pluginBaseDir, env, [
288
+ ...prefix,
289
+ pluginId,
290
+ ]);
291
+ Object.assign(out, nested);
292
+ }
293
+ }
294
+
295
+ return out;
410
296
  }
411
297
 
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;
298
+ async function resolveRemotePluginRuntimeName(baseUrl: string, fallback: string): Promise<string> {
299
+ try {
300
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/plugin.manifest.json`);
301
+ if (!response.ok) {
302
+ return fallback;
303
+ }
304
+
305
+ const manifest = (await response.json()) as {
306
+ plugin?: { name?: unknown };
307
+ };
308
+
309
+ return typeof manifest.plugin?.name === "string" && manifest.plugin.name.length > 0
310
+ ? manifest.plugin.name
311
+ : fallback;
312
+ } catch {
313
+ return fallback;
314
+ }
479
315
  }
480
316
 
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);
317
+ function buildRuntimePluginConfig(
318
+ pluginId: string,
319
+ config: BosConfigInput,
320
+ baseDir: string,
321
+ env: "development" | "production",
322
+ source: BosConfigInput,
323
+ ): RuntimePluginConfig {
324
+ const apiConfig = config.app?.api ?? {};
325
+ const apiDevelopment =
326
+ typeof apiConfig.development === "string" ? apiConfig.development : undefined;
327
+ const apiProduction = typeof apiConfig.production === "string" ? apiConfig.production : undefined;
328
+ const sourceDevelopment = typeof source.development === "string" ? source.development : undefined;
329
+ const sourceProduction = typeof source.production === "string" ? source.production : undefined;
330
+ const proxy = typeof apiConfig.proxy === "string" ? apiConfig.proxy : undefined;
331
+ const runtimeTarget =
332
+ env === "development"
333
+ ? resolveRuntimeTarget(apiDevelopment ?? sourceDevelopment, baseDir)
334
+ : resolveRuntimeTarget(apiProduction ?? sourceProduction, baseDir, "remote");
335
+ const apiName = resolvePluginRuntimeName(
336
+ typeof apiConfig.name === "string" ? apiConfig.name : undefined,
337
+ runtimeTarget.localPath,
338
+ pluginId,
339
+ );
340
+
341
+ return {
342
+ name: apiName,
343
+ url: runtimeTarget.url,
344
+ entry: runtimeTarget.url
345
+ ? `${runtimeTarget.url.replace(/\/$/, "")}/mf-manifest.json`
346
+ : "/mf-manifest.json",
347
+ source: runtimeTarget.source,
348
+ localPath: runtimeTarget.localPath,
349
+ port: runtimeTarget.port,
350
+ proxy: proxy ?? (typeof source.proxy === "string" ? source.proxy : undefined),
351
+ variables: normalizeStringRecord(apiConfig.variables ?? source.variables),
352
+ secrets: normalizeStringArray(apiConfig.secrets ?? source.secrets),
353
+ };
529
354
  }
530
355
 
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
- };
356
+ function resolvePluginRuntimeName(
357
+ explicitName: string | undefined,
358
+ localPath: string | undefined,
359
+ fallback: string,
360
+ ): string {
361
+ if (explicitName) {
362
+ return explicitName;
363
+ }
364
+
365
+ if (!localPath) {
366
+ return fallback;
367
+ }
368
+
369
+ try {
370
+ const packageJsonPath = join(localPath, "package.json");
371
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
372
+ if (typeof packageJson.name === "string" && packageJson.name.length > 0) {
373
+ return packageJson.name;
374
+ }
375
+ } catch {}
376
+
377
+ return fallback;
378
+ }
379
+
380
+ async function resolveBosConfigInput(
381
+ input: BosConfigInput,
382
+ baseDir: string,
383
+ visited: Set<string>,
384
+ chain: string[],
385
+ ): Promise<{ config: BosConfigInput; baseDir: string }> {
386
+ if (input.extends) {
387
+ const parentBaseDir = input.extends.startsWith("bos://")
388
+ ? baseDir
389
+ : isAbsolute(input.extends)
390
+ ? dirname(input.extends)
391
+ : baseDir;
392
+ const config = await resolveConfigWithExtends(input.extends, parentBaseDir, visited, chain);
393
+ return { config: mergeConfigs(config, input), baseDir: parentBaseDir };
394
+ }
395
+
396
+ return { config: input, baseDir };
397
+ }
398
+
399
+ function mergeValues(parent: unknown, child: unknown): unknown {
400
+ if (Array.isArray(parent) && Array.isArray(child)) {
401
+ return child;
402
+ }
403
+
404
+ if (isPlainObject(parent) && isPlainObject(child)) {
405
+ const merged: Record<string, unknown> = { ...parent };
406
+ for (const [key, value] of Object.entries(child)) {
407
+ merged[key] = key in merged ? mergeValues(merged[key], value) : value;
408
+ }
409
+ return merged;
410
+ }
411
+
412
+ return child ?? parent;
413
+ }
414
+
415
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
416
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
417
+ }
418
+
419
+ function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
420
+ if (!isPlainObject(value)) return undefined;
421
+ const out: Record<string, string> = {};
422
+ for (const [key, raw] of Object.entries(value)) {
423
+ if (typeof raw === "string") {
424
+ out[key] = raw;
425
+ }
426
+ }
427
+ return Object.keys(out).length > 0 ? out : undefined;
428
+ }
429
+
430
+ function normalizeStringArray(value: unknown): string[] | undefined {
431
+ if (!Array.isArray(value)) return undefined;
432
+ const out = value.filter((item): item is string => typeof item === "string" && item.length > 0);
433
+ return out.length > 0 ? out : undefined;
434
+ }
435
+
436
+ function resolveRuntimeTarget(
437
+ value: string | undefined,
438
+ baseDir: string,
439
+ defaultSource: "local" | "remote" = "remote",
440
+ ): RuntimeTarget {
441
+ if (!value) {
442
+ return { source: defaultSource, url: "" };
443
+ }
444
+
445
+ if (value.startsWith(LOCAL_PREFIX)) {
446
+ const localTarget = value.slice(LOCAL_PREFIX.length).trim();
447
+ if (!localTarget) {
448
+ throw new Error(`Invalid local development target: ${value}`);
449
+ }
450
+
451
+ return {
452
+ source: "local",
453
+ url: "",
454
+ localPath: resolve(baseDir, localTarget),
455
+ };
456
+ }
457
+
458
+ return {
459
+ source: defaultSource,
460
+ url: value.replace(/\/$/, ""),
461
+ port: parsePort(value),
462
+ };
463
+ }
464
+
465
+ export function isLocalDevelopmentTarget(value: string | undefined): boolean {
466
+ return typeof value === "string" && value.startsWith(LOCAL_PREFIX);
573
467
  }
468
+
469
+ export function resolveLocalDevelopmentPath(
470
+ value: string | undefined,
471
+ baseDir: string,
472
+ ): string | null {
473
+ if (!isLocalDevelopmentTarget(value)) {
474
+ return null;
475
+ }
476
+
477
+ const localTarget = value!.slice(LOCAL_PREFIX.length).trim();
478
+ return localTarget ? resolve(baseDir, localTarget) : null;
479
+ }
480
+
481
+ export function resolveDevelopmentHostUrl(value: string | undefined): string {
482
+ if (!value || isLocalDevelopmentTarget(value)) {
483
+ return `http://localhost:${DEFAULT_HOST_PORT}`;
484
+ }
485
+
486
+ return value.replace(/\/$/, "");
487
+ }
488
+
489
+ export function getHostDevelopmentPort(value: string | undefined): number {
490
+ return parsePort(resolveDevelopmentHostUrl(value));
491
+ }
492
+
493
+ export function parsePort(url: string): number {
494
+ try {
495
+ const parsed = new URL(url);
496
+ return parsed.port ? parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : 80;
497
+ } catch {
498
+ return 3000;
499
+ }
500
+ }
501
+
502
+ export type { BosConfig, RuntimeConfig } from "./types";
503
+ export { BosConfigSchema } from "./types";