{"version":3,"file":"app.mjs","names":["configBuildRuntimeConfig"],"sources":["../src/app.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { createServer } from \"node:net\";\nimport { join } from \"node:path\";\nimport { Context, Data, Effect, Layer } from \"effect\";\nimport type { DevPortState } from \"./cli/infra\";\nimport {\n  buildRuntimeConfig as configBuildRuntimeConfig,\n  getProjectRoot,\n  resolveLocalDevelopmentPath,\n} from \"./config\";\nimport { claimedPorts } from \"./process-registry\";\nimport type { AppOrchestrator } from \"./service-descriptor\";\nimport type { BosConfig, RuntimeConfig, RuntimePluginConfig } from \"./types\";\n\nexport type { AppOrchestrator };\n\nconst DEFAULT_HOST_PORT = 3000;\nconst DEFAULT_API_PORT = 3001;\nconst DEFAULT_AUTH_PORT = 3002;\nconst DEFAULT_UI_PORT = 3003;\nconst DEFAULT_PLUGIN_PORT_START = 3010;\n\nconst PROBE_TIMEOUT_MS = 250;\nconst MAX_PORT_SCAN_STEPS = 1000;\nconst PARALLEL_PROBE_WINDOW = 8;\n\nexport type PortBudget = { min: number; max: number };\n\nexport class PortAllocationError extends Data.TaggedError(\"PortAllocationError\")<{\n  preferred: number;\n  budget?: PortBudget;\n  cause?: unknown;\n}> {}\n\nexport class PortAllocator extends Context.Tag(\"PortAllocator\")<\n  PortAllocator,\n  {\n    pickAvailable: (\n      preferred: number,\n      budget?: PortBudget,\n    ) => Effect.Effect<number, PortAllocationError>;\n  }\n>() {}\n\nexport function detectLocalPackages(\n  bosConfig?: BosConfig,\n  runtimeConfig?: RuntimeConfig,\n): string[] {\n  const packages: string[] = [];\n  const configDir = getProjectRoot();\n\n  const uiLocalPath =\n    runtimeConfig?.ui.localPath ??\n    resolveLocalDevelopmentPath(bosConfig?.app.ui.development, configDir);\n  if (uiLocalPath && existsSync(join(uiLocalPath, \"package.json\"))) {\n    packages.push(\"ui\");\n  }\n\n  const apiLocalPath =\n    runtimeConfig?.api.localPath ??\n    resolveLocalDevelopmentPath(bosConfig?.app.api.development, configDir);\n  if (apiLocalPath && existsSync(join(apiLocalPath, \"package.json\"))) {\n    packages.push(\"api\");\n  }\n\n  const hostLocalPath =\n    runtimeConfig?.host?.localPath ??\n    resolveLocalDevelopmentPath(bosConfig?.app.host.development, configDir);\n  if (hostLocalPath && existsSync(join(hostLocalPath, \"package.json\"))) {\n    packages.push(\"host\");\n  } else if (existsSync(join(configDir, \"host\", \"package.json\"))) {\n    packages.push(\"host\");\n  }\n\n  for (const [pluginId, pluginConfig] of Object.entries(runtimeConfig?.plugins ?? {})) {\n    if (pluginConfig.localPath && existsSync(join(pluginConfig.localPath, \"package.json\"))) {\n      packages.push(`plugin:${pluginId}`);\n    }\n    if (pluginConfig.ui?.localPath && existsSync(join(pluginConfig.ui.localPath, \"package.json\"))) {\n      packages.push(`plugin-ui:${pluginId}`);\n    }\n  }\n\n  const authLocalPath =\n    runtimeConfig?.auth?.localPath ??\n    resolveLocalDevelopmentPath(bosConfig?.app.auth?.development, configDir);\n  if (authLocalPath && existsSync(join(authLocalPath, \"package.json\"))) {\n    packages.push(\"auth\");\n  }\n\n  return packages;\n}\n\nexport function buildRuntimeConfig(\n  bosConfig: BosConfig,\n  options: {\n    hostSource?: \"local\" | \"remote\";\n    uiSource?: \"local\" | \"remote\";\n    apiSource?: \"local\" | \"remote\";\n    authSource?: \"local\" | \"remote\";\n    proxy?: string;\n    env?: \"development\" | \"production\";\n    plugins?: Record<string, RuntimePluginConfig>;\n  },\n): RuntimeConfig {\n  return configBuildRuntimeConfig(bosConfig, getProjectRoot(), options.env ?? \"development\", {\n    hostSource: options.hostSource,\n    uiSource: options.uiSource,\n    apiSource: options.apiSource,\n    authSource: options.authSource,\n    proxy: options.proxy,\n    plugins: options.plugins,\n  });\n}\n\nfunction probePortBindable(port: number): Effect.Effect<boolean> {\n  return Effect.async<boolean>((resume) => {\n    const server = createServer();\n\n    server.once(\"listening\", () => {\n      server.close(() => {\n        resume(Effect.succeed(true));\n      });\n    });\n\n    server.once(\"error\", () => {\n      server.removeAllListeners();\n      // EADDRINUSE, EACCES, or any other bind error → not available\n      resume(Effect.succeed(false));\n    });\n\n    server.listen(port, \"127.0.0.1\");\n\n    const timer = setTimeout(() => {\n      server.removeAllListeners();\n      try {\n        server.close();\n      } catch {\n        // ignore\n      }\n      resume(Effect.succeed(false));\n    }, PROBE_TIMEOUT_MS);\n\n    server.once(\"listening\", () => clearTimeout(timer));\n    server.once(\"error\", () => clearTimeout(timer));\n  });\n}\n\nfunction pickAvailablePort(\n  preferred: number,\n  usedPorts: Set<number>,\n  budget?: PortBudget,\n): Effect.Effect<number, PortAllocationError> {\n  return Effect.gen(function* () {\n    const within = (candidate: number): boolean =>\n      !budget || (candidate >= budget.min && candidate <= budget.max);\n\n    let port = preferred;\n    if (!within(port)) {\n      port = budget ? budget.min : port;\n    }\n\n    const ceiling = budget ? budget.max + 1 : Number.MAX_SAFE_INTEGER;\n    let steps = 0;\n\n    const fail = () =>\n      Effect.fail(\n        new PortAllocationError({\n          preferred,\n          budget,\n          cause: budget\n            ? `No free port in budget [${budget.min}, ${budget.max}] starting from ${preferred}`\n            : `No free port found starting from ${preferred} within ${MAX_PORT_SCAN_STEPS} steps`,\n        }),\n      );\n\n    while (true) {\n      if (port >= ceiling || steps > MAX_PORT_SCAN_STEPS) {\n        yield* fail();\n      }\n\n      const candidates: number[] = [];\n      for (let i = 0; i < PARALLEL_PROBE_WINDOW && port + i < ceiling; i++) {\n        const candidate = port + i;\n        if (!usedPorts.has(candidate)) {\n          candidates.push(candidate);\n        }\n      }\n\n      if (candidates.length === 0) {\n        port += PARALLEL_PROBE_WINDOW;\n        steps += PARALLEL_PROBE_WINDOW;\n        continue;\n      }\n\n      const results = yield* Effect.forEach(\n        candidates,\n        (c) => probePortBindable(c).pipe(Effect.map((free) => ({ port: c, free }))),\n        { concurrency: \"unbounded\" },\n      );\n\n      const firstFree = results.find((r) => r.free);\n      if (firstFree) {\n        usedPorts.add(firstFree.port);\n        return firstFree.port;\n      }\n\n      port += PARALLEL_PROBE_WINDOW;\n      steps += PARALLEL_PROBE_WINDOW;\n    }\n  });\n}\n\nexport const PortAllocatorLive: Layer.Layer<PortAllocator> = Layer.sync(PortAllocator, () => {\n  const usedPorts = claimedPorts();\n  return {\n    pickAvailable: (preferred, budget) => pickAvailablePort(preferred, usedPorts, budget),\n  };\n});\n\nfunction withLocalRuntimeUrl<\n  T extends { url: string; entry: string; port?: number; localPath?: string },\n>(entry: T, port: number): T {\n  const url = `http://localhost:${port}`;\n  return {\n    ...entry,\n    url,\n    entry: `${url}/mf-manifest.json`,\n    port,\n  };\n}\n\nexport interface DevPortOptions {\n  hostPort?: number;\n  apiPort?: number;\n  uiPort?: number;\n  authPort?: number;\n  pluginPortStart?: number;\n  ssr?: boolean;\n  portBudget?: PortBudget;\n}\n\nexport interface PreparedDevRuntime {\n  runtimeConfig: RuntimeConfig;\n  devPorts: DevPortState;\n}\n\nexport function prepareDevelopmentRuntimeConfig(\n  runtimeConfig: RuntimeConfig,\n  options?: DevPortOptions,\n): Effect.Effect<PreparedDevRuntime, PortAllocationError, PortAllocator> {\n  return Effect.gen(function* () {\n    const allocator = yield* PortAllocator;\n    const budget = options?.portBudget;\n\n    const pickedHostPort = yield* allocator.pickAvailable(\n      options?.hostPort ?? DEFAULT_HOST_PORT,\n      budget,\n    );\n\n    const hostIsLocal = runtimeConfig.host.source === \"local\";\n    const next: RuntimeConfig = {\n      ...runtimeConfig,\n      host: hostIsLocal\n        ? {\n            ...runtimeConfig.host,\n            url: `http://localhost:${pickedHostPort}`,\n            port: pickedHostPort,\n          }\n        : { ...runtimeConfig.host },\n      ui: { ...runtimeConfig.ui },\n      api: { ...runtimeConfig.api },\n      auth: runtimeConfig.auth ? { ...runtimeConfig.auth } : undefined,\n      plugins: runtimeConfig.plugins ? { ...runtimeConfig.plugins } : undefined,\n    };\n\n    const devPorts: DevPortState = {\n      host: hostIsLocal ? pickedHostPort : undefined,\n      api: undefined,\n      ui: undefined,\n      auth: undefined,\n      pluginPortStart: undefined,\n    };\n\n    if (next.api.source === \"local\" && next.api.localPath) {\n      const apiPort = yield* allocator.pickAvailable(\n        options?.apiPort ?? next.api.port ?? DEFAULT_API_PORT,\n        budget,\n      );\n      next.api = withLocalRuntimeUrl(next.api, apiPort);\n      devPorts.api = apiPort;\n    }\n\n    if (next.auth?.source === \"local\" && next.auth.localPath) {\n      const authPort = yield* allocator.pickAvailable(\n        options?.authPort ?? next.auth.port ?? DEFAULT_AUTH_PORT,\n        budget,\n      );\n      next.auth = withLocalRuntimeUrl(next.auth, authPort);\n      devPorts.auth = authPort;\n    }\n\n    if (next.ui.source === \"local\" && next.ui.localPath) {\n      const uiPort = yield* allocator.pickAvailable(\n        options?.uiPort ?? next.ui.port ?? DEFAULT_UI_PORT,\n        budget,\n      );\n      next.ui = withLocalRuntimeUrl(next.ui, uiPort);\n      devPorts.ui = uiPort;\n      if (options?.ssr) {\n        const ssrPort = yield* allocator.pickAvailable(uiPort + 1, budget);\n        next.ui.ssrUrl = `http://localhost:${ssrPort}`;\n      } else {\n        next.ui.ssrUrl = undefined;\n      }\n    }\n\n    if (next.plugins) {\n      const entries = Object.entries(next.plugins).sort(([a], [b]) => a.localeCompare(b));\n      let pluginBasePort = options?.pluginPortStart ?? DEFAULT_PLUGIN_PORT_START;\n      let firstLocalPluginPort: number | undefined;\n\n      for (const [pluginId, plugin] of entries) {\n        if (plugin.source === \"local\" && plugin.localPath) {\n          const pluginPort = yield* allocator.pickAvailable(plugin.port ?? pluginBasePort, budget);\n          next.plugins[pluginId] = withLocalRuntimeUrl(plugin, pluginPort);\n          if (firstLocalPluginPort === undefined) firstLocalPluginPort = pluginPort;\n          pluginBasePort = pluginPort + 1;\n        }\n\n        if (plugin.ui?.source === \"local\" && plugin.ui.localPath) {\n          const pluginUiPort = yield* allocator.pickAvailable(\n            plugin.ui.port ?? pluginBasePort,\n            budget,\n          );\n          next.plugins[pluginId] = {\n            ...next.plugins[pluginId]!,\n            ui: withLocalRuntimeUrl(plugin.ui, pluginUiPort),\n          };\n          if (firstLocalPluginPort === undefined) firstLocalPluginPort = pluginUiPort;\n          pluginBasePort = pluginUiPort + 1;\n        }\n      }\n\n      devPorts.pluginPortStart = firstLocalPluginPort;\n    }\n\n    return { runtimeConfig: next, devPorts };\n  });\n}\n"],"mappings":"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"}