{"version":3,"file":"plugins-admin.mjs","names":[],"sources":["../../src/services/plugins-admin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { Context, Effect, Layer } from \"effect\";\nimport { MISSING_CONFIG_MESSAGE } from \"../config\";\nimport type {\n  PluginAddResult,\n  PluginListResult,\n  PluginPublishResult,\n  PluginRemoveResult,\n} from \"../contract\";\nimport type { BosConfig } from \"../types\";\nimport { saveBosConfig } from \"../utils/save-config\";\nimport { generateCodeArtifacts } from \"./codegen/artifacts\";\nimport { DeployTrainService } from \"./deploy/train\";\nimport { fetchRemotePluginManifest } from \"./registry/fastkv\";\nimport { openResolution, type ResolutionSession } from \"./resolution/session\";\nimport { getPluginRef } from \"./workspace/build-train\";\n\nexport class PluginAdminService extends Context.Service<\n  PluginAdminService,\n  {\n    add: (\n      session: ResolutionSession | null,\n      input: { source: string; as?: string | undefined; production?: string | undefined },\n    ) => Effect.Effect<PluginAddResult, unknown>;\n    remove: (\n      session: ResolutionSession | null,\n      input: { key: string },\n    ) => Effect.Effect<PluginRemoveResult, unknown>;\n    list: (session: ResolutionSession | null) => Effect.Effect<PluginListResult[\"plugins\"], never>;\n    publish: (\n      session: ResolutionSession | null,\n      input: { key: string },\n    ) => Effect.Effect<PluginPublishResult, unknown>;\n  }\n>()(\"bos/PluginAdminService\") {\n  static Live = Layer.effect(\n    PluginAdminService,\n    Effect.map(DeployTrainService, (train) =>\n      PluginAdminService.of({\n        add: (session, input) =>\n          Effect.gen(function* () {\n            if (!session?.config) {\n              return {\n                status: \"error\" as const,\n                key: \"\",\n                error: MISSING_CONFIG_MESSAGE,\n              };\n            }\n\n            const isBosRef = input.source.startsWith(\"bos://\");\n            const isLocal = input.source.startsWith(\"local:\");\n            const key = sanitizePluginKey(\n              input.as ??\n                (isBosRef\n                  ? (input.source.split(\"/\").pop() ?? \"plugin\")\n                  : defaultPluginKey(input.source)),\n            );\n            const existing = session.config.plugins?.[key];\n            const existingEntry = existing && typeof existing === \"object\" ? existing : {};\n            // Adding a source is a switch, not an additive patch: a remote URL (or\n            // bos:// ref) replaces a conflicting local development ref. Dual-mode\n            // (local in dev, production URL in prod) remains expressible by\n            // hand-editing the authored config.\n            const applyEntryDelta = (base: Record<string, unknown>): Record<string, unknown> => {\n              if (isBosRef) {\n                const { development: _dropped, ...rest } = base;\n                return { ...rest, extends: input.source };\n              }\n              if (isLocal) {\n                const { extends: _dropped, ...rest } = base;\n                return { ...rest, development: input.source };\n              }\n              const { development: _dropped, ...rest } = base;\n              return { ...rest, production: input.production ?? input.source };\n            };\n            const nextPlugins = { ...session.config.plugins };\n            nextPlugins[key] = applyEntryDelta(existingEntry);\n\n            const nextConfig: BosConfig = {\n              ...session.config,\n              plugins: nextPlugins,\n            };\n\n            const authored = session.rawConfig;\n            if (authored) {\n              // Authoring-surface save: the authored leaf input + delta, never the\n              // resolved config (which would bake inherited values into the file).\n              const authoredExisting = authored.plugins?.[key];\n              const authoredBase: Record<string, unknown> =\n                typeof authoredExisting === \"string\"\n                  ? { extends: authoredExisting }\n                  : authoredExisting && typeof authoredExisting === \"object\"\n                    ? (authoredExisting as Record<string, unknown>)\n                    : {};\n              yield* Effect.tryPromise(() =>\n                saveBosConfig(session.root, {\n                  ...authored,\n                  plugins: {\n                    ...authored.plugins,\n                    [key]: applyEntryDelta(authoredBase),\n                  },\n                }),\n              );\n            } else {\n              yield* Effect.tryPromise(() => saveBosConfig(session.root, nextConfig));\n            }\n            yield* Effect.tryPromise(async () => {\n              await generateCodeArtifacts(session.root, nextConfig);\n            });\n\n            const stored = nextConfig.plugins?.[key];\n            const storedObj = stored && typeof stored === \"object\" ? stored : {};\n\n            return {\n              status: \"added\" as const,\n              key,\n              development: storedObj.development,\n              production: storedObj.production,\n              integrity: storedObj.integrity,\n              version: storedObj.version,\n            };\n          }),\n\n        remove: (session, input) =>\n          Effect.gen(function* () {\n            if (!session?.config) {\n              return {\n                status: \"error\" as const,\n                key: input.key,\n                error: MISSING_CONFIG_MESSAGE,\n              };\n            }\n\n            if (!session.config.plugins?.[input.key]) {\n              return {\n                status: \"error\" as const,\n                key: input.key,\n                error: `Plugin '${input.key}' is not configured`,\n              };\n            }\n\n            const nextPlugins = { ...session.config.plugins };\n            delete nextPlugins[input.key];\n            const nextConfig: BosConfig = {\n              ...session.config,\n              plugins: Object.keys(nextPlugins).length > 0 ? nextPlugins : undefined,\n            };\n\n            const authored = session.rawConfig;\n            if (authored) {\n              const authoredPlugins: Record<string, unknown> = { ...authored.plugins };\n              if (authoredPlugins[input.key] === undefined) {\n                // Inherited from the parent runtime. JSON-form children can express\n                // removal with a null sentinel (dropped by the extends merge); the\n                // TS descriptor schema cannot, so only those error out.\n                if (existsSync(join(session.root, \"bos.app.ts\"))) {\n                  return {\n                    status: \"error\" as const,\n                    key: input.key,\n                    error: `'${input.key}' is inherited from the parent runtime — remove it in the parent config or override it there (the authored descriptor cannot express removal)`,\n                  };\n                }\n                authoredPlugins[input.key] = null;\n              } else {\n                delete authoredPlugins[input.key];\n              }\n              yield* Effect.tryPromise(() =>\n                saveBosConfig(session.root, {\n                  ...authored,\n                  plugins: Object.keys(authoredPlugins).length > 0 ? authoredPlugins : undefined,\n                }),\n              );\n            } else {\n              yield* Effect.tryPromise(() => saveBosConfig(session.root, nextConfig));\n            }\n            yield* Effect.tryPromise(async () => {\n              await generateCodeArtifacts(session.root, nextConfig);\n            });\n\n            return {\n              status: \"removed\" as const,\n              key: input.key,\n            };\n          }),\n\n        list: (session) => Effect.succeed(listPluginAttachments(session?.config ?? null)),\n\n        publish: (session, input) =>\n          Effect.gen(function* () {\n            if (!session?.config) {\n              return {\n                status: \"error\" as const,\n                key: input.key,\n                error: MISSING_CONFIG_MESSAGE,\n              };\n            }\n\n            const attachment = session.config?.plugins?.[input.key];\n            if (!attachment) {\n              return {\n                status: \"error\" as const,\n                key: input.key,\n                error: `Plugin '${input.key}' is not configured`,\n              };\n            }\n\n            const localPath = pluginLocalPath(session.root, attachment);\n            if (!localPath) {\n              return {\n                status: \"error\" as const,\n                key: input.key,\n                error: `Plugin '${input.key}' does not have a local development path`,\n              };\n            }\n\n            const result = yield* train\n              .publish({\n                bosConfig: session.config,\n                runtimeConfig: session.runtime,\n                configDir: session.root,\n                env: \"production\",\n                build: true,\n                dryRun: false,\n                verbose: false,\n                packages: input.key,\n              })\n              .pipe(\n                Effect.catchTag(\"PublishTrainError\", (error) => Effect.succeed(error.toResult())),\n              );\n            if (result.status === \"error\") {\n              return {\n                status: \"error\" as const,\n                key: input.key,\n                error: result.error,\n              };\n            }\n\n            const nextSession =\n              (yield* Effect.tryPromise({\n                try: () => openResolution({ cwd: session.root }),\n                catch: () => null,\n              })) ?? session;\n            const production = (\n              nextSession.config?.plugins?.[input.key] as\n                | {\n                    production?: string;\n                  }\n                | undefined\n            )?.production;\n\n            const manifest = production\n              ? yield* Effect.tryPromise({\n                  try: () => fetchRemotePluginManifest(production),\n                  catch: () => null,\n                })\n              : null;\n            const nextConfig = nextSession.config;\n            if (production && nextConfig) {\n              yield* Effect.tryPromise({\n                try: async () => {\n                  await generateCodeArtifacts(session.root, nextConfig);\n                },\n                catch: () => undefined,\n              });\n            }\n\n            return {\n              status: \"published\" as const,\n              key: input.key,\n              path: localPath,\n              production,\n              fingerprint: result.fingerprint,\n              version: manifest?.plugin.version,\n            };\n          }),\n      }),\n    ),\n  );\n}\n\ntype PluginAttachmentConfig = NonNullable<BosConfig[\"plugins\"]>[string];\n\nfunction sanitizePluginKey(value: string): string {\n  return value\n    .replace(/[^A-Za-z0-9/_-]/g, \"-\")\n    .replace(/\\/+/g, \"/\")\n    .split(\"/\")\n    .filter(Boolean)\n    .map((segment) => segment.replace(/[^A-Za-z0-9_-]/g, \"-\"))\n    .join(\"/\")\n    .replace(/^\\/+|\\/+$/g, \"\");\n}\n\nfunction defaultPluginKey(source: string): string {\n  const normalized = source.replace(/^local:/, \"\").replace(/\\/$/, \"\");\n  if (source.startsWith(\"local:\")) {\n    return sanitizePluginKey(basename(normalized)) || \"plugin\";\n  }\n\n  try {\n    const url = new URL(source);\n    return sanitizePluginKey(basename(url.pathname) || url.hostname) || \"plugin\";\n  } catch {\n    return sanitizePluginKey(source) || \"plugin\";\n  }\n}\n\nfunction pluginLocalPath(configDir: string, attachment: PluginAttachmentConfig): string | null {\n  const ref = getPluginRef(attachment);\n  const source = ref?.development ?? ref?.production;\n  if (!source?.startsWith(\"local:\")) {\n    return null;\n  }\n\n  return join(configDir, source.slice(\"local:\".length));\n}\n\nfunction listPluginAttachments(config: BosConfig | null) {\n  return (Object.entries(config?.plugins ?? {}) as Array<[string, PluginAttachmentConfig]>)\n    .map(([key, attachment]) => {\n      const ref = getPluginRef(attachment);\n      return {\n        key,\n        development: ref?.development,\n        production: ref?.production,\n        localPath: ref?.development?.startsWith(\"local:\")\n          ? ref.development.slice(\"local:\".length)\n          : undefined,\n        source: ref?.development?.startsWith(\"local:\") ? (\"local\" as const) : (\"remote\" as const),\n        integrity: ref?.integrity,\n        version: ref?.version,\n        name: ref?.name,\n      };\n    })\n    .sort((a, b) => a.key.localeCompare(b.key));\n}\n"],"mappings":"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"}