{"version":3,"file":"ops.mjs","names":[],"sources":["../../src/services/ops.ts"],"sourcesContent":["import { dirname, resolve } from \"node:path\";\nimport { Context, Effect, Layer } from \"effect\";\nimport { findConfigPath, MISSING_CONFIG_MESSAGE } from \"../config\";\nimport type { KillResult, LogsResult, StatusResult } from \"../contract\";\nimport { ownerOfPort } from \"../infra/port-ownership\";\nimport { probePortBindable } from \"../infra/ports\";\nimport { readDevLatestLog, resolveDevLatestFile } from \"./dev/logs\";\nimport { killProcessGroupEscalating, reapGroup } from \"./processes/kill\";\nimport { isPidAlive, readRegistry, unregisterPid } from \"./processes/registry\";\nimport { fetchBosConfigFromFastKv } from \"./registry/fastkv\";\nimport { pointerFingerprint } from \"./registry/fingerprint\";\nimport type { ResolutionSession } from \"./resolution/session\";\nimport { getStatus } from \"./status\";\nimport { computeDeployedVersionStatus, type DeployedVersionStatus } from \"./version/status\";\n\nexport class OpsService extends Context.Service<\n  OpsService,\n  {\n    status: (session: ResolutionSession | null) => Effect.Effect<StatusResult, unknown>;\n    logs: (\n      input: { tail?: number | undefined; service?: string | undefined },\n      configDir: string,\n    ) => Effect.Effect<LogsResult, unknown>;\n    kill: (input: {\n      all?: boolean | undefined;\n      configDir?: string | undefined;\n      signal?: \"SIGTERM\" | \"SIGKILL\" | undefined;\n    }) => Effect.Effect<KillResult, unknown>;\n  }\n>()(\"bos/OpsService\") {\n  static Live = Layer.succeed(\n    OpsService,\n    OpsService.of({\n      status: (session) =>\n        Effect.tryPromise({\n          try: async () => {\n            const configPath = findConfigPath();\n            if (!configPath) {\n              return {\n                status: \"error\" as const,\n                packages: [],\n                envFile: \"missing\" as const,\n                error: `${MISSING_CONFIG_MESSAGE} in current directory`,\n              };\n            }\n\n            const projectDir = resolve(dirname(configPath));\n            const status = await getStatus(projectDir);\n            return {\n              ...status,\n              deployedVersion: await deployedVersionStatus(session),\n            };\n          },\n          catch: (error) => ({\n            status: \"error\" as const,\n            packages: [],\n            envFile: \"missing\" as const,\n            error: error instanceof Error ? error.message : \"Unknown error\",\n          }),\n        }),\n\n      logs: (input, configDir) =>\n        Effect.tryPromise({\n          try: async () => {\n            const text = await readDevLatestLog(configDir, { tail: input.tail });\n            const service = input.service;\n            const lines = text\n              .split(\"\\n\")\n              .filter((line) => line.length > 0)\n              .filter((line) => {\n                if (!service) return true;\n                const match = /\\] \\[([^\\]]+)\\] \\[(?:OUT|ERR)\\] /.exec(line);\n                return match?.[1] === service || match?.[1] === `plugin:${service}`;\n              });\n            return {\n              logFile: resolveDevLatestFile(configDir),\n              lines,\n            };\n          },\n          catch: (error) => ({\n            logFile: \"unknown\",\n            lines: [\n              error instanceof Error\n                ? `Failed to read logs: ${error.message}`\n                : \"Failed to read logs\",\n            ],\n          }),\n        }),\n\n      kill: (input) =>\n        Effect.tryPromise({\n          try: async () => {\n            const configPath = findConfigPath();\n            const targetConfigDir = input.all\n              ? undefined\n              : (input.configDir ?? (configPath ? resolve(dirname(configPath)) : undefined));\n\n            const targets = targetConfigDir\n              ? readRegistry().filter((entry) => entry.configDir === targetConfigDir)\n              : readRegistry();\n\n            const killed: Array<{ pid: number; configDir: string }> = [];\n            const skipped: Array<{ pid: number; reason: string }> = [];\n\n            for (const entry of targets) {\n              const wasAlive = isPidAlive(entry.pid);\n              if (wasAlive) {\n                await Effect.runPromise(\n                  killProcessGroupEscalating(entry.pid, {\n                    terminateMs: 5000,\n                    signal: input.signal === \"SIGKILL\" ? \"SIGKILL\" : \"SIGTERM\",\n                  }),\n                );\n              }\n\n              const reapedChildren: number[] = [];\n              for (const childPid of entry.childPids ?? []) {\n                if (isPidAlive(childPid)) {\n                  reapGroup(childPid);\n                  reapedChildren.push(childPid);\n                }\n              }\n\n              const ports = Object.values(entry.ports ?? {}).filter(\n                (port) => Number.isFinite(port) && port > 0,\n              );\n              const stillBound: number[] = [];\n              for (const port of ports) {\n                let bindable = false;\n                for (let attempt = 0; attempt < 4; attempt++) {\n                  bindable = await Effect.runPromise(probePortBindable(port));\n                  if (bindable) break;\n                  await new Promise((resolve) => setTimeout(resolve, 500));\n                }\n                if (!bindable) stillBound.push(port);\n              }\n\n              if (stillBound.length > 0) {\n                const owner = await Effect.runPromise(ownerOfPort(stillBound[0]));\n                skipped.push({\n                  pid: entry.pid,\n                  reason: `ports still bound after kill: ${stillBound.join(\", \")}${\n                    owner ? ` — held by pid ${owner.pid} (${owner.command})` : \" — owner unknown\"\n                  }`,\n                });\n                continue;\n              }\n\n              unregisterPid(entry.pid);\n              killed.push({ pid: entry.pid, configDir: entry.configDir });\n              if (!wasAlive && reapedChildren.length > 0) {\n                skipped.push({\n                  pid: entry.pid,\n                  reason: `process already exited; reaped orphaned children ${reapedChildren.join(\", \")}`,\n                });\n              }\n            }\n\n            return {\n              status: \"killed\" as const,\n              killed,\n              skipped,\n            };\n          },\n          catch: (error) => ({\n            status: \"error\" as const,\n            killed: [],\n            skipped: [],\n            error: error instanceof Error ? error.message : \"Unknown error\",\n          }),\n        }),\n    }),\n  );\n}\n\nasync function deployedVersionStatus(\n  session: ResolutionSession | null,\n): Promise<DeployedVersionStatus | undefined> {\n  const config = session?.config;\n  if (!config?.account || !config.domain) return undefined;\n  const { account, domain } = config;\n  let publishedFingerprint: string | undefined;\n  try {\n    const published = await fetchBosConfigFromFastKv<Record<string, unknown>>(\n      `bos://${account}/${domain}`,\n    );\n    publishedFingerprint = pointerFingerprint(published as never);\n  } catch {\n    return undefined;\n  }\n\n  const hostUrl = session?.runtime?.host?.url;\n  if (!hostUrl) {\n    return computeDeployedVersionStatus({ publishedFingerprint, servedFingerprint: null });\n  }\n  try {\n    const response = await fetch(new URL(\"/.well-known/version\", hostUrl));\n    if (!response.ok) {\n      return computeDeployedVersionStatus({\n        publishedFingerprint,\n        servedFingerprint: null,\n        servedError: `version endpoint returned ${response.status}`,\n      });\n    }\n    const body = (await response.json()) as { fingerprint?: string };\n    return computeDeployedVersionStatus({\n      publishedFingerprint,\n      servedFingerprint: body.fingerprint ?? null,\n    });\n  } catch (error) {\n    return computeDeployedVersionStatus({\n      publishedFingerprint,\n      servedFingerprint: null,\n      servedError: error instanceof Error ? error.message : String(error),\n    });\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,IAAa,aAAb,MAAa,mBAAmB,QAAQ,QActC,CAAC,CAAC,gBAAgB,CAAC,CAAC;CACpB,OAAO,OAAO,MAAM,QAClB,YACA,WAAW,GAAG;EACZ,SAAS,YACP,OAAO,WAAW;GAChB,KAAK,YAAY;IACf,MAAM,aAAa,eAAe;IAClC,IAAI,CAAC,YACH,OAAO;KACL,QAAQ;KACR,UAAU,CAAC;KACX,SAAS;KACT,OAAO,GAAG,uBAAuB;IACnC;IAGF,MAAM,aAAa,QAAQ,QAAQ,UAAU,CAAC;IAE9C,OAAO;KACL,GAAG,MAFgB,UAAU,UAAU;KAGvC,iBAAiB,MAAM,sBAAsB,OAAO;IACtD;GACF;GACA,QAAQ,WAAW;IACjB,QAAQ;IACR,UAAU,CAAC;IACX,SAAS;IACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;GAClD;EACF,CAAC;EAEH,OAAO,OAAO,cACZ,OAAO,WAAW;GAChB,KAAK,YAAY;IACf,MAAM,OAAO,MAAM,iBAAiB,WAAW,EAAE,MAAM,MAAM,KAAK,CAAC;IACnE,MAAM,UAAU,MAAM;IACtB,MAAM,QAAQ,KACX,MAAM,IAAI,CAAC,CACX,QAAQ,SAAS,KAAK,SAAS,CAAC,CAAC,CACjC,QAAQ,SAAS;KAChB,IAAI,CAAC,SAAS,OAAO;KACrB,MAAM,QAAQ,mCAAmC,KAAK,IAAI;KAC1D,OAAO,QAAQ,OAAO,WAAW,QAAQ,OAAO,UAAU;IAC5D,CAAC;IACH,OAAO;KACL,SAAS,qBAAqB,SAAS;KACvC;IACF;GACF;GACA,QAAQ,WAAW;IACjB,SAAS;IACT,OAAO,CACL,iBAAiB,QACb,wBAAwB,MAAM,YAC9B,qBACN;GACF;EACF,CAAC;EAEH,OAAO,UACL,OAAO,WAAW;GAChB,KAAK,YAAY;IACf,MAAM,aAAa,eAAe;IAClC,MAAM,kBAAkB,MAAM,MAC1B,SACC,MAAM,cAAc,aAAa,QAAQ,QAAQ,UAAU,CAAC,IAAI;IAErE,MAAM,UAAU,kBACZ,aAAa,CAAC,CAAC,QAAQ,UAAU,MAAM,cAAc,eAAe,IACpE,aAAa;IAEjB,MAAM,SAAoD,CAAC;IAC3D,MAAM,UAAkD,CAAC;IAEzD,KAAK,MAAM,SAAS,SAAS;KAC3B,MAAM,WAAW,WAAW,MAAM,GAAG;KACrC,IAAI,UACF,MAAM,OAAO,WACX,2BAA2B,MAAM,KAAK;MACpC,aAAa;MACb,QAAQ,MAAM,WAAW,YAAY,YAAY;KACnD,CAAC,CACH;KAGF,MAAM,iBAA2B,CAAC;KAClC,KAAK,MAAM,YAAY,MAAM,aAAa,CAAC,GACzC,IAAI,WAAW,QAAQ,GAAG;MACxB,UAAU,QAAQ;MAClB,eAAe,KAAK,QAAQ;KAC9B;KAGF,MAAM,QAAQ,OAAO,OAAO,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,QAC5C,SAAS,OAAO,SAAS,IAAI,KAAK,OAAO,CAC5C;KACA,MAAM,aAAuB,CAAC;KAC9B,KAAK,MAAM,QAAQ,OAAO;MACxB,IAAI,WAAW;MACf,KAAK,IAAI,UAAU,GAAG,UAAU,GAAG,WAAW;OAC5C,WAAW,MAAM,OAAO,WAAW,kBAAkB,IAAI,CAAC;OAC1D,IAAI,UAAU;OACd,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;MACzD;MACA,IAAI,CAAC,UAAU,WAAW,KAAK,IAAI;KACrC;KAEA,IAAI,WAAW,SAAS,GAAG;MACzB,MAAM,QAAQ,MAAM,OAAO,WAAW,YAAY,WAAW,EAAE,CAAC;MAChE,QAAQ,KAAK;OACX,KAAK,MAAM;OACX,QAAQ,iCAAiC,WAAW,KAAK,IAAI,IAC3D,QAAQ,kBAAkB,MAAM,IAAI,IAAI,MAAM,QAAQ,KAAK;MAE/D,CAAC;MACD;KACF;KAEA,cAAc,MAAM,GAAG;KACvB,OAAO,KAAK;MAAE,KAAK,MAAM;MAAK,WAAW,MAAM;KAAU,CAAC;KAC1D,IAAI,CAAC,YAAY,eAAe,SAAS,GACvC,QAAQ,KAAK;MACX,KAAK,MAAM;MACX,QAAQ,oDAAoD,eAAe,KAAK,IAAI;KACtF,CAAC;IAEL;IAEA,OAAO;KACL,QAAQ;KACR;KACA;IACF;GACF;GACA,QAAQ,WAAW;IACjB,QAAQ;IACR,QAAQ,CAAC;IACT,SAAS,CAAC;IACV,OAAO,iBAAiB,QAAQ,MAAM,UAAU;GAClD;EACF,CAAC;CACL,CAAC,CACH;AACF;AAEA,eAAe,sBACb,SAC4C;CAC5C,MAAM,SAAS,SAAS;CACxB,IAAI,CAAC,QAAQ,WAAW,CAAC,OAAO,QAAQ,OAAO;CAC/C,MAAM,EAAE,SAAS,WAAW;CAC5B,IAAI;CACJ,IAAI;EACF,MAAM,YAAY,MAAM,yBACtB,SAAS,QAAQ,GAAG,QACtB;EACA,uBAAuB,mBAAmB,SAAkB;CAC9D,QAAQ;EACN;CACF;CAEA,MAAM,UAAU,SAAS,SAAS,MAAM;CACxC,IAAI,CAAC,SACH,OAAO,6BAA6B;EAAE;EAAsB,mBAAmB;CAAK,CAAC;CAEvF,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,IAAI,IAAI,wBAAwB,OAAO,CAAC;EACrE,IAAI,CAAC,SAAS,IACZ,OAAO,6BAA6B;GAClC;GACA,mBAAmB;GACnB,aAAa,6BAA6B,SAAS;EACrD,CAAC;EAEH,MAAM,OAAQ,MAAM,SAAS,KAAK;EAClC,OAAO,6BAA6B;GAClC;GACA,mBAAmB,KAAK,eAAe;EACzC,CAAC;CACH,SAAS,OAAO;EACd,OAAO,6BAA6B;GAClC;GACA,mBAAmB;GACnB,aAAa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACpE,CAAC;CACH;AACF"}