@prisma/cli 3.0.0-beta.17 → 3.0.0-beta.19

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 (48) hide show
  1. package/dist/adapters/local-state.js +14 -3
  2. package/dist/adapters/token-storage.js +1 -1
  3. package/dist/cli2.js +6 -2
  4. package/dist/commands/agent/index.js +60 -0
  5. package/dist/commands/app/index.js +5 -3
  6. package/dist/commands/auth/index.js +1 -1
  7. package/dist/commands/build/index.js +29 -0
  8. package/dist/commands/git/index.js +1 -1
  9. package/dist/commands/project/index.js +1 -1
  10. package/dist/controllers/agent.js +228 -0
  11. package/dist/controllers/app.js +157 -73
  12. package/dist/controllers/auth.js +38 -3
  13. package/dist/controllers/build.js +88 -0
  14. package/dist/controllers/project.js +2 -2
  15. package/dist/controllers/select-prompt-port.js +1 -0
  16. package/dist/lib/agent/cli-command.js +17 -0
  17. package/dist/lib/agent/constants.js +12 -0
  18. package/dist/lib/agent/package-manager.js +99 -0
  19. package/dist/lib/agent/setup-status.js +83 -0
  20. package/dist/lib/app/app-provider.js +3 -3
  21. package/dist/lib/app/branch-database-deploy.js +3 -2
  22. package/dist/lib/app/branch-database.js +2 -1
  23. package/dist/lib/app/build-settings.js +1 -1
  24. package/dist/lib/app/build.js +2 -2
  25. package/dist/lib/app/bun-project.js +1 -1
  26. package/dist/lib/app/compute-config.js +27 -29
  27. package/dist/lib/app/deploy-plan.js +1 -0
  28. package/dist/lib/app/env-file.js +1 -1
  29. package/dist/lib/app/local-dev.js +1 -1
  30. package/dist/lib/app/production-deploy-gate.js +2 -1
  31. package/dist/lib/auth/login.js +1 -1
  32. package/dist/lib/git/local-branch.js +1 -1
  33. package/dist/lib/project/interactive-setup.js +1 -0
  34. package/dist/lib/project/local-pin.js +1 -1
  35. package/dist/lib/project/resolution.js +2 -2
  36. package/dist/lib/project/setup.js +1 -1
  37. package/dist/presenters/agent.js +74 -0
  38. package/dist/presenters/auth.js +1 -1
  39. package/dist/presenters/project.js +1 -1
  40. package/dist/shell/cli-command.js +12 -0
  41. package/dist/shell/command-arguments.js +7 -1
  42. package/dist/shell/command-meta.js +88 -0
  43. package/dist/shell/command-runner.js +3 -3
  44. package/dist/shell/help.js +6 -5
  45. package/dist/shell/prompt.js +7 -3
  46. package/dist/shell/runtime.js +2 -2
  47. package/dist/shell/update-check.js +2 -2
  48. package/package.json +4 -3
