{"version":3,"file":"docker.cjs","names":["Layer","AlchemyContext","AlchemyContextLive","Effect","Context","PlatformServices","FetchHttpClient","Interaction","ArtifactStore","createArtifactStore","AdoptPolicy","AuthProviders","State","Docker","Stack","Stage","HostingAdapter","HttpClient","Duration","Clock","HttpClientRequest","HealthTimeout","Alchemy","Deploy","DeployFailed","HostingOperationFailed","InstanceNotFound"],"sources":["../../../src/services/hosting/docker.ts"],"sourcesContent":["/**\n * Docker dev-substrate `HostingAdapter` (ADR 0035).\n *\n * Alchemy is the control plane, embedded programmatically: each instance is\n * one Alchemy stack holding a `Docker.Container` resource, evaluated\n * in-process through `alchemy/Deploy`'s `deploy()` — the same plan/apply and\n * state-ledger semantics as `alchemy deploy` without a CLI subprocess. The\n * choice was deliberate: `Deploy.deploy` is a public programmatic API and\n * alchemy's own test harness (`lib/Test/Core.js`) drives stacks exactly this\n * way, so idempotency comes from Alchemy's plan diffing (an unchanged\n * descriptor yields a no-op apply and never recreates the container) and the\n * ledger is Alchemy's local file state store under `stateDir`.\n * Stop/start/inspect go through the same provider's `Docker` service, so\n * every mutation flows through the one control plane — the adapter never\n * shells `docker` itself. Docker is the development/CI substrate only;\n * Railway and Fly adapters implement the same seam.\n *\n * The host port is deliberately ephemeral (`external: 0`): Docker binds a\n * free host port at container creation, the provider reports the resolved\n * mapping in the resource output, and the instance URL derives from it. The\n * binding is stable for the container's lifetime (stop/start preserves it);\n * only a re-create — i.e. a descriptor change — rebinds it.\n */\n\nimport * as Alchemy from \"alchemy\";\nimport { AlchemyContext, AlchemyContextLive, AuthProviders, Stack, Stage } from \"alchemy\";\nimport { AdoptPolicy } from \"alchemy/AdoptPolicy\";\nimport { ArtifactStore, createArtifactStore } from \"alchemy/Artifacts\";\nimport * as Deploy from \"alchemy/Deploy\";\nimport * as Docker from \"alchemy/Docker\";\nimport * as Interaction from \"alchemy/Interaction\";\nimport * as State from \"alchemy/State\";\nimport { PlatformServices } from \"alchemy/Util/PlatformServices\";\nimport { Clock, Context, Duration, Effect, Layer } from \"effect\";\nimport * as FetchHttpClient from \"effect/unstable/http/FetchHttpClient\";\nimport * as HttpClient from \"effect/unstable/http/HttpClient\";\nimport * as HttpClientRequest from \"effect/unstable/http/HttpClientRequest\";\nimport {\n  type DeployedInstance,\n  DeployFailed,\n  HealthTimeout,\n  HostingAdapter,\n  HostingOperationFailed,\n  InstanceNotFound,\n  type InstanceSnapshot,\n  type RuntimeDescriptor,\n} from \"./adapter\";\n\nexport interface DockerHostingOptions {\n  /** Container port the runtime serves HTTP on. */\n  readonly containerPort?: number | undefined;\n  /** Path polled for readiness after deploy/restart. */\n  readonly healthPath?: string | undefined;\n  /** Bounded window for the readiness poll. */\n  readonly healthTimeout?: Duration.Input | undefined;\n  /** Interval between readiness polls. */\n  readonly healthInterval?: Duration.Input | undefined;\n  /** Alchemy state ledger directory (defaults to `<cwd>/.alchemy`). */\n  readonly stateDir?: string | undefined;\n  /** Adopt pre-existing containers instead of failing on conflicts. */\n  readonly adopt?: boolean | undefined;\n  /** Alchemy stage scoping the state ledger. */\n  readonly stage?: string | undefined;\n}\n\nconst DEFAULT_CONTAINER_PORT = 3000;\nconst DEFAULT_HEALTH_PATH = \"/health\";\nconst DEFAULT_HEALTH_TIMEOUT: Duration.Input = \"90 seconds\";\nconst DEFAULT_HEALTH_INTERVAL: Duration.Input = \"500 millis\";\nconst DEFAULT_STAGE = \"local\";\n\ninterface ContainerInspect {\n  readonly State: { readonly Status: string };\n  readonly Config: { readonly Image: string };\n  readonly NetworkSettings: {\n    readonly Ports?:\n      | Record<string, ReadonlyArray<{ readonly HostIp: string; readonly HostPort: string }> | null>\n      | null\n      | undefined;\n  };\n}\n\nconst containerName = (instance: string): string =>\n  `bos-hosting-${instance.replaceAll(/[^a-zA-Z0-9_.-]/g, \"-\").toLowerCase()}`;\n\nconst stackName = (instance: string): string => `BosHosting-${containerName(instance)}`;\n\nconst describeHostError = (error: unknown): string =>\n  error instanceof Error ? error.message : String(error);\n\nconst isNotFound = (error: unknown): boolean =>\n  error instanceof Error &&\n  \"reason\" in error &&\n  (error as { reason?: { _tag?: string } }).reason?._tag === \"NotFound\";\n\nconst alchemyContextLayer = (options: DockerHostingOptions) =>\n  Layer.effect(\n    AlchemyContext,\n    Layer.build(AlchemyContextLive).pipe(\n      Effect.map((context) => Context.get(context, AlchemyContext)),\n      Effect.orDie,\n      Effect.map((context) =>\n        AlchemyContext.of({\n          ...context,\n          dev: false,\n          adopt: options.adopt ?? false,\n          dotAlchemy: options.stateDir ?? context.dotAlchemy ?? \".alchemy\",\n        }),\n      ),\n    ),\n  );\n\nconst platformLayer = Layer.mergeAll(PlatformServices, FetchHttpClient.layer);\n\nconst runtimeLayer = (options: DockerHostingOptions) =>\n  Layer.mergeAll(\n    alchemyContextLayer(options),\n    Interaction.layerNonInteractive(),\n    Layer.succeed(ArtifactStore, createArtifactStore()),\n    Layer.succeed(AdoptPolicy, options.adopt ?? false),\n    Layer.succeed(AuthProviders, {}),\n    State.localState(),\n  ).pipe(Layer.provideMerge(platformLayer));\n\nconst dockerServiceLayer = (options: DockerHostingOptions) =>\n  Docker.providers().pipe(\n    Layer.provide(\n      Layer.succeed(Stack, {\n        name: stackName(\"shared\"),\n        stage: options.stage ?? DEFAULT_STAGE,\n        resources: {},\n        bindings: {},\n        actions: {},\n      }),\n    ),\n    Layer.provide(Layer.succeed(Stage, options.stage ?? DEFAULT_STAGE)),\n  );\n\nexport const dockerHostingAdapterLayer = (options: DockerHostingOptions = {}) =>\n  // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- alchemy's Docker.providers() requirements resolve as any under tsgo; the seam's R is closed by the provides below\n  Layer.effect(\n    HostingAdapter,\n    Effect.gen(function* () {\n      const http = yield* HttpClient.HttpClient;\n      const docker = yield* Docker.Docker;\n      const containerPort = options.containerPort ?? DEFAULT_CONTAINER_PORT;\n      const healthPath = options.healthPath ?? DEFAULT_HEALTH_PATH;\n      const stage = options.stage ?? DEFAULT_STAGE;\n      const runtime = runtimeLayer(options);\n\n      const hostPortOf = (info: ContainerInspect): number | undefined => {\n        const bindings = info.NetworkSettings.Ports?.[`${containerPort}/tcp`];\n        const hostPort = bindings?.[0]?.HostPort;\n        return hostPort === undefined || hostPort === \"\" ? undefined : Number(hostPort);\n      };\n\n      const hostUrl = (hostPort: number): string => `http://127.0.0.1:${hostPort}`;\n\n      const inspectInstance = (instance: string) =>\n        docker.container.inspect(containerName(instance));\n\n      const waitForHealth = Effect.fn(\"DockerHostingAdapter.waitForHealth\")(function* (\n        instance: string,\n        url: string,\n      ) {\n        const timeoutMs = Duration.toMillis(\n          Duration.fromInputUnsafe(options.healthTimeout ?? DEFAULT_HEALTH_TIMEOUT),\n        );\n        const interval = Duration.fromInputUnsafe(\n          options.healthInterval ?? DEFAULT_HEALTH_INTERVAL,\n        );\n        const deadline = (yield* Clock.currentTimeMillis) + timeoutMs;\n        let attempts = 0;\n        while (true) {\n          attempts += 1;\n          const healthy = yield* http.execute(HttpClientRequest.get(`${url}${healthPath}`)).pipe(\n            Effect.map((response) => response.status >= 200 && response.status < 300),\n            Effect.orElseSucceed(() => false),\n          );\n          if (healthy) return url;\n          if ((yield* Clock.currentTimeMillis) >= deadline) {\n            return yield* new HealthTimeout({ instance, url, attempts });\n          }\n          yield* Effect.sleep(interval);\n        }\n      });\n\n      const snapshotOf = (instance: string, info: ContainerInspect): InstanceSnapshot => {\n        const running = info.State.Status === \"running\";\n        const port = running ? hostPortOf(info) : undefined;\n        return {\n          instance,\n          running,\n          url: port === undefined ? undefined : hostUrl(port),\n          image: info.Config.Image,\n        };\n      };\n\n      const deployDescriptor = (descriptor: RuntimeDescriptor) =>\n        Effect.gen(function* () {\n          const name = containerName(descriptor.instance);\n          const stack = Alchemy.Stack(\n            stackName(descriptor.instance),\n            {\n              providers: Docker.providers(),\n              state: State.localState(),\n            },\n            Effect.gen(function* () {\n              const container = yield* Docker.Container(\"runtime\", {\n                image: descriptor.image,\n                name,\n                environment: { ...descriptor.identityEnv },\n                labels: { \"bos.hosting/instance\": descriptor.instance },\n                ports: [{ external: 0, internal: containerPort }],\n                start: true,\n                restart: \"no\",\n              });\n              return { container };\n            }),\n          );\n          yield* Deploy.deploy({ stack, stage, dev: false }).pipe(\n            Effect.provide(runtime),\n            Effect.mapError(\n              (error) =>\n                new DeployFailed({\n                  instance: descriptor.instance,\n                  reason: describeHostError(error),\n                }),\n            ),\n          );\n          const info = yield* inspectInstance(descriptor.instance).pipe(\n            Effect.mapError(\n              (error) =>\n                new DeployFailed({\n                  instance: descriptor.instance,\n                  reason: describeHostError(error),\n                }),\n            ),\n          );\n          const port = hostPortOf(info);\n          if (port === undefined) {\n            return yield* new DeployFailed({\n              instance: descriptor.instance,\n              reason: `container published no host port for ${containerPort}/tcp`,\n            });\n          }\n          const url = hostUrl(port);\n          yield* waitForHealth(descriptor.instance, url);\n          return { instance: descriptor.instance, url };\n        });\n\n      const current = (instance: string) =>\n        inspectInstance(instance).pipe(\n          Effect.map((info) => snapshotOf(instance, info)),\n          Effect.catchTag(\n            \"PlatformError\",\n            (error): Effect.Effect<InstanceSnapshot, HostingOperationFailed> =>\n              isNotFound(error)\n                ? Effect.succeed({ instance, running: false, url: undefined, image: undefined })\n                : Effect.fail(\n                    new HostingOperationFailed({\n                      instance,\n                      operation: \"current\",\n                      reason: describeHostError(error),\n                    }),\n                  ),\n          ),\n        );\n\n      const restart = (\n        instance: string,\n      ): Effect.Effect<\n        DeployedInstance,\n        InstanceNotFound | HealthTimeout | HostingOperationFailed\n      > =>\n        Effect.gen(function* () {\n          const info = yield* inspectInstance(instance);\n          if (info.State.Status === \"running\") {\n            yield* docker.container.stop(containerName(instance));\n          }\n          yield* docker.container.start(containerName(instance));\n          const after = yield* inspectInstance(instance);\n          const port = hostPortOf(after);\n          if (port === undefined) {\n            return yield* new HostingOperationFailed({\n              instance,\n              operation: \"restart\",\n              reason: `container published no host port for ${containerPort}/tcp`,\n            });\n          }\n          const url = hostUrl(port);\n          yield* waitForHealth(instance, url);\n          return { instance, url };\n        }).pipe(\n          Effect.catchTag(\n            \"PlatformError\",\n            (error): Effect.Effect<DeployedInstance, InstanceNotFound | HostingOperationFailed> =>\n              isNotFound(error)\n                ? Effect.fail(new InstanceNotFound({ instance }))\n                : Effect.fail(\n                    new HostingOperationFailed({\n                      instance,\n                      operation: \"restart\",\n                      reason: describeHostError(error),\n                    }),\n                  ),\n          ),\n        );\n\n      const stop = (instance: string) =>\n        docker.container.stop(containerName(instance)).pipe(\n          Effect.asVoid,\n          Effect.catchTag(\n            \"PlatformError\",\n            (error): Effect.Effect<void, InstanceNotFound | HostingOperationFailed> =>\n              isNotFound(error)\n                ? Effect.fail(new InstanceNotFound({ instance }))\n                : Effect.fail(\n                    new HostingOperationFailed({\n                      instance,\n                      operation: \"stop\",\n                      reason: describeHostError(error),\n                    }),\n                  ),\n          ),\n        );\n\n      return HostingAdapter.of({ deploy: deployDescriptor, current, restart, stop });\n    }),\n  ).pipe(\n    Layer.provide(dockerServiceLayer(options)),\n    Layer.provide(runtimeLayer(options)),\n  ) as Layer.Layer<HostingAdapter>;\n"],"mappings":"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"}