@prisma/cli 3.0.0-beta.4 → 3.0.0-beta.5

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.
@@ -0,0 +1,82 @@
1
+ import { usageError } from "../../shell/errors.js";
2
+ import { validateKey } from "./env-config.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { parse } from "dotenv";
6
+ //#region src/lib/app/env-file.ts
7
+ const ASSIGNMENT_KEY_PATTERN = /^\s*(?:export\s+)?([^#=\s]+)\s*=/;
8
+ async function readEnvFileAssignments(cwd, filePath, command) {
9
+ const resolvedPath = path.resolve(cwd, filePath);
10
+ let contents;
11
+ try {
12
+ contents = await readFile(resolvedPath, "utf8");
13
+ } catch (error) {
14
+ throw usageError(`Failed to read env file "${filePath}"`, error instanceof Error ? error.message : "The file could not be read.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
15
+ }
16
+ return parseEnvFileContents(contents, filePath, command);
17
+ }
18
+ function parseEnvFileContents(contents, filePath, command) {
19
+ const parsedKeys = extractParsedKeys(contents);
20
+ if (parsedKeys.length === 0) throw usageError(`No environment variables found in "${filePath}"`, "The file does not contain any KEY=VALUE assignments.", "Pass a dotenv file with at least one non-empty variable.", [], "app");
21
+ const seen = /* @__PURE__ */ new Map();
22
+ for (const entry of parsedKeys) {
23
+ validateEnvFileKey(entry.key, entry.line, filePath, command);
24
+ const firstLine = seen.get(entry.key);
25
+ if (firstLine !== void 0) throw usageError(`Duplicate environment variable "${entry.key}" in "${filePath}"`, `Lines ${firstLine} and ${entry.line} both define ${entry.key}.`, "Keep one assignment for each key before importing the file.", [], "app");
26
+ seen.set(entry.key, entry.line);
27
+ }
28
+ const parsedValues = parse(contents);
29
+ return parsedKeys.map(({ key }) => {
30
+ const value = parsedValues[key];
31
+ if (typeof value !== "string" || value.length === 0) {
32
+ const line = seen.get(key);
33
+ throw usageError(`Environment variable "${key}" in "${filePath}" has an empty value`, line === void 0 ? `${key} has an empty value.` : `Line ${line} defines ${key} with an empty value.`, "Pass a non-empty value, or omit the key from the file.", [], "app");
34
+ }
35
+ return {
36
+ key,
37
+ value
38
+ };
39
+ });
40
+ }
41
+ function extractParsedKeys(contents) {
42
+ const keys = [];
43
+ let multilineQuote = null;
44
+ const lines = contents.split(/\n/);
45
+ for (const [index, line] of lines.entries()) {
46
+ const lineNumber = index + 1;
47
+ if (multilineQuote !== null) {
48
+ if (hasClosingQuote(line, multilineQuote, 0)) multilineQuote = null;
49
+ continue;
50
+ }
51
+ const match = ASSIGNMENT_KEY_PATTERN.exec(line);
52
+ if (!match) continue;
53
+ const key = match[1];
54
+ keys.push({
55
+ key,
56
+ line: lineNumber
57
+ });
58
+ const valueStart = line.slice(match[0].length).trimStart();
59
+ const openingQuote = valueStart[0];
60
+ if ((openingQuote === "\"" || openingQuote === "'" || openingQuote === "`") && !hasClosingQuote(valueStart, openingQuote, 1)) multilineQuote = openingQuote;
61
+ }
62
+ return keys;
63
+ }
64
+ function validateEnvFileKey(key, line, filePath, command) {
65
+ try {
66
+ validateKey(key, command);
67
+ } catch (error) {
68
+ const reason = error instanceof Error && error.message.length > 0 ? error.message : "Invalid environment variable key.";
69
+ throw usageError(`Invalid environment variable "${key}" in "${filePath}"`, `Line ${line}: ${reason}`, "Use a valid env-var key and retry the import.", [], "app");
70
+ }
71
+ }
72
+ function hasClosingQuote(value, quote, startIndex) {
73
+ for (let index = startIndex; index < value.length; index += 1) if (value[index] === quote && !isEscaped(value, index)) return true;
74
+ return false;
75
+ }
76
+ function isEscaped(value, index) {
77
+ let backslashes = 0;
78
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) backslashes += 1;
79
+ return backslashes % 2 === 1;
80
+ }
81
+ //#endregion
82
+ export { readEnvFileAssignments };
@@ -0,0 +1,102 @@
1
+ //#region src/lib/app/preview-branch-database.ts
2
+ async function createBranchDatabase(client, options) {
3
+ const result = await client.POST("/v1/databases", {
4
+ body: {
5
+ projectId: options.projectId,
6
+ branchId: options.branchId,
7
+ name: options.branchName,
8
+ source: { type: "empty" }
9
+ },
10
+ signal: options.signal
11
+ });
12
+ if (result.error || !result.data) throw apiCallError(`Failed to create database for branch "${options.branchName}"`, result.response, result.error);
13
+ return normalizeBranchDatabaseRecord(result.data.data);
14
+ }
15
+ async function listEnvironmentVariables(client, options) {
16
+ const variables = [];
17
+ let cursor;
18
+ while (true) {
19
+ const result = await client.GET("/v1/environment-variables", {
20
+ params: { query: {
21
+ projectId: options.projectId,
22
+ class: options.className,
23
+ key: options.key,
24
+ branchId: options.branchId,
25
+ cursor
26
+ } },
27
+ signal: options.signal
28
+ });
29
+ if (result.error || !result.data) throw apiCallError("Failed to list environment variables", result.response, result.error);
30
+ variables.push(...result.data.data);
31
+ if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
32
+ cursor = result.data.pagination.nextCursor;
33
+ }
34
+ return variables.map((variable) => normalizeEnvironmentVariable(variable));
35
+ }
36
+ async function createEnvironmentVariable(client, options) {
37
+ const result = await client.POST("/v1/environment-variables", {
38
+ body: {
39
+ projectId: options.projectId,
40
+ class: options.className,
41
+ key: options.key,
42
+ value: options.value,
43
+ ...options.branchId ? { branchId: options.branchId } : {}
44
+ },
45
+ signal: options.signal
46
+ });
47
+ if (result.error || !result.data) throw apiCallError(`Failed to add ${options.key}`, result.response, result.error);
48
+ return normalizeEnvironmentVariable(result.data.data);
49
+ }
50
+ async function deleteBranchDatabase(client, options) {
51
+ const result = await client.DELETE("/v1/databases/{databaseId}", {
52
+ params: { path: { databaseId: options.databaseId } },
53
+ signal: options.signal
54
+ });
55
+ if (result.error) throw apiCallError("Failed to delete branch database", result.response, result.error);
56
+ }
57
+ async function updateEnvironmentVariable(client, options) {
58
+ const result = await client.PATCH("/v1/environment-variables/{envVarId}", {
59
+ params: { path: { envVarId: options.envVarId } },
60
+ body: { value: options.value },
61
+ signal: options.signal
62
+ });
63
+ if (result.error || !result.data) throw apiCallError("Failed to update environment variable", result.response, result.error);
64
+ return normalizeEnvironmentVariable(result.data.data);
65
+ }
66
+ async function deleteEnvironmentVariable(client, options) {
67
+ const result = await client.DELETE("/v1/environment-variables/{envVarId}", {
68
+ params: { path: { envVarId: options.envVarId } },
69
+ signal: options.signal
70
+ });
71
+ if (result.error) throw apiCallError("Failed to delete environment variable", result.response, result.error);
72
+ }
73
+ function normalizeEnvironmentVariable(variable) {
74
+ return {
75
+ id: variable.id,
76
+ key: variable.key,
77
+ branchId: variable.branchId,
78
+ className: variable.class,
79
+ isManagedBySystem: variable.isManagedBySystem
80
+ };
81
+ }
82
+ function normalizeBranchDatabaseRecord(database) {
83
+ const connection = database.connections?.[0];
84
+ const databaseUrl = connection?.endpoints?.pooled?.connectionString;
85
+ const directUrl = connection?.endpoints?.direct?.connectionString ?? null;
86
+ if (!databaseUrl) throw new Error("Created database did not return a pooled connection string.");
87
+ return {
88
+ id: database.id,
89
+ name: database.name,
90
+ branchId: database.branchId,
91
+ databaseUrl,
92
+ directUrl
93
+ };
94
+ }
95
+ function apiCallError(summary, response, error) {
96
+ if (response.status === 404) return /* @__PURE__ */ new Error("Resource Not Found");
97
+ const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`;
98
+ const hint = error.error?.hint ? ` ${error.error.hint}` : "";
99
+ return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
100
+ }
101
+ //#endregion
102
+ export { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable };
@@ -1,5 +1,6 @@
1
1
  import { envVarNames } from "./env-vars.js";
2
2
  import { PreviewBuildStrategy } from "./preview-build.js";
3
+ import { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable } from "./preview-branch-database.js";
3
4
  import path from "node:path";
4
5
  import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
5
6
  //#region src/lib/app/preview-provider.ts
@@ -48,6 +49,24 @@ function createPreviewAppProvider(client, options) {
48
49
  role: branch.role
49
50
  };
50
51
  },
52
+ async createBranchDatabase(options) {
53
+ return createBranchDatabase(client, options);
54
+ },
55
+ async deleteBranchDatabase(options) {
56
+ return deleteBranchDatabase(client, options);
57
+ },
58
+ async listEnvironmentVariables(options) {
59
+ return listEnvironmentVariables(client, options);
60
+ },
61
+ async createEnvironmentVariable(options) {
62
+ return createEnvironmentVariable(client, options);
63
+ },
64
+ async updateEnvironmentVariable(options) {
65
+ return updateEnvironmentVariable(client, options);
66
+ },
67
+ async deleteEnvironmentVariable(options) {
68
+ return deleteEnvironmentVariable(client, options);
69
+ },
51
70
  async removeApp(appId, options) {
52
71
  const appResult = await sdk.showService({
53
72
  serviceId: appId,
@@ -5,8 +5,12 @@ import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  //#region src/lib/project/resolution.ts
7
7
  async function resolveProjectTarget(options) {
8
+ const localPin = await readImplicitLocalPin(options, { allowEnvProjectId: true });
8
9
  const projects = await options.listProjects();
9
- const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: true });
10
+ const target = await resolveBoundProjectTarget(options, projects, {
11
+ allowEnvProjectId: true,
12
+ localPin
13
+ });
10
14
  if (target) return target;
11
15
  throw await projectSetupRequiredError({
12
16
  cwd: options.context.runtime.cwd,
@@ -16,8 +20,12 @@ async function resolveProjectTarget(options) {
16
20
  });
17
21
  }
18
22
  async function inspectProjectBinding(options) {
23
+ const localPin = await readImplicitLocalPin(options, { allowEnvProjectId: false });
19
24
  const projects = await options.listProjects();
20
- const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: false });
25
+ const target = await resolveBoundProjectTarget(options, projects, {
26
+ allowEnvProjectId: false,
27
+ localPin
28
+ });
21
29
  if (target) return target;
22
30
  return {
23
31
  workspace: options.workspace,
@@ -73,6 +81,28 @@ function localStateStaleError() {
73
81
  nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"]
74
82
  });
75
83
  }
84
+ function localProjectWorkspaceMismatchError(options) {
85
+ return new CliError({
86
+ code: "LOCAL_PROJECT_WORKSPACE_MISMATCH",
87
+ domain: "project",
88
+ summary: "Project link uses another workspace",
89
+ why: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} links this directory to project ${options.pinnedProjectId} in workspace ${options.pinnedWorkspaceId}, but your current CLI session is workspace "${options.activeWorkspace.name}" (${options.activeWorkspace.id}).`,
90
+ fix: "Sign in to the linked workspace, or relink this directory to a project in the current workspace.",
91
+ meta: {
92
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
93
+ pinnedWorkspaceId: options.pinnedWorkspaceId,
94
+ pinnedProjectId: options.pinnedProjectId,
95
+ activeWorkspaceId: options.activeWorkspace.id,
96
+ activeWorkspaceName: options.activeWorkspace.name
97
+ },
98
+ exitCode: 1,
99
+ nextSteps: [
100
+ "prisma-cli auth login",
101
+ "prisma-cli project list",
102
+ "prisma-cli project link <id-or-name>"
103
+ ]
104
+ });
105
+ }
76
106
  async function buildProjectSetupSuggestion(options) {
77
107
  const suggestedName = await inferTargetName(options.cwd, options.signal);
78
108
  const candidates = sortProjects(options.projects.filter((project) => projectMatchesSuggestedName(project, suggestedName.name))).map(toProjectSummary);
@@ -192,10 +222,15 @@ async function resolveBoundProjectTarget(options, projects, settings) {
192
222
  targetNameSource: "env"
193
223
  });
