@prisma/cli 3.0.0-alpha.0 → 3.0.0-alpha.10

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.
Files changed (55) hide show
  1. package/README.md +1 -16
  2. package/dist/adapters/git.js +49 -0
  3. package/dist/adapters/local-state.js +39 -1
  4. package/dist/cli2.js +60 -4
  5. package/dist/commands/app/index.js +43 -30
  6. package/dist/commands/auth/index.js +3 -2
  7. package/dist/commands/branch/index.js +2 -1
  8. package/dist/commands/env.js +87 -0
  9. package/dist/commands/git/index.js +36 -0
  10. package/dist/commands/project/index.js +12 -14
  11. package/dist/commands/version/index.js +18 -0
  12. package/dist/controllers/app-env.js +223 -0
  13. package/dist/controllers/app.js +1051 -173
  14. package/dist/controllers/auth.js +9 -9
  15. package/dist/controllers/branch.js +6 -6
  16. package/dist/controllers/project.js +451 -161
  17. package/dist/controllers/version.js +12 -0
  18. package/dist/lib/app/bun-project.js +1 -1
  19. package/dist/lib/app/deploy-output.js +15 -0
  20. package/dist/lib/app/env-config.js +57 -0
  21. package/dist/lib/app/env-vars.js +4 -4
  22. package/dist/lib/app/local-dev.js +2 -1
  23. package/dist/lib/app/preview-build.js +130 -144
  24. package/dist/lib/app/preview-interaction.js +2 -35
  25. package/dist/lib/app/preview-progress.js +43 -58
  26. package/dist/lib/app/preview-provider.js +125 -24
  27. package/dist/lib/auth/auth-ops.js +58 -13
  28. package/dist/lib/auth/client.js +1 -1
  29. package/dist/lib/auth/guard.js +1 -1
  30. package/dist/lib/auth/login.js +115 -4
  31. package/dist/lib/project/local-pin.js +51 -0
  32. package/dist/lib/project/resolution.js +201 -0
  33. package/dist/lib/version.js +55 -0
  34. package/dist/output/patterns.js +15 -18
  35. package/dist/presenters/app-env.js +129 -0
  36. package/dist/presenters/app.js +16 -29
  37. package/dist/presenters/auth.js +2 -2
  38. package/dist/presenters/branch.js +6 -6
  39. package/dist/presenters/project.js +87 -44
  40. package/dist/presenters/version.js +29 -0
  41. package/dist/shell/command-meta.js +150 -84
  42. package/dist/shell/command-runner.js +32 -2
  43. package/dist/shell/errors.js +8 -3
  44. package/dist/shell/global-flags.js +13 -1
  45. package/dist/shell/help.js +8 -7
  46. package/dist/shell/output.js +29 -12
  47. package/dist/shell/prompt.js +12 -2
  48. package/dist/shell/runtime.js +1 -1
  49. package/dist/shell/ui.js +19 -1
  50. package/dist/use-cases/auth.js +9 -12
  51. package/dist/use-cases/branch.js +20 -20
  52. package/dist/use-cases/create-cli-gateways.js +3 -13
  53. package/dist/use-cases/project.js +2 -48
  54. package/package.json +3 -3
  55. package/dist/adapters/config.js +0 -74
@@ -1,9 +1,9 @@
1
1
  import { envVarNames } from "./env-vars.js";
2
2
  import { PreviewBuildStrategy } from "./preview-build.js";
3
3
  import path from "node:path";
4
- import { ApiError, ComputeClient } from "@prisma/compute-sdk";
4
+ import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
5
5
  //#region src/lib/app/preview-provider.ts
