@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.30

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 (99) hide show
  1. package/README.md +6 -15
  2. package/dist/adapters/local-state.js +15 -4
  3. package/dist/adapters/mock-api.js +244 -0
  4. package/dist/adapters/token-storage.js +335 -34
  5. package/dist/cli.js +7 -7
  6. package/dist/cli2.js +24 -5
  7. package/dist/commands/agent/index.js +60 -0
  8. package/dist/commands/app/index.js +91 -61
  9. package/dist/commands/auth/index.js +55 -2
  10. package/dist/commands/branch/index.js +2 -27
  11. package/dist/commands/bucket/index.js +123 -0
  12. package/dist/commands/build/index.js +29 -0
  13. package/dist/commands/database/index.js +249 -0
  14. package/dist/commands/env.js +8 -4
  15. package/dist/commands/feedback/index.js +20 -0
  16. package/dist/commands/git/index.js +1 -1
  17. package/dist/commands/init/index.js +33 -0
  18. package/dist/commands/project/index.js +54 -5
  19. package/dist/controllers/agent-setup.js +52 -0
  20. package/dist/controllers/agent.js +228 -0
  21. package/dist/controllers/app-env-api.js +55 -0
  22. package/dist/controllers/app-env-file.js +181 -0
  23. package/dist/controllers/app-env.js +227 -104
  24. package/dist/controllers/app.js +746 -306
  25. package/dist/controllers/auth.js +247 -3
  26. package/dist/controllers/branch.js +78 -48
  27. package/dist/controllers/bucket.js +278 -0
  28. package/dist/controllers/build.js +88 -0
  29. package/dist/controllers/database.js +567 -0
  30. package/dist/controllers/feedback.js +86 -0
  31. package/dist/controllers/init.js +753 -0
  32. package/dist/controllers/project.js +377 -22
  33. package/dist/controllers/select-prompt-port.js +1 -0
  34. package/dist/lib/agent/cli-command.js +20 -0
  35. package/dist/lib/agent/constants.js +12 -0
  36. package/dist/lib/agent/package-manager.js +99 -0
  37. package/dist/lib/agent/setup-status.js +83 -0
  38. package/dist/lib/app/{preview-provider.js → app-provider.js} +137 -88
  39. package/dist/lib/app/branch-database-api.js +102 -0
  40. package/dist/lib/app/branch-database-deploy.js +326 -0
  41. package/dist/lib/app/branch-database.js +216 -0
  42. package/dist/lib/app/build-settings.js +93 -0
  43. package/dist/lib/app/build.js +83 -0
  44. package/dist/lib/app/bun-project.js +3 -4
  45. package/dist/lib/app/compute-config.js +145 -0
  46. package/dist/lib/app/deploy-plan.js +59 -0
  47. package/dist/lib/app/{preview-progress.js → deploy-progress.js} +12 -12
  48. package/dist/lib/app/env-config.js +1 -1
  49. package/dist/lib/app/env-file.js +82 -0
  50. package/dist/lib/app/env-vars.js +28 -2
  51. package/dist/lib/app/local-dev.js +3 -60
  52. package/dist/lib/app/production-deploy-gate.js +162 -0
  53. package/dist/lib/app/read-branch.js +30 -0
  54. package/dist/lib/auth/auth-ops.js +10 -4
  55. package/dist/lib/auth/guard.js +4 -1
  56. package/dist/lib/auth/login.js +33 -26
  57. package/dist/lib/auth/recipient.js +42 -0
  58. package/dist/lib/bucket/provider.js +139 -0
  59. package/dist/lib/database/provider.js +378 -0
  60. package/dist/lib/diagnostics.js +15 -0
  61. package/dist/lib/fs/home-path.js +24 -0
  62. package/dist/lib/git/local-branch.js +53 -0
  63. package/dist/lib/git/local-status.js +57 -0
  64. package/dist/lib/project/interactive-setup.js +5 -4
  65. package/dist/lib/project/local-pin.js +171 -41
  66. package/dist/lib/project/provider.js +92 -0
  67. package/dist/lib/project/resolution.js +199 -48
  68. package/dist/lib/project/setup.js +67 -20
  69. package/dist/output/patterns.js +1 -1
  70. package/dist/presenters/agent.js +74 -0
  71. package/dist/presenters/app-env.js +149 -14
  72. package/dist/presenters/app.js +208 -27
  73. package/dist/presenters/auth.js +99 -2
  74. package/dist/presenters/branch.js +37 -102
  75. package/dist/presenters/bucket.js +174 -0
  76. package/dist/presenters/database.js +448 -0
  77. package/dist/presenters/feedback.js +26 -0
  78. package/dist/presenters/init.js +30 -0
  79. package/dist/presenters/project.js +139 -27
  80. package/dist/presenters/verbose-context.js +64 -0
  81. package/dist/shell/cli-command.js +12 -0
  82. package/dist/shell/command-arguments.js +7 -1
  83. package/dist/shell/command-meta.js +458 -17
  84. package/dist/shell/command-runner.js +58 -18
  85. package/dist/shell/diagnostics-output.js +57 -0
  86. package/dist/shell/errors.js +56 -1
  87. package/dist/shell/help.js +31 -20
  88. package/dist/shell/output.js +72 -1
  89. package/dist/shell/prompt.js +12 -5
  90. package/dist/shell/runtime.js +8 -4
  91. package/dist/shell/ui.js +42 -3
  92. package/dist/shell/update-check.js +2 -2
  93. package/dist/use-cases/auth.js +68 -1
  94. package/dist/use-cases/branch.js +20 -68
  95. package/dist/use-cases/create-cli-gateways.js +2 -17
  96. package/dist/use-cases/project.js +2 -1
  97. package/package.json +21 -4
  98. package/dist/lib/app/preview-build.js +0 -312
  99. package/dist/lib/app/preview-interaction.js +0 -5
