Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/product/output-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,12 @@ context, status, decoration, and errors stay on stderr.

## Design Rule

Delegated commands must support installed Windows `.cmd` shims as well as
native executables and POSIX shebang scripts. The host uses cross-spawn for
platform-specific resolution and argument escaping; callers still pass a
command and argument array, not a shell command string. Human stdio inheritance,
structured diagnostic forwarding, and child exit status remain unchanged.

Human output and JSON output should describe the same underlying model.

The CLI should never require users or agents to learn different meanings for the same command.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@prisma/orm-toolchain": "8.0.0-rc.8",
"@vercel/detect-agent": "^1.2.3",
"better-result": "^2.9.2",
"cross-spawn": "^7.0.6",
"dotenv": "^17.4.2",
"execa": "^9.6.1",
"open": "^11.0.0"
Expand All @@ -67,6 +68,7 @@
"@repo/cli-telemetry": "workspace:8.0.0-rc.13",
"@repo/tsconfig": "workspace:8.0.0-rc.13",
"@types/node": "^22.19.19",
"@types/cross-spawn": "^6.0.6",
"tsdown": "^0.21.10",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/spawn.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawn } from "node:child_process";
import { type Readable, Writable } from "node:stream";
import { pipeline } from "node:stream/promises";
import type { ChildResult, SpawnChild } from "@prisma/cli-engine";
import spawn from "cross-spawn";

interface DiagnosticStream {
write(text: string): unknown;
Expand All @@ -25,7 +25,7 @@ export interface SpawnChildOptions {
}

/**
* The engine's spawn seam, adapted to node:child_process. Human mode
* The engine's spawn seam, adapted through cross-spawn. Human mode
* inherits stdio; structured mode pipes both child output streams to
* diagnostics. Neither mode detaches or opens a new console, so the child
* stays in this process's group (POSIX) or console (Windows).
Expand Down
30 changes: 25 additions & 5 deletions packages/cli/tests/spawn-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,32 @@
*/
import { existsSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { beforeEach, describe, expect, test, vi } from "vitest";

const spawnOptionsSeen = vi.hoisted(() => [] as Array<Record<string, unknown>>);

vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
vi.mock("cross-spawn", async (importOriginal) => {
const actual = await importOriginal<{
default: typeof import("cross-spawn");
}>();
return {
...actual,
spawn: (
default: (
command: string,
args: readonly string[],
options: Record<string, unknown>,
) => {
spawnOptionsSeen.push(options);
return actual.spawn(command, [...args], options as never);
return actual.default(command, [...args], options as never);
},
};
});

import { makeSpawnChild } from "../src/spawn";

const NODE = process.execPath;
const VERSION = /^\d+\.\d+\.\d+$/;
let diagnosticText = "";
const spawnChild = makeSpawnChild({
write: (text) => {
Expand All @@ -55,6 +58,23 @@ async function waitForFile(path: string, timeoutMs = 10_000): Promise<void> {
}

describe("the shipped spawn adapter", () => {
test.skipIf(process.platform !== "win32")(
"runs the installed npm.cmd through the shipped spawn adapter",
async () => {
const command = join(dirname(process.execPath), "npm.cmd");
expect(existsSync(command)).toBe(true);
const child = spawnChild({
command,
args: ["--version"],
cwd: process.cwd(),
env: process.env,
output: "diagnostic",
});
await expect(child.ended).resolves.toEqual({ exitCode: 0, signal: null });
expect(diagnosticText.trim()).toMatch(VERSION);
},
);

test("passes the spawn options the design rests on: inherited stdio, no detached, no new console", async () => {
const child = spawnChild({
command: NODE,
Expand Down
1 change: 1 addition & 0 deletions packages/prisma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@prisma/orm-toolchain": "8.0.0-rc.8",
"@vercel/detect-agent": "^1.2.3",
"better-result": "^2.9.2",
"cross-spawn": "^7.0.6",
"dotenv": "^17.4.2",
"execa": "^9.6.1",
"open": "^11.0.0"
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading