{"version":3,"file":"host.mjs","names":[],"sources":["../src/host.ts"],"sourcesContent":["// PiHostOnDSH: one DSH plugin that hosts unmodified Pi packages.\n//\n// Pi packages are ordinary npm dependencies of the profile; DSH's plugin\n// manager (pnpm) installs them, and at load time this module resolves each\n// installed package, discovers its Pi entry points, and mounts it through\n// the same package-agnostic runtime. One host, any package — there is\n// deliberately no per-package branching here.\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { readFile, stat, writeFile, mkdir, cp } from 'node:fs/promises'\nimport { createRequire } from 'node:module'\nimport { basename, dirname, join, relative } from 'node:path'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { applyPiPackage, registerVisionCompanions } from './runtime.js'\nimport { resolvePiPackage } from './source.js'\nimport type { GeneratedRuntimeManifest, ResolvedPiPackage } from './types.js'\n\ntype UnknownRecord = Record<string, unknown>\n\nexport interface PiHostPackageSpec {\n  /** npm package name as installed in the host bundle's node_modules. */\n  name: string\n  /** Optional per-package config forwarded to the runtime. */\n  config?: UnknownRecord\n  /**\n   * Resolution anchor for THIS package (a package.json path). A suite's\n   * members are its own dependencies, so under pnpm's isolated layout they\n   * resolve from the suite package, not from the profile root.\n   */\n  anchor?: string\n}\n\nexport interface PiHostConfig {\n  packages: Array<string | PiHostPackageSpec>\n  /** Image-admission companions: default automatic; `false` off; explicit map narrows. */\n  visionCompanions?: false | Record<string, readonly string[]>\n}\n\nexport interface PreparedPiHostPackage {\n  readonly name: string\n  readonly rootUrl: URL\n  readonly manifest: GeneratedRuntimeManifest\n  readonly config?: UnknownRecord\n}\n\nfunction parseFrontmatter(text: string): { attributes: Record<string, string>; body: string } {\n  const normalized = text.replace(/\\r\\n?/gu, '\\n')\n  if (!normalized.startsWith('---')) return { attributes: {}, body: normalized }\n  const endIndex = normalized.indexOf('\\n---', 3)\n  if (endIndex === -1) return { attributes: {}, body: normalized }\n  const attributes: Record<string, string> = {}\n  for (const line of normalized.slice(4, endIndex).split('\\n')) {\n    const separator = line.indexOf(':')\n    if (separator === -1) continue\n    const key = line.slice(0, separator).trim()\n    let value = line.slice(separator + 1).trim()\n    if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n      value = value.slice(1, -1)\n    }\n    if (key.length > 0) attributes[key] = value\n  }\n  return { attributes, body: normalized.slice(endIndex + 4).trim() }\n}\n\n/** Build a runtime manifest in place over an installed Pi package directory. */\nexport async function manifestForInstalled(pkg: ResolvedPiPackage): Promise<GeneratedRuntimeManifest> {\n  const relativeTo = (file: string): string => relative(pkg.rootDir, file).replaceAll('\\\\', '/')\n\n  const skillDirs = new Set<string>()\n  for (const file of pkg.resources.skills) {\n    // <dir>/<name>/SKILL.md contributes <dir>; a flat <dir>/<name>.md contributes <dir>.\n    skillDirs.add(relativeTo(basename(file) === 'SKILL.md' ? dirname(dirname(file)) : dirname(file)))\n  }\n\n  const prompts: GeneratedRuntimeManifest['prompts'] = []\n  const promptNames = new Set<string>()\n  for (const source of pkg.resources.prompts) {\n    const name = basename(source, '.md').toLowerCase().replace(/[^a-z0-9_-]+/gu, '-')\n    if (promptNames.has(name)) throw new Error(`prompt command name collision in ${pkg.identity.name}: ${name}`)\n    promptNames.add(name)\n    const { attributes, body } = parseFrontmatter(await readFile(source, 'utf8'))\n    const firstLine = body.split(/\\r?\\n/u).map(line => line.trim()).find(Boolean)\n    prompts.push({\n      name,\n      description: attributes.description ?? firstLine ?? `Run migrated Pi prompt ${name}`,\n      ...(attributes['argument-hint'] !== undefined ? { argumentHint: attributes['argument-hint'] } : {}),\n      path: relativeTo(source),\n    })\n  }\n\n  return {\n    schemaVersion: 1,\n    package: pkg.identity,\n    extensions: pkg.resources.extensions.map(relativeTo),\n    skillDirs: [...skillDirs].sort(),\n    prompts,\n  }\n}\n\nfunction normalizeSpecs(config: PiHostConfig): PiHostPackageSpec[] {\n  const packages = Array.isArray(config?.packages) ? config.packages : []\n  return packages.map(spec => (typeof spec === 'string' ? { name: spec } : spec))\n    .filter(spec => typeof spec?.name === 'string' && spec.name.length > 0)\n}\n\nfunction resolveInstalledDir(anchor: string, packageName: string): string {\n  const require = createRequire(anchor)\n  try {\n    return dirname(require.resolve(`${packageName}/package.json`))\n  } catch {\n    // Modern strict `exports` maps refuse the package.json subpath, and pure\n    // ESM packages (no \"require\" condition) refuse CJS entry resolution too.\n    // Locate the installed directory on the filesystem instead: probe every\n    // node_modules candidate on the resolution path — no exports involved.\n    for (const candidate of require.resolve.paths(packageName) ?? []) {\n      const dir = join(candidate, packageName)\n      if (existsSync(join(dir, 'package.json'))) return dir\n    }\n    throw new Error(`cannot locate the installed package directory for ${JSON.stringify(packageName)} near ${JSON.stringify(anchor)}`)\n  }\n}\n\n/**\n * Mount every configured Pi package from the host bundle's own node_modules.\n * Packages that fail to mount report their error and do not take down the\n * host or their siblings — matching Pi's own per-extension error isolation.\n */\nexport async function applyPiHost(ctx: Context, config: PiHostConfig, anchor?: string): Promise<void> {\n  registerVisionCompanions(ctx, config.visionCompanions)\n  const prepared = await preparePiHost(config, anchor)\n  await applyPreparedPiHost(ctx, prepared)\n}\n\n/** Resolve installed Pi packages and build immutable manifests without mounting their runtimes. */\nexport async function preparePiHost(config: PiHostConfig, anchor?: string): Promise<PreparedPiHostPackage[]> {\n  const anchorPath = anchor ?? fileURLToPath(import.meta.url)\n  const errors: Array<{ name: string; error: string }> = []\n  const prepared: PreparedPiHostPackage[] = []\n  for (const spec of normalizeSpecs(config)) {\n    try {\n      const dir = resolveInstalledDir(spec.anchor ?? anchorPath, spec.name)\n      const pkg = await resolvePiPackage(dir)\n      try {\n        const manifest = await manifestForInstalled(pkg)\n        prepared.push({\n          name: spec.name,\n          rootUrl: pathToFileURL(`${pkg.rootDir}/`),\n          manifest,\n          ...(spec.config === undefined ? {} : { config: spec.config }),\n        })\n      } finally {\n        await pkg.dispose()\n      }\n    } catch (error) {\n      errors.push({ name: spec.name, error: error instanceof Error ? error.message : String(error) })\n    }\n  }\n  for (const failure of errors) {\n    // Console AND logger, the same rule the engine states for its own mount\n    // line: a profile's logger level must never be able to hide which packages\n    // did not mount. It could, and it did — a package failed to mount and the\n    // only symptom was its absence from a list nobody was diffing.\n    const message = `[pi2dsh host] failed to mount ${failure.name}: ${failure.error}`\n    console.warn(message)\n  }\n  if (errors.length > 0 && errors.length === normalizeSpecs(config).length) {\n    throw new Error(`pi2dsh host mounted no packages; first failure: ${errors[0]!.name}: ${errors[0]!.error}`)\n  }\n  return prepared\n}\n\n/**\n * Mount prepared Pi package runtimes into one exact DSH context.\n * @returns per-package mount failures (empty when every package mounted) —\n *   Pi's per-extension error isolation, consumable by bindExtensions' onError.\n */\nexport async function applyPreparedPiHost(\n  ctx: Context,\n  prepared: readonly PreparedPiHostPackage[],\n  ownerAgent?: UnknownRecord,\n  mode: { hostAnchor?: boolean } = {},\n): Promise<Array<{ name: string, error: string }>> {\n  const errors: Array<{ name: string; error: string }> = []\n  for (const pkg of prepared) {\n    try {\n      // One real Cordis activation per Pi package, owned by this Agent scope.\n      // Besides giving teardown the correct owner, dsh-TUI's contribution\n      // services bind registration and later use to this activation identity;\n      // calling applyPiPackage() naked from the setup event has no live plugin\n      // caller and cannot register or open a scene correctly.\n      await ctx.plugin(Object.assign(\n        async (packageCtx: Context) => {\n          await applyPiPackage(packageCtx, {\n            rootUrl: pkg.rootUrl,\n            manifest: pkg.manifest,\n            // The engine/host registers companion routes once on the host\n            // before mounting prepared packages. Suppress applyPiPackage's\n            // standalone default here so one package cannot undo an explicit\n            // host-level `visionCompanions: false` or duplicate the catalog.\n            config: { ...(pkg.config ?? {}), visionCompanions: false },\n            ...(ownerAgent === undefined ? {} : { ownerAgent }),\n            ...(mode.hostAnchor === true ? { hostAnchor: true } : {}),\n          })\n        },\n        { inject: ['tools', 'systemPrompt', 'commands'] },\n      ))\n    } catch (error) {\n      errors.push({ name: pkg.name, error: error instanceof Error ? error.message : String(error) })\n    }\n  }\n  for (const failure of errors) {\n    const log = (ctx as unknown as { logger?: { warn?(message: string): void } }).logger\n    const message = `[pi2dsh host] failed to mount ${failure.name}: ${failure.error}`\n    log?.warn?.(message)\n    console.warn(message)\n  }\n  if (errors.length > 0 && errors.length === prepared.length) {\n    throw new Error(`pi2dsh host mounted no packages; first failure: ${errors[0]!.name}: ${errors[0]!.error}`)\n  }\n  return errors\n}\n"],"mappings":"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"}