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.
@@ -1,243 +1,276 @@
1
1
  import { appendFile } from "node:fs/promises";
2
- import { BunContext, BunRuntime } from "@effect/platform-bun";
3
2
  import { Effect } from "every-plugin/effect";
4
3
  import path from "path";
5
4
  import {
6
- type DevViewHandle,
7
- type LogEntry,
8
- type ProcessState,
9
- renderDevView,
5
+ type DevViewHandle,
6
+ type LogEntry,
7
+ type ProcessState,
8
+ renderDevView,
10
9
  } from "../components/dev-view";
11
10
  import { renderStreamingView } from "../components/streaming-view";
12
11
  import type { AppConfig, BosConfig } from "../config";
13
- import { getProcessConfig, makeDevProcess, type ProcessCallbacks, type ProcessHandle } from "./process";
12
+ import {
13
+ getProcessConfig,
14
+ makeDevProcess,
15
+ type ProcessCallbacks,
16
+ type ProcessHandle,
17
+ } from "./process";
14
18
 
15
19
  let activeCleanup: (() => Promise<void>) | null = null;
16
20
 
17
21
  const LOG_NOISE_PATTERNS = [
18
- /\[ Federation Runtime \] Version .* from host of shared singleton module/,
19
- /Executing an Effect versioned \d+\.\d+\.\d+ with a Runtime of version/,
20
- /you may want to dedupe the effect dependencies/,
22
+ /\[ Federation Runtime \] Version .* from (host|ui) of shared singleton module/,
23
+ /Executing an Effect versioned \d+\.\d+\.\d+ with a Runtime of version/,
24
+ /you may want to dedupe the effect dependencies/,
21
25
  ];
22
26
 
23
27
  const isDebugMode = (): boolean => {
24
- return process.env.DEBUG === "true" || process.env.DEBUG === "1";
28
+ return process.env.DEBUG === "true" || process.env.DEBUG === "1";
25
29
  };
26
30
 
27
31
  const shouldDisplayLog = (line: string): boolean => {
28
- if (isDebugMode()) return true;
29
- return !LOG_NOISE_PATTERNS.some(pattern => pattern.test(line));
32
+ if (isDebugMode()) return true;
33
+ return !LOG_NOISE_PATTERNS.some((pattern) => pattern.test(line));
30
34
  };
31
35
 
32
36
  export interface AppOrchestrator {
33
- packages: string[];
34
- env: Record<string, string>;
35
- description: string;
36
- appConfig: AppConfig;
37
- bosConfig?: BosConfig;
38
- port?: number;
39
- interactive?: boolean;
40
- noLogs?: boolean;
37
+ packages: string[];
38
+ env: Record<string, string>;
39
+ description: string;
40
+ appConfig: AppConfig;
41
+ bosConfig?: BosConfig;
42
+ port?: number;
43
+ interactive?: boolean;
44
+ noLogs?: boolean;
41
45
  }
42
46
 
43
47
  const isInteractiveSupported = (): boolean => {
44
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
48
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
45
49
  };
46
50
 
47
51
  const STARTUP_ORDER = ["ui-ssr", "ui", "api", "host"];
48
52
 
49
53
  const sortByOrder = (packages: string[]): string[] => {
50
- return [...packages].sort((a, b) => {
51
- const aIdx = STARTUP_ORDER.indexOf(a);
52
- const bIdx = STARTUP_ORDER.indexOf(b);
53
- if (aIdx === -1 && bIdx === -1) return 0;
54
- if (aIdx === -1) return 1;
55
- if (bIdx === -1) return -1;
56
- return aIdx - bIdx;
57
- });
54
+ return [...packages].sort((a, b) => {
55
+ const aIdx = STARTUP_ORDER.indexOf(a);
56
+ const bIdx = STARTUP_ORDER.indexOf(b);
57
+ if (aIdx === -1 && bIdx === -1) return 0;
58
+ if (aIdx === -1) return 1;
59
+ if (bIdx === -1) return -1;
60
+ return aIdx - bIdx;
61
+ });
58
62
  };
59
63
 
60
64
  const getLogDir = () => path.join(process.cwd(), ".bos", "logs");
