everything-dev 1.48.0 → 1.50.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.
- package/dist/app.cjs +66 -86
- package/dist/app.cjs.map +1 -1
- package/dist/app.mjs +66 -87
- package/dist/app.mjs.map +1 -1
- package/dist/cli/db-doctor.cjs +6 -23
- package/dist/cli/db-doctor.cjs.map +1 -1
- package/dist/cli/db-doctor.mjs +7 -24
- package/dist/cli/db-doctor.mjs.map +1 -1
- package/dist/cli/db-repair.cjs +25 -20
- package/dist/cli/db-repair.cjs.map +1 -1
- package/dist/cli/db-repair.mjs +25 -20
- package/dist/cli/db-repair.mjs.map +1 -1
- package/dist/cli/db-studio.cjs +4 -2
- package/dist/cli/db-studio.cjs.map +1 -1
- package/dist/cli/db-studio.mjs +5 -3
- package/dist/cli/db-studio.mjs.map +1 -1
- package/dist/cli/framework-version.cjs +45 -9
- package/dist/cli/framework-version.cjs.map +1 -1
- package/dist/cli/framework-version.mjs +45 -10
- package/dist/cli/framework-version.mjs.map +1 -1
- package/dist/cli/infra.cjs +89 -3
- package/dist/cli/infra.cjs.map +1 -1
- package/dist/cli/infra.mjs +83 -4
- package/dist/cli/infra.mjs.map +1 -1
- package/dist/cli/init.cjs +39 -15
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +8 -3
- package/dist/cli/init.d.cts.map +1 -1
- package/dist/cli/init.d.mts +8 -3
- package/dist/cli/init.d.mts.map +1 -1
- package/dist/cli/init.mjs +39 -16
- package/dist/cli/init.mjs.map +1 -1
- package/dist/cli/status.cjs +6 -2
- package/dist/cli/status.cjs.map +1 -1
- package/dist/cli/status.mjs +7 -3
- package/dist/cli/status.mjs.map +1 -1
- package/dist/cli/sync.cjs +1 -3
- package/dist/cli/sync.cjs.map +1 -1
- package/dist/cli/sync.mjs +1 -3
- package/dist/cli/sync.mjs.map +1 -1
- package/dist/cli/upgrade.cjs +8 -4
- package/dist/cli/upgrade.cjs.map +1 -1
- package/dist/cli/upgrade.mjs +8 -4
- package/dist/cli/upgrade.mjs.map +1 -1
- package/dist/cli.cjs +73 -9
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +74 -10
- package/dist/cli.mjs.map +1 -1
- package/dist/config.cjs +7 -3
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +7 -3
- package/dist/config.mjs.map +1 -1
- package/dist/contract.cjs +76 -4
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +164 -19
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +164 -19
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.meta.cjs +17 -2
- package/dist/contract.meta.cjs.map +1 -1
- package/dist/contract.meta.d.cts +23 -2
- package/dist/contract.meta.d.mts +23 -2
- package/dist/contract.meta.mjs +17 -2
- package/dist/contract.meta.mjs.map +1 -1
- package/dist/contract.mjs +72 -5
- package/dist/contract.mjs.map +1 -1
- package/dist/db.cjs +33 -25
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +16 -9
- package/dist/db.d.cts.map +1 -1
- package/dist/db.d.mts +16 -9
- package/dist/db.d.mts.map +1 -1
- package/dist/db.mjs +33 -24
- package/dist/db.mjs.map +1 -1
- package/dist/dev-session.cjs +28 -0
- package/dist/dev-session.cjs.map +1 -1
- package/dist/dev-session.mjs +29 -1
- package/dist/dev-session.mjs.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/infra/planner.cjs +337 -0
- package/dist/infra/planner.cjs.map +1 -0
- package/dist/infra/planner.mjs +336 -0
- package/dist/infra/planner.mjs.map +1 -0
- package/dist/infra/preflight.cjs +131 -0
- package/dist/infra/preflight.cjs.map +1 -0
- package/dist/infra/preflight.mjs +130 -0
- package/dist/infra/preflight.mjs.map +1 -0
- package/dist/infra/types.cjs +9 -0
- package/dist/infra/types.cjs.map +1 -0
- package/dist/infra/types.mjs +8 -0
- package/dist/infra/types.mjs.map +1 -0
- package/dist/near-cli.cjs +37 -0
- package/dist/near-cli.cjs.map +1 -1
- package/dist/near-cli.mjs +36 -1
- package/dist/near-cli.mjs.map +1 -1
- package/dist/orchestrator.cjs +10 -2
- package/dist/orchestrator.cjs.map +1 -1
- package/dist/orchestrator.mjs +10 -2
- package/dist/orchestrator.mjs.map +1 -1
- package/dist/plugin.cjs +160 -43
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +64 -8
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +64 -8
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +164 -47
- package/dist/plugin.mjs.map +1 -1
- package/dist/process-registry.cjs +88 -0
- package/dist/process-registry.cjs.map +1 -0
- package/dist/process-registry.mjs +83 -0
- package/dist/process-registry.mjs.map +1 -0
- package/dist/publish.cjs +4 -0
- package/dist/publish.cjs.map +1 -1
- package/dist/publish.mjs +4 -0
- package/dist/publish.mjs.map +1 -1
- package/dist/service-descriptor.cjs +4 -0
- package/dist/service-descriptor.cjs.map +1 -1
- package/dist/service-descriptor.mjs +4 -1
- package/dist/service-descriptor.mjs.map +1 -1
- package/dist/types.d.cts +2 -2
- package/dist/types.d.mts +2 -2
- package/dist/ui/index.cjs +2 -1
- package/dist/ui/index.d.cts +2 -2
- package/dist/ui/index.d.mts +2 -2
- package/dist/ui/index.mjs +2 -2
- package/dist/ui/router.cjs +40 -0
- package/dist/ui/router.cjs.map +1 -1
- package/dist/ui/router.d.cts +6 -1
- package/dist/ui/router.d.cts.map +1 -1
- package/dist/ui/router.d.mts +6 -1
- package/dist/ui/router.d.mts.map +1 -1
- package/dist/ui/router.mjs +40 -1
- package/dist/ui/router.mjs.map +1 -1
- package/package.json +4 -1
- package/skills/cli-reference/SKILL.md +5 -4
- package/skills/dev-workflow/SKILL.md +40 -11
- package/skills/plugin-development/SKILL.md +9 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { PortAllocator } from "../app.mjs";
|
|
2
|
+
import { buildDatabaseConfigs, buildOriginMap, buildRedisConfigs, getSecretGroups, loadPortState, savePortState } from "../cli/infra.mjs";
|
|
3
|
+
import { buildDescription } from "../service-descriptor.mjs";
|
|
4
|
+
import { InfraError } from "./types.mjs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
//#region src/infra/planner.ts
|
|
10
|
+
const DEFAULT_HOST_PORT = 3e3;
|
|
11
|
+
const DEFAULT_API_PORT = 3001;
|
|
12
|
+
const DEFAULT_AUTH_PORT = 3002;
|
|
13
|
+
const DEFAULT_UI_PORT = 3003;
|
|
14
|
+
const DEFAULT_PLUGIN_PORT_START = 3010;
|
|
15
|
+
const POSTGRES_USER = "everythingdev";
|
|
16
|
+
const POSTGRES_PASSWORD = "everythingdev";
|
|
17
|
+
function workspaceKey(configDir) {
|
|
18
|
+
return createHash("sha256").update(resolve(configDir)).digest("hex").slice(0, 12);
|
|
19
|
+
}
|
|
20
|
+
function normalizeCliPorts(input) {
|
|
21
|
+
return {
|
|
22
|
+
host: input.port,
|
|
23
|
+
api: input.apiPort,
|
|
24
|
+
auth: input.authPort,
|
|
25
|
+
ui: input.uiPort,
|
|
26
|
+
uiSsr: void 0,
|
|
27
|
+
pluginsStart: input.pluginPortStart,
|
|
28
|
+
plugins: input.plugins
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function allocateServices(cliPorts, plugins, configDir) {
|
|
32
|
+
return Effect.gen(function* () {
|
|
33
|
+
const wKey = workspaceKey(configDir);
|
|
34
|
+
const persisted = loadPortState(configDir).devPorts;
|
|
35
|
+
const allocator = yield* PortAllocator;
|
|
36
|
+
const hostPort = yield* allocator.pickAvailable(cliPorts.host ?? persisted?.host ?? DEFAULT_HOST_PORT);
|
|
37
|
+
const apiPort = yield* allocator.pickAvailable(cliPorts.api ?? persisted?.api ?? DEFAULT_API_PORT);
|
|
38
|
+
const authPort = yield* allocator.pickAvailable(cliPorts.auth ?? persisted?.auth ?? DEFAULT_AUTH_PORT);
|
|
39
|
+
const uiPort = yield* allocator.pickAvailable(cliPorts.ui ?? persisted?.ui ?? DEFAULT_UI_PORT);
|
|
40
|
+
const uiSsrPort = yield* allocator.pickAvailable(cliPorts.uiSsr ?? uiPort + 1);
|
|
41
|
+
const pluginApiPorts = {};
|
|
42
|
+
const pluginUiPorts = {};
|
|
43
|
+
const pluginKeys = Object.keys(plugins).sort();
|
|
44
|
+
const pluginStart = cliPorts.pluginsStart ?? persisted?.pluginPortStart ?? DEFAULT_PLUGIN_PORT_START;
|
|
45
|
+
let nextPluginPort = pluginStart;
|
|
46
|
+
for (const pluginId of pluginKeys) {
|
|
47
|
+
const pluginCfg = plugins[pluginId];
|
|
48
|
+
const preferred = cliPorts.plugins?.[pluginId]?.api ?? nextPluginPort;
|
|
49
|
+
const pluginPort = yield* allocator.pickAvailable(preferred);
|
|
50
|
+
pluginApiPorts[pluginId] = pluginPort;
|
|
51
|
+
nextPluginPort = pluginPort + 1;
|
|
52
|
+
if (pluginCfg?.source === "local" && pluginCfg?.localPath && pluginCfg?.ui?.source === "local") {
|
|
53
|
+
const uiPreferred = cliPorts.plugins?.[pluginId]?.ui ?? nextPluginPort;
|
|
54
|
+
const pluginUiPort = yield* allocator.pickAvailable(uiPreferred);
|
|
55
|
+
pluginUiPorts[pluginId] = pluginUiPort;
|
|
56
|
+
nextPluginPort = pluginUiPort + 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const resolved = {
|
|
60
|
+
host: hostPort,
|
|
61
|
+
api: apiPort,
|
|
62
|
+
auth: authPort,
|
|
63
|
+
ui: uiPort,
|
|
64
|
+
uiSsr: uiSsrPort,
|
|
65
|
+
plugins: Object.fromEntries(Object.entries(pluginApiPorts).map(([k, v]) => [k, {
|
|
66
|
+
api: v,
|
|
67
|
+
ui: pluginUiPorts[k]
|
|
68
|
+
}])),
|
|
69
|
+
postgres: {},
|
|
70
|
+
redis: {}
|
|
71
|
+
};
|
|
72
|
+
const devPortsState = {
|
|
73
|
+
host: hostPort,
|
|
74
|
+
api: apiPort,
|
|
75
|
+
auth: authPort,
|
|
76
|
+
ui: uiPort,
|
|
77
|
+
pluginPortStart: pluginStart
|
|
78
|
+
};
|
|
79
|
+
const claimPorts = {
|
|
80
|
+
host: hostPort,
|
|
81
|
+
api: apiPort,
|
|
82
|
+
auth: authPort,
|
|
83
|
+
ui: uiPort,
|
|
84
|
+
uiSsr: uiSsrPort
|
|
85
|
+
};
|
|
86
|
+
for (const [id, port] of Object.entries(pluginApiPorts)) claimPorts[`plugin:${id}`] = port;
|
|
87
|
+
for (const [id, port] of Object.entries(pluginUiPorts)) claimPorts[`plugin-ui:${id}`] = port;
|
|
88
|
+
return {
|
|
89
|
+
ports: resolved,
|
|
90
|
+
claims: [{
|
|
91
|
+
resourceKey: `workspace:${wKey}`,
|
|
92
|
+
pid: process.pid,
|
|
93
|
+
configDir,
|
|
94
|
+
ports: claimPorts,
|
|
95
|
+
startedAt: Date.now()
|
|
96
|
+
}],
|
|
97
|
+
devPortsState
|
|
98
|
+
};
|
|
99
|
+
}).pipe(Effect.mapError((portErr) => new InfraError({
|
|
100
|
+
phase: "allocate-services",
|
|
101
|
+
message: `Port allocation failed: ${String(portErr)}`,
|
|
102
|
+
cause: portErr
|
|
103
|
+
})));
|
|
104
|
+
}
|
|
105
|
+
function allocateDatabases(runtimeConfig, configDir) {
|
|
106
|
+
return Effect.gen(function* () {
|
|
107
|
+
const persisted = loadPortState(configDir);
|
|
108
|
+
const allSecrets = getSecretGroups(runtimeConfig).flatMap((group) => group.secrets);
|
|
109
|
+
const originMap = buildOriginMap(configDir, runtimeConfig);
|
|
110
|
+
const allocator = yield* PortAllocator;
|
|
111
|
+
const infraDatabases = yield* Effect.sync(() => buildDatabaseConfigs(allSecrets, originMap, { ...persisted.postgresPorts }));
|
|
112
|
+
const infraRedis = yield* Effect.sync(() => buildRedisConfigs(allSecrets, originMap, { ...persisted.redisPorts }));
|
|
113
|
+
const postgres = {};
|
|
114
|
+
const dbs = [];
|
|
115
|
+
for (const db of infraDatabases) {
|
|
116
|
+
const port = yield* allocator.pickAvailable(db.port);
|
|
117
|
+
postgres[db.slug] = port;
|
|
118
|
+
dbs.push({
|
|
119
|
+
secret: db.secret,
|
|
120
|
+
slug: db.slug,
|
|
121
|
+
port,
|
|
122
|
+
dbName: db.databaseName,
|
|
123
|
+
containerName: db.containerName,
|
|
124
|
+
volumeName: db.volumeName,
|
|
125
|
+
url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${db.slug}_db`
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const redis = {};
|
|
129
|
+
const redisPlans = [];
|
|
130
|
+
for (const r of infraRedis) {
|
|
131
|
+
const port = yield* allocator.pickAvailable(r.port);
|
|
132
|
+
redis[r.slug] = port;
|
|
133
|
+
redisPlans.push({
|
|
134
|
+
secret: r.secret,
|
|
135
|
+
slug: r.slug,
|
|
136
|
+
port,
|
|
137
|
+
containerName: r.containerName,
|
|
138
|
+
volumeName: r.volumeName,
|
|
139
|
+
url: `redis://localhost:${port}`
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
postgres,
|
|
144
|
+
redis,
|
|
145
|
+
dbs,
|
|
146
|
+
redisPlans
|
|
147
|
+
};
|
|
148
|
+
}).pipe(Effect.mapError((portErr) => new InfraError({
|
|
149
|
+
phase: "allocate-databases",
|
|
150
|
+
message: `Database port allocation failed: ${String(portErr)}`,
|
|
151
|
+
cause: portErr
|
|
152
|
+
})));
|
|
153
|
+
}
|
|
154
|
+
function buildServiceDescriptors(runtimeConfig, resolvedPorts) {
|
|
155
|
+
const descriptors = [];
|
|
156
|
+
if (runtimeConfig.host) {
|
|
157
|
+
const isLocal = runtimeConfig.host.source === "local";
|
|
158
|
+
descriptors.push({
|
|
159
|
+
key: "host",
|
|
160
|
+
source: runtimeConfig.host.source,
|
|
161
|
+
url: isLocal ? `http://localhost:${resolvedPorts.host}` : runtimeConfig.host.url,
|
|
162
|
+
port: isLocal ? resolvedPorts.host : void 0,
|
|
163
|
+
localPath: isLocal ? runtimeConfig.host.localPath : void 0
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (runtimeConfig.api) {
|
|
167
|
+
const isLocal = runtimeConfig.api.source === "local";
|
|
168
|
+
descriptors.push({
|
|
169
|
+
key: "api",
|
|
170
|
+
source: runtimeConfig.api.source,
|
|
171
|
+
url: isLocal ? `http://localhost:${resolvedPorts.api}` : runtimeConfig.api.url,
|
|
172
|
+
port: isLocal ? resolvedPorts.api : void 0,
|
|
173
|
+
localPath: isLocal ? runtimeConfig.api.localPath : void 0
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (runtimeConfig.auth) {
|
|
177
|
+
const isLocal = runtimeConfig.auth.source === "local";
|
|
178
|
+
descriptors.push({
|
|
179
|
+
key: "auth",
|
|
180
|
+
source: runtimeConfig.auth.source,
|
|
181
|
+
url: isLocal ? `http://localhost:${resolvedPorts.auth}` : runtimeConfig.auth.url,
|
|
182
|
+
port: isLocal ? resolvedPorts.auth : void 0,
|
|
183
|
+
localPath: isLocal ? runtimeConfig.auth.localPath : void 0
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (runtimeConfig.ui) {
|
|
187
|
+
const isLocal = runtimeConfig.ui.source === "local";
|
|
188
|
+
descriptors.push({
|
|
189
|
+
key: "ui",
|
|
190
|
+
source: runtimeConfig.ui.source,
|
|
191
|
+
url: isLocal ? `http://localhost:${resolvedPorts.ui}` : runtimeConfig.ui.url,
|
|
192
|
+
port: isLocal ? resolvedPorts.ui : void 0,
|
|
193
|
+
localPath: isLocal ? runtimeConfig.ui.localPath : void 0
|
|
194
|
+
});
|
|
195
|
+
if (isLocal && resolvedPorts.uiSsr) descriptors.push({
|
|
196
|
+
key: "ui-ssr",
|
|
197
|
+
source: "local",
|
|
198
|
+
url: `http://localhost:${resolvedPorts.uiSsr}`,
|
|
199
|
+
port: resolvedPorts.uiSsr,
|
|
200
|
+
localPath: runtimeConfig.ui.localPath
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (runtimeConfig.plugins) for (const [pluginId, pluginCfg] of Object.entries(runtimeConfig.plugins)) {
|
|
204
|
+
const pluginIsLocal = pluginCfg.source === "local";
|
|
205
|
+
const p = resolvedPorts.plugins[pluginId];
|
|
206
|
+
if (pluginIsLocal && p?.api) descriptors.push({
|
|
207
|
+
key: `plugin:${pluginId}`,
|
|
208
|
+
source: "local",
|
|
209
|
+
url: `http://localhost:${p.api}`,
|
|
210
|
+
port: p.api,
|
|
211
|
+
localPath: pluginCfg.localPath
|
|
212
|
+
});
|
|
213
|
+
if (pluginIsLocal && p?.ui && pluginCfg.ui?.source === "local") descriptors.push({
|
|
214
|
+
key: `plugin-ui:${pluginId}`,
|
|
215
|
+
source: "local",
|
|
216
|
+
url: `http://localhost:${p.ui}`,
|
|
217
|
+
port: p.ui,
|
|
218
|
+
localPath: pluginCfg.ui?.localPath
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return descriptors;
|
|
222
|
+
}
|
|
223
|
+
function buildLaunchSpec(runtimeConfig, resolvedPorts) {
|
|
224
|
+
const hostPort = runtimeConfig.host?.source === "local" ? resolvedPorts.host : runtimeConfig.host?.port;
|
|
225
|
+
const corsOrigin = hostPort ? `http://localhost:${hostPort}` : `http://localhost:3000`;
|
|
226
|
+
return {
|
|
227
|
+
port: resolvedPorts.host,
|
|
228
|
+
hostUrl: runtimeConfig.host?.url,
|
|
229
|
+
corsOrigin,
|
|
230
|
+
env: {
|
|
231
|
+
...resolvedPorts.host ? { PORT: String(resolvedPorts.host) } : {},
|
|
232
|
+
...resolvedPorts.api ? { API_PORT: String(resolvedPorts.api) } : {},
|
|
233
|
+
...resolvedPorts.ui ? { UI_PORT: String(resolvedPorts.ui) } : {},
|
|
234
|
+
...resolvedPorts.auth ? { AUTH_PORT: String(resolvedPorts.auth) } : {}
|
|
235
|
+
},
|
|
236
|
+
runtimeConfig
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function buildComposeModel(dbs, redisPlans) {
|
|
240
|
+
return {
|
|
241
|
+
databases: dbs,
|
|
242
|
+
redis: redisPlans
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function buildEnvGenerated(resolvedPorts, dbs, redisPlans) {
|
|
246
|
+
const env = {};
|
|
247
|
+
if (resolvedPorts.host) env.CORS_ORIGIN = `http://localhost:${resolvedPorts.host}`;
|
|
248
|
+
for (const db of dbs) env[db.secret] = db.url;
|
|
249
|
+
for (const r of redisPlans) env[r.secret] = r.url;
|
|
250
|
+
return env;
|
|
251
|
+
}
|
|
252
|
+
function planInfra(input) {
|
|
253
|
+
return Effect.gen(function* () {
|
|
254
|
+
const cliPorts = normalizeCliPorts(input.cli);
|
|
255
|
+
const wKey = workspaceKey(input.configDir);
|
|
256
|
+
const { ports: svcPorts, claims, devPortsState } = yield* allocateServices(cliPorts, input.bosConfig.plugins ?? {}, input.configDir);
|
|
257
|
+
const { dbs, redisPlans, postgres: pgPorts, redis: rdPorts } = yield* allocateDatabases(input.bosConfig, input.configDir);
|
|
258
|
+
const resolvedPorts = {
|
|
259
|
+
...svcPorts,
|
|
260
|
+
postgres: pgPorts,
|
|
261
|
+
redis: rdPorts
|
|
262
|
+
};
|
|
263
|
+
savePortState(input.configDir, {
|
|
264
|
+
postgresPorts: pgPorts,
|
|
265
|
+
redisPorts: rdPorts,
|
|
266
|
+
devPorts: devPortsState
|
|
267
|
+
});
|
|
268
|
+
const hostIsLocal = input.bosConfig.host?.source === "local";
|
|
269
|
+
const apiIsLocal = input.bosConfig.api?.source === "local";
|
|
270
|
+
const uiIsLocal = input.bosConfig.ui?.source === "local";
|
|
271
|
+
const authIsLocal = input.bosConfig.auth?.source === "local";
|
|
272
|
+
const assignedRuntimeConfig = {
|
|
273
|
+
...input.bosConfig,
|
|
274
|
+
host: resolvedPorts.host ? {
|
|
275
|
+
...input.bosConfig.host,
|
|
276
|
+
port: resolvedPorts.host,
|
|
277
|
+
url: `http://localhost:${resolvedPorts.host}`,
|
|
278
|
+
remoteUrl: !hostIsLocal ? input.bosConfig.host.remoteUrl ?? input.bosConfig.host.url : void 0
|
|
279
|
+
} : input.bosConfig.host,
|
|
280
|
+
api: apiIsLocal && resolvedPorts.api ? {
|
|
281
|
+
...input.bosConfig.api,
|
|
282
|
+
port: resolvedPorts.api,
|
|
283
|
+
url: `http://localhost:${resolvedPorts.api}`
|
|
284
|
+
} : input.bosConfig.api,
|
|
285
|
+
ui: uiIsLocal && resolvedPorts.ui ? {
|
|
286
|
+
...input.bosConfig.ui,
|
|
287
|
+
port: resolvedPorts.ui,
|
|
288
|
+
url: `http://localhost:${resolvedPorts.ui}`
|
|
289
|
+
} : input.bosConfig.ui,
|
|
290
|
+
auth: authIsLocal && resolvedPorts.auth && input.bosConfig.auth ? {
|
|
291
|
+
...input.bosConfig.auth,
|
|
292
|
+
port: resolvedPorts.auth,
|
|
293
|
+
url: `http://localhost:${resolvedPorts.auth}`
|
|
294
|
+
} : input.bosConfig.auth,
|
|
295
|
+
plugins: input.bosConfig.plugins ? Object.fromEntries(Object.entries(input.bosConfig.plugins).map(([id, p]) => {
|
|
296
|
+
const pluginPort = resolvedPorts.plugins[id];
|
|
297
|
+
if (p.source === "local" && pluginPort?.api) return [id, {
|
|
298
|
+
...p,
|
|
299
|
+
port: pluginPort.api,
|
|
300
|
+
url: `http://localhost:${pluginPort.api}`
|
|
301
|
+
}];
|
|
302
|
+
return [id, p];
|
|
303
|
+
})) : void 0
|
|
304
|
+
};
|
|
305
|
+
const serviceDescriptors = buildServiceDescriptors(input.bosConfig, resolvedPorts);
|
|
306
|
+
const launch = buildLaunchSpec(input.bosConfig, resolvedPorts);
|
|
307
|
+
const composeModel = buildComposeModel(dbs, redisPlans);
|
|
308
|
+
const envGenerated = buildEnvGenerated(resolvedPorts, dbs, redisPlans);
|
|
309
|
+
const packages = serviceDescriptors.map((d) => d.key);
|
|
310
|
+
const description = buildDescription(new Map(serviceDescriptors.map((d) => [d.key, d])));
|
|
311
|
+
const orchestrator = {
|
|
312
|
+
packages,
|
|
313
|
+
env: {},
|
|
314
|
+
description,
|
|
315
|
+
port: resolvedPorts.host,
|
|
316
|
+
interactive: input.cli.interactive
|
|
317
|
+
};
|
|
318
|
+
return {
|
|
319
|
+
workspaceKey: wKey,
|
|
320
|
+
cliPorts,
|
|
321
|
+
resolvedPorts,
|
|
322
|
+
runtimeConfig: assignedRuntimeConfig,
|
|
323
|
+
launch,
|
|
324
|
+
description,
|
|
325
|
+
serviceDescriptors: new Map(serviceDescriptors.map((d) => [d.key, d])),
|
|
326
|
+
envGenerated,
|
|
327
|
+
composeModel,
|
|
328
|
+
claims,
|
|
329
|
+
orchestrator
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
export { planInfra };
|
|
336
|
+
//# sourceMappingURL=planner.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.mjs","names":[],"sources":["../../src/infra/planner.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { resolve } from \"node:path\";\nimport { Effect } from \"effect\";\nimport { PortAllocator } from \"../app\";\nimport {\n buildDatabaseConfigs,\n buildOriginMap,\n buildRedisConfigs,\n type DatabaseSecretConfig,\n getSecretGroups,\n loadPortState,\n type RedisSecretConfig,\n savePortState,\n} from \"../cli/infra\";\nimport { buildDescription } from \"../service-descriptor\";\nimport type { RuntimeConfig } from \"../types\";\nimport type {\n ClaimRecord,\n CliPorts,\n ComposeModelPlan,\n DatabasePlan,\n InfraInput,\n InfraPlan,\n RedisPlan,\n ResolvedPorts,\n RuntimeLaunchSpec,\n ServiceDescriptorPlan,\n} from \"./types\";\nimport { InfraError } from \"./types\";\n\nconst DEFAULT_HOST_PORT = 3000;\nconst DEFAULT_API_PORT = 3001;\nconst DEFAULT_AUTH_PORT = 3002;\nconst DEFAULT_UI_PORT = 3003;\nconst DEFAULT_PLUGIN_PORT_START = 3010;\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\n\nexport function workspaceKey(configDir: string): string {\n const hash = createHash(\"sha256\").update(resolve(configDir)).digest(\"hex\");\n return hash.slice(0, 12);\n}\n\nfunction normalizeCliPorts(input: InfraInput[\"cli\"]): CliPorts {\n return {\n host: input.port,\n api: input.apiPort,\n auth: input.authPort,\n ui: input.uiPort,\n uiSsr: undefined,\n pluginsStart: input.pluginPortStart,\n plugins: input.plugins,\n };\n}\n\ninterface AllocateServicesResult {\n ports: ResolvedPorts;\n claims: ClaimRecord[];\n devPortsState: { host: number; api: number; auth: number; ui: number; pluginPortStart: number };\n}\n\nfunction allocateServices(\n cliPorts: CliPorts,\n plugins: Record<\n string,\n { source: string; localPath?: string; ui?: { source: string; localPath?: string } }\n >,\n configDir: string,\n): Effect.Effect<AllocateServicesResult, InfraError, PortAllocator> {\n return Effect.gen(function* () {\n const wKey = workspaceKey(configDir);\n const persisted = loadPortState(configDir).devPorts;\n const allocator = yield* PortAllocator;\n\n const hostPort = yield* allocator.pickAvailable(\n cliPorts.host ?? persisted?.host ?? DEFAULT_HOST_PORT,\n );\n\n const apiPort = yield* allocator.pickAvailable(\n cliPorts.api ?? persisted?.api ?? DEFAULT_API_PORT,\n );\n\n const authPort = yield* allocator.pickAvailable(\n cliPorts.auth ?? persisted?.auth ?? DEFAULT_AUTH_PORT,\n );\n\n const uiPort = yield* allocator.pickAvailable(cliPorts.ui ?? persisted?.ui ?? DEFAULT_UI_PORT);\n\n const uiSsrPort = yield* allocator.pickAvailable(cliPorts.uiSsr ?? uiPort + 1);\n\n const pluginApiPorts: Record<string, number> = {};\n const pluginUiPorts: Record<string, number> = {};\n\n const pluginKeys = Object.keys(plugins).sort();\n const pluginStart =\n cliPorts.pluginsStart ?? persisted?.pluginPortStart ?? DEFAULT_PLUGIN_PORT_START;\n let nextPluginPort = pluginStart;\n for (const pluginId of pluginKeys) {\n const pluginCfg = plugins[pluginId];\n const preferred = cliPorts.plugins?.[pluginId]?.api ?? nextPluginPort;\n const pluginPort = yield* allocator.pickAvailable(preferred);\n pluginApiPorts[pluginId] = pluginPort;\n nextPluginPort = pluginPort + 1;\n\n if (\n pluginCfg?.source === \"local\" &&\n pluginCfg?.localPath &&\n pluginCfg?.ui?.source === \"local\"\n ) {\n const uiPreferred = cliPorts.plugins?.[pluginId]?.ui ?? nextPluginPort;\n const pluginUiPort = yield* allocator.pickAvailable(uiPreferred);\n pluginUiPorts[pluginId] = pluginUiPort;\n nextPluginPort = pluginUiPort + 1;\n }\n }\n\n const resolved: ResolvedPorts = {\n host: hostPort,\n api: apiPort,\n auth: authPort,\n ui: uiPort,\n uiSsr: uiSsrPort,\n plugins: Object.fromEntries(\n Object.entries(pluginApiPorts).map(([k, v]) => [k, { api: v, ui: pluginUiPorts[k] }]),\n ),\n postgres: {},\n redis: {},\n };\n\n const devPortsState = {\n host: hostPort,\n api: apiPort,\n auth: authPort,\n ui: uiPort,\n pluginPortStart: pluginStart,\n };\n\n const claimPorts: Record<string, number> = {\n host: hostPort,\n api: apiPort,\n auth: authPort,\n ui: uiPort,\n uiSsr: uiSsrPort,\n };\n for (const [id, port] of Object.entries(pluginApiPorts)) {\n claimPorts[`plugin:${id}`] = port;\n }\n for (const [id, port] of Object.entries(pluginUiPorts)) {\n claimPorts[`plugin-ui:${id}`] = port;\n }\n\n const claim: ClaimRecord = {\n resourceKey: `workspace:${wKey}`,\n pid: process.pid,\n configDir,\n ports: claimPorts,\n startedAt: Date.now(),\n };\n\n return { ports: resolved, claims: [claim], devPortsState };\n }).pipe(\n Effect.mapError(\n (portErr) =>\n new InfraError({\n phase: \"allocate-services\",\n message: `Port allocation failed: ${String(portErr)}`,\n cause: portErr,\n }),\n ),\n );\n}\n\nfunction allocateDatabases(\n runtimeConfig: RuntimeConfig,\n configDir: string,\n): Effect.Effect<\n {\n postgres: Record<string, number>;\n redis: Record<string, number>;\n dbs: DatabasePlan[];\n redisPlans: RedisPlan[];\n },\n InfraError,\n PortAllocator\n> {\n return Effect.gen(function* () {\n const persisted = loadPortState(configDir);\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const originMap = buildOriginMap(configDir, runtimeConfig);\n const allocator = yield* PortAllocator;\n\n const infraDatabases: DatabaseSecretConfig[] = yield* Effect.sync(() =>\n buildDatabaseConfigs(allSecrets, originMap, { ...persisted.postgresPorts }),\n );\n const infraRedis: RedisSecretConfig[] = yield* Effect.sync(() =>\n buildRedisConfigs(allSecrets, originMap, { ...persisted.redisPorts }),\n );\n\n const postgres: Record<string, number> = {};\n const dbs: DatabasePlan[] = [];\n for (const db of infraDatabases) {\n const port = yield* allocator.pickAvailable(db.port);\n postgres[db.slug] = port;\n dbs.push({\n secret: db.secret,\n slug: db.slug,\n port,\n dbName: db.databaseName,\n containerName: db.containerName,\n volumeName: db.volumeName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${db.slug}_db`,\n });\n }\n\n const redis: Record<string, number> = {};\n const redisPlans: RedisPlan[] = [];\n for (const r of infraRedis) {\n const port = yield* allocator.pickAvailable(r.port);\n redis[r.slug] = port;\n redisPlans.push({\n secret: r.secret,\n slug: r.slug,\n port,\n containerName: r.containerName,\n volumeName: r.volumeName,\n url: `redis://localhost:${port}`,\n });\n }\n\n return { postgres, redis, dbs, redisPlans };\n }).pipe(\n Effect.mapError(\n (portErr) =>\n new InfraError({\n phase: \"allocate-databases\",\n message: `Database port allocation failed: ${String(portErr)}`,\n cause: portErr,\n }),\n ),\n );\n}\n\nexport function buildServiceDescriptors(\n runtimeConfig: RuntimeConfig,\n resolvedPorts: ResolvedPorts,\n): ServiceDescriptorPlan[] {\n const descriptors: ServiceDescriptorPlan[] = [];\n\n if (runtimeConfig.host) {\n const isLocal = runtimeConfig.host.source === \"local\";\n descriptors.push({\n key: \"host\",\n source: runtimeConfig.host.source,\n url: isLocal ? `http://localhost:${resolvedPorts.host}` : runtimeConfig.host.url,\n port: isLocal ? resolvedPorts.host : undefined,\n localPath: isLocal ? runtimeConfig.host.localPath : undefined,\n });\n }\n\n if (runtimeConfig.api) {\n const isLocal = runtimeConfig.api.source === \"local\";\n descriptors.push({\n key: \"api\",\n source: runtimeConfig.api.source,\n url: isLocal ? `http://localhost:${resolvedPorts.api}` : runtimeConfig.api.url,\n port: isLocal ? resolvedPorts.api : undefined,\n localPath: isLocal ? runtimeConfig.api.localPath : undefined,\n });\n }\n\n if (runtimeConfig.auth) {\n const isLocal = runtimeConfig.auth.source === \"local\";\n descriptors.push({\n key: \"auth\",\n source: runtimeConfig.auth.source,\n url: isLocal ? `http://localhost:${resolvedPorts.auth}` : runtimeConfig.auth.url,\n port: isLocal ? resolvedPorts.auth : undefined,\n localPath: isLocal ? runtimeConfig.auth.localPath : undefined,\n });\n }\n\n if (runtimeConfig.ui) {\n const isLocal = runtimeConfig.ui.source === \"local\";\n descriptors.push({\n key: \"ui\",\n source: runtimeConfig.ui.source,\n url: isLocal ? `http://localhost:${resolvedPorts.ui}` : runtimeConfig.ui.url,\n port: isLocal ? resolvedPorts.ui : undefined,\n localPath: isLocal ? runtimeConfig.ui.localPath : undefined,\n });\n if (isLocal && resolvedPorts.uiSsr) {\n descriptors.push({\n key: \"ui-ssr\",\n source: \"local\",\n url: `http://localhost:${resolvedPorts.uiSsr}`,\n port: resolvedPorts.uiSsr,\n localPath: runtimeConfig.ui.localPath,\n });\n }\n }\n\n if (runtimeConfig.plugins) {\n for (const [pluginId, pluginCfg] of Object.entries(runtimeConfig.plugins)) {\n const pluginIsLocal = pluginCfg.source === \"local\";\n const p = resolvedPorts.plugins[pluginId];\n if (pluginIsLocal && p?.api) {\n descriptors.push({\n key: `plugin:${pluginId}`,\n source: \"local\",\n url: `http://localhost:${p.api}`,\n port: p.api,\n localPath: pluginCfg.localPath,\n });\n }\n if (pluginIsLocal && p?.ui && pluginCfg.ui?.source === \"local\") {\n descriptors.push({\n key: `plugin-ui:${pluginId}`,\n source: \"local\",\n url: `http://localhost:${p.ui}`,\n port: p.ui,\n localPath: pluginCfg.ui?.localPath,\n });\n }\n }\n }\n\n return descriptors;\n}\n\nexport function buildLaunchSpec(\n runtimeConfig: RuntimeConfig,\n resolvedPorts: ResolvedPorts,\n): RuntimeLaunchSpec {\n const hostPort =\n runtimeConfig.host?.source === \"local\" ? resolvedPorts.host : runtimeConfig.host?.port;\n const corsOrigin = hostPort ? `http://localhost:${hostPort}` : `http://localhost:3000`;\n\n return {\n port: resolvedPorts.host,\n hostUrl: runtimeConfig.host?.url,\n corsOrigin,\n env: {\n ...(resolvedPorts.host ? { PORT: String(resolvedPorts.host) } : {}),\n ...(resolvedPorts.api ? { API_PORT: String(resolvedPorts.api) } : {}),\n ...(resolvedPorts.ui ? { UI_PORT: String(resolvedPorts.ui) } : {}),\n ...(resolvedPorts.auth ? { AUTH_PORT: String(resolvedPorts.auth) } : {}),\n },\n runtimeConfig,\n };\n}\n\nexport function buildComposeModel(dbs: DatabasePlan[], redisPlans: RedisPlan[]): ComposeModelPlan {\n return { databases: dbs, redis: redisPlans };\n}\n\nexport function buildEnvGenerated(\n resolvedPorts: ResolvedPorts,\n dbs: DatabasePlan[],\n redisPlans: RedisPlan[],\n): Record<string, string> {\n const env: Record<string, string> = {};\n\n if (resolvedPorts.host) {\n env.CORS_ORIGIN = `http://localhost:${resolvedPorts.host}`;\n }\n\n for (const db of dbs) {\n env[db.secret] = db.url;\n }\n for (const r of redisPlans) {\n env[r.secret] = r.url;\n }\n\n return env;\n}\n\nexport function planInfra(input: InfraInput): Effect.Effect<InfraPlan, InfraError, PortAllocator> {\n return Effect.gen(function* () {\n const cliPorts = normalizeCliPorts(input.cli);\n const wKey = workspaceKey(input.configDir);\n\n const plugins = (input.bosConfig.plugins ?? {}) as Record<\n string,\n { source: string; localPath?: string; ui?: { source: string; localPath?: string } }\n >;\n\n const {\n ports: svcPorts,\n claims,\n devPortsState,\n } = yield* allocateServices(cliPorts, plugins, input.configDir);\n\n const {\n dbs,\n redisPlans,\n postgres: pgPorts,\n redis: rdPorts,\n } = yield* allocateDatabases(input.bosConfig, input.configDir);\n\n const resolvedPorts: ResolvedPorts = {\n ...svcPorts,\n postgres: pgPorts,\n redis: rdPorts,\n };\n\n // Write merged state once after all allocations succeed\n savePortState(input.configDir, {\n postgresPorts: pgPorts,\n redisPorts: rdPorts,\n devPorts: devPortsState,\n });\n\n const hostIsLocal = input.bosConfig.host?.source === \"local\";\n const apiIsLocal = input.bosConfig.api?.source === \"local\";\n const uiIsLocal = input.bosConfig.ui?.source === \"local\";\n const authIsLocal = input.bosConfig.auth?.source === \"local\";\n const assignedRuntimeConfig: RuntimeConfig = {\n ...input.bosConfig,\n host: resolvedPorts.host\n ? {\n ...input.bosConfig.host,\n port: resolvedPorts.host,\n url: `http://localhost:${resolvedPorts.host}`,\n remoteUrl: !hostIsLocal\n ? (input.bosConfig.host.remoteUrl ?? input.bosConfig.host.url)\n : undefined,\n }\n : input.bosConfig.host,\n api:\n apiIsLocal && resolvedPorts.api\n ? {\n ...input.bosConfig.api,\n port: resolvedPorts.api,\n url: `http://localhost:${resolvedPorts.api}`,\n }\n : input.bosConfig.api,\n ui:\n uiIsLocal && resolvedPorts.ui\n ? {\n ...input.bosConfig.ui,\n port: resolvedPorts.ui,\n url: `http://localhost:${resolvedPorts.ui}`,\n }\n : input.bosConfig.ui,\n auth:\n authIsLocal && resolvedPorts.auth && input.bosConfig.auth\n ? {\n ...input.bosConfig.auth,\n port: resolvedPorts.auth,\n url: `http://localhost:${resolvedPorts.auth}`,\n }\n : input.bosConfig.auth,\n plugins: input.bosConfig.plugins\n ? Object.fromEntries(\n Object.entries(input.bosConfig.plugins).map(([id, p]) => {\n const pluginPort = resolvedPorts.plugins[id];\n if (p.source === \"local\" && pluginPort?.api) {\n return [\n id,\n { ...p, port: pluginPort.api, url: `http://localhost:${pluginPort.api}` },\n ];\n }\n return [id, p];\n }),\n )\n : undefined,\n };\n\n const serviceDescriptors = buildServiceDescriptors(input.bosConfig, resolvedPorts);\n\n const launch = buildLaunchSpec(input.bosConfig, resolvedPorts);\n const composeModel = buildComposeModel(dbs, redisPlans);\n const envGenerated = buildEnvGenerated(resolvedPorts, dbs, redisPlans);\n\n const packages = serviceDescriptors.map((d) => d.key);\n const descriptionMap = new Map(serviceDescriptors.map((d) => [d.key, d]));\n const description = buildDescription(descriptionMap);\n\n const orchestrator = {\n packages,\n env: {},\n description,\n port: resolvedPorts.host,\n interactive: input.cli.interactive,\n };\n\n return {\n workspaceKey: wKey,\n cliPorts,\n resolvedPorts,\n runtimeConfig: assignedRuntimeConfig,\n launch,\n description,\n serviceDescriptors: new Map(serviceDescriptors.map((d) => [d.key, d])),\n envGenerated,\n composeModel,\n claims,\n orchestrator,\n };\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
|
|
2
|
+
let effect = require("effect");
|
|
3
|
+
let node_net = require("node:net");
|
|
4
|
+
|
|
5
|
+
//#region src/infra/preflight.ts
|
|
6
|
+
function parseLocalUrl(url) {
|
|
7
|
+
try {
|
|
8
|
+
const match = url.match(/:\/\/([^:/]+):(\d+)/);
|
|
9
|
+
if (!match) return null;
|
|
10
|
+
const host = match[1];
|
|
11
|
+
const port = Number.parseInt(match[2], 10);
|
|
12
|
+
if (Number.isNaN(port)) return null;
|
|
13
|
+
if (host !== "localhost" && host !== "127.0.0.1") return null;
|
|
14
|
+
return {
|
|
15
|
+
host,
|
|
16
|
+
port
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function checkTcpReachable(host, port, timeoutMs = 2e3) {
|
|
23
|
+
return effect.Effect.async((resume) => {
|
|
24
|
+
const socket = (0, node_net.createConnection)({
|
|
25
|
+
host,
|
|
26
|
+
port
|
|
27
|
+
});
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
socket.destroy();
|
|
30
|
+
resume(effect.Effect.succeed(false));
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
socket.once("connect", () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
socket.destroy();
|
|
35
|
+
resume(effect.Effect.succeed(true));
|
|
36
|
+
});
|
|
37
|
+
socket.once("error", () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
socket.destroy();
|
|
40
|
+
resume(effect.Effect.succeed(false));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function checkPgConnection(url) {
|
|
45
|
+
return effect.Effect.gen(function* () {
|
|
46
|
+
if (!(yield* checkTcpReachable(new URL(url).hostname, Number(new URL(url).port) || 5432, 4e3))) return false;
|
|
47
|
+
try {
|
|
48
|
+
const { Pool } = yield* effect.Effect.promise(() => Promise.resolve().then(() => require("../node_modules/pg/esm/index.cjs")));
|
|
49
|
+
const pool = new Pool({
|
|
50
|
+
connectionString: url,
|
|
51
|
+
connectionTimeoutMillis: 3e3
|
|
52
|
+
});
|
|
53
|
+
const client = yield* effect.Effect.promise(() => pool.connect().then((c) => ({
|
|
54
|
+
client: c,
|
|
55
|
+
pool
|
|
56
|
+
})));
|
|
57
|
+
try {
|
|
58
|
+
yield* effect.Effect.promise(() => client.client.query("SELECT 1"));
|
|
59
|
+
return true;
|
|
60
|
+
} finally {
|
|
61
|
+
client.client.release();
|
|
62
|
+
yield* effect.Effect.promise(() => client.pool.end().catch(() => {}));
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function preflightTargetsFromEnv(env) {
|
|
70
|
+
const targets = [];
|
|
71
|
+
for (const [secret, value] of Object.entries(env)) {
|
|
72
|
+
if (!value) continue;
|
|
73
|
+
if (secret.endsWith("_DATABASE_URL")) {
|
|
74
|
+
const parsed = parseLocalUrl(value);
|
|
75
|
+
if (parsed) targets.push({
|
|
76
|
+
secret,
|
|
77
|
+
host: parsed.host,
|
|
78
|
+
port: parsed.port,
|
|
79
|
+
kind: "postgres",
|
|
80
|
+
url: value
|
|
81
|
+
});
|
|
82
|
+
} else if (secret.endsWith("_REDIS_URL")) {
|
|
83
|
+
const parsed = parseLocalUrl(value);
|
|
84
|
+
if (parsed) targets.push({
|
|
85
|
+
secret,
|
|
86
|
+
host: parsed.host,
|
|
87
|
+
port: parsed.port,
|
|
88
|
+
kind: "redis",
|
|
89
|
+
url: value
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return targets;
|
|
94
|
+
}
|
|
95
|
+
function preflightLocalInfra(env, overrides) {
|
|
96
|
+
const targets = preflightTargetsFromEnv(overrides ? {
|
|
97
|
+
...env,
|
|
98
|
+
...overrides
|
|
99
|
+
} : env);
|
|
100
|
+
if (targets.length === 0) return effect.Effect.succeed([]);
|
|
101
|
+
return effect.Effect.forEach(targets, (target) => effect.Effect.gen(function* () {
|
|
102
|
+
if (target.kind === "postgres") {
|
|
103
|
+
if (yield* checkPgConnection(target.url)) return null;
|
|
104
|
+
const tcpOk = yield* checkTcpReachable(target.host, target.port);
|
|
105
|
+
const pluginContext = target.secret.endsWith("_DATABASE_URL") ? ` The plugin for ${target.secret} runs inside the local host process, so this DB must be reachable. Run \`docker compose up -d --wait\` to start local Postgres/Redis.` : "";
|
|
106
|
+
if (tcpOk) return {
|
|
107
|
+
secret: target.secret,
|
|
108
|
+
host: target.host,
|
|
109
|
+
port: target.port,
|
|
110
|
+
error: `${target.secret} at ${target.host}:${target.port} is reachable but Postgres connection failed. Check credentials and database name.${pluginContext}`
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
secret: target.secret,
|
|
114
|
+
host: target.host,
|
|
115
|
+
port: target.port,
|
|
116
|
+
error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening.${pluginContext}`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (yield* checkTcpReachable(target.host, target.port)) return null;
|
|
120
|
+
return {
|
|
121
|
+
secret: target.secret,
|
|
122
|
+
host: target.host,
|
|
123
|
+
port: target.port,
|
|
124
|
+
error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening`
|
|
125
|
+
};
|
|
126
|
+
}), { concurrency: "unbounded" }).pipe(effect.Effect.map((results) => results.filter((r) => r !== null)));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
exports.preflightLocalInfra = preflightLocalInfra;
|
|
131
|
+
//# sourceMappingURL=preflight.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.cjs","names":["Effect"],"sources":["../../src/infra/preflight.ts"],"sourcesContent":["import { createConnection } from \"node:net\";\nimport { Effect } from \"effect\";\n\nexport interface PreflightTarget {\n secret: string;\n host: string;\n port: number;\n kind: \"postgres\" | \"redis\";\n url: string;\n}\n\nexport interface PreflightFailure {\n secret: string;\n host: string;\n port: number;\n error: string;\n}\n\nfunction parseLocalUrl(url: string): { host: string; port: number } | null {\n try {\n const match = url.match(/:\\/\\/([^:/]+):(\\d+)/);\n if (!match) return null;\n const host = match[1];\n const port = Number.parseInt(match[2], 10);\n if (Number.isNaN(port)) return null;\n if (host !== \"localhost\" && host !== \"127.0.0.1\") return null;\n return { host, port };\n } catch {\n return null;\n }\n}\n\nfunction checkTcpReachable(host: string, port: number, timeoutMs = 2000): Effect.Effect<boolean> {\n return Effect.async<boolean>((resume) => {\n const socket = createConnection({ host, port });\n const timer = setTimeout(() => {\n socket.destroy();\n resume(Effect.succeed(false));\n }, timeoutMs);\n\n socket.once(\"connect\", () => {\n clearTimeout(timer);\n socket.destroy();\n resume(Effect.succeed(true));\n });\n\n socket.once(\"error\", () => {\n clearTimeout(timer);\n socket.destroy();\n resume(Effect.succeed(false));\n });\n });\n}\n\nfunction checkPgConnection(url: string): Effect.Effect<boolean> {\n return Effect.gen(function* () {\n const reachable = yield* checkTcpReachable(\n new URL(url).hostname,\n Number(new URL(url).port) || 5432,\n 4000,\n );\n if (!reachable) return false;\n\n try {\n const { Pool } = yield* Effect.promise(() => import(\"pg\"));\n const pool = new Pool({ connectionString: url, connectionTimeoutMillis: 3000 });\n const client = yield* Effect.promise(() => pool.connect().then((c) => ({ client: c, pool })));\n try {\n yield* Effect.promise(() => client.client.query(\"SELECT 1\"));\n return true;\n } finally {\n client.client.release();\n yield* Effect.promise(() => client.pool.end().catch(() => {}));\n }\n } catch {\n return false;\n }\n });\n}\n\nfunction preflightTargetsFromEnv(env: Record<string, string>): PreflightTarget[] {\n const targets: PreflightTarget[] = [];\n for (const [secret, value] of Object.entries(env)) {\n if (!value) continue;\n if (secret.endsWith(\"_DATABASE_URL\")) {\n const parsed = parseLocalUrl(value);\n if (parsed) {\n targets.push({\n secret,\n host: parsed.host,\n port: parsed.port,\n kind: \"postgres\",\n url: value,\n });\n }\n } else if (secret.endsWith(\"_REDIS_URL\")) {\n const parsed = parseLocalUrl(value);\n if (parsed) {\n targets.push({ secret, host: parsed.host, port: parsed.port, kind: \"redis\", url: value });\n }\n }\n }\n return targets;\n}\n\nexport function preflightLocalInfra(\n env: Record<string, string>,\n overrides?: Record<string, string>,\n): Effect.Effect<PreflightFailure[], never> {\n const merged = overrides ? { ...env, ...overrides } : env;\n const targets = preflightTargetsFromEnv(merged);\n if (targets.length === 0) return Effect.succeed([]);\n\n return Effect.forEach(\n targets,\n (target) =>\n Effect.gen(function* () {\n if (target.kind === \"postgres\") {\n const ok = yield* checkPgConnection(target.url);\n if (ok) return null;\n const tcpOk = yield* checkTcpReachable(target.host, target.port);\n const pluginContext = target.secret.endsWith(\"_DATABASE_URL\")\n ? ` The plugin for ${target.secret} runs inside the local host process, so this DB must be reachable. Run \\`docker compose up -d --wait\\` to start local Postgres/Redis.`\n : \"\";\n if (tcpOk) {\n return {\n secret: target.secret,\n host: target.host,\n port: target.port,\n error: `${target.secret} at ${target.host}:${target.port} is reachable but Postgres connection failed. Check credentials and database name.${pluginContext}`,\n } satisfies PreflightFailure;\n }\n return {\n secret: target.secret,\n host: target.host,\n port: target.port,\n error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening.${pluginContext}`,\n } satisfies PreflightFailure;\n }\n\n const reachable = yield* checkTcpReachable(target.host, target.port);\n if (reachable) return null;\n return {\n secret: target.secret,\n host: target.host,\n port: target.port,\n error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening`,\n } satisfies PreflightFailure;\n }),\n { concurrency: \"unbounded\" },\n ).pipe(Effect.map((results) => results.filter((r): r is PreflightFailure => r !== null)));\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
|
|
4
|
+
//#region src/infra/preflight.ts
|
|
5
|
+
function parseLocalUrl(url) {
|
|
6
|
+
try {
|
|
7
|
+
const match = url.match(/:\/\/([^:/]+):(\d+)/);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
const host = match[1];
|
|
10
|
+
const port = Number.parseInt(match[2], 10);
|
|
11
|
+
if (Number.isNaN(port)) return null;
|
|
12
|
+
if (host !== "localhost" && host !== "127.0.0.1") return null;
|
|
13
|
+
return {
|
|
14
|
+
host,
|
|
15
|
+
port
|
|
16
|
+
};
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function checkTcpReachable(host, port, timeoutMs = 2e3) {
|
|
22
|
+
return Effect.async((resume) => {
|
|
23
|
+
const socket = createConnection({
|
|
24
|
+
host,
|
|
25
|
+
port
|
|
26
|
+
});
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
socket.destroy();
|
|
29
|
+
resume(Effect.succeed(false));
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
socket.once("connect", () => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
socket.destroy();
|
|
34
|
+
resume(Effect.succeed(true));
|
|
35
|
+
});
|
|
36
|
+
socket.once("error", () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
socket.destroy();
|
|
39
|
+
resume(Effect.succeed(false));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function checkPgConnection(url) {
|
|
44
|
+
return Effect.gen(function* () {
|
|
45
|
+
if (!(yield* checkTcpReachable(new URL(url).hostname, Number(new URL(url).port) || 5432, 4e3))) return false;
|
|
46
|
+
try {
|
|
47
|
+
const { Pool } = yield* Effect.promise(() => import("../node_modules/pg/esm/index.mjs"));
|
|
48
|
+
const pool = new Pool({
|
|
49
|
+
connectionString: url,
|
|
50
|
+
connectionTimeoutMillis: 3e3
|
|
51
|
+
});
|
|
52
|
+
const client = yield* Effect.promise(() => pool.connect().then((c) => ({
|
|
53
|
+
client: c,
|
|
54
|
+
pool
|
|
55
|
+
})));
|
|
56
|
+
try {
|
|
57
|
+
yield* Effect.promise(() => client.client.query("SELECT 1"));
|
|
58
|
+
return true;
|
|
59
|
+
} finally {
|
|
60
|
+
client.client.release();
|
|
61
|
+
yield* Effect.promise(() => client.pool.end().catch(() => {}));
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function preflightTargetsFromEnv(env) {
|
|
69
|
+
const targets = [];
|
|
70
|
+
for (const [secret, value] of Object.entries(env)) {
|
|
71
|
+
if (!value) continue;
|
|
72
|
+
if (secret.endsWith("_DATABASE_URL")) {
|
|
73
|
+
const parsed = parseLocalUrl(value);
|
|
74
|
+
if (parsed) targets.push({
|
|
75
|
+
secret,
|
|
76
|
+
host: parsed.host,
|
|
77
|
+
port: parsed.port,
|
|
78
|
+
kind: "postgres",
|
|
79
|
+
url: value
|
|
80
|
+
});
|
|
81
|
+
} else if (secret.endsWith("_REDIS_URL")) {
|
|
82
|
+
const parsed = parseLocalUrl(value);
|
|
83
|
+
if (parsed) targets.push({
|
|
84
|
+
secret,
|
|
85
|
+
host: parsed.host,
|
|
86
|
+
port: parsed.port,
|
|
87
|
+
kind: "redis",
|
|
88
|
+
url: value
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return targets;
|
|
93
|
+
}
|
|
94
|
+
function preflightLocalInfra(env, overrides) {
|
|
95
|
+
const targets = preflightTargetsFromEnv(overrides ? {
|
|
96
|
+
...env,
|
|
97
|
+
...overrides
|
|
98
|
+
} : env);
|
|
99
|
+
if (targets.length === 0) return Effect.succeed([]);
|
|
100
|
+
return Effect.forEach(targets, (target) => Effect.gen(function* () {
|
|
101
|
+
if (target.kind === "postgres") {
|
|
102
|
+
if (yield* checkPgConnection(target.url)) return null;
|
|
103
|
+
const tcpOk = yield* checkTcpReachable(target.host, target.port);
|
|
104
|
+
const pluginContext = target.secret.endsWith("_DATABASE_URL") ? ` The plugin for ${target.secret} runs inside the local host process, so this DB must be reachable. Run \`docker compose up -d --wait\` to start local Postgres/Redis.` : "";
|
|
105
|
+
if (tcpOk) return {
|
|
106
|
+
secret: target.secret,
|
|
107
|
+
host: target.host,
|
|
108
|
+
port: target.port,
|
|
109
|
+
error: `${target.secret} at ${target.host}:${target.port} is reachable but Postgres connection failed. Check credentials and database name.${pluginContext}`
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
secret: target.secret,
|
|
113
|
+
host: target.host,
|
|
114
|
+
port: target.port,
|
|
115
|
+
error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening.${pluginContext}`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (yield* checkTcpReachable(target.host, target.port)) return null;
|
|
119
|
+
return {
|
|
120
|
+
secret: target.secret,
|
|
121
|
+
host: target.host,
|
|
122
|
+
port: target.port,
|
|
123
|
+
error: `${target.secret} points to ${target.host}:${target.port} but nothing is listening`
|
|
124
|
+
};
|
|
125
|
+
}), { concurrency: "unbounded" }).pipe(Effect.map((results) => results.filter((r) => r !== null)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
export { preflightLocalInfra };
|
|
130
|
+
//# sourceMappingURL=preflight.mjs.map
|