@prisma/cli 3.0.0-dev.51.1 → 3.0.0-dev.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { runUpdateDiscoveryWorker } from "./shell/update-check.js";
2
3
  import { runCli } from "./cli2.js";
3
4
  import process from "node:process";
4
5
  //#region src/bin.ts
5
- runCli().then((exitCode) => {
6
+ if (process.env.PRISMA_CLI_RUN_UPDATE_CHECK_WORKER === "1") runUpdateDiscoveryWorker().then(() => {
7
+ process.exitCode = 0;
8
+ });
9
+ else runCli().then((exitCode) => {
6
10
  process.exitCode = exitCode;
7
11
  });
8
12
  //#endregion
package/dist/cli2.js CHANGED
@@ -13,6 +13,7 @@ import { createProjectCommand } from "./commands/project/index.js";
13
13
  import { getCliName, getCliVersion } from "./lib/version.js";
14
14
  import { runVersion } from "./controllers/version.js";
15
15
  import { createVersionCommand } from "./commands/version/index.js";
16
+ import { maybeWriteCachedUpdateNotification } from "./shell/update-check.js";
16
17
  import process from "node:process";
17
18
  import { Command, CommanderError, Option } from "commander";
18
19
  //#region src/cli.ts
@@ -21,6 +22,7 @@ async function runCli(options = {}) {
21
22
  const program = createProgram(runtime);
22
23
  process.exitCode = 0;
23
24
  try {
25
+ await maybeWriteCachedUpdateNotification(runtime);
24
26
  if (runtime.argv.includes("--version")) return await handleVersionFlag(runtime);
25
27
  const bareHelpCommand = resolveBareHelpCommand(program, runtime.argv);
26
28
  if (bareHelpCommand) {
@@ -4,6 +4,7 @@ import open from "open";
4
4
  import { AuthError, createManagementApiSdk } from "@prisma/management-api-sdk";
5
5
  import events from "node:events";
6
6
  import http from "node:http";
7
+ import readline from "node:readline/promises";
7
8
  //#region src/lib/auth/login.ts
8
9
  var AuthError$1 = class extends Error {
9
10
  constructor(message) {
@@ -14,11 +15,15 @@ var AuthError$1 = class extends Error {
14
15
  async function login(options = {}) {
15
16
  const hostname = options.hostname ?? "localhost";
16
17
  const port = options.port ?? 0;
18
+ const input = options.input ?? process.stdin;
19
+ const output = options.output ?? process.stderr;
20
+ const interactive = input.isTTY === true;
17
21
  const server = http.createServer();
18
22
  server.listen({
19
23
  host: hostname,
20
24
  port
21
25
  });
26
+ const pasteAbort = new AbortController();
22
27
  try {
23
28
  const state = new LoginState({
24
29
  hostname,
@@ -28,9 +33,21 @@ async function login(options = {}) {
28
33
  apiBaseUrl: options.apiBaseUrl,
29
34
  authBaseUrl: options.authBaseUrl,
30
35
  openUrl: options.openUrl,
31
- env: options.env
36
+ env: options.env,
37
+ output
32
38
  });
33
- const authResult = new Promise((resolve, reject) => {
39
+ let completed = false;
40
+ let completion;
41
+ const completeOnce = (url) => {
42
+ if (!completion) completion = state.handleCallback(url).then(() => {
43
+ completed = true;
44
+ }, (error) => {
45
+ completion = void 0;
46
+ throw error;
47
+ });
48
+ return completion;
49
+ };
50
+ const httpResult = new Promise((resolve, reject) => {
34
51
  server.on("request", async (req, res) => {
35
52
  const url = new URL(`http://${state.host}${req.url}`);
36
53
  if (url.pathname !== "/auth/callback") {
@@ -38,8 +55,14 @@ async function login(options = {}) {
38
55
  res.end("Not found");
39
56
  return;
40
57
  }
58
+ if (completed) {
59
+ const workspaceName = await state.resolveWorkspaceName();
60
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
61
+ res.end(renderSuccessPage(workspaceName));
62
+ return;
63
+ }
41
64
  try {
42
- await state.handleCallback(url);
65
+ await completeOnce(url);
43
66
  } catch (error) {
44
67
  res.statusCode = 400;
45
68
  const message = error instanceof Error ? error.message : String(error);
@@ -53,18 +76,65 @@ async function login(options = {}) {
53
76
  resolve();
54
77
  });
55
78
  });
56
- await state.openLoginPage();
57
- await authResult;
79
+ await state.openLoginPage(interactive);
80
+ if (interactive) {
81
+ const pasteResult = consumePastedCallback({
82
+ input,
83
+ output,
84
+ signal: pasteAbort.signal,
85
+ complete: completeOnce
86
+ });
87
+ await Promise.race([httpResult, pasteResult]);
88
+ } else await httpResult;
58
89
  } finally {
90
+ pasteAbort.abort();
59
91
  if (server.listening) await new Promise((resolve) => server.close(() => resolve()));
60
92
  }
61
93
  }
94
+ async function consumePastedCallback(options) {
95
+ if (!options.input.isTTY) return;
96
+ const rl = readline.createInterface({
97
+ input: options.input,
98
+ output: options.output
99
+ });
100
+ try {
101
+ for (;;) {
102
+ let answer;
103
+ try {
104
+ answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
105
+ } catch (error) {
106
+ if (error?.name === "AbortError") return;
107
+ throw error;
108
+ }
109
+ const trimmed = answer.trim().replace(/^["']|["']$/g, "");
110
+ let url;
111
+ try {
112
+ if (!trimmed) throw new Error("empty input");
113
+ url = new URL(trimmed);
114
+ } catch {
115
+ options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
116
+ continue;
117
+ }
118
+ try {
119
+ await options.complete(url);
120
+ return;
121
+ } catch (error) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
124
+ continue;
125
+ }
126
+ }
127
+ } finally {
128
+ rl.close();
129
+ }
130
+ }
62
131
  var LoginState = class {
63
132
  latestVerifier;
64
133
  latestState;
65
134
  sdk;
66
135
  openUrl;
67
136
  tokenStorage;
137
+ output;
68
138
  constructor(options) {
69
139
  this.options = options;
70
140
  this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env);
@@ -76,8 +146,9 @@ var LoginState = class {
76
146
  authBaseUrl: options.authBaseUrl
77
147
  });
78
148
  this.openUrl = options.openUrl ?? open;
149
+ this.output = options.output;
79
150
  }
80
- async openLoginPage() {
151
+ async openLoginPage(interactive) {
81
152
  const { url, state, verifier } = await this.sdk.getLoginUrl({
82
153
  scope: "workspace:admin offline_access",
83
154
  additionalParams: {
@@ -88,7 +159,17 @@ var LoginState = class {
88
159
  });
89
160
  this.latestState = state;
90
161
  this.latestVerifier = verifier;
91
- await this.openUrl(url);
162
+ if (interactive) this.printLoginInstructions(url);
163
+ try {
164
+ await this.openUrl(url);
165
+ } catch (error) {
166
+ if (!interactive) throw error;
167
+ }
168
+ }
169
+ printLoginInstructions(url) {
170
+ const output = this.output;
171
+ if (!output) return;
172
+ output.write(`\nOpen this URL to sign in: ${url}\n\nIf the browser opens on another machine, finish sign-in there. When it\nredirects to localhost, copy the full localhost URL from the address bar\nand paste it here.\n\n`);
92
173
  }
93
174
  async handleCallback(url) {
94
175
  if (url.pathname !== "/auth/callback") throw new AuthError$1("Not a callback URL");
@@ -0,0 +1,247 @@
1
+ import { getCliName, getCliVersion } from "../lib/version.js";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import os from "node:os";
6
+ import { spawn } from "node:child_process";
7
+ //#region src/shell/update-check.ts
8
+ const UPDATE_CHECK_FILE_NAME = "update-check.json";
9
+ const FALLBACK_INSTALL_DOCS_URL = "https://www.prisma.io/docs/orm/tools/prisma-cli";
10
+ const NOTIFICATION_INTERVAL_MS = 1440 * 60 * 1e3;
11
+ const REGISTRY_URL = "https://registry.npmjs.org/@prisma%2fcli";
12
+ const REGISTRY_TIMEOUT_MS = 3e3;
13
+ var UpdateCheckStore = class {
14
+ filePath;
15
+ constructor(cacheDir) {
16
+ this.filePath = path.join(cacheDir, UPDATE_CHECK_FILE_NAME);
17
+ }
18
+ async read() {
19
+ try {
20
+ return JSON.parse(await readFile(this.filePath, "utf8"));
21
+ } catch (error) {
22
+ if (isUnreadableCacheError(error)) return null;
23
+ throw error;
24
+ }
25
+ }
26
+ async write(state) {
27
+ const dir = path.dirname(this.filePath);
28
+ const tempPath = path.join(dir, `${UPDATE_CHECK_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`);
29
+ await mkdir(dir, { recursive: true });
30
+ await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
31
+ await rename(tempPath, this.filePath);
32
+ }
33
+ };
34
+ async function maybeWriteCachedUpdateNotification(runtime) {
35
+ if (!canRunUpdateCheck(runtime)) return;
36
+ try {
37
+ const cacheDir = resolveUpdateCheckCacheDir(runtime);
38
+ const store = new UpdateCheckStore(cacheDir);
39
+ const state = await store.read();
40
+ const latestVersion = state?.latestVersion;
41
+ if (latestVersion && isInstalledVersionStale(getCliVersion(), latestVersion) && shouldNotify(state)) {
42
+ runtime.stderr.write(renderUpdateNotification(latestVersion, selectUpdateInstruction(runtime.env)));
43
+ await store.write({
44
+ ...state,
45
+ packageName: "@prisma/cli",
46
+ installedVersion: getCliVersion(),
47
+ notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
48
+ });
49
+ }
50
+ await scheduleRemoteDiscovery(runtime, store, state, cacheDir);
51
+ } catch {
52
+ return;
53
+ }
54
+ }
55
+ async function runUpdateDiscovery(options) {
56
+ try {
57
+ const latestVersion = await fetchLatestVersion(options.registryUrl ?? REGISTRY_URL, options.fetchImpl ?? fetch);
58
+ if (!latestVersion) return;
59
+ const store = new UpdateCheckStore(options.cacheDir);
60
+ const previousState = await store.read();
61
+ await store.write({
62
+ ...previousState,
63
+ packageName: "@prisma/cli",
64
+ installedVersion: options.installedVersion,
65
+ latestVersion,
66
+ checkedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString()
67
+ });
68
+ } catch {
69
+ return;
70
+ }
71
+ }
72
+ function isUnreadableCacheError(error) {
73
+ const code = error.code;
74
+ return code === "ENOENT" || code === "EACCES" || code === "EPERM" || error instanceof SyntaxError;
75
+ }
76
+ async function runUpdateDiscoveryWorker(env = process.env) {
77
+ const cacheDir = env.PRISMA_CLI_UPDATE_CHECK_DIR;
78
+ const installedVersion = env.PRISMA_CLI_UPDATE_CHECK_INSTALLED_VERSION;
79
+ if (!cacheDir || !installedVersion) return;
80
+ await runUpdateDiscovery({
81
+ cacheDir,
82
+ installedVersion,
83
+ registryUrl: env.PRISMA_CLI_UPDATE_CHECK_REGISTRY_URL
84
+ });
85
+ }
86
+ function canRunUpdateCheck(runtime) {
87
+ if (runtime.env.NO_UPDATE_NOTIFIER !== void 0) return false;
88
+ if (isTestRuntime(runtime.env) && runtime.env.PRISMA_CLI_TEST_ENABLE_UPDATE_CHECK !== "1") return false;
89
+ if (runtime.env.CI || runtime.env.GITHUB_ACTIONS) return false;
90
+ if (!runtime.stderr.isTTY) return false;
91
+ if (runtime.argv.includes("--json") || runtime.argv.includes("--quiet") || runtime.argv.includes("-q")) return false;
92
+ if (runtime.argv.includes("--version")) return false;
93
+ return true;
94
+ }
95
+ function shouldNotify(state) {
96
+ return !state.notifiedAt || isAtLeastIntervalAgo(state.notifiedAt);
97
+ }
98
+ async function scheduleRemoteDiscovery(runtime, store, state, cacheDir) {
99
+ if (state?.checkedAt && !isAtLeastIntervalAgo(state.checkedAt)) return;
100
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
101
+ await store.write({
102
+ ...state,
103
+ packageName: "@prisma/cli",
104
+ installedVersion: getCliVersion(),
105
+ checkedAt
106
+ });
107
+ if (isTestRuntime(runtime.env)) return;
108
+ const entrypoint = process.argv[1];
109
+ if (!entrypoint) return;
110
+ spawn(process.execPath, [entrypoint], {
111
+ detached: true,
112
+ stdio: "ignore",
113
+ env: {
114
+ ...process.env,
115
+ PRISMA_CLI_RUN_UPDATE_CHECK_WORKER: "1",
116
+ PRISMA_CLI_UPDATE_CHECK_DIR: cacheDir,
117
+ PRISMA_CLI_UPDATE_CHECK_INSTALLED_VERSION: getCliVersion(),
118
+ PRISMA_CLI_UPDATE_CHECK_REGISTRY_URL: runtime.env.PRISMA_CLI_UPDATE_CHECK_REGISTRY_URL ?? REGISTRY_URL
119
+ }
120
+ }).unref();
121
+ }
122
+ function selectUpdateInstruction(env, processArgv = process.argv) {
123
+ const entrypoint = (processArgv[1] ?? "").replace(/\\/g, "/").toLowerCase();
124
+ const userAgent = env.npm_config_user_agent?.toLowerCase() ?? "";
125
+ if (isEphemeralInvocation(entrypoint, env.npm_lifecycle_event?.toLowerCase() ?? "")) return docsInstruction();
126
+ if (entrypoint.includes("/node_modules/.bin/")) {
127
+ if (userAgent.startsWith("pnpm")) return commandInstruction("pnpm add -D @prisma/cli@latest");
128
+ if (userAgent.startsWith("bun")) return commandInstruction("bun add -d @prisma/cli@latest");
129
+ if (userAgent.startsWith("npm")) return commandInstruction("npm install --save-dev @prisma/cli@latest");
130
+ }
131
+ if (env.npm_config_global === "true" || isLikelyGlobalNpmEntrypoint(entrypoint)) return commandInstruction("npm install --global @prisma/cli@latest");
132
+ return docsInstruction();
133
+ }
134
+ function renderUpdateNotification(latestVersion, instruction) {
135
+ return [
136
+ `Update available: ${getCliName()} ${getCliVersion()} -> ${latestVersion}`,
137
+ renderUpdateInstruction(instruction),
138
+ ""
139
+ ].join("\n");
140
+ }
141
+ function renderUpdateInstruction(instruction) {
142
+ if (instruction.type === "command") return `Run ${instruction.value} to update.`;
143
+ return `See ${instruction.value} for update instructions.`;
144
+ }
145
+ function isEphemeralInvocation(entrypoint, lifecycle) {
146
+ return lifecycle === "npx" || lifecycle === "pnpx" || entrypoint.includes("/_npx/") || entrypoint.includes("/.bun/");
147
+ }
148
+ function isLikelyGlobalNpmEntrypoint(entrypoint) {
149
+ return /\/npm\/prisma-cli(\.cmd|\.exe)?$/.test(entrypoint) || /\/npm-global\/bin\/prisma-cli$/.test(entrypoint);
150
+ }
151
+ function commandInstruction(value) {
152
+ return {
153
+ type: "command",
154
+ value
155
+ };
156
+ }
157
+ function docsInstruction() {
158
+ return {
159
+ type: "docs",
160
+ value: FALLBACK_INSTALL_DOCS_URL
161
+ };
162
+ }
163
+ function resolveUpdateCheckCacheDir(runtime) {
164
+ const configured = runtime.env.PRISMA_CLI_UPDATE_CHECK_DIR;
165
+ if (configured?.trim()) return path.resolve(configured);
166
+ if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Caches", "prisma-cli");
167
+ if (process.platform === "win32") {
168
+ const localAppData = runtime.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
169
+ return path.join(localAppData, "prisma-cli", "cache");
170
+ }
171
+ const xdgCacheHome = runtime.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache");
172
+ return path.join(xdgCacheHome, "prisma-cli");
173
+ }
174
+ function isTestRuntime(env) {
175
+ return env.VITEST !== void 0 || env.NODE_ENV === "test";
176
+ }
177
+ function isAtLeastIntervalAgo(value) {
178
+ const timestamp = Date.parse(value);
179
+ return Number.isNaN(timestamp) || Date.now() - timestamp >= NOTIFICATION_INTERVAL_MS;
180
+ }
181
+ function isInstalledVersionStale(installedVersion, latestVersion) {
182
+ const installed = parseVersion(installedVersion);
183
+ const latest = parseVersion(latestVersion);
184
+ if (!installed || !latest) return false;
185
+ return compareVersions(installed, latest) < 0;
186
+ }
187
+ function parseVersion(version) {
188
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version);
189
+ if (!match) return null;
190
+ return {
191
+ major: Number(match[1]),
192
+ minor: Number(match[2]),
193
+ patch: Number(match[3]),
194
+ prerelease: match[4]?.split(".") ?? []
195
+ };
196
+ }
197
+ function compareVersions(left, right) {
198
+ for (const key of [
199
+ "major",
200
+ "minor",
201
+ "patch"
202
+ ]) {
203
+ const diff = left[key] - right[key];
204
+ if (diff !== 0) return diff;
205
+ }
206
+ return comparePrerelease(left.prerelease, right.prerelease);
207
+ }
208
+ function comparePrerelease(left, right) {
209
+ if (left.length === 0 && right.length === 0) return 0;
210
+ if (left.length === 0) return 1;
211
+ if (right.length === 0) return -1;
212
+ const count = Math.max(left.length, right.length);
213
+ for (let index = 0; index < count; index += 1) {
214
+ const leftPart = left[index];
215
+ const rightPart = right[index];
216
+ if (leftPart === void 0) return -1;
217
+ if (rightPart === void 0) return 1;
218
+ const diff = comparePrereleasePart(leftPart, rightPart);
219
+ if (diff !== 0) return diff;
220
+ }
221
+ return 0;
222
+ }
223
+ function comparePrereleasePart(left, right) {
224
+ const leftNumber = /^\d+$/.test(left) ? Number(left) : null;
225
+ const rightNumber = /^\d+$/.test(right) ? Number(right) : null;
226
+ if (leftNumber !== null && rightNumber !== null) return leftNumber - rightNumber;
227
+ if (leftNumber !== null) return -1;
228
+ if (rightNumber !== null) return 1;
229
+ return left.localeCompare(right);
230
+ }
231
+ async function fetchLatestVersion(registryUrl, fetchImpl) {
232
+ const controller = new AbortController();
233
+ const timeout = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS);
234
+ try {
235
+ const response = await fetchImpl(registryUrl, {
236
+ signal: controller.signal,
237
+ headers: { accept: "application/json" }
238
+ });
239
+ if (!response.ok) return null;
240
+ const latest = (await response.json())["dist-tags"]?.latest;
241
+ return typeof latest === "string" ? latest : null;
242
+ } finally {
243
+ clearTimeout(timeout);
244
+ }
245
+ }
246
+ //#endregion
247
+ export { maybeWriteCachedUpdateNotification, runUpdateDiscoveryWorker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.51.1",
3
+ "version": "3.0.0-dev.53.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {