diff --git a/docs/assets/auth-logout-update-docs.png b/docs/assets/auth-logout-update-docs.png new file mode 100644 index 00000000..a199f73a Binary files /dev/null and b/docs/assets/auth-logout-update-docs.png differ diff --git a/docs/assets/auth-logout-update-npm.png b/docs/assets/auth-logout-update-npm.png new file mode 100644 index 00000000..fceea57b Binary files /dev/null and b/docs/assets/auth-logout-update-npm.png differ diff --git a/docs/product/output-conventions.md b/docs/product/output-conventions.md index aab53d9f..fbae7b2d 100644 --- a/docs/product/output-conventions.md +++ b/docs/product/output-conventions.md @@ -73,7 +73,7 @@ when all of these are true: - `--quiet` is not active - CI is not detected - `NO_UPDATE_NOTIFIER` is not set -- cached update-check state already shows a newer official `@prisma/cli` +- cached update-check state already shows a newer official `prisma` version The notification is human-only stderr output. It must never be written to @@ -87,12 +87,13 @@ Update available: prisma -> Run to update. ``` -When the CLI cannot confidently infer the install context, link to installation -docs instead of guessing a package-manager command: +The update command must name the published `prisma` package. When the CLI +cannot confidently infer the install context, link directly to the Prisma CLI +installation instructions instead of guessing a package-manager command: ```text Update available: prisma -> -See https://www.prisma.io/docs/orm/tools/prisma-cli for update instructions. +See https://www.prisma.io/docs/orm/release-status#what-you-get-when-you-install-today for installation commands. ``` ## Out-Of-Date Agent Skills @@ -195,6 +196,10 @@ Current MVP commands map to patterns like this: No current MVP command uses `verify` or `inspect`, but new commands must still choose one existing pattern rather than inventing a new one casually. +`auth logout` reports the number of ended workspace sessions in its success +summary, not as a second `ended` field. The structured result retains +`endedCount` and `workspaceIds` for automation. + ### Workspace session identity `auth login --ui-context prisma-plugin` changes only the browser completion diff --git a/packages/cli/src/cli-name.ts b/packages/cli/src/cli-name.ts index 065536d5..fdfe3fdc 100644 --- a/packages/cli/src/cli-name.ts +++ b/packages/cli/src/cli-name.ts @@ -8,8 +8,7 @@ */ export const CLI_NAME = "prisma"; -/** The unified CLI's docs section (also the update-check fallback - * instruction URL). */ +/** The unified CLI's docs section. */ export const CLI_DOCS_URL = "https://www.prisma.io/docs/cli"; /** diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index ade46ac8..b74e3236 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -23,16 +23,9 @@ function presentationsFor( result.endedCount === 0 ? "No workspace sessions to end." : `Ended ${result.endedCount} workspace ${result.endedCount === 1 ? "session" : "sessions"}.`; - const rows = [{ label: "ended", value: String(result.endedCount) }]; return { json: () => result, human: () => [ - { - kind: "summary", - status: "info", - text: "Clearing your stored workspace sessions.", - }, - { kind: "fields", rows }, { kind: "summary", status: "ok", text: summary }, ...(environmentInForce ? [ @@ -44,7 +37,7 @@ function presentationsFor( ] : []), ], - stdout: () => rows.map((row) => `${row.label}: ${row.value}`), + stdout: () => [], next: () => [SIGN_IN], }; } diff --git a/packages/cli/src/update-check.ts b/packages/cli/src/update-check.ts index ed3138f8..a3b3d226 100644 --- a/packages/cli/src/update-check.ts +++ b/packages/cli/src/update-check.ts @@ -5,7 +5,6 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { CLI_DOCS_URL } from "./cli-name"; import { compareVersionStrings } from "./lib/semver-order"; import { getCliName, getCliVersion } from "./lib/version"; @@ -18,9 +17,11 @@ export interface UpdateCheckRuntime { } const UPDATE_CHECK_FILE_NAME = "update-check.json"; -const FALLBACK_INSTALL_DOCS_URL = CLI_DOCS_URL; +const PACKAGE_NAME = "prisma"; +const FALLBACK_INSTALL_DOCS_URL = + "https://www.prisma.io/docs/orm/release-status#what-you-get-when-you-install-today"; const NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000; -const REGISTRY_URL = "https://registry.npmjs.org/@prisma%2fcli"; +const REGISTRY_URL = "https://registry.npmjs.org/prisma"; const REGISTRY_TIMEOUT_MS = 3_000; export interface UpdateCheckState { @@ -79,7 +80,8 @@ export async function maybeWriteCachedUpdateNotification( try { const cacheDir = resolveUpdateCheckCacheDir(runtime); const store = new UpdateCheckStore(cacheDir); - const state = await store.read(); + const cached = await store.read(); + const state = cached?.packageName === PACKAGE_NAME ? cached : null; const latestVersion = state?.latestVersion; if ( @@ -95,7 +97,7 @@ export async function maybeWriteCachedUpdateNotification( ); await store.write({ ...state, - packageName: "@prisma/cli", + packageName: PACKAGE_NAME, installedVersion: getCliVersion(), notifiedAt: new Date().toISOString(), }); @@ -124,10 +126,11 @@ export async function runUpdateDiscovery(options: { } const store = new UpdateCheckStore(options.cacheDir); - const previousState = await store.read(); + const cached = await store.read(); + const previousState = cached?.packageName === PACKAGE_NAME ? cached : null; await store.write({ ...previousState, - packageName: "@prisma/cli", + packageName: PACKAGE_NAME, installedVersion: options.installedVersion, latestVersion, checkedAt: (options.now ?? new Date()).toISOString(), @@ -216,7 +219,7 @@ async function scheduleRemoteDiscovery( const checkedAt = new Date().toISOString(); await store.write({ ...state, - packageName: "@prisma/cli", + packageName: PACKAGE_NAME, installedVersion: getCliVersion(), checkedAt, }); @@ -257,27 +260,27 @@ export function selectUpdateInstruction( return docsInstruction(); } - if (entrypoint.includes("/node_modules/.bin/")) { + if ( + env.npm_config_global === "true" || + isLikelyGlobalNpmEntrypoint(entrypoint) + ) { + return commandInstruction("npm install --global prisma@latest"); + } + + if (entrypoint.includes("/node_modules/")) { if (userAgent.startsWith("pnpm")) { - return commandInstruction("pnpm add -D @prisma/cli@latest"); + return commandInstruction("pnpm add -D prisma@latest"); } if (userAgent.startsWith("bun")) { - return commandInstruction("bun add -d @prisma/cli@latest"); + return commandInstruction("bun add -d prisma@latest"); } if (userAgent.startsWith("npm")) { - return commandInstruction("npm install --save-dev @prisma/cli@latest"); + return commandInstruction("npm install --save-dev prisma@latest"); } } - if ( - env.npm_config_global === "true" || - isLikelyGlobalNpmEntrypoint(entrypoint) - ) { - return commandInstruction("npm install --global @prisma/cli@latest"); - } - return docsInstruction(); } @@ -297,7 +300,7 @@ function renderUpdateInstruction(instruction: UpdateInstruction): string { return `Run ${instruction.value} to update.`; } - return `See ${instruction.value} for update instructions.`; + return `See ${instruction.value} for installation commands.`; } function isEphemeralInvocation(entrypoint: string, lifecycle: string): boolean { @@ -305,14 +308,14 @@ function isEphemeralInvocation(entrypoint: string, lifecycle: string): boolean { lifecycle === "npx" || lifecycle === "pnpx" || entrypoint.includes("/_npx/") || - entrypoint.includes("/.bun/") + entrypoint.includes("/.bun/install/cache/") ); } function isLikelyGlobalNpmEntrypoint(entrypoint: string): boolean { return ( - /\/npm\/prisma-cli(\.cmd|\.exe)?$/.test(entrypoint) || - /\/npm-global\/bin\/prisma-cli$/.test(entrypoint) + /\/npm\/prisma(\.cmd|\.exe)?$/.test(entrypoint) || + /\/npm-global\/bin\/prisma$/.test(entrypoint) ); } diff --git a/packages/cli/tests/golden-rendering.test.ts b/packages/cli/tests/golden-rendering.test.ts index 3bd72bbf..5289b10b 100644 --- a/packages/cli/tests/golden-rendering.test.ts +++ b/packages/cli/tests/golden-rendering.test.ts @@ -101,14 +101,9 @@ describe("golden rendering", () => { expect(result.exitCode).toBe(0); expect(result.stderr).toBe( - "ℹ Clearing your stored workspace sessions.\n" + - "\n" + - "ended: 1\n" + - "\n" + - "✔ Ended 1 workspace session.\n" + - "→ Sign in: prisma auth login\n", + "✔ Ended 1 workspace session.\n" + "→ Sign in: prisma auth login\n", ); - expect(result.stdout).toBe("ended: 1\n"); + expect(result.stdout).toBe(""); }); it("table (representative: auth workspace list)", async () => { diff --git a/packages/cli/tests/update-check-wiring.test.ts b/packages/cli/tests/update-check-wiring.test.ts index ba67af36..6de084df 100644 --- a/packages/cli/tests/update-check-wiring.test.ts +++ b/packages/cli/tests/update-check-wiring.test.ts @@ -74,7 +74,7 @@ function nextMajorVersion(): string { async function seedStaleUpdate(updateCheckDir: string): Promise { await new UpdateCheckStore(updateCheckDir).write({ - packageName: "@prisma/cli", + packageName: "prisma", installedVersion: getCliVersion(), latestVersion: nextMajorVersion(), checkedAt: new Date().toISOString(), @@ -101,6 +101,9 @@ describe("main update-check wiring", () => { expect(proc.stderrText).toContain( `Update available: prisma ${getCliVersion()} -> ${nextMajorVersion()}`, ); + expect(proc.stderrText).toContain( + "See https://www.prisma.io/docs/orm/release-status#what-you-get-when-you-install-today for installation commands.", + ); expect(proc.stdoutText).toBe(""); }); @@ -153,6 +156,26 @@ describe("main update-check wiring", () => { expect(second.stderrText).not.toContain("Update available"); }); + it("discards cached versions from the old @prisma/cli package", async () => { + const updateCheckDir = await makeUpdateCheckDir(); + await new UpdateCheckStore(updateCheckDir).write({ + packageName: "@prisma/cli", + installedVersion: getCliVersion(), + latestVersion: nextMajorVersion(), + checkedAt: new Date().toISOString(), + }); + const proc = makeProcess({ env: updateCheckEnv(updateCheckDir) }); + + await main(proc, stubCli); + + expect(proc.stderrText).not.toContain("Update available"); + expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1); + const state = JSON.parse( + await readFile(path.join(updateCheckDir, "update-check.json"), "utf8"), + ) as Record; + expect(state.packageName).toBe("prisma"); + }); + it("stays silent in json mode (legacy behavior, copied)", async () => { const updateCheckDir = await makeUpdateCheckDir(); await seedStaleUpdate(updateCheckDir); @@ -219,7 +242,7 @@ describe("main update-check wiring", () => { it("skips the refresh spawn inside the 24-hour discovery interval", async () => { const updateCheckDir = await makeUpdateCheckDir(); await new UpdateCheckStore(updateCheckDir).write({ - packageName: "@prisma/cli", + packageName: "prisma", installedVersion: getCliVersion(), checkedAt: new Date().toISOString(), }); diff --git a/packages/cli/tests/update-check.test.ts b/packages/cli/tests/update-check.test.ts index cfc88fcb..5b4d0ea7 100644 --- a/packages/cli/tests/update-check.test.ts +++ b/packages/cli/tests/update-check.test.ts @@ -13,17 +13,21 @@ import { createTempCwd } from "./helpers"; describe("update discovery and instructions", () => { it("persists successful remote discovery results from injected registry metadata", async () => { const { updateCheckDir } = await createUpdateCheckTestDirs(); + let requestedUrl: string | undefined; await runUpdateDiscovery({ cacheDir: updateCheckDir, installedVersion: getCliVersion(), now: new Date("2026-01-02T00:00:00.000Z"), - fetchImpl: async () => - Response.json({ "dist-tags": { latest: "9.8.7" } }), + fetchImpl: async (url) => { + requestedUrl = String(url); + return Response.json({ "dist-tags": { latest: "9.8.7" } }); + }, }); + expect(requestedUrl).toBe("https://registry.npmjs.org/prisma"); expect(await readUpdateCheckState(updateCheckDir)).toMatchObject({ - packageName: "@prisma/cli", + packageName: "prisma", installedVersion: getCliVersion(), latestVersion: "9.8.7", checkedAt: "2026-01-02T00:00:00.000Z", @@ -33,7 +37,7 @@ describe("update discovery and instructions", () => { it("preserves notification throttling when remote discovery succeeds", async () => { const { updateCheckDir } = await createUpdateCheckTestDirs(); await new UpdateCheckStore(updateCheckDir).write({ - packageName: "@prisma/cli", + packageName: "prisma", installedVersion: getCliVersion(), latestVersion: "9.8.6", checkedAt: "2026-01-01T00:00:00.000Z", @@ -76,10 +80,10 @@ describe("update discovery and instructions", () => { { name: "local npm", env: { npm_config_user_agent: "npm/10.9.0 node/v24.14.1 darwin arm64" }, - argv: ["node", "/repo/node_modules/.bin/prisma-cli"], + argv: ["node", "/repo/node_modules/prisma/dist/prisma.js"], expected: { type: "command", - value: "npm install --save-dev @prisma/cli@latest", + value: "npm install --save-dev prisma@latest", }, }, { @@ -88,10 +92,22 @@ describe("update discovery and instructions", () => { npm_config_user_agent: "npm/10.9.0 node/v24.14.1 darwin arm64", npm_config_global: "true", }, - argv: ["node", "/usr/local/bin/prisma-cli"], + argv: ["node", "/usr/local/bin/prisma"], + expected: { + type: "command", + value: "npm install --global prisma@latest", + }, + }, + { + name: "global npm under node_modules", + env: { + npm_config_user_agent: "npm/10.9.0 node/v24.14.1 darwin arm64", + npm_config_global: "true", + }, + argv: ["node", "/usr/local/lib/node_modules/prisma/dist/prisma.js"], expected: { type: "command", - value: "npm install --global @prisma/cli@latest", + value: "npm install --global prisma@latest", }, }, { @@ -99,24 +115,31 @@ describe("update discovery and instructions", () => { env: { npm_config_user_agent: "pnpm/10.30.0 npm/? node/v24.14.1 darwin arm64", }, - argv: ["node", "/repo/node_modules/.bin/prisma-cli"], - expected: { type: "command", value: "pnpm add -D @prisma/cli@latest" }, + argv: [ + "node", + "/repo/node_modules/.pnpm/prisma@8.0.0-rc.17/node_modules/prisma/dist/prisma.js", + ], + expected: { type: "command", value: "pnpm add -D prisma@latest" }, }, { name: "local bun", env: { npm_config_user_agent: "bun/1.3.0 npm/? node/v24.14.1 darwin arm64", }, - argv: ["node", "/repo/node_modules/.bin/prisma-cli"], - expected: { type: "command", value: "bun add -d @prisma/cli@latest" }, + argv: [ + "node", + "/repo/node_modules/.bun/prisma@8.0.0-rc.17/node_modules/prisma/dist/prisma.js", + ], + expected: { type: "command", value: "bun add -d prisma@latest" }, }, { name: "npx", env: { npm_lifecycle_event: "npx" }, - argv: ["node", "/Users/alice/.npm/_npx/123/node_modules/.bin/prisma-cli"], + argv: ["node", "/Users/alice/.npm/_npx/123/node_modules/.bin/prisma"], expected: { type: "docs", - value: "https://www.prisma.io/docs/cli", + value: + "https://www.prisma.io/docs/orm/release-status#what-you-get-when-you-install-today", }, }, { @@ -125,28 +148,31 @@ describe("update discovery and instructions", () => { npm_lifecycle_event: "pnpx", npm_config_user_agent: "pnpm/10.30.0", }, - argv: ["node", "/repo/node_modules/.bin/prisma-cli"], + argv: ["node", "/repo/node_modules/.bin/prisma"], expected: { type: "docs", - value: "https://www.prisma.io/docs/cli", + value: + "https://www.prisma.io/docs/orm/release-status#what-you-get-when-you-install-today", }, }, { name: "bunx", env: { npm_config_user_agent: "bun/1.3.0" }, - argv: ["node", "/Users/alice/.bun/install/cache/@prisma/cli/prisma-cli"], + argv: ["node", "/Users/alice/.bun/install/cache/prisma/prisma"], expected: { type: "docs", - value: "https://www.prisma.io/docs/cli", + value: + "https://www.prisma.io/docs/orm/release-status#what-you-get-when-you-install-today", }, }, { name: "unknown", env: {}, - argv: ["node", "/some/path/prisma-cli"], + argv: ["node", "/some/path/prisma"], expected: { type: "docs", - value: "https://www.prisma.io/docs/cli", + value: + "https://www.prisma.io/docs/orm/release-status#what-you-get-when-you-install-today", }, }, ])("selects update instructions for $name", ({ env, argv, expected }) => {