@@ -0,0 +1,99 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/lib/agent/package-manager.ts
4
+ const LOCKFILE_PACKAGE_MANAGERS = [
5
+ {
6
+ packageManager: "bun",
7
+ fileNames: ["bun.lock", "bun.lockb"]
8
+ },
9
+ {
10
+ packageManager: "pnpm",
11
+ fileNames: ["pnpm-lock.yaml", "pnpm-workspace.yaml"]
12
+ },
13
+ {
14
+ packageManager: "yarn",
15
+ fileNames: ["yarn.lock"]
16
+ },
17
+ {
18
+ packageManager: "npm",
19
+ fileNames: ["package-lock.json", "npm-shrinkwrap.json"]
20
+ }
21
+ ];
22
+ async function resolveSkillsPackageRunner(options) {
23
+ return resolvePackageRunner(options);
24
+ }
25
+ async function resolvePackageRunner(options) {
26
+ options.signal.throwIfAborted();
27
+ const packageManager = detectPackageManagerSync(options.cwd, options.signal) ?? "npm";
28
+ options.signal.throwIfAborted();
29
+ return packageRunnerForPackageManager(packageManager);
30
+ }
31
+ function resolvePackageRunnerSync(cwd) {
32
+ return packageRunnerForPackageManager(detectPackageManagerSync(cwd) ?? "npm");
33
+ }
34
+ function detectPackageManagerSync(cwd, signal) {
35
+ let directory = path.resolve(cwd);
36
+ while (true) {
37
+ signal?.throwIfAborted();
38
+ const packageJsonManager = readPackageJsonPackageManager(directory);
39
+ if (packageJsonManager) return packageJsonManager;
40
+ const lockfileManager = readLockfilePackageManager(directory, signal);
41
+ if (lockfileManager) return lockfileManager;
42
+ const parent = path.dirname(directory);
43
+ if (parent === directory) return null;
44
+ directory = parent;
45
+ }
46
+ }
47
+ function readPackageJsonPackageManager(directory) {
48
+ const packageJsonPath = path.join(directory, "package.json");
49
+ let content;
50
+ try {
51
+ content = readFileSync(packageJsonPath, "utf8");
52
+ } catch (error) {
53
+ if (isMissingFileError(error)) return null;
54
+ throw error;
55
+ }
56
+ try {
57
+ return parsePackageManager(JSON.parse(content).packageManager);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ function readLockfilePackageManager(directory, signal) {
63
+ for (const candidate of LOCKFILE_PACKAGE_MANAGERS) for (const fileName of candidate.fileNames) {
64
+ signal?.throwIfAborted();
65
+ if (fileExists(path.join(directory, fileName))) return candidate.packageManager;
66
+ }
67
+ return null;
68
+ }
69
+ function fileExists(filePath) {
70
+ try {
71
+ return statSync(filePath).isFile();
72
+ } catch (error) {
73
+ if (isMissingFileError(error)) return false;
74
+ throw error;
75
+ }
76
+ }
77
+ function parsePackageManager(value) {
78
+ if (typeof value !== "string") return null;
79
+ const normalized = value.trim().toLowerCase();
80
+ if (normalized === "bun" || normalized.startsWith("bun@")) return "bun";
81
+ if (normalized === "pnpm" || normalized.startsWith("pnpm@")) return "pnpm";
82
+ if (normalized === "yarn" || normalized.startsWith("yarn@")) return "yarn";
83
+ if (normalized === "npm" || normalized.startsWith("npm@")) return "npm";
84
+ return null;
85
+ }
86
+ function packageRunnerForPackageManager(packageManager) {
87
+ switch (packageManager) {
88
+ case "bun": return ["bunx"];
89
+ case "pnpm": return ["pnpm", "dlx"];
90
+ case "yarn": return ["yarn", "dlx"];
91
+ case "npm": return ["npx", "-y"];
92
+ }
93
+ }
94
+ function isMissingFileError(error) {
95
+ const code = error.code;
96
+ return code === "ENOENT" || code === "ENOTDIR";
97
+ }
98
+ //#endregion
99
+ export { resolvePackageRunner, resolvePackageRunnerSync, resolveSkillsPackageRunner };
@@ -0,0 +1,83 @@
1
+ import { PRISMA_SKILLS_LOCK_FILENAME } from "./constants.js";
2
+ import path from "node:path";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { findComputeConfigDir } from "@prisma/compute-sdk/config";
5
+ //#region src/lib/agent/setup-status.ts
6
+ async function readPrismaAgentSetupStatus(options) {
7
+ const setupCwd = await resolvePrismaAgentSetupCwd(options);
8
+ const skillsLockPath = path.join(setupCwd, PRISMA_SKILLS_LOCK_FILENAME);
9
+ const [skillsInstalled, promptDismissedAt] = await Promise.all([hasPrismaSkillsLock(skillsLockPath, options.signal, options.requiredSkill), options.stateStore?.readAgentSetupPromptDismissedAt() ?? null]);
10
+ return {
11
+ skillsLockPath: path.basename(skillsLockPath),
12
+ skillsInstalled,
13
+ promptDismissedAt
14
+ };
15
+ }
16
+ async function resolvePrismaAgentSetupCwd(options) {
17
+ options.signal.throwIfAborted();
18
+ const configDir = await findComputeConfigDir(options.cwd, options.signal);
19
+ options.signal.throwIfAborted();
20
+ return configDir ?? options.cwd;
21
+ }
22
+ function isPrismaAgentSetupComplete(status) {
23
+ return status.skillsInstalled;
24
+ }
25
+ function shouldOfferPrismaAgentSetup(status) {
26
+ return !isPrismaAgentSetupComplete(status) && !status.promptDismissedAt;
27
+ }
28
+ async function isLikelyProjectDirectory(options) {
29
+ return (await Promise.all([
30
+ "package.json",
31
+ "prisma.compute.ts",
32
+ "prisma.config.ts",
33
+ ".git"
34
+ ].map((fileName) => pathExists(path.join(options.cwd, fileName), options.signal)))).some(Boolean);
35
+ }
36
+ async function hasPrismaSkillsLock(filePath, signal, requiredSkill) {
37
+ try {
38
+ const raw = await readFile(filePath, {
39
+ encoding: "utf8",
40
+ signal
41
+ });
42
+ return hasPrismaSkillsLockEntry(JSON.parse(raw), requiredSkill);
43
+ } catch (error) {
44
+ if (isNotFoundError(error) || error instanceof SyntaxError) return false;
45
+ throw error;
46
+ }
47
+ }
48
+ function hasPrismaSkillsLockEntry(value, requiredSkill) {
49
+ if (!isRecord(value)) return false;
50
+ if (requiredSkill) return skillLockEntryUsesPrismaSource(readSkillLockEntries(value)[requiredSkill]);
51
+ if (readLegacySources(value).includes("prisma/skills")) return true;
52
+ return Object.values(readSkillLockEntries(value)).some(skillLockEntryUsesPrismaSource);
53
+ }
54
+ function readLegacySources(value) {
55
+ return Array.isArray(value.sources) ? value.sources.filter((source) => typeof source === "string") : [];
56
+ }
57
+ function readSkillLockEntries(value) {
58
+ return isRecord(value.skills) ? value.skills : {};
59
+ }
60
+ function skillLockEntryUsesPrismaSource(value) {
61
+ return isRecord(value) && value.source === "prisma/skills";
62
+ }
63
+ function isRecord(value) {
64
+ return typeof value === "object" && value !== null;
65
+ }
66
+ async function pathExists(filePath, signal) {
67
+ signal.throwIfAborted();
68
+ try {
69
+ await stat(filePath);
70
+ signal.throwIfAborted();
71
+ return true;
72
+ } catch (error) {
73
+ if (signal.aborted) throw error;
74
+ if (isNotFoundError(error)) return false;
75
+ throw error;
76
+ }
77
+ }
78
+ function isNotFoundError(error) {
79
+ const code = error.code;
80
+ return code === "ENOENT" || code === "ENOTDIR";
81
+ }
82
+ //#endregion
83
+ export { isLikelyProjectDirectory, readPrismaAgentSetupStatus, resolvePrismaAgentSetupCwd, shouldOfferPrismaAgentSetup };
@@ -404,9 +404,9 @@ async function listComputeServices(client, options) {
404
404
  liveUrl: toAbsoluteUrl(service.appEndpointDomain ?? null)
405
405
  }));
