everything-dev 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +40 -0
- package/src/cli.ts +735 -0
- package/src/components/dev-view.tsx +243 -0
- package/src/components/status-view.tsx +173 -0
- package/src/components/streaming-view.ts +110 -0
- package/src/config.ts +214 -0
- package/src/contract.ts +364 -0
- package/src/index.ts +3 -0
- package/src/lib/env.ts +91 -0
- package/src/lib/near-cli.ts +289 -0
- package/src/lib/nova.ts +254 -0
- package/src/lib/orchestrator.ts +213 -0
- package/src/lib/process.ts +370 -0
- package/src/lib/secrets.ts +28 -0
- package/src/plugin.ts +930 -0
- package/src/utils/banner.ts +19 -0
- package/src/utils/run.ts +21 -0
- package/src/utils/theme.ts +101 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { appendFile } from "node:fs/promises";
|
|
2
|
+
import { BunContext, BunRuntime } from "@effect/platform-bun";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import type { AppConfig } from "../config";
|
|
6
|
+
import {
|
|
7
|
+
type DevViewHandle,
|
|
8
|
+
type LogEntry,
|
|
9
|
+
type ProcessState,
|
|
10
|
+
renderDevView,
|
|
11
|
+
} from "../components/dev-view";
|
|
12
|
+
import { renderStreamingView, type StreamingViewHandle } from "../components/streaming-view";
|
|
13
|
+
import { getProcessConfig, makeDevProcess, type ProcessCallbacks, type ProcessHandle } from "./process";
|
|
14
|
+
|
|
15
|
+
export interface AppOrchestrator {
|
|
16
|
+
packages: string[];
|
|
17
|
+
env: Record<string, string>;
|
|
18
|
+
description: string;
|
|
19
|
+
appConfig: AppConfig;
|
|
20
|
+
port?: number;
|
|
21
|
+
interactive?: boolean;
|
|
22
|
+
noLogs?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isInteractiveSupported = (): boolean => {
|
|
26
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const STARTUP_ORDER = ["ui-ssr", "ui", "api", "host"];
|
|
30
|
+
|
|
31
|
+
const sortByOrder = (packages: string[]): string[] => {
|
|
32
|
+
return [...packages].sort((a, b) => {
|
|
33
|
+
const aIdx = STARTUP_ORDER.indexOf(a);
|
|
34
|
+
const bIdx = STARTUP_ORDER.indexOf(b);
|
|
35
|
+
if (aIdx === -1 && bIdx === -1) return 0;
|
|
36
|
+
if (aIdx === -1) return 1;
|
|
37
|
+
if (bIdx === -1) return -1;
|
|
38
|
+
return aIdx - bIdx;
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getLogDir = () => path.join(process.cwd(), ".bos", "logs");
|
|
43
|
+
const getLogFile = () => {
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const ts = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
46
|
+
return path.join(getLogDir(), `dev-${ts}.log`);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ensureLogDir = async () => {
|
|
50
|
+
const dir = getLogDir();
|
|
51
|
+
await Bun.spawn(["mkdir", "-p", dir]).exited;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatLogLine = (entry: LogEntry): string => {
|
|
55
|
+
const ts = new Date(entry.timestamp).toISOString();
|
|
56
|
+
const prefix = entry.isError ? "ERR" : "OUT";
|
|
57
|
+
return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const runDevServers = (orchestrator: AppOrchestrator) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const orderedPackages = sortByOrder(orchestrator.packages);
|
|
63
|
+
|
|
64
|
+
const initialProcesses: ProcessState[] = orderedPackages.map((pkg) => {
|
|
65
|
+
const portOverride = pkg === "host" ? orchestrator.port : undefined;
|
|
66
|
+
const config = getProcessConfig(pkg, undefined, portOverride);
|
|
67
|
+
const source = pkg === "host"
|
|
68
|
+
? orchestrator.appConfig.host
|
|
69
|
+
: pkg === "ui" || pkg === "ui-ssr"
|
|
70
|
+
? orchestrator.appConfig.ui
|
|
71
|
+
: pkg === "api"
|
|
72
|
+
? orchestrator.appConfig.api
|
|
73
|
+
: undefined;
|
|
74
|
+
return {
|
|
75
|
+
name: pkg,
|
|
76
|
+
status: "pending" as const,
|
|
77
|
+
port: config?.port ?? 0,
|
|
78
|
+
source,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const handles: ProcessHandle[] = [];
|
|
83
|
+
const allLogs: LogEntry[] = [];
|
|
84
|
+
let logFile: string | null = null;
|
|
85
|
+
let view: DevViewHandle | null = null;
|
|
86
|
+
let shuttingDown = false;
|
|
87
|
+
|
|
88
|
+
if (!orchestrator.noLogs) {
|
|
89
|
+
yield* Effect.promise(async () => {
|
|
90
|
+
await ensureLogDir();
|
|
91
|
+
logFile = getLogFile();
|
|
92
|
+
await Bun.write(logFile, `# BOS Dev Session: ${orchestrator.description}\n# Started: ${new Date().toISOString()}\n\n`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const killAll = async () => {
|
|
97
|
+
const reversed = [...handles].reverse();
|
|
98
|
+
for (const handle of reversed) {
|
|
99
|
+
try {
|
|
100
|
+
await handle.kill();
|
|
101
|
+
} catch { }
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const exportLogs = async () => {
|
|
106
|
+
console.log("\n\n--- SESSION LOGS ---\n");
|
|
107
|
+
for (const entry of allLogs) {
|
|
108
|
+
console.log(formatLogLine(entry));
|
|
109
|
+
}
|
|
110
|
+
console.log("\n--- END LOGS ---\n");
|
|
111
|
+
if (logFile) {
|
|
112
|
+
console.log(`Full logs saved to: ${logFile}\n`);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const cleanup = async (showLogs = false) => {
|
|
117
|
+
if (shuttingDown) return;
|
|
118
|
+
shuttingDown = true;
|
|
119
|
+
view?.unmount();
|
|
120
|
+
await killAll();
|
|
121
|
+
if (showLogs) {
|
|
122
|
+
await exportLogs();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const useInteractive = orchestrator.interactive ?? isInteractiveSupported();
|
|
127
|
+
|
|
128
|
+
view = useInteractive
|
|
129
|
+
? renderDevView(
|
|
130
|
+
initialProcesses,
|
|
131
|
+
orchestrator.description,
|
|
132
|
+
orchestrator.env,
|
|
133
|
+
() => cleanup(false),
|
|
134
|
+
() => cleanup(true)
|
|
135
|
+
)
|
|
136
|
+
: renderStreamingView(
|
|
137
|
+
initialProcesses,
|
|
138
|
+
orchestrator.description,
|
|
139
|
+
orchestrator.env,
|
|
140
|
+
() => cleanup(false),
|
|
141
|
+
() => cleanup(true)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const callbacks: ProcessCallbacks = {
|
|
145
|
+
onStatus: (name, status, message) => {
|
|
146
|
+
view?.updateProcess(name, status, message);
|
|
147
|
+
},
|
|
148
|
+
onLog: (name, line, isError) => {
|
|
149
|
+
const entry: LogEntry = {
|
|
150
|
+
source: name,
|
|
151
|
+
line,
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
isError,
|
|
154
|
+
};
|
|
155
|
+
allLogs.push(entry);
|
|
156
|
+
view?.addLog(name, line, isError);
|
|
157
|
+
|
|
158
|
+
if (logFile) {
|
|
159
|
+
const logLine = formatLogLine(entry) + "\n";
|
|
160
|
+
appendFile(logFile, logLine).catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const pkg of orderedPackages) {
|
|
166
|
+
const portOverride = pkg === "host" ? orchestrator.port : undefined;
|
|
167
|
+
const handle = yield* makeDevProcess(pkg, orchestrator.env, callbacks, portOverride);
|
|
168
|
+
handles.push(handle);
|
|
169
|
+
|
|
170
|
+
yield* Effect.race(
|
|
171
|
+
handle.waitForReady,
|
|
172
|
+
Effect.sleep("30 seconds").pipe(
|
|
173
|
+
Effect.andThen(Effect.sync(() => {
|
|
174
|
+
callbacks.onLog(pkg, "Timeout waiting for ready, continuing...", true);
|
|
175
|
+
}))
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
yield* Effect.addFinalizer(() =>
|
|
181
|
+
Effect.promise(() => cleanup(false))
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
yield* Effect.never;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const startApp = (orchestrator: AppOrchestrator) => {
|
|
188
|
+
const program = Effect.scoped(runDevServers(orchestrator)).pipe(
|
|
189
|
+
Effect.provide(BunContext.layer),
|
|
190
|
+
Effect.catchAll((e) => Effect.sync(() => {
|
|
191
|
+
if (e instanceof Error) {
|
|
192
|
+
console.error("App server error:", e.message);
|
|
193
|
+
if (e.stack) {
|
|
194
|
+
console.error(e.stack);
|
|
195
|
+
}
|
|
196
|
+
} else if (typeof e === 'object' && e !== null) {
|
|
197
|
+
console.error("App server error:", JSON.stringify(e, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
console.error("App server error:", e);
|
|
200
|
+
}
|
|
201
|
+
}))
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
process.on("SIGINT", () => {
|
|
205
|
+
setTimeout(() => process.exit(0), 500);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
process.on("SIGTERM", () => {
|
|
209
|
+
setTimeout(() => process.exit(0), 500);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
BunRuntime.runMain(program);
|
|
213
|
+
};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Command } from "@effect/platform";
|
|
3
|
+
import { Deferred, Effect, Fiber, Ref, Stream } from "effect";
|
|
4
|
+
import { getConfigDir, getPortsFromConfig, type SourceMode } from "../config";
|
|
5
|
+
import type { ProcessStatus } from "../components/dev-view";
|
|
6
|
+
import { loadSecretsFor } from "./secrets";
|
|
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
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ProcessConfigBase {
|
|
20
|
+
name: string;
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
cwd: string;
|
|
24
|
+
readyPatterns: RegExp[];
|
|
25
|
+
errorPatterns: RegExp[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const processConfigBases: Record<string, ProcessConfigBase> = {
|
|
29
|
+
host: {
|
|
30
|
+
name: "host",
|
|
31
|
+
command: "bun",
|
|
32
|
+
args: ["run", "tsx", "server.ts"],
|
|
33
|
+
cwd: "host",
|
|
34
|
+
readyPatterns: [/listening on/i, /server started/i, /ready/i, /running at/i],
|
|
35
|
+
errorPatterns: [/error:/i, /failed/i, /exception/i],
|
|
36
|
+
},
|
|
37
|
+
"ui-ssr": {
|
|
38
|
+
name: "ui-ssr",
|
|
39
|
+
command: "bun",
|
|
40
|
+
args: ["run", "rsbuild", "build", "--watch"],
|
|
41
|
+
cwd: "ui",
|
|
42
|
+
readyPatterns: [/built in/i, /compiled.*successfully/i],
|
|
43
|
+
errorPatterns: [/error/i, /failed/i],
|
|
44
|
+
},
|
|
45
|
+
ui: {
|
|
46
|
+
name: "ui",
|
|
47
|
+
command: "bun",
|
|
48
|
+
args: ["run", "rsbuild", "dev"],
|
|
49
|
+
cwd: "ui",
|
|
50
|
+
readyPatterns: [/ready in/i, /compiled.*successfully/i, /➜.*local:/i],
|
|
51
|
+
errorPatterns: [/error/i, /failed to compile/i],
|
|
52
|
+
},
|
|
53
|
+
api: {
|
|
54
|
+
name: "api",
|
|
55
|
+
command: "bun",
|
|
56
|
+
args: ["run", "rspack", "serve"],
|
|
57
|
+
cwd: "api",
|
|
58
|
+
readyPatterns: [/compiled.*successfully/i, /listening/i, /started/i],
|
|
59
|
+
errorPatterns: [/error/i, /failed/i],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const getProcessConfig = (
|
|
64
|
+
pkg: string,
|
|
65
|
+
env?: Record<string, string>,
|
|
66
|
+
portOverride?: number
|
|
67
|
+
): DevProcess | null => {
|
|
68
|
+
const base = processConfigBases[pkg];
|
|
69
|
+
if (!base) return null;
|
|
70
|
+
|
|
71
|
+
const ports = getPortsFromConfig();
|
|
72
|
+
|
|
73
|
+
let port: number;
|
|
74
|
+
if (pkg === "host") {
|
|
75
|
+
port = portOverride ?? ports.host;
|
|
76
|
+
} else if (pkg === "ui" || pkg === "ui-ssr") {
|
|
77
|
+
port = pkg === "ui-ssr" ? 0 : ports.ui;
|
|
78
|
+
} else if (pkg === "api") {
|
|
79
|
+
port = ports.api;
|
|
80
|
+
} else {
|
|
81
|
+
port = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const processEnv = pkg === "ui-ssr"
|
|
85
|
+
? { ...env, BUILD_TARGET: "server" }
|
|
86
|
+
: env;
|
|
87
|
+
|
|
88
|
+
return { ...base, port, env: processEnv };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export interface ProcessCallbacks {
|
|
92
|
+
onStatus: (name: string, status: ProcessStatus, message?: string) => void;
|
|
93
|
+
onLog: (name: string, line: string, isError?: boolean) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ProcessHandle {
|
|
97
|
+
name: string;
|
|
98
|
+
pid: number | undefined;
|
|
99
|
+
kill: () => Promise<void>;
|
|
100
|
+
waitForReady: Effect.Effect<void>;
|
|
101
|
+
waitForExit: Effect.Effect<unknown, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const detectStatus = (
|
|
105
|
+
line: string,
|
|
106
|
+
config: DevProcess
|
|
107
|
+
): { status: ProcessStatus; isError: boolean } | null => {
|
|
108
|
+
for (const pattern of config.errorPatterns) {
|
|
109
|
+
if (pattern.test(line)) {
|
|
110
|
+
return { status: "error", isError: true };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const pattern of config.readyPatterns) {
|
|
114
|
+
if (pattern.test(line)) {
|
|
115
|
+
return { status: "ready", isError: false };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const spawnDevProcess = (
|
|
122
|
+
config: DevProcess,
|
|
123
|
+
callbacks: ProcessCallbacks
|
|
124
|
+
) =>
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
const configDir = getConfigDir();
|
|
127
|
+
const fullCwd = `${configDir}/${config.cwd}`;
|
|
128
|
+
const readyDeferred = yield* Deferred.make<void>();
|
|
129
|
+
const statusRef = yield* Ref.make<ProcessStatus>("starting");
|
|
130
|
+
|
|
131
|
+
callbacks.onStatus(config.name, "starting");
|
|
132
|
+
|
|
133
|
+
const cmd = Command.make(config.command, ...config.args).pipe(
|
|
134
|
+
Command.workingDirectory(fullCwd),
|
|
135
|
+
Command.env({
|
|
136
|
+
...process.env,
|
|
137
|
+
...config.env,
|
|
138
|
+
BOS_CONFIG_PATH: "../bos.config.json",
|
|
139
|
+
FORCE_COLOR: "1",
|
|
140
|
+
...(config.port > 0 ? { PORT: String(config.port) } : {}),
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const proc = yield* Command.start(cmd);
|
|
145
|
+
|
|
146
|
+
const handleLine = (line: string, isStderr: boolean) =>
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
if (!line.trim()) return;
|
|
149
|
+
|
|
150
|
+
callbacks.onLog(config.name, line, isStderr);
|
|
151
|
+
|
|
152
|
+
const currentStatus = yield* Ref.get(statusRef);
|
|
153
|
+
if (currentStatus === "ready") return;
|
|
154
|
+
|
|
155
|
+
const detected = detectStatus(line, config);
|
|
156
|
+
if (detected) {
|
|
157
|
+
yield* Ref.set(statusRef, detected.status);
|
|
158
|
+
callbacks.onStatus(config.name, detected.status);
|
|
159
|
+
if (detected.status === "ready") {
|
|
160
|
+
yield* Deferred.succeed(readyDeferred, undefined);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const stdoutFiber = yield* Effect.fork(
|
|
166
|
+
proc.stdout.pipe(
|
|
167
|
+
Stream.decodeText(),
|
|
168
|
+
Stream.splitLines,
|
|
169
|
+
Stream.runForEach((line) => handleLine(line, false))
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const stderrFiber = yield* Effect.fork(
|
|
174
|
+
proc.stderr.pipe(
|
|
175
|
+
Stream.decodeText(),
|
|
176
|
+
Stream.splitLines,
|
|
177
|
+
Stream.runForEach((line) => handleLine(line, true))
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const handle: ProcessHandle = {
|
|
182
|
+
name: config.name,
|
|
183
|
+
pid: proc.pid,
|
|
184
|
+
kill: async () => {
|
|
185
|
+
proc.kill("SIGTERM");
|
|
186
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
187
|
+
try {
|
|
188
|
+
proc.kill("SIGKILL");
|
|
189
|
+
} catch { }
|
|
190
|
+
},
|
|
191
|
+
waitForReady: Deferred.await(readyDeferred),
|
|
192
|
+
waitForExit: Effect.gen(function* () {
|
|
193
|
+
yield* Fiber.joinAll([stdoutFiber, stderrFiber]);
|
|
194
|
+
return yield* proc.exitCode;
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return handle;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
interface ServerHandle {
|
|
202
|
+
ready: Promise<void>;
|
|
203
|
+
shutdown: () => Promise<void>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface BootstrapConfig {
|
|
207
|
+
configPath?: string;
|
|
208
|
+
secrets?: Record<string, string>;
|
|
209
|
+
host?: { url?: string };
|
|
210
|
+
ui?: { source?: SourceMode };
|
|
211
|
+
api?: { source?: SourceMode; proxy?: string };
|
|
212
|
+
database?: { url?: string };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const patchConsole = (
|
|
216
|
+
name: string,
|
|
217
|
+
callbacks: ProcessCallbacks
|
|
218
|
+
): (() => void) => {
|
|
219
|
+
const originalLog = console.log;
|
|
220
|
+
const originalError = console.error;
|
|
221
|
+
const originalWarn = console.warn;
|
|
222
|
+
const originalInfo = console.info;
|
|
223
|
+
|
|
224
|
+
const formatArgs = (args: unknown[]): string => {
|
|
225
|
+
return args
|
|
226
|
+
.map((arg) =>
|
|
227
|
+
typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)
|
|
228
|
+
)
|
|
229
|
+
.join(" ");
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
console.log = (...args: unknown[]) => {
|
|
233
|
+
callbacks.onLog(name, formatArgs(args), false);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
console.error = (...args: unknown[]) => {
|
|
237
|
+
callbacks.onLog(name, formatArgs(args), true);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
console.warn = (...args: unknown[]) => {
|
|
241
|
+
callbacks.onLog(name, formatArgs(args), false);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
console.info = (...args: unknown[]) => {
|
|
245
|
+
callbacks.onLog(name, formatArgs(args), false);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return () => {
|
|
249
|
+
console.log = originalLog;
|
|
250
|
+
console.error = originalError;
|
|
251
|
+
console.warn = originalWarn;
|
|
252
|
+
console.info = originalInfo;
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const spawnRemoteHost = (
|
|
257
|
+
config: DevProcess,
|
|
258
|
+
callbacks: ProcessCallbacks
|
|
259
|
+
) =>
|
|
260
|
+
Effect.gen(function* () {
|
|
261
|
+
const remoteUrl = config.env?.HOST_REMOTE_URL;
|
|
262
|
+
|
|
263
|
+
if (!remoteUrl) {
|
|
264
|
+
return yield* Effect.fail(new Error("HOST_REMOTE_URL not provided for remote host"));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
callbacks.onStatus(config.name, "starting");
|
|
268
|
+
|
|
269
|
+
const configDir = getConfigDir();
|
|
270
|
+
const configPath = resolve(configDir, "bos.config.json");
|
|
271
|
+
const localUrl = `http://localhost:${config.port}`;
|
|
272
|
+
|
|
273
|
+
const hostSecrets = loadSecretsFor("host");
|
|
274
|
+
const apiSecrets = loadSecretsFor("api");
|
|
275
|
+
const allSecrets = { ...hostSecrets, ...apiSecrets };
|
|
276
|
+
|
|
277
|
+
const uiSource = (config.env?.UI_SOURCE as SourceMode) ?? "local";
|
|
278
|
+
const apiSource = (config.env?.API_SOURCE as SourceMode) ?? "local";
|
|
279
|
+
const apiProxy = config.env?.API_PROXY;
|
|
280
|
+
|
|
281
|
+
const bootstrap: BootstrapConfig = {
|
|
282
|
+
configPath,
|
|
283
|
+
secrets: allSecrets,
|
|
284
|
+
host: { url: localUrl },
|
|
285
|
+
ui: { source: uiSource },
|
|
286
|
+
api: { source: apiSource, proxy: apiProxy },
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
callbacks.onLog(config.name, `Remote: ${remoteUrl}`);
|
|
290
|
+
|
|
291
|
+
const restoreConsole = patchConsole(config.name, callbacks);
|
|
292
|
+
|
|
293
|
+
callbacks.onLog(config.name, "Loading Module Federation runtime...");
|
|
294
|
+
|
|
295
|
+
const mfRuntime = yield* Effect.tryPromise({
|
|
296
|
+
try: () => import("@module-federation/enhanced/runtime"),
|
|
297
|
+
catch: (e) => new Error(`Failed to load MF runtime: ${e}`),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const mfCore = yield* Effect.tryPromise({
|
|
301
|
+
try: () => import("@module-federation/runtime-core"),
|
|
302
|
+
catch: (e) => new Error(`Failed to load MF core: ${e}`),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
let mf = mfRuntime.getInstance();
|
|
306
|
+
if (!mf) {
|
|
307
|
+
mf = mfRuntime.createInstance({ name: "cli-host", remotes: [] });
|
|
308
|
+
mfCore.setGlobalFederationInstance(mf);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const remoteEntryUrl = remoteUrl.endsWith("/remoteEntry.js")
|
|
312
|
+
? remoteUrl
|
|
313
|
+
: `${remoteUrl}/remoteEntry.js`;
|
|
314
|
+
|
|
315
|
+
mf.registerRemotes([{ name: "host", entry: remoteEntryUrl }]);
|
|
316
|
+
|
|
317
|
+
callbacks.onLog(config.name, `Loading host from ${remoteEntryUrl}...`);
|
|
318
|
+
|
|
319
|
+
const hostModule = yield* Effect.tryPromise({
|
|
320
|
+
try: () => mf.loadRemote<{ runServer: (bootstrap?: BootstrapConfig) => ServerHandle }>("host/Server"),
|
|
321
|
+
catch: (e) => new Error(`Failed to load host module: ${e}`),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (!hostModule?.runServer) {
|
|
325
|
+
return yield* Effect.fail(new Error("Host module does not export runServer function"));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
callbacks.onLog(config.name, "Starting server...");
|
|
329
|
+
const serverHandle = hostModule.runServer(bootstrap);
|
|
330
|
+
|
|
331
|
+
yield* Effect.tryPromise({
|
|
332
|
+
try: () => serverHandle.ready,
|
|
333
|
+
catch: (e) => new Error(`Server failed to start: ${e}`),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
callbacks.onStatus(config.name, "ready");
|
|
337
|
+
|
|
338
|
+
const handle: ProcessHandle = {
|
|
339
|
+
name: config.name,
|
|
340
|
+
pid: process.pid,
|
|
341
|
+
kill: async () => {
|
|
342
|
+
callbacks.onLog(config.name, "Shutting down remote host...");
|
|
343
|
+
restoreConsole();
|
|
344
|
+
await serverHandle.shutdown();
|
|
345
|
+
},
|
|
346
|
+
waitForReady: Effect.void,
|
|
347
|
+
waitForExit: Effect.never,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return handle;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
export const makeDevProcess = (
|
|
354
|
+
pkg: string,
|
|
355
|
+
env: Record<string, string> | undefined,
|
|
356
|
+
callbacks: ProcessCallbacks,
|
|
357
|
+
portOverride?: number
|
|
358
|
+
) =>
|
|
359
|
+
Effect.gen(function* () {
|
|
360
|
+
const config = getProcessConfig(pkg, env, portOverride);
|
|
361
|
+
if (!config) {
|
|
362
|
+
return yield* Effect.fail(new Error(`Unknown package: ${pkg}`));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (pkg === "host" && env?.HOST_SOURCE === "remote") {
|
|
366
|
+
return yield* spawnRemoteHost(config, callbacks);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return yield* spawnDevProcess(config, callbacks);
|
|
370
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { loadConfig } from "../config";
|
|
2
|
+
|
|
3
|
+
export function loadSecretsFor(component: string): Record<string, string> {
|
|
4
|
+
const config = loadConfig();
|
|
5
|
+
const componentConfig = config.app[component];
|
|
6
|
+
if (!componentConfig) return {};
|
|
7
|
+
|
|
8
|
+
const secretNames = ("secrets" in componentConfig ? componentConfig.secrets : undefined) ?? [];
|
|
9
|
+
if (secretNames.length === 0) return {};
|
|
10
|
+
|
|
11
|
+
const secrets: Record<string, string> = {};
|
|
12
|
+
for (const name of secretNames) {
|
|
13
|
+
const value = process.env[name];
|
|
14
|
+
if (value) secrets[name] = value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return secrets;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadAllSecrets(): {
|
|
21
|
+
host: Record<string, string>;
|
|
22
|
+
api: Record<string, string>;
|
|
23
|
+
} {
|
|
24
|
+
return {
|
|
25
|
+
host: loadSecretsFor("host"),
|
|
26
|
+
api: loadSecretsFor("api"),
|
|
27
|
+
};
|
|
28
|
+
}
|