194
224
  }
195
- const localPin = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
225
+ const localPin = settings.localPin;
226
+ if (!localPin) return null;
196
227
  if (localPin.kind === "invalid") throw localStateStaleError();
197
228
  if (localPin.kind === "present") {
198
- if (localPin.pin.workspaceId !== options.workspace.id) throw localStateStaleError();
229
+ if (localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
230
+ pinnedWorkspaceId: localPin.pin.workspaceId,
231
+ pinnedProjectId: localPin.pin.projectId,
232
+ activeWorkspace: options.workspace
233
+ });
199
234
  const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
200
235
  if (!project) throw localStateStaleError();
201
236
  return resolvedTarget(options.workspace, project, "local-pin", {
@@ -210,6 +245,16 @@ async function resolveBoundProjectTarget(options, projects, settings) {
210
245
  });
211
246
  return null;
212
247
  }
248
+ async function readImplicitLocalPin(options, settings) {
249
+ if (options.explicitProject || settings.allowEnvProjectId && options.envProjectId) return null;
250
+ const localPin = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
251
+ if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
252
+ pinnedWorkspaceId: localPin.pin.workspaceId,
253
+ pinnedProjectId: localPin.pin.projectId,
254
+ activeWorkspace: options.workspace
255
+ });
256
+ return localPin;
257
+ }
213
258
  function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
