@prisma/cli
Version:
Command-line interface for the Prisma Developer Platform.
193 lines (192 loc) • 6.56 kB
JavaScript
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 {};