everything-dev 0.1.4 → 0.2.0

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.
@@ -1,8 +1,8 @@
1
1
  import { Command } from "@effect/platform";
2
2
  import { Deferred, Effect, Fiber, Ref, Stream } from "every-plugin/effect";
3
- import { type BosConfig, getConfigDir, getPortsFromConfig, type SourceMode } from "../config";
4
3
  import type { ProcessStatus } from "../components/dev-view";
5
- import { loadSecretsFor } from "./secrets";
4
+ import { type BosConfig, getConfigDir, getPortsFromConfig, type RemoteConfig, type SourceMode } from "../config";
5
+ import type { RuntimeConfig } from "../types";
6
6
 
7
7
  export interface DevProcess {
8
8
  name: string;
@@ -68,7 +68,7 @@ export const getProcessConfig = (
68
68
  if (!base) return null;
69
69
 
70
70
  const ports = getPortsFromConfig();
71
-
71
+
72
72
  let port: number;
73
73
  if (pkg === "host") {
74
74
  port = portOverride ?? ports.host;
@@ -157,9 +157,45 @@ const killProcessTree = (pid: number) =>
157
157
  }
158
158
  });
159
159
 
160
+ export function buildRuntimeConfig(
161
+ bosConfig: BosConfig,
162
+ options: {
163
+ uiSource: SourceMode;
164
+ apiSource: SourceMode;
165
+ hostUrl: string;
166
+ proxy?: string;
167
+ env?: "development" | "production";
168
+ }
169
+ ): RuntimeConfig {
170
+ const uiConfig = bosConfig.app.ui as RemoteConfig;
171
+ const apiConfig = bosConfig.app.api as RemoteConfig;
172
+
173
+ return {
174
+ env: options.env ?? "development",
175
+ account: bosConfig.account,
176
+ hostUrl: options.hostUrl,
177
+ shared: (bosConfig as { shared?: { ui?: Record<string, unknown> } }).shared as RuntimeConfig["shared"],
178
+ ui: {
179
+ name: uiConfig.name,
180
+ url: options.uiSource === "remote" ? uiConfig.production : uiConfig.development,
181
+ ssrUrl: options.uiSource === "remote" ? uiConfig.ssr : undefined,
182
+ source: options.uiSource,
183
+ },
184
+ api: {
185
+ name: apiConfig.name,
186
+ url: options.apiSource === "remote" ? apiConfig.production : apiConfig.development,
187
+ source: options.apiSource,
188
+ proxy: options.proxy,
189
+ variables: apiConfig.variables,
190
+ secrets: apiConfig.secrets,
191
+ },
192
+ };
193
+ }
194
+
160
195
  export const spawnDevProcess = (
161
196
  config: DevProcess,
162
- callbacks: ProcessCallbacks
197
+ callbacks: ProcessCallbacks,
198
+ runtimeConfig?: RuntimeConfig
163
199
  ) =>