@@ -0,0 +1,57 @@
1
+ import { renderVerboseBlock } from "./ui.js";
2
+ import { shortenHomePath } from "../lib/fs/home-path.js";
3
+ //#region src/shell/diagnostics-output.ts
4
+ function renderCommandDiagnostics(context, diagnostics, rows = [], options = {}) {
5
+ if (!diagnostics) return [];
6
+ const { env } = context.runtime;
7
+ const git = diagnostics.git;
8
+ return renderVerboseBlock(context.ui, [
9
+ ...rows,
10
+ ...diagnostics.durationMs === void 0 ? [] : [{
11
+ key: "duration",
12
+ value: formatDuration(diagnostics.durationMs)
13
+ }],
14
+ {
15
+ key: "cwd",
16
+ value: formatLocalPath(diagnostics.cwd, env)
17
+ },
18
+ {
19
+ key: "state file",
20
+ value: formatLocalPath(diagnostics.stateFilePath, env)
21
+ },
22
+ ...git ? [
23
+ {
24
+ key: "git ref",
25
+ value: git.ref ?? "detached",
26
+ tone: git.ref ? "default" : "dim"
27
+ },
28
+ {
29
+ key: "git sha",
30
+ value: git.sha ?? "unknown",
31
+ tone: git.sha ? "default" : "dim"
32
+ },
33
+ {
34
+ key: "git dirty",
35
+ value: formatDirtyState(git.dirty),
36
+ tone: git.dirty ? "warning" : "dim"
37
+ }
38
+ ] : [{
39
+ key: "git",
40
+ value: "not detected",
41
+ tone: "dim"
42
+ }]
43
+ ], { title: options.title ?? "Local context" });
44
+ }
45
+ function formatLocalPath(value, env) {
46
+ return shortenHomePath(value, env);
47
+ }
48
+ function formatDirtyState(dirty) {
49
+ if (dirty === null) return "unknown";
50
+ return dirty ? "yes" : "no";
51
+ }
52
+ function formatDuration(durationMs) {
53
+ if (durationMs < 1e3) return `${durationMs}ms`;
54
+ return `${(durationMs / 1e3).toFixed(1)}s`;
55
+ }
56
+ //#endregion
57
+ export { renderCommandDiagnostics };
@@ -44,6 +44,12 @@ function usageError(summary, why, fix, nextSteps = [], domain = "cli") {
44
44
  nextSteps
45
45
  });
46
46
  }
47
+ function isUsageError(error, summary) {
48
+ return isErrorRecord(error) && error.code === "USAGE_ERROR" && (summary === void 0 || error.summary === summary);
49
+ }
50
+ function isErrorRecord(error) {
51
+ return typeof error === "object" && error !== null;
52
+ }
47
53
  function authRequiredError(nextSteps = ["prisma-cli auth login"], options = {}) {
48
54
  return new CliError({
49
55
  code: "AUTH_REQUIRED",
@@ -56,6 +62,17 @@ function authRequiredError(nextSteps = ["prisma-cli auth login"], options = {})
56
62
  nextSteps
57
63
  });
58
64
  }