214
259
  return {
215
260
  workspace,
@@ -15,6 +15,21 @@ function listTargetLabel(result) {
15
15
  return scopeLabel(result.scope);
16
16
  }
17
17
  function renderEnvAdd(context, descriptor, result) {
18
+ if (result.variables !== void 0) return renderList({
19
+ title: "Setting new environment variables from file.",
20
+ descriptor,
21
+ parentContext: {
22
+ key: "target",
23
+ value: `${scopeLabel(result.scope)} from ${result.file.path}`
24
+ },
25
+ items: result.variables.map((variable) => ({
26
+ noun: "variable",
27
+ label: `${variable.key} (${variable.source})`,
28
+ id: variable.id,
29
+ status: variable.isManagedBySystem ? "default" : null
30
+ })),
31
+ emptyMessage: "No environment variables imported."
32
+ }, context.ui);
18
33
  return renderShow({
19
34
  title: "Setting a new environment variable.",
20
35
  descriptor,
@@ -48,6 +63,21 @@ function serializeEnvAdd(result) {
48
63
  return result;
49
64
  }
50
65
  function renderEnvUpdate(context, descriptor, result) {
66
+ if (result.variables !== void 0) return renderList({
67
+ title: "Replacing environment variable values from file.",
68
+ descriptor,
69
+ parentContext: {
70
+ key: "target",
71
+ value: `${scopeLabel(result.scope)} from ${result.file.path}`
72
+ },
73
+ items: result.variables.map((variable) => ({
74
+ noun: "variable",
75
+ label: `${variable.key} (${variable.source})`,
76
+ id: variable.id,
77
+ status: variable.isManagedBySystem ? "default" : null
78
+ })),
79
+ emptyMessage: "No environment variables updated."
80
+ }, context.ui);
51
81
  return renderShow({
52
82
  title: "Replacing the environment variable's value.",
53
83
  descriptor,
@@ -30,6 +30,7 @@ function renderAppDeploy(context, descriptor, result) {
30
30
  return [
31
31
  `Live in ${formatDuration(result.durationMs)}`,
32
32
  ...result.deployment.url ? [context.ui.link(result.deployment.url)] : [],
33
+ ...renderBranchDatabaseDeploySummary(context, result),
33
34
  "",
34
35
  ...renderDeployOutputRows(context.ui, [{
35
36
  label: "Logs",
@@ -41,6 +42,30 @@ function serializeAppDeploy(result) {
41
42
  const { localPin: _localPin, ...serialized } = result;
42
43
  return serialized;
43
44
  }
45
+ function renderBranchDatabaseDeploySummary(context, result) {
46
+ if (!result.branchDatabase || result.branchDatabase.status !== "created") return [];
47
+ return ["", ...renderDeployOutputRows(context.ui, [
48
+ {
49
+ label: "Database",
50
+ value: result.branchDatabase.database?.name ?? "created"
51
+ },
52
+ {
53
+ label: "Env",
54
+ value: result.branchDatabase.envVars.join(", ")
55
+ },
56
+ ...result.branchDatabase.schema ? [{
57
+ label: "Schema",
58
+ value: formatBranchDatabaseSchemaCommand(result.branchDatabase.schema.command)
59
+ }] : []
60
+ ])];
61
+ }
62
+ function formatBranchDatabaseSchemaCommand(command) {
63
+ switch (command) {
64
+ case "migrate-deploy": return "prisma migrate deploy";
65
+ case "db-push": return "prisma db push";
66
+ case "prisma-next-db-init": return "prisma-next db init";
67
+ }
68
+ }
44
69
  function formatDuration(durationMs) {
45
70
  if (durationMs < 1e3) return `${durationMs}ms`;
46
71
  return `${(durationMs / 1e3).toFixed(1)}s`;
@@ -1,25 +1,24 @@
1
1
  import { padDisplay, renderNextSteps, renderSummaryLine } from "../shell/ui.js";
2
2
  import { formatDescriptorLabel } from "../shell/command-meta.js";
3
3
  import { formatCommandArgument } from "../shell/command-arguments.js";
4
- import { renderList, renderMutate, renderShow, serializeList } from "../output/patterns.js";
4
+ import { renderMutate, renderShow, serializeList } from "../output/patterns.js";
5
5
  import path from "node:path";
6
+ import stringWidth from "string-width";
6
7
  //#region src/presenters/project.ts
7
8
  function renderProjectList(context, descriptor, result) {
8
- const lines = renderList({
9
- title: "Listing projects for the authenticated workspace.",
10
- descriptor,
11
- parentContext: {
12
- key: "workspace",
13
- value: result.workspace.name
14
- },
15
- items: result.projects.map((project) => ({
16
- noun: "project",
17
- label: project.name,
18
- id: project.id,
19
- status: null
20
- })),
21
- emptyMessage: "No projects found."
22
- }, context.ui);
9
+ const ui = context.ui;
10
+ const rail = ui.dim("│");
11
+ const lines = [`${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim("Listing projects for the authenticated workspace.")}`, ""];
12
+ lines.push(`${rail} ${ui.accent("workspace:")} ${result.workspace.name}`);
13
+ if (result.projects.length === 0) {
14
+ lines.push(`${rail} ${ui.dim("No projects found.")}`);
15
+ if (result.localBinding?.status === "not-linked" || result.localBinding?.status === "invalid") lines.push(...renderNextSteps(["Link an existing Project you choose: prisma-cli project link <id-or-name>", "Create a new Project: prisma-cli project create <name>"]));
16
+ return lines;
17
+ }
18
+ const nameWidth = Math.max(4, ...result.projects.map((project) => stringWidth(project.name)));
19
+ lines.push(rail);
20
+ lines.push(`${rail} ${ui.accent(padDisplay("name", nameWidth))} ${ui.accent("id")}`);
21
+ for (const project of result.projects) lines.push(`${rail} ${padDisplay(project.name, nameWidth)} ${project.id}`);
23
22
  if (result.localBinding?.status === "not-linked" || result.localBinding?.status === "invalid") lines.push(...renderNextSteps(["Link an existing Project you choose: prisma-cli project link <id-or-name>", "Create a new Project: prisma-cli project create <name>"]));
24
23
  return lines;
25
24
  }
@@ -194,6 +194,8 @@ const DESCRIPTORS = [
194
194
  "prisma-cli app deploy --project proj_123",
195
195
  "prisma-cli app deploy --create-project my-app --yes",
196
196
  "prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example",
197
+ "prisma-cli app deploy --db",
198
+ "prisma-cli app deploy --db --yes",
197
199
  "prisma-cli app deploy --app my-app --framework nextjs --http-port 3000",
198
200
  "prisma-cli app deploy --branch feat-login --framework hono",
199
201
  "prisma-cli app deploy --prod --yes",
@@ -361,6 +363,7 @@ const DESCRIPTORS = [
361
363
  examples: [
362
364
  "prisma-cli project env list",
363
365
  "prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production",
366
+ "prisma-cli project env add --file .env --role preview",
364
367
  "prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo",
365
368
  "prisma-cli project env remove STRIPE_KEY --role preview"
366
369
  ]
@@ -377,7 +380,9 @@ const DESCRIPTORS = [
377
380
  examples: [
378
381
  "prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production",
379
382
  "prisma-cli project env add STRIPE_KEY=sk_test_xxx --role preview",
383
+ "prisma-cli project env add --file .env --role preview",
380
384
  "prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo",
385
+ "prisma-cli project env add --file .env.local --branch feature/foo",
381
386
  "API_URL=https://api.example prisma-cli project env add API_URL --project proj_123 --role preview"
382
387
  ]
383
388
  },
@@ -393,6 +398,7 @@ const DESCRIPTORS = [
393
398
  examples: [
394
399
  "prisma-cli project env update STRIPE_KEY=sk_new_xxx --role production",
395
400
  "prisma-cli project env update STRIPE_KEY=sk_new_xxx --role preview",
401
+ "prisma-cli project env update --file .env --role production",
396
402
  "prisma-cli project env update DATABASE_URL=postgresql://branch --branch feature/foo"
397
403
  ]
398
404
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-beta.4",
3
+ "version": "3.0.0-beta.5",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,6 +42,7 @@
42
42
  "c12": "4.0.0-beta.5",
43
43
  "colorette": "^2.0.20",
44
44
  "commander": "^14.0.3",
45
+ "dotenv": "^17.4.2",
45
46
  "magicast": "^0.5.3",
46
47
  "open": "^11.0.0",
47
48
  "string-width": "^8.2.1",