everything-dev 2.0.0-rc.7 → 2.0.0-rc.8
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/api/auth-middleware.cjs +14 -1
- package/dist/api/auth-middleware.cjs.map +1 -1
- package/dist/api/auth-middleware.d.cts +9 -0
- package/dist/api/auth-middleware.d.mts +9 -0
- package/dist/api/auth-middleware.mjs +14 -1
- package/dist/api/auth-middleware.mjs.map +1 -1
- package/dist/cli/init.cjs +22 -10
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.mjs +22 -10
- package/dist/cli/init.mjs.map +1 -1
- package/dist/cli.cjs +4 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +4 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/commands/keys.cjs +1 -1
- package/dist/commands/keys.cjs.map +1 -1
- package/dist/commands/keys.mjs +1 -1
- package/dist/commands/keys.mjs.map +1 -1
- package/dist/contract.meta.cjs +1 -1
- package/dist/contract.meta.cjs.map +1 -1
- package/dist/contract.meta.d.cts +1 -1
- package/dist/contract.meta.d.mts +1 -1
- package/dist/contract.meta.mjs +1 -1
- package/dist/contract.meta.mjs.map +1 -1
- package/dist/db/core.cjs +22 -4
- package/dist/db/core.cjs.map +1 -1
- package/dist/db/core.mjs +22 -4
- package/dist/db/core.mjs.map +1 -1
- package/dist/http-client.cjs +14 -4
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.mjs +14 -4
- package/dist/http-client.mjs.map +1 -1
- package/dist/infra/planner.cjs +6 -2
- package/dist/infra/planner.cjs.map +1 -1
- package/dist/infra/planner.mjs +6 -2
- package/dist/infra/planner.mjs.map +1 -1
- package/dist/ui/auth.d.cts +217 -123
- package/dist/ui/auth.d.mts +217 -123
- package/dist/ui/router-client.cjs +2 -1
- package/dist/ui/router-client.cjs.map +1 -1
- package/dist/ui/router-client.mjs +2 -1
- package/dist/ui/router-client.mjs.map +1 -1
- package/package.json +10 -10
- package/skills/api-and-auth/SKILL.md +12 -9
- package/skills/api-and-auth/references/middleware.md +38 -0
- package/skills/cli-reference/SKILL.md +22 -6
- package/skills/talk-to-the-app/SKILL.md +32 -5
package/dist/http-client.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-client.cjs","names":["Data","Duration","Effect","Schedule","Context","Layer"],"sources":["../src/http-client.ts"],"sourcesContent":["import { Context, Data, Duration, Effect, Layer, Schedule } from \"effect\";\n\n// --- Tagged errors ---\n\nexport class FetchNetworkError extends Data.TaggedError(\"FetchNetworkError\")<{\n url: string;\n cause: unknown;\n}> {}\n\nexport class FetchTimeoutError extends Data.TaggedError(\"FetchTimeoutError\")<{\n url: string;\n}> {}\n\nexport class FetchHttpError extends Data.TaggedError(\"FetchHttpError\")<{\n url: string;\n status: number;\n statusText: string;\n}> {}\n\nexport type FetchError = FetchNetworkError | FetchTimeoutError | FetchHttpError;\n\n// --- Options ---\n\nexport interface FetchOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: BodyInit;\n redirect?: RequestRedirect;\n timeout?: Duration.Input;\n}\n\nexport interface FetchWithRetryOptions extends FetchOptions {\n retries?: number;\n}\n\n// --- Defaults ---\n\nconst DEFAULT_TIMEOUT = \"10 seconds\";\nconst DEFAULT_RETRIES = 3;\n\nconst EXPONENTIAL_BASE = Duration.seconds(1);\nconst EXPONENTIAL_CAP = Duration.seconds(15);\n\ntype FetchImpl = typeof globalThis.fetch;\n\n// --- Helpers ---\n\nconst isRetryable = (error: FetchError): boolean => {\n if (error instanceof FetchNetworkError) return true;\n if (error instanceof FetchTimeoutError) return true;\n if (error instanceof FetchHttpError) return error.status >= 500;\n return false;\n};\n\nexport interface HttpFetchShape {\n /** One network attempt: no status assertion, no internal deadline. The\n * fiber's signal is composed with any caller-provided signal, so an\n * `Effect.timeout` around the call aborts the in-flight request, and a\n * caller-initiated cancellation still wins. */\n readonly attempt: (\n input: string | URL | Request,\n init?: RequestInit,\n ) => Effect.Effect<Response, FetchNetworkError>;\n /** attempt + deadline + ok assertion (non-2xx → `FetchHttpError`) + capped\n * exponential jittered retry (5xx/network/timeout retried, 4xx not). */\n readonly get: (\n url: string,\n options?: FetchWithRetryOptions,\n ) => Effect.Effect<Response, FetchError>;\n}\n\nconst makeHttpFetch = (\n base: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,\n): HttpFetchShape => {\n const urlOf = (input: string | URL | Request): string =>\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n const attempt = (\n input: string | URL | Request,\n init?: RequestInit,\n ): Effect.Effect<Response, FetchNetworkError> =>\n Effect.tryPromise({\n try: (signal) =>\n base(input, {\n ...init,\n signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal,\n }),\n catch: (cause) => new FetchNetworkError({ url: urlOf(input), cause }),\n });\n\n const get = (\n url: string,\n options?: FetchWithRetryOptions,\n ): Effect.Effect<Response, FetchError> => {\n const timeoutMs = Duration.toMillis(options?.timeout ?? DEFAULT_TIMEOUT);\n const retries = options?.retries ?? DEFAULT_RETRIES;\n const once = attempt(url, {\n method: options?.method ?? \"GET\",\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n }).pipe(\n Effect.timeoutOrElse({\n duration: timeoutMs,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n Effect.filterOrFail(\n (response) => response.ok,\n (response) =>\n new FetchHttpError({ url, status: response.status, statusText: response.statusText }),\n ),\n );\n if (retries <= 0) return once;\n return once.pipe(\n Effect.retry({\n schedule: Schedule.exponential(EXPONENTIAL_BASE).pipe(\n Schedule.upTo({ duration: EXPONENTIAL_CAP, times: retries }),\n Schedule.jittered,\n ),\n while: isRetryable,\n }),\n );\n };\n\n return { attempt, get };\n};\n\nconst defaultHttpFetch = makeHttpFetch((url, init) => globalThis.fetch(url, init));\n\n/**\n * The HTTP seam for boot-path consumers (bundle resolution, federation\n * loading, orchestrator deadlines): one resilience policy — interruption-\n * backed deadlines, capped exponential jittered retry, typed errors — with\n * the base fetch injectable so the bundle fetch interceptor can point it at\n * the pre-patch `fetch` and not re-enter itself.\n */\nexport class HttpFetch extends Context.Service<HttpFetch, HttpFetchShape>()(\n \"everything-dev/http-client/HttpFetch\",\n) {\n static layerWith(options: { fetchImpl: FetchImpl }): Layer.Layer<HttpFetch> {\n return Layer.succeed(HttpFetch, makeHttpFetch(options.fetchImpl));\n }\n\n static readonly layer: Layer.Layer<HttpFetch> = Layer.succeed(HttpFetch, defaultHttpFetch);\n}\n\n// --- In-memory GET cache (per-process, prevents duplicate round trips) ---\n\nconst getCache = new Map<string, { data: unknown; expiresAt: number }>();\nconst GET_CACHE_TTL_MS = 30_000;\n\nexport function clearHttpCache(): void {\n getCache.clear();\n}\n\nfunction isCacheable(_url: string, options?: FetchWithRetryOptions): boolean {\n if (options?.method && options.method !== \"GET\") return false;\n if (options?.body) return false;\n return true;\n}\n\n// --- Promise wrappers (bridge for non-Effect code) — the ambient call-time\n// lookup means a boot fetch crossing the bundle interceptor still gains its\n// retry/deadline policy; the fiber-signal composition aborts the in-flight\n// request at the deadline. ---\n\n/** One raw fetch attempt with a deadline: returns the Response regardless of\n * status; rejects on network failure or deadline expiry. */\nexport const fetchRaw = (\n url: string,\n init?: RequestInit,\n timeout: Duration.Input = DEFAULT_TIMEOUT,\n): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch.attempt(url, init).pipe(\n Effect.timeoutOrElse({\n duration: timeout,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchResponse = (url: string, options?: FetchOptions): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch\n .attempt(url, {\n method: options?.method,\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n })\n .pipe(\n Effect.timeoutOrElse({\n duration: options?.timeout ?? DEFAULT_TIMEOUT,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchJsonOrNull = async <T>(\n url: string,\n options?: FetchWithRetryOptions,\n): Promise<T | null> => {\n if (isCacheable(url, options)) {\n const cached = getCache.get(url);\n if (cached && cached.expiresAt > Date.now()) {\n return cached.data as T;\n }\n }\n\n try {\n const res = await Effect.runPromise(defaultHttpFetch.get(url, options));\n const data = (await res.json()) as T;\n if (isCacheable(url, options)) {\n getCache.set(url, { data, expiresAt: Date.now() + GET_CACHE_TTL_MS });\n }\n return data;\n } catch (error) {\n const msg =\n error instanceof FetchNetworkError\n ? `[http] Network error: ${error.url} — ${String(error.cause)}`\n : error instanceof FetchTimeoutError\n ? `[http] Timeout: ${error.url}`\n : error instanceof FetchHttpError\n ? `[http] HTTP ${error.status}: ${error.url}`\n : `[http] Unknown error while fetching ${url}`;\n console.error(msg);\n return null;\n }\n};\n\nexport const fetchWithRetryEff = (\n url: string,\n options?: FetchWithRetryOptions,\n): Effect.Effect<Response, FetchError> => defaultHttpFetch.get(url, options);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http-client.cjs","names":["Data","Duration","Effect","Schedule","Context","Layer"],"sources":["../src/http-client.ts"],"sourcesContent":["import { Context, Data, Duration, Effect, Layer, Schedule } from \"effect\";\n\n// --- Tagged errors ---\n\nexport class FetchNetworkError extends Data.TaggedError(\"FetchNetworkError\")<{\n url: string;\n cause: unknown;\n}> {}\n\nexport class FetchTimeoutError extends Data.TaggedError(\"FetchTimeoutError\")<{\n url: string;\n}> {}\n\nexport class FetchHttpError extends Data.TaggedError(\"FetchHttpError\")<{\n url: string;\n status: number;\n statusText: string;\n}> {}\n\nexport type FetchError = FetchNetworkError | FetchTimeoutError | FetchHttpError;\n\n// --- Options ---\n\nexport interface FetchOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: BodyInit;\n redirect?: RequestRedirect;\n timeout?: Duration.Input;\n}\n\nexport interface FetchWithRetryOptions extends FetchOptions {\n retries?: number;\n}\n\n// --- Defaults ---\n\nconst DEFAULT_TIMEOUT = \"10 seconds\";\nconst DEFAULT_RETRIES = 3;\n\nconst EXPONENTIAL_BASE = Duration.seconds(1);\nconst EXPONENTIAL_CAP = Duration.seconds(15);\n\ntype FetchImpl = typeof globalThis.fetch;\n\n// --- Helpers ---\n\nconst isRetryable = (error: FetchError): boolean => {\n if (error instanceof FetchNetworkError) return true;\n if (error instanceof FetchTimeoutError) return true;\n if (error instanceof FetchHttpError) return error.status >= 500;\n return false;\n};\n\nexport interface HttpFetchShape {\n /** One network attempt: no status assertion, no internal deadline. The\n * fiber's signal is composed with any caller-provided signal, so an\n * `Effect.timeout` around the call aborts the in-flight request, and a\n * caller-initiated cancellation still wins. */\n readonly attempt: (\n input: string | URL | Request,\n init?: RequestInit,\n ) => Effect.Effect<Response, FetchNetworkError>;\n /** attempt + deadline + ok assertion (non-2xx → `FetchHttpError`) + capped\n * exponential jittered retry (5xx/network/timeout retried, 4xx not). */\n readonly get: (\n url: string,\n options?: FetchWithRetryOptions,\n ) => Effect.Effect<Response, FetchError>;\n}\n\nconst makeHttpFetch = (\n base: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,\n): HttpFetchShape => {\n const urlOf = (input: string | URL | Request): string =>\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n const attempt = (\n input: string | URL | Request,\n init?: RequestInit,\n ): Effect.Effect<Response, FetchNetworkError> =>\n Effect.tryPromise({\n try: (signal) =>\n base(input, {\n ...init,\n signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal,\n }),\n catch: (cause) => new FetchNetworkError({ url: urlOf(input), cause }),\n });\n\n const get = (\n url: string,\n options?: FetchWithRetryOptions,\n ): Effect.Effect<Response, FetchError> => {\n const timeoutMs = Duration.toMillis(options?.timeout ?? DEFAULT_TIMEOUT);\n const retries = options?.retries ?? DEFAULT_RETRIES;\n const once = attempt(url, {\n method: options?.method ?? \"GET\",\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n }).pipe(\n Effect.timeoutOrElse({\n duration: timeoutMs,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n Effect.filterOrFail(\n (response) => response.ok,\n (response) =>\n new FetchHttpError({ url, status: response.status, statusText: response.statusText }),\n ),\n );\n if (retries <= 0) return once;\n return once.pipe(\n Effect.retry({\n schedule: Schedule.exponential(EXPONENTIAL_BASE).pipe(\n Schedule.upTo({ duration: EXPONENTIAL_CAP, times: retries }),\n Schedule.jittered,\n ),\n while: isRetryable,\n }),\n );\n };\n\n return { attempt, get };\n};\n\nconst defaultHttpFetch = makeHttpFetch((url, init) => globalThis.fetch(url, init));\n\n/**\n * The HTTP seam for boot-path consumers (bundle resolution, federation\n * loading, orchestrator deadlines): one resilience policy — interruption-\n * backed deadlines, capped exponential jittered retry, typed errors — with\n * the base fetch injectable so the bundle fetch interceptor can point it at\n * the pre-patch `fetch` and not re-enter itself.\n */\nexport class HttpFetch extends Context.Service<HttpFetch, HttpFetchShape>()(\n \"everything-dev/http-client/HttpFetch\",\n) {\n static layerWith(options: { fetchImpl: FetchImpl }): Layer.Layer<HttpFetch> {\n return Layer.succeed(HttpFetch, makeHttpFetch(options.fetchImpl));\n }\n\n static readonly layer: Layer.Layer<HttpFetch> = Layer.succeed(HttpFetch, defaultHttpFetch);\n}\n\n// --- In-memory GET cache (per-process, prevents duplicate round trips) ---\n\nconst getCache = new Map<string, { data: unknown; expiresAt: number }>();\nconst GET_CACHE_TTL_MS = 30_000;\nconst GET_CACHE_MAX_ENTRIES = 128;\n\nexport function clearHttpCache(): void {\n getCache.clear();\n}\n\nexport function httpCacheSize(): number {\n return getCache.size;\n}\n\nfunction setCached(url: string, data: unknown): void {\n const now = Date.now();\n for (const [key, entry] of getCache) {\n if (entry.expiresAt <= now) getCache.delete(key);\n }\n if (getCache.size >= GET_CACHE_MAX_ENTRIES) {\n for (const key of getCache.keys()) {\n getCache.delete(key);\n if (getCache.size < GET_CACHE_MAX_ENTRIES) break;\n }\n }\n getCache.set(url, { data, expiresAt: now + GET_CACHE_TTL_MS });\n}\n\nfunction isCacheable(_url: string, options?: FetchWithRetryOptions): boolean {\n if (options?.method && options.method !== \"GET\") return false;\n if (options?.body) return false;\n return true;\n}\n\n// --- Promise wrappers (bridge for non-Effect code) — the ambient call-time\n// lookup means a boot fetch crossing the bundle interceptor still gains its\n// retry/deadline policy; the fiber-signal composition aborts the in-flight\n// request at the deadline. ---\n\n/** One raw fetch attempt with a deadline: returns the Response regardless of\n * status; rejects on network failure or deadline expiry. */\nexport const fetchRaw = (\n url: string,\n init?: RequestInit,\n timeout: Duration.Input = DEFAULT_TIMEOUT,\n): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch.attempt(url, init).pipe(\n Effect.timeoutOrElse({\n duration: timeout,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchResponse = (url: string, options?: FetchOptions): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch\n .attempt(url, {\n method: options?.method,\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n })\n .pipe(\n Effect.timeoutOrElse({\n duration: options?.timeout ?? DEFAULT_TIMEOUT,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchJsonOrNull = async <T>(\n url: string,\n options?: FetchWithRetryOptions,\n): Promise<T | null> => {\n if (isCacheable(url, options)) {\n const cached = getCache.get(url);\n if (cached && cached.expiresAt > Date.now()) {\n return cached.data as T;\n }\n }\n\n try {\n const res = await Effect.runPromise(defaultHttpFetch.get(url, options));\n const data = (await res.json()) as T;\n if (isCacheable(url, options)) {\n setCached(url, data);\n }\n return data;\n } catch (error) {\n const msg =\n error instanceof FetchNetworkError\n ? `[http] Network error: ${error.url} — ${String(error.cause)}`\n : error instanceof FetchTimeoutError\n ? `[http] Timeout: ${error.url}`\n : error instanceof FetchHttpError\n ? `[http] HTTP ${error.status}: ${error.url}`\n : `[http] Unknown error while fetching ${url}`;\n console.error(msg);\n return null;\n }\n};\n\nexport const fetchWithRetryEff = (\n url: string,\n options?: FetchWithRetryOptions,\n): Effect.Effect<Response, FetchError> => defaultHttpFetch.get(url, options);\n"],"mappings":"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"}
|
package/dist/http-client.mjs
CHANGED
|
@@ -72,6 +72,19 @@ var HttpFetch = class HttpFetch extends Context.Service()("everything-dev/http-c
|
|
|
72
72
|
};
|
|
73
73
|
const getCache = /* @__PURE__ */ new Map();
|
|
74
74
|
const GET_CACHE_TTL_MS = 3e4;
|
|
75
|
+
const GET_CACHE_MAX_ENTRIES = 128;
|
|
76
|
+
function setCached(url, data) {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
for (const [key, entry] of getCache) if (entry.expiresAt <= now) getCache.delete(key);
|
|
79
|
+
if (getCache.size >= GET_CACHE_MAX_ENTRIES) for (const key of getCache.keys()) {
|
|
80
|
+
getCache.delete(key);
|
|
81
|
+
if (getCache.size < GET_CACHE_MAX_ENTRIES) break;
|
|
82
|
+
}
|
|
83
|
+
getCache.set(url, {
|
|
84
|
+
data,
|
|
85
|
+
expiresAt: now + GET_CACHE_TTL_MS
|
|
86
|
+
});
|
|
87
|
+
}
|
|
75
88
|
function isCacheable(_url, options) {
|
|
76
89
|
if (options?.method && options.method !== "GET") return false;
|
|
77
90
|
if (options?.body) return false;
|
|
@@ -99,10 +112,7 @@ const fetchJsonOrNull = async (url, options) => {
|
|
|
99
112
|
}
|
|
100
113
|
try {
|
|
101
114
|
const data = await (await Effect.runPromise(defaultHttpFetch.get(url, options))).json();
|
|
102
|
-
if (isCacheable(url, options))
|
|
103
|
-
data,
|
|
104
|
-
expiresAt: Date.now() + GET_CACHE_TTL_MS
|
|
105
|
-
});
|
|
115
|
+
if (isCacheable(url, options)) setCached(url, data);
|
|
106
116
|
return data;
|
|
107
117
|
} catch (error) {
|
|
108
118
|
const msg = error instanceof FetchNetworkError ? `[http] Network error: ${error.url} — ${String(error.cause)}` : error instanceof FetchTimeoutError ? `[http] Timeout: ${error.url}` : error instanceof FetchHttpError ? `[http] HTTP ${error.status}: ${error.url}` : `[http] Unknown error while fetching ${url}`;
|
package/dist/http-client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-client.mjs","names":[],"sources":["../src/http-client.ts"],"sourcesContent":["import { Context, Data, Duration, Effect, Layer, Schedule } from \"effect\";\n\n// --- Tagged errors ---\n\nexport class FetchNetworkError extends Data.TaggedError(\"FetchNetworkError\")<{\n url: string;\n cause: unknown;\n}> {}\n\nexport class FetchTimeoutError extends Data.TaggedError(\"FetchTimeoutError\")<{\n url: string;\n}> {}\n\nexport class FetchHttpError extends Data.TaggedError(\"FetchHttpError\")<{\n url: string;\n status: number;\n statusText: string;\n}> {}\n\nexport type FetchError = FetchNetworkError | FetchTimeoutError | FetchHttpError;\n\n// --- Options ---\n\nexport interface FetchOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: BodyInit;\n redirect?: RequestRedirect;\n timeout?: Duration.Input;\n}\n\nexport interface FetchWithRetryOptions extends FetchOptions {\n retries?: number;\n}\n\n// --- Defaults ---\n\nconst DEFAULT_TIMEOUT = \"10 seconds\";\nconst DEFAULT_RETRIES = 3;\n\nconst EXPONENTIAL_BASE = Duration.seconds(1);\nconst EXPONENTIAL_CAP = Duration.seconds(15);\n\ntype FetchImpl = typeof globalThis.fetch;\n\n// --- Helpers ---\n\nconst isRetryable = (error: FetchError): boolean => {\n if (error instanceof FetchNetworkError) return true;\n if (error instanceof FetchTimeoutError) return true;\n if (error instanceof FetchHttpError) return error.status >= 500;\n return false;\n};\n\nexport interface HttpFetchShape {\n /** One network attempt: no status assertion, no internal deadline. The\n * fiber's signal is composed with any caller-provided signal, so an\n * `Effect.timeout` around the call aborts the in-flight request, and a\n * caller-initiated cancellation still wins. */\n readonly attempt: (\n input: string | URL | Request,\n init?: RequestInit,\n ) => Effect.Effect<Response, FetchNetworkError>;\n /** attempt + deadline + ok assertion (non-2xx → `FetchHttpError`) + capped\n * exponential jittered retry (5xx/network/timeout retried, 4xx not). */\n readonly get: (\n url: string,\n options?: FetchWithRetryOptions,\n ) => Effect.Effect<Response, FetchError>;\n}\n\nconst makeHttpFetch = (\n base: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,\n): HttpFetchShape => {\n const urlOf = (input: string | URL | Request): string =>\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n const attempt = (\n input: string | URL | Request,\n init?: RequestInit,\n ): Effect.Effect<Response, FetchNetworkError> =>\n Effect.tryPromise({\n try: (signal) =>\n base(input, {\n ...init,\n signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal,\n }),\n catch: (cause) => new FetchNetworkError({ url: urlOf(input), cause }),\n });\n\n const get = (\n url: string,\n options?: FetchWithRetryOptions,\n ): Effect.Effect<Response, FetchError> => {\n const timeoutMs = Duration.toMillis(options?.timeout ?? DEFAULT_TIMEOUT);\n const retries = options?.retries ?? DEFAULT_RETRIES;\n const once = attempt(url, {\n method: options?.method ?? \"GET\",\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n }).pipe(\n Effect.timeoutOrElse({\n duration: timeoutMs,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n Effect.filterOrFail(\n (response) => response.ok,\n (response) =>\n new FetchHttpError({ url, status: response.status, statusText: response.statusText }),\n ),\n );\n if (retries <= 0) return once;\n return once.pipe(\n Effect.retry({\n schedule: Schedule.exponential(EXPONENTIAL_BASE).pipe(\n Schedule.upTo({ duration: EXPONENTIAL_CAP, times: retries }),\n Schedule.jittered,\n ),\n while: isRetryable,\n }),\n );\n };\n\n return { attempt, get };\n};\n\nconst defaultHttpFetch = makeHttpFetch((url, init) => globalThis.fetch(url, init));\n\n/**\n * The HTTP seam for boot-path consumers (bundle resolution, federation\n * loading, orchestrator deadlines): one resilience policy — interruption-\n * backed deadlines, capped exponential jittered retry, typed errors — with\n * the base fetch injectable so the bundle fetch interceptor can point it at\n * the pre-patch `fetch` and not re-enter itself.\n */\nexport class HttpFetch extends Context.Service<HttpFetch, HttpFetchShape>()(\n \"everything-dev/http-client/HttpFetch\",\n) {\n static layerWith(options: { fetchImpl: FetchImpl }): Layer.Layer<HttpFetch> {\n return Layer.succeed(HttpFetch, makeHttpFetch(options.fetchImpl));\n }\n\n static readonly layer: Layer.Layer<HttpFetch> = Layer.succeed(HttpFetch, defaultHttpFetch);\n}\n\n// --- In-memory GET cache (per-process, prevents duplicate round trips) ---\n\nconst getCache = new Map<string, { data: unknown; expiresAt: number }>();\nconst GET_CACHE_TTL_MS = 30_000;\n\nexport function clearHttpCache(): void {\n getCache.clear();\n}\n\nfunction isCacheable(_url: string, options?: FetchWithRetryOptions): boolean {\n if (options?.method && options.method !== \"GET\") return false;\n if (options?.body) return false;\n return true;\n}\n\n// --- Promise wrappers (bridge for non-Effect code) — the ambient call-time\n// lookup means a boot fetch crossing the bundle interceptor still gains its\n// retry/deadline policy; the fiber-signal composition aborts the in-flight\n// request at the deadline. ---\n\n/** One raw fetch attempt with a deadline: returns the Response regardless of\n * status; rejects on network failure or deadline expiry. */\nexport const fetchRaw = (\n url: string,\n init?: RequestInit,\n timeout: Duration.Input = DEFAULT_TIMEOUT,\n): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch.attempt(url, init).pipe(\n Effect.timeoutOrElse({\n duration: timeout,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchResponse = (url: string, options?: FetchOptions): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch\n .attempt(url, {\n method: options?.method,\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n })\n .pipe(\n Effect.timeoutOrElse({\n duration: options?.timeout ?? DEFAULT_TIMEOUT,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchJsonOrNull = async <T>(\n url: string,\n options?: FetchWithRetryOptions,\n): Promise<T | null> => {\n if (isCacheable(url, options)) {\n const cached = getCache.get(url);\n if (cached && cached.expiresAt > Date.now()) {\n return cached.data as T;\n }\n }\n\n try {\n const res = await Effect.runPromise(defaultHttpFetch.get(url, options));\n const data = (await res.json()) as T;\n if (isCacheable(url, options)) {\n getCache.set(url, { data, expiresAt: Date.now() + GET_CACHE_TTL_MS });\n }\n return data;\n } catch (error) {\n const msg =\n error instanceof FetchNetworkError\n ? `[http] Network error: ${error.url} — ${String(error.cause)}`\n : error instanceof FetchTimeoutError\n ? `[http] Timeout: ${error.url}`\n : error instanceof FetchHttpError\n ? `[http] HTTP ${error.status}: ${error.url}`\n : `[http] Unknown error while fetching ${url}`;\n console.error(msg);\n return null;\n }\n};\n\nexport const fetchWithRetryEff = (\n url: string,\n options?: FetchWithRetryOptions,\n): Effect.Effect<Response, FetchError> => defaultHttpFetch.get(url, options);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http-client.mjs","names":[],"sources":["../src/http-client.ts"],"sourcesContent":["import { Context, Data, Duration, Effect, Layer, Schedule } from \"effect\";\n\n// --- Tagged errors ---\n\nexport class FetchNetworkError extends Data.TaggedError(\"FetchNetworkError\")<{\n url: string;\n cause: unknown;\n}> {}\n\nexport class FetchTimeoutError extends Data.TaggedError(\"FetchTimeoutError\")<{\n url: string;\n}> {}\n\nexport class FetchHttpError extends Data.TaggedError(\"FetchHttpError\")<{\n url: string;\n status: number;\n statusText: string;\n}> {}\n\nexport type FetchError = FetchNetworkError | FetchTimeoutError | FetchHttpError;\n\n// --- Options ---\n\nexport interface FetchOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: BodyInit;\n redirect?: RequestRedirect;\n timeout?: Duration.Input;\n}\n\nexport interface FetchWithRetryOptions extends FetchOptions {\n retries?: number;\n}\n\n// --- Defaults ---\n\nconst DEFAULT_TIMEOUT = \"10 seconds\";\nconst DEFAULT_RETRIES = 3;\n\nconst EXPONENTIAL_BASE = Duration.seconds(1);\nconst EXPONENTIAL_CAP = Duration.seconds(15);\n\ntype FetchImpl = typeof globalThis.fetch;\n\n// --- Helpers ---\n\nconst isRetryable = (error: FetchError): boolean => {\n if (error instanceof FetchNetworkError) return true;\n if (error instanceof FetchTimeoutError) return true;\n if (error instanceof FetchHttpError) return error.status >= 500;\n return false;\n};\n\nexport interface HttpFetchShape {\n /** One network attempt: no status assertion, no internal deadline. The\n * fiber's signal is composed with any caller-provided signal, so an\n * `Effect.timeout` around the call aborts the in-flight request, and a\n * caller-initiated cancellation still wins. */\n readonly attempt: (\n input: string | URL | Request,\n init?: RequestInit,\n ) => Effect.Effect<Response, FetchNetworkError>;\n /** attempt + deadline + ok assertion (non-2xx → `FetchHttpError`) + capped\n * exponential jittered retry (5xx/network/timeout retried, 4xx not). */\n readonly get: (\n url: string,\n options?: FetchWithRetryOptions,\n ) => Effect.Effect<Response, FetchError>;\n}\n\nconst makeHttpFetch = (\n base: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,\n): HttpFetchShape => {\n const urlOf = (input: string | URL | Request): string =>\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n\n const attempt = (\n input: string | URL | Request,\n init?: RequestInit,\n ): Effect.Effect<Response, FetchNetworkError> =>\n Effect.tryPromise({\n try: (signal) =>\n base(input, {\n ...init,\n signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal,\n }),\n catch: (cause) => new FetchNetworkError({ url: urlOf(input), cause }),\n });\n\n const get = (\n url: string,\n options?: FetchWithRetryOptions,\n ): Effect.Effect<Response, FetchError> => {\n const timeoutMs = Duration.toMillis(options?.timeout ?? DEFAULT_TIMEOUT);\n const retries = options?.retries ?? DEFAULT_RETRIES;\n const once = attempt(url, {\n method: options?.method ?? \"GET\",\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n }).pipe(\n Effect.timeoutOrElse({\n duration: timeoutMs,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n Effect.filterOrFail(\n (response) => response.ok,\n (response) =>\n new FetchHttpError({ url, status: response.status, statusText: response.statusText }),\n ),\n );\n if (retries <= 0) return once;\n return once.pipe(\n Effect.retry({\n schedule: Schedule.exponential(EXPONENTIAL_BASE).pipe(\n Schedule.upTo({ duration: EXPONENTIAL_CAP, times: retries }),\n Schedule.jittered,\n ),\n while: isRetryable,\n }),\n );\n };\n\n return { attempt, get };\n};\n\nconst defaultHttpFetch = makeHttpFetch((url, init) => globalThis.fetch(url, init));\n\n/**\n * The HTTP seam for boot-path consumers (bundle resolution, federation\n * loading, orchestrator deadlines): one resilience policy — interruption-\n * backed deadlines, capped exponential jittered retry, typed errors — with\n * the base fetch injectable so the bundle fetch interceptor can point it at\n * the pre-patch `fetch` and not re-enter itself.\n */\nexport class HttpFetch extends Context.Service<HttpFetch, HttpFetchShape>()(\n \"everything-dev/http-client/HttpFetch\",\n) {\n static layerWith(options: { fetchImpl: FetchImpl }): Layer.Layer<HttpFetch> {\n return Layer.succeed(HttpFetch, makeHttpFetch(options.fetchImpl));\n }\n\n static readonly layer: Layer.Layer<HttpFetch> = Layer.succeed(HttpFetch, defaultHttpFetch);\n}\n\n// --- In-memory GET cache (per-process, prevents duplicate round trips) ---\n\nconst getCache = new Map<string, { data: unknown; expiresAt: number }>();\nconst GET_CACHE_TTL_MS = 30_000;\nconst GET_CACHE_MAX_ENTRIES = 128;\n\nexport function clearHttpCache(): void {\n getCache.clear();\n}\n\nexport function httpCacheSize(): number {\n return getCache.size;\n}\n\nfunction setCached(url: string, data: unknown): void {\n const now = Date.now();\n for (const [key, entry] of getCache) {\n if (entry.expiresAt <= now) getCache.delete(key);\n }\n if (getCache.size >= GET_CACHE_MAX_ENTRIES) {\n for (const key of getCache.keys()) {\n getCache.delete(key);\n if (getCache.size < GET_CACHE_MAX_ENTRIES) break;\n }\n }\n getCache.set(url, { data, expiresAt: now + GET_CACHE_TTL_MS });\n}\n\nfunction isCacheable(_url: string, options?: FetchWithRetryOptions): boolean {\n if (options?.method && options.method !== \"GET\") return false;\n if (options?.body) return false;\n return true;\n}\n\n// --- Promise wrappers (bridge for non-Effect code) — the ambient call-time\n// lookup means a boot fetch crossing the bundle interceptor still gains its\n// retry/deadline policy; the fiber-signal composition aborts the in-flight\n// request at the deadline. ---\n\n/** One raw fetch attempt with a deadline: returns the Response regardless of\n * status; rejects on network failure or deadline expiry. */\nexport const fetchRaw = (\n url: string,\n init?: RequestInit,\n timeout: Duration.Input = DEFAULT_TIMEOUT,\n): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch.attempt(url, init).pipe(\n Effect.timeoutOrElse({\n duration: timeout,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchResponse = (url: string, options?: FetchOptions): Promise<Response> =>\n Effect.runPromise(\n defaultHttpFetch\n .attempt(url, {\n method: options?.method,\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n })\n .pipe(\n Effect.timeoutOrElse({\n duration: options?.timeout ?? DEFAULT_TIMEOUT,\n orElse: () => Effect.fail(new FetchTimeoutError({ url })),\n }),\n ),\n );\n\nexport const fetchJsonOrNull = async <T>(\n url: string,\n options?: FetchWithRetryOptions,\n): Promise<T | null> => {\n if (isCacheable(url, options)) {\n const cached = getCache.get(url);\n if (cached && cached.expiresAt > Date.now()) {\n return cached.data as T;\n }\n }\n\n try {\n const res = await Effect.runPromise(defaultHttpFetch.get(url, options));\n const data = (await res.json()) as T;\n if (isCacheable(url, options)) {\n setCached(url, data);\n }\n return data;\n } catch (error) {\n const msg =\n error instanceof FetchNetworkError\n ? `[http] Network error: ${error.url} — ${String(error.cause)}`\n : error instanceof FetchTimeoutError\n ? `[http] Timeout: ${error.url}`\n : error instanceof FetchHttpError\n ? `[http] HTTP ${error.status}: ${error.url}`\n : `[http] Unknown error while fetching ${url}`;\n console.error(msg);\n return null;\n }\n};\n\nexport const fetchWithRetryEff = (\n url: string,\n options?: FetchWithRetryOptions,\n): Effect.Effect<Response, FetchError> => defaultHttpFetch.get(url, options);\n"],"mappings":"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"}
|
package/dist/infra/planner.cjs
CHANGED
|
@@ -273,13 +273,17 @@ function planInfra(input) {
|
|
|
273
273
|
return effect.Effect.gen(function* () {
|
|
274
274
|
const cliPorts = normalizeCliPorts(input.cli);
|
|
275
275
|
const wKey = workspaceKey(input.configDir);
|
|
276
|
-
const
|
|
276
|
+
const plugins = input.bosConfig.plugins ?? {};
|
|
277
|
+
const { ports: svcPorts, claims, devPortsState } = yield* allocateServices(cliPorts, plugins, input.configDir, input.bosConfig.auth, {
|
|
277
278
|
host: input.bosConfig.host?.source,
|
|
278
279
|
api: input.bosConfig.api?.source,
|
|
279
280
|
auth: input.bosConfig.auth?.source,
|
|
280
281
|
ui: input.bosConfig.ui?.source
|
|
281
282
|
});
|
|
282
|
-
const { dbs, redisPlans } = yield* allocateDatabases(input.bosConfig)
|
|
283
|
+
const { dbs, redisPlans } = input.bosConfig.host?.source === "local" || input.bosConfig.api?.source === "local" || input.bosConfig.auth?.source === "local" || input.bosConfig.ui?.source === "local" || Object.values(plugins).some((p) => p?.source === "local" || p?.ui?.source === "local") ? yield* allocateDatabases(input.bosConfig) : {
|
|
284
|
+
dbs: [],
|
|
285
|
+
redisPlans: []
|
|
286
|
+
};
|
|
283
287
|
const resolvedPorts = { ...svcPorts };
|
|
284
288
|
const previousDevPorts = require_infra.loadPortState(input.configDir).devPorts ?? {};
|
|
285
289
|
const devPorts = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner.cjs","names":["createHash","resolve","Effect","loadPortState","PortAllocator","isAuthMirrorPluginEntry","ownerOfPort","Clock","InfraError","getSecretGroups","buildConventionalDatabases","buildConventionalRedis","shouldPersistPortState","buildDescription"],"sources":["../../src/infra/planner.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { resolve } from \"node:path\";\nimport { Clock, Effect } from \"effect\";\nimport { PortAllocator, type PortBlockEntry } from \"../app\";\nimport {\n buildConventionalDatabases,\n buildConventionalRedis,\n getSecretGroups,\n loadPortState,\n savePortState,\n} from \"../cli/infra\";\nimport {\n type AuthSlotShape,\n buildDescription,\n isAuthMirrorPluginEntry,\n} from \"../service-descriptor\";\nimport type { RuntimeConfig } from \"../types\";\nimport { shouldPersistPortState } from \"./materializer\";\nimport { ownerOfPort } from \"./port-ownership\";\nimport type {\n ClaimRecord,\n CliPorts,\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;\n\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 pluginsStart: input.pluginPortStart,\n plugins: input.plugins,\n };\n}\n\ninterface AllocateServicesResult {\n ports: ResolvedPorts;\n claims: ClaimRecord[];\n devPortsState: {\n host?: number;\n api?: number;\n auth?: number;\n ui?: number;\n pluginPortStart?: number;\n };\n}\n\nexport interface ServiceSources {\n host?: \"local\" | \"remote\";\n api?: \"local\" | \"remote\";\n auth?: \"local\" | \"remote\";\n ui?: \"local\" | \"remote\";\n}\n\nconst PLUGIN_START_OFFSET = 10;\nconst BLOCK_STEP = 100;\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 auth?: AuthSlotShape,\n sources?: ServiceSources,\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 base = cliPorts.host ?? persisted?.host ?? DEFAULT_HOST_PORT;\n const pluginStartPreferred =\n cliPorts.pluginsStart ?? persisted?.pluginPortStart ?? base + PLUGIN_START_OFFSET;\n const pluginStartPinned =\n cliPorts.pluginsStart !== undefined || persisted?.pluginPortStart !== undefined;\n\n const entries: PortBlockEntry[] = [];\n const addEntry = (\n key: string,\n flag: number | undefined,\n persistedValue: number | undefined,\n derived: number,\n ) => {\n if (flag !== undefined) {\n entries.push({ key, preferred: flag, pinned: true });\n } else if (persistedValue !== undefined) {\n entries.push({ key, preferred: persistedValue, pinned: true });\n } else {\n entries.push({ key, preferred: derived, pinned: false });\n }\n };\n\n // Remote-source services spawn nothing locally (ADR 0009 start stack:\n // remotes load over MF from their published origins) — no port is\n // allocated for them. The host is the exception: even when loaded as a\n // remote module, runServer binds its own port.\n addEntry(\"host\", cliPorts.host, undefined, base);\n if (sources?.api !== \"remote\") {\n addEntry(\"api\", cliPorts.api, persisted?.api, base + 1);\n }\n if (sources?.auth !== \"remote\") {\n addEntry(\"auth\", cliPorts.auth, persisted?.auth, base + 2);\n }\n if (sources?.ui !== \"remote\") {\n addEntry(\"ui\", cliPorts.ui, persisted?.ui, base + 3);\n }\n\n const pluginKeys = Object.keys(plugins).sort();\n const isAuthMirrorFor = (\n pluginId: string,\n pluginCfg: { source: string; localPath?: string } | undefined,\n ) => pluginCfg !== undefined && isAuthMirrorPluginEntry(auth, pluginId, pluginCfg);\n\n let pluginSlot = 0;\n for (const pluginId of pluginKeys) {\n const pluginCfg = plugins[pluginId];\n const pluginIsLocal = pluginCfg?.source === \"local\";\n const slotBase = pluginStartPinned ? pluginStartPreferred : base + PLUGIN_START_OFFSET;\n // The auth mirror's backend is owned by the `auth` app slot — no api\n // port. Its ui surface is still real: without a port the runtime config\n // advertises an empty ui.url and the browser can never load the remote\n // (the /login page never renders).\n if (!isAuthMirrorFor(pluginId, pluginCfg) && pluginIsLocal) {\n addEntry(\n `plugin:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.api,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n if (\n pluginCfg?.source === \"local\" &&\n pluginCfg?.localPath &&\n pluginCfg?.ui?.source === \"local\"\n ) {\n addEntry(\n `plugin-ui:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.ui,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n }\n\n const allocation = yield* allocator.acquireBlock({ base, step: BLOCK_STEP, entries });\n\n if (allocation.base !== base) {\n yield* Effect.logError(\n `[Dev] Preferred port block starting at ${base} is occupied — using ${allocation.base} (this run only; restarts will retry ${base})`,\n );\n for (const conflict of allocation.conflicts.slice(0, 6)) {\n if (conflict.claimed) {\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — live bos session (registry claim)`,\n );\n } else {\n const owner = yield* ownerOfPort(conflict.port);\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — ${\n owner ? `pid ${owner.pid} (${owner.command})` : \"foreign holder (lsof unavailable)\"\n }`,\n );\n }\n }\n }\n\n const pluginApiPorts: Record<string, number> = {};\n const pluginUiPorts: Record<string, number> = {};\n for (const pluginId of pluginKeys) {\n const api = allocation.ports[`plugin:${pluginId}`];\n if (api !== undefined) pluginApiPorts[pluginId] = api;\n const ui = allocation.ports[`plugin-ui:${pluginId}`];\n if (ui !== undefined) pluginUiPorts[pluginId] = ui;\n }\n\n const resolved: ResolvedPorts = {\n host: allocation.ports.host,\n api: allocation.ports.api,\n auth: allocation.ports.auth,\n ui: allocation.ports.ui,\n plugins: Object.fromEntries(\n pluginKeys.map((k) => [k, { api: pluginApiPorts[k], ui: pluginUiPorts[k] }]),\n ),\n };\n\n const devPortsState = {\n host: cliPorts.host,\n api: cliPorts.api,\n auth: cliPorts.auth,\n ui: cliPorts.ui,\n pluginPortStart: cliPorts.pluginsStart,\n };\n\n const claimPorts: Record<string, number> = {};\n for (const [key, port] of Object.entries(allocation.ports)) {\n claimPorts[key] = port;\n }\n\n const claim: ClaimRecord = {\n resourceKey: `workspace:${wKey}`,\n pid: process.pid,\n configDir,\n ports: claimPorts,\n startedAt: yield* Clock.currentTimeMillis,\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(runtimeConfig: RuntimeConfig): Effect.Effect<\n {\n dbs: DatabasePlan[];\n redisPlans: RedisPlan[];\n },\n InfraError,\n PortAllocator\n> {\n return Effect.gen(function* () {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const allocator = yield* PortAllocator;\n\n const conventionalDatabases = buildConventionalDatabases(allSecrets);\n const conventionalRedis = buildConventionalRedis(allSecrets);\n\n const dbs: DatabasePlan[] = [];\n const allocatedByDesired = new Map<number, number>();\n for (const db of conventionalDatabases) {\n let port: number;\n if (allocatedByDesired.has(db.port)) {\n port = allocatedByDesired.get(db.port)!;\n } else {\n port = yield* allocator.pickAvailable(db.port);\n allocatedByDesired.set(db.port, port);\n }\n dbs.push({\n secret: db.secret,\n slug: db.slug,\n port,\n dbName: db.databaseName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${db.databaseName}`,\n });\n }\n\n const redisPlans: RedisPlan[] = [];\n for (const r of conventionalRedis) {\n const port = yield* allocator.pickAvailable(r.port);\n redisPlans.push({\n secret: r.secret,\n slug: r.slug,\n port,\n url: `redis://localhost:${port}`,\n });\n }\n\n return { 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 }\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 const isAuthMirror = isAuthMirrorPluginEntry(runtimeConfig.auth, pluginId, pluginCfg);\n if (pluginIsLocal && p?.api && !isAuthMirror) {\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 && pluginCfg.url && !isAuthMirror) {\n descriptors.push({\n key: `plugin:${pluginId}`,\n source: \"remote\",\n url: pluginCfg.url,\n port: undefined,\n localPath: undefined,\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 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 // The host origin for plugins that derive absolute URLs from it (e.g. the\n // auth plugin's Better Auth baseURL — email links, passkey RP id).\n env.BASE_URL = `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, input.bosConfig.auth, {\n host: input.bosConfig.host?.source,\n api: input.bosConfig.api?.source,\n auth: input.bosConfig.auth?.source,\n ui: input.bosConfig.ui?.source,\n });\n\n const { dbs, redisPlans } = yield* allocateDatabases(input.bosConfig);\n\n const resolvedPorts: ResolvedPorts = { ...svcPorts };\n\n // Write merged state once after all allocations succeed\n // Skip persistence for regression tests / ephemeral runs\n // devPorts pins are explicit choices only (ADR 0012 §4): this run's flags,\n // falling back to previously-persisted explicit choices — never drift.\n const previousDevPorts = loadPortState(input.configDir).devPorts ?? {};\n const devPorts = {\n host: devPortsState.host ?? previousDevPorts.host,\n api: devPortsState.api ?? previousDevPorts.api,\n auth: devPortsState.auth ?? previousDevPorts.auth,\n ui: devPortsState.ui ?? previousDevPorts.ui,\n pluginPortStart: devPortsState.pluginPortStart ?? previousDevPorts.pluginPortStart,\n };\n if (shouldPersistPortState()) {\n savePortState(input.configDir, { devPorts });\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 ssrUrl: undefined,\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 isAuthMirror = isAuthMirrorPluginEntry(input.bosConfig.auth, id, p);\n const pluginPort = resolvedPorts.plugins[id];\n const patchedUi = (() => {\n if (p.ui?.source !== \"local\" || !p.ui?.localPath || !pluginPort?.ui) return p.ui;\n return {\n ...p.ui,\n port: pluginPort.ui,\n url: `http://localhost:${pluginPort.ui}`,\n ssrUrl: undefined,\n };\n })();\n if (isAuthMirror) {\n return [id, { ...p, ui: patchedUi }];\n }\n if (p.source === \"local\" && pluginPort?.api) {\n return [\n id,\n {\n ...p,\n port: pluginPort.api,\n url: `http://localhost:${pluginPort.api}`,\n ui: patchedUi,\n },\n ];\n }\n return [id, p];\n }),\n )\n : undefined,\n };\n\n const serviceDescriptors = buildServiceDescriptors(assignedRuntimeConfig, resolvedPorts);\n\n const launch = buildLaunchSpec(input.bosConfig, resolvedPorts);\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 claims,\n orchestrator,\n };\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"planner.cjs","names":["createHash","resolve","Effect","loadPortState","PortAllocator","isAuthMirrorPluginEntry","ownerOfPort","Clock","InfraError","getSecretGroups","buildConventionalDatabases","buildConventionalRedis","shouldPersistPortState","buildDescription"],"sources":["../../src/infra/planner.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { resolve } from \"node:path\";\nimport { Clock, Effect } from \"effect\";\nimport { PortAllocator, type PortBlockEntry } from \"../app\";\nimport {\n buildConventionalDatabases,\n buildConventionalRedis,\n getSecretGroups,\n loadPortState,\n savePortState,\n} from \"../cli/infra\";\nimport {\n type AuthSlotShape,\n buildDescription,\n isAuthMirrorPluginEntry,\n} from \"../service-descriptor\";\nimport type { RuntimeConfig } from \"../types\";\nimport { shouldPersistPortState } from \"./materializer\";\nimport { ownerOfPort } from \"./port-ownership\";\nimport type {\n ClaimRecord,\n CliPorts,\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;\n\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 pluginsStart: input.pluginPortStart,\n plugins: input.plugins,\n };\n}\n\ninterface AllocateServicesResult {\n ports: ResolvedPorts;\n claims: ClaimRecord[];\n devPortsState: {\n host?: number;\n api?: number;\n auth?: number;\n ui?: number;\n pluginPortStart?: number;\n };\n}\n\nexport interface ServiceSources {\n host?: \"local\" | \"remote\";\n api?: \"local\" | \"remote\";\n auth?: \"local\" | \"remote\";\n ui?: \"local\" | \"remote\";\n}\n\nconst PLUGIN_START_OFFSET = 10;\nconst BLOCK_STEP = 100;\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 auth?: AuthSlotShape,\n sources?: ServiceSources,\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 base = cliPorts.host ?? persisted?.host ?? DEFAULT_HOST_PORT;\n const pluginStartPreferred =\n cliPorts.pluginsStart ?? persisted?.pluginPortStart ?? base + PLUGIN_START_OFFSET;\n const pluginStartPinned =\n cliPorts.pluginsStart !== undefined || persisted?.pluginPortStart !== undefined;\n\n const entries: PortBlockEntry[] = [];\n const addEntry = (\n key: string,\n flag: number | undefined,\n persistedValue: number | undefined,\n derived: number,\n ) => {\n if (flag !== undefined) {\n entries.push({ key, preferred: flag, pinned: true });\n } else if (persistedValue !== undefined) {\n entries.push({ key, preferred: persistedValue, pinned: true });\n } else {\n entries.push({ key, preferred: derived, pinned: false });\n }\n };\n\n // Remote-source services spawn nothing locally (ADR 0009 start stack:\n // remotes load over MF from their published origins) — no port is\n // allocated for them. The host is the exception: even when loaded as a\n // remote module, runServer binds its own port.\n addEntry(\"host\", cliPorts.host, undefined, base);\n if (sources?.api !== \"remote\") {\n addEntry(\"api\", cliPorts.api, persisted?.api, base + 1);\n }\n if (sources?.auth !== \"remote\") {\n addEntry(\"auth\", cliPorts.auth, persisted?.auth, base + 2);\n }\n if (sources?.ui !== \"remote\") {\n addEntry(\"ui\", cliPorts.ui, persisted?.ui, base + 3);\n }\n\n const pluginKeys = Object.keys(plugins).sort();\n const isAuthMirrorFor = (\n pluginId: string,\n pluginCfg: { source: string; localPath?: string } | undefined,\n ) => pluginCfg !== undefined && isAuthMirrorPluginEntry(auth, pluginId, pluginCfg);\n\n let pluginSlot = 0;\n for (const pluginId of pluginKeys) {\n const pluginCfg = plugins[pluginId];\n const pluginIsLocal = pluginCfg?.source === \"local\";\n const slotBase = pluginStartPinned ? pluginStartPreferred : base + PLUGIN_START_OFFSET;\n // The auth mirror's backend is owned by the `auth` app slot — no api\n // port. Its ui surface is still real: without a port the runtime config\n // advertises an empty ui.url and the browser can never load the remote\n // (the /login page never renders).\n if (!isAuthMirrorFor(pluginId, pluginCfg) && pluginIsLocal) {\n addEntry(\n `plugin:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.api,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n if (\n pluginCfg?.source === \"local\" &&\n pluginCfg?.localPath &&\n pluginCfg?.ui?.source === \"local\"\n ) {\n addEntry(\n `plugin-ui:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.ui,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n }\n\n const allocation = yield* allocator.acquireBlock({ base, step: BLOCK_STEP, entries });\n\n if (allocation.base !== base) {\n yield* Effect.logError(\n `[Dev] Preferred port block starting at ${base} is occupied — using ${allocation.base} (this run only; restarts will retry ${base})`,\n );\n for (const conflict of allocation.conflicts.slice(0, 6)) {\n if (conflict.claimed) {\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — live bos session (registry claim)`,\n );\n } else {\n const owner = yield* ownerOfPort(conflict.port);\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — ${\n owner ? `pid ${owner.pid} (${owner.command})` : \"foreign holder (lsof unavailable)\"\n }`,\n );\n }\n }\n }\n\n const pluginApiPorts: Record<string, number> = {};\n const pluginUiPorts: Record<string, number> = {};\n for (const pluginId of pluginKeys) {\n const api = allocation.ports[`plugin:${pluginId}`];\n if (api !== undefined) pluginApiPorts[pluginId] = api;\n const ui = allocation.ports[`plugin-ui:${pluginId}`];\n if (ui !== undefined) pluginUiPorts[pluginId] = ui;\n }\n\n const resolved: ResolvedPorts = {\n host: allocation.ports.host,\n api: allocation.ports.api,\n auth: allocation.ports.auth,\n ui: allocation.ports.ui,\n plugins: Object.fromEntries(\n pluginKeys.map((k) => [k, { api: pluginApiPorts[k], ui: pluginUiPorts[k] }]),\n ),\n };\n\n const devPortsState = {\n host: cliPorts.host,\n api: cliPorts.api,\n auth: cliPorts.auth,\n ui: cliPorts.ui,\n pluginPortStart: cliPorts.pluginsStart,\n };\n\n const claimPorts: Record<string, number> = {};\n for (const [key, port] of Object.entries(allocation.ports)) {\n claimPorts[key] = port;\n }\n\n const claim: ClaimRecord = {\n resourceKey: `workspace:${wKey}`,\n pid: process.pid,\n configDir,\n ports: claimPorts,\n startedAt: yield* Clock.currentTimeMillis,\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(runtimeConfig: RuntimeConfig): Effect.Effect<\n {\n dbs: DatabasePlan[];\n redisPlans: RedisPlan[];\n },\n InfraError,\n PortAllocator\n> {\n return Effect.gen(function* () {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const allocator = yield* PortAllocator;\n\n const conventionalDatabases = buildConventionalDatabases(allSecrets);\n const conventionalRedis = buildConventionalRedis(allSecrets);\n\n const dbs: DatabasePlan[] = [];\n const allocatedByDesired = new Map<number, number>();\n for (const db of conventionalDatabases) {\n let port: number;\n if (allocatedByDesired.has(db.port)) {\n port = allocatedByDesired.get(db.port)!;\n } else {\n port = yield* allocator.pickAvailable(db.port);\n allocatedByDesired.set(db.port, port);\n }\n dbs.push({\n secret: db.secret,\n slug: db.slug,\n port,\n dbName: db.databaseName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${db.databaseName}`,\n });\n }\n\n const redisPlans: RedisPlan[] = [];\n for (const r of conventionalRedis) {\n const port = yield* allocator.pickAvailable(r.port);\n redisPlans.push({\n secret: r.secret,\n slug: r.slug,\n port,\n url: `redis://localhost:${port}`,\n });\n }\n\n return { 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 }\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 const isAuthMirror = isAuthMirrorPluginEntry(runtimeConfig.auth, pluginId, pluginCfg);\n if (pluginIsLocal && p?.api && !isAuthMirror) {\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 && pluginCfg.url && !isAuthMirror) {\n descriptors.push({\n key: `plugin:${pluginId}`,\n source: \"remote\",\n url: pluginCfg.url,\n port: undefined,\n localPath: undefined,\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 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 // The host origin for plugins that derive absolute URLs from it (e.g. the\n // auth plugin's Better Auth baseURL — email links, passkey RP id).\n env.BASE_URL = `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, input.bosConfig.auth, {\n host: input.bosConfig.host?.source,\n api: input.bosConfig.api?.source,\n auth: input.bosConfig.auth?.source,\n ui: input.bosConfig.ui?.source,\n });\n\n // DB/Redis port probes only matter for services that will actually spawn\n // locally — their generated URLs are consumed exclusively by local spawn\n // env (composeSpawnEnv). When every source is remote (start mode, ADR\n // 0009), nothing binds those ports and the probes are wasted work in the\n // container boot path — and a probe against an occupied port (the staged\n // dist servers) can drift the whole plan (the #363 failure class).\n const hasLocalServices =\n input.bosConfig.host?.source === \"local\" ||\n input.bosConfig.api?.source === \"local\" ||\n input.bosConfig.auth?.source === \"local\" ||\n input.bosConfig.ui?.source === \"local\" ||\n Object.values(plugins).some((p) => p?.source === \"local\" || p?.ui?.source === \"local\");\n\n const { dbs, redisPlans } = hasLocalServices\n ? yield* allocateDatabases(input.bosConfig)\n : { dbs: [], redisPlans: [] };\n\n const resolvedPorts: ResolvedPorts = { ...svcPorts };\n\n // Write merged state once after all allocations succeed\n // Skip persistence for regression tests / ephemeral runs\n // devPorts pins are explicit choices only (ADR 0012 §4): this run's flags,\n // falling back to previously-persisted explicit choices — never drift.\n const previousDevPorts = loadPortState(input.configDir).devPorts ?? {};\n const devPorts = {\n host: devPortsState.host ?? previousDevPorts.host,\n api: devPortsState.api ?? previousDevPorts.api,\n auth: devPortsState.auth ?? previousDevPorts.auth,\n ui: devPortsState.ui ?? previousDevPorts.ui,\n pluginPortStart: devPortsState.pluginPortStart ?? previousDevPorts.pluginPortStart,\n };\n if (shouldPersistPortState()) {\n savePortState(input.configDir, { devPorts });\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 ssrUrl: undefined,\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 isAuthMirror = isAuthMirrorPluginEntry(input.bosConfig.auth, id, p);\n const pluginPort = resolvedPorts.plugins[id];\n const patchedUi = (() => {\n if (p.ui?.source !== \"local\" || !p.ui?.localPath || !pluginPort?.ui) return p.ui;\n return {\n ...p.ui,\n port: pluginPort.ui,\n url: `http://localhost:${pluginPort.ui}`,\n ssrUrl: undefined,\n };\n })();\n if (isAuthMirror) {\n return [id, { ...p, ui: patchedUi }];\n }\n if (p.source === \"local\" && pluginPort?.api) {\n return [\n id,\n {\n ...p,\n port: pluginPort.api,\n url: `http://localhost:${pluginPort.api}`,\n ui: patchedUi,\n },\n ];\n }\n return [id, p];\n }),\n )\n : undefined,\n };\n\n const serviceDescriptors = buildServiceDescriptors(assignedRuntimeConfig, resolvedPorts);\n\n const launch = buildLaunchSpec(input.bosConfig, resolvedPorts);\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 claims,\n orchestrator,\n };\n });\n}\n"],"mappings":"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"}
|
package/dist/infra/planner.mjs
CHANGED
|
@@ -273,13 +273,17 @@ function planInfra(input) {
|
|
|
273
273
|
return Effect.gen(function* () {
|
|
274
274
|
const cliPorts = normalizeCliPorts(input.cli);
|
|
275
275
|
const wKey = workspaceKey(input.configDir);
|
|
276
|
-
const
|
|
276
|
+
const plugins = input.bosConfig.plugins ?? {};
|
|
277
|
+
const { ports: svcPorts, claims, devPortsState } = yield* allocateServices(cliPorts, plugins, input.configDir, input.bosConfig.auth, {
|
|
277
278
|
host: input.bosConfig.host?.source,
|
|
278
279
|
api: input.bosConfig.api?.source,
|
|
279
280
|
auth: input.bosConfig.auth?.source,
|
|
280
281
|
ui: input.bosConfig.ui?.source
|
|
281
282
|
});
|
|
282
|
-
const { dbs, redisPlans } = yield* allocateDatabases(input.bosConfig)
|
|
283
|
+
const { dbs, redisPlans } = input.bosConfig.host?.source === "local" || input.bosConfig.api?.source === "local" || input.bosConfig.auth?.source === "local" || input.bosConfig.ui?.source === "local" || Object.values(plugins).some((p) => p?.source === "local" || p?.ui?.source === "local") ? yield* allocateDatabases(input.bosConfig) : {
|
|
284
|
+
dbs: [],
|
|
285
|
+
redisPlans: []
|
|
286
|
+
};
|
|
283
287
|
const resolvedPorts = { ...svcPorts };
|
|
284
288
|
const previousDevPorts = loadPortState(input.configDir).devPorts ?? {};
|
|
285
289
|
const devPorts = {
|
|
@@ -1 +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 { Clock, Effect } from \"effect\";\nimport { PortAllocator, type PortBlockEntry } from \"../app\";\nimport {\n buildConventionalDatabases,\n buildConventionalRedis,\n getSecretGroups,\n loadPortState,\n savePortState,\n} from \"../cli/infra\";\nimport {\n type AuthSlotShape,\n buildDescription,\n isAuthMirrorPluginEntry,\n} from \"../service-descriptor\";\nimport type { RuntimeConfig } from \"../types\";\nimport { shouldPersistPortState } from \"./materializer\";\nimport { ownerOfPort } from \"./port-ownership\";\nimport type {\n ClaimRecord,\n CliPorts,\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;\n\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 pluginsStart: input.pluginPortStart,\n plugins: input.plugins,\n };\n}\n\ninterface AllocateServicesResult {\n ports: ResolvedPorts;\n claims: ClaimRecord[];\n devPortsState: {\n host?: number;\n api?: number;\n auth?: number;\n ui?: number;\n pluginPortStart?: number;\n };\n}\n\nexport interface ServiceSources {\n host?: \"local\" | \"remote\";\n api?: \"local\" | \"remote\";\n auth?: \"local\" | \"remote\";\n ui?: \"local\" | \"remote\";\n}\n\nconst PLUGIN_START_OFFSET = 10;\nconst BLOCK_STEP = 100;\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 auth?: AuthSlotShape,\n sources?: ServiceSources,\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 base = cliPorts.host ?? persisted?.host ?? DEFAULT_HOST_PORT;\n const pluginStartPreferred =\n cliPorts.pluginsStart ?? persisted?.pluginPortStart ?? base + PLUGIN_START_OFFSET;\n const pluginStartPinned =\n cliPorts.pluginsStart !== undefined || persisted?.pluginPortStart !== undefined;\n\n const entries: PortBlockEntry[] = [];\n const addEntry = (\n key: string,\n flag: number | undefined,\n persistedValue: number | undefined,\n derived: number,\n ) => {\n if (flag !== undefined) {\n entries.push({ key, preferred: flag, pinned: true });\n } else if (persistedValue !== undefined) {\n entries.push({ key, preferred: persistedValue, pinned: true });\n } else {\n entries.push({ key, preferred: derived, pinned: false });\n }\n };\n\n // Remote-source services spawn nothing locally (ADR 0009 start stack:\n // remotes load over MF from their published origins) — no port is\n // allocated for them. The host is the exception: even when loaded as a\n // remote module, runServer binds its own port.\n addEntry(\"host\", cliPorts.host, undefined, base);\n if (sources?.api !== \"remote\") {\n addEntry(\"api\", cliPorts.api, persisted?.api, base + 1);\n }\n if (sources?.auth !== \"remote\") {\n addEntry(\"auth\", cliPorts.auth, persisted?.auth, base + 2);\n }\n if (sources?.ui !== \"remote\") {\n addEntry(\"ui\", cliPorts.ui, persisted?.ui, base + 3);\n }\n\n const pluginKeys = Object.keys(plugins).sort();\n const isAuthMirrorFor = (\n pluginId: string,\n pluginCfg: { source: string; localPath?: string } | undefined,\n ) => pluginCfg !== undefined && isAuthMirrorPluginEntry(auth, pluginId, pluginCfg);\n\n let pluginSlot = 0;\n for (const pluginId of pluginKeys) {\n const pluginCfg = plugins[pluginId];\n const pluginIsLocal = pluginCfg?.source === \"local\";\n const slotBase = pluginStartPinned ? pluginStartPreferred : base + PLUGIN_START_OFFSET;\n // The auth mirror's backend is owned by the `auth` app slot — no api\n // port. Its ui surface is still real: without a port the runtime config\n // advertises an empty ui.url and the browser can never load the remote\n // (the /login page never renders).\n if (!isAuthMirrorFor(pluginId, pluginCfg) && pluginIsLocal) {\n addEntry(\n `plugin:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.api,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n if (\n pluginCfg?.source === \"local\" &&\n pluginCfg?.localPath &&\n pluginCfg?.ui?.source === \"local\"\n ) {\n addEntry(\n `plugin-ui:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.ui,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n }\n\n const allocation = yield* allocator.acquireBlock({ base, step: BLOCK_STEP, entries });\n\n if (allocation.base !== base) {\n yield* Effect.logError(\n `[Dev] Preferred port block starting at ${base} is occupied — using ${allocation.base} (this run only; restarts will retry ${base})`,\n );\n for (const conflict of allocation.conflicts.slice(0, 6)) {\n if (conflict.claimed) {\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — live bos session (registry claim)`,\n );\n } else {\n const owner = yield* ownerOfPort(conflict.port);\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — ${\n owner ? `pid ${owner.pid} (${owner.command})` : \"foreign holder (lsof unavailable)\"\n }`,\n );\n }\n }\n }\n\n const pluginApiPorts: Record<string, number> = {};\n const pluginUiPorts: Record<string, number> = {};\n for (const pluginId of pluginKeys) {\n const api = allocation.ports[`plugin:${pluginId}`];\n if (api !== undefined) pluginApiPorts[pluginId] = api;\n const ui = allocation.ports[`plugin-ui:${pluginId}`];\n if (ui !== undefined) pluginUiPorts[pluginId] = ui;\n }\n\n const resolved: ResolvedPorts = {\n host: allocation.ports.host,\n api: allocation.ports.api,\n auth: allocation.ports.auth,\n ui: allocation.ports.ui,\n plugins: Object.fromEntries(\n pluginKeys.map((k) => [k, { api: pluginApiPorts[k], ui: pluginUiPorts[k] }]),\n ),\n };\n\n const devPortsState = {\n host: cliPorts.host,\n api: cliPorts.api,\n auth: cliPorts.auth,\n ui: cliPorts.ui,\n pluginPortStart: cliPorts.pluginsStart,\n };\n\n const claimPorts: Record<string, number> = {};\n for (const [key, port] of Object.entries(allocation.ports)) {\n claimPorts[key] = port;\n }\n\n const claim: ClaimRecord = {\n resourceKey: `workspace:${wKey}`,\n pid: process.pid,\n configDir,\n ports: claimPorts,\n startedAt: yield* Clock.currentTimeMillis,\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(runtimeConfig: RuntimeConfig): Effect.Effect<\n {\n dbs: DatabasePlan[];\n redisPlans: RedisPlan[];\n },\n InfraError,\n PortAllocator\n> {\n return Effect.gen(function* () {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const allocator = yield* PortAllocator;\n\n const conventionalDatabases = buildConventionalDatabases(allSecrets);\n const conventionalRedis = buildConventionalRedis(allSecrets);\n\n const dbs: DatabasePlan[] = [];\n const allocatedByDesired = new Map<number, number>();\n for (const db of conventionalDatabases) {\n let port: number;\n if (allocatedByDesired.has(db.port)) {\n port = allocatedByDesired.get(db.port)!;\n } else {\n port = yield* allocator.pickAvailable(db.port);\n allocatedByDesired.set(db.port, port);\n }\n dbs.push({\n secret: db.secret,\n slug: db.slug,\n port,\n dbName: db.databaseName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${db.databaseName}`,\n });\n }\n\n const redisPlans: RedisPlan[] = [];\n for (const r of conventionalRedis) {\n const port = yield* allocator.pickAvailable(r.port);\n redisPlans.push({\n secret: r.secret,\n slug: r.slug,\n port,\n url: `redis://localhost:${port}`,\n });\n }\n\n return { 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 }\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 const isAuthMirror = isAuthMirrorPluginEntry(runtimeConfig.auth, pluginId, pluginCfg);\n if (pluginIsLocal && p?.api && !isAuthMirror) {\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 && pluginCfg.url && !isAuthMirror) {\n descriptors.push({\n key: `plugin:${pluginId}`,\n source: \"remote\",\n url: pluginCfg.url,\n port: undefined,\n localPath: undefined,\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 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 // The host origin for plugins that derive absolute URLs from it (e.g. the\n // auth plugin's Better Auth baseURL — email links, passkey RP id).\n env.BASE_URL = `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, input.bosConfig.auth, {\n host: input.bosConfig.host?.source,\n api: input.bosConfig.api?.source,\n auth: input.bosConfig.auth?.source,\n ui: input.bosConfig.ui?.source,\n });\n\n const { dbs, redisPlans } = yield* allocateDatabases(input.bosConfig);\n\n const resolvedPorts: ResolvedPorts = { ...svcPorts };\n\n // Write merged state once after all allocations succeed\n // Skip persistence for regression tests / ephemeral runs\n // devPorts pins are explicit choices only (ADR 0012 §4): this run's flags,\n // falling back to previously-persisted explicit choices — never drift.\n const previousDevPorts = loadPortState(input.configDir).devPorts ?? {};\n const devPorts = {\n host: devPortsState.host ?? previousDevPorts.host,\n api: devPortsState.api ?? previousDevPorts.api,\n auth: devPortsState.auth ?? previousDevPorts.auth,\n ui: devPortsState.ui ?? previousDevPorts.ui,\n pluginPortStart: devPortsState.pluginPortStart ?? previousDevPorts.pluginPortStart,\n };\n if (shouldPersistPortState()) {\n savePortState(input.configDir, { devPorts });\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 ssrUrl: undefined,\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 isAuthMirror = isAuthMirrorPluginEntry(input.bosConfig.auth, id, p);\n const pluginPort = resolvedPorts.plugins[id];\n const patchedUi = (() => {\n if (p.ui?.source !== \"local\" || !p.ui?.localPath || !pluginPort?.ui) return p.ui;\n return {\n ...p.ui,\n port: pluginPort.ui,\n url: `http://localhost:${pluginPort.ui}`,\n ssrUrl: undefined,\n };\n })();\n if (isAuthMirror) {\n return [id, { ...p, ui: patchedUi }];\n }\n if (p.source === \"local\" && pluginPort?.api) {\n return [\n id,\n {\n ...p,\n port: pluginPort.api,\n url: `http://localhost:${pluginPort.api}`,\n ui: patchedUi,\n },\n ];\n }\n return [id, p];\n }),\n )\n : undefined,\n };\n\n const serviceDescriptors = buildServiceDescriptors(assignedRuntimeConfig, resolvedPorts);\n\n const launch = buildLaunchSpec(input.bosConfig, resolvedPorts);\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 claims,\n orchestrator,\n };\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"planner.mjs","names":[],"sources":["../../src/infra/planner.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { resolve } from \"node:path\";\nimport { Clock, Effect } from \"effect\";\nimport { PortAllocator, type PortBlockEntry } from \"../app\";\nimport {\n buildConventionalDatabases,\n buildConventionalRedis,\n getSecretGroups,\n loadPortState,\n savePortState,\n} from \"../cli/infra\";\nimport {\n type AuthSlotShape,\n buildDescription,\n isAuthMirrorPluginEntry,\n} from \"../service-descriptor\";\nimport type { RuntimeConfig } from \"../types\";\nimport { shouldPersistPortState } from \"./materializer\";\nimport { ownerOfPort } from \"./port-ownership\";\nimport type {\n ClaimRecord,\n CliPorts,\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;\n\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 pluginsStart: input.pluginPortStart,\n plugins: input.plugins,\n };\n}\n\ninterface AllocateServicesResult {\n ports: ResolvedPorts;\n claims: ClaimRecord[];\n devPortsState: {\n host?: number;\n api?: number;\n auth?: number;\n ui?: number;\n pluginPortStart?: number;\n };\n}\n\nexport interface ServiceSources {\n host?: \"local\" | \"remote\";\n api?: \"local\" | \"remote\";\n auth?: \"local\" | \"remote\";\n ui?: \"local\" | \"remote\";\n}\n\nconst PLUGIN_START_OFFSET = 10;\nconst BLOCK_STEP = 100;\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 auth?: AuthSlotShape,\n sources?: ServiceSources,\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 base = cliPorts.host ?? persisted?.host ?? DEFAULT_HOST_PORT;\n const pluginStartPreferred =\n cliPorts.pluginsStart ?? persisted?.pluginPortStart ?? base + PLUGIN_START_OFFSET;\n const pluginStartPinned =\n cliPorts.pluginsStart !== undefined || persisted?.pluginPortStart !== undefined;\n\n const entries: PortBlockEntry[] = [];\n const addEntry = (\n key: string,\n flag: number | undefined,\n persistedValue: number | undefined,\n derived: number,\n ) => {\n if (flag !== undefined) {\n entries.push({ key, preferred: flag, pinned: true });\n } else if (persistedValue !== undefined) {\n entries.push({ key, preferred: persistedValue, pinned: true });\n } else {\n entries.push({ key, preferred: derived, pinned: false });\n }\n };\n\n // Remote-source services spawn nothing locally (ADR 0009 start stack:\n // remotes load over MF from their published origins) — no port is\n // allocated for them. The host is the exception: even when loaded as a\n // remote module, runServer binds its own port.\n addEntry(\"host\", cliPorts.host, undefined, base);\n if (sources?.api !== \"remote\") {\n addEntry(\"api\", cliPorts.api, persisted?.api, base + 1);\n }\n if (sources?.auth !== \"remote\") {\n addEntry(\"auth\", cliPorts.auth, persisted?.auth, base + 2);\n }\n if (sources?.ui !== \"remote\") {\n addEntry(\"ui\", cliPorts.ui, persisted?.ui, base + 3);\n }\n\n const pluginKeys = Object.keys(plugins).sort();\n const isAuthMirrorFor = (\n pluginId: string,\n pluginCfg: { source: string; localPath?: string } | undefined,\n ) => pluginCfg !== undefined && isAuthMirrorPluginEntry(auth, pluginId, pluginCfg);\n\n let pluginSlot = 0;\n for (const pluginId of pluginKeys) {\n const pluginCfg = plugins[pluginId];\n const pluginIsLocal = pluginCfg?.source === \"local\";\n const slotBase = pluginStartPinned ? pluginStartPreferred : base + PLUGIN_START_OFFSET;\n // The auth mirror's backend is owned by the `auth` app slot — no api\n // port. Its ui surface is still real: without a port the runtime config\n // advertises an empty ui.url and the browser can never load the remote\n // (the /login page never renders).\n if (!isAuthMirrorFor(pluginId, pluginCfg) && pluginIsLocal) {\n addEntry(\n `plugin:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.api,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n if (\n pluginCfg?.source === \"local\" &&\n pluginCfg?.localPath &&\n pluginCfg?.ui?.source === \"local\"\n ) {\n addEntry(\n `plugin-ui:${pluginId}`,\n cliPorts.plugins?.[pluginId]?.ui,\n undefined,\n slotBase + pluginSlot,\n );\n pluginSlot += 1;\n }\n }\n\n const allocation = yield* allocator.acquireBlock({ base, step: BLOCK_STEP, entries });\n\n if (allocation.base !== base) {\n yield* Effect.logError(\n `[Dev] Preferred port block starting at ${base} is occupied — using ${allocation.base} (this run only; restarts will retry ${base})`,\n );\n for (const conflict of allocation.conflicts.slice(0, 6)) {\n if (conflict.claimed) {\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — live bos session (registry claim)`,\n );\n } else {\n const owner = yield* ownerOfPort(conflict.port);\n yield* Effect.logError(\n `[Dev] port ${conflict.port} (${conflict.key}) — ${\n owner ? `pid ${owner.pid} (${owner.command})` : \"foreign holder (lsof unavailable)\"\n }`,\n );\n }\n }\n }\n\n const pluginApiPorts: Record<string, number> = {};\n const pluginUiPorts: Record<string, number> = {};\n for (const pluginId of pluginKeys) {\n const api = allocation.ports[`plugin:${pluginId}`];\n if (api !== undefined) pluginApiPorts[pluginId] = api;\n const ui = allocation.ports[`plugin-ui:${pluginId}`];\n if (ui !== undefined) pluginUiPorts[pluginId] = ui;\n }\n\n const resolved: ResolvedPorts = {\n host: allocation.ports.host,\n api: allocation.ports.api,\n auth: allocation.ports.auth,\n ui: allocation.ports.ui,\n plugins: Object.fromEntries(\n pluginKeys.map((k) => [k, { api: pluginApiPorts[k], ui: pluginUiPorts[k] }]),\n ),\n };\n\n const devPortsState = {\n host: cliPorts.host,\n api: cliPorts.api,\n auth: cliPorts.auth,\n ui: cliPorts.ui,\n pluginPortStart: cliPorts.pluginsStart,\n };\n\n const claimPorts: Record<string, number> = {};\n for (const [key, port] of Object.entries(allocation.ports)) {\n claimPorts[key] = port;\n }\n\n const claim: ClaimRecord = {\n resourceKey: `workspace:${wKey}`,\n pid: process.pid,\n configDir,\n ports: claimPorts,\n startedAt: yield* Clock.currentTimeMillis,\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(runtimeConfig: RuntimeConfig): Effect.Effect<\n {\n dbs: DatabasePlan[];\n redisPlans: RedisPlan[];\n },\n InfraError,\n PortAllocator\n> {\n return Effect.gen(function* () {\n const groups = getSecretGroups(runtimeConfig);\n const allSecrets = groups.flatMap((group) => group.secrets);\n const allocator = yield* PortAllocator;\n\n const conventionalDatabases = buildConventionalDatabases(allSecrets);\n const conventionalRedis = buildConventionalRedis(allSecrets);\n\n const dbs: DatabasePlan[] = [];\n const allocatedByDesired = new Map<number, number>();\n for (const db of conventionalDatabases) {\n let port: number;\n if (allocatedByDesired.has(db.port)) {\n port = allocatedByDesired.get(db.port)!;\n } else {\n port = yield* allocator.pickAvailable(db.port);\n allocatedByDesired.set(db.port, port);\n }\n dbs.push({\n secret: db.secret,\n slug: db.slug,\n port,\n dbName: db.databaseName,\n url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${db.databaseName}`,\n });\n }\n\n const redisPlans: RedisPlan[] = [];\n for (const r of conventionalRedis) {\n const port = yield* allocator.pickAvailable(r.port);\n redisPlans.push({\n secret: r.secret,\n slug: r.slug,\n port,\n url: `redis://localhost:${port}`,\n });\n }\n\n return { 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 }\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 const isAuthMirror = isAuthMirrorPluginEntry(runtimeConfig.auth, pluginId, pluginCfg);\n if (pluginIsLocal && p?.api && !isAuthMirror) {\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 && pluginCfg.url && !isAuthMirror) {\n descriptors.push({\n key: `plugin:${pluginId}`,\n source: \"remote\",\n url: pluginCfg.url,\n port: undefined,\n localPath: undefined,\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 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 // The host origin for plugins that derive absolute URLs from it (e.g. the\n // auth plugin's Better Auth baseURL — email links, passkey RP id).\n env.BASE_URL = `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, input.bosConfig.auth, {\n host: input.bosConfig.host?.source,\n api: input.bosConfig.api?.source,\n auth: input.bosConfig.auth?.source,\n ui: input.bosConfig.ui?.source,\n });\n\n // DB/Redis port probes only matter for services that will actually spawn\n // locally — their generated URLs are consumed exclusively by local spawn\n // env (composeSpawnEnv). When every source is remote (start mode, ADR\n // 0009), nothing binds those ports and the probes are wasted work in the\n // container boot path — and a probe against an occupied port (the staged\n // dist servers) can drift the whole plan (the #363 failure class).\n const hasLocalServices =\n input.bosConfig.host?.source === \"local\" ||\n input.bosConfig.api?.source === \"local\" ||\n input.bosConfig.auth?.source === \"local\" ||\n input.bosConfig.ui?.source === \"local\" ||\n Object.values(plugins).some((p) => p?.source === \"local\" || p?.ui?.source === \"local\");\n\n const { dbs, redisPlans } = hasLocalServices\n ? yield* allocateDatabases(input.bosConfig)\n : { dbs: [], redisPlans: [] };\n\n const resolvedPorts: ResolvedPorts = { ...svcPorts };\n\n // Write merged state once after all allocations succeed\n // Skip persistence for regression tests / ephemeral runs\n // devPorts pins are explicit choices only (ADR 0012 §4): this run's flags,\n // falling back to previously-persisted explicit choices — never drift.\n const previousDevPorts = loadPortState(input.configDir).devPorts ?? {};\n const devPorts = {\n host: devPortsState.host ?? previousDevPorts.host,\n api: devPortsState.api ?? previousDevPorts.api,\n auth: devPortsState.auth ?? previousDevPorts.auth,\n ui: devPortsState.ui ?? previousDevPorts.ui,\n pluginPortStart: devPortsState.pluginPortStart ?? previousDevPorts.pluginPortStart,\n };\n if (shouldPersistPortState()) {\n savePortState(input.configDir, { devPorts });\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 ssrUrl: undefined,\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 isAuthMirror = isAuthMirrorPluginEntry(input.bosConfig.auth, id, p);\n const pluginPort = resolvedPorts.plugins[id];\n const patchedUi = (() => {\n if (p.ui?.source !== \"local\" || !p.ui?.localPath || !pluginPort?.ui) return p.ui;\n return {\n ...p.ui,\n port: pluginPort.ui,\n url: `http://localhost:${pluginPort.ui}`,\n ssrUrl: undefined,\n };\n })();\n if (isAuthMirror) {\n return [id, { ...p, ui: patchedUi }];\n }\n if (p.source === \"local\" && pluginPort?.api) {\n return [\n id,\n {\n ...p,\n port: pluginPort.api,\n url: `http://localhost:${pluginPort.api}`,\n ui: patchedUi,\n },\n ];\n }\n return [id, p];\n }),\n )\n : undefined,\n };\n\n const serviceDescriptors = buildServiceDescriptors(assignedRuntimeConfig, resolvedPorts);\n\n const launch = buildLaunchSpec(input.bosConfig, resolvedPorts);\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 claims,\n orchestrator,\n };\n });\n}\n"],"mappings":"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"}
|