61
65
  const getLogFile = () => {
62
- const now = new Date();
63
- const ts = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
64
- return path.join(getLogDir(), `dev-${ts}.log`);
66
+ const now = new Date();
67
+ const ts = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
68
+ return path.join(getLogDir(), `dev-${ts}.log`);
65
69
  };
66
70
 
67
71
  const ensureLogDir = async () => {
68
- const dir = getLogDir();
69
- await Bun.spawn(["mkdir", "-p", dir]).exited;
72
+ const dir = getLogDir();
73
+ await Bun.spawn(["mkdir", "-p", dir]).exited;
70
74
  };
71
75
 
72
76
  const formatLogLine = (entry: LogEntry): string => {
73
- const ts = new Date(entry.timestamp).toISOString();
74
- const prefix = entry.isError ? "ERR" : "OUT";
75
- return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`;
77
+ const ts = new Date(entry.timestamp).toISOString();
78
+ const prefix = entry.isError ? "ERR" : "OUT";
79
+ return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`;
76
80
  };
77
81
 
78
82
  export const runDevServers = (orchestrator: AppOrchestrator) =>
79
- Effect.gen(function* () {
80
- const orderedPackages = sortByOrder(orchestrator.packages);
81
-
82
- const initialProcesses: ProcessState[] = orderedPackages.map((pkg) => {
83
- const portOverride = pkg === "host" ? orchestrator.port : undefined;
84
- const config = getProcessConfig(pkg, undefined, portOverride);
85
- const source = pkg === "host"
86
- ? orchestrator.appConfig.host
87
- : pkg === "ui" || pkg === "ui-ssr"
88
- ? orchestrator.appConfig.ui
89
- : pkg === "api"
90
- ? orchestrator.appConfig.api
91
- : undefined;
92
- return {
93
- name: pkg,
94
- status: "pending" as const,
95
- port: config?.port ?? 0,
96
- source,
97
- };
98
- });
99
-
100
- const handles: ProcessHandle[] = [];
101
- const allLogs: LogEntry[] = [];
102
- let logFile: string | null = null;
103
- let view: DevViewHandle | null = null;
104
- let shuttingDown = false;
105
-
106
- if (!orchestrator.noLogs) {
107
- yield* Effect.promise(async () => {
108
- await ensureLogDir();
109
- logFile = getLogFile();
110
- await Bun.write(logFile, `# BOS Dev Session: ${orchestrator.description}\n# Started: ${new Date().toISOString()}\n\n`);
111
- });
112
- }
113
-
114
- const killAll = async () => {
115
- const reversed = [...handles].reverse();
116
- for (const handle of reversed) {
117
- try {
118
- await handle.kill();
119
- } catch { }
120
- }
121
- };
122
-
123
- const exportLogs = async () => {
124
- console.log("\n\n--- SESSION LOGS ---\n");
125
- for (const entry of allLogs) {
126
- console.log(formatLogLine(entry));
127
- }
128
- console.log("\n--- END LOGS ---\n");
129
- if (logFile) {
130
- console.log(`Full logs saved to: ${logFile}\n`);
131
- }
132
- };
133
-
134
- const cleanup = async (showLogs = false) => {
135
- if (shuttingDown) return;
136
- shuttingDown = true;
137
- view?.unmount();
138
- await killAll();
139
- if (showLogs) {
140
- await exportLogs();
141
- }
142
- activeCleanup = null;
143
- };
144
-
145
- activeCleanup = cleanup;
146
-
147
- const useInteractive = orchestrator.interactive ?? isInteractiveSupported();
148
-
149
- view = useInteractive
150
- ? renderDevView(
151
- initialProcesses,
152
- orchestrator.description,
153
- orchestrator.env,
154
- () => cleanup(false),
155
- () => cleanup(true)
156
- )
157
- : renderStreamingView(
158
- initialProcesses,
159
- orchestrator.description,
160
- orchestrator.env,
161
- () => cleanup(false),
162
- () => cleanup(true)
163
- );
164
-
165
- const callbacks: ProcessCallbacks = {
166
- onStatus: (name, status, message) => {
167
- view?.updateProcess(name, status, message);
168
- },
169
- onLog: (name, line, isError) => {
170
- const entry: LogEntry = {
171
- source: name,
172
- line,
173
- timestamp: Date.now(),
174
- isError,
175
- };
176
- allLogs.push(entry);
177
-
178
- if (shouldDisplayLog(line)) {
179
- view?.addLog(name, line, isError);
180
- }
181
-
182
- if (logFile) {
183
- const logLine = formatLogLine(entry) + "\n";
184
- appendFile(logFile, logLine).catch(() => { });
185
- }
186
- },
187
- };
188
-
189
- for (const pkg of orderedPackages) {
190
- const portOverride = pkg === "host" ? orchestrator.port : undefined;
191
- const handle = yield* makeDevProcess(pkg, orchestrator.env, callbacks, portOverride, orchestrator.bosConfig);
192
- handles.push(handle);
193
-
194
- yield* Effect.race(
195
- handle.waitForReady,
196
- Effect.sleep("30 seconds").pipe(
197
- Effect.andThen(Effect.sync(() => {
198
- callbacks.onLog(pkg, "Timeout waiting for ready, continuing...", true);
199
- }))
200
- )
201
- );
202
- }
203
-
204
- yield* Effect.addFinalizer(() =>
205
- Effect.promise(() => cleanup(false))
206
- );
207
-
208
- yield* Effect.never;
209
- });
83
+ Effect.gen(function* () {
84
+ const orderedPackages = sortByOrder(orchestrator.packages);
85
+
86
+ const initialProcesses: ProcessState[] = orderedPackages.map((pkg) => {
87
+ const portOverride = pkg === "host" ? orchestrator.port : undefined;
88
+ const config = getProcessConfig(pkg, undefined, portOverride);
89
+ const source =
90
+ pkg === "host"
91
+ ? orchestrator.appConfig.host
92
+ : pkg === "ui"
93
+ ? orchestrator.appConfig.ui
94
+ : pkg === "api"
95
+ ? orchestrator.appConfig.api
96
+ : undefined;
97
+ return {
98
+ name: pkg,
99
+ status: "pending" as const,
100
+ port: config?.port ?? 0,
101
+ source,
102
+ };
103
+ });
104
+
105
+ const handles: ProcessHandle[] = [];
106
+ const allLogs: LogEntry[] = [];
107
+ let logFile: string | null = null;
108
+ let view: DevViewHandle | null = null;
109
+ let shuttingDown = false;
110
+
111
+ if (!orchestrator.noLogs) {
112
+ yield* Effect.promise(async () => {
113
+ await ensureLogDir();
114
+ logFile = getLogFile();
115
+ await Bun.write(
116
+ logFile,
117
+ `# BOS Dev Session: ${orchestrator.description}\n# Started: ${new Date().toISOString()}\n\n`,
118
+ );
119
+ });
120
+ }
121
+
122
+ const killAll = async () => {
123
+ const reversed = [...handles].reverse();
124
+ for (const handle of reversed) {
125
+ try {
126
+ await handle.kill();
127
+ } catch {}
128
+ }
129
+ };
130
+
131
+ const exportLogs = async () => {
132
+ console.log("\n");
133
+ console.log("═".repeat(70));
134
+ console.log(` SESSION LOGS: ${orchestrator.description}`);
135
+ console.log(
136
+ ` Started: ${new Date(allLogs[0]?.timestamp || Date.now()).toISOString()}`,
137
+ );
138
+ console.log(` Total entries: ${allLogs.length}`);
139
+ console.log("═".repeat(70));
140
+ console.log("");
141
+
142
+ for (const entry of allLogs) {
143
+ console.log(formatLogLine(entry));
144
+ }
145
+
146
+ console.log("");
147
+ console.log("═".repeat(70));
148
+ console.log(" END OF LOGS");
149
+ if (logFile) {
150
+ console.log(` Full logs saved to: ${logFile}`);
151
+ }
152
+ console.log("═".repeat(70));
153
+ console.log("");
154
+ };
155
+
156
+ const cleanup = async (showLogs = false) => {
157
+ if (shuttingDown) return;
158
+ shuttingDown = true;
159
+ view?.unmount();
160
+ await killAll();
161
+ if (showLogs) {
162
+ await exportLogs();
163
+ }
164
+ activeCleanup = null;
165
+ };
166
+
167
+ activeCleanup = cleanup;
168
+
169
+ const useInteractive = orchestrator.interactive ?? isInteractiveSupported();
170
+
171
+ view = useInteractive
172
+ ? renderDevView(
173
+ initialProcesses,
174
+ orchestrator.description,
175
+ orchestrator.env,
176
+ () => cleanup(false),
177
+ () => cleanup(true),
178
+ )
179
+ : renderStreamingView(
180
+ initialProcesses,
181
+ orchestrator.description,
182
+ orchestrator.env,
183
+ () => cleanup(false),
184
+ () => cleanup(true),
185
+ );
186
+
187
+ const callbacks: ProcessCallbacks = {
188
+ onStatus: (name, status, message) => {
189
+ view?.updateProcess(name, status, message);
190
+ },
191
+ onLog: (name, line, isError) => {
192
+ const entry: LogEntry = {
193
+ source: name,
194
+ line,
195
+ timestamp: Date.now(),
196
+ isError,
197
+ };
198
+ allLogs.push(entry);
199
+
200
+ if (shouldDisplayLog(line)) {
201
+ view?.addLog(name, line, isError);
202
+ }
203
+
204
+ if (logFile) {
205
+ const logLine = formatLogLine(entry) + "\n";
206
+ appendFile(logFile, logLine).catch(() => {});
207
+ }
208
+ },
209
+ };
210
+
211
+ for (const pkg of orderedPackages) {
212
+ const portOverride = pkg === "host" ? orchestrator.port : undefined;
213
+ const handle = yield* makeDevProcess(
214
+ pkg,
215
+ orchestrator.env,
216
+ callbacks,
217
+ portOverride,
218
+ orchestrator.bosConfig,
219
+ );
220
+ handles.push(handle);
221
+
222
+ yield* Effect.race(
223
+ handle.waitForReady,
224
+ Effect.sleep("30 seconds").pipe(
225
+ Effect.andThen(
226
+ Effect.sync(() => {
227
+ callbacks.onLog(
228
+ pkg,
229
+ "Timeout waiting for ready, continuing...",
230
+ true,
231
+ );
232
+ }),
233
+ ),
234
+ ),
235
+ );
236
+ }
237
+
238
+ yield* Effect.addFinalizer(() => Effect.promise(() => cleanup(false)));
239
+
240
+ yield* Effect.never;
241
+ });
210
242
 