6
- function createPreviewAppProvider(client) {
6
+ function createPreviewAppProvider(client, options) {
7
7
  const sdk = new ComputeClient(client);
8
8
  return {
9
9
  async createProject(options) {
@@ -14,25 +14,11 @@ function createPreviewAppProvider(client) {
14
14
  name: projectResult.value.name
15
15
  };
16
16
  },
17
- async listApps(projectId) {
18
- const servicesResult = await sdk.listServices({ projectId });
19
- if (servicesResult.isErr()) throw new Error(servicesResult.error.message);
20
- return (await Promise.all(servicesResult.value.map(async (service) => {
21
- const detailResult = await sdk.showService({ serviceId: service.id });
22
- return detailResult.isOk() ? detailResult.value : {
23
- id: service.id,
24
- name: service.name,
25
- region: service.region,
26
- latestVersionId: null,
27
- serviceEndpointDomain: void 0
28
- };
29
- }))).map((service) => ({
30
- id: service.id,
31
- name: service.name,
32
- region: service.region ?? null,
33
- liveDeploymentId: service.latestVersionId ?? null,
34
- liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
35
- }));
17
+ async listApps(projectId, options) {
18
+ return listComputeServices(client, {
19
+ projectId,
20
+ branchGitName: options?.branchName
21
+ });
36
22
  },
37
23
  async removeApp(appId) {
38
24
  const appResult = await sdk.showService({ serviceId: appId });
@@ -60,6 +46,20 @@ function createPreviewAppProvider(client) {
60
46
  if (promoteResult.isErr()) throw new Error(promoteResult.error.message);
61
47
  },
62
48
  async deployApp(options) {
49
+ const resolvedApp = options.appId ? {
50
+ appId: options.appId,
51
+ appName: options.appName,
52
+ region: options.region
53
+ } : options.branchName && options.appName ? await createBranchApp(client, {
54
+ projectId: options.projectId,
55
+ branchName: options.branchName,
56
+ appName: options.appName,
57
+ region: options.region
58
+ }) : {
59
+ appId: void 0,
60
+ appName: options.appName,
61
+ region: options.region
62
+ };
63
63
  const deployResult = await sdk.deploy({
64
64
  strategy: new PreviewBuildStrategy({
65
65
  appPath: path.resolve(options.cwd),
@@ -67,9 +67,9 @@ function createPreviewAppProvider(client) {
67
67
  buildType: options.buildType
68
68
  }),
69
69
  projectId: options.projectId,
70
- serviceId: options.appId,
71
- serviceName: options.appName,
72
- region: options.region,
70
+ serviceId: resolvedApp.appId,
71
+ serviceName: resolvedApp.appName,
72
+ region: resolvedApp.region,
73
73
  portMapping: options.portMapping,
74
74
  envVars: options.envVars,
75
75
  timeoutSeconds: 120,
@@ -197,9 +197,110 @@ function createPreviewAppProvider(client) {
197
197
  live: null
198
198
  }
199
199
  };
200
+ },
201
+ async streamDeploymentLogs(streamOptions) {
202
+ if (!options?.baseUrl || !options.getToken) throw new Error("Log streaming requires an authenticated API base URL and token.");
203
+ const result = await streamLogs({
204
+ baseUrl: options.baseUrl,
205
+ token: await options.getToken(),
206
+ versionId: streamOptions.deploymentId,
207
+ signal: streamOptions.signal
208
+ }, streamOptions.onRecord);
209
+ if (result.isErr()) {
210
+ if (CancelledError.is(result.error)) return;
211
+ throw result.error;
212
+ }
200
213
  }
201
214
  };
202
215
  }
216
+ async function listBranches(client, options) {
217
+ const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
218
+ path: { projectId: options.projectId },
219
+ query: { gitName: options.gitName }
220
+ } });
221
+ if (result.error || !result.data) throw apiCallError("Failed to list branches", result.response, result.error);
222
+ return result.data.data;
223
+ }
224
+ async function resolveOrCreateBranch(client, options) {
225
+ const existing = (await listBranches(client, options))[0];
226
+ if (existing) return existing;
227
+ const result = await client.POST("/v1/projects/{projectId}/branches", {
228
+ params: { path: { projectId: options.projectId } },
229
+ body: {
230
+ gitName: options.gitName,
231
+ isDefault: options.gitName === "main"
232
+ }
233
+ });
234
+ if (result.error || !result.data) {
235
+ if (result.response.status === 409) {
236
+ const raced = (await listBranches(client, options))[0];
237
+ if (raced) return raced;
238
+ }
239
+ throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
240
+ }
241
+ return result.data.data;
242
+ }
243
+ async function listComputeServices(client, options) {
244
+ const services = [];
245
+ let cursor;
246
+ while (true) {
247
+ const result = await client.GET("/v1/compute-services", { params: { query: {
248
+ projectId: options.projectId,
249
+ branchGitName: options.branchGitName,
250
+ cursor
251
+ } } });
252
+ if (result.error || !result.data) throw apiCallError("Failed to list apps", result.response, result.error);
253
+ services.push(...result.data.data);
254
+ if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
255
+ cursor = result.data.pagination.nextCursor;
256
+ }
257
+ return services.map((service) => ({
258
+ id: service.id,
259
+ name: service.name,
260
+ region: service.region.id ?? null,
261
+ branchId: service.branchId,
262
+ liveDeploymentId: service.latestVersionId ?? null,
263
+ liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
264
+ }));
265
+ }
266
+ async function createBranchApp(client, options) {
267
+ const branch = await resolveOrCreateBranch(client, {
268
+ projectId: options.projectId,
269
+ gitName: options.branchName
270
+ });
271
+ const result = await client.POST("/v1/compute-services", { body: {
272
+ projectId: options.projectId,
273
+ branchId: branch.id,
274
+ displayName: options.appName,
275
+ ...options.region ? { regionId: options.region } : {}
276
+ } });
277
+ if (result.error || !result.data) {
278
+ if (result.response.status === 409) {
279
+ const matched = (await listComputeServices(client, {
280
+ projectId: options.projectId,
281
+ branchGitName: options.branchName
282
+ })).find((app) => app.name === options.appName);
283
+ if (matched) return {
284
+ appId: matched.id,
285
+ appName: matched.name,
286
+ region: matched.region ?? options.region
287
+ };
288
+ }
289
+ throw apiCallError(`Failed to create app "${options.appName}"`, result.response, result.error);
290
+ }
291
+ const service = result.data.data;
292
+ return {
293
+ appId: service.id,
294
+ appName: service.name,
295
+ region: service.region.id ?? options.region
296
+ };
297
+ }
298
+ function apiCallError(summary, response, error) {
299
+ if (response.status === 404) return /* @__PURE__ */ new Error("Resource Not Found");
300
+ const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`;
301
+ const hint = error.error?.hint ? ` ${error.error.hint}` : "";
302
+ return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
303
+ }
203
304
  async function findAppForDeployment(sdk, deploymentId) {
204
305
  const projectsResult = await sdk.listProjects();
205
306
  if (projectsResult.isErr()) throw new Error(projectsResult.error.message);
@@ -1,7 +1,9 @@
1
+ import { SERVICE_TOKEN_ENV_VAR } from "./client.js";
1
2
  import { FileTokenStorage } from "../../adapters/token-storage.js";
2
3
  import { requireComputeAuth } from "./guard.js";
3
4
  import { login } from "./login.js";
4
5
  //#region src/lib/auth/auth-ops.ts
6
+ const WORKSPACE_SUB_PREFIX = "workspace:";
5
7
  function decodeJwtPayload(token) {
6
8
  try {
7
9
  const payload = token.split(".")[1];
@@ -11,6 +13,17 @@ function decodeJwtPayload(token) {
11
13
  return {};
12
14
  }
13
15
  }
16
+ function emailFromClaims(claims) {
17
+ const email = claims.email;
18
+ return typeof email === "string" && email.trim().length > 0 ? email.trim() : null;
19
+ }
20
+ function workspaceIdFromClaims(claims) {
21
+ const sub = claims.sub;
22
+ if (typeof sub !== "string") return null;
23
+ if (!sub.startsWith(WORKSPACE_SUB_PREFIX)) return null;
24
+ const id = sub.slice(10).trim();
25
+ return id.length > 0 ? id : null;
26
+ }
14
27
  async function performLogin(env) {
15
28
  await login({
16
29
  tokenStorage: new FileTokenStorage(env),
@@ -18,36 +31,68 @@ async function performLogin(env) {
18
31
  });
19
32
  }
20
33
  async function readAuthState(env) {
34
+ const rawServiceToken = env[SERVICE_TOKEN_ENV_VAR];
35
+ if (rawServiceToken !== void 0) {
36
+ const serviceToken = rawServiceToken.trim();
37
+ if (serviceToken.length === 0) throw new Error(`${SERVICE_TOKEN_ENV_VAR} is set but empty. Provide a valid token or unset the variable.`);
38
+ return readServiceTokenAuthState(serviceToken, env);
39
+ }
21
40
  const tokens = await new FileTokenStorage(env).getTokens();
22
41
  if (!tokens) return {
23
42
  authenticated: false,
24
43
  provider: null,
25
44
  user: null,
26
- workspace: null,
27
- linkedProjectId: null
45
+ workspace: null
28
46
  };
29
47
  const claims = decodeJwtPayload(tokens.accessToken);
48
+ return buildAuthState({
49
+ workspaceIdFromCredential: tokens.workspaceId,
50
+ claims,
51
+ env
52
+ });
53
+ }
54
+ async function readServiceTokenAuthState(token, env) {
55
+ const claims = decodeJwtPayload(token);
56
+ const workspaceId = workspaceIdFromClaims(claims);
57
+ if (!workspaceId) return {
58
+ authenticated: false,
59
+ provider: null,
60
+ user: null,
61
+ workspace: null
62
+ };
63
+ return buildAuthState({
64
+ workspaceIdFromCredential: workspaceId,
65
+ claims,
66
+ env
67
+ });
68
+ }
69
+ async function buildAuthState({ workspaceIdFromCredential, claims, env }) {
70
+ let workspaceId = workspaceIdFromCredential;
71
+ let workspaceName = workspaceIdFromCredential;
30
72
  const client = await requireComputeAuth(env);
31
- let workspaceId = tokens.workspaceId;
32
- let workspaceName = tokens.workspaceId;
33
73
  if (client) try {
34
- const { data } = await client.GET("/v1/workspaces/{id}", { params: { path: { id: tokens.workspaceId } } });
35
- if (data?.data?.id) workspaceId = data.data.id;
74
+ const { data, response } = await client.GET("/v1/workspaces/{id}", { params: { path: { id: workspaceIdFromCredential } } });
75
+ if (response?.status === 401) return {
76
+ authenticated: false,
77
+ provider: null,
78
+ user: null,
79
+ workspace: null
80
+ };
81
+ if (data?.data?.id) {
82
+ workspaceId = data.data.id;
83
+ workspaceName = data.data.id;
84
+ }
36
85
  if (data?.data?.name) workspaceName = data.data.name;
37
86
  } catch {}
87
+ const email = emailFromClaims(claims);
38
88
  return {
39
89
  authenticated: true,
40
90
  provider: null,
41
- user: {
42
- id: claims.sub ?? "",
43
- name: claims.name ?? "",
44
- email: claims.email ?? ""
45
- },
91
+ user: email ? { email } : null,
46
92
  workspace: {
47
93
  id: workspaceId,
48
94
  name: workspaceName
49
- },
50
- linkedProjectId: null
95
+ }
51
96
  };
52
97
  }
53
98
  async function performLogout(env) {
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import os from "node:os";
3
3
  //#region src/lib/auth/client.ts
4
4
  const CLIENT_ID = "cmm3lndn701oo0uefvxzo0ivw";
5
- const SERVICE_TOKEN_ENV_VAR = "PRISMA_API_TOKEN";
5
+ const SERVICE_TOKEN_ENV_VAR = "PRISMA_SERVICE_TOKEN";
6
6
  const AUTH_FILE_ENV_VAR = "PRISMA_COMPUTE_AUTH_FILE";
7
7
  function getApiBaseUrl(env = process.env) {
8
8
  return env.PRISMA_MANAGEMENT_API_URL?.trim() || "https://api.prisma.io";
@@ -6,7 +6,7 @@ import { createManagementApiClient, createManagementApiSdk } from "@prisma/manag
6
6
  * Resolve authentication and return a ManagementApiClient.
7
7
  *
8
8
  * Priority:
9
- * 1. PRISMA_API_TOKEN env var → service token (CI / headless)
9
+ * 1. PRISMA_SERVICE_TOKEN env var → service token (CI / headless)
10
10
  * 2. Stored OAuth tokens → SDK with auto-refresh
11
11
  *
12
12
  * Returns null if not authenticated.
@@ -47,8 +47,9 @@ async function login(options = {}) {
47
47
  reject(error);
48
48
  return;
49
49
  }
50
- res.setHeader("Content-Type", "text/html");
51
- res.end(`<html><body style="font-family:system-ui;max-width:400px;margin:80px auto;text-align:center"><h2>✓ Signed in</h2><p>You may now close this tab and return to the terminal.</p></body></html>`);
50
+ const workspaceName = await state.resolveWorkspaceName();
51
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
52
+ res.end(renderSuccessPage(workspaceName));
52
53
  resolve();
53
54
  });
54
55
  });
@@ -63,13 +64,14 @@ var LoginState = class {
63
64
  latestState;
64
65
  sdk;
65
66
  openUrl;
67
+ tokenStorage;
66
68
  constructor(options) {
67
69
  this.options = options;
68
- const tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env);
70
+ this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env);
69
71
  this.sdk = createManagementApiSdk({
70
72
  clientId: options.clientId ?? "cmm3lndn701oo0uefvxzo0ivw",
71
73
  redirectUri: `http://${options.hostname}:${options.port}/auth/callback`,
72
- tokenStorage,
74
+ tokenStorage: this.tokenStorage,
73
75
  apiBaseUrl: options.apiBaseUrl ?? getApiBaseUrl(options.env),
74
76
  authBaseUrl: options.authBaseUrl
75
77
  });
@@ -109,9 +111,118 @@ var LoginState = class {
109
111
  throw new AuthError$1(error instanceof Error ? error.message : "Unknown error during login");
110
112
  }
111
113
  }
