everything-dev 1.7.2 → 1.8.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.
Files changed (134) hide show
  1. package/dist/api.cjs +1 -1
  2. package/dist/api.mjs +1 -1
  3. package/dist/app.cjs +82 -51
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.mjs +82 -51
  6. package/dist/app.mjs.map +1 -1
  7. package/dist/cli/upgrade.cjs.map +1 -1
  8. package/dist/cli/upgrade.mjs.map +1 -1
  9. package/dist/components/dev-view.cjs +6 -3
  10. package/dist/components/dev-view.cjs.map +1 -1
  11. package/dist/components/dev-view.mjs +6 -3
  12. package/dist/components/dev-view.mjs.map +1 -1
  13. package/dist/components/streaming-view.cjs +5 -2
  14. package/dist/components/streaming-view.cjs.map +1 -1
  15. package/dist/components/streaming-view.mjs +5 -2
  16. package/dist/components/streaming-view.mjs.map +1 -1
  17. package/dist/config.cjs +28 -5
  18. package/dist/config.cjs.map +1 -1
  19. package/dist/config.d.cts.map +1 -1
  20. package/dist/config.d.mts.map +1 -1
  21. package/dist/config.mjs +28 -5
  22. package/dist/config.mjs.map +1 -1
  23. package/dist/contract.cjs +1 -0
  24. package/dist/contract.cjs.map +1 -1
  25. package/dist/contract.d.cts +10 -2
  26. package/dist/contract.d.cts.map +1 -1
  27. package/dist/contract.d.mts +10 -2
  28. package/dist/contract.d.mts.map +1 -1
  29. package/dist/contract.mjs +1 -0
  30. package/dist/contract.mjs.map +1 -1
  31. package/dist/dev-logs.cjs +6 -2
  32. package/dist/dev-logs.cjs.map +1 -1
  33. package/dist/dev-logs.mjs +7 -2
  34. package/dist/dev-logs.mjs.map +1 -1
  35. package/dist/dev-session.cjs +27 -23
  36. package/dist/dev-session.cjs.map +1 -1
  37. package/dist/dev-session.mjs +27 -24
  38. package/dist/dev-session.mjs.map +1 -1
  39. package/dist/federation.server.cjs +1 -1
  40. package/dist/federation.server.mjs +1 -1
  41. package/dist/host.cjs +4 -3
  42. package/dist/host.cjs.map +1 -1
  43. package/dist/host.d.cts.map +1 -1
  44. package/dist/host.d.mts.map +1 -1
  45. package/dist/host.mjs +4 -3
  46. package/dist/host.mjs.map +1 -1
  47. package/dist/integrity.cjs +68 -2
  48. package/dist/integrity.cjs.map +1 -1
  49. package/dist/integrity.d.cts +14 -1
  50. package/dist/integrity.d.cts.map +1 -1
  51. package/dist/integrity.d.mts +14 -1
  52. package/dist/integrity.d.mts.map +1 -1
  53. package/dist/integrity.mjs +66 -3
  54. package/dist/integrity.mjs.map +1 -1
  55. package/dist/mf.cjs +32 -0
  56. package/dist/mf.cjs.map +1 -1
  57. package/dist/mf.d.cts +3 -1
  58. package/dist/mf.d.cts.map +1 -1
  59. package/dist/mf.d.mts +3 -1
  60. package/dist/mf.d.mts.map +1 -1
  61. package/dist/mf.mjs +32 -1
  62. package/dist/mf.mjs.map +1 -1
  63. package/dist/orchestrator.cjs +167 -317
  64. package/dist/orchestrator.cjs.map +1 -1
  65. package/dist/orchestrator.d.cts +24 -21
  66. package/dist/orchestrator.d.cts.map +1 -1
  67. package/dist/orchestrator.d.mts +24 -21
  68. package/dist/orchestrator.d.mts.map +1 -1
  69. package/dist/orchestrator.mjs +168 -316
  70. package/dist/orchestrator.mjs.map +1 -1
  71. package/dist/plugin.cjs +38 -107
  72. package/dist/plugin.cjs.map +1 -1
  73. package/dist/plugin.d.cts +16 -2
  74. package/dist/plugin.d.cts.map +1 -1
  75. package/dist/plugin.d.mts +16 -2
  76. package/dist/plugin.d.mts.map +1 -1
  77. package/dist/plugin.mjs +39 -108
  78. package/dist/plugin.mjs.map +1 -1
  79. package/dist/service-descriptor.cjs +188 -0
  80. package/dist/service-descriptor.cjs.map +1 -0
  81. package/dist/service-descriptor.d.cts +107 -0
  82. package/dist/service-descriptor.d.cts.map +1 -0
  83. package/dist/service-descriptor.d.mts +107 -0
  84. package/dist/service-descriptor.d.mts.map +1 -0
  85. package/dist/service-descriptor.mjs +182 -0
  86. package/dist/service-descriptor.mjs.map +1 -0
  87. package/dist/types.cjs +8 -1
  88. package/dist/types.cjs.map +1 -1
  89. package/dist/types.d.cts +16 -1
  90. package/dist/types.d.cts.map +1 -1
  91. package/dist/types.d.mts +16 -1
  92. package/dist/types.d.mts.map +1 -1
  93. package/dist/types.mjs +8 -1
  94. package/dist/types.mjs.map +1 -1
  95. package/dist/ui/index.cjs +1 -0
  96. package/dist/ui/index.d.cts +2 -2
  97. package/dist/ui/index.d.mts +2 -2
  98. package/dist/ui/index.mjs +2 -2
  99. package/dist/ui/runtime.cjs +4 -0
  100. package/dist/ui/runtime.cjs.map +1 -1
  101. package/dist/ui/runtime.d.cts +2 -1
  102. package/dist/ui/runtime.d.cts.map +1 -1
  103. package/dist/ui/runtime.d.mts +2 -1
  104. package/dist/ui/runtime.d.mts.map +1 -1
  105. package/dist/ui/runtime.mjs +4 -1
  106. package/dist/ui/runtime.mjs.map +1 -1
  107. package/package.json +12 -4
  108. package/skills/dev-workflow/SKILL.md +105 -0
  109. package/skills/publish-sync/SKILL.md +130 -0
  110. package/src/app.ts +98 -204
  111. package/src/cli/upgrade.ts +20 -4
  112. package/src/components/dev-view.tsx +8 -3
  113. package/src/components/streaming-view.ts +7 -2
  114. package/src/config.ts +40 -8
  115. package/src/contract.ts +1 -0
  116. package/src/dev-logs.ts +8 -1
  117. package/src/dev-session.ts +56 -79
  118. package/src/host.ts +4 -3
  119. package/src/integrity.ts +96 -10
  120. package/src/mf.ts +42 -0
  121. package/src/orchestrator.ts +232 -411
  122. package/src/plugin.ts +48 -136
  123. package/src/service-descriptor.ts +258 -0
  124. package/src/types.ts +8 -1
  125. package/src/ui/runtime.ts +5 -0
  126. package/dist/process-registry.cjs +0 -120
  127. package/dist/process-registry.cjs.map +0 -1
  128. package/dist/process-registry.d.cts +0 -25
  129. package/dist/process-registry.d.cts.map +0 -1
  130. package/dist/process-registry.d.mts +0 -25
  131. package/dist/process-registry.d.mts.map +0 -1
  132. package/dist/process-registry.mjs +0 -119
  133. package/dist/process-registry.mjs.map +0 -1
  134. package/src/process-registry.ts +0 -154