211
243
  export const startApp = (orchestrator: AppOrchestrator) => {
212
- const program = Effect.scoped(runDevServers(orchestrator)).pipe(
213
- Effect.provide(BunContext.layer),
214
- Effect.catchAll((e) => Effect.sync(() => {
215
- if (e instanceof Error) {
216
- console.error("App server error:", e.message);
217
- if (e.stack) {
218
- console.error(e.stack);
219
- }
220
- } else if (typeof e === 'object' && e !== null) {
221
- console.error("App server error:", JSON.stringify(e, null, 2));
222
- } else {
223
- console.error("App server error:", e);
224
- }
225
- }))
226
- );
227
-
228
- const handleSignal = async () => {
229
- if (activeCleanup) {
230
- await activeCleanup();
231
- }
232
- };
233
-
234
- process.on("SIGINT", () => {
235
- handleSignal().finally(() => process.exit(0));
236
- });
237
-
238
- process.on("SIGTERM", () => {
239
- handleSignal().finally(() => process.exit(0));
240
- });
241
-
242
- BunRuntime.runMain(program);
244
+ const program = Effect.scoped(runDevServers(orchestrator)).pipe(
245
+ Effect.catchAll((e) =>
246
+ Effect.sync(() => {
247
+ if (e instanceof Error) {
248
+ console.error("App server error:", e.message);
249
+ if (e.stack) {
250
+ console.error(e.stack);
251
+ }
252
+ } else if (typeof e === "object" && e !== null) {
253
+ console.error("App server error:", JSON.stringify(e, null, 2));
254
+ } else {
255
+ console.error("App server error:", e);
256
+ }
257
+ }),
258
+ ),
259
+ );
260
+
261
+ const handleSignal = async () => {
262
+ if (activeCleanup) {
263
+ await activeCleanup();
264
+ }
265
+ };
266
+
267
+ process.on("SIGINT", () => {
268
+ handleSignal().finally(() => process.exit(0));
269
+ });
270
+
271
+ process.on("SIGTERM", () => {
272
+ handleSignal().finally(() => process.exit(0));
273
+ });
274
+
275
+ Effect.runPromise(program);
243
276
  };