114
+ async resolveWorkspaceName() {
115
+ try {
116
+ const tokens = await this.tokenStorage.getTokens();
117
+ if (!tokens?.workspaceId) return null;
118
+ const { data } = await this.sdk.client.GET("/v1/workspaces/{id}", { params: { path: { id: tokens.workspaceId } } });
119
+ const name = data?.data?.name;
120
+ return typeof name === "string" && name.trim().length > 0 ? name.trim() : null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
112
125
  get host() {
113
126
  return `${this.options.hostname}:${this.options.port}`;
114
127
  }
115
128
  };
129
+ function renderSuccessPage(workspaceName) {
130
+ return `<!doctype html>
131
+ <html lang="en">
132
+ <head>
133
+ <meta charset="utf-8">
134
+ <meta name="viewport" content="width=device-width, initial-scale=1">
135
+ <title>Prisma Developer Platform</title>
136
+ <style>
137
+ :root {
138
+ color-scheme: light dark;
139
+ --background: #ffffff;
140
+ --foreground: #1f2430;
141
+ --muted: #4f5665;
142
+ --mark-color: #050812;
143
+ }
144
+
145
+ @media (prefers-color-scheme: dark) {
146
+ :root {
147
+ --background: #050812;
148
+ --foreground: #f6f7fb;
149
+ --muted: #c5cad6;
150
+ --mark-color: #ffffff;
151
+ }
152
+ }
153
+
154
+ * {
155
+ box-sizing: border-box;
156
+ }
157
+
158
+ body {
159
+ min-height: 100vh;
160
+ margin: 0;
161
+ background: var(--background);
162
+ color: var(--foreground);
163
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
164
+ display: grid;
165
+ grid-template-rows: 128px 1fr;
166
+ }
167
+
168
+ .mark {
169
+ align-self: end;
170
+ justify-self: center;
171
+ width: 36px;
172
+ height: 36px;
173
+ color: var(--mark-color);
174
+ }
175
+
176
+ .mark path {
177
+ fill: currentColor !important;
178
+ }
179
+
180
+ main {
181
+ align-self: center;
182
+ justify-self: center;
183
+ width: min(520px, calc(100vw - 48px));
184
+ margin-top: -128px;
185
+ text-align: center;
186
+ }
187
+
188
+ h1 {
189
+ margin: 0 0 12px;
190
+ font-size: 26px;
191
+ line-height: 1.2;
192
+ font-weight: 700;
193
+ letter-spacing: 0;
194
+ }
195
+
196
+ p {
197
+ margin: 0 auto;
198
+ max-width: 480px;
199
+ color: var(--muted);
200
+ font-size: 15px;
201
+ line-height: 1.55;
202
+ letter-spacing: 0;
203
+ }
204
+ </style>
205
+ </head>
206
+ <body>
207
+ <svg class="mark" width="36" height="36" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M25.21,24.21,12.739,27.928a.525.525,0,0,1-.667-.606L16.528,5.811a.43.43,0,0,1,.809-.094l8.249,17.661A.6.6,0,0,1,25.21,24.21Zm2.139-.878L17.8,2.883h0A1.531,1.531,0,0,0,16.491,2a1.513,1.513,0,0,0-1.4.729L4.736,19.648a1.592,1.592,0,0,0,.018,1.7l5.064,7.909a1.628,1.628,0,0,0,1.83.678l14.7-4.383a1.6,1.6,0,0,0,1-2.218Z" style="fill:#0c344b;fill-rule:evenodd"/></svg>
208
+ <main>
209
+ <h1>You're all set.</h1>
210
+ <p>${workspaceName ? `Your terminal is now connected to your ${escapeHtml(workspaceName)} workspace. Head back to your terminal to continue.` : "Your terminal is now connected to your Prisma workspace. Head back to your terminal to continue."}</p>
211
+ </main>
212
+ </body>
213
+ </html>`;
214
+ }
215
+ function escapeHtml(value) {
216
+ return value.replace(/[&<>"']/g, (char) => {
217
+ switch (char) {
218
+ case "&": return "&amp;";
219
+ case "<": return "&lt;";
220
+ case ">": return "&gt;";
221
+ case "\"": return "&quot;";
222
+ case "'": return "&#39;";
223
+ default: return char;
224
+ }
225
+ });
226
+ }
116
227
  //#endregion
117
228
  export { login };
@@ -0,0 +1,51 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ //#region src/lib/project/local-pin.ts
4
+ const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
5
+ async function readLocalResolutionPin(cwd) {
6
+ try {
7
+ const raw = await readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), "utf8");
8
+ const parsed = JSON.parse(raw);
9
+ if (!isLocalResolutionPin(parsed)) return { kind: "invalid" };
10
+ return {
11
+ kind: "present",
12
+ pin: parsed
13
+ };
14
+ } catch (error) {
15
+ if (error.code === "ENOENT") return { kind: "missing" };
16
+ if (error instanceof SyntaxError) return { kind: "invalid" };
17
+ throw error;
18
+ }
19
+ }
20
+ async function writeLocalResolutionPin(cwd, pin) {
21
+ const prismaDir = path.join(cwd, ".prisma");
22
+ await mkdir(prismaDir, { recursive: true });
23
+ const pinPath = path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH);
24
+ const tmpPath = path.join(prismaDir, `local.${process.pid}.${Date.now()}.tmp`);
25
+ await writeFile(tmpPath, `${JSON.stringify(pin, null, 2)}\n`, "utf8");
26
+ await rename(tmpPath, pinPath);
27
+ }
28
+ async function ensureLocalResolutionPinGitignore(cwd) {
29
+ const gitignorePath = path.join(cwd, ".gitignore");
30
+ let existing = null;
31
+ try {
32
+ existing = await readFile(gitignorePath, "utf8");
33
+ } catch (error) {
34
+ if (error.code !== "ENOENT") throw error;
35
+ }
36
+ if (existing === null) {
37
+ await writeFile(gitignorePath, ".prisma/\n", "utf8");
38
+ return;
39
+ }
40
+ if (existing.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".prisma/" || line === ".prisma/local.json")) return;
41
+ await writeFile(gitignorePath, existing.endsWith("\n") ? `${existing}.prisma/\n` : `${existing}\n.prisma/\n`, "utf8");
42
+ }
43
+ function isLocalResolutionPin(value) {
44
+ if (!value || typeof value !== "object") return false;
45
+ const keys = Object.keys(value);
46
+ if (keys.length !== 2 || !keys.includes("workspaceId") || !keys.includes("projectId")) return false;
47
+ const candidate = value;
48
+ return typeof candidate.workspaceId === "string" && candidate.workspaceId.trim().length > 0 && typeof candidate.projectId === "string" && candidate.projectId.trim().length > 0;
49
+ }
50
+ //#endregion
51
+ export { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, readLocalResolutionPin, writeLocalResolutionPin };