@@ -1,20 +1,14 @@
1
1
  import { createConnection } from "node:net";
2
- import { Deferred, Effect, Fiber, Ref } from "effect";
3
- import { getHostDevelopmentPort, getProjectRoot, parsePort } from "./config";
2
+ import { Command } from "@effect/platform";
3
+ import type { ExitCode } from "@effect/platform/CommandExecutor";
4
+ import { Deferred, Effect, Option, Ref, Stream } from "effect";
4
5
  import { patchManifestFetchForSsrPublicPath } from "./mf";
5
- import type { ProcessRegistry } from "./process-registry";
6
- import type { BosConfig, RuntimeConfig } from "./types";
7
-
8
- export interface DevProcess {
9
- name: string;
10
- command: string;
11
- args: string[];
12
- cwd: string;
13
- env?: Record<string, string>;
14
- port: number;
15
- readyPatterns: RegExp[];
16
- errorPatterns: RegExp[];
17
- }
6
+ import {
7
+ DevRuntimeConfig,
8
+ type ServiceDescriptor,
9
+ ServiceDescriptorMap,
10
+ } from "./service-descriptor";
11
+ import type { RuntimeConfig } from "./types";
18
12
 
19
13
  export interface ProcessCallbacks {
20
14
  onStatus: (name: string, status: ProcessStatus, message?: string) => void;
@@ -26,148 +20,17 @@ export interface ProcessHandle {
26
20
  pid: number | undefined;
27
21
  kill: Effect.Effect<void, unknown>;
28
22
  waitForReady: Effect.Effect<void, Error>;
29
- waitForExit: Effect.Effect<unknown>;
23
+ waitForExit: Effect.Effect<ExitCode, unknown>;
30
24
  }
31
25
 
32
26
  export type ProcessStatus = "pending" | "starting" | "ready" | "error";
33
27
 
34
- interface ProcessConfigBase {
28
+ export interface ProcessState {
35
29
  name: string;
36
- command: string;
37
- args: string[];
38
- cwd: string;
39
- readyPatterns: RegExp[];
40
- errorPatterns: RegExp[];
41
- }
42
-
43
- const processConfigBases: Record<string, ProcessConfigBase> = {
44
- "host-build": {
45
- name: "host-build",
46
- command: "bun",
47
- args: ["run", "build"],
48
- cwd: "host",
49
- readyPatterns: [/built in/i, /compiled.*successfully/i],
50
- errorPatterns: [/error:/i, /failed/i, /exception/i],
51
- },
52
- host: {
53
- name: "host",
54
- command: "bun",
55
- args: ["run", "dev"],
56
- cwd: "host",
57
- readyPatterns: [/Host (dev|production) server running at/i, /Server running at/i],
58
- errorPatterns: [/error:/i, /failed/i, /exception/i],
59
- },
60
- ui: {
61
- name: "ui",
62
- command: "bun",
63
- args: ["run", "dev"],
64
- cwd: "ui",
65
- readyPatterns: [/\bready\s+built in\b/i, /\bLocal:\b/i, /\bcompiled\b.*successfully/i],
66
- errorPatterns: [/error/i, /failed to compile/i],
67
- },
68
- "ui-ssr": {
69
- name: "ui-ssr",
70
- command: "bun",
71
- args: ["run", "dev:ssr"],
72
- cwd: "ui",
73
- readyPatterns: [/\bready\s+built in\b/i, /\bcompiled\b.*successfully/i],
74
- errorPatterns: [/error/i, /failed/i],
75
- },
76
- api: {
77
- name: "api",
78
- command: "bun",
79
- args: ["run", "dev"],
80
- cwd: "api",
81
- readyPatterns: [/ready in/i, /compiled.*successfully/i, /listening/i, /started/i],
82
- errorPatterns: [/error/i, /failed/i],
83
- },
84
- };
85
-
86
- export function getProcessConfig(
87
- pkg: string,
88
- env?: Record<string, string>,
89
- portOverride?: number,
90
- bosConfig?: BosConfig,
91
- runtimeConfig?: RuntimeConfig,
92
- ): DevProcess | null {
93
- if (pkg === "auth") {
94
- const authConfig = runtimeConfig?.auth;
95
- if (!authConfig?.localPath || authConfig.source !== "local") return null;
96
-
97
- const port =
98
- portOverride ?? authConfig.port ?? (authConfig.url ? parsePort(authConfig.url) : 3020);
99
-
100
- return {
101
- name: "auth",
102
- command: "bun",
103
- args: ["run", "dev"],
104
- cwd: authConfig.localPath,
105
- port,
106
- readyPatterns: [/ready in/i, /compiled.*successfully/i, /listening/i, /started/i],
107
- errorPatterns: [/error/i, /failed/i],
108
- env,
109
- };
110
- }
111
-
112
- if (pkg.startsWith("plugin:")) {
113
- const pluginId = pkg.slice("plugin:".length);
114
- const pluginConfig = runtimeConfig?.plugins?.[pluginId] ?? null;
115
- const localPath = pluginConfig?.localPath;
116
-
117
- if (!localPath || pluginConfig?.source !== "local") return null;
118
-
119
- const port =
120
- portOverride ?? pluginConfig?.port ?? (pluginConfig?.url ? parsePort(pluginConfig.url) : 0);
121
-
122
- return {
123
- name: pkg,
124
- command: "bun",
125
- args: ["run", "dev"],
126
- cwd: localPath,
127
- port,
128
- readyPatterns: [/ready in/i, /compiled.*successfully/i, /listening/i, /started/i],
129
- errorPatterns: [/error/i, /failed/i],
130
- env,
131
- };
132
- }
133
-
134
- const base = processConfigBases[pkg];
135
- if (!base) return null;
136
-
137
- let port: number;
138
- if (pkg === "host") {
139
- port =
140
- portOverride ??
141
- (runtimeConfig?.hostUrl
142
- ? parsePort(runtimeConfig.hostUrl)
143
- : bosConfig
144
- ? getHostDevelopmentPort(bosConfig.app.host.development)
145
- : 3000);
146
- } else if (pkg === "ui") {
147
- port =
148
- runtimeConfig?.ui.port ?? (runtimeConfig?.ui.url ? parsePort(runtimeConfig.ui.url) : 3002);
149
- } else if (pkg === "ui-ssr") {
150
- const uiPort = runtimeConfig?.ui.ssrUrl
151
- ? parsePort(runtimeConfig.ui.ssrUrl)
152
- : runtimeConfig?.ui.port
153
- ? runtimeConfig.ui.port + 1
154
- : 3003;
155
- port = uiPort;
156
- } else if (pkg === "api") {
157
- port =
158
- runtimeConfig?.api.port ?? (runtimeConfig?.api.url ? parsePort(runtimeConfig.api.url) : 3014);
159
- } else {
160
- port = 0;
161
- }
162
-
163
- const cwd =
164
- pkg === "ui"
165
- ? (runtimeConfig?.ui.localPath ?? base.cwd)
166
- : pkg === "api"
167
- ? (runtimeConfig?.api.localPath ?? base.cwd)
168
- : base.cwd;
169
-
170
- return { ...base, cwd, port, env };
30
+ status: ProcessStatus;
31
+ port: number;
32
+ message?: string;
33
+ source?: "local" | "remote";
171
34
  }
172
35
 
173
36
  const stripAnsi = (input: string): string => {
@@ -178,49 +41,54 @@ const stripAnsi = (input: string): string => {
178
41
  .replace(new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, "g"), "");
179
42
  };
180
43
 
181
- const probeHttpOk = async (url: string, timeoutMs = 400): Promise<boolean> => {
182
- const controller = new AbortController();
183
- const timer = setTimeout(() => controller.abort(), timeoutMs);
184
- try {
185
- const res = await fetch(url, { signal: controller.signal });
186
- return res.ok;
187
- } catch {
188
- return false;
189
- } finally {
190
- clearTimeout(timer);
191
- }
192
- };
44
+ const probeHttpOk = (url: string, timeoutMs = 400) =>
45
+ Effect.tryPromise({
46
+ try: async () => {
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
49
+ try {
50
+ const res = await fetch(url, { signal: controller.signal });
51
+ return res.ok;
52
+ } catch {
53
+ return false;
54
+ } finally {
55
+ clearTimeout(timer);
56
+ }
57
+ },
58
+ catch: () => false,
59
+ });
193
60
 
194
- const probeTcpOpen = async (port: number, timeoutMs = 250): Promise<boolean> => {
195
- return new Promise((resolve) => {
61
+ const probeTcpOpen = (port: number, timeoutMs = 250) =>
62
+ Effect.async<boolean>((resume) => {
196
63
  const socket = createConnection({ host: "127.0.0.1", port });
197
64
  const timer = setTimeout(() => {
198
65
  socket.destroy();
199
- resolve(false);
66
+ resume(Effect.succeed(false));
200
67
  }, timeoutMs);
201
68
  socket.once("connect", () => {
202
69
  clearTimeout(timer);
203
70
  socket.destroy();
204
- resolve(true);
71
+ resume(Effect.succeed(true));
205
72
  });
206
73
  socket.once("error", () => {
207
74
  clearTimeout(timer);
208
- resolve(false);
75
+ resume(Effect.succeed(false));
209
76
  });
210
77
  });
211
- };
212
78
 
213
79
  const detectStatus = (
214
80
  line: string,
215
- config: DevProcess,
81
+ descriptor: ServiceDescriptor,
216
82
  ): { status: ProcessStatus; isError: boolean } | null => {
217
83
  const cleanLine = stripAnsi(line);
218
- for (const pattern of config.errorPatterns) {
84
+ const errorPatterns = descriptor.errorPatterns ?? [];
85
+ const readyPatterns = descriptor.readyPatterns ?? [];
86
+ for (const pattern of errorPatterns) {
219
87
  if (pattern.test(cleanLine)) {
220
88
  return { status: "error", isError: true };
221
89
  }
222
90
  }
223
- for (const pattern of config.readyPatterns) {
91
+ for (const pattern of readyPatterns) {
224
92
  if (pattern.test(cleanLine)) {
225
93
  return { status: "ready", isError: false };
226
94
  }
@@ -228,46 +96,6 @@ const detectStatus = (
228
96
  return null;
229
97
  };
230
98
 
231
- const killProcessTree = (pid: number) =>
232
- Effect.gen(function* () {
233
- const killSignal = (signal: NodeJS.Signals) =>
234
- Effect.try({
235
- try: () => {
236
- process.kill(-pid, signal);
237
- },
238
- catch: () => null,
239
- }).pipe(Effect.ignore);
240
-
241
- const killDirect = (signal: NodeJS.Signals) =>
242
- Effect.try({
243
- try: () => {
244
- process.kill(pid, signal);
245
- },
246
- catch: () => null,
247
- }).pipe(Effect.ignore);
248
-
249
- const isRunning = () =>
250
- Effect.try({
251
- try: () => {
252
- process.kill(pid, 0);
253
- return true;
254
- },
255
- catch: () => false,
256
- });
257
-
258
- yield* killSignal("SIGTERM");
259
- yield* killDirect("SIGTERM");
260
-
261
- yield* Effect.sleep("200 millis");
262
-
263
- const stillRunning = yield* isRunning();
264
- if (stillRunning) {
265
- yield* killSignal("SIGKILL");
266
- yield* killDirect("SIGKILL");
267
- yield* Effect.sleep("100 millis");
268
- }
269
- });
270
-
271
99
  interface ServerHandle {
272
100
  ready: Promise<void>;
273
101
  shutdown: () => Promise<void>;
@@ -310,27 +138,18 @@ const patchConsole = (name: string, callbacks: ProcessCallbacks): (() => void) =
310
138
  };
311
139
  };
312
140
 
313
- export const spawnRemoteHost = (
314
- config: DevProcess,
315
- callbacks: ProcessCallbacks,
316
- runtimeConfig: RuntimeConfig,
317
- ) =>
141
+ const spawnRemoteHost = (descriptor: ServiceDescriptor, callbacks: ProcessCallbacks) =>
318
142
  Effect.gen(function* () {
319
- const remoteUrl = config.env?.HOST_REMOTE_URL;
143
+ const runtimeConfig = yield* DevRuntimeConfig;
144
+ const remoteUrl = descriptor.remoteUrl;
320
145
  if (!remoteUrl) {
321
- return yield* Effect.fail(new Error("HOST_REMOTE_URL not provided for remote host"));
146
+ return yield* Effect.fail(new Error("remoteUrl not provided on host descriptor"));
322
147
  }
323
148
 
324
- if (config.env) {
325
- for (const [key, value] of Object.entries(config.env)) {
326
- process.env[key] = value;
327
- }
328
- }
329
-
330
- callbacks.onStatus(config.name, "starting");
331
- callbacks.onLog(config.name, `Remote: ${remoteUrl}`);
332
- const restoreConsole = patchConsole(config.name, callbacks);
333
- callbacks.onLog(config.name, "Loading Module Federation runtime...");
149
+ callbacks.onStatus(descriptor.key, "starting");
150
+ callbacks.onLog(descriptor.key, `Remote: ${remoteUrl}`);
151
+ const restoreConsole = patchConsole(descriptor.key, callbacks);
152
+ callbacks.onLog(descriptor.key, "Loading Module Federation runtime...");
334
153
 
335
154
  const mfRuntime = yield* Effect.tryPromise({
336
155
  try: () => import("@module-federation/enhanced/runtime"),
@@ -378,7 +197,7 @@ export const spawnRemoteHost = (
378
197
  });
379
198
 
380
199
  (mf as any).registerRemotes([{ name: "host", entry: entryUrl }]);
381
- callbacks.onLog(config.name, `Loading host from ${entryUrl}...`);
200
+ callbacks.onLog(descriptor.key, `Loading host from ${entryUrl}...`);
382
201
 
383
202
  const hostModule = yield* Effect.tryPromise({
384
203
  try: () =>
@@ -392,20 +211,20 @@ export const spawnRemoteHost = (
392
211
  return yield* Effect.fail(new Error("Host module does not export runServer function"));
393
212
  }
394
213
 
395
- callbacks.onLog(config.name, "Starting server...");
214
+ callbacks.onLog(descriptor.key, "Starting server...");
396
215
  const serverHandle = hostModule.runServer({ config: runtimeConfig });
397
216
  yield* Effect.tryPromise({
398
217
  try: () => serverHandle.ready,
399
218
  catch: (e) => new Error(`Server failed to start: ${e}`),
400
219
  });
401
220
 
402
- callbacks.onStatus(config.name, "ready");
221
+ callbacks.onStatus(descriptor.key, "ready");
403
222
 
404
223
  return {
405
- name: config.name,
224
+ name: descriptor.key,
406
225
  pid: process.pid,
407
226
  kill: Effect.gen(function* () {
408
- callbacks.onLog(config.name, "Shutting down remote host...");
227
+ callbacks.onLog(descriptor.key, "Shutting down remote host...");
409
228
  restoreConsole();
410
229
  yield* Effect.tryPromise({
411
230
  try: () => serverHandle.shutdown(),
@@ -417,55 +236,53 @@ export const spawnRemoteHost = (
417
236
  } satisfies ProcessHandle;
418
237
  });
419
238
 
420
- export const spawnDevProcess = (
421
- config: DevProcess,
422
- callbacks: ProcessCallbacks,
423
- runtimeConfig?: RuntimeConfig,
424
- registry?: ProcessRegistry,
425
- ) =>
239
+ const spawnDevProcess = (descriptor: ServiceDescriptor, callbacks: ProcessCallbacks) =>
426
240
  Effect.gen(function* () {
427
- let configDir: string;
428
- try {
429
- configDir = getProjectRoot();
430
- } catch {
431
- configDir = process.cwd();
241
+ const runtimeConfig = yield* DevRuntimeConfig;
242
+
243
+ if (!descriptor.localPath) {
244
+ return yield* Effect.fail(new Error(`No localPath for local service: ${descriptor.key}`));
432
245
  }
433
- const fullCwd = config.cwd.startsWith("/") ? config.cwd : `${configDir}/${config.cwd}`;
246
+
247
+ const fullCwd = descriptor.localPath;
248
+ const command = descriptor.command ?? "bun";
249
+ const args = descriptor.args ?? ["run", "dev"];
250
+ const port = descriptor.port ?? descriptor.defaultPort;
251
+ const name = descriptor.key;
252
+
434
253
  const readyDeferred = yield* Deferred.make<void, Error>();
435
254
  const statusRef = yield* Ref.make<ProcessStatus>("starting");
436
255
 
437
- callbacks.onStatus(config.name, "starting");
256
+ callbacks.onStatus(name, "starting");
438
257
 
439
258
  const envVars: Record<string, string> = {
440
259
  ...(process.env as Record<string, string>),
441
- ...config.env,
442
260
  FORCE_COLOR: "1",
443
- ...(config.port > 0 ? { PORT: String(config.port) } : {}),
261
+ ...(port > 0 ? { PORT: String(port) } : {}),
444
262
  };
445
263
 
446
- if (runtimeConfig && config.name === "host") {
264
+ if (name === "host") {
447
265
  envVars.BOS_RUNTIME_CONFIG = JSON.stringify(runtimeConfig);
448
266
  }
449
267
 
450
- const proc = Bun.spawn({
451
- cmd: [config.command, ...config.args],
452
- cwd: fullCwd,
453
- env: envVars,
454
- stdio: ["inherit", "pipe", "pipe"],
455
- });
268
+ const cmd = Command.make(command, ...args).pipe(
269
+ Command.workingDirectory(fullCwd),
270
+ Command.env(envVars),
271
+ );
272
+
273
+ const proc = yield* Command.start(cmd);
456
274
 
457
275
  const markReady = Effect.gen(function* () {
458
276
  const currentStatus = yield* Ref.get(statusRef);
459
277
  if (currentStatus === "ready" || currentStatus === "error") return;
460
278
  yield* Ref.set(statusRef, "ready");
461
- callbacks.onStatus(config.name, "ready");
279
+ callbacks.onStatus(name, "ready");
462
280
  yield* Deferred.succeed(readyDeferred, undefined).pipe(Effect.ignore);
463
281
  });
464
282
 
465
- if (config.port > 0) {
466
- const readinessPath =
467
- config.name === "host" ? "/health" : config.name === "ui-ssr" ? "/" : "/remoteEntry.js";
468
- const url = `http://127.0.0.1:${config.port}${readinessPath}`;
283
+ if (port > 0) {
284
+ const readinessPath = descriptor.readinessPath;
285
+ const url = `http://127.0.0.1:${port}${readinessPath}`;
469
286
 
470
287
  yield* Effect.forkScoped(
471
288
  Effect.gen(function* () {
@@ -473,74 +290,61 @@ export const spawnDevProcess = (
473
290
  while (Date.now() < deadline) {
474
291
  const status = yield* Ref.get(statusRef);
475
292
  if (status === "ready" || status === "error") return;
476
- const ok = url
477
- ? yield* Effect.tryPromise({
478
- try: () => probeHttpOk(url),
479
- catch: () => false,
480
- })
481
- : yield* Effect.tryPromise({
482
- try: () => probeTcpOpen(config.port),
483
- catch: () => false,
484
- });
293
+ const ok = yield* probeHttpOk(url);
485
294
  if (ok) {
486
295
  yield* markReady;
487
296
  return;
488
297
  }
298
+ const tcpOk = yield* probeTcpOpen(port);
299
+ if (tcpOk) {
300
+ yield* markReady;
301
+ return;
302
+ }
489
303
  yield* Effect.sleep("200 millis");
490
304
  }
491
305
  }),
492
306
  );
493
307
  }
494
308
 
495
- if (registry && proc.pid) {
496
- yield* registry.track({
497
- pid: proc.pid,
498
- name: config.name,
499
- port: config.port,
500
- startedAt: Date.now(),
501
- command: [config.command, ...config.args].join(" "),
502
- });
503
- }
309
+ const pid = Number(proc.pid);
504
310
 
505
311
  yield* Effect.forkScoped(
506
- Effect.promise(() => proc.exited).pipe(
507
- Effect.andThen((code) =>
508
- Effect.gen(function* () {
509
- if (registry && proc.pid) {
510
- yield* registry.untrack(proc.pid).pipe(Effect.ignore);
511
- }
512
- const currentStatus = yield* Ref.get(statusRef);
513
- if (currentStatus === "ready") return;
514
- callbacks.onLog(config.name, `Process exited before ready (exit code: ${code})`, true);
515
- yield* Ref.set(statusRef, "error");
516
- callbacks.onStatus(config.name, "error");
517
- yield* Deferred.fail(
518
- readyDeferred,
519
- new Error(`Process exited before ready: ${config.name}`),
520
- ).pipe(Effect.ignore);
521
- }),
522
- ),
523
- ),
312
+ Effect.gen(function* () {
313
+ const exitCode = yield* proc.exitCode;
314
+ const currentStatus = yield* Ref.get(statusRef);
315
+ if (currentStatus === "ready") return;
316
+ callbacks.onLog(name, `Process exited before ready (exit code: ${exitCode})`, true);
317
+ yield* Ref.set(statusRef, "error");
318
+ callbacks.onStatus(name, "error");
319
+ yield* Deferred.fail(readyDeferred, new Error(`Process exited before ready: ${name}`)).pipe(
320
+ Effect.ignore,
321
+ );
322
+ }),
524
323
  );
525
324
 
526
325
  const handleLine = (line: string, isStderr: boolean) =>
527
326
  Effect.gen(function* () {
528
327
  if (!line.trim()) return;
529
328
 
530
- callbacks.onLog(config.name, line, isStderr);
329
+ const cleanLine = stripAnsi(line);
330
+ const looksLikeError =
331
+ isStderr &&
332
+ /^(error|fail|fatal|exception|unhandled|reject)/i.test(cleanLine) &&
333
+ !/^\$/.test(cleanLine);
334
+ callbacks.onLog(name, line, looksLikeError);
531
335
 
532
336
  const currentStatus = yield* Ref.get(statusRef);
533
337
  if (currentStatus === "ready") return;
534
338
 
535
- const detected = detectStatus(line, config);
339
+ const detected = detectStatus(line, descriptor);
536
340
  if (detected) {
537
341
  yield* Ref.set(statusRef, detected.status);
538
- callbacks.onStatus(config.name, detected.status);
342
+ callbacks.onStatus(name, detected.status);
539
343
  if (detected.status === "ready" || detected.status === "error") {
540
344
  if (detected.status === "ready") {
541
345
  yield* Deferred.succeed(readyDeferred, undefined).pipe(Effect.ignore);
542
346
  } else {
543
- yield* Deferred.fail(readyDeferred, new Error(`Process failed: ${config.name}`)).pipe(
347
+ yield* Deferred.fail(readyDeferred, new Error(`Process failed: ${name}`)).pipe(
544
348
  Effect.ignore,
545
349
  );
546
350
  }
@@ -548,131 +352,148 @@ export const spawnDevProcess = (
548
352
  }
549
353
  });
550
354
 
551
- const decoder = new TextDecoder();
355
+ yield* Effect.forkScoped(
356
+ Stream.runForEach((line: string) => handleLine(line, false))(
357
+ Stream.splitLines(Stream.decodeText(proc.stdout, "utf-8")),
358
+ ),
359
+ );
360
+
361
+ yield* Effect.forkScoped(
362
+ Stream.runForEach((line: string) => handleLine(line, true))(
363
+ Stream.splitLines(Stream.decodeText(proc.stderr, "utf-8")),
364
+ ),
365
+ );
552
366
 
553
- const stdoutFiber = yield* Effect.forkScoped(
554
- Effect.async<void>((resume) => {
555
- if (!proc.stdout) {
556
- resume(Effect.void);
557
- return;
367
+ return {
368
+ name,
369
+ pid,
370
+ kill: Effect.gen(function* () {
371
+ const result = yield* proc.kill("SIGTERM").pipe(Effect.timeout("3 seconds"), Effect.option);
372
+ if (Option.isNone(result)) {
373
+ const pid = Number(proc.pid);
374
+ yield* Effect.try(() => process.kill(-pid, "SIGKILL")).pipe(Effect.ignore);
375
+ yield* Effect.sleep("250 millis");
558
376
  }
559
- const reader = proc.stdout.getReader();
560
- let buffer = "";
561
- let active = true;
562
-
563
- const pump = (): Promise<void> =>
564
- reader.read().then(({ done, value }) => {
565
- if (!active) return;
566
- if (done) {
567
- if (buffer) Effect.runSync(handleLine(buffer, false));
568
- return;
569
- }
570
- buffer += decoder
571
- .decode(value, { stream: true })
572
- .replace(/\r\n/g, "\n")
573
- .replace(/\r/g, "\n");
574
- const lines = buffer.split("\n");
575
- buffer = lines.pop() ?? "";
576
- for (const line of lines) {
577
- Effect.runSync(handleLine(line, false));
578
- }
579
- return pump();
580
- });
377
+ }).pipe(Effect.ignore),
378
+ waitForReady: Deferred.await(readyDeferred),
379
+ waitForExit: proc.exitCode,
380
+ } satisfies ProcessHandle;
381
+ });
581
382
 
582
- pump().then(() => {
583
- if (active) resume(Effect.void);
584
- });
383
+ const spawnRemoteProbe = (
384
+ pkg: string,
385
+ descriptor: ServiceDescriptor,
386
+ callbacks: ProcessCallbacks,
387
+ ) =>
388
+ Effect.gen(function* () {
389
+ callbacks.onStatus(pkg, "starting");
390
+ const readyDeferred = yield* Deferred.make<void, Error>();
391
+ const statusRef = yield* Ref.make<ProcessStatus>("starting");
585
392
 
586
- return Effect.sync(() => {
587
- active = false;
588
- reader.cancel();
589
- });
590
- }),
591
- );
393
+ const markReady = Effect.gen(function* () {
394
+ yield* Ref.set(statusRef, "ready");
395
+ yield* Deferred.succeed(readyDeferred, undefined);
396
+ callbacks.onStatus(pkg, "ready", "loaded");
397
+ });
592
398
 
593
- const stderrFiber = yield* Effect.forkScoped(
594
- Effect.async<void>((resume) => {
595
- if (!proc.stderr) {
596
- resume(Effect.void);
597
- return;
598
- }
599
- const reader = proc.stderr.getReader();
600
- let buffer = "";
601
- let active = true;
602
-
603
- const pump = (): Promise<void> =>
604
- reader.read().then(({ done, value }) => {
605
- if (!active) return;
606
- if (done) {
607
- if (buffer) Effect.runSync(handleLine(buffer, true));
608
- return;
609
- }
610
- buffer += decoder
611
- .decode(value, { stream: true })
612
- .replace(/\r\n/g, "\n")
613
- .replace(/\r/g, "\n");
614
- const lines = buffer.split("\n");
615
- buffer = lines.pop() ?? "";
616
- for (const line of lines) {
617
- Effect.runSync(handleLine(line, true));
618
- }
619
- return pump();
620
- });
399
+ const markError = Effect.gen(function* () {
400
+ yield* Ref.set(statusRef, "error");
401
+ yield* Deferred.fail(readyDeferred, new Error(`Remote ${pkg} unreachable`));
402
+ callbacks.onStatus(pkg, "error", "unreachable");
403
+ });
404
+
405
+ const baseUrl = descriptor.url.replace(/\/$/, "");
406
+ const manifestUrl = `${baseUrl}/mf-manifest.json`;
407
+ const entryUrl = `${baseUrl}${descriptor.readinessPath}`;
408
+ const probeUrl = descriptor.readinessPath === "/health" ? `${baseUrl}/health` : manifestUrl;
409
+
410
+ yield* Effect.forkScoped(
411
+ Effect.gen(function* () {
412
+ const deadline = Date.now() + 60_000;
413
+ while (Date.now() < deadline) {
414
+ const status = yield* Ref.get(statusRef);
415
+ if (status === "ready" || status === "error") return;
416
+
417
+ const ok = yield* probeHttpOk(probeUrl, 400);
621
418
 
622
- pump().then(() => {
623
- if (active) resume(Effect.void);
624
- });
419
+ if (ok) {
420
+ yield* markReady;
421
+ return;
422
+ }
625
423
 
626
- return Effect.sync(() => {
627
- active = false;
628
- reader.cancel();
629
- });
424
+ const fallbackOk = yield* probeHttpOk(entryUrl, 400);
425
+
426
+ if (fallbackOk) {
427
+ yield* markReady;
428
+ return;
429
+ }
430
+
431
+ yield* Effect.sleep("500 millis");
432
+ }
433
+
434
+ const status = yield* Ref.get(statusRef);
435
+ if (status !== "ready") {
436
+ yield* markError;
437
+ }
630
438
  }),
631
439
  );
632
440
 
633
- const handle: ProcessHandle = {
634
- name: config.name,
635
- pid: proc.pid,
636
- kill: proc.pid
637
- ? killProcessTree(proc.pid)
638
- : Effect.gen(function* () {
639
- proc.kill("SIGTERM");
640
- yield* Effect.sleep("100 millis");
641
- try {
642
- proc.kill("SIGKILL");
643
- } catch {}
644
- }),
645
- waitForReady: Deferred.await(readyDeferred),
646
- waitForExit: Effect.gen(function* () {
647
- yield* Fiber.joinAll([stdoutFiber, stderrFiber]);
648
- return yield* Effect.promise(() => proc.exited);
441
+ return {
442
+ name: pkg,
443
+ pid: undefined,
444
+ kill: Effect.gen(function* () {
445
+ yield* Ref.set(statusRef, "error");
446
+ yield* Deferred.fail(readyDeferred, new Error("Killed")).pipe(Effect.ignore);
649
447
  }),
650
- };
651
-
652
- return handle;
448
+ waitForReady: Deferred.await(readyDeferred),
449
+ waitForExit: Effect.never,
450
+ } satisfies ProcessHandle;
653
451
  });
654
452
 
655
- export const makeDevProcess = (
656
- pkg: string,
657
- env: Record<string, string> | undefined,
658
- callbacks: ProcessCallbacks,
659
- portOverride?: number,
660
- bosConfig?: BosConfig,
661
- runtimeConfig?: RuntimeConfig,
662
- registry?: ProcessRegistry,
663
- ) =>
453
+ export const makeDevProcess = (pkg: string, callbacks: ProcessCallbacks, portOverride?: number) =>
664
454
  Effect.gen(function* () {
665
- const config = getProcessConfig(pkg, env, portOverride, bosConfig, runtimeConfig);
666
- if (!config) {
667
- return yield* Effect.fail(new Error(`Unknown package: ${pkg}`));
455
+ const services = yield* ServiceDescriptorMap;
456
+ const descriptor = services.get(pkg);
457
+
458
+ if (!descriptor) {
459
+ callbacks.onStatus(pkg, "ready", "Remote");
460
+ return {
461
+ name: pkg,
462
+ pid: undefined,
463
+ kill: Effect.void,
464
+ waitForReady: Effect.void,
465
+ waitForExit: Effect.never,
466
+ } satisfies ProcessHandle;
668
467
  }
669
468
 
670
- if (pkg === "host" && runtimeConfig) {
671
- if (env?.HOST_SOURCE === "remote") {
672
- return yield* spawnRemoteHost(config, callbacks, runtimeConfig);
673
- }
674
- return yield* spawnDevProcess(config, callbacks, runtimeConfig, registry);
469
+ if (pkg === "host" && descriptor.source === "remote") {
470
+ return yield* spawnRemoteHost(descriptor, callbacks);
471
+ }
472
+
473
+ if (descriptor.source === "remote" || !descriptor.localPath) {
474
+ return yield* spawnRemoteProbe(pkg, descriptor, callbacks);
675
475
  }
676
476
 
677
- return yield* spawnDevProcess(config, callbacks, runtimeConfig, registry);
477
+ const resolvedDescriptor = portOverride ? { ...descriptor, port: portOverride } : descriptor;
478
+
479
+ return yield* spawnDevProcess(resolvedDescriptor, callbacks);
480
+ });
481
+
482
+ export function getProcessStates(
483
+ packages: string[],
484
+ services: Map<string, ServiceDescriptor>,
485
+ portOverride?: number,
486
+ ): ProcessState[] {
487
+ return packages.map((pkg) => {
488
+ const descriptor = services.get(pkg);
489
+ return {
490
+ name: pkg,
491
+ status: "pending" as const,
492
+ port:
493
+ portOverride && pkg === "host"
494
+ ? portOverride
495
+ : (descriptor?.port ?? descriptor?.defaultPort ?? 0),
496
+ source: descriptor?.source,
497
+ };
678
498
  });
499
+ }