164
200
  Effect.gen(function* () {
165
201
  const configDir = getConfigDir();
@@ -169,15 +205,20 @@ export const spawnDevProcess = (
169
205
 
170
206
  callbacks.onStatus(config.name, "starting");
171
207
 
208
+ const envVars: Record<string, string> = {
209
+ ...process.env as Record<string, string>,
210
+ ...config.env,
211
+ FORCE_COLOR: "1",
212
+ ...(config.port > 0 ? { PORT: String(config.port) } : {}),
213
+ };
214
+
215
+ if (runtimeConfig && config.name === "host") {
216
+ envVars.BOS_RUNTIME_CONFIG = JSON.stringify(runtimeConfig);
217
+ }
218
+
172
219
  const cmd = Command.make(config.command, ...config.args).pipe(
173
220
  Command.workingDirectory(fullCwd),
174
- Command.env({
175
- ...process.env,
176
- ...config.env,
177
- BOS_CONFIG_PATH: "../bos.config.json",
178
- FORCE_COLOR: "1",
179
- ...(config.port > 0 ? { PORT: String(config.port) } : {}),
180
- })
221
+ Command.env(envVars)
181
222
  );
182
223
 
183
224
  const proc = yield* Command.start(cmd);
@@ -247,13 +288,8 @@ interface ServerHandle {
247
288
  shutdown: () => Promise<void>;
248
289
  }
249
290
 
250
- interface BootstrapConfig {
251
- config?: BosConfig;
252
- secrets?: Record<string, string>;
253
- host?: { url?: string };
254
- ui?: { source?: SourceMode };
255
- api?: { source?: SourceMode; proxy?: string };
256
- database?: { url?: string };
291
+ interface ServerInput {
292
+ config: RuntimeConfig;
257
293
  }
258
294
 
259
295
  const patchConsole = (
@@ -300,7 +336,7 @@ const patchConsole = (
300
336
  export const spawnRemoteHost = (
301
337
  config: DevProcess,
302
338
  callbacks: ProcessCallbacks,
303
- bosConfig?: BosConfig
339
+ runtimeConfig: RuntimeConfig
304
340
  ) =>
305
341
  Effect.gen(function* () {
306
342
  const remoteUrl = config.env?.HOST_REMOTE_URL;
@@ -317,27 +353,6 @@ export const spawnRemoteHost = (
317
353
 
318
354
  callbacks.onStatus(config.name, "starting");
319
355
 
320
- let hostUrl = `http://localhost:${config.port}`;
321
- if (process.env.HOST_URL) {
322
- hostUrl = process.env.HOST_URL;
323
- }
324
-
325
- const hostSecrets = loadSecretsFor("host");
326
- const apiSecrets = loadSecretsFor("api");
327
- const allSecrets = { ...hostSecrets, ...apiSecrets };
328
-
329
- const uiSource = (config.env?.UI_SOURCE as SourceMode) ?? "local";
330
- const apiSource = (config.env?.API_SOURCE as SourceMode) ?? "local";
331
- const apiProxy = config.env?.API_PROXY;
332
-
333
- const bootstrap: BootstrapConfig = {
334
- config: bosConfig,
335
- secrets: allSecrets,
336
- host: { url: hostUrl },
337
- ui: { source: uiSource },
338
- api: { source: apiSource, proxy: apiProxy },
339
- };
340
-
341
356
  callbacks.onLog(config.name, `Remote: ${remoteUrl}`);
342
357
 
343
358
  const restoreConsole = patchConsole(config.name, callbacks);
@@ -369,7 +384,7 @@ export const spawnRemoteHost = (
369
384
  callbacks.onLog(config.name, `Loading host from ${remoteEntryUrl}...`);
370
385
 
371
386
  const hostModule = yield* Effect.tryPromise({
372
- try: () => mf.loadRemote<{ runServer: (bootstrap?: BootstrapConfig) => ServerHandle }>("host/Server"),
387
+ try: () => mf.loadRemote<{ runServer: (input: ServerInput) => ServerHandle }>("host/Server"),
373
388
  catch: (e) => new Error(`Failed to load host module: ${e}`),
374
389
  });
375
390
 
@@ -378,7 +393,7 @@ export const spawnRemoteHost = (
378
393
  }
379
394
 
380
395
  callbacks.onLog(config.name, "Starting server...");
381
- const serverHandle = hostModule.runServer(bootstrap);
396
+ const serverHandle = hostModule.runServer({ config: runtimeConfig });
382
397
 
383
398
  yield* Effect.tryPromise({
384
399
  try: () => serverHandle.ready,
@@ -415,8 +430,29 @@ export const makeDevProcess = (
415
430
  return yield* Effect.fail(new Error(`Unknown package: ${pkg}`));
416
431
  }
417
432
 
418
- if (pkg === "host" && env?.HOST_SOURCE === "remote") {
419
- return yield* spawnRemoteHost(config, callbacks, bosConfig);
433
+ if (pkg === "host" && bosConfig) {
434
+ const uiSource = (env?.UI_SOURCE as SourceMode) ?? "local";
435
+ const apiSource = (env?.API_SOURCE as SourceMode) ?? "local";
436
+ const apiProxy = env?.API_PROXY;
437
+
438
+ let hostUrl = `http://localhost:${config.port}`;
439
+ if (process.env.HOST_URL) {
440
+ hostUrl = process.env.HOST_URL;
441
+ }
442
+
443
+ const runtimeConfig = buildRuntimeConfig(bosConfig, {
444
+ uiSource,
445
+ apiSource,
446
+ hostUrl,
447
+ proxy: apiProxy,
448
+ env: "development",
449
+ });
450
+
451
+ if (env?.HOST_SOURCE === "remote") {
452
+ return yield* spawnRemoteHost(config, callbacks, runtimeConfig);
453
+ }
454
+
455
+ return yield* spawnDevProcess(config, callbacks, runtimeConfig);
420
456
  }
421
457
 
422
458
  return yield* spawnDevProcess(config, callbacks);
@@ -21,17 +21,13 @@ export const diffSnapshots = (from: Snapshot, to: Snapshot): SnapshotDiff => {
21
21
  }
22
22
  }
23
23
 
24
- const stillBoundPids = new Set(
25
- stillBoundPorts.map((p) => p.pid).filter((pid): pid is number => pid !== null)
26
- );
27
-
28
- const orphanedProcesses = from.processes.filter(
29
- (p) =>
30
- fromPids.has(p.pid) &&
31
- !toPids.has(p.pid) &&
32
- isProcessAliveSync(p.pid) &&
33
- stillBoundPids.has(p.pid)
34
- );
24
+ const orphanedProcesses = from.processes.filter((p) => {
25
+ const wasInBaseline = fromPids.has(p.pid);
26
+ const notInAfter = !toPids.has(p.pid);
27
+ const stillAlive = isProcessAliveSync(p.pid);
28
+
29
+ return wasInBaseline && notInAfter && stillAlive;
30
+ });
35
31
 
36
32
  const newProcesses = to.processes.filter((p) => !fromPids.has(p.pid));
37
33
  const killedProcesses = from.processes.filter((p) => !toPids.has(p.pid));
@@ -211,7 +211,7 @@ const getMemoryInfo = (): Effect.Effect<MemoryInfo, never> =>
211
211
  return {
212
212
  total,
213
213
  used,
214
- free: total - used,
214
+ free: Math.max(0, total - used),
215
215
  processRss: 0,
216
216
  };
217
217
  });
package/src/lib/sync.ts CHANGED
@@ -1,133 +1 @@
1
- import { execa } from "execa";
2
- import { cp, mkdir, mkdtemp, rm } from "fs/promises";
3
- import { tmpdir } from "os";
4
- import { dirname, join } from "path";
5
- import type { BosConfig } from "../config";
6
-
7
- export interface FileSyncResult {
8
- package: string;
9
- files: string[];
10
- depsAdded?: string[];
11
- depsUpdated?: string[];
12
- }
13
-
14
- export interface FileSyncOptions {
15
- configDir: string;
16
- packages: string[];
17
- bosConfig: BosConfig;
18
- catalog?: Record<string, string>;
19
- force?: boolean;
20
- }
21
-
22
- export async function syncFiles(options: FileSyncOptions): Promise<FileSyncResult[]> {
23
- const { configDir, packages, bosConfig, catalog = {}, force } = options;
24
- const results: FileSyncResult[] = [];
25
-
26
- for (const pkg of packages) {
27
- const pkgDir = `${configDir}/${pkg}`;
28
- const pkgDirExists = await Bun.file(`${pkgDir}/package.json`).exists();
29
- if (!pkgDirExists) continue;
30
-
31
- const appConfig = bosConfig.app[pkg] as {
32
- template?: string;
33
- files?: string[];
34
- sync?: { dependencies?: boolean; devDependencies?: boolean };
35
- };
36
-
37
- if (!appConfig?.template || !appConfig?.files) {
38
- continue;
39
- }
40
-
41
- const tempDir = await mkdtemp(join(tmpdir(), `bos-files-${pkg}-`));
42
-
43
- try {
44
- await execa("npx", ["degit", appConfig.template, tempDir, "--force"], {
45
- stdio: "pipe",
46
- });
47
-
48
- const filesSynced: string[] = [];
49
- const depsAdded: string[] = [];
50
- const depsUpdated: string[] = [];
51
-
52
- for (const file of appConfig.files) {
53
- const srcPath = join(tempDir, file);
54
- const destPath = join(pkgDir, file);
55
-
56
- try {
57
- const destDir = dirname(destPath);
58
- await mkdir(destDir, { recursive: true });
59
- await cp(srcPath, destPath, { force: true, recursive: true });
60
- filesSynced.push(file);
61
- } catch {
62
- }
63
- }
64
-
65
- const syncConfig = appConfig.sync ?? { dependencies: true, devDependencies: true };
66
-
67
- if (syncConfig.dependencies !== false || syncConfig.devDependencies !== false) {
68
- const templatePkgPath = join(tempDir, "package.json");
69
- const localPkgPath = join(pkgDir, "package.json");
70
-
71
- try {
72
- const templatePkg = await Bun.file(templatePkgPath).json() as {
73
- dependencies?: Record<string, string>;
74
- devDependencies?: Record<string, string>;
75
- scripts?: Record<string, string>;
76
- };
77
- const localPkg = await Bun.file(localPkgPath).json() as {
78
- dependencies?: Record<string, string>;
79
- devDependencies?: Record<string, string>;
80
- scripts?: Record<string, string>;
81
- };
82
-
83
- if (syncConfig.dependencies !== false && templatePkg.dependencies) {
84
- if (!localPkg.dependencies) localPkg.dependencies = {};
85
- for (const [name, version] of Object.entries(templatePkg.dependencies)) {
86
- if (!(name in localPkg.dependencies)) {
87
- localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
88
- depsAdded.push(name);
89
- } else if (localPkg.dependencies[name] !== "catalog:" && version !== localPkg.dependencies[name]) {
90
- localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
91
- depsUpdated.push(name);
92
- }
93
- }
94
- }
95
-
96
- if (syncConfig.devDependencies !== false && templatePkg.devDependencies) {
97
- if (!localPkg.devDependencies) localPkg.devDependencies = {};
98
- for (const [name, version] of Object.entries(templatePkg.devDependencies)) {
99
- if (!(name in localPkg.devDependencies)) {
100
- localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
101
- depsAdded.push(name);
102
- } else if (localPkg.devDependencies[name] !== "catalog:" && version !== localPkg.devDependencies[name]) {
103
- localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
104
- depsUpdated.push(name);
105
- }
106
- }
107
- }
108
-
109
- if (templatePkg.scripts) {
110
- if (!localPkg.scripts) localPkg.scripts = {};
111
- for (const [name, script] of Object.entries(templatePkg.scripts)) {
112
- localPkg.scripts[name] = script;
113
- }
114
- }
115
-
116
- await Bun.write(localPkgPath, JSON.stringify(localPkg, null, 2));
117
- } catch {
118
- }
119
- }
120
-
121
- results.push({
122
- package: pkg,
123
- files: filesSynced,
124
- depsAdded: depsAdded.length > 0 ? depsAdded : undefined,
125
- depsUpdated: depsUpdated.length > 0 ? depsUpdated : undefined,
126
- });
127
- } finally {
128
- await rm(tempDir, { recursive: true, force: true });
129
- }
130
- }
131
-
132
- return results;
133
- }
1
+ export { syncFiles, type FileSyncOptions, type FileSyncResult } from "../ui/files";
package/src/plugin.ts CHANGED
@@ -159,7 +159,33 @@ function determineProcesses(config: AppConfig): string[] {
159
159
  return processes;
160
160
  }
161
161
 
162
- function buildEnvVars(config: AppConfig): Record<string, string> {
162
+ function isValidProxyUrl(url: string): boolean {
163
+ try {
164
+ const parsed = new URL(url);
165
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ function resolveProxyUrl(bosConfig: BosConfigType | null): string | null {
172
+ if (!bosConfig) return null;
173
+
174
+ const apiConfig = bosConfig.app.api as RemoteConfig | undefined;
175
+ if (!apiConfig) return null;
176
+
177
+ if (apiConfig.proxy && isValidProxyUrl(apiConfig.proxy)) {
178
+ return apiConfig.proxy;
179
+ }
180
+
181
+ if (apiConfig.production && isValidProxyUrl(apiConfig.production)) {
182
+ return apiConfig.production;
183
+ }
184
+
185
+ return null;
186
+ }
187
+
188
+ function buildEnvVars(config: AppConfig, bosConfig?: BosConfigType | null): Record<string, string> {
163
189
  const env: Record<string, string> = {};
164
190
 
165
191
  env.HOST_SOURCE = config.host;
@@ -174,9 +200,11 @@ function buildEnvVars(config: AppConfig): Record<string, string> {
174
200
  }
175
201
 
176
202
  if (config.proxy) {
177
- const bosConfig = loadConfig();
178
- const apiConfig = bosConfig?.app.api as RemoteConfig | undefined;
179
- env.API_PROXY = apiConfig?.proxy || apiConfig?.production || "true";
203
+ const resolvedBosConfig = bosConfig ?? loadConfig();
204
+ const proxyUrl = resolveProxyUrl(resolvedBosConfig);
205
+ if (proxyUrl) {
206
+ env.API_PROXY = proxyUrl;
207
+ }
180
208
  }
181
209
 
182
210
  return env;
@@ -248,8 +276,28 @@ export default createPlugin({
248
276
  }
249
277
  }
250
278
 
279
+ let proxyUrl: string | undefined;
280
+ if (appConfig.proxy) {
281
+ proxyUrl = resolveProxyUrl(deps.bosConfig) ?? undefined;
282
+ if (!proxyUrl) {
283
+ console.log();
284
+ console.log(colors.red(` ${icons.err} Proxy mode requested but no valid proxy URL found`));
285
+ console.log(colors.dim(` Configure 'api.proxy' or 'api.production' in bos.config.json`));
286
+ console.log();
287
+ return {
288
+ status: "error" as const,
289
+ description: "No valid proxy URL configured in bos.config.json",
290
+ processes: [],
291
+ autoRemote,
292
+ };
293
+ }
294
+ console.log();
295
+ console.log(colors.cyan(` ${icons.arrow} API Proxy: ${colors.bold(proxyUrl)}`));
296
+ console.log();
297
+ }
298
+
251
299
  const processes = determineProcesses(appConfig);
252
- const env = buildEnvVars(appConfig);
300
+ const env = buildEnvVars(appConfig, deps.bosConfig);
253
301
  const description = buildDescription(appConfig);
254
302
 
255
303
  const orchestrator: AppOrchestrator = {
@@ -1302,7 +1350,7 @@ export default createPlugin({
1302
1350
  };
1303
1351
  }),
1304
1352
 
1305
- sync: builder.sync.handler(async ({ input }) => {
1353
+ update: builder.update.handler(async ({ input }) => {
1306
1354
  const { configDir, bosConfig } = deps;
1307
1355
 
1308
1356
  const DEFAULT_ACCOUNT = "every.near";
@@ -1476,24 +1524,20 @@ export default createPlugin({
1476
1524
  }
1477
1525
  }
1478
1526
 
1479
- let filesSynced: Array<{ package: string; files: string[] }> | undefined;
1480
-
1481
- if (input.files) {
1482
- const results = await syncFiles({
1483
- configDir,
1484
- packages: Object.keys(updatedBosConfig.app),
1485
- bosConfig: updatedBosConfig,
1486
- catalog: rootPkg.workspaces?.catalog ?? {},
1487
- force: input.force,
1488
- });
1527
+ const results = await syncFiles({
1528
+ configDir,
1529
+ packages: Object.keys(updatedBosConfig.app),
1530
+ bosConfig: updatedBosConfig,
1531
+ catalog: rootPkg.workspaces?.catalog ?? {},
1532
+ force: input.force,
1533
+ });
1489
1534
 
1490
- if (results.length > 0) {
1491
- filesSynced = results.map(r => ({ package: r.package, files: r.files }));
1492
- }
1493
- }
1535
+ const filesSynced = results.length > 0
1536
+ ? results.map(r => ({ package: r.package, files: r.files }))
1537
+ : undefined;
1494
1538
 
1495
1539
  return {
1496
- status: "synced" as const,
1540
+ status: "updated" as const,
1497
1541
  account,
1498
1542
  gateway,
1499
1543
  socialUrl,
package/src/types.ts CHANGED
@@ -4,8 +4,6 @@ export const SourceModeSchema = z.enum(["local", "remote"]);
4
4
  export type SourceMode = z.infer<typeof SourceModeSchema>;
5
5
 
6
6
  export const HostConfigSchema = z.object({
7
- title: z.string(),
8
- description: z.string().optional(),
9
7
  development: z.string(),
10
8
  production: z.string(),
11
9
  secrets: z.array(z.string()).optional(),
@@ -21,7 +19,6 @@ export const RemoteConfigSchema = z.object({
21
19
  production: z.string(),
22
20
  ssr: z.string().optional(),
23
21
  proxy: z.string().optional(),
24
- exposes: z.record(z.string(), z.string()).optional(),
25
22
  variables: z.record(z.string(), z.string()).optional(),
26
23
  secrets: z.array(z.string()).optional(),
27
24
  template: z.string().optional(),
@@ -88,3 +85,51 @@ export const PortConfigSchema = z.object({
88
85
  api: z.number(),
89
86
  });
90
87
  export type PortConfig = z.infer<typeof PortConfigSchema>;
88
+
89
+ export const SharedConfigSchema = z.object({
90
+ requiredVersion: z.string().optional(),
91
+ singleton: z.boolean().optional(),
92
+ eager: z.boolean().optional(),
93
+ strictVersion: z.boolean().optional(),
94
+ shareScope: z.string().optional(),
95
+ });
96
+ export type SharedConfig = z.infer<typeof SharedConfigSchema>;
97
+
98
+ export const RuntimeConfigSchema = z.object({
99
+ env: z.enum(["development", "production"]),
100
+ account: z.string(),
101
+ title: z.string().optional(),
102
+ hostUrl: z.string(),
103
+ shared: z.object({
104
+ ui: z.record(z.string(), SharedConfigSchema).optional(),
105
+ }).optional(),
106
+ ui: z.object({
107
+ name: z.string(),
108
+ url: z.string(),
109
+ ssrUrl: z.string().optional(),
110
+ source: SourceModeSchema,
111
+ }),
112
+ api: z.object({
113
+ name: z.string(),
114
+ url: z.string(),
115
+ source: SourceModeSchema,
116
+ proxy: z.string().optional(),
117
+ variables: z.record(z.string(), z.string()).optional(),
118
+ secrets: z.array(z.string()).optional(),
119
+ }),
120
+ });
121
+ export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
122
+
123
+ export const ClientRuntimeConfigSchema = z.object({
124
+ env: z.enum(["development", "production"]),
125
+ account: z.string(),
126
+ hostUrl: z.string().optional(),
127
+ assetsUrl: z.string(),
128
+ apiBase: z.string(),
129
+ rpcBase: z.string(),
130
+ ui: z.object({
131
+ name: z.string(),
132
+ url: z.string(),
133
+ }).optional(),
134
+ });
135
+ export type ClientRuntimeConfig = z.infer<typeof ClientRuntimeConfigSchema>;
@@ -0,0 +1,134 @@
1
+ import { execa } from "execa";
2
+ import { cp, mkdir, mkdtemp, rm } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { dirname, join } from "path";
5
+
6
+ export interface FileSyncResult {
7
+ package: string;
8
+ files: string[];
9
+ depsAdded?: string[];
10
+ depsUpdated?: string[];
11
+ }
12
+
13
+ export interface FileSyncOptions {
14
+ configDir: string;
15
+ packages: string[];
16
+ bosConfig: {
17
+ app: Record<string, {
18
+ template?: string;
19
+ files?: string[];
20
+ sync?: { dependencies?: boolean; devDependencies?: boolean };
21
+ }>;
22
+ };
23
+ catalog?: Record<string, string>;
24
+ force?: boolean;
25
+ }
26
+
27
+ export async function syncFiles(options: FileSyncOptions): Promise<FileSyncResult[]> {
28
+ const { configDir, packages, bosConfig, catalog = {}, force } = options;
29
+ const results: FileSyncResult[] = [];
30
+
31
+ for (const pkg of packages) {
32
+ const pkgDir = `${configDir}/${pkg}`;
33
+ const pkgDirExists = await Bun.file(`${pkgDir}/package.json`).exists();
34
+ if (!pkgDirExists) continue;
35
+
36
+ const appConfig = bosConfig.app[pkg];
37
+
38
+ if (!appConfig?.template || !appConfig?.files) {
39
+ continue;
40
+ }
41
+
42
+ const tempDir = await mkdtemp(join(tmpdir(), `bos-files-${pkg}-`));
43
+
44
+ try {
45
+ await execa("npx", ["degit", appConfig.template, tempDir, "--force"], {
46
+ stdio: "pipe",
47
+ });
48
+
49
+ const filesSynced: string[] = [];
50
+ const depsAdded: string[] = [];
51
+ const depsUpdated: string[] = [];
52
+
53
+ for (const file of appConfig.files) {
54
+ const srcPath = join(tempDir, file);
55
+ const destPath = join(pkgDir, file);
56
+
57
+ try {
58
+ const destDir = dirname(destPath);
59
+ await mkdir(destDir, { recursive: true });
60
+ await cp(srcPath, destPath, { force: true, recursive: true });
61
+ filesSynced.push(file);
62
+ } catch {
63
+ }
64
+ }
65
+
66
+ const syncConfig = appConfig.sync ?? { dependencies: true, devDependencies: true };
67
+
68
+ if (syncConfig.dependencies !== false || syncConfig.devDependencies !== false) {
69
+ const templatePkgPath = join(tempDir, "package.json");
70
+ const localPkgPath = join(pkgDir, "package.json");
71
+
72
+ try {
73
+ const templatePkg = await Bun.file(templatePkgPath).json() as {
74
+ dependencies?: Record<string, string>;
75
+ devDependencies?: Record<string, string>;
76
+ scripts?: Record<string, string>;
77
+ };
78
+ const localPkg = await Bun.file(localPkgPath).json() as {
79
+ dependencies?: Record<string, string>;
80
+ devDependencies?: Record<string, string>;
81
+ scripts?: Record<string, string>;
82
+ };
83
+
84
+ if (syncConfig.dependencies !== false && templatePkg.dependencies) {
85
+ if (!localPkg.dependencies) localPkg.dependencies = {};
86
+ for (const [name, version] of Object.entries(templatePkg.dependencies)) {
87
+ if (!(name in localPkg.dependencies)) {
88
+ localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
89
+ depsAdded.push(name);
90
+ } else if (localPkg.dependencies[name] !== "catalog:" && version !== localPkg.dependencies[name]) {
91
+ localPkg.dependencies[name] = name in catalog ? "catalog:" : version;
92
+ depsUpdated.push(name);
93
+ }
94
+ }
95
+ }
96
+
97
+ if (syncConfig.devDependencies !== false && templatePkg.devDependencies) {
98
+ if (!localPkg.devDependencies) localPkg.devDependencies = {};
99
+ for (const [name, version] of Object.entries(templatePkg.devDependencies)) {
100
+ if (!(name in localPkg.devDependencies)) {
101
+ localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
102
+ depsAdded.push(name);
103
+ } else if (localPkg.devDependencies[name] !== "catalog:" && version !== localPkg.devDependencies[name]) {
104
+ localPkg.devDependencies[name] = name in catalog ? "catalog:" : version;
105
+ depsUpdated.push(name);
106
+ }
107
+ }
108
+ }
109
+
110
+ if (templatePkg.scripts) {
111
+ if (!localPkg.scripts) localPkg.scripts = {};
112
+ for (const [name, script] of Object.entries(templatePkg.scripts)) {
113
+ localPkg.scripts[name] = script;
114
+ }
115
+ }
116
+
117
+ await Bun.write(localPkgPath, JSON.stringify(localPkg, null, 2));
118
+ } catch {
119
+ }
120
+ }
121
+
122
+ results.push({
123
+ package: pkg,
124
+ files: filesSynced,
125
+ depsAdded: depsAdded.length > 0 ? depsAdded : undefined,
126
+ depsUpdated: depsUpdated.length > 0 ? depsUpdated : undefined,
127
+ });
128
+ } finally {
129
+ await rm(tempDir, { recursive: true, force: true });
130
+ }
131
+ }
132
+
133
+ return results;
134
+ }