@prisma/cli 3.0.0-dev.97.1 → 8.0.0-rc.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/README.md +3 -14
- package/dist/adapters/local-state.js +14 -3
- package/dist/{lib/auth → auth}/client.js +11 -3
- package/dist/auth/errors.js +50 -0
- package/dist/{lib/auth → auth}/guard.js +7 -6
- package/dist/{lib/auth → auth}/login.js +147 -6
- package/dist/{lib/auth/auth-ops.js → auth/operations.js} +63 -13
- package/dist/auth/recipient.js +42 -0
- package/dist/{adapters → auth}/token-storage.js +28 -7
- package/dist/auth/workspaces.js +171 -0
- package/dist/cli-command.js +14 -0
- package/dist/cli-name.js +12 -0
- package/dist/cli.js +1 -1
- package/dist/cli2.js +25 -8
- package/dist/command-arguments.js +12 -0
- package/dist/commands/agent/index.js +60 -0
- package/dist/commands/app/index.js +11 -6
- package/dist/commands/auth/index.js +2 -3
- package/dist/commands/bucket/index.js +123 -0
- package/dist/commands/build/index.js +29 -0
- package/dist/commands/database/index.js +92 -2
- package/dist/commands/feedback/index.js +20 -0
- package/dist/commands/git/index.js +1 -1
- package/dist/commands/init/index.js +33 -0
- package/dist/commands/project/index.js +54 -5
- package/dist/controllers/agent-setup.js +52 -0
- package/dist/controllers/agent.js +228 -0
- package/dist/controllers/app-env-api.js +3 -2
- package/dist/controllers/app-env-file.js +1 -1
- package/dist/controllers/app-env.js +5 -5
- package/dist/controllers/app.js +423 -358
- package/dist/controllers/auth.js +62 -254
- package/dist/controllers/branch.js +5 -16
- package/dist/controllers/bucket.js +184 -0
- package/dist/controllers/build.js +88 -0
- package/dist/controllers/database.js +230 -85
- package/dist/controllers/feedback.js +90 -0
- package/dist/controllers/init.js +814 -0
- package/dist/controllers/project.js +345 -184
- package/dist/controllers/select-prompt-port.js +1 -0
- package/dist/{shell/errors.js → errors.js} +8 -40
- package/dist/lib/agent/cli-command.js +20 -0
- package/dist/lib/agent/constants.js +12 -0
- package/dist/lib/agent/package-manager.js +99 -0
- package/dist/lib/agent/setup-status.js +83 -0
- package/dist/lib/app/app-provider.js +168 -95
- package/dist/lib/app/branch-database-deploy.js +70 -42
- package/dist/lib/app/branch-database.js +39 -20
- package/dist/lib/app/build-settings.js +8 -3
- package/dist/lib/app/build.js +16 -14
- package/dist/lib/app/bun-project.js +1 -1
- package/dist/lib/app/compute-config.js +29 -31
- package/dist/lib/app/deploy-plan.js +1 -0
- package/dist/lib/app/deploy-progress.js +7 -7
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +2 -2
- package/dist/lib/app/env-vars.js +1 -1
- package/dist/lib/app/local-dev.js +1 -1
- package/dist/lib/app/production-deploy-gate.js +3 -2
- package/dist/lib/bucket/provider.js +139 -0
- package/dist/lib/database/provider.js +235 -17
- package/dist/lib/diagnostics.js +2 -1
- package/dist/lib/feedback.js +15 -0
- package/dist/lib/git/local-branch.js +1 -1
- package/dist/lib/project/interactive-setup.js +6 -5
- package/dist/lib/project/local-pin.js +1 -1
- package/dist/lib/project/provider.js +93 -0
- package/dist/lib/project/resolution.js +11 -8
- package/dist/lib/project/setup.js +9 -6
- package/dist/lib/version.js +3 -2
- package/dist/lib/workspace-id.js +18 -0
- package/dist/payload-B88Qvzxk-CrtTXSOe.js +34 -0
- package/dist/presenters/agent.js +74 -0
- package/dist/presenters/app.js +33 -7
- package/dist/presenters/auth.js +3 -2
- package/dist/presenters/bucket.js +174 -0
- package/dist/presenters/database.js +193 -5
- package/dist/presenters/feedback.js +26 -0
- package/dist/presenters/init.js +30 -0
- package/dist/presenters/project.js +98 -4
- package/dist/shell/cli-command.js +2 -0
- package/dist/shell/command-meta.js +303 -4
- package/dist/shell/command-runner.js +13 -6
- package/dist/shell/help.js +6 -5
- package/dist/shell/output.js +65 -3
- package/dist/shell/prompt.js +12 -5
- package/dist/shell/runtime.js +4 -21
- package/dist/state-dir.js +12 -0
- package/dist/{shell/update-check.js → update-check.js} +6 -5
- package/dist/v8/cli.js +14699 -0
- package/dist/v8/sender.js +192 -0
- package/package.json +21 -13
- package/dist/adapters/mock-api.js +0 -136
- package/dist/lib/app/app-interaction.js +0 -5
- package/dist/shell/command-arguments.js +0 -6
- package/dist/use-cases/auth.js +0 -145
- package/dist/use-cases/branch.js +0 -47
- package/dist/use-cases/create-cli-gateways.js +0 -68
- package/dist/use-cases/project.js +0 -30
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatPrismaCliCommand } from "../../cli-command.js";
|
|
2
|
+
import { CliError } from "../../errors.js";
|
|
3
|
+
import "../../shell/cli-command.js";
|
|
2
4
|
//#region src/lib/database/provider.ts
|
|
3
|
-
|
|
5
|
+
const SUBSCRIPTION_LOOKUP_TIMEOUT_MS = 3e3;
|
|
6
|
+
function createManagementDatabaseProvider(client, options) {
|
|
7
|
+
const formatCommand = options?.formatCommand ?? ((args) => formatPrismaCliCommand(args));
|
|
8
|
+
const toDatabaseApiError = (summary, response, error, signal) => databaseApiError({
|
|
9
|
+
client,
|
|
10
|
+
workspaceId: options?.workspaceId,
|
|
11
|
+
summary,
|
|
12
|
+
response,
|
|
13
|
+
error,
|
|
14
|
+
signal
|
|
15
|
+
});
|
|
4
16
|
return {
|
|
5
17
|
async listDatabases(options) {
|
|
6
18
|
const databases = [];
|
|
@@ -14,7 +26,7 @@ function createManagementDatabaseProvider(client) {
|
|
|
14
26
|
} },
|
|
15
27
|
signal: options.signal
|
|
16
28
|
});
|
|
17
|
-
if (result.error || !result.data) throw
|
|
29
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to list databases", result.response, result.error, options.signal);
|
|
18
30
|
databases.push(...result.data.data);
|
|
19
31
|
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
20
32
|
cursor = result.data.pagination.nextCursor;
|
|
@@ -26,8 +38,8 @@ function createManagementDatabaseProvider(client) {
|
|
|
26
38
|
params: { path: { databaseId } },
|
|
27
39
|
signal: options?.signal
|
|
28
40
|
});
|
|
29
|
-
if (result.response?.status === 404) return null;
|
|
30
|
-
if (result.error || !result.data) throw
|
|
41
|
+
if (result.response?.status === 404 && !isPlanLimitApiError(result.error)) return null;
|
|
42
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to show database", result.response, result.error, options?.signal);
|
|
31
43
|
const database = result.data.data;
|
|
32
44
|
return normalizeDatabase(database, requireDatabaseProjectId(database, options?.projectId));
|
|
33
45
|
},
|
|
@@ -42,7 +54,7 @@ function createManagementDatabaseProvider(client) {
|
|
|
42
54
|
},
|
|
43
55
|
signal: options.signal
|
|
44
56
|
});
|
|
45
|
-
if (result.error || !result.data) throw
|
|
57
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to create database", result.response, result.error, options.signal);
|
|
46
58
|
return normalizeCreatedDatabase(result.data.data, options.projectId);
|
|
47
59
|
},
|
|
48
60
|
async removeDatabase(databaseId, options) {
|
|
@@ -50,14 +62,14 @@ function createManagementDatabaseProvider(client) {
|
|
|
50
62
|
params: { path: { databaseId } },
|
|
51
63
|
signal: options?.signal
|
|
52
64
|
});
|
|
53
|
-
if (result.error) throw
|
|
65
|
+
if (result.error) throw await toDatabaseApiError("Failed to remove database", result.response, result.error, options?.signal);
|
|
54
66
|
},
|
|
55
67
|
async listConnections(databaseId, options) {
|
|
56
68
|
const result = await client.GET("/v1/databases/{databaseId}/connections", {
|
|
57
69
|
params: { path: { databaseId } },
|
|
58
70
|
signal: options?.signal
|
|
59
71
|
});
|
|
60
|
-
if (result.error || !result.data) throw
|
|
72
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to list database connections", result.response, result.error, options?.signal);
|
|
61
73
|
return result.data.data.map((connection) => normalizeConnection(connection, databaseId));
|
|
62
74
|
},
|
|
63
75
|
async createConnection(options) {
|
|
@@ -66,7 +78,7 @@ function createManagementDatabaseProvider(client) {
|
|
|
66
78
|
body: { name: options.name },
|
|
67
79
|
signal: options.signal
|
|
68
80
|
});
|
|
69
|
-
if (result.error || !result.data) throw
|
|
81
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to create database connection", result.response, result.error, options.signal);
|
|
70
82
|
return normalizeCreatedConnection(result.data.data, options.databaseId);
|
|
71
83
|
},
|
|
72
84
|
async removeConnection(connectionId, options) {
|
|
@@ -74,7 +86,56 @@ function createManagementDatabaseProvider(client) {
|
|
|
74
86
|
params: { path: { id: connectionId } },
|
|
75
87
|
signal: options?.signal
|
|
76
88
|
});
|
|
77
|
-
if (result.error) throw
|
|
89
|
+
if (result.error) throw await toDatabaseApiError("Failed to remove database connection", result.response, result.error, options?.signal);
|
|
90
|
+
},
|
|
91
|
+
async getUsage(databaseId, options) {
|
|
92
|
+
const result = await client.GET("/v1/databases/{databaseId}/usage", {
|
|
93
|
+
params: {
|
|
94
|
+
path: { databaseId },
|
|
95
|
+
query: {
|
|
96
|
+
...options?.from ? { startDate: options.from } : {},
|
|
97
|
+
...options?.to ? { endDate: options.to } : {}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
signal: options?.signal
|
|
101
|
+
});
|
|
102
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to fetch database usage", result.response, result.error, options?.signal);
|
|
103
|
+
return normalizeUsage(result.data);
|
|
104
|
+
},
|
|
105
|
+
async listBackups(databaseId, options) {
|
|
106
|
+
const result = await client.GET("/v1/databases/{databaseId}/backups", {
|
|
107
|
+
params: {
|
|
108
|
+
path: { databaseId },
|
|
109
|
+
query: { ...options?.limit !== void 0 ? { limit: options.limit } : {} }
|
|
110
|
+
},
|
|
111
|
+
signal: options?.signal
|
|
112
|
+
});
|
|
113
|
+
if (result.response?.status === 422 && !isPlanLimitApiError(result.error)) throw backupsUnsupportedError(databaseId, result.error);
|
|
114
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to list database backups", result.response, result.error, options?.signal);
|
|
115
|
+
return normalizeBackupList(result.data);
|
|
116
|
+
},
|
|
117
|
+
async restoreDatabase(options) {
|
|
118
|
+
const result = await client.POST("/v1/databases/{targetDatabaseId}/restore", {
|
|
119
|
+
params: { path: { targetDatabaseId: options.targetDatabaseId } },
|
|
120
|
+
body: { source: {
|
|
121
|
+
type: "backup",
|
|
122
|
+
databaseId: options.sourceDatabaseId,
|
|
123
|
+
backupId: options.backupId
|
|
124
|
+
} },
|
|
125
|
+
signal: options.signal
|
|
126
|
+
});
|
|
127
|
+
if (result.response?.status === 409 && !isPlanLimitApiError(result.error)) throw restoreConflictError(options.targetDatabaseId, result.error, formatCommand);
|
|
128
|
+
if (result.response?.status === 404 && !isPlanLimitApiError(result.error)) throw restoreBackupNotFoundError(options, result.error, formatCommand);
|
|
129
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to restore database", result.response, result.error, options.signal);
|
|
130
|
+
return normalizeDatabase(result.data.data, options.projectId);
|
|
131
|
+
},
|
|
132
|
+
async rotateConnection(connectionId, options) {
|
|
133
|
+
const result = await client.POST("/v1/connections/{id}/rotate", {
|
|
134
|
+
params: { path: { id: connectionId } },
|
|
135
|
+
signal: options?.signal
|
|
136
|
+
});
|
|
137
|
+
if (result.error || !result.data) throw await toDatabaseApiError("Failed to rotate database connection", result.response, result.error, options?.signal);
|
|
138
|
+
return normalizeRotatedConnection(result.data.data);
|
|
78
139
|
}
|
|
79
140
|
};
|
|
80
141
|
}
|
|
@@ -151,17 +212,174 @@ function requireDatabaseProjectId(database, fallbackProjectId) {
|
|
|
151
212
|
function extractConnectionString(connection) {
|
|
152
213
|
return connection.endpoints?.pooled?.connectionString ?? connection.connectionString ?? connection.endpoints?.direct?.connectionString ?? connection.endpoints?.accelerate?.connectionString ?? null;
|
|
153
214
|
}
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
/** Absence is carried, not filled in. A `0` here reached the card, the
|
|
216
|
+
* stdout lane and the json record as though the API had measured it, and
|
|
217
|
+
* "you used nothing" is the answer a user acts on. The unit is the same
|
|
218
|
+
* problem an order of magnitude worse: `GiB` was a guess, and a value the
|
|
219
|
+
* API denominates in bytes would print against it unchanged. */
|
|
220
|
+
function normalizeUsageMetric(metric) {
|
|
221
|
+
return {
|
|
222
|
+
used: metric?.used ?? null,
|
|
223
|
+
unit: metric?.unit ?? null
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function normalizeUsage(usage) {
|
|
227
|
+
return {
|
|
228
|
+
period: {
|
|
229
|
+
start: usage.period?.start ?? null,
|
|
230
|
+
end: usage.period?.end ?? null
|
|
231
|
+
},
|
|
232
|
+
metrics: {
|
|
233
|
+
operations: normalizeUsageMetric(usage.metrics?.operations),
|
|
234
|
+
storage: normalizeUsageMetric(usage.metrics?.storage)
|
|
235
|
+
},
|
|
236
|
+
generatedAt: usage.generatedAt ?? null
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function normalizeBackupList(body) {
|
|
240
|
+
return {
|
|
241
|
+
backups: (body.data ?? []).map((backup) => ({
|
|
242
|
+
id: backup.id,
|
|
243
|
+
backupType: backup.backupType ?? "",
|
|
244
|
+
status: backup.status ?? "",
|
|
245
|
+
size: backup.size ?? null,
|
|
246
|
+
createdAt: backup.createdAt ?? ""
|
|
247
|
+
})),
|
|
248
|
+
retentionDays: body.meta?.backupRetentionDays ?? null,
|
|
249
|
+
hasMore: body.pagination?.hasMore ?? false
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function normalizeRotatedConnection(connection) {
|
|
253
|
+
const connectionString = extractConnectionString(connection);
|
|
254
|
+
if (!connectionString) throw new CliError({
|
|
255
|
+
code: "DATABASE_CONNECTION_STRING_MISSING",
|
|
256
|
+
domain: "database",
|
|
257
|
+
summary: "Rotated connection did not return a connection string",
|
|
258
|
+
why: "Rotated connection strings are one-time-view secrets, but the Management API did not include one in this rotate response.",
|
|
259
|
+
fix: "Re-run the rotation, or create a replacement connection and store the returned URL immediately.",
|
|
260
|
+
exitCode: 1,
|
|
261
|
+
nextSteps: []
|
|
262
|
+
});
|
|
263
|
+
const database = connection.database?.id && connection.database?.name ? {
|
|
264
|
+
id: connection.database.id,
|
|
265
|
+
name: connection.database.name
|
|
266
|
+
} : null;
|
|
267
|
+
return {
|
|
268
|
+
connection: normalizeConnection(connection, connection.database?.id ?? connection.databaseId ?? ""),
|
|
269
|
+
database,
|
|
270
|
+
connectionString
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function backupsUnsupportedError(databaseId, error) {
|
|
156
274
|
return new CliError({
|
|
157
|
-
code:
|
|
275
|
+
code: "DATABASE_BACKUPS_UNSUPPORTED",
|
|
158
276
|
domain: "database",
|
|
159
|
-
summary,
|
|
160
|
-
why: error?.error?.message ?? `The
|
|
161
|
-
fix:
|
|
277
|
+
summary: "Backups are not available for this database",
|
|
278
|
+
why: error?.error?.message ?? `The platform does not manage backups for database "${databaseId}", for example because it is a remote/BYO database.`,
|
|
279
|
+
fix: "Use your own backup tooling for externally managed databases.",
|
|
280
|
+
exitCode: 1,
|
|
281
|
+
nextSteps: []
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function restoreBackupNotFoundError(options, error, formatCommand) {
|
|
285
|
+
const listCommand = formatCommand([
|
|
286
|
+
"database",
|
|
287
|
+
"backup",
|
|
288
|
+
"list",
|
|
289
|
+
options.sourceDatabaseId
|
|
290
|
+
]);
|
|
291
|
+
return new CliError({
|
|
292
|
+
code: "DATABASE_BACKUP_NOT_FOUND",
|
|
293
|
+
domain: "database",
|
|
294
|
+
summary: "Database backup not found",
|
|
295
|
+
why: error?.error?.message ?? `No backup matched "${options.backupId}" for database "${options.sourceDatabaseId}".`,
|
|
296
|
+
fix: `Pass a backup id from ${listCommand}.`,
|
|
297
|
+
exitCode: 1,
|
|
298
|
+
nextSteps: [listCommand]
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function restoreConflictError(targetDatabaseId, error, formatCommand) {
|
|
302
|
+
return new CliError({
|
|
303
|
+
code: "DATABASE_RESTORE_CONFLICT",
|
|
304
|
+
domain: "database",
|
|
305
|
+
summary: "Database cannot be restored right now",
|
|
306
|
+
why: error?.error?.message ?? `Database "${targetDatabaseId}" is provisioning or already recovering.`,
|
|
307
|
+
fix: "Wait for the database to become ready, then retry the restore.",
|
|
308
|
+
exitCode: 1,
|
|
309
|
+
nextSteps: [formatCommand([
|
|
310
|
+
"database",
|
|
311
|
+
"show",
|
|
312
|
+
targetDatabaseId
|
|
313
|
+
])]
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async function databaseApiError(options) {
|
|
317
|
+
if (isPlanLimitApiError(options.error)) return planLimitReachedError(options);
|
|
318
|
+
const status = options.response?.status ?? 0;
|
|
319
|
+
return new CliError({
|
|
320
|
+
code: options.error?.error?.code ?? "DATABASE_API_ERROR",
|
|
321
|
+
domain: "database",
|
|
322
|
+
summary: options.summary,
|
|
323
|
+
why: options.error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
|
|
324
|
+
fix: options.error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
|
|
162
325
|
exitCode: 1,
|
|
163
326
|
nextSteps: []
|
|
164
327
|
});
|
|
165
328
|
}
|
|
329
|
+
async function planLimitReachedError(options) {
|
|
330
|
+
const subscription = options.workspaceId ? await readWorkspaceSubscription(options.client, options.workspaceId, options.signal) : null;
|
|
331
|
+
const workspaceLine = options.workspaceId ? `Workspace: ${options.workspaceId}` : "Workspace: unavailable";
|
|
332
|
+
const planName = subscription?.planName || null;
|
|
333
|
+
const usageBlocked = subscription?.usageBlocked ?? null;
|
|
334
|
+
const upgradeUrl = subscription?.upgradeUrl || null;
|
|
335
|
+
const recoveryLines = [...planName ? [`Current plan: ${planName}`] : [], upgradeUrl ? `Upgrade: ${upgradeUrl}` : "Upgrade: Open Prisma Console and upgrade the affected workspace plan."];
|
|
336
|
+
return new CliError({
|
|
337
|
+
code: "PLAN_LIMIT_REACHED",
|
|
338
|
+
domain: "database",
|
|
339
|
+
summary: "Workspace plan limit reached",
|
|
340
|
+
why: "Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.",
|
|
341
|
+
fix: upgradeUrl ? `Upgrade the workspace plan at ${upgradeUrl}.` : "Open Prisma Console and upgrade the affected workspace plan.",
|
|
342
|
+
meta: {
|
|
343
|
+
workspaceId: options.workspaceId ?? null,
|
|
344
|
+
blockedFeature: null,
|
|
345
|
+
planName,
|
|
346
|
+
usageBlocked,
|
|
347
|
+
upgradeUrl
|
|
348
|
+
},
|
|
349
|
+
exitCode: 1,
|
|
350
|
+
nextSteps: [],
|
|
351
|
+
humanLines: [
|
|
352
|
+
"Workspace plan limit reached [PLAN_LIMIT_REACHED]",
|
|
353
|
+
"",
|
|
354
|
+
"Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.",
|
|
355
|
+
"",
|
|
356
|
+
workspaceLine,
|
|
357
|
+
...recoveryLines
|
|
358
|
+
]
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function isPlanLimitApiError(error) {
|
|
362
|
+
return error?.error?.code === "planLimitReached";
|
|
363
|
+
}
|
|
364
|
+
async function readWorkspaceSubscription(client, workspaceId, signal) {
|
|
365
|
+
signal?.throwIfAborted();
|
|
366
|
+
const timeoutController = new AbortController();
|
|
367
|
+
const timeout = setTimeout(() => timeoutController.abort(), SUBSCRIPTION_LOOKUP_TIMEOUT_MS);
|
|
368
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutController.signal]) : timeoutController.signal;
|
|
369
|
+
try {
|
|
370
|
+
const result = await client.GET("/v1/workspaces/{id}/subscription", {
|
|
371
|
+
params: { path: { id: workspaceId } },
|
|
372
|
+
signal: requestSignal
|
|
373
|
+
});
|
|
374
|
+
signal?.throwIfAborted();
|
|
375
|
+
if (result.error) return null;
|
|
376
|
+
return result.data?.data ?? null;
|
|
377
|
+
} catch {
|
|
378
|
+
signal?.throwIfAborted();
|
|
379
|
+
return null;
|
|
380
|
+
} finally {
|
|
381
|
+
clearTimeout(timeout);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
166
384
|
//#endregion
|
|
167
|
-
export { createManagementDatabaseProvider
|
|
385
|
+
export { createManagementDatabaseProvider };
|
package/dist/lib/diagnostics.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveLocalStateFilePath } from "../adapters/local-state.js";
|
|
2
|
-
import { resolveStateDir } from "../
|
|
2
|
+
import { resolveStateDir } from "../state-dir.js";
|
|
3
|
+
import "../shell/runtime.js";
|
|
3
4
|
import { readLocalGitState } from "./git/local-status.js";
|
|
4
5
|
//#region src/lib/diagnostics.ts
|
|
5
6
|
async function collectCommandDiagnostics(context, options = {}) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/lib/feedback.ts
|
|
2
|
+
const FEEDBACK_TIMEOUT_MS = 3e3;
|
|
3
|
+
const TIMEOUT_DETAIL = `The feedback service did not answer within ${FEEDBACK_TIMEOUT_MS / 1e3} seconds.`;
|
|
4
|
+
function isTimeout(error) {
|
|
5
|
+
return error instanceof Error && error.name === "TimeoutError";
|
|
6
|
+
}
|
|
7
|
+
function unreachableDetail(error) {
|
|
8
|
+
if (isTimeout(error)) return TIMEOUT_DETAIL;
|
|
9
|
+
return `The feedback service could not be reached${error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : ""}.`;
|
|
10
|
+
}
|
|
11
|
+
function unreadableBodyDetail(error) {
|
|
12
|
+
return isTimeout(error) ? TIMEOUT_DETAIL : "The feedback service response could not be read.";
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { FEEDBACK_TIMEOUT_MS, unreachableDetail, unreadableBodyDetail };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { usageError } from "../../
|
|
1
|
+
import { usageError } from "../../errors.js";
|
|
2
2
|
import { selectPrompt, textPrompt } from "../../shell/prompt.js";
|
|
3
3
|
import { inferTargetName, sortProjects } from "./resolution.js";
|
|
4
4
|
import { toProjectSummary, validateProjectSetupNameText } from "./setup.js";
|
|
@@ -12,6 +12,10 @@ async function promptForProjectSetupChoice(options) {
|
|
|
12
12
|
output: options.context.runtime.stderr,
|
|
13
13
|
message: "Which Project should this directory use?",
|
|
14
14
|
choices: [
|
|
15
|
+
{
|
|
16
|
+
label: "+ Create a new Project",
|
|
17
|
+
value: { kind: "create" }
|
|
18
|
+
},
|
|
15
19
|
...sortedProjects.map((project) => ({
|
|
16
20
|
label: duplicateNames.has(project.name) ? `${project.name} (${project.id})` : project.name,
|
|
17
21
|
value: {
|
|
@@ -19,10 +23,6 @@ async function promptForProjectSetupChoice(options) {
|
|
|
19
23
|
project
|
|
20
24
|
}
|
|
21
25
|
})),
|
|
22
|
-
{
|
|
23
|
-
label: "Create a new Project",
|
|
24
|
-
value: { kind: "create" }
|
|
25
|
-
},
|
|
26
26
|
{
|
|
27
27
|
label: "Cancel",
|
|
28
28
|
value: { kind: "cancel" }
|
|
@@ -40,6 +40,7 @@ async function promptForProjectSetupChoice(options) {
|
|
|
40
40
|
const rawName = await textPrompt({
|
|
41
41
|
input: options.context.runtime.stdin,
|
|
42
42
|
output: options.context.runtime.stderr,
|
|
43
|
+
signal: options.context.runtime.signal,
|
|
43
44
|
message: "Project name",
|
|
44
45
|
placeholder: suggestedName.name,
|
|
45
46
|
validate: (value) => validateProjectSetupNameText(value, suggestedName.name)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { Result, TaggedError, UnhandledException } from "better-result";
|
|
4
4
|
//#region src/lib/project/local-pin.ts
|
|
5
5
|
const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { formatPrismaCliCommand } from "../../cli-command.js";
|
|
2
|
+
import { CliError } from "../../errors.js";
|
|
3
|
+
import "../../shell/cli-command.js";
|
|
4
|
+
//#region src/lib/project/provider.ts
|
|
5
|
+
function createManagementProjectProvider(client) {
|
|
6
|
+
return {
|
|
7
|
+
async renameProject(options) {
|
|
8
|
+
const result = await client.PATCH("/v1/projects/{id}", {
|
|
9
|
+
params: { path: { id: options.projectId } },
|
|
10
|
+
body: { name: options.name },
|
|
11
|
+
signal: options.signal
|
|
12
|
+
});
|
|
13
|
+
const status = result.response?.status ?? 0;
|
|
14
|
+
if (status === 400 || status === 422) throw projectRenameFailedError(options.name, result.error);
|
|
15
|
+
if (result.error || !result.data) throw projectApiError("Failed to rename project", result.response, result.error);
|
|
16
|
+
const project = result.data.data;
|
|
17
|
+
return {
|
|
18
|
+
id: project.id,
|
|
19
|
+
name: project.name,
|
|
20
|
+
...project.url ? { url: project.url } : {}
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
async removeProject(options) {
|
|
24
|
+
const result = await client.DELETE("/v1/projects/{id}", {
|
|
25
|
+
params: { path: { id: options.projectId } },
|
|
26
|
+
signal: options.signal
|
|
27
|
+
});
|
|
28
|
+
if (result.response?.status === 400) throw projectRemoveBlockedError(options.projectId, result.error);
|
|
29
|
+
if (result.error) throw projectApiError("Failed to remove project", result.response, result.error);
|
|
30
|
+
},
|
|
31
|
+
async transferProject(options) {
|
|
32
|
+
const result = await client.POST("/v1/projects/{id}/transfer", {
|
|
33
|
+
params: { path: { id: options.projectId } },
|
|
34
|
+
body: { recipientAccessToken: options.recipientAccessToken },
|
|
35
|
+
signal: options.signal
|
|
36
|
+
});
|
|
37
|
+
if (result.response?.status === 400) throw projectTransferRejectedError(options.projectId, result.error);
|
|
38
|
+
if (result.error) throw projectApiError("Failed to transfer project", result.response, result.error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function projectRenameFailedError(name, error) {
|
|
43
|
+
return new CliError({
|
|
44
|
+
code: "PROJECT_RENAME_FAILED",
|
|
45
|
+
domain: "project",
|
|
46
|
+
summary: "Project rename failed",
|
|
47
|
+
why: error?.error?.message ?? `The platform rejected the name "${name}".`,
|
|
48
|
+
fix: error?.error?.hint ?? "Pass a different project name and retry the rename.",
|
|
49
|
+
exitCode: 1,
|
|
50
|
+
nextSteps: []
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function projectRemoveBlockedError(projectId, error) {
|
|
54
|
+
return new CliError({
|
|
55
|
+
code: "PROJECT_REMOVE_BLOCKED",
|
|
56
|
+
domain: "project",
|
|
57
|
+
summary: "Project cannot be removed yet",
|
|
58
|
+
why: error?.error?.message ?? `Project "${projectId}" still has active deployments.`,
|
|
59
|
+
fix: "Remove the project's apps first, then retry the removal.",
|
|
60
|
+
exitCode: 1,
|
|
61
|
+
nextSteps: [formatPrismaCliCommand([
|
|
62
|
+
"app",
|
|
63
|
+
"remove",
|
|
64
|
+
"--app",
|
|
65
|
+
"<name>"
|
|
66
|
+
])]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function projectTransferRejectedError(projectId, error) {
|
|
70
|
+
return new CliError({
|
|
71
|
+
code: "PROJECT_TRANSFER_REJECTED",
|
|
72
|
+
domain: "project",
|
|
73
|
+
summary: "Project transfer was rejected",
|
|
74
|
+
why: error?.error?.message ?? `The platform rejected the transfer of project "${projectId}", for example because the recipient token is invalid or expired.`,
|
|
75
|
+
fix: "Check the recipient workspace session or token and retry the transfer.",
|
|
76
|
+
exitCode: 1,
|
|
77
|
+
nextSteps: []
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function projectApiError(summary, response, error) {
|
|
81
|
+
const status = response?.status ?? 0;
|
|
82
|
+
return new CliError({
|
|
83
|
+
code: error?.error?.code ?? "PROJECT_API_ERROR",
|
|
84
|
+
domain: "project",
|
|
85
|
+
summary,
|
|
86
|
+
why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
|
|
87
|
+
fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
|
|
88
|
+
exitCode: 1,
|
|
89
|
+
nextSteps: []
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { createManagementProjectProvider, projectApiError };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatCommandArgument } from "../../command-arguments.js";
|
|
2
|
+
import { CliError } from "../../errors.js";
|
|
3
|
+
import { sameWorkspaceId } from "../workspace-id.js";
|
|
2
4
|
import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
|
|
3
|
-
import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
4
|
-
import { readFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
6
7
|
import { Result, TaggedError, matchError } from "better-result";
|
|
7
8
|
//#region src/lib/project/resolution.ts
|
|
8
9
|
var ProjectNotFoundError = class extends TaggedError("ProjectNotFoundError")() {
|
|
@@ -283,8 +284,9 @@ async function inferTargetName(cwd, signal) {
|
|
|
283
284
|
source: "directory-name"
|
|
284
285
|
};
|
|
285
286
|
}
|
|
287
|
+
const INFERRED_TARGET_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
286
288
|
function isValidInferredTargetName(value) {
|
|
287
|
-
return
|
|
289
|
+
return INFERRED_TARGET_NAME.test(value);
|
|
288
290
|
}
|
|
289
291
|
function sortProjects(projects) {
|
|
290
292
|
return projects.slice().sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
|
|
@@ -321,7 +323,7 @@ async function resolveBoundProjectTarget(options, projects, settings) {
|
|
|
321
323
|
const localPin = settings.localPin;
|
|
322
324
|
if (!localPin) return Result.ok(null);
|
|
323
325
|
if (localPin.kind === "present") {
|
|
324
|
-
if (localPin.pin.workspaceId
|
|
326
|
+
if (!sameWorkspaceId(localPin.pin.workspaceId, options.workspace.id)) return Result.err(new LocalProjectWorkspaceMismatchError({
|
|
325
327
|
pinnedWorkspaceId: localPin.pin.workspaceId,
|
|
326
328
|
pinnedProjectId: localPin.pin.projectId,
|
|
327
329
|
activeWorkspace: options.workspace
|
|
@@ -334,7 +336,7 @@ async function resolveBoundProjectTarget(options, projects, settings) {
|
|
|
334
336
|
}));
|
|
335
337
|
}
|
|
336
338
|
const platformMapping = await resolveDurablePlatformMapping();
|
|
337
|
-
if (platformMapping && platformMapping.workspace.id
|
|
339
|
+
if (platformMapping && sameWorkspaceId(platformMapping.workspace.id, options.workspace.id)) return Result.ok(resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
|
|
338
340
|
targetName: platformMapping.name,
|
|
339
341
|
targetNameSource: "platform-mapping"
|
|
340
342
|
}));
|
|
@@ -345,7 +347,7 @@ async function readImplicitLocalPin(options, settings) {
|
|
|
345
347
|
const localPinResult = await readLocalResolutionPin(options.projectDir ?? options.context.runtime.cwd, options.context.runtime.signal);
|
|
346
348
|
if (localPinResult.isErr()) return Result.err(localPinReadErrorToProjectError(localPinResult.error));
|
|
347
349
|
const localPin = localPinResult.value;
|
|
348
|
-
if (localPin.kind === "present" && localPin.pin.workspaceId
|
|
350
|
+
if (localPin.kind === "present" && !sameWorkspaceId(localPin.pin.workspaceId, options.workspace.id)) return Result.err(new LocalProjectWorkspaceMismatchError({
|
|
349
351
|
pinnedWorkspaceId: localPin.pin.workspaceId,
|
|
350
352
|
pinnedProjectId: localPin.pin.projectId,
|
|
351
353
|
activeWorkspace: options.workspace
|
|
@@ -379,7 +381,8 @@ function toProjectSummary(project) {
|
|
|
379
381
|
return {
|
|
380
382
|
id: project.id,
|
|
381
383
|
name: project.name,
|
|
382
|
-
...project.url ? { url: project.url } : {}
|
|
384
|
+
...project.url ? { url: project.url } : {},
|
|
385
|
+
...project.defaultRegion != null ? { defaultRegion: project.defaultRegion } : {}
|
|
383
386
|
};
|
|
384
387
|
}
|
|
385
388
|
//#endregion
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { CliError, usageError } from "../../
|
|
1
|
+
import { CliError, usageError } from "../../errors.js";
|
|
2
2
|
import { shortenHomePath } from "../fs/home-path.js";
|
|
3
3
|
import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
|
|
4
|
-
import "../../shell/command-arguments.js";
|
|
5
4
|
import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import { Result, matchError } from "better-result";
|
|
@@ -15,8 +14,9 @@ function validateProjectSetupNameText(value, fallback) {
|
|
|
15
14
|
}
|
|
16
15
|
function resolveProjectForSetup(projectRef, projects, workspace) {
|
|
17
16
|
const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
|
|
18
|
-
if (matches.length === 1) return matches[0];
|
|
19
17
|
if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
|
|
18
|
+
const match = matches[0];
|
|
19
|
+
if (match !== void 0) return match;
|
|
20
20
|
throw projectNotFoundError(projectRef, workspace);
|
|
21
21
|
}
|
|
22
22
|
async function bindProjectToDirectory(context, workspace, project, action, directory = context.runtime.cwd) {
|
|
@@ -82,7 +82,8 @@ function toProjectSummary(project) {
|
|
|
82
82
|
return {
|
|
83
83
|
id: project.id,
|
|
84
84
|
name: project.name,
|
|
85
|
-
...project.url ? { url: project.url } : {}
|
|
85
|
+
...project.url ? { url: project.url } : {},
|
|
86
|
+
...project.defaultRegion != null ? { defaultRegion: project.defaultRegion } : {}
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
function projectSetupNameRequiredError(command) {
|
|
@@ -111,18 +112,20 @@ function projectCreateFailedError(error, projectName, workspace, options) {
|
|
|
111
112
|
nextSteps: options.nextSteps
|
|
112
113
|
});
|
|
113
114
|
}
|
|
115
|
+
const PATH_SEPARATORS = /[\\/]/;
|
|
114
116
|
function formatSetupDirectory(directory, context) {
|
|
115
117
|
if (path.resolve(directory) !== path.resolve(context.runtime.cwd)) return shortenHomePath(directory, context.runtime.env);
|
|
116
|
-
const basename = directory.split(
|
|
118
|
+
const basename = directory.split(PATH_SEPARATORS).filter(Boolean).pop();
|
|
117
119
|
return basename ? `./${basename}` : ".";
|
|
118
120
|
}
|
|
121
|
+
const HTTP_STATUS_IN_MESSAGE = /\(HTTP (\d{3})\)/;
|
|
119
122
|
function extractHttpStatus(error) {
|
|
120
123
|
if (!error || typeof error !== "object") return null;
|
|
121
124
|
const candidate = error;
|
|
122
125
|
if (typeof candidate.statusCode === "number") return candidate.statusCode;
|
|
123
126
|
if (typeof candidate.status === "number") return candidate.status;
|
|
124
127
|
if (typeof candidate.message === "string") {
|
|
125
|
-
const match =
|
|
128
|
+
const match = HTTP_STATUS_IN_MESSAGE.exec(candidate.message);
|
|
126
129
|
if (match) return Number.parseInt(match[1], 10);
|
|
127
130
|
}
|
|
128
131
|
return null;
|
package/dist/lib/version.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CliError } from "../
|
|
1
|
+
import { CliError } from "../errors.js";
|
|
2
|
+
import { CLI_NAME } from "../cli-name.js";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
//#region src/lib/version.ts
|
|
@@ -23,7 +24,7 @@ function getCliVersion() {
|
|
|
23
24
|
return pkg.version;
|
|
24
25
|
}
|
|
25
26
|
function getCliName() {
|
|
26
|
-
return
|
|
27
|
+
return CLI_NAME;
|
|
27
28
|
}
|
|
28
29
|
function detectInvocation(env, argv) {
|
|
29
30
|
if (env.npm_config_user_agent?.startsWith("bun")) return "bunx";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/lib/workspace-id.ts
|
|
2
|
+
/**
|
|
3
|
+
* A workspace reaches the CLI under two ids for the same workspace: a
|
|
4
|
+
* credential's `workspace_id` claim carries the bare id, while the
|
|
5
|
+
* management API and anything derived from it carry the same id behind
|
|
6
|
+
* a `wksp_` prefix. Comparing the two forms directly silently matches
|
|
7
|
+
* nothing, so every comparison between workspace ids of different
|
|
8
|
+
* origin goes through here.
|
|
9
|
+
*/
|
|
10
|
+
const WORKSPACE_ID_PREFIX = "wksp_";
|
|
11
|
+
function stripWorkspacePrefix(value) {
|
|
12
|
+
return value.startsWith(WORKSPACE_ID_PREFIX) ? value.slice(5) : value;
|
|
13
|
+
}
|
|
14
|
+
function sameWorkspaceId(left, right) {
|
|
15
|
+
return stripWorkspacePrefix(left) === stripWorkspacePrefix(right);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { sameWorkspaceId };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region ../cli-telemetry/dist/payload-B88Qvzxk.js
|
|
2
|
+
function isNonEmptyString(value) {
|
|
3
|
+
return typeof value === "string" && value.length > 0;
|
|
4
|
+
}
|
|
5
|
+
function isStringArray(value) {
|
|
6
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Runtime validation for {@link ParentToSenderPayload}. The child sender
|
|
10
|
+
* uses this before `postEvent` so a payload missing a required field
|
|
11
|
+
* cannot silently produce a degraded telemetry event downstream.
|
|
12
|
+
*
|
|
13
|
+
* Same semantics as the ORM CLI's arktype schema (this repo carries no
|
|
14
|
+
* arktype dependency, so the checks are spelled out): required scalars
|
|
15
|
+
* must be non-empty strings; the optional `databaseTarget` override is
|
|
16
|
+
* a `string` when present (no `null` — see the type's doc-block); the
|
|
17
|
+
* string array is validated element-by-element. Size caps are enforced
|
|
18
|
+
* by the backend, not here — IPC is structured-cloned and the
|
|
19
|
+
* parent/child agree on the schema by version-coupling.
|
|
20
|
+
*/
|
|
21
|
+
function isParentToSenderPayload(value) {
|
|
22
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
23
|
+
const record = value;
|
|
24
|
+
if (!isNonEmptyString(record.installationId)) return false;
|
|
25
|
+
if (!isNonEmptyString(record.version)) return false;
|
|
26
|
+
if (!isNonEmptyString(record.command)) return false;
|
|
27
|
+
if (!isStringArray(record.flags)) return false;
|
|
28
|
+
if (!isNonEmptyString(record.projectRoot)) return false;
|
|
29
|
+
if (!isNonEmptyString(record.endpoint)) return false;
|
|
30
|
+
if ("databaseTarget" in record && typeof record.databaseTarget !== "string") return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { isParentToSenderPayload as t };
|