406
406
  }
407
- async function listComputeServiceDomains(client, computeServiceId, signal) {
407
+ async function listComputeServiceDomains(client, appId, signal) {
408
408
  const result = await client.GET("/v1/apps/{appId}/domains", {
409
- params: { path: { appId: computeServiceId } },
409
+ params: { path: { appId } },
410
410
  signal
411
411
  });
412
412
  if (result.error || !result.data) throw domainApiCallError("Failed to list custom domains", result.response, result.error);
@@ -418,7 +418,7 @@ function normalizeDomainRecord(domain) {
418
418
  type: domain.type,
419
419
  url: domain.url,
420
420
  hostname: domain.hostname,
421
- computeServiceId: domain.computeServiceId,
421
+ appId: domain.appId,
422
422
  status: domain.status,
423
423
  foundryStatus: domain.foundryStatus,
424
424
  failureReason: domain.failureReason,
@@ -1,8 +1,8 @@
1
+ import { formatCommandArgument } from "../../shell/command-arguments.js";
1
2
  import { CliError, usageError } from "../../shell/errors.js";
2
- import { confirmPrompt } from "../../shell/prompt.js";
3
3
  import { renderSummaryLine } from "../../shell/ui.js";
4
4
  import { canPrompt } from "../../shell/runtime.js";
5
- import { formatCommandArgument } from "../../shell/command-arguments.js";
5
+ import { confirmPrompt } from "../../shell/prompt.js";
6
6
  import "../project/setup.js";
7
7
  import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal } from "./branch-database.js";
8
8
  import path from "node:path";
@@ -51,6 +51,7 @@ async function maybeSetupBranchDatabase(context, provider, projectId, branch, op
51
51
  if (!await confirmPrompt({
52
52
  input: context.runtime.stdin,
53
53
  output: context.output.stderr,
54
+ signal: context.runtime.signal,
54
55
  message: databasePromptMessage(branch),
55
56
  initialValue: false
56
57
  })) return emptyBranchDatabaseSetupOutcome();
@@ -1,8 +1,9 @@
1
- import { access, readFile, readdir, stat } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { access, readFile, readdir, stat } from "node:fs/promises";
3
3
  //#region src/lib/app/branch-database.ts
4
4
  const SKIPPED_DIRECTORIES = new Set([
5
5
  ".git",
6
+ ".agents",
6
7
  ".next",
7
8
  ".nuxt",
8
9
  ".output",
@@ -1,5 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
3
  import { resolveBuildSettings, resolveConfiguredBuildSettings } from "@prisma/compute-sdk";
4
4
  //#region src/lib/app/build-settings.ts
5
5
  /** Legacy build-settings file: no longer read or written, only detected for migration. */
@@ -1,7 +1,7 @@
1
1
  import "./build-settings.js";
2
- import { FRAMEWORKS } from "@prisma/compute-sdk/config";
3
- import "node:fs/promises";
4
2
  import "node:path";
3
+ import "node:fs/promises";
4
+ import { FRAMEWORKS } from "@prisma/compute-sdk/config";
5
5
  import { normalizeArtifactSymlinks, resolveBuildStrategy } from "@prisma/compute-sdk";
6
6
  //#region src/lib/app/build.ts
7
7
  const RESOLVED_APP_BUILD_TYPES = [...frameworkBuildTypesByLastOccurrence()];
@@ -1,5 +1,5 @@
1
- import { access, readFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { access, readFile } from "node:fs/promises";
3
3
  //#region src/lib/app/bun-project.ts
4
4
  async function readBunPackageJson(appPath, signal) {
5
5
  const packageJsonPath = path.join(appPath, "package.json");
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
- import { COMPUTE_CONFIG_FILENAME, COMPUTE_CONFIG_FILENAME as COMPUTE_CONFIG_FILENAME$1, ComputeConfigTargetRequiredError, computeTargetAppDir, frameworkByKey, inferComputeTargetFromCwd, loadComputeConfig, selectComputeDeployTarget } from "@prisma/compute-sdk/config";
3
2
  import path from "node:path";
3
+ import { COMPUTE_CONFIG_FILENAME, COMPUTE_CONFIG_FILENAME as COMPUTE_CONFIG_FILENAME$1, ComputeConfigTargetRequiredError, computeTargetAppDir, frameworkByKey, inferComputeTargetFromCwd, loadComputeConfig, selectComputeDeployTarget } from "@prisma/compute-sdk/config";
4
4
  import { matchError } from "better-result";
5
5
  //#region src/lib/app/compute-config.ts
6
6
  /**
@@ -18,47 +18,45 @@ function computeFrameworkToBuildType(framework) {
18
18
  function mergeComputeDeployInputs(options) {
19
19
  const { cli, target, configFilename } = options;
20
20
  const configAnnotation = `set by ${configFilename}`;
21
- const framework = cli.framework ? {
22
- value: cli.framework,
23
- annotation: "set by --framework"
24
- } : target?.framework ? {
25
- value: target.framework,
26
- annotation: configAnnotation
27
- } : void 0;
28
- const entrypoint = cli.entrypoint ? {
29
- value: cli.entrypoint,
30
- annotation: "set by --entry"
31
- } : target?.entry ? {
32
- value: target.entry,
33
- annotation: configAnnotation
34
- } : void 0;
35
- const httpPort = cli.httpPort ? {
36
- value: cli.httpPort,
37
- annotation: "set by --http-port"
38
- } : target?.httpPort ? {
39
- value: String(target.httpPort),
40
- annotation: configAnnotation
41
- } : void 0;
21
+ const framework = mergeStringInput(cli.framework, "set by --framework", target?.framework ?? void 0, configAnnotation);
22
+ const entrypoint = mergeStringInput(cli.entrypoint, "set by --entry", target?.entry ?? void 0, configAnnotation);
23
+ const httpPort = mergeStringInput(cli.httpPort, "set by --http-port", target?.httpPort ? String(target.httpPort) : void 0, configAnnotation);
24
+ const region = mergeStringInput(cli.region, "set by --region", target?.region ?? void 0, configAnnotation);
42
25
  const cliEnvInputs = cli.envInputs && cli.envInputs.length > 0 ? cli.envInputs : void 0;
43
26
  const configEnvInputs = target && target.envInputs.length > 0 ? target.envInputs : void 0;
44
27
  const envInputs = cliEnvInputs ?? configEnvInputs;
45
- const configAppName = target?.name ? {
46
- value: target.name,
47
- annotation: configAnnotation
48
- } : target?.key ? {
49
- value: target.key,
50
- annotation: configAnnotation
51
- } : void 0;
28
+ const configAppName = readConfigAppName(target, configAnnotation);
52
29
  return {
53
30
  framework,
54
31
  entrypoint,
55
32
  httpPort,
33
+ region,
56
34
  envInputs,
57
35
  envInputsFromConfig: !cliEnvInputs && configEnvInputs !== void 0,
58
36
  configAppName,
59
37
  appRoot: target?.root ?? void 0
60
38
  };
61
39
  }
40
+ function mergeStringInput(cliValue, cliAnnotation, configValue, configAnnotation) {
41
+ if (cliValue !== void 0) return {
42
+ value: cliValue,
43
+ annotation: cliAnnotation
44
+ };
45
+ if (configValue) return {
46
+ value: configValue,
47
+ annotation: configAnnotation
48
+ };
49
+ }
50
+ function readConfigAppName(target, configAnnotation) {
51
+ if (target?.name) return {
52
+ value: target.name,
53
+ annotation: configAnnotation
54
+ };
55
+ if (target?.key) return {
56
+ value: target.key,
57
+ annotation: configAnnotation
58
+ };
59
+ }
62
60
  /**
63
61
  * Merges CLI inputs for the local `app build` and `app run` commands with a
64
62
  * selected config target. Explicit flags win; `--build-type auto` is the
@@ -30,6 +30,7 @@ function perAppInputsForDeployAll(inputs) {
30
30
  ["--framework", inputs.framework],
31
31
  ["--entry", inputs.entrypoint],
32
32
  ["--http-port", inputs.httpPort],
33
+ ["--region", inputs.region],
33
34
  ["--env", inputs.envAssignments?.length ? inputs.envAssignments : void 0],
34
35
  [inputs.appIdEnvVar.name, inputs.appIdEnvVar.value]
35
36
  ].filter(([, value]) => value !== void 0).map(([flag]) => flag);
@@ -1,7 +1,7 @@
1
1
  import { usageError } from "../../shell/errors.js";
2
2
  import { validateKey } from "./env-config.js";
3
- import { readFile } from "node:fs/promises";
4
3
  import path from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
5
  import { parse } from "dotenv";
6
6
  //#region src/lib/app/env-file.ts
7
7
  const ASSIGNMENT_KEY_PATTERN = /^\s*(?:export\s+)?([^#=\s]+)\s*=/;
@@ -1,6 +1,6 @@
1
1
  import { resolveBunEntrypoint } from "./bun-project.js";
2
- import "node:fs/promises";
3
2
  import path from "node:path";
3
+ import "node:fs/promises";
4
4
  import { spawn } from "node:child_process";
5
5
  //#region src/lib/app/local-dev.ts
6
6
  const DEFAULT_LOCAL_DEV_PORT = 3e3;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
- import { confirmPrompt } from "../../shell/prompt.js";
3
2
  import { canPrompt } from "../../shell/runtime.js";
3
+ import { confirmPrompt } from "../../shell/prompt.js";
4
4
  //#region src/lib/app/production-deploy-gate.ts
5
5
  async function enforceProductionDeployGate(context, provider, options) {
6
6
  if (options.branchKind !== "production") return { firstProductionDeploy: false };
@@ -25,6 +25,7 @@ async function enforceProductionDeployGate(context, provider, options) {
25
25
  if (!await confirmPrompt({
26
26
  input: context.runtime.stdin,
27
27
  output: context.output.stderr,
28
+ signal: context.runtime.signal,
28
29
  message: "Deploy to production?",
29
30
  initialValue: false
30
31
  })) throw productionDeployCancelledError();
@@ -1,7 +1,7 @@
1
1
  import { getApiBaseUrl } from "./client.js";
2
2
  import { FileTokenStorage } from "../../adapters/token-storage.js";
3
- import open from "open";
4
3
  import { AuthError, createManagementApiSdk } from "@prisma/management-api-sdk";
4
+ import open from "open";
5
5
  import events from "node:events";
6
6
  import http from "node:http";
7
7
  import readline from "node:readline/promises";
@@ -1,5 +1,5 @@
1
- import { access, readFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { access, readFile } from "node:fs/promises";
3
3
  //#region src/lib/git/local-branch.ts
4
4
  /**
5
5
  * Resolves the checked-out branch the way git does: the nearest `.git`
@@ -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";
@@ -1,8 +1,8 @@
1
+ import { formatCommandArgument } from "../../shell/command-arguments.js";
1
2
  import { CliError } from "../../shell/errors.js";
2
3
  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
4
  import path from "node:path";
5
+ import { readFile } from "node:fs/promises";
6
6
  import { Result, TaggedError, matchError } from "better-result";
7
7
  //#region src/lib/project/resolution.ts
8
8
  var ProjectNotFoundError = class extends TaggedError("ProjectNotFoundError")() {
@@ -1,7 +1,7 @@
1
+ import "../../shell/command-arguments.js";
1
2
  import { CliError, usageError } from "../../shell/errors.js";
2
3
  import { shortenHomePath } from "../fs/home-path.js";
3
4
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
4
- import "../../shell/command-arguments.js";
5
5
  import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
6
6
  import path from "node:path";
7
7
  import { Result, matchError } from "better-result";
@@ -0,0 +1,74 @@
1
+ import { formatShellCommand } from "../shell/command-arguments.js";
2
+ import { formatDescriptorLabel } from "../shell/command-meta.js";
3
+ import { renderSummaryLine } from "../shell/ui.js";
4
+ //#region src/presenters/agent.ts
5
+ function renderAgentInstall(context, descriptor, result) {
6
+ const ui = context.ui;
7
+ const rail = ui.dim("│");
8
+ return [
9
+ renderSummaryLine(ui, "success", `${formatDescriptorLabel(descriptor)} → ${operationSummary(result)} Prisma skills.`),
10
+ "",
11
+ `${rail} ${ui.accent("skills:")} ${result.skills.status.replace("-", " ")}`,
12
+ `${rail} ${ui.dim(formatShellCommand(result.skills.command))}`
13
+ ];
14
+ }
15
+ function renderAgentStatus(context, descriptor, result) {
16
+ const ui = context.ui;
17
+ const rail = ui.dim("│");
18
+ return [
19
+ `${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim(`Checking ${result.statusScope} Prisma skills.`)}`,
20
+ "",
21
+ `${rail} ${ui.accent("skills:")} ${result.skillsInstalled ? "installed" : "not found"}`,
22
+ ...renderInstalledSkills(context, result.skills),
23
+ `${rail}`,
24
+ `${rail} ${ui.accent("source:")} ${formatStatusSource(result)}`,
25
+ `${rail} ${ui.dim(formatShellCommand(result.skillsListCommand))}`,
26
+ ...renderProjectStatusDetails(context, result)
27
+ ];
28
+ }
29
+ function serializeAgentInstall(result) {
30
+ return result;
31
+ }
32
+ function serializeAgentStatus(result) {
33
+ return result;
34
+ }
35
+ function formatSetupPromptStatus(result) {
36
+ if (result.skillsInstalled) return "not needed";
37
+ if (result.promptDismissedAt) return `dismissed ${result.promptDismissedAt}`;
38
+ return "active";
39
+ }
40
+ function formatStatusSource(result) {
41
+ if (result.statusSource === "skills-cli") return result.statusScope === "global" ? "skills list -g --json" : "skills list --json";
42
+ if (result.statusSource === "skills-lock") return result.skillsLockPath;
43
+ return "unavailable";
44
+ }
45
+ function renderProjectStatusDetails(context, result) {
46
+ if (result.statusScope !== "project") return [];
47
+ const ui = context.ui;
48
+ const rail = ui.dim("│");
49
+ return [
50
+ `${rail}`,
51
+ `${rail} ${ui.accent("skills lock:")} ${result.skillsLockInstalled ? "installed" : "not found"}`,
52
+ `${rail} ${ui.dim(result.skillsLockPath)}`,
53
+ `${rail}`,
54
+ `${rail} ${ui.accent("setup prompt:")} ${formatSetupPromptStatus(result)}`
55
+ ];
56
+ }
57
+ function renderInstalledSkills(context, skills) {
58
+ const ui = context.ui;
59
+ const rail = ui.dim("│");
60
+ if (skills.length === 0) return [`${rail} ${ui.dim("No Prisma skills reported.")}`];
61
+ return skills.map((skill) => {
62
+ const agents = skill.agents.length > 0 ? skill.agents.join(", ") : "no agents reported";
63
+ return `${rail} ${skill.name} ${ui.dim(`${skill.scope}; ${agents}`)}`;
64
+ });
65
+ }
66
+ function operationSummary(result) {
67
+ if (result.skills.status === "would-install") return "Would install";
68
+ return operationLabel(result.operation);
69
+ }
70
+ function operationLabel(operation) {
71
+ return operation === "update" ? "Updated" : "Installed";
72
+ }
73
+ //#endregion
74
+ export { renderAgentInstall, renderAgentStatus, serializeAgentInstall, serializeAgentStatus };
@@ -25,7 +25,7 @@ function renderAuthSuccess(context, descriptor, command, result) {
25
25
  context: rows,
26
26
  operationDescription: "Applying authentication session changes",
27
27
  operationCount: 1,
28
- details: ["Session stored in local CLI state."]
28
+ details: ["Session stored in local CLI state.", ...result.agentSetupTip ? [`Install Prisma skills for this project with ${result.agentSetupTip.command}.`] : []]
29
29
  }, context.ui);
30
30
  }
31
31
  if (command === "auth.logout") return renderMutate({
@@ -1,7 +1,7 @@
1
+ import { formatCommandArgument } from "../shell/command-arguments.js";
1
2
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
3
  import { padDisplay, renderNextSteps, renderSummaryLine, renderVerboseBlock } from "../shell/ui.js";
3
4
  import { shortenHomePath } from "../lib/fs/home-path.js";
4
- import { formatCommandArgument } from "../shell/command-arguments.js";
5
5
  import { renderMutate, renderShow, serializeList } from "../output/patterns.js";
6
6
  import { renderResolvedProjectContextBlock } from "./verbose-context.js";
7
7
  import stringWidth from "string-width";
@@ -0,0 +1,12 @@
1
+ const PRISMA_CLI_PACKAGE_SPEC = `@prisma/cli@latest`;
2
+ const DEFAULT_PRISMA_CLI_PACKAGE_RUNNER = ["npx", "-y"];
3
+ const PRISMA_CLI_BINARY = "prisma-cli";
4
+ function formatPrismaCliCommand(args, options = {}) {
5
+ return [...getPrismaCliCommandPrefix(options), ...args].join(" ");
6
+ }
7
+ function getPrismaCliCommandPrefix({ invocation = "package", packageRunner = DEFAULT_PRISMA_CLI_PACKAGE_RUNNER }) {
8
+ if (invocation === "binary") return [PRISMA_CLI_BINARY];
9
+ return [...packageRunner, PRISMA_CLI_PACKAGE_SPEC];
10
+ }
11
+ //#endregion
12
+ export { formatPrismaCliCommand };
@@ -2,5 +2,11 @@
2
2
  function formatCommandArgument(value) {
3
3
  return /^[A-Za-z0-9._/-]+$/.test(value) && !value.startsWith("-") ? value : `'${value.replace(/'/g, "'\\''")}'`;
4
4
  }
5
+ function formatShellCommand(command) {
6
+ return command.map(formatShellCommandWord).join(" ");
7
+ }
8
+ function formatShellCommandWord(value) {
9
+ return /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
10
+ }
5
11
  //#endregion
6
- export { formatCommandArgument };
12
+ export { formatCommandArgument, formatShellCommand };