UNPKG

@prisma/cli

Version:

Command-line interface for the Prisma Developer Platform.

193 lines (192 loc) 6.56 kB
import { t as isParentToSenderPayload } from "./payload-B88Qvzxk-CrtTXSOe.js"; import { join } from "node:path"; import { readFileSync } from "node:fs"; import { determineAgent } from "@vercel/detect-agent"; //#region ../cli-telemetry/dist/sender.js /** * Identify the runtime the sender is running in. Same-runtime as the * parent is a correctness requirement: the parent forked us via * `child_process.fork`, which inherits the parent's runtime. Detection * keys on the runtime-specific version field rather than env vars so a * spoofed env can't lie about the actual interpreter. */ function resolveRuntime(versions) { if (versions.bun !== void 0) return { name: "bun", version: versions.bun }; if (versions.deno !== void 0) return { name: "deno", version: versions.deno }; return { name: "node", version: versions.node }; } const WHITESPACE = /\s+/; const SEMVER_RANGE_PREFIX = /^[\^~]/; /** * Parse `npm_config_user_agent` into a `<pm>/<version>` token. The * value, when present, looks like * `"pnpm/10.27.0 npm/? node/v24.13.0 darwin arm64"` — we take the first * whitespace-separated token. Any failure → `null`. */ function parsePackageManager(userAgent) { if (userAgent === void 0) return null; const first = userAgent.split(WHITESPACE)[0]; if (first === void 0 || first.length === 0) return null; if (!first.includes("/")) return null; return first; } /** * Read the user's project `package.json` and resolve a TypeScript * version from `devDependencies.typescript` (preferred) or * `dependencies.typescript`. Strips a leading `^` or `~` semver * prefix. Returns `null` on any failure mode — file missing, * unreadable, malformed JSON, key absent, not a string. */ function readTsVersionFromPackageJson(raw) { if (raw === null) return null; let parsed; try { parsed = JSON.parse(raw); } catch { return null; } const candidate = pickStringDep(parsed.devDependencies) ?? pickStringDep(parsed.dependencies); if (candidate === null) return null; return candidate.replace(SEMVER_RANGE_PREFIX, ""); } function pickStringDep(deps) { if (deps === null || typeof deps !== "object" || Array.isArray(deps)) return null; const value = deps.typescript; return typeof value === "string" ? value : null; } /** * Build the full backend event from the parent's payload, the * project-config slice, and the child's per-process snapshot. Pure * given a `projectConfig` + `EnrichEnvironment`. */ function buildTelemetryEvent(payload, projectConfig, env) { const runtime = resolveRuntime(env.versions); return { installationId: payload.installationId, version: payload.version, command: payload.command, flags: payload.flags, runtimeName: runtime.name, runtimeVersion: runtime.version, os: env.platform, arch: env.arch, packageManager: parsePackageManager(env.env.npm_config_user_agent), databaseTarget: projectConfig.databaseTarget, tsVersion: readTsVersionFromPackageJson(env.readProjectPackageJson()), agent: env.agent, extensions: projectConfig.extensions }; } /** * Resolve the agent label for the telemetry event via * `@vercel/detect-agent`, collapsing its discriminated result to the * event's `string | null` shape. Any detection failure counts as * "no agent" — telemetry is best-effort and non-blocking. */ async function resolveAgentLabel() { try { const result = await determineAgent(); return result.isAgent ? result.agent.name : null; } catch { return null; } } /** * Convenience for the sender entry: build the event from the live * `process` plus a real project-package.json reader, swallowing any * I/O errors in the file read. * * The project-config slice is payload-only: `databaseTarget` is the * parent's override when present (else `null`), and `extensions` is * always empty — there is no `prisma-next.config.*` in this product to * derive them from. */ async function buildTelemetryEventFromProcess(payload) { return buildTelemetryEvent(payload, { databaseTarget: payload.databaseTarget ?? null, extensions: [] }, { platform: process.platform, arch: process.arch, versions: process.versions, env: process.env, agent: await resolveAgentLabel(), readProjectPackageJson: () => { try { return readFileSync(join(payload.projectRoot, "package.json"), "utf-8"); } catch { return null; } } }); } /** * Sender script entry — forked into a detached child by the parent CLI via * `child_process.fork(senderPath, [], { detached: true, ... })`. * * Lifecycle: * 1. Wait for the parent's IPC `message` event carrying a * `ParentToSenderPayload`. * 2. Enrich with the local-process probes (runtime, os, arch, agent, * package manager, tsVersion). * 3. POST the event to the endpoint URL with a hard 1.5 s timeout. * 4. Exit 0 unconditionally — successful POST, network failure, server * error, parse error of the response, anything else: same outcome. * * Every error is swallowed; the only escape valve for visibility is * `PRISMA_DEBUG=1`, which routes diagnostics to stderr. In normal * operation no telemetry-originating output ever reaches the user — the * parent's stdio map ignores our streams anyway, but stderr writes are * also held behind the debug flag so the same binary is safe to invoke * directly outside the spawn flow. */ const REQUEST_TIMEOUT_MS = 1500; function debugLog(message, error) { if (process.env.PRISMA_DEBUG !== "1") return; if (error !== void 0) process.stderr.write(`[cli-telemetry] ${message}: ${String(error)}\n`); else process.stderr.write(`[cli-telemetry] ${message}\n`); } async function postEvent(payload) { const event = await buildTelemetryEventFromProcess(payload); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { debugLog(`sent event: status=${(await fetch(payload.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(event), signal: controller.signal })).status}`); } catch (err) { debugLog("send failed", err); } finally { clearTimeout(timer); } } function exitClean() { try { process.disconnect?.(); } catch {} process.exit(0); } process.once("message", (message) => { if (!isParentToSenderPayload(message)) { debugLog("received malformed payload; exiting"); exitClean(); return; } postEvent(message).catch((err) => debugLog("post threw", err)).finally(exitClean); }); const SENDER_IDLE_EXIT_MS = REQUEST_TIMEOUT_MS * 2; setTimeout(exitClean, SENDER_IDLE_EXIT_MS).unref(); //#endregion export {};