{"version":3,"file":"enrich-DZlKsZox.mjs","names":[],"sources":["../src/enrich.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport type { PrismaNextConfig } from '@prisma-next/config/config-types';\nimport { determineAgent } from '@vercel/detect-agent';\nimport { join } from 'pathe';\nimport type { ParentToSenderPayload, TelemetryEvent } from './payload';\n\n/**\n * Subset of the user's `prisma-next.config.*` the telemetry event\n * surfaces. Loaded inside the detached child via {@link loadProjectConfig}\n * — see the design rationale on {@link ParentToSenderPayload} for why\n * this side runs c12 instead of the parent CLI.\n */\nexport interface ProjectConfigFields {\n  readonly databaseTarget: string | null;\n  readonly extensions: readonly string[];\n}\n\nconst EMPTY_PROJECT_CONFIG: ProjectConfigFields = {\n  databaseTarget: null,\n  extensions: [],\n};\n\n/**\n * Best-effort load of `prisma-next.config.*` from `projectRoot`,\n * validated against the canonical `@prisma-next/config` schema.\n * Returns `{ databaseTarget: null, extensions: [] }` on any failure\n * mode — missing config file (e.g. before `prisma-next init`), c12\n * throws while evaluating user TS, validator rejects a malformed\n * shape, etc. Telemetry is non-blocking and best-effort; an empty\n * result is the only downside of an unloadable or invalid config.\n *\n * Both `c12` and `@prisma-next/config/config-validation` are imported\n * lazily so the detached sender's cold-start cost is paid only when\n * telemetry actually fires, not on every fork even when gates\n * short-circuit before reaching this code path.\n */\nexport async function loadProjectConfig(projectRoot: string): Promise<ProjectConfigFields> {\n  try {\n    const { loadConfig } = await import('c12');\n    const result = await loadConfig<Record<string, unknown>>({\n      name: 'prisma-next',\n      cwd: projectRoot,\n      dotenv: false,\n      rcFile: false,\n      globalRc: false,\n    });\n    const config = result.config ?? null;\n    // c12 returns an empty object when no config file exists in the\n    // search path — distinct from \"file existed but parsed to an empty\n    // object\". Either way, the canonical validator below would reject\n    // it on the first required field (`family`), so short-circuit\n    // without paying the import cost.\n    if (config === null || Object.keys(config).length === 0) {\n      return EMPTY_PROJECT_CONFIG;\n    }\n    const validation = await import('@prisma-next/config/config-validation');\n    // TS 4.7+ only flows `asserts cfg is X` narrowing when the\n    // assertion function is called via a directly-declared name with\n    // an explicit signature. The dynamic-import binding doesn't\n    // satisfy that, so wrap the call in a local declaration that\n    // re-asserts the signature.\n    const validate: (cfg: unknown) => asserts cfg is PrismaNextConfig = validation.validateConfig;\n    validate(config);\n    return {\n      databaseTarget: config.target.targetId,\n      extensions: (config.extensionPacks ?? []).map((pack) => pack.id),\n    };\n  } catch {\n    return EMPTY_PROJECT_CONFIG;\n  }\n}\n\n/**\n * Versions surface the enrichment cares about. Modelled as a structural\n * record with a required `node` field so tests can pass a literal object\n * without faking every field of `NodeJS.ProcessVersions` (which adds\n * properties between Node versions and includes a long tail the\n * enrichment never touches). Both `bun` and `deno` are read on the\n * runtime-resolution path; everything else is ignored.\n */\nexport interface VersionsSnapshot {\n  readonly node: string;\n  readonly bun?: string;\n  readonly deno?: string;\n}\n\n/**\n * Snapshot of process-level inputs the enrichment reads. Tests pass an\n * explicit snapshot so the enrichment is deterministic per case; the\n * sender entry point passes a fresh snapshot from `process`.\n */\nexport interface EnrichEnvironment {\n  readonly platform: NodeJS.Platform;\n  readonly arch: string;\n  readonly versions: VersionsSnapshot;\n  /**\n   * Included because package-manager detection intentionally reads\n   * environment variables from the same process snapshot as platform/versions.\n   */\n  readonly env: Readonly<Record<string, string | undefined>>;\n  /**\n   * Pre-resolved AI coding-agent label, or `null` for a human session.\n   * Detection lives in `@vercel/detect-agent`, whose `determineAgent()`\n   * reads the live `process.env` and is async (it probes the filesystem\n   * for Devin), so it cannot run inside the pure event builder; the\n   * sender entry resolves it via {@link resolveAgentLabel} and passes\n   * the label here. Detection runs in the **child** sender process,\n   * never the parent. Best-effort: false negatives are expected and\n   * documented in the user-facing telemetry docs.\n   */\n  readonly agent: string | null;\n  /**\n   * Best-effort reader for the project's `package.json`, used only to derive\n   * the optional `tsVersion` telemetry field. Returning `null` means unknown.\n   */\n  readonly readProjectPackageJson: () => string | null;\n}\n\n/**\n * Identify the runtime the sender is running in. Same-runtime as the\n * parent is a correctness requirement: the parent forked us via\n * `child_process.fork`, which inherits the parent's runtime. Detection\n * keys on the runtime-specific version field rather than env vars so a\n * spoofed env can't lie about the actual interpreter.\n */\nfunction resolveRuntime(versions: VersionsSnapshot): {\n  readonly name: 'node' | 'bun' | 'deno';\n  readonly version: string;\n} {\n  if (versions.bun !== undefined) {\n    return { name: 'bun', version: versions.bun };\n  }\n  if (versions.deno !== undefined) {\n    return { name: 'deno', version: versions.deno };\n  }\n  return { name: 'node', version: versions.node };\n}\n\n/**\n * Parse `npm_config_user_agent` into a `<pm>/<version>` token. The\n * value, when present, looks like\n * `\"pnpm/10.27.0 npm/? node/v24.13.0 darwin arm64\"` — we take the first\n * whitespace-separated token. Any failure → `null`.\n */\nexport function parsePackageManager(userAgent: string | undefined): string | null {\n  if (userAgent === undefined) return null;\n  const first = userAgent.split(/\\s+/)[0];\n  if (first === undefined || first.length === 0) return null;\n  if (!first.includes('/')) return null;\n  return first;\n}\n\n/**\n * Read the user's project `package.json` and resolve a TypeScript\n * version from `devDependencies.typescript` (preferred) or\n * `dependencies.typescript`. Strips a leading `^` or `~` semver\n * prefix. Returns `null` on any failure mode — file missing,\n * unreadable, malformed JSON, key absent, not a string.\n */\nexport function readTsVersionFromPackageJson(raw: string | null): string | null {\n  if (raw === null) return null;\n  let parsed: Record<string, unknown>;\n  try {\n    parsed = JSON.parse(raw) as Record<string, unknown>;\n  } catch {\n    return null;\n  }\n  const candidate =\n    pickStringDep(parsed['devDependencies']) ?? pickStringDep(parsed['dependencies']);\n  if (candidate === null) return null;\n  return candidate.replace(/^[\\^~]/, '');\n}\n\nfunction pickStringDep(deps: unknown): string | null {\n  if (deps === null || typeof deps !== 'object' || Array.isArray(deps)) return null;\n  const value = (deps as Record<string, unknown>)['typescript'];\n  return typeof value === 'string' ? value : null;\n}\n\n/**\n * Build the full backend event from the parent's payload, the\n * c12-loaded project-config slice, and the child's per-process\n * snapshot. Pure given a `projectConfig` + `EnrichEnvironment`.\n */\nexport function buildTelemetryEvent(\n  payload: ParentToSenderPayload,\n  projectConfig: ProjectConfigFields,\n  env: EnrichEnvironment,\n): TelemetryEvent {\n  const runtime = resolveRuntime(env.versions);\n  return {\n    installationId: payload.installationId,\n    version: payload.version,\n    command: payload.command,\n    flags: payload.flags,\n    runtimeName: runtime.name,\n    runtimeVersion: runtime.version,\n    os: env.platform,\n    arch: env.arch,\n    packageManager: parsePackageManager(env.env['npm_config_user_agent']),\n    databaseTarget: projectConfig.databaseTarget,\n    tsVersion: readTsVersionFromPackageJson(env.readProjectPackageJson()),\n    agent: env.agent,\n    extensions: projectConfig.extensions,\n  };\n}\n\n/**\n * Resolve the agent label for the telemetry event via\n * `@vercel/detect-agent`, collapsing its discriminated result to the\n * event's `string | null` shape. Any detection failure counts as\n * \"no agent\" — telemetry is best-effort and non-blocking.\n */\nasync function resolveAgentLabel(): Promise<string | null> {\n  try {\n    const result = await determineAgent();\n    return result.isAgent ? result.agent.name : null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Convenience for the sender entry: build the event from the live\n * `process` plus a c12 load of `prisma-next.config.*` from\n * `payload.projectRoot` plus a real project-package.json reader,\n * swallowing any I/O errors in the file read.\n *\n * The parent's `payload.databaseTarget` (when present) wins over the\n * c12-derived value. The parent sets this for the first-`init` run,\n * where the config file does not exist on disk yet but the user has\n * just declared a target via the consent prompt; every other\n * invocation leaves it unset and the c12 load supplies the value.\n */\nexport async function buildTelemetryEventFromProcess(\n  payload: ParentToSenderPayload,\n): Promise<TelemetryEvent> {\n  const loadedConfig = await loadProjectConfig(payload.projectRoot);\n  const projectConfig: ProjectConfigFields = {\n    databaseTarget: payload.databaseTarget ?? loadedConfig.databaseTarget,\n    extensions: loadedConfig.extensions,\n  };\n  return buildTelemetryEvent(payload, projectConfig, {\n    platform: process.platform,\n    arch: process.arch,\n    versions: process.versions,\n    env: process.env,\n    agent: await resolveAgentLabel(),\n    readProjectPackageJson: () => {\n      try {\n        return readFileSync(join(payload.projectRoot, 'package.json'), 'utf-8');\n      } catch {\n        return null;\n      }\n    },\n  });\n}\n"],"mappings":"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"}