65
+ function authConfigInvalidError(message) {
66
+ return new CliError({
67
+ code: "AUTH_CONFIG_INVALID",
68
+ domain: "auth",
69
+ summary: "Authentication configuration is invalid",
70
+ why: message,
71
+ fix: "Provide a valid PRISMA_SERVICE_TOKEN value, or unset the variable to use local OAuth login.",
72
+ exitCode: 1,
73
+ nextSteps: ["prisma-cli auth login"]
74
+ });
75
+ }
59
76
  function commandCanceledError() {
60
77
  return new CliError({
61
78
  code: "COMMAND_CANCELED",
@@ -70,6 +87,44 @@ function commandCanceledError() {
70
87
  function workspaceRequiredError() {
71
88
  return usageError("Workspace required", "This command needs an active workspace, but the authenticated session does not have one.", "Run prisma-cli auth login and choose a workspace.", ["prisma-cli auth login"], "auth");
72
89
  }
90
+ function workspaceSwitchUnavailableError() {
91
+ return new CliError({
92
+ code: "WORKSPACE_SWITCH_UNAVAILABLE",
93
+ domain: "auth",
94
+ summary: "Workspace switching is unavailable",
95
+ why: "PRISMA_SERVICE_TOKEN is set, so authenticated commands use that token instead of local OAuth workspaces.",
96
+ fix: "Unset PRISMA_SERVICE_TOKEN to switch between local OAuth workspaces, or use a token for the workspace you want.",
97
+ exitCode: 1,
98
+ nextSteps: ["unset PRISMA_SERVICE_TOKEN", "prisma-cli auth workspace list"]
99
+ });
100
+ }
101
+ function workspaceNotAuthenticatedError(workspaceRef) {
102
+ return new CliError({
103
+ code: "WORKSPACE_NOT_AUTHENTICATED",
104
+ domain: "auth",
105
+ summary: "Workspace is not authenticated",
106
+ why: `No stored OAuth session matched "${workspaceRef}".`,
107
+ fix: "Run prisma-cli auth login and authorize that workspace, then switch to it.",
108
+ meta: { workspaceRef },
109
+ exitCode: 1,
110
+ nextSteps: ["prisma-cli auth workspace list", "prisma-cli auth login"]
111
+ });
112
+ }
113
+ function workspaceAmbiguousError(workspaceRef, matches) {
114
+ return new CliError({
115
+ code: "WORKSPACE_AMBIGUOUS",
116
+ domain: "auth",
117
+ summary: "Workspace name is ambiguous",
118
+ why: `Multiple authenticated workspaces matched "${workspaceRef}".`,
119
+ fix: "Run prisma-cli auth workspace list and switch by workspace id.",
120
+ meta: {
121
+ workspaceRef,
122
+ matches
123
+ },
124
+ exitCode: 2,
125
+ nextSteps: ["prisma-cli auth workspace list"]
126
+ });
127
+ }
73
128
  function featureUnavailableError(summary, why, fix, nextSteps = [], domain = "cli") {
74
129
  return new CliError({
75
130
  code: "FEATURE_UNAVAILABLE",
@@ -82,4 +137,4 @@ function featureUnavailableError(summary, why, fix, nextSteps = [], domain = "cl
82
137
  });
83
138
  }
84
139
  //#endregion
85
- export { CliError, authRequiredError, commandCanceledError, featureUnavailableError, usageError, workspaceRequiredError };
140
+ export { CliError, authConfigInvalidError, authRequiredError, commandCanceledError, featureUnavailableError, isUsageError, usageError, workspaceAmbiguousError, workspaceNotAuthenticatedError, workspaceRequiredError, workspaceSwitchUnavailableError };
@@ -1,5 +1,5 @@
1
- import { createShellUi, padDisplay, wrapText } from "./ui.js";
2
1
  import { formatDescriptorLabel, getDescriptorForCommand } from "./command-meta.js";
2
+ import { createShellUi, padDisplay, wrapText } from "./ui.js";
3
3
  import { COMPACT_GLOBAL_OPTION_FLAGS, resolveGlobalFlags } from "./global-flags.js";
4
4
  //#region src/shell/help.ts
5
5
  function renderHelp(command, runtime) {
@@ -10,28 +10,39 @@ function renderHelp(command, runtime) {
10
10
  const visibleCommands = command.commands.filter((candidate) => candidate.name() !== "help" && !candidate.hidden);
11
11
  const visibleOptions = command.options.filter((candidate) => !candidate.hidden);
12
12
  if (visibleCommands.length > 0) lines.push(...renderCommandRows(rail, ui, visibleCommands));
13
- if (descriptor.longDescription) {
14
- lines.push(`${rail}`);
15
- const wrapped = wrapText(descriptor.longDescription, Math.max(ui.width - 3, 40));
16
- for (const line of wrapped) lines.push(`${rail} ${line}`);
17
- }
18
- if (visibleOptions.length > 0) {
19
- if (visibleCommands.length > 0) lines.push(`${rail}`);
20
- if (visibleCommands.length > 0 && visibleOptions.every((option) => COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags))) lines.push(`${rail} Global options:`);
21
- lines.push(...renderOptionRows(rail, ui, visibleOptions));
22
- }
23
- if (descriptor.examples && descriptor.examples.length > 0) {
24
- lines.push(`${rail}`);
25
- lines.push(`${rail} Examples:`);
26
- for (const example of descriptor.examples) lines.push(`${rail} $ ${example}`);
27
- }
28
- if (descriptor.docsPath) {
29
- lines.push(`${rail}`);
30
- lines.push(`${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(descriptor.docsPath)}`);
31
- }
13
+ lines.push(...renderLongDescription(rail, ui, descriptor.longDescription));
14
+ lines.push(...renderVisibleOptions(rail, ui, visibleCommands, visibleOptions));
15
+ lines.push(...renderExamples(rail, runtime, descriptor.examples));
16
+ lines.push(...renderDocsPath(rail, ui, descriptor.docsPath));
32
17
  lines.push("");
33
18
  return `${lines.join("\n")}`;
34
19
  }
20
+ function renderLongDescription(rail, ui, longDescription) {
21
+ if (!longDescription) return [];
22
+ return [`${rail}`, ...wrapText(longDescription, Math.max(ui.width - 3, 40)).map((line) => `${rail} ${line}`)];
23
+ }
24
+ function renderVisibleOptions(rail, ui, visibleCommands, visibleOptions) {
25
+ if (visibleOptions.length === 0) return [];
26
+ const lines = visibleCommands.length > 0 ? [`${rail}`] : [];
27
+ if (shouldLabelGlobalOptions(visibleCommands, visibleOptions)) lines.push(`${rail} Global options:`);
28
+ return [...lines, ...renderOptionRows(rail, ui, visibleOptions)];
29
+ }
30
+ function shouldLabelGlobalOptions(visibleCommands, visibleOptions) {
31
+ return visibleCommands.length > 0 && visibleOptions.every((option) => COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags));
32
+ }
33
+ function renderExamples(rail, runtime, examples) {
34
+ const resolvedExamples = typeof examples === "function" ? examples(runtime) : examples;
35
+ if (!resolvedExamples || resolvedExamples.length === 0) return [];
36
+ return [
37
+ `${rail}`,
38
+ `${rail} Examples:`,
39
+ ...resolvedExamples.map((example) => `${rail} $ ${example}`)
40
+ ];
41
+ }
42
+ function renderDocsPath(rail, ui, docsPath) {
43
+ if (!docsPath) return [];
44
+ return [`${rail}`, `${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(docsPath)}`];
45
+ }
35
46
  function renderCommandRows(rail, ui, commands) {
36
47
  return renderAlignedRows(rail, ui, commands.map((command) => {
37
48
  const descriptor = getDescriptorForCommand(command);
@@ -1,3 +1,4 @@
1
+ import { formatCommandArgument } from "./command-arguments.js";
1
2
  import { renderNextSteps, renderSummaryLine } from "./ui.js";
2
3
  //#region src/shell/output.ts
3
4
  function writeJsonSuccess(output, success) {
@@ -23,6 +24,76 @@ function cliErrorToJson(error) {
23
24
  docsUrl: error.docsUrl
24
25
  };
25
26
  }
27
+ function formatUnexpectedError(error, trace, feedbackCommand) {
28
+ const feedbackLine = feedbackCommand ? `Tell us what happened: ${feedbackCommand}` : null;
29
+ const debug = error instanceof Error ? error.stack ?? error.message : String(error);
30
+ if (trace) return [
31
+ debug,
32
+ ...feedbackLine ? [feedbackLine] : [],
33
+ ""
34
+ ].join("\n");
35
+ return [
36
+ `Unexpected CLI error: ${error instanceof Error && error.message ? error.message : String(error)}`,
37
+ "More: Re-run with --trace for deeper diagnostics",
38
+ ...feedbackLine ? [feedbackLine] : [],
39
+ ""
40
+ ].join("\n");
41
+ }
42
+ /** Command groups whose second argv token names the subcommand. */
43
+ const COMMAND_GROUPS = new Set([
44
+ "agent",
45
+ "auth",
46
+ "project",
47
+ "git",
48
+ "branch",
49
+ "build",
50
+ "database",
51
+ "app"
52
+ ]);
53
+ /** Best-effort `command` label for a crash that predates envelope assembly. */
54
+ function unexpectedErrorCommandLabel(argv) {
55
+ const [group, action] = argv.filter((arg) => !arg.startsWith("-"));
56
+ if (!group) return "unknown";
57
+ return action && COMMAND_GROUPS.has(group) ? `${group}.${action}` : group;
58
+ }
59
+ /**
60
+ * Pre-filled feedback command for a crash, so the report arrives carrying the
61
+ * failing command and the error's first line.
62
+ */
63
+ function unexpectedErrorFeedbackCommand(argv, error) {
64
+ return `prisma-cli feedback ${formatCommandArgument(`${unexpectedErrorCommandLabel(argv).replace(".", " ")} crashed: ${(error instanceof Error && error.message ? error.message : String(error)).split("\n")[0]}`.slice(0, 200))}`;
65
+ }
66
+ /**
67
+ * A crash must not break the `--json` contract: agents still get a structured
68
+ * envelope, with a recover action pointing at the feedback command.
69
+ */
70
+ function writeJsonUnexpectedError(output, argv, error) {
71
+ const feedbackCommand = unexpectedErrorFeedbackCommand(argv, error);
72
+ output.stdout.write(`${JSON.stringify({
73
+ ok: false,
74
+ command: unexpectedErrorCommandLabel(argv),
75
+ error: {
76
+ code: "UNEXPECTED_ERROR",
77
+ domain: "cli",
78
+ severity: "error",
79
+ summary: "Unexpected CLI error",
80
+ why: error instanceof Error && error.message ? error.message : String(error),
81
+ fix: "Re-run with --trace for deeper diagnostics, and report the crash.",
82
+ where: null,
83
+ meta: {},
84
+ docsUrl: null
85
+ },
86
+ warnings: [],
87
+ nextSteps: [feedbackCommand],
88
+ nextActions: [{
89
+ kind: "run-command",
90
+ journey: "recover",
91
+ label: "Report this crash to the Prisma team",
92
+ command: feedbackCommand,
93
+ reason: "This looks like a CLI bug; the pre-filled report carries the failing command and error."
94
+ }]
95
+ }, null, 2)}\n`);
96
+ }
26
97
  function writeJsonError(output, command, error) {
27
98
  output.stdout.write(`${JSON.stringify({
28
99
  ok: false,
@@ -70,4 +141,4 @@ function writeHumanError(output, ui, error, options) {
70
141
  writeHumanLines(output, lines);
71
142
  }
72
143
  //#endregion
73
- export { cliErrorToJson, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess };
144
+ export { cliErrorToJson, formatUnexpectedError, unexpectedErrorFeedbackCommand, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess, writeJsonUnexpectedError };
@@ -1,6 +1,7 @@
1
- import { usageError } from "./errors.js";
1
+ import { isUsageError, usageError } from "./errors.js";
2
2
  import { confirm, isCancel, select, text } from "@clack/prompts";
3
3
  //#region src/shell/prompt.ts
4
+ const PROMPT_CANCELED_SUMMARY = "Interactive prompt canceled";
4
5
  async function selectPrompt(options) {
5
6
  const promptOptions = options.choices.map((choice) => ({
6
7
  label: choice.label,
@@ -9,33 +10,39 @@ async function selectPrompt(options) {
9
10
  const response = await select({
10
11
  input: options.input,
11
12
  output: options.output,
13
+ signal: options.signal,
12
14
  message: options.message,
13
15
  options: promptOptions
14
16
  });
15
- if (isCancel(response)) throw usageError("Interactive prompt canceled", "The command was canceled before a selection was made.", "Re-run the command and choose an option to continue.");
17
+ if (isCancel(response)) throw usageError(PROMPT_CANCELED_SUMMARY, "The command was canceled before a selection was made.", "Re-run the command and choose an option to continue.");
16
18
  return response;
17
19
  }
18
20
  async function textPrompt(options) {
19
21
  const response = await text({
20
22
  input: options.input,
21
23
  output: options.output,
24
+ signal: options.signal,
22
25
  message: options.message,
23
26
  placeholder: options.placeholder,
24
27
  validate: options.validate
25
28
  });
26
- if (isCancel(response)) throw usageError("Interactive prompt canceled", "The command was canceled before a value was entered.", "Re-run the command and provide a value to continue.");
29
+ if (isCancel(response)) throw usageError(PROMPT_CANCELED_SUMMARY, "The command was canceled before a value was entered.", "Re-run the command and provide a value to continue.");
27
30
  return response;
28
31
  }
29
32
  async function confirmPrompt(options) {
30
33
  const response = await confirm({
31
34
  input: options.input,
32
35
  output: options.output,
36
+ signal: options.signal,
33
37
  message: options.message,
34
38
  initialValue: options.initialValue ?? false
35
39
  });
36
- if (isCancel(response)) throw usageError("Interactive prompt canceled", "The command was canceled before a confirmation was made.", "Re-run the command and choose an option to continue.");
40
+ if (isCancel(response)) throw usageError(PROMPT_CANCELED_SUMMARY, "The command was canceled before a confirmation was made.", "Re-run the command and choose an option to continue.");
37
41
  return response;
38
42
  }
43
+ function isPromptCancelError(error) {
44
+ return isUsageError(error, PROMPT_CANCELED_SUMMARY);
45
+ }
39
46
  function disposePromptState(_input) {}
40
47
  //#endregion
41
- export { confirmPrompt, disposePromptState, selectPrompt, textPrompt };
48
+ export { confirmPrompt, disposePromptState, isPromptCancelError, selectPrompt, textPrompt };
@@ -3,6 +3,7 @@ import { LocalStateStore } from "../adapters/local-state.js";
3
3
  import { MockApi } from "../adapters/mock-api.js";
4
4
  import { renderHelp } from "./help.js";
5
5
  import path from "node:path";
6
+ import { findComputeConfigDir } from "@prisma/compute-sdk/config";
6
7
  //#region src/shell/runtime.ts
7
8
  const DEFAULT_STATE_DIR_NAME = path.join(".prisma", "cli");
8
9
  function configureRuntimeCommand(command, runtime) {
@@ -20,7 +21,7 @@ function configureRuntimeCommand(command, runtime) {
20
21
  }
21
22
  async function createCommandContext(runtime, flags) {
22
23
  const fixturePath = runtime.fixturePath ?? runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
23
- const stateDir = resolveStateDir(runtime);
24
+ const stateDir = await resolveStateDir(runtime);
24
25
  let loadedApi;
25
26
  if (fixturePath) loadedApi = await MockApi.load(fixturePath, runtime.signal);
26
27
  return {
@@ -38,8 +39,11 @@ async function createCommandContext(runtime, flags) {
38
39
  ui: createShellUi(runtime, flags)
39
40
  };
40
41
  }
41
- function resolveStateDir(runtime) {
42
- return runtime.stateDir ?? runtime.env.PRISMA_CLI_STATE_DIR ?? path.join(runtime.cwd, DEFAULT_STATE_DIR_NAME);
42
+ async function resolveStateDir(runtime) {
43
+ const explicitStateDir = runtime.stateDir ?? runtime.env.PRISMA_CLI_STATE_DIR;
44
+ if (explicitStateDir) return explicitStateDir;
45
+ const projectDir = await findComputeConfigDir(runtime.cwd, runtime.signal);
46
+ return path.join(projectDir ?? runtime.cwd, DEFAULT_STATE_DIR_NAME);
43
47
  }
44
48
  function canPrompt(context) {
45
49
  if (context.flags.json) return false;
@@ -48,4 +52,4 @@ function canPrompt(context) {
48
52
  return Boolean(context.runtime.stdin.isTTY && context.runtime.stderr.isTTY);
49
53
  }
50
54
  //#endregion
51
- export { canPrompt, configureRuntimeCommand, createCommandContext };
55
+ export { canPrompt, configureRuntimeCommand, createCommandContext, resolveStateDir };
package/dist/shell/ui.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { createColors } from "colorette";
1
2
  import stringWidth from "string-width";
2
3
  import stripAnsi from "strip-ansi";
3
4
  import wrapAnsi from "wrap-ansi";
4
- import { createColors } from "colorette";
5
5
  //#region src/shell/ui.ts
6
+ const URL_CREDENTIALS_PATTERN = /:\/\/[^:@/\s]+:[^@/\s]+@/g;
6
7
  const DEFAULT_WIDTH = 80;
7
8
  function createShellUi(runtime, flags) {
8
9
  const isTTY = Boolean(runtime.stderr.isTTY);
@@ -48,6 +49,20 @@ function renderNextSteps(steps) {
48
49
  ...steps.map((step) => `- ${step}`)
49
50
  ];
50
51
  }
52
+ function renderVerboseBlock(ui, rows, options = {}) {
53
+ if (!ui.verbose || rows.length === 0) return [];
54
+ const title = options.title ?? "Details";
55
+ const keyWidth = Math.max(...rows.map((row) => stringWidth(`${row.key}:`)));
56
+ const rail = ui.dim("│");
57
+ return [
58
+ "",
59
+ `${ui.dim(title)}:`,
60
+ ...rows.map((row) => `${rail} ${ui.accent(padDisplay(`${row.key}:`, keyWidth))} ${formatVerboseValue(ui, row)}`)
61
+ ];
62
+ }
63
+ function formatColumns(columns, widths) {
64
+ return columns.map((value, index) => padDisplay(value, widths[index])).join(" ").trimEnd();
65
+ }
51
66
  function wrapText(text, width, indent = "") {
52
67
  return wrapAnsi(text, width, {
53
68
  hard: false,
@@ -59,7 +74,23 @@ function padDisplay(text, width) {
59
74
  return `${text}${" ".repeat(padding)}`;
60
75
  }
61
76
  function maskValue(value) {
62
- return value.replace(/([A-Za-z0-9._%+-]{1,})(?=@)/g, "****").replace(/:\/\/[^:@/\s]+:[^@/\s]+@/g, "://****:****@");
77
+ return maskEmailLocalParts(value).replace(URL_CREDENTIALS_PATTERN, "://****:****@");
78
+ }
79
+ function maskEmailLocalParts(value) {
80
+ let masked = "";
81
+ let segmentStart = 0;
82
+ for (let index = 0; index < value.length; index += 1) {
83
+ if (value[index] !== "@") continue;
84
+ let localStart = index;
85
+ while (localStart > segmentStart && isEmailLocalPartChar(value[localStart - 1])) localStart -= 1;
86
+ if (localStart === index) continue;
87
+ masked += `${value.slice(segmentStart, localStart)}****@`;
88
+ segmentStart = index + 1;
89
+ }
90
+ return masked + value.slice(segmentStart);
91
+ }
92
+ function isEmailLocalPartChar(char) {
93
+ return char >= "A" && char <= "Z" || char >= "a" && char <= "z" || char >= "0" && char <= "9" || char === "!" || char === "#" || char === "$" || char === "." || char === "&" || char === "'" || char === "*" || char === "%" || char === "+" || char === "-" || char === "/" || char === "=" || char === "?" || char === "^" || char === "_" || char === "`" || char === "{" || char === "|" || char === "}" || char === "~";
63
94
  }
64
95
  function resolveColorEnabled(runtime, flags, isTTY) {
65
96
  if (flags.color === true) return true;
@@ -73,5 +104,13 @@ function formatHeaderValue(ui, row) {
73
104
  if (row.tone === "link") return ui.link(value);
74
105
  return value;
75
106
  }
107
+ function formatVerboseValue(ui, row) {
108
+ const value = row.sensitive ? maskValue(row.value) : row.value;
109
+ if (row.tone === "dim") return ui.dim(value);
110
+ if (row.tone === "success") return ui.success(value);
111
+ if (row.tone === "warning") return ui.warning(value);
112
+ if (row.tone === "link") return ui.link(value);
113
+ return value;
114
+ }
76
115
  //#endregion
77
- export { createShellUi, maskValue, padDisplay, renderCommandHeader, renderNextSteps, renderSummaryLine, wrapText };
116
+ export { createShellUi, formatColumns, maskValue, padDisplay, renderCommandHeader, renderNextSteps, renderSummaryLine, renderVerboseBlock, wrapText };
@@ -1,9 +1,9 @@
1
1
  import { getCliName, getCliVersion } from "../lib/version.js";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
2
  import path from "node:path";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
+ import { spawn } from "node:child_process";
4
5
  import { randomUUID } from "node:crypto";
5
6
  import os from "node:os";
6
- import { spawn } from "node:child_process";
7
7
  //#region src/shell/update-check.ts
8
8
  const UPDATE_CHECK_FILE_NAME = "update-check.json";
9
9
  const FALLBACK_INSTALL_DOCS_URL = "https://www.prisma.io/docs/orm/tools/prisma-cli";
@@ -1,4 +1,4 @@
1
- import { usageError } from "../shell/errors.js";
1
+ import { authRequiredError, usageError, workspaceAmbiguousError, workspaceNotAuthenticatedError } from "../shell/errors.js";
2
2
  //#region src/use-cases/auth.ts
3
3
  function createAuthUseCases(dependencies) {
4
4
  return {
@@ -15,6 +15,70 @@ function createAuthUseCases(dependencies) {
15
15
  await dependencies.sessionGateway.clearAuthSession();
16
16
  return resolveCurrentAuthState(dependencies);
17
17
  },
18
+ listWorkspaces: async () => {
19
+ const session = await dependencies.sessionGateway.readAuthSession();
20
+ if (!session) return {
21
+ authSource: "none",
22
+ activeWorkspace: null,
23
+ workspaces: []
24
+ };
25
+ const workspaces = dependencies.identityGateway.listUserWorkspaces(session.userId);
26
+ return {
27
+ authSource: "oauth",
28
+ activeWorkspace: workspaces.find((workspace) => workspace.id === session.workspaceId) ?? null,
29
+ workspaces: workspaces.map((workspace) => ({
30
+ ...workspace,
31
+ credentialWorkspaceId: workspace.id,
32
+ active: workspace.id === session.workspaceId,
33
+ source: "oauth",
34
+ switchable: true,
35
+ lastSeenAt: null
36
+ }))
37
+ };
38
+ },
39
+ useWorkspace: async (workspaceRef) => {
40
+ const session = await dependencies.sessionGateway.readAuthSession();
41
+ if (!session) throw authRequiredError(["prisma-cli auth login"]);
42
+ const ref = workspaceRef.trim();
43
+ const matches = dependencies.identityGateway.listUserWorkspaces(session.userId).filter((workspace) => workspaceMatchesRef(workspace, ref));
44
+ if (matches.length === 0) throw workspaceNotAuthenticatedError(workspaceRef);
45
+ if (matches.length > 1) throw workspaceAmbiguousError(workspaceRef, matches.map((workspace) => ({
46
+ id: workspace.id,
47
+ name: workspace.name,
48
+ credentialWorkspaceId: workspace.id
49
+ })));
50
+ const selected = matches[0];
51
+ const previousWorkspace = dependencies.identityGateway.getWorkspace(session.workspaceId) ?? null;
52
+ await dependencies.sessionGateway.writeAuthSession({
53
+ ...session,
54
+ workspaceId: selected.id
55
+ });
56
+ return {
57
+ previousWorkspace,
58
+ workspace: selected
59
+ };
60
+ },
61
+ logoutWorkspace: async (workspaceRef) => {
62
+ const session = await dependencies.sessionGateway.readAuthSession();
63
+ if (!session) throw workspaceNotAuthenticatedError(workspaceRef);
64
+ const ref = workspaceRef.trim();
65
+ const matches = dependencies.identityGateway.listUserWorkspaces(session.userId).filter((workspace) => workspaceMatchesRef(workspace, ref));
66
+ if (matches.length === 0) throw workspaceNotAuthenticatedError(workspaceRef);
67
+ if (matches.length > 1) throw workspaceAmbiguousError(workspaceRef, matches.map((workspace) => ({
68
+ id: workspace.id,
69
+ name: workspace.name,
70
+ credentialWorkspaceId: workspace.id
71
+ })));
72
+ const workspace = matches[0];
73
+ const wasActive = workspace.id === session.workspaceId;
74
+ const activeWorkspace = wasActive ? null : dependencies.identityGateway.getWorkspace(session.workspaceId) ?? null;
75
+ if (wasActive) await dependencies.sessionGateway.clearAuthSession();
76
+ return {
77
+ workspace,
78
+ wasActive,
79
+ activeWorkspace
80
+ };
81
+ },
18
82
  listProviders: async () => dependencies.identityGateway.listProviders(),
19
83
  resolveProvider: async (providerId) => {
20
84
  const provider = dependencies.identityGateway.getProvider(providerId);
@@ -39,6 +103,9 @@ function createAuthUseCases(dependencies) {
39
103
  }
40
104
  };
41
105
  }
106
+ function workspaceMatchesRef(workspace, ref) {
107
+ return workspace.id === ref || workspace.name.toLowerCase() === ref.toLowerCase();
108
+ }
42
109
  async function resolveCurrentAuthState(dependencies) {
43
110
  const session = await dependencies.sessionGateway.readAuthSession();
44
111
  if (!session) return {
@@ -1,34 +1,19 @@
1
1
  //#region src/use-cases/branch.ts
2
2
  function createBranchUseCases(dependencies) {
3
- return {
4
- list: async () => {
5
- const [projectId, activeBranch] = await Promise.all([dependencies.projectStateGateway.readRememberedProjectId(), dependencies.branchStateGateway.readActiveBranch()]);
6
- const remoteBranches = await listRemoteBranches(dependencies.branchGateway, projectId);
7
- return {
8
- projectId,
9
- projectName: resolveProjectName(dependencies.projectGateway, projectId),
10
- activeBranch,
11
- branches: buildBranchSummaries(activeBranch, remoteBranches)
12
- };
13
- },
14
- show: async () => {
15
- const [projectId, activeBranch] = await Promise.all([dependencies.projectStateGateway.readRememberedProjectId(), dependencies.branchStateGateway.readActiveBranch()]);
16
- return {
17
- projectId,
18
- projectName: resolveProjectName(dependencies.projectGateway, projectId),
19
- branch: buildBranchDetail(dependencies.branchGateway, projectId, activeBranch)
20
- };
21
- },
22
- use: async (branchName) => {
23
- await dependencies.branchStateGateway.writeActiveBranch(branchName);
24
- const projectId = await dependencies.projectStateGateway.readRememberedProjectId();
25
- return {
26
- projectId,
27
- projectName: resolveProjectName(dependencies.projectGateway, projectId),
28
- branch: buildBranchDetail(dependencies.branchGateway, projectId, branchName)
29
- };
30
- }
31
- };
3
+ return { list: async () => {
4
+ const projectId = await dependencies.projectStateGateway.readRememberedProjectId();
5
+ if (!projectId) return {
6
+ projectId: "",
7
+ projectName: "not resolved",
8
+ branches: []
9
+ };
10
+ const remoteBranches = await listRemoteBranches(dependencies.branchGateway, projectId);
11
+ return {
12
+ projectId,
13
+ projectName: resolveProjectName(dependencies.projectGateway, projectId) ?? "not resolved",
14
+ branches: buildBranchSummaries(remoteBranches)
15
+ };
16
+ } };
32
17
  }
33
18
  function resolveProjectName(projectGateway, projectId) {
34
19
  if (!projectId) return null;
@@ -38,38 +23,13 @@ async function listRemoteBranches(branchGateway, projectId) {
38
23
  if (!projectId) return [];
39
24
  return branchGateway.listBranchesForProject(projectId);
40
25
  }
41
- function buildBranchSummaries(activeBranch, remoteBranches) {
42
- const byName = /* @__PURE__ */ new Map();
43
- for (const branch of remoteBranches) byName.set(branch.name, {
26
+ function buildBranchSummaries(remoteBranches) {
27
+ return sortBranches(remoteBranches.map((branch) => ({
44
28
  id: branch.id,
45
29
  name: branch.name,
46
- kind: branch.kind,
47
- active: activeBranch === branch.name,
48
- remoteState: true
49
- });
50
- if (!byName.has(activeBranch)) byName.set(activeBranch, {
51
- id: activeBranch,
52
- name: activeBranch,
53
- kind: toBranchKind(activeBranch),
54
- active: true,
55
- remoteState: false
56
- });
57
- return sortBranches([...byName.values()]);
58
- }
59
- function buildBranchDetail(branchGateway, projectId, branchName) {
60
- const kind = toBranchKind(branchName);
61
- const remoteBranch = projectId ? branchGateway.getBranchForProject(projectId, branchName) : void 0;
62
- return {
63
- name: branchName,
64
- kind,
65
- active: true,
66
- remoteState: Boolean(remoteBranch),
67
- liveDeployment: remoteBranch && remoteBranch.currentDeploymentId ? toLiveDeployment(branchGateway.getDeployment(remoteBranch.currentDeploymentId)) : null
68
- };
69
- }
70
- function toBranchKind(name) {
71
- if (name === "production") return "production";
72
- return "preview";
30
+ role: branch.role,
31
+ envMap: branch.role
32
+ })));
73
33
  }
74
34
  function sortBranches(branches) {
75
35
  return branches.slice().sort((left, right) => {
@@ -80,16 +40,8 @@ function sortBranches(branches) {
80
40
  });
81
41
  }
82
42
  function branchOrder(branch) {
83
- if (branch.name === "production") return 0;
43
+ if (branch.role === "production") return 0;
84
44
  return 1;
85
45
  }
86
- function toLiveDeployment(deployment) {
87
- if (!deployment) return null;
88
- return {
89
- id: deployment.id,
90
- status: deployment.status,
91
- url: deployment.url
92
- };
93
- }
94
46
  //#endregion
95
47
  export { createBranchUseCases };