@prisma/cli 8.0.0-rc.8-dev.71 → 8.0.0-rc.8-dev.72

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 (3) hide show
  1. package/dist/cli.js +2471 -1698
  2. package/dist/sender.js +1 -1
  3. package/package.json +6 -5
package/dist/cli.js CHANGED
@@ -1,841 +1,148 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import process$1 from "node:process";
4
- import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, claimedIdentity, createCli, credentialWorkspaceId, credentialWorkspaceMismatchError, credentialsRequiredError, defineCommand, defineCommandFamily, defineSessionCommand, emptyServiceTokenError, flag, loadConfig, noSessionForWorkspaceError, positional, readActiveAccessToken, telemetryCommandGroup } from "@prisma/cli-engine";
4
+ import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, claimedIdentity, createCli, credentialWorkspaceId, credentialWorkspaceMismatchError, credentialsRequiredError, defineCommand, defineCommandFamily, defineConfigSection, defineSessionCommand, emptyServiceTokenError, flag, loadConfig, noSessionForWorkspaceError, positional, readActiveAccessToken, telemetryCommandGroup } from "@prisma/cli-engine";
5
5
  import { createComposerFamily } from "@prisma/composer-cli/family";
6
6
  import { ormCommandFamily } from "@prisma/orm-toolchain/cli";
7
7
  import { CliStructuredError, notOk, ok } from "@prisma/cli-engine/protocol";
8
- import { fstatSync, readFileSync, statSync } from "node:fs";
9
- import path from "node:path";
10
- import { execa } from "execa";
11
- import fs, { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
12
8
  import { AuthError, createManagementApiClient, createManagementApiSdk } from "@prisma/management-api-sdk";
13
9
  import os from "node:os";
10
+ import path from "node:path";
14
11
  import { randomBytes, randomUUID } from "node:crypto";
12
+ import fs, { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
15
13
  import { CredentialsStore } from "@prisma/credentials-store";
16
14
  import events from "node:events";
17
15
  import http from "node:http";
18
16
  import readline from "node:readline/promises";
19
17
  import open from "open";
18
+ import { existsSync, fstatSync, readFileSync, statSync } from "node:fs";
19
+ import { PnpmTool, YarnTool } from "@manypkg/tools";
20
20
  import { Result, TaggedError, UnhandledException, matchError } from "better-result";
21
21
  import { execFile, fork, spawn } from "node:child_process";
22
22
  import { promisify } from "node:util";
23
23
  import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
24
24
  import { parse } from "dotenv";
25
25
  import { fileURLToPath } from "node:url";
26
+ import { execa } from "execa";
26
27
  import { Writable } from "node:stream";
27
28
  import { pipeline } from "node:stream/promises";
28
29
  //#region src/cli-name.ts
29
30
  /**
30
- * The CLI's user-facing identity, in one place. The npm package is
31
- * "@prisma/cli" but the binary on PATH is "prisma-cli" (the S1
32
- * convention) — every user-facing command string and notice consumes
33
- * this constant rather than restating the name.
31
+ * The CLI's user-facing identity, in one place: the binary on PATH is
32
+ * `prisma`, published by the `prisma` package, and every user-facing
33
+ * command string and notice consumes this constant rather than
34
+ * restating the name. The `@prisma/cli` package installs the same shell
35
+ * under the name `prisma-cli`; what a user is told to type is the
36
+ * unified binary's name.
34
37
  */
35
- const CLI_NAME = "prisma-cli";
38
+ const CLI_NAME = "prisma";
36
39
  /** The CLI docs page (also the update-check fallback instruction URL).
37
40
  * The old /docs/orm/tools/prisma-cli path 308-redirects to the ORM CLI
38
41
  * reference — the wrong docs for the unified CLI — so this points at
39
42
  * the docs root until the unified CLI has its own page. */
40
43
  const CLI_DOCS_URL = "https://www.prisma.io/docs";
41
- const PRISMA_CLI_PACKAGE_SPEC = `@prisma/cli@next`;
42
- const DEFAULT_PRISMA_CLI_PACKAGE_RUNNER = ["npx", "-y"];
43
- const PRISMA_CLI_BINARY = "prisma-cli";
44
- function formatPrismaCliCommand(args, options = {}) {
45
- return [...getPrismaCliCommandPrefix(options), ...args].join(" ");
46
- }
47
- function getPrismaCliCommandPrefix({ invocation = "package", packageRunner = DEFAULT_PRISMA_CLI_PACKAGE_RUNNER }) {
48
- if (invocation === "binary") return [PRISMA_CLI_BINARY];
49
- return [...packageRunner, PRISMA_CLI_PACKAGE_SPEC];
50
- }
51
44
  //#endregion
52
- //#region src/lib/agent/package-manager.ts
53
- const LOCKFILE_PACKAGE_MANAGERS = [
54
- {
55
- packageManager: "bun",
56
- fileNames: ["bun.lock", "bun.lockb"]
57
- },
58
- {
59
- packageManager: "pnpm",
60
- fileNames: ["pnpm-lock.yaml", "pnpm-workspace.yaml"]
61
- },
62
- {
63
- packageManager: "yarn",
64
- fileNames: ["yarn.lock"]
65
- },
66
- {
67
- packageManager: "npm",
68
- fileNames: ["package-lock.json", "npm-shrinkwrap.json"]
69
- }
70
- ];
71
- async function resolveSkillsPackageRunner(options) {
72
- return resolvePackageRunner(options);
73
- }
74
- async function resolvePackageRunner(options) {
75
- options.signal.throwIfAborted();
76
- const packageManager = detectPackageManagerSync(options.cwd, options.signal) ?? "npm";
77
- options.signal.throwIfAborted();
78
- return packageRunnerForPackageManager(packageManager);
79
- }
80
- function detectPackageManagerSync(cwd, signal) {
81
- let directory = path.resolve(cwd);
82
- while (true) {
83
- signal?.throwIfAborted();
84
- const packageJsonManager = readPackageJsonPackageManager(directory);
85
- if (packageJsonManager) return packageJsonManager;
86
- const lockfileManager = readLockfilePackageManager(directory, signal);
87
- if (lockfileManager) return lockfileManager;
88
- const parent = path.dirname(directory);
89
- if (parent === directory) return null;
90
- directory = parent;
91
- }
92
- }
93
- function readPackageJsonPackageManager(directory) {
94
- const packageJsonPath = path.join(directory, "package.json");
95
- let content;
96
- try {
97
- content = readFileSync(packageJsonPath, "utf8");
98
- } catch (error) {
99
- if (isMissingFileError(error)) return null;
100
- throw error;
101
- }
102
- try {
103
- return parsePackageManager(JSON.parse(content).packageManager);
104
- } catch {
105
- return null;
106
- }
107
- }
108
- function readLockfilePackageManager(directory, signal) {
109
- for (const candidate of LOCKFILE_PACKAGE_MANAGERS) for (const fileName of candidate.fileNames) {
110
- signal?.throwIfAborted();
111
- if (fileExists(path.join(directory, fileName))) return candidate.packageManager;
112
- }
113
- return null;
45
+ //#region src/auth/client.ts
46
+ const CLIENT_ID = "cmm3lndn701oo0uefvxzo0ivw";
47
+ const AUTH_FILE_ENV_VAR = "PRISMA_COMPUTE_AUTH_FILE";
48
+ /**
49
+ * The redirect the OAuth client is registered with. `performLogin`
50
+ * replaces it with its own ephemeral callback server's port; the
51
+ * refreshing client never reads it.
52
+ */
53
+ const DEFAULT_REDIRECT_URI = "http://localhost/auth/callback";
54
+ function getApiBaseUrl(env = process.env) {
55
+ return env.PRISMA_MANAGEMENT_API_URL?.trim() || "https://api.prisma.io";
114
56
  }
115
- function fileExists(filePath) {
116
- try {
117
- return statSync(filePath).isFile();
118
- } catch (error) {
119
- if (isMissingFileError(error)) return false;
120
- throw error;
121
- }
57
+ function getAuthBaseUrl(env = process.env) {
58
+ return env.PRISMA_AUTH_BASE_URL?.trim() || "https://auth.prisma.io";
122
59
  }
123
- function parsePackageManager(value) {
124
- if (typeof value !== "string") return null;
125
- const normalized = value.trim().toLowerCase();
126
- if (normalized === "bun" || normalized.startsWith("bun@")) return "bun";
127
- if (normalized === "pnpm" || normalized.startsWith("pnpm@")) return "pnpm";
128
- if (normalized === "yarn" || normalized.startsWith("yarn@")) return "yarn";
129
- if (normalized === "npm" || normalized.startsWith("npm@")) return "npm";
130
- return null;
60
+ function getAuthFilePath(env = process.env) {
61
+ const configured = env[AUTH_FILE_ENV_VAR];
62
+ if (configured?.trim()) return path.resolve(configured);
63
+ return defaultAuthFilePath(env);
131
64
  }
132
- function packageRunnerForPackageManager(packageManager) {
133
- switch (packageManager) {
134
- case "bun": return ["bunx"];
135
- case "pnpm": return ["pnpm", "dlx"];
136
- case "yarn": return ["yarn", "dlx"];
137
- case "npm": return ["npx", "-y"];
65
+ function defaultAuthFilePath(env = process.env) {
66
+ if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "prisma", "auth.json");
67
+ if (process.platform === "win32") {
68
+ const appData = env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
69
+ return path.join(appData, "prisma", "auth.json");
138
70
  }
139
- }
140
- function isMissingFileError(error) {
141
- const code = error.code;
142
- return code === "ENOENT" || code === "ENOTDIR";
71
+ const xdgConfigHome = env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
72
+ return path.join(xdgConfigHome, "prisma", "auth.json");
143
73
  }
144
74
  //#endregion
145
- //#region src/lib/agent/cli-command.ts
146
- async function resolvePrismaCliPackageCommandFormatter(options) {
147
- return createPrismaCliPackageCommandFormatter(await resolvePackageRunner(options));
148
- }
149
- async function resolvePrismaCliPackageCommand(options) {
150
- return (await resolvePrismaCliPackageCommandFormatter(options))(options.args);
75
+ //#region src/lib/workspace-id.ts
76
+ /**
77
+ * A workspace reaches the CLI under two ids for the same workspace: a
78
+ * credential's `workspace_id` claim carries the bare id, while the
79
+ * management API and anything derived from it carry the same id behind
80
+ * a `wksp_` prefix. Comparing the two forms directly silently matches
81
+ * nothing, so every comparison between workspace ids of different
82
+ * origin goes through here.
83
+ */
84
+ const WORKSPACE_ID_PREFIX = "wksp_";
85
+ function stripWorkspacePrefix(value) {
86
+ return value.startsWith(WORKSPACE_ID_PREFIX) ? value.slice(5) : value;
151
87
  }
152
- function createPrismaCliPackageCommandFormatter(packageRunner) {
153
- return (args) => formatPrismaCliCommand(args, { packageRunner });
88
+ function sameWorkspaceId(left, right) {
89
+ return stripWorkspacePrefix(left) === stripWorkspacePrefix(right);
154
90
  }
155
91
  //#endregion
156
- //#region src/lib/agent/constants.ts
157
- const PRISMA_SKILLS_SOURCE = "prisma/skills";
158
- const PRISMA_SKILLS_LOCK_FILENAME = "skills-lock.json";
159
- const SKILLS_CLI_PACKAGE = "skills@latest";
160
- const DEFAULT_PRISMA_AGENT_SKILLS = ["*"];
161
- const DEFAULT_PRISMA_AGENT_TARGETS = ["codex", "claude-code"];
162
- const PRISMA_AGENT_INSTALL_ARGS = ["agent", "install"];
163
- const PRISMA_AGENT_STATUS_ARGS = ["agent", "status"];
164
- //#endregion
165
- //#region src/shell-command.ts
166
- /** A word a shell needs no quoting for. */
167
- const SHELL_SAFE_WORD = /^[A-Za-z0-9_./:@=-]+$/;
168
- const SINGLE_QUOTE = /'/g;
169
- /** Renders a command as a line a user can paste into a shell, quoting
170
- * any word that needs it. */
171
- function formatShellCommand(command) {
172
- return command.map(formatShellCommandWord).join(" ");
173
- }
174
- function formatShellCommandWord(value) {
175
- return SHELL_SAFE_WORD.test(value) ? value : `'${value.replace(SINGLE_QUOTE, "'\\''")}'`;
92
+ //#region src/auth/token-storage.ts
93
+ const REFRESH_LOCK_RETRY_MS$1 = 100;
94
+ const REFRESH_LOCK_STALE_MS$1 = 3e4;
95
+ const REFRESH_LOCK_WAIT_TIMEOUT_MS$1 = 25e3;
96
+ const EMPTY_AUTH_CONTEXT = {
97
+ activeWorkspaceId: null,
98
+ workspaces: {}
99
+ };
100
+ const UNKNOWN_WORKSPACE_NAME = "Unknown workspace";
101
+ function getAuthContextFilePath(authFilePath) {
102
+ const extension = path.extname(authFilePath);
103
+ if (!extension) return `${authFilePath}.context.json`;
104
+ return `${authFilePath.slice(0, -extension.length)}.context${extension}`;
176
105
  }
177
- //#endregion
178
- //#region src/commands/agent/presentation.ts
179
- function fields$1(rows) {
180
- return {
181
- kind: "fields",
182
- rows
183
- };
106
+ function findLatestValidTokens(allCredentials) {
107
+ for (let i = allCredentials.length - 1; i >= 0; i -= 1) {
108
+ const credential = allCredentials[i];
109
+ if (!credential) continue;
110
+ if (typeof credential.workspaceId !== "string" || credential.workspaceId.length === 0 || typeof credential.token !== "string" || credential.token.length === 0 || typeof credential.refreshToken !== "string" || credential.refreshToken.length === 0) continue;
111
+ return {
112
+ workspaceId: credential.workspaceId,
113
+ accessToken: credential.token,
114
+ refreshToken: credential.refreshToken
115
+ };
116
+ }
117
+ return null;
184
118
  }
185
- function title$1(text) {
119
+ function storedCredentialToTokens(credential) {
120
+ if (!credential) return null;
121
+ if (typeof credential.workspaceId !== "string" || credential.workspaceId.length === 0 || typeof credential.token !== "string" || credential.token.length === 0 || typeof credential.refreshToken !== "string" || credential.refreshToken.length === 0) return null;
186
122
  return {
187
- kind: "summary",
188
- status: "info",
189
- text
123
+ workspaceId: credential.workspaceId,
124
+ accessToken: credential.token,
125
+ refreshToken: credential.refreshToken
190
126
  };
191
127
  }
192
- function operationSummary(result) {
193
- if (result.skills.status === "would-install") return "Would install";
194
- return result.operation === "update" ? "Updated" : "Installed";
195
- }
196
- function statusSourceValue(result) {
197
- if (result.statusSource === "skills-cli") return result.statusScope === "global" ? "skills list -g --json" : "skills list --json";
198
- if (result.statusSource === "skills-lock") return result.skillsLockPath;
199
- return "unavailable";
200
- }
201
- function setupPromptValue(result) {
202
- if (result.skillsInstalled) return "not needed";
203
- if (result.promptDismissedAt) return `dismissed ${result.promptDismissedAt}`;
204
- return "active";
205
- }
206
- function projectStatusRows(result) {
207
- if (result.statusScope !== "project") return [];
208
- return [
209
- {
210
- label: "skills lock",
211
- value: result.skillsLockInstalled ? "installed" : "not found"
212
- },
213
- {
214
- label: "skills lock path",
215
- value: result.skillsLockPath
216
- },
217
- {
218
- label: "setup prompt",
219
- value: setupPromptValue(result)
220
- }
221
- ];
222
- }
223
- function installPresentations(result, statusCommand) {
224
- return {
225
- stdout: () => [],
226
- json: () => result,
227
- human: () => [{
228
- kind: "summary",
229
- status: result.skills.status === "installed" ? "ok" : "info",
230
- text: `${operationSummary(result)} Prisma skills.`
231
- }, fields$1([{
232
- label: "skills",
233
- value: result.skills.status.replace("-", " ")
234
- }, {
235
- label: "command",
236
- value: formatShellCommand(result.skills.command)
237
- }])],
238
- next: () => statusCommand === null ? [] : [{
239
- kind: "run-command",
240
- label: "Verify the installed Prisma skills",
241
- command: statusCommand
242
- }]
243
- };
128
+ function findTokensForWorkspace(allCredentials, workspaceId) {
129
+ return storedCredentialToTokens(allCredentials.find((credential) => credential?.workspaceId === workspaceId)) ?? null;
244
130
  }
245
- function statusPresentations(result, installCommand) {
246
- return {
247
- stdout: () => [],
248
- json: () => result,
249
- human: () => [
250
- title$1(`Checking ${result.statusScope} Prisma skills.`),
251
- fields$1([
252
- {
253
- label: "skills",
254
- value: result.skillsInstalled ? "installed" : "not found"
255
- },
256
- {
257
- label: "source",
258
- value: statusSourceValue(result)
259
- },
260
- {
261
- label: "command",
262
- value: formatShellCommand(result.skillsListCommand)
263
- },
264
- ...projectStatusRows(result)
265
- ]),
266
- result.skills.length === 0 ? {
267
- kind: "summary",
268
- status: "info",
269
- text: "No Prisma skills reported."
270
- } : {
271
- kind: "table",
272
- columns: [
273
- "skill",
274
- "scope",
275
- "agents"
276
- ],
277
- rows: result.skills.map((skill) => [
278
- skill.name,
279
- skill.scope,
280
- skill.agents.length > 0 ? skill.agents.join(", ") : "no agents reported"
281
- ])
282
- }
283
- ],
284
- next: () => installCommand === null ? [] : [{
285
- kind: "run-command",
286
- label: "Install or refresh Prisma skills",
287
- command: installCommand
288
- }]
289
- };
131
+ function tokensEqual(a, b) {
132
+ return a?.workspaceId === b?.workspaceId && a?.accessToken === b?.accessToken && a?.refreshToken === b?.refreshToken;
290
133
  }
291
- //#endregion
292
- //#region src/commands/agent/errors.ts
293
- /**
294
- * The installer's own command line is the next action: legacy carried it
295
- * as the error's single nextStep with the fix "Run the command below to
296
- * retry the installer directly."
297
- */
298
- function skillsInstallFailedError(options) {
299
- return new CliStructuredError("AGENT.SKILLS_INSTALL_FAILED", "Prisma skills install failed", {
300
- why: `The skills installer exited with code ${options.exitCode ?? "unknown"}.`,
301
- nextActions: [{
302
- kind: "run-command",
303
- label: "Retry the installer directly",
304
- command: formatShellCommand(options.command)
305
- }],
306
- cause: options.cause
307
- });
308
- }
309
- //#endregion
310
- //#region src/commands/agent/skills-cli.ts
311
- async function buildSkillsInstallCommand(ctx, inputs, cwd) {
312
- const command = [
313
- ...await resolveSkillsPackageRunner({
314
- cwd,
315
- signal: ctx.signal
316
- }),
317
- SKILLS_CLI_PACKAGE,
318
- "add",
319
- PRISMA_SKILLS_SOURCE
320
- ];
321
- const skills = inputs.skill && inputs.skill.length > 0 ? inputs.skill : DEFAULT_PRISMA_AGENT_SKILLS;
322
- for (const skill of skills) command.push("--skill", skill);
323
- for (const agent of resolveTargetAgents(inputs)) command.push("--agent", agent);
324
- if (inputs.global) command.push("--global");
325
- if (inputs.copy || ctx.host.platform === "win32") command.push("--copy");
326
- command.push("--yes");
327
- return command;
328
- }
329
- function resolveTargetAgents(inputs) {
330
- if (inputs.allAgents) return ["*"];
331
- if (inputs.agent && inputs.agent.length > 0) return inputs.agent;
332
- return DEFAULT_PRISMA_AGENT_TARGETS;
333
- }
334
- async function runSkillsInstall(ctx, command, cwd) {
335
- const [executable, args] = splitCommand(command);
336
- try {
337
- await execa(executable, args, {
338
- cwd,
339
- env: ctx.env,
340
- cancelSignal: ctx.signal,
341
- stdin: "ignore"
342
- });
343
- } catch (error) {
344
- if (isAbortError$1(error)) throw error;
345
- throw skillsInstallFailedError({
346
- command,
347
- exitCode: exitCodeFromError(error),
348
- cause: error
349
- });
350
- }
351
- }
352
- async function listInstalledPrismaSkills(ctx, cwd, scope) {
353
- const command = [
354
- ...await resolveSkillsPackageRunner({
355
- cwd,
356
- signal: ctx.signal
357
- }),
358
- SKILLS_CLI_PACKAGE,
359
- "list",
360
- ...scope === "global" ? ["-g"] : [],
361
- "--json"
362
- ];
363
- const [executable, args] = splitCommand(command);
364
- try {
365
- const { stdout } = await execa(executable, args, {
366
- cwd,
367
- env: ctx.env,
368
- cancelSignal: ctx.signal,
369
- stdin: "ignore"
370
- });
371
- return {
372
- status: "ok",
373
- command,
374
- skills: parseSkillsListOutput(stdout ?? "").filter((skill) => isPrismaSkillName(skill.name))
375
- };
376
- } catch (error) {
377
- if (isAbortError$1(error) || ctx.signal.aborted) throw error;
378
- return {
379
- status: "failed",
380
- command,
381
- message: error instanceof Error ? error.message : String(error)
382
- };
383
- }
384
- }
385
- function splitCommand(command) {
386
- const [executable, ...args] = command;
387
- if (!executable) throw new Error("Cannot run an empty command.");
388
- return [executable, args];
389
- }
390
- function isAbortError$1(error) {
391
- return error instanceof Error && error.name === "AbortError" || isObject(error) && error.isCanceled === true;
392
- }
393
- function exitCodeFromError(error) {
394
- if (!isObject(error) || typeof error.exitCode !== "number") return null;
395
- return error.exitCode;
396
- }
397
- function isObject(value) {
398
- return typeof value === "object" && value !== null;
399
- }
400
- function parseSkillsListOutput(output) {
401
- const parsed = JSON.parse(output);
402
- if (!Array.isArray(parsed)) throw new Error("skills list did not return a JSON array");
403
- return parsed.flatMap((item) => {
404
- const skill = parseInstalledSkill(item);
405
- return skill ? [skill] : [];
406
- });
407
- }
408
- function parseInstalledSkill(value) {
409
- if (!isObject(value)) return null;
410
- if (typeof value.name !== "string" || typeof value.path !== "string" || typeof value.scope !== "string" || !Array.isArray(value.agents)) return null;
411
- return {
412
- name: value.name,
413
- path: value.path,
414
- scope: value.scope,
415
- agents: value.agents.filter((agent) => typeof agent === "string")
416
- };
417
- }
418
- function isPrismaSkillName(name) {
419
- return name === "prisma" || name.startsWith("prisma-");
420
- }
421
- //#endregion
422
- //#region src/commands/agent/install.ts
423
- /** `agent install` and `agent update` are one operation with two names,
424
- * as in the legacy shell: same flags, same flow, different reported
425
- * operation. */
426
- const agentInstallFlags = {
427
- agent: flag.repeated({
428
- brief: "Agent target for Prisma skills; repeat for multiple agents",
429
- placeholder: "agent"
430
- }),
431
- allAgents: flag.boolean({ brief: "Install Prisma skills for every agent supported by the skills CLI" }),
432
- skill: flag.repeated({
433
- brief: "Prisma skill to install; repeat for multiple skills",
434
- placeholder: "skill"
435
- }),
436
- global: flag.boolean({ brief: "Install skills into the user directory instead of the project" }),
437
- copy: flag.boolean({ brief: "Ask the skills CLI to copy files instead of symlinking them (always on Windows)" }),
438
- dryRun: flag.boolean({ brief: "Show the installer command without running it" })
439
- };
440
- async function runAgentSkillsInstall(flags, ctx, operation) {
441
- const command = await buildSkillsInstallCommand(ctx, {
442
- agent: flags.agent ?? [],
443
- skill: flags.skill ?? [],
444
- allAgents: flags.allAgents,
445
- copy: flags.copy,
446
- global: flags.global
447
- }, ctx.cwd);
448
- if (!flags.dryRun) await runSkillsInstall(ctx, command, ctx.cwd);
449
- const result = {
450
- operation,
451
- skills: {
452
- status: flags.dryRun ? "would-install" : "installed",
453
- command
454
- }
455
- };
456
- const statusCommand = flags.dryRun ? null : await resolvePrismaCliPackageCommand({
457
- cwd: ctx.cwd,
458
- signal: ctx.signal,
459
- args: flags.global ? [...PRISMA_AGENT_STATUS_ARGS, "--global"] : PRISMA_AGENT_STATUS_ARGS
460
- });
461
- return ok(ctx.present({ data: result }, installPresentations(result, statusCommand)));
462
- }
463
- const agentInstallCommand = defineCommand({
464
- help: {
465
- summary: "Install Prisma skills for AI coding agents",
466
- examples: [
467
- "agent install",
468
- "agent install --agent codex",
469
- "agent install --all-agents",
470
- "agent install --skill prisma-compute"
471
- ]
472
- },
473
- args: { flags: agentInstallFlags },
474
- handler: async (args, ctx) => runAgentSkillsInstall(args.flags, ctx, "install")
475
- });
476
- //#endregion
477
- //#region src/adapters/local-state.ts
478
- const DEFAULT_STATE = {
479
- auth: null,
480
- project: {
481
- rememberedByWorkspace: {},
482
- lastResolved: null,
483
- repositoryConnectionsByProject: {}
484
- },
485
- branch: { active: "preview" },
486
- agent: { setupPromptDismissedAt: null }
487
- };
488
- const DEFAULT_STATE_FILE_NAME = "state.json";
489
- function resolveLocalStateFilePath(stateDir) {
490
- return path.join(stateDir, DEFAULT_STATE_FILE_NAME);
491
- }
492
- var LocalStateStore = class {
493
- stateFilePath;
494
- constructor(stateDir, signal) {
495
- this.signal = signal;
496
- this.stateFilePath = resolveLocalStateFilePath(stateDir);
497
- }
498
- async read() {
499
- this.signal?.throwIfAborted();
500
- try {
501
- const raw = await readFile(this.stateFilePath, {
502
- encoding: "utf8",
503
- signal: this.signal
504
- });
505
- const parsed = JSON.parse(raw);
506
- return {
507
- auth: parsed.auth ?? structuredClone(DEFAULT_STATE.auth),
508
- project: {
509
- rememberedByWorkspace: parsed.project?.rememberedByWorkspace ?? {},
510
- lastResolved: parsed.project?.lastResolved ?? null,
511
- repositoryConnectionsByProject: parsed.project?.repositoryConnectionsByProject ?? {}
512
- },
513
- branch: { active: parsed.branch?.active ?? DEFAULT_STATE.branch.active },
514
- agent: { setupPromptDismissedAt: parsed.agent?.setupPromptDismissedAt ?? null }
515
- };
516
- } catch (error) {
517
- if (error.code === "ENOENT") return structuredClone(DEFAULT_STATE);
518
- throw error;
519
- }
520
- }
521
- async write(state) {
522
- this.signal?.throwIfAborted();
523
- await mkdir(path.dirname(this.stateFilePath), { recursive: true });
524
- this.signal?.throwIfAborted();
525
- await writeFile(this.stateFilePath, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8" });
526
- this.signal?.throwIfAborted();
527
- }
528
- async setAuthSession(session) {
529
- const state = await this.read();
530
- state.auth = session;
531
- await this.write(state);
532
- return state;
533
- }
534
- async clearAuthSession() {
535
- const state = await this.read();
536
- state.auth = null;
537
- await this.write(state);
538
- return state;
539
- }
540
- async setActiveBranch(active) {
541
- const state = await this.read();
542
- state.branch.active = active;
543
- await this.write(state);
544
- return state;
545
- }
546
- async readRememberedProject(workspaceId) {
547
- return (await this.read()).project.rememberedByWorkspace[workspaceId] ?? null;
548
- }
549
- async readLastResolvedProject() {
550
- return (await this.read()).project.lastResolved;
551
- }
552
- async setRememberedProject(project) {
553
- const state = await this.read();
554
- state.project.rememberedByWorkspace[project.workspaceId] = project;
555
- state.project.lastResolved = project;
556
- await this.write(state);
557
- return state;
558
- }
559
- async readRepositoryConnection(projectId) {
560
- return (await this.read()).project.repositoryConnectionsByProject[projectId] ?? null;
561
- }
562
- async setRepositoryConnection(projectId, connection) {
563
- const state = await this.read();
564
- state.project.repositoryConnectionsByProject[projectId] = connection;
565
- await this.write(state);
566
- return state;
567
- }
568
- async clearRepositoryConnection(projectId) {
569
- const state = await this.read();
570
- delete state.project.repositoryConnectionsByProject[projectId];
571
- await this.write(state);
572
- return state;
573
- }
574
- async readAgentSetupPromptDismissedAt() {
575
- return (await this.read()).agent.setupPromptDismissedAt;
576
- }
577
- async setAgentSetupPromptDismissedAt(dismissedAt) {
578
- const state = await this.read();
579
- state.agent.setupPromptDismissedAt = dismissedAt;
580
- await this.write(state);
581
- return state;
582
- }
583
- };
584
- //#endregion
585
- //#region src/lib/agent/setup-status.ts
586
- async function readPrismaAgentSetupStatus(options) {
587
- const skillsLockPath = path.join(options.cwd, PRISMA_SKILLS_LOCK_FILENAME);
588
- const [skillsInstalled, promptDismissedAt] = await Promise.all([hasPrismaSkillsLock(skillsLockPath, options.signal, options.requiredSkill), options.stateStore?.readAgentSetupPromptDismissedAt() ?? null]);
589
- return {
590
- skillsLockPath: path.basename(skillsLockPath),
591
- skillsInstalled,
592
- promptDismissedAt
593
- };
594
- }
595
- function isPrismaAgentSetupComplete(status) {
596
- return status.skillsInstalled;
597
- }
598
- function shouldOfferPrismaAgentSetup(status) {
599
- return !isPrismaAgentSetupComplete(status) && !status.promptDismissedAt;
600
- }
601
- async function isLikelyProjectDirectory(options) {
602
- return (await Promise.all([
603
- "package.json",
604
- "prisma.config.ts",
605
- ".git"
606
- ].map((fileName) => pathExists(path.join(options.cwd, fileName), options.signal)))).some(Boolean);
607
- }
608
- async function hasPrismaSkillsLock(filePath, signal, requiredSkill) {
609
- try {
610
- const raw = await readFile(filePath, {
611
- encoding: "utf8",
612
- signal
613
- });
614
- return hasPrismaSkillsLockEntry(JSON.parse(raw), requiredSkill);
615
- } catch (error) {
616
- if (isNotFoundError(error) || error instanceof SyntaxError) return false;
617
- throw error;
618
- }
619
- }
620
- function hasPrismaSkillsLockEntry(value, requiredSkill) {
621
- if (!isRecord(value)) return false;
622
- if (requiredSkill) return skillLockEntryUsesPrismaSource(readSkillLockEntries(value)[requiredSkill]);
623
- if (readLegacySources(value).includes("prisma/skills")) return true;
624
- return Object.values(readSkillLockEntries(value)).some(skillLockEntryUsesPrismaSource);
625
- }
626
- function readLegacySources(value) {
627
- return Array.isArray(value.sources) ? value.sources.filter((source) => typeof source === "string") : [];
628
- }
629
- function readSkillLockEntries(value) {
630
- return isRecord(value.skills) ? value.skills : {};
631
- }
632
- function skillLockEntryUsesPrismaSource(value) {
633
- return isRecord(value) && value.source === "prisma/skills";
634
- }
635
- function isRecord(value) {
636
- return typeof value === "object" && value !== null;
637
- }
638
- async function pathExists(filePath, signal) {
639
- signal.throwIfAborted();
640
- try {
641
- await stat(filePath);
642
- signal.throwIfAborted();
643
- return true;
644
- } catch (error) {
645
- if (signal.aborted) throw error;
646
- if (isNotFoundError(error)) return false;
647
- throw error;
648
- }
649
- }
650
- function isNotFoundError(error) {
651
- const code = error.code;
652
- return code === "ENOENT" || code === "ENOTDIR";
653
- }
654
- //#endregion
655
- //#region src/state-dir.ts
656
- const DEFAULT_STATE_DIR_NAME = path.join(".prisma", "cli");
657
- function resolveStateDir(inputs) {
658
- const explicitStateDir = inputs.stateDir ?? inputs.env.PRISMA_CLI_STATE_DIR;
659
- if (explicitStateDir) return explicitStateDir;
660
- return path.join(inputs.cwd, DEFAULT_STATE_DIR_NAME);
661
- }
662
- //#endregion
663
- //#region src/commands/agent/status.ts
664
- function resolveStatusSource(skillsList, statusScope) {
665
- if (skillsList.status === "ok") return "skills-cli";
666
- return statusScope === "project" ? "skills-lock" : "unavailable";
667
- }
668
- function openStateStore(ctx) {
669
- return new LocalStateStore(resolveStateDir({
670
- env: ctx.env,
671
- cwd: ctx.cwd
672
- }), ctx.signal);
673
- }
674
- function skillsListUnavailable(failure, scope, skillsLockPath) {
675
- const commandText = formatShellCommand(failure.command);
676
- return {
677
- code: "AGENT.SKILLS_LIST_UNAVAILABLE",
678
- severity: "warn",
679
- summary: scope === "project" ? `Could not read installed skills with ${commandText}: ${failure.message}. Falling back to ${skillsLockPath}.` : `Could not read globally installed skills with ${commandText}: ${failure.message}.`,
680
- nextActions: []
681
- };
682
- }
683
- const agentStatusCommand = defineCommand({
684
- help: {
685
- summary: "Show installed Prisma skills",
686
- examples: [
687
- "agent status",
688
- "agent status --json",
689
- "agent status --global"
690
- ]
691
- },
692
- args: { flags: { global: flag.boolean({ brief: "Check globally installed Prisma skills instead of project skills" }) } },
693
- handler: async (args, ctx) => {
694
- const statusScope = args.flags.global ? "global" : "project";
695
- const setupStatus = await readPrismaAgentSetupStatus({
696
- cwd: ctx.cwd,
697
- stateStore: openStateStore(ctx),
698
- signal: ctx.signal
699
- });
700
- const skillsList = await listInstalledPrismaSkills(ctx, ctx.cwd, statusScope);
701
- const skillsInstalled = skillsList.status === "ok" ? skillsList.skills.length > 0 : statusScope === "project" && setupStatus.skillsInstalled;
702
- const result = {
703
- skills: skillsList.status === "ok" ? skillsList.skills : [],
704
- skillsListCommand: skillsList.command,
705
- statusScope,
706
- skillsLockPath: setupStatus.skillsLockPath,
707
- skillsLockInstalled: setupStatus.skillsInstalled,
708
- skillsInstalled,
709
- statusSource: resolveStatusSource(skillsList, statusScope),
710
- promptDismissedAt: setupStatus.promptDismissedAt
711
- };
712
- const installCommand = skillsInstalled ? null : await resolvePrismaCliPackageCommand({
713
- cwd: ctx.cwd,
714
- signal: ctx.signal,
715
- args: args.flags.global ? [...PRISMA_AGENT_INSTALL_ARGS, "--global"] : PRISMA_AGENT_INSTALL_ARGS
716
- });
717
- return ok(ctx.present({
718
- data: result,
719
- diagnostics: skillsList.status === "ok" ? [] : [skillsListUnavailable(skillsList, statusScope, setupStatus.skillsLockPath)]
720
- }, statusPresentations(result, installCommand)));
721
- }
722
- });
723
- //#endregion
724
- //#region src/commands/agent/update.ts
725
- const agentUpdateCommand = defineCommand({
726
- help: {
727
- summary: "Refresh Prisma skills for AI coding agents",
728
- examples: [
729
- "agent update",
730
- "agent update --agent codex",
731
- "agent update --all-agents"
732
- ]
733
- },
734
- args: { flags: agentInstallFlags },
735
- handler: async (args, ctx) => runAgentSkillsInstall(args.flags, ctx, "update")
736
- });
737
- //#endregion
738
- //#region src/auth/client.ts
739
- const CLIENT_ID = "cmm3lndn701oo0uefvxzo0ivw";
740
- const AUTH_FILE_ENV_VAR = "PRISMA_COMPUTE_AUTH_FILE";
741
- /**
742
- * The redirect the OAuth client is registered with. `performLogin`
743
- * replaces it with its own ephemeral callback server's port; the
744
- * refreshing client never reads it.
745
- */
746
- const DEFAULT_REDIRECT_URI = "http://localhost/auth/callback";
747
- function getApiBaseUrl(env = process.env) {
748
- return env.PRISMA_MANAGEMENT_API_URL?.trim() || "https://api.prisma.io";
749
- }
750
- function getAuthBaseUrl(env = process.env) {
751
- return env.PRISMA_AUTH_BASE_URL?.trim() || "https://auth.prisma.io";
752
- }
753
- function getAuthFilePath(env = process.env) {
754
- const configured = env[AUTH_FILE_ENV_VAR];
755
- if (configured?.trim()) return path.resolve(configured);
756
- return defaultAuthFilePath(env);
757
- }
758
- function defaultAuthFilePath(env = process.env) {
759
- if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "prisma", "auth.json");
760
- if (process.platform === "win32") {
761
- const appData = env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
762
- return path.join(appData, "prisma", "auth.json");
763
- }
764
- const xdgConfigHome = env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
765
- return path.join(xdgConfigHome, "prisma", "auth.json");
766
- }
767
- //#endregion
768
- //#region src/lib/workspace-id.ts
769
- /**
770
- * A workspace reaches the CLI under two ids for the same workspace: a
771
- * credential's `workspace_id` claim carries the bare id, while the
772
- * management API and anything derived from it carry the same id behind
773
- * a `wksp_` prefix. Comparing the two forms directly silently matches
774
- * nothing, so every comparison between workspace ids of different
775
- * origin goes through here.
776
- */
777
- const WORKSPACE_ID_PREFIX = "wksp_";
778
- function stripWorkspacePrefix(value) {
779
- return value.startsWith(WORKSPACE_ID_PREFIX) ? value.slice(5) : value;
780
- }
781
- function sameWorkspaceId(left, right) {
782
- return stripWorkspacePrefix(left) === stripWorkspacePrefix(right);
783
- }
784
- //#endregion
785
- //#region src/auth/token-storage.ts
786
- const REFRESH_LOCK_RETRY_MS$1 = 100;
787
- const REFRESH_LOCK_STALE_MS$1 = 3e4;
788
- const REFRESH_LOCK_WAIT_TIMEOUT_MS$1 = 25e3;
789
- const EMPTY_AUTH_CONTEXT = {
790
- activeWorkspaceId: null,
791
- workspaces: {}
792
- };
793
- const UNKNOWN_WORKSPACE_NAME = "Unknown workspace";
794
- function getAuthContextFilePath(authFilePath) {
795
- const extension = path.extname(authFilePath);
796
- if (!extension) return `${authFilePath}.context.json`;
797
- return `${authFilePath.slice(0, -extension.length)}.context${extension}`;
798
- }
799
- function findLatestValidTokens(allCredentials) {
800
- for (let i = allCredentials.length - 1; i >= 0; i -= 1) {
801
- const credential = allCredentials[i];
802
- if (!credential) continue;
803
- if (typeof credential.workspaceId !== "string" || credential.workspaceId.length === 0 || typeof credential.token !== "string" || credential.token.length === 0 || typeof credential.refreshToken !== "string" || credential.refreshToken.length === 0) continue;
804
- return {
805
- workspaceId: credential.workspaceId,
806
- accessToken: credential.token,
807
- refreshToken: credential.refreshToken
808
- };
809
- }
810
- return null;
811
- }
812
- function storedCredentialToTokens(credential) {
813
- if (!credential) return null;
814
- if (typeof credential.workspaceId !== "string" || credential.workspaceId.length === 0 || typeof credential.token !== "string" || credential.token.length === 0 || typeof credential.refreshToken !== "string" || credential.refreshToken.length === 0) return null;
815
- return {
816
- workspaceId: credential.workspaceId,
817
- accessToken: credential.token,
818
- refreshToken: credential.refreshToken
819
- };
820
- }
821
- function findTokensForWorkspace(allCredentials, workspaceId) {
822
- return storedCredentialToTokens(allCredentials.find((credential) => credential?.workspaceId === workspaceId)) ?? null;
823
- }
824
- function tokensEqual(a, b) {
825
- return a?.workspaceId === b?.workspaceId && a?.accessToken === b?.accessToken && a?.refreshToken === b?.refreshToken;
826
- }
827
- function sleep$2(ms, signal) {
828
- signal?.throwIfAborted();
829
- return new Promise((resolve, reject) => {
830
- const onAbort = () => {
831
- clearTimeout(timeout);
832
- reject(signal?.reason);
833
- };
834
- const timeout = setTimeout(() => {
835
- signal?.removeEventListener("abort", onAbort);
836
- resolve();
837
- }, ms);
838
- signal?.addEventListener("abort", onAbort, { once: true });
134
+ function sleep$2(ms, signal) {
135
+ signal?.throwIfAborted();
136
+ return new Promise((resolve, reject) => {
137
+ const onAbort = () => {
138
+ clearTimeout(timeout);
139
+ reject(signal?.reason);
140
+ };
141
+ const timeout = setTimeout(() => {
142
+ signal?.removeEventListener("abort", onAbort);
143
+ resolve();
144
+ }, ms);
145
+ signal?.addEventListener("abort", onAbort, { once: true });
839
146
  });
840
147
  }
841
148
  var FileTokenStorage = class {
@@ -1731,41 +1038,737 @@ function environmentServiceToken(env) {
1731
1038
  function environmentCredentialInForce(env) {
1732
1039
  return environmentServiceToken(env) !== void 0;
1733
1040
  }
1734
- //#endregion
1735
- //#region src/commands/auth/agent-setup-tip.ts
1736
- /**
1737
- * Port of the legacy shell's post-login agent-setup tip (the real-mode
1738
- * path of `resolveAgentSetupTipCommand` in controllers/auth.ts). The
1739
- * legacy --json / --quiet / stderr-TTY suppressions do not translate:
1740
- * the engine's format selection already keeps the tip line out of json
1741
- * output, and handlers cannot read TTY-ness or the interactive flag —
1742
- * both recorded in the S2 parity divergence list. CI suppression is
1743
- * kept via ctx.env.
1744
- */
1745
- async function resolveAgentSetupTipCommand(ctx) {
1746
- if (ctx.env.CI) return null;
1747
- if (!await isLikelyProjectDirectory({
1748
- cwd: ctx.cwd,
1749
- signal: ctx.signal
1750
- })) return null;
1751
- const stateStore = new LocalStateStore(resolveStateDir({
1752
- env: ctx.env,
1753
- cwd: ctx.cwd
1754
- }), ctx.signal);
1755
- if (!shouldOfferPrismaAgentSetup(await readPrismaAgentSetupStatus({
1756
- cwd: ctx.cwd,
1757
- stateStore,
1758
- signal: ctx.signal
1759
- }))) return null;
1760
- return await resolvePrismaCliPackageCommand({
1761
- cwd: ctx.cwd,
1762
- signal: ctx.signal,
1763
- args: PRISMA_AGENT_INSTALL_ARGS
1764
- });
1041
+ const PRISMA_CLI_PACKAGE_SPEC = `@prisma/cli@next`;
1042
+ const DEFAULT_PRISMA_CLI_PACKAGE_RUNNER = ["npx", "-y"];
1043
+ const PRISMA_CLI_BINARY = CLI_NAME;
1044
+ function formatPrismaCliCommand(args, options = {}) {
1045
+ return [...getPrismaCliCommandPrefix(options), ...args].join(" ");
1046
+ }
1047
+ function getPrismaCliCommandPrefix({ invocation = "package", packageRunner = DEFAULT_PRISMA_CLI_PACKAGE_RUNNER }) {
1048
+ if (invocation === "binary") return [PRISMA_CLI_BINARY];
1049
+ return [...packageRunner, PRISMA_CLI_PACKAGE_SPEC];
1765
1050
  }
1766
1051
  //#endregion
1767
- //#region src/commands/auth/credential-card.ts
1768
- const ENVIRONMENT_CREDENTIAL_NOTICE = `${SERVICE_TOKEN_ENV_VAR} supplies the credential in force; unset it to use your stored workspace sessions.`;
1052
+ //#region src/lib/agent/package-manager.ts
1053
+ const LOCKFILE_PACKAGE_MANAGERS = [
1054
+ {
1055
+ packageManager: "bun",
1056
+ fileNames: ["bun.lock", "bun.lockb"]
1057
+ },
1058
+ {
1059
+ packageManager: "pnpm",
1060
+ fileNames: ["pnpm-lock.yaml", "pnpm-workspace.yaml"]
1061
+ },
1062
+ {
1063
+ packageManager: "yarn",
1064
+ fileNames: ["yarn.lock"]
1065
+ },
1066
+ {
1067
+ packageManager: "npm",
1068
+ fileNames: ["package-lock.json", "npm-shrinkwrap.json"]
1069
+ }
1070
+ ];
1071
+ async function resolvePackageRunner(options) {
1072
+ options.signal.throwIfAborted();
1073
+ const packageManager = detectPackageManagerSync(options.cwd, options.signal) ?? "npm";
1074
+ options.signal.throwIfAborted();
1075
+ return packageRunnerForPackageManager(packageManager);
1076
+ }
1077
+ function detectPackageManagerSync(cwd, signal) {
1078
+ let directory = path.resolve(cwd);
1079
+ while (true) {
1080
+ signal?.throwIfAborted();
1081
+ const packageJsonManager = readPackageJsonPackageManager(directory);
1082
+ if (packageJsonManager) return packageJsonManager;
1083
+ const lockfileManager = readLockfilePackageManager(directory, signal);
1084
+ if (lockfileManager) return lockfileManager;
1085
+ const parent = path.dirname(directory);
1086
+ if (parent === directory) return null;
1087
+ directory = parent;
1088
+ }
1089
+ }
1090
+ function readPackageJsonPackageManager(directory) {
1091
+ const packageJsonPath = path.join(directory, "package.json");
1092
+ let content;
1093
+ try {
1094
+ content = readFileSync(packageJsonPath, "utf8");
1095
+ } catch (error) {
1096
+ if (isMissingFileError(error)) return null;
1097
+ throw error;
1098
+ }
1099
+ try {
1100
+ return parsePackageManager(JSON.parse(content).packageManager);
1101
+ } catch {
1102
+ return null;
1103
+ }
1104
+ }
1105
+ function readLockfilePackageManager(directory, signal) {
1106
+ for (const candidate of LOCKFILE_PACKAGE_MANAGERS) for (const fileName of candidate.fileNames) {
1107
+ signal?.throwIfAborted();
1108
+ if (fileExists(path.join(directory, fileName))) return candidate.packageManager;
1109
+ }
1110
+ return null;
1111
+ }
1112
+ function fileExists(filePath) {
1113
+ try {
1114
+ return statSync(filePath).isFile();
1115
+ } catch (error) {
1116
+ if (isMissingFileError(error)) return false;
1117
+ throw error;
1118
+ }
1119
+ }
1120
+ function parsePackageManager(value) {
1121
+ if (typeof value !== "string") return null;
1122
+ const normalized = value.trim().toLowerCase();
1123
+ if (normalized === "bun" || normalized.startsWith("bun@")) return "bun";
1124
+ if (normalized === "pnpm" || normalized.startsWith("pnpm@")) return "pnpm";
1125
+ if (normalized === "yarn" || normalized.startsWith("yarn@")) return "yarn";
1126
+ if (normalized === "npm" || normalized.startsWith("npm@")) return "npm";
1127
+ return null;
1128
+ }
1129
+ function packageRunnerForPackageManager(packageManager) {
1130
+ switch (packageManager) {
1131
+ case "bun": return ["bunx"];
1132
+ case "pnpm": return ["pnpm", "dlx"];
1133
+ case "yarn": return ["yarn", "dlx"];
1134
+ case "npm": return ["npx", "-y"];
1135
+ }
1136
+ }
1137
+ function isMissingFileError(error) {
1138
+ const code = error.code;
1139
+ return code === "ENOENT" || code === "ENOTDIR";
1140
+ }
1141
+ //#endregion
1142
+ //#region src/lib/agent/cli-command.ts
1143
+ async function resolvePrismaCliPackageCommandFormatter(options) {
1144
+ return createPrismaCliPackageCommandFormatter(await resolvePackageRunner(options));
1145
+ }
1146
+ async function resolvePrismaCliPackageCommand(options) {
1147
+ return (await resolvePrismaCliPackageCommandFormatter(options))(options.args);
1148
+ }
1149
+ function createPrismaCliPackageCommandFormatter(packageRunner) {
1150
+ return (args) => formatPrismaCliCommand(args, { packageRunner });
1151
+ }
1152
+ //#endregion
1153
+ //#region src/lib/semver-order.ts
1154
+ function parseVersion(version) {
1155
+ const match = VERSION_PATTERN.exec(version);
1156
+ if (!match) return null;
1157
+ return {
1158
+ major: Number(match[1]),
1159
+ minor: Number(match[2]),
1160
+ patch: Number(match[3]),
1161
+ prerelease: match[4]?.split(".") ?? []
1162
+ };
1163
+ }
1164
+ const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
1165
+ const NUMERIC_PART = /^\d+$/;
1166
+ /** Negative when left is older, positive when newer, 0 when equal, and
1167
+ * null when either side is not a version this understands. */
1168
+ function compareVersionStrings(left, right) {
1169
+ const parsedLeft = parseVersion(left);
1170
+ const parsedRight = parseVersion(right);
1171
+ if (!parsedLeft || !parsedRight) return null;
1172
+ return compareVersions(parsedLeft, parsedRight);
1173
+ }
1174
+ function compareVersions(left, right) {
1175
+ for (const key of [
1176
+ "major",
1177
+ "minor",
1178
+ "patch"
1179
+ ]) {
1180
+ const diff = left[key] - right[key];
1181
+ if (diff !== 0) return diff;
1182
+ }
1183
+ return comparePrerelease(left.prerelease, right.prerelease);
1184
+ }
1185
+ function comparePrerelease(left, right) {
1186
+ if (left.length === 0 && right.length === 0) return 0;
1187
+ if (left.length === 0) return 1;
1188
+ if (right.length === 0) return -1;
1189
+ const count = Math.max(left.length, right.length);
1190
+ for (let index = 0; index < count; index += 1) {
1191
+ const leftPart = left[index];
1192
+ const rightPart = right[index];
1193
+ if (leftPart === void 0) return -1;
1194
+ if (rightPart === void 0) return 1;
1195
+ const diff = comparePrereleasePart(leftPart, rightPart);
1196
+ if (diff !== 0) return diff;
1197
+ }
1198
+ return 0;
1199
+ }
1200
+ function comparePrereleasePart(left, right) {
1201
+ const leftNumber = NUMERIC_PART.test(left) ? Number(left) : null;
1202
+ const rightNumber = NUMERIC_PART.test(right) ? Number(right) : null;
1203
+ if (leftNumber !== null && rightNumber !== null) return leftNumber - rightNumber;
1204
+ if (leftNumber !== null) return -1;
1205
+ if (rightNumber !== null) return 1;
1206
+ return left.localeCompare(right);
1207
+ }
1208
+ //#endregion
1209
+ //#region src/lib/skills/allowlist.ts
1210
+ /**
1211
+ * SECURITY INVARIANT — read before changing this list.
1212
+ *
1213
+ * Skill content only ever comes from the packages named here. The sync
1214
+ * command never scans node_modules, or any other directory, looking for
1215
+ * skills to install, and no discovery mode may be added: a skill is
1216
+ * instructions an agent will follow, so installing one from an
1217
+ * arbitrary transitive dependency hands that dependency's author
1218
+ * influence over the user's agent. Resolving these names keeps the
1219
+ * trust boundary identical to the code's — if you run
1220
+ * `@prisma/orm-postgres`, you already trust its author.
1221
+ *
1222
+ * Adding a package here is a deliberate decision about a package Prisma
1223
+ * publishes. This is permanent.
1224
+ */
1225
+ const SKILL_SOURCE_PACKAGES = [
1226
+ "@prisma/orm-postgres",
1227
+ "@prisma/orm-sqlite",
1228
+ "@prisma/orm-mongo",
1229
+ "@prisma/composer"
1230
+ ];
1231
+ /** The directory inside a source package's tarball that holds its skill
1232
+ * trees, one directory per skill. */
1233
+ const PACKAGE_SKILLS_DIR = "skills";
1234
+ /**
1235
+ * The agent harnesses this CLI can install skills for, each mapped to
1236
+ * the project-relative directory that harness reads its skills from.
1237
+ * There is no harness detection anywhere: which of these a project uses
1238
+ * comes from `skills: { agents: [...] }` in prisma.config.ts, and every
1239
+ * one of them when the config says nothing.
1240
+ */
1241
+ const AGENT_SKILL_DIRS = {
1242
+ claude: ".claude/skills",
1243
+ cursor: ".cursor/skills",
1244
+ agents: ".agents/skills",
1245
+ devin: ".devin/skills"
1246
+ };
1247
+ const KNOWN_AGENTS = Object.keys(AGENT_SKILL_DIRS);
1248
+ /** Without a config, sync writes every known agent's directory, so a
1249
+ * harness adopted later finds the skills already there. */
1250
+ const DEFAULT_AGENTS = KNOWN_AGENTS;
1251
+ function isKnownAgent(name) {
1252
+ return name in AGENT_SKILL_DIRS;
1253
+ }
1254
+ function agentSkillDirs(agents) {
1255
+ return agents.map((agent) => AGENT_SKILL_DIRS[agent]);
1256
+ }
1257
+ function isSkillSourcePackage(name) {
1258
+ return SKILL_SOURCE_PACKAGES.includes(name);
1259
+ }
1260
+ //#endregion
1261
+ //#region src/lib/skills/unquote.ts
1262
+ const QUOTED = /^(["'])(.*)\1$/;
1263
+ /** Strips one layer of matching single or double quotes. */
1264
+ function unquote(value) {
1265
+ return QUOTED.exec(value)?.[2] ?? value;
1266
+ }
1267
+ //#endregion
1268
+ //#region src/lib/skills/frontmatter.ts
1269
+ const LINE_BREAK = /\r?\n/;
1270
+ const INDENTED = /^[ \t]/;
1271
+ const EMPTY_STAMP = {
1272
+ library: null,
1273
+ libraryVersion: null
1274
+ };
1275
+ const METADATA_KEY = "metadata";
1276
+ const STAMP_KEYS = new Map([["library", "library"], ["library_version", "libraryVersion"]]);
1277
+ /**
1278
+ * The `library` and `library_version` entries of a SKILL.md's
1279
+ * `metadata` map. The Agent Skills spec defines no custom top-level
1280
+ * frontmatter keys — extensions live under `metadata`, a map of strings
1281
+ * — so the stamp is read there and nowhere else. A file without
1282
+ * frontmatter, without a `metadata` map, or without those entries
1283
+ * reports nulls rather than failing.
1284
+ *
1285
+ * Contract: this reads only the stamp this CLI writes. A file it
1286
+ * cannot read classifies as unmanaged, which sync refuses to touch, so
1287
+ * a parse bug can never delete user files.
1288
+ */
1289
+ function parseSkillStamp(source) {
1290
+ const lines = source.split(LINE_BREAK);
1291
+ if (lines[0]?.trim() !== "---") return EMPTY_STAMP;
1292
+ const stamp = {
1293
+ library: null,
1294
+ libraryVersion: null
1295
+ };
1296
+ let inMetadata = false;
1297
+ for (const line of lines.slice(1)) {
1298
+ if (line.trim() === "---") break;
1299
+ if (line.trim() === "") continue;
1300
+ if (!INDENTED.test(line)) {
1301
+ inMetadata = keyOf(line) === METADATA_KEY;
1302
+ continue;
1303
+ }
1304
+ if (!inMetadata) continue;
1305
+ const field = STAMP_KEYS.get(keyOf(line) ?? "");
1306
+ if (field) stamp[field] = valueAfterKey(line);
1307
+ }
1308
+ return stamp;
1309
+ }
1310
+ async function readSkillStamp(path) {
1311
+ try {
1312
+ return parseSkillStamp(await readFile(path, "utf8"));
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ }
1317
+ function keyOf(line) {
1318
+ const separator = line.indexOf(":");
1319
+ return separator === -1 ? null : line.slice(0, separator).trim();
1320
+ }
1321
+ function valueAfterKey(line) {
1322
+ const separator = line.indexOf(":");
1323
+ return unquote(line.slice(separator + 1).trim());
1324
+ }
1325
+ //#endregion
1326
+ //#region src/lib/skills/opt-out.ts
1327
+ /**
1328
+ * The project's persisted answer to the staleness check, written by
1329
+ * `skills sync --disable` and read by the check on every command. It
1330
+ * sits at the project root beside the CLI's other local state, so the
1331
+ * opt-out follows the project rather than one machine's environment.
1332
+ */
1333
+ const SKILLS_STATE_FILE = path.join(".prisma", "skills.json");
1334
+ function skillsStatePath(projectRoot) {
1335
+ return path.join(projectRoot, SKILLS_STATE_FILE);
1336
+ }
1337
+ async function readSkillsCheckDisabled(projectRoot) {
1338
+ try {
1339
+ return JSON.parse(await readFile(skillsStatePath(projectRoot), "utf8")).check === false;
1340
+ } catch {
1341
+ return false;
1342
+ }
1343
+ }
1344
+ async function writeSkillsCheckDisabled(projectRoot, disabled) {
1345
+ const target = skillsStatePath(projectRoot);
1346
+ await mkdir(path.dirname(target), { recursive: true });
1347
+ await writeFile(target, `${JSON.stringify({ check: !disabled }, null, 2)}\n`, "utf8");
1348
+ }
1349
+ //#endregion
1350
+ //#region src/lib/skills/resolve.ts
1351
+ /**
1352
+ * Standard module resolution of one named package from one directory.
1353
+ * Under Yarn PnP this goes through the PnP resolver and answers a path
1354
+ * inside a zip, which the patched filesystem reads like any other.
1355
+ */
1356
+ async function resolvePackage(fromDir, packageName) {
1357
+ const dir = resolvePackageDir(fromDir, packageName);
1358
+ if (dir === null) return null;
1359
+ const version = await readPackageVersion(path.join(dir, "package.json"));
1360
+ return version === null ? null : {
1361
+ name: packageName,
1362
+ version,
1363
+ dir,
1364
+ resolvedFrom: fromDir
1365
+ };
1366
+ }
1367
+ function resolvePackageDir(fromDir, packageName) {
1368
+ const requireFrom = createRequire(path.join(fromDir, "package.json"));
1369
+ try {
1370
+ return path.dirname(requireFrom.resolve(`${packageName}/package.json`));
1371
+ } catch {}
1372
+ try {
1373
+ return packageRootOf(requireFrom.resolve(packageName), packageName);
1374
+ } catch {
1375
+ return null;
1376
+ }
1377
+ }
1378
+ /** Walks up from a resolved entry point to the directory named by the
1379
+ * package specifier — the last `node_modules/<name>` segment on the
1380
+ * path, or the first ancestor holding a package.json with that name. */
1381
+ function packageRootOf(entry, packageName) {
1382
+ const marker = `${path.sep}node_modules${path.sep}${packageName.split("/").join(path.sep)}`;
1383
+ const at = entry.lastIndexOf(marker);
1384
+ if (at !== -1) return entry.slice(0, at + marker.length);
1385
+ let dir = path.dirname(entry);
1386
+ for (;;) {
1387
+ if (path.basename(dir) === path.basename(packageName)) return dir;
1388
+ const parent = path.dirname(dir);
1389
+ if (parent === dir) return null;
1390
+ dir = parent;
1391
+ }
1392
+ }
1393
+ async function readPackageVersion(manifestPath) {
1394
+ try {
1395
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1396
+ return typeof manifest.version === "string" ? manifest.version : null;
1397
+ } catch {
1398
+ return null;
1399
+ }
1400
+ }
1401
+ //#endregion
1402
+ //#region src/lib/skills/workspace-members.ts
1403
+ /**
1404
+ * The workspace member directories declared by the root's workspace
1405
+ * config, expanded from its globs by @manypkg/tools. Enumeration reads
1406
+ * the declared globs from plain files (pnpm-workspace.yaml or
1407
+ * package.json) and its glob expansion ignores node_modules, so a
1408
+ * package is resolvable from a member directory only because the user
1409
+ * declared that member — never because something was found by scanning.
1410
+ *
1411
+ * PnpmTool covers pnpm; YarnTool reads the package.json `workspaces`
1412
+ * field in both its array and `{ packages }` forms, which is how npm,
1413
+ * Yarn (Plug'n'Play included — the globs live in plain files), and bun
1414
+ * all declare their members. Only directories holding a package.json
1415
+ * come back.
1416
+ */
1417
+ async function workspaceMemberDirs(root) {
1418
+ const dirs = /* @__PURE__ */ new Set();
1419
+ if (await hasPnpmWorkspace(root)) for (const dir of await memberDirsVia(PnpmTool, root)) dirs.add(dir);
1420
+ if (await hasPackageJsonWorkspaces(root)) for (const dir of await memberDirsVia(YarnTool, root)) dirs.add(dir);
1421
+ dirs.delete(path.resolve(root));
1422
+ return [...dirs].sort();
1423
+ }
1424
+ async function memberDirsVia(tool, root) {
1425
+ try {
1426
+ const { packages } = await tool.getPackages(root);
1427
+ return packages.map((pkg) => path.resolve(pkg.dir));
1428
+ } catch {
1429
+ return [];
1430
+ }
1431
+ }
1432
+ async function hasPnpmWorkspace(root) {
1433
+ try {
1434
+ await readFile(path.join(root, "pnpm-workspace.yaml"), "utf8");
1435
+ return true;
1436
+ } catch {
1437
+ return false;
1438
+ }
1439
+ }
1440
+ async function hasPackageJsonWorkspaces(root) {
1441
+ try {
1442
+ return JSON.parse(await readFile(path.join(root, "package.json"), "utf8")).workspaces !== void 0;
1443
+ } catch {
1444
+ return false;
1445
+ }
1446
+ }
1447
+ //#endregion
1448
+ //#region src/lib/skills/status.ts
1449
+ async function readSkillsStatus(cwd, options) {
1450
+ const projectRoot = path.resolve(cwd);
1451
+ const dirs = agentSkillDirs(options?.agents ?? DEFAULT_AGENTS);
1452
+ const checkDisabled = options?.checkDisabled ?? await readSkillsCheckDisabled(projectRoot);
1453
+ const packages = await findInstalledSourcePackages(projectRoot);
1454
+ const sources = await collectSkillSources(packages);
1455
+ const skills = [];
1456
+ for (const source of sources.values()) skills.push(await readSkillStatus(projectRoot, dirs, source));
1457
+ skills.sort((left, right) => left.skill.localeCompare(right.skill));
1458
+ return {
1459
+ projectRoot,
1460
+ checkDisabled,
1461
+ packages,
1462
+ skills,
1463
+ orphans: options?.orphans === false ? [] : await findOrphanedSkills(projectRoot, dirs, new Set(sources.keys())),
1464
+ upToDate: skills.every((skill) => skill.upToDate)
1465
+ };
1466
+ }
1467
+ /**
1468
+ * The allowlisted packages installed in this project, resolved by name
1469
+ * from the project root and from each declared workspace member. Never
1470
+ * a directory scan.
1471
+ */
1472
+ async function findInstalledSourcePackages(projectRoot) {
1473
+ const searchDirs = [projectRoot, ...await workspaceMemberDirs(projectRoot)];
1474
+ const found = [];
1475
+ for (const name of SKILL_SOURCE_PACKAGES) {
1476
+ const resolutions = [];
1477
+ for (const dir of searchDirs) {
1478
+ const resolved = await resolvePackage(dir, name);
1479
+ if (resolved !== null) resolutions.push(resolved);
1480
+ }
1481
+ if (resolutions.length === 0) continue;
1482
+ const highest = resolutions.reduce((best, candidate) => (compareVersionStrings(candidate.version, best.version) ?? 0) > 0 ? candidate : best);
1483
+ const versions = [...new Set(resolutions.map((one) => one.version))].sort();
1484
+ found.push({
1485
+ name,
1486
+ version: highest.version,
1487
+ dir: highest.dir,
1488
+ conflictingVersions: versions.length > 1 ? versions : []
1489
+ });
1490
+ }
1491
+ return found;
1492
+ }
1493
+ /** Every skill tree the installed source packages ship, keyed by skill
1494
+ * name. When two packages ship the same skill, the higher version
1495
+ * wins — the public packages version in lockstep, so this only
1496
+ * arbitrates a half-finished upgrade. */
1497
+ async function collectSkillSources(packages) {
1498
+ const sources = /* @__PURE__ */ new Map();
1499
+ for (const installed of packages) {
1500
+ const skillsDir = path.join(installed.dir, PACKAGE_SKILLS_DIR);
1501
+ for (const skill of await skillDirectories(skillsDir)) {
1502
+ const existing = sources.get(skill);
1503
+ if (existing !== void 0 && (compareVersionStrings(installed.version, existing.version) ?? 0) <= 0) continue;
1504
+ sources.set(skill, {
1505
+ skill,
1506
+ library: installed.name,
1507
+ version: installed.version,
1508
+ dir: path.join(skillsDir, skill)
1509
+ });
1510
+ }
1511
+ }
1512
+ return sources;
1513
+ }
1514
+ async function readSkillStatus(projectRoot, dirs, source) {
1515
+ const targets = [];
1516
+ for (const dir of dirs) {
1517
+ const skillFile = path.join(projectRoot, dir, source.skill, "SKILL.md");
1518
+ const stamp = await readSkillStamp(skillFile);
1519
+ targets.push({
1520
+ dir,
1521
+ syncedVersion: stamp?.libraryVersion ?? null,
1522
+ state: await targetState(skillFile, stamp, source.version)
1523
+ });
1524
+ }
1525
+ return {
1526
+ skill: source.skill,
1527
+ library: source.library,
1528
+ version: source.version,
1529
+ sourceDir: source.dir,
1530
+ targets,
1531
+ upToDate: targets.every((target) => target.state === "synced" || target.state === "unmanaged")
1532
+ };
1533
+ }
1534
+ async function targetState(skillFile, stamp, sourceVersion) {
1535
+ if (stamp === null) return await missingFromDisk(skillFile) ? "absent" : "unmanaged";
1536
+ if (stamp.library === null || !isSkillSourcePackage(stamp.library)) return "unmanaged";
1537
+ return stamp.libraryVersion === sourceVersion ? "synced" : "stale";
1538
+ }
1539
+ async function missingFromDisk(target) {
1540
+ try {
1541
+ await stat(target);
1542
+ return false;
1543
+ } catch (error) {
1544
+ return error.code === "ENOENT";
1545
+ }
1546
+ }
1547
+ /**
1548
+ * Copies in the harness directories that this CLI installed — their
1549
+ * SKILL.md names an allowlisted package as its `library` — and that no
1550
+ * installed package still provides. A skill from anywhere else is
1551
+ * someone else's file and is never touched.
1552
+ */
1553
+ async function findOrphanedSkills(projectRoot, dirs, provided) {
1554
+ const orphans = /* @__PURE__ */ new Map();
1555
+ for (const dir of dirs) {
1556
+ const harnessDir = path.join(projectRoot, dir);
1557
+ for (const skill of await skillDirectories(harnessDir)) {
1558
+ if (provided.has(skill)) continue;
1559
+ const stamp = await readSkillStamp(path.join(harnessDir, skill, "SKILL.md"));
1560
+ if (stamp?.library === null || stamp === null) continue;
1561
+ if (!isSkillSourcePackage(stamp.library)) continue;
1562
+ const entry = orphans.get(skill) ?? {
1563
+ library: stamp.library,
1564
+ dirs: []
1565
+ };
1566
+ entry.dirs.push(dir);
1567
+ orphans.set(skill, entry);
1568
+ }
1569
+ }
1570
+ return [...orphans.entries()].map(([skill, entry]) => ({
1571
+ skill,
1572
+ library: entry.library,
1573
+ dirs: entry.dirs
1574
+ }));
1575
+ }
1576
+ /** The subdirectories of `dir` that hold a SKILL.md. */
1577
+ async function skillDirectories(dir) {
1578
+ let entries;
1579
+ try {
1580
+ entries = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1581
+ } catch {
1582
+ return [];
1583
+ }
1584
+ const skills = [];
1585
+ for (const name of entries.sort()) if (await isFile(path.join(dir, name, "SKILL.md"))) skills.push(name);
1586
+ return skills;
1587
+ }
1588
+ async function isFile(target) {
1589
+ try {
1590
+ return (await stat(target)).isFile();
1591
+ } catch {
1592
+ return false;
1593
+ }
1594
+ }
1595
+ //#endregion
1596
+ //#region src/commands/skills/config.ts
1597
+ const DEFAULT = {
1598
+ check: true,
1599
+ agents: DEFAULT_AGENTS,
1600
+ agentsConfigured: false
1601
+ };
1602
+ const SKILLS_CONFIG_SECTION_NAME = "skills";
1603
+ function invalidSection(value) {
1604
+ return {
1605
+ code: "SKILLS.CONFIG_INVALID",
1606
+ severity: "error",
1607
+ summary: `The 'skills' config section must be an object, and is ${describe(value)}.`,
1608
+ nextActions: [{
1609
+ kind: "user-choice",
1610
+ label: "Write skills: { check: false } to silence the skills check."
1611
+ }]
1612
+ };
1613
+ }
1614
+ function invalidCheck(value) {
1615
+ return {
1616
+ code: "SKILLS.CONFIG_INVALID",
1617
+ severity: "error",
1618
+ summary: `skills.check must be true or false, and is ${describe(value)}.`,
1619
+ nextActions: [{
1620
+ kind: "user-choice",
1621
+ label: "Set skills.check to true or false, or remove it."
1622
+ }]
1623
+ };
1624
+ }
1625
+ function invalidAgents(value) {
1626
+ return {
1627
+ code: "SKILLS.CONFIG_INVALID",
1628
+ severity: "error",
1629
+ summary: `skills.agents must be an array of agent names, and is ${describe(value)}.`,
1630
+ nextActions: [{
1631
+ kind: "user-choice",
1632
+ label: `List the agents to install skills for (${KNOWN_AGENTS.join(", ")}), or remove skills.agents to install for all of them.`
1633
+ }]
1634
+ };
1635
+ }
1636
+ function unknownAgent(name) {
1637
+ return {
1638
+ code: "SKILLS.CONFIG_INVALID",
1639
+ severity: "error",
1640
+ summary: `skills.agents names '${name}', which this CLI does not know. The known agents are ${KNOWN_AGENTS.join(", ")}.`,
1641
+ nextActions: [{
1642
+ kind: "user-choice",
1643
+ label: `Remove '${name}' from skills.agents, or update the CLI if a newer version knows it.`
1644
+ }]
1645
+ };
1646
+ }
1647
+ function describe(value) {
1648
+ return value === null ? "null" : typeof value;
1649
+ }
1650
+ function validateAgents(raw) {
1651
+ if (raw === void 0) return {
1652
+ ok: true,
1653
+ agents: DEFAULT_AGENTS
1654
+ };
1655
+ if (!Array.isArray(raw)) return {
1656
+ ok: false,
1657
+ diagnostic: invalidAgents(raw)
1658
+ };
1659
+ const agents = [];
1660
+ for (const entry of raw) {
1661
+ if (typeof entry !== "string" || !isKnownAgent(entry)) return {
1662
+ ok: false,
1663
+ diagnostic: typeof entry === "string" ? unknownAgent(entry) : invalidAgents(entry)
1664
+ };
1665
+ if (!agents.includes(entry)) agents.push(entry);
1666
+ }
1667
+ return {
1668
+ ok: true,
1669
+ agents
1670
+ };
1671
+ }
1672
+ /**
1673
+ * The validated skills section of an already-loaded config, or null
1674
+ * when the section does not validate. Used by code that runs outside a
1675
+ * command handler (the staleness check, the post-login tip), which has
1676
+ * no ctx.config.
1677
+ *
1678
+ * Null deliberately collapses "no config" and "config invalid": both
1679
+ * callers fall back to the default agent set, so a broken config never
1680
+ * silences the check. The commands that consume the config surface the
1681
+ * validation error themselves.
1682
+ */
1683
+ function readSkillsConfig(loaded) {
1684
+ const section = skillsConfigSection.validate(loaded.sections[SKILLS_CONFIG_SECTION_NAME]);
1685
+ return section.ok ? section.value : null;
1686
+ }
1687
+ /**
1688
+ * The project's skills settings from prisma.config.ts, or null when no
1689
+ * config file exists — decided with one stat, so a project without a
1690
+ * config never pays the file's TypeScript transpile — or the section
1691
+ * does not validate.
1692
+ */
1693
+ async function readProjectSkillsConfig(cwd, configPath) {
1694
+ if (!existsSync(configPath === void 0 ? path.join(cwd, "prisma.config.ts") : path.resolve(cwd, configPath))) return null;
1695
+ return readSkillsConfig(await loadConfig(cwd, configPath));
1696
+ }
1697
+ const skillsConfigSection = defineConfigSection({
1698
+ name: SKILLS_CONFIG_SECTION_NAME,
1699
+ validate: (raw) => {
1700
+ if (raw === void 0) return {
1701
+ ok: true,
1702
+ value: DEFAULT,
1703
+ diagnostics: []
1704
+ };
1705
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {
1706
+ ok: false,
1707
+ diagnostics: [invalidSection(raw)]
1708
+ };
1709
+ let check;
1710
+ let rawAgents;
1711
+ try {
1712
+ check = raw.check;
1713
+ rawAgents = raw.agents;
1714
+ } catch {
1715
+ return {
1716
+ ok: false,
1717
+ diagnostics: [invalidSection(raw)]
1718
+ };
1719
+ }
1720
+ if (check !== void 0 && typeof check !== "boolean") return {
1721
+ ok: false,
1722
+ diagnostics: [invalidCheck(check)]
1723
+ };
1724
+ const agents = validateAgents(rawAgents);
1725
+ if (!agents.ok) return {
1726
+ ok: false,
1727
+ diagnostics: [agents.diagnostic]
1728
+ };
1729
+ return {
1730
+ ok: true,
1731
+ value: {
1732
+ check: check ?? true,
1733
+ agents: agents.agents,
1734
+ agentsConfigured: rawAgents !== void 0
1735
+ },
1736
+ diagnostics: []
1737
+ };
1738
+ }
1739
+ });
1740
+ //#endregion
1741
+ //#region src/commands/auth/agent-setup-tip.ts
1742
+ /**
1743
+ * The post-login skills tip. Login is the moment a developer sets a
1744
+ * project up, so it points at `skills sync` when the project's synced
1745
+ * agent skills do not match its installed Prisma packages. Silent in
1746
+ * CI, in a directory with no skill-bearing Prisma packages, when the
1747
+ * copies are current, and when the check is opted out.
1748
+ */
1749
+ const SKILLS_SYNC_ARGS = ["skills", "sync"];
1750
+ async function resolveAgentSetupTipCommand(ctx) {
1751
+ if (ctx.env.CI) return null;
1752
+ try {
1753
+ const config = await readProjectSkillsConfig(ctx.cwd);
1754
+ if (config !== null && !config.check) return null;
1755
+ const status = await readSkillsStatus(ctx.cwd, {
1756
+ orphans: false,
1757
+ agents: config?.agents
1758
+ });
1759
+ if (status.packages.length === 0 || status.upToDate || status.checkDisabled) return null;
1760
+ return await resolvePrismaCliPackageCommand({
1761
+ cwd: ctx.cwd,
1762
+ signal: ctx.signal,
1763
+ args: SKILLS_SYNC_ARGS
1764
+ });
1765
+ } catch {
1766
+ return null;
1767
+ }
1768
+ }
1769
+ //#endregion
1770
+ //#region src/commands/auth/credential-card.ts
1771
+ const ENVIRONMENT_CREDENTIAL_NOTICE = `${SERVICE_TOKEN_ENV_VAR} supplies the credential in force; unset it to use your stored workspace sessions.`;
1769
1772
  /** The card rows for the active credential, or the signed-out row when
1770
1773
  * there is none. A credential nothing names — an environment token
1771
1774
  * whose claims carry no workspace — has no workspace row at all. */
@@ -2150,7 +2153,7 @@ function serializeWorkspaceList(result) {
2150
2153
  count: result.sessions.length
2151
2154
  };
2152
2155
  }
2153
- function listPresentations$8(result) {
2156
+ function listPresentations$9(result) {
2154
2157
  const columns = [
2155
2158
  "name",
2156
2159
  "id",
@@ -2201,7 +2204,7 @@ const authWorkspaceListCommand = defineCommand({
2201
2204
  selectedWorkspaceId: stored.selectedWorkspaceId,
2202
2205
  environmentCredentialInForce: environmentCredentialInForce(ctx.env)
2203
2206
  };
2204
- return ok(ctx.present({ data: result }, listPresentations$8(result)));
2207
+ return ok(ctx.present({ data: result }, listPresentations$9(result)));
2205
2208
  }
2206
2209
  });
2207
2210
  //#endregion
@@ -2416,13 +2419,13 @@ function usageError(summary, why, fix, nextSteps = [], domain = "cli") {
2416
2419
  nextSteps
2417
2420
  });
2418
2421
  }
2419
- function authRequiredError(nextSteps = ["prisma-cli auth login"], options = {}) {
2422
+ function authRequiredError(nextSteps = ["prisma auth login"], options = {}) {
2420
2423
  return new CliError({
2421
2424
  code: "AUTH_REQUIRED",
2422
2425
  domain: "auth",
2423
2426
  summary: "Authentication required",
2424
2427
  why: "This command needs an authenticated session.",
2425
- fix: "Run prisma-cli auth login, or rerun the command in a TTY to sign in interactively.",
2428
+ fix: "Run prisma auth login, or rerun the command in a TTY to sign in interactively.",
2426
2429
  debug: options.debug,
2427
2430
  exitCode: 1,
2428
2431
  nextSteps
@@ -2482,6 +2485,31 @@ function branchApiError(summary, response, error) {
2482
2485
  });
2483
2486
  }
2484
2487
  //#endregion
2488
+ //#region src/lib/project/prisma-dir.ts
2489
+ /**
2490
+ * Walks up from cwd to the nearest directory containing a `.prisma/`
2491
+ * directory and returns that directory, or null when no ancestor has
2492
+ * one. Pure filesystem check — no config file is read or evaluated.
2493
+ * Nearest wins by design: a nested directory deliberately linked to a
2494
+ * different project beats the repo root.
2495
+ */
2496
+ async function findNearestPrismaDir(cwd) {
2497
+ let dir = path.resolve(cwd);
2498
+ for (;;) {
2499
+ if (await isDirectory(path.join(dir, ".prisma"))) return dir;
2500
+ const parent = path.dirname(dir);
2501
+ if (parent === dir) return null;
2502
+ dir = parent;
2503
+ }
2504
+ }
2505
+ async function isDirectory(candidate) {
2506
+ try {
2507
+ return (await stat(candidate)).isDirectory();
2508
+ } catch {
2509
+ return false;
2510
+ }
2511
+ }
2512
+ //#endregion
2485
2513
  //#region src/lib/project/local-pin.ts
2486
2514
  const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
2487
2515
  var LocalResolutionPinInvalidJsonError = class extends TaggedError("LocalResolutionPinInvalidJsonError")() {
@@ -2557,16 +2585,21 @@ var LocalResolutionPinGitignoreUpdateFailedError = class extends TaggedError("Lo
2557
2585
  });
2558
2586
  }
2559
2587
  };
2588
+ /** Reads the pin at the nearest ancestor with a `.prisma/` directory;
2589
+ * without one anywhere up the tree, reads at cwd and finds nothing.
2590
+ * Writes never walk — only discovery does. */
2560
2591
  async function readLocalResolutionPin(cwd, signal) {
2561
2592
  return Result.gen(async function* () {
2562
2593
  yield* ensureLocalResolutionPinReadNotAborted(signal);
2563
- const file = yield* Result.await(readLocalResolutionPinFile(cwd, signal));
2594
+ const directory = await findNearestPrismaDir(cwd) ?? cwd;
2595
+ const file = yield* Result.await(readLocalResolutionPinFile(directory, signal));
2564
2596
  if (file.kind === "missing") return Result.ok({ kind: "missing" });
2565
2597
  const parsed = yield* parseLocalResolutionPin(file.raw);
2566
2598
  if (!isLocalResolutionPin(parsed)) return Result.err(new LocalResolutionPinInvalidShapeError());
2567
2599
  return Result.ok({
2568
2600
  kind: "present",
2569
- pin: parsed
2601
+ pin: parsed,
2602
+ directory
2570
2603
  });
2571
2604
  });
2572
2605
  }
@@ -2811,7 +2844,7 @@ var LocalProjectWorkspaceMismatchError = class extends TaggedError("LocalProject
2811
2844
  };
2812
2845
  var ProjectSetupRequiredError = class extends TaggedError("ProjectSetupRequiredError")() {
2813
2846
  constructor(options) {
2814
- const commandLabel = options.commandName ? `prisma-cli ${options.commandName}` : "this command";
2847
+ const commandLabel = options.commandName ? `prisma ${options.commandName}` : "this command";
2815
2848
  super({
2816
2849
  message: `This directory is not linked to a Prisma Project, and ${commandLabel} will not choose one from package or directory names.`,
2817
2850
  commandName: options.commandName,
@@ -2862,9 +2895,9 @@ function projectNotFoundCliError(projectRef, workspace) {
2862
2895
  domain: "project",
2863
2896
  summary: "Project not found",
2864
2897
  why: `The project "${projectRef}" does not exist in workspace "${workspace.name}" or is not accessible.`,
2865
- fix: "Pass a project id or name from prisma-cli project list.",
2898
+ fix: "Pass a project id or name from prisma project list.",
2866
2899
  exitCode: 1,
2867
- nextSteps: ["prisma-cli project list"]
2900
+ nextSteps: ["prisma project list"]
2868
2901
  });
2869
2902
  }
2870
2903
  function projectAmbiguousError(projectRef, matches) {
@@ -2872,8 +2905,8 @@ function projectAmbiguousError(projectRef, matches) {
2872
2905
  }
2873
2906
  function projectAmbiguousCliError(projectRef, matches) {
2874
2907
  const firstMatch = matches[0];
2875
- const nextSteps = ["prisma-cli project list"];
2876
- if (firstMatch) nextSteps.push(`prisma-cli project link ${firstMatch.id}`);
2908
+ const nextSteps = ["prisma project list"];
2909
+ if (firstMatch) nextSteps.push(`prisma project link ${firstMatch.id}`);
2877
2910
  return new CliError({
2878
2911
  code: "PROJECT_AMBIGUOUS",
2879
2912
  domain: "project",
@@ -2897,7 +2930,7 @@ function localStateStaleCliError() {
2897
2930
  fix: `Delete ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}, then choose a Project explicitly.`,
2898
2931
  meta: { pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH },
2899
2932
  exitCode: 1,
2900
- nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"]
2933
+ nextSteps: ["prisma project list", "prisma project link <id-or-name>"]
2901
2934
  });
2902
2935
  }
2903
2936
  function localProjectWorkspaceMismatchCliError(options) {
@@ -2916,9 +2949,9 @@ function localProjectWorkspaceMismatchCliError(options) {
2916
2949
  },
2917
2950
  exitCode: 1,
2918
2951
  nextSteps: [
2919
- `prisma-cli auth workspace use ${options.pinnedWorkspaceId}`,
2920
- "prisma-cli project list",
2921
- "prisma-cli project link <id-or-name>"
2952
+ `prisma auth workspace use ${options.pinnedWorkspaceId}`,
2953
+ "prisma project list",
2954
+ "prisma project link <id-or-name>"
2922
2955
  ]
2923
2956
  });
2924
2957
  }
@@ -2974,7 +3007,7 @@ function projectSetupRequiredCliError(error) {
2974
3007
  fix: "Link the directory to an existing Project, or pass --project <id-or-name> for this command.",
2975
3008
  meta: { ...suggestion },
2976
3009
  exitCode: 1,
2977
- nextSteps: ["prisma-cli project list", ...suggestion.recoveryCommands],
3010
+ nextSteps: ["prisma project list", ...suggestion.recoveryCommands],
2978
3011
  nextActions: buildProjectSetupNextActions({
2979
3012
  commandName: error.commandName,
2980
3013
  suggestedProjectName: suggestion.suggestedProjectName
@@ -2983,14 +3016,14 @@ function projectSetupRequiredCliError(error) {
2983
3016
  }
2984
3017
  function buildProjectSetupNextActions(options = {}) {
2985
3018
  const recoveryCommands = buildProjectRecoveryCommands(options.commandName);
2986
- const linkCommand = recoveryCommands[0] ?? "prisma-cli project link <id-or-name>";
3019
+ const linkCommand = recoveryCommands[0] ?? "prisma project link <id-or-name>";
2987
3020
  const retryCommand = options.retryCommand ?? recoveryCommands[1];
2988
3021
  const actions = [{
2989
3022
  kind: "user-choice",
2990
3023
  journey: "project-setup",
2991
3024
  label: "Ask the user whether to link an existing Project or create a new one",
2992
3025
  commands: [
2993
- "prisma-cli project list",
3026
+ "prisma project list",
2994
3027
  linkCommand,
2995
3028
  ...retryCommand ? [retryCommand] : []
2996
3029
  ],
@@ -3002,7 +3035,7 @@ function buildProjectSetupNextActions(options = {}) {
3002
3035
  command: linkCommand,
3003
3036
  reason: "Linking writes the durable local Project binding for this directory."
3004
3037
  }];
3005
- const createCommand = options.createCommand ?? (options.suggestedProjectName ? `prisma-cli project create ${formatCommandArgument(options.suggestedProjectName)}` : void 0);
3038
+ const createCommand = options.createCommand ?? (options.suggestedProjectName ? `prisma project create ${formatCommandArgument(options.suggestedProjectName)}` : void 0);
3006
3039
  if (createCommand) actions.push({
3007
3040
  kind: "run-command",
3008
3041
  journey: "project-setup",
@@ -3014,7 +3047,7 @@ function buildProjectSetupNextActions(options = {}) {
3014
3047
  kind: "run-command",
3015
3048
  journey: "recover",
3016
3049
  label: "Retry with an explicit Project",
3017
- command: retryCommand ?? `prisma-cli ${options.commandName} --project <id-or-name>`
3050
+ command: retryCommand ?? `prisma ${options.commandName} --project <id-or-name>`
3018
3051
  });
3019
3052
  return actions;
3020
3053
  }
@@ -3128,8 +3161,8 @@ function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
3128
3161
  };
3129
3162
  }
3130
3163
  function buildProjectRecoveryCommands(commandName) {
3131
- const commands = ["prisma-cli project link <id-or-name>"];
3132
- if (commandName) commands.push(`prisma-cli ${commandName} --project <id-or-name>`);
3164
+ const commands = ["prisma project link <id-or-name>"];
3165
+ if (commandName) commands.push(`prisma ${commandName} --project <id-or-name>`);
3133
3166
  return commands;
3134
3167
  }
3135
3168
  function toProjectSummary$1(project) {
@@ -3211,7 +3244,7 @@ async function cleanupLocalPinForProject(context, projectId, hooks) {
3211
3244
  const pin = pinResult.value;
3212
3245
  if (pin.kind !== "present" || pin.pin.projectId !== projectId) return false;
3213
3246
  try {
3214
- await unlink(path.join(context.runtime.cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH));
3247
+ await unlink(path.join(pin.directory, LOCAL_RESOLUTION_PIN_RELATIVE_PATH));
3215
3248
  return true;
3216
3249
  } catch {
3217
3250
  hooks.onError(`The local pin ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} points at the deleted project but could not be deleted.`);
@@ -3224,7 +3257,7 @@ async function rewriteOrClearLocalPinForProject(context, projectId, recipientWor
3224
3257
  const pin = pinResult.value;
3225
3258
  if (pin.kind !== "present" || pin.pin.projectId !== projectId) return "none";
3226
3259
  if (recipientWorkspaceId) {
3227
- if ((await writeLocalResolutionPin(context.runtime.cwd, {
3260
+ if ((await writeLocalResolutionPin(pin.directory, {
3228
3261
  workspaceId: recipientWorkspaceId,
3229
3262
  projectId
3230
3263
  }, context.runtime.signal)).isOk()) return "rewritten";
@@ -3232,7 +3265,7 @@ async function rewriteOrClearLocalPinForProject(context, projectId, recipientWor
3232
3265
  return "none";
3233
3266
  }
3234
3267
  try {
3235
- await unlink(path.join(context.runtime.cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH));
3268
+ await unlink(path.join(pin.directory, LOCAL_RESOLUTION_PIN_RELATIVE_PATH));
3236
3269
  return "cleared";
3237
3270
  } catch {
3238
3271
  hooks.onError(`The local pin ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} points at the transferred project but could not be cleared.`);
@@ -3401,7 +3434,7 @@ function unsupportedRepositoryProviderError() {
3401
3434
  why: "Repository connection supports GitHub repository URLs only.",
3402
3435
  fix: "Pass a GitHub repository URL such as git@github.com:prisma/prisma-cli.git.",
3403
3436
  exitCode: 2,
3404
- nextSteps: ["prisma-cli git connect git@github.com:owner/repo.git"]
3437
+ nextSteps: ["prisma git connect git@github.com:owner/repo.git"]
3405
3438
  });
3406
3439
  }
3407
3440
  function repoNotConnectedError() {
@@ -3410,9 +3443,9 @@ function repoNotConnectedError() {
3410
3443
  domain: "project",
3411
3444
  summary: "No GitHub repository connected",
3412
3445
  why: "The resolved project does not have an active GitHub repository connection.",
3413
- fix: "Run prisma-cli git connect before disconnecting.",
3446
+ fix: "Run prisma git connect before disconnecting.",
3414
3447
  exitCode: 1,
3415
- nextSteps: ["prisma-cli git connect"]
3448
+ nextSteps: ["prisma git connect"]
3416
3449
  });
3417
3450
  }
3418
3451
  function repoInstallationRequiredError(repository, installUrl, opened) {
@@ -3421,14 +3454,14 @@ function repoInstallationRequiredError(repository, installUrl, opened) {
3421
3454
  domain: "project",
3422
3455
  summary: "GitHub App installation required",
3423
3456
  why: `The selected workspace does not have a GitHub App installation that can be used to link ${repository.fullName}.`,
3424
- fix: opened ? "Finish installing the GitHub App in the browser, then rerun prisma-cli git connect." : "Open the GitHub App installation URL, approve access, then rerun prisma-cli git connect.",
3457
+ fix: opened ? "Finish installing the GitHub App in the browser, then rerun prisma git connect." : "Open the GitHub App installation URL, approve access, then rerun prisma git connect.",
3425
3458
  meta: {
3426
3459
  repository: repository.fullName,
3427
3460
  installUrl,
3428
3461
  opened
3429
3462
  },
3430
3463
  exitCode: 1,
3431
- nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`]
3464
+ nextSteps: [installUrl, `prisma git connect ${repository.url}`]
3432
3465
  });
3433
3466
  }
3434
3467
  function repoNotAccessibleError(repository, installUrl, opened) {
@@ -3437,14 +3470,14 @@ function repoNotAccessibleError(repository, installUrl, opened) {
3437
3470
  domain: "project",
3438
3471
  summary: "GitHub repository is not accessible",
3439
3472
  why: `The GitHub App installations connected to this workspace do not expose ${repository.fullName}.`,
3440
- fix: "Open the GitHub App installation URL, grant access to this repository, then rerun prisma-cli git connect.",
3473
+ fix: "Open the GitHub App installation URL, grant access to this repository, then rerun prisma git connect.",
3441
3474
  meta: {
3442
3475
  repository: repository.fullName,
3443
3476
  installUrl,
3444
3477
  opened
3445
3478
  },
3446
3479
  exitCode: 1,
3447
- nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`]
3480
+ nextSteps: [installUrl, `prisma git connect ${repository.url}`]
3448
3481
  });
3449
3482
  }
3450
3483
  function repoAlreadyConnectedError(repositoryFullName) {
@@ -3456,7 +3489,7 @@ function repoAlreadyConnectedError(repositoryFullName) {
3456
3489
  fix: "Disconnect the existing repository before connecting a different one.",
3457
3490
  meta: { repository: repositoryFullName },
3458
3491
  exitCode: 1,
3459
- nextSteps: ["prisma-cli git disconnect"]
3492
+ nextSteps: ["prisma git disconnect"]
3460
3493
  });
3461
3494
  }
3462
3495
  function repositoryFullNamesMatch(left, right) {
@@ -3467,7 +3500,7 @@ function repoConnectionApiError(summary, response, error) {
3467
3500
  const apiCode = error?.error?.code;
3468
3501
  const apiMessage = error?.error?.message;
3469
3502
  const apiHint = error?.error?.hint;
3470
- if (status === 401 || status === 403) return authRequiredError(["prisma-cli auth login"]);
3503
+ if (status === 401 || status === 403) return authRequiredError(["prisma auth login"]);
3471
3504
  return new CliError({
3472
3505
  code: "REPO_CONNECTION_FAILED",
3473
3506
  domain: "project",
@@ -3479,11 +3512,11 @@ function repoConnectionApiError(summary, response, error) {
3479
3512
  ...apiCode ? { apiCode } : {}
3480
3513
  },
3481
3514
  exitCode: 1,
3482
- nextSteps: ["prisma-cli project show"]
3515
+ nextSteps: ["prisma project show"]
3483
3516
  });
3484
3517
  }
3485
3518
  function repoConnectionFixForStatus(status) {
3486
- if (status === 404) return "Install the GitHub App for this workspace, then rerun prisma-cli git connect.";
3519
+ if (status === 404) return "Install the GitHub App for this workspace, then rerun prisma git connect.";
3487
3520
  if (status === 409) return "This project or repository is already linked. Disconnect the old link first, then try again.";
3488
3521
  if (status === 422) return "Make sure the GitHub App installation has access to this repository.";
3489
3522
  return "Re-run with --trace for the underlying API response details.";
@@ -3537,7 +3570,7 @@ function localStateWriteFailedError(error, options) {
3537
3570
  debug: formatDebugDetails(error.cause),
3538
3571
  meta: options.meta,
3539
3572
  exitCode: 1,
3540
- nextSteps: ["prisma-cli project link <id-or-name>"]
3573
+ nextSteps: ["prisma project link <id-or-name>"]
3541
3574
  });
3542
3575
  }
3543
3576
  function toProjectSummary(project) {
@@ -3549,7 +3582,7 @@ function toProjectSummary(project) {
3549
3582
  };
3550
3583
  }
3551
3584
  function projectSetupNameRequiredError(command) {
3552
- return usageError("Project create requires a name", "The project name must be a non-empty value.", "Pass a Project name explicitly.", [`prisma-cli ${command} my-app`], "project");
3585
+ return usageError("Project create requires a name", "The project name must be a non-empty value.", "Pass a Project name explicitly.", [`prisma ${command} my-app`], "project");
3553
3586
  }
3554
3587
  function projectCreateFailedError(error, projectName, workspace, options) {
3555
3588
  const status = extractHttpStatus(error);
@@ -3715,11 +3748,12 @@ const PROJECT_CODE_MAP = {
3715
3748
  };
3716
3749
  const PACKAGE_RUNNER_PREFIX = /^\S+(?: -y)? @prisma\/cli@\S+ /;
3717
3750
  const COMMENT_PREFIX$1 = /^#\s*/;
3718
- /** Legacy command strings are `prisma-cli …`, except one `prisma auth
3719
- * login` copy bug and the package-runner formatter's output. */
3751
+ /** Ported command strings already name this binary; what still needs
3752
+ * porting is the package-runner spelling the legacy formatter emitted
3753
+ * (`npx -y @prisma/cli@next auth login`), which becomes a plain
3754
+ * invocation. Anything else is passed through untouched. */
3720
3755
  function portCommandString(command) {
3721
- if (command.startsWith(`prisma-cli `)) return command;
3722
- if (command.startsWith("prisma ")) return `${CLI_NAME} ${command.slice(7)}`;
3756
+ if (command.startsWith(`prisma `)) return command;
3723
3757
  return command.replace(PACKAGE_RUNNER_PREFIX, `${CLI_NAME} `);
3724
3758
  }
3725
3759
  const STALE_INTERACTIVE_SIGN_IN$4 = /, or rerun the command in a TTY to sign in interactively\./g;
@@ -3812,7 +3846,7 @@ function mapBranchOperationError(error) {
3812
3846
  //#region src/commands/branch/list.ts
3813
3847
  /** The `branch list` command. */
3814
3848
  const TITLE$12 = "Listing branches for the resolved project.";
3815
- function listPresentations$7(result) {
3849
+ function listPresentations$8(result) {
3816
3850
  const rows = result.branches.map((branch) => [
3817
3851
  branch.name,
3818
3852
  branch.role,
@@ -3870,7 +3904,7 @@ const branchListCommand = defineCommand({
3870
3904
  projectName: target.project.name,
3871
3905
  branches: sortBranches(branches.map(toBranchSummary))
3872
3906
  };
3873
- return ok(ctx.present({ data: result }, listPresentations$7(result)));
3907
+ return ok(ctx.present({ data: result }, listPresentations$8(result)));
3874
3908
  } catch (error) {
3875
3909
  const mapped = mapBranchOperationError(error);
3876
3910
  if (mapped) return notOk(mapped);
@@ -3963,7 +3997,7 @@ function createManagementBucketProvider(client) {
3963
3997
  why: "Bucket key credentials are one-time-view secrets, but the Management API did not include them in this create response.",
3964
3998
  fix: "Create another bucket key and store the returned credentials immediately.",
3965
3999
  exitCode: 1,
3966
- nextSteps: [`prisma-cli bucket key create ${options.bucketId}`]
4000
+ nextSteps: [`prisma bucket key create ${options.bucketId}`]
3967
4001
  });
3968
4002
  return {
3969
4003
  key: normalizeKey(raw),
@@ -4311,17 +4345,184 @@ const bucketKeyCreateCommand = defineCommand({
4311
4345
  handler: async (args, ctx) => {
4312
4346
  try {
4313
4347
  const bucketId = args.positionals.bucketId.trim();
4314
- if (!bucketId) throw usageError("Bucket id required", "Bucket key creation needs a bucket id.", "Pass the bucket id.", ["prisma-cli bucket list"], "bucket");
4348
+ if (!bucketId) throw usageError("Bucket id required", "Bucket key creation needs a bucket id.", "Pass the bucket id.", ["prisma bucket list"], "bucket");
4349
+ const result = {
4350
+ bucketId,
4351
+ ...await resolveBucketProviderOnly(ctx).createKey({
4352
+ bucketId,
4353
+ name: args.flags.name?.trim() || void 0,
4354
+ role: resolveKeyRole(args.flags.role),
4355
+ signal: ctx.signal
4356
+ })
4357
+ };
4358
+ return ok(ctx.present({ data: result }, createPresentations$1(result)));
4359
+ } catch (error) {
4360
+ const mapped = mapBucketOperationError(error);
4361
+ if (mapped) return notOk(mapped);
4362
+ throw error;
4363
+ }
4364
+ }
4365
+ });
4366
+ //#endregion
4367
+ //#region src/commands/bucket/key-delete.ts
4368
+ /** The `bucket key delete` command. */
4369
+ function deletePresentations$4(result) {
4370
+ return {
4371
+ stdout: () => [],
4372
+ json: () => result,
4373
+ next: () => [],
4374
+ human: () => [
4375
+ {
4376
+ kind: "summary",
4377
+ status: "ok",
4378
+ text: "Deleting bucket access key."
4379
+ },
4380
+ {
4381
+ kind: "fields",
4382
+ rows: [{
4383
+ label: "key",
4384
+ value: result.key.id
4385
+ }]
4386
+ },
4387
+ {
4388
+ kind: "list",
4389
+ items: ["The access key was revoked and removed."]
4390
+ }
4391
+ ]
4392
+ };
4393
+ }
4394
+ const bucketKeyDeleteCommand = defineCommand({
4395
+ args: { positionals: {
4396
+ bucketId: bucketPositional,
4397
+ keyId: positional.string({
4398
+ brief: "Key id",
4399
+ placeholder: "key-id"
4400
+ })
4401
+ } },
4402
+ help: {
4403
+ summary: "Revoke and delete a bucket access key",
4404
+ examples: ["bucket key delete bkt_123 bkey_456"]
4405
+ },
4406
+ needs: { credentials: true },
4407
+ handler: async (args, ctx) => {
4408
+ try {
4409
+ const bucketId = args.positionals.bucketId.trim();
4410
+ const keyId = args.positionals.keyId.trim();
4411
+ if (!bucketId || !keyId) throw usageError("Bucket id and key id required", "Bucket key deletion needs both a bucket id and a key id.", "Pass the bucket id and key id.", ["prisma bucket key list <bucketId>"], "bucket");
4412
+ await resolveBucketProviderOnly(ctx).deleteKey(bucketId, keyId, { signal: ctx.signal });
4413
+ const result = { key: { id: keyId } };
4414
+ return ok(ctx.present({ data: result }, deletePresentations$4(result)));
4415
+ } catch (error) {
4416
+ const mapped = mapBucketOperationError(error);
4417
+ if (mapped) return notOk(mapped);
4418
+ throw error;
4419
+ }
4420
+ }
4421
+ });
4422
+ //#endregion
4423
+ //#region src/output/patterns.ts
4424
+ function serializeList(input) {
4425
+ return {
4426
+ context: input.context,
4427
+ items: input.items.map((item) => ({
4428
+ name: item.label,
4429
+ id: item.id,
4430
+ status: item.status
4431
+ })),
4432
+ count: input.items.length
4433
+ };
4434
+ }
4435
+ //#endregion
4436
+ //#region src/presenters/bucket.ts
4437
+ function serializeBucketList(result) {
4438
+ return {
4439
+ context: {
4440
+ project: result.projectName,
4441
+ ...result.branchName ? { branch: result.branchName } : {}
4442
+ },
4443
+ items: result.buckets.map((bucket) => ({
4444
+ name: bucket.name,
4445
+ id: bucket.id,
4446
+ status: bucket.status
4447
+ })),
4448
+ count: result.buckets.length,
4449
+ projectId: result.projectId,
4450
+ branchName: result.branchName,
4451
+ buckets: result.buckets
4452
+ };
4453
+ }
4454
+ function serializeBucketKeyList(result) {
4455
+ return {
4456
+ ...serializeList({
4457
+ context: { bucket: result.bucketId },
4458
+ items: result.keys.map((key) => ({
4459
+ noun: "key",
4460
+ label: key.name,
4461
+ id: key.id,
4462
+ status: null
4463
+ }))
4464
+ }),
4465
+ bucketId: result.bucketId,
4466
+ keys: result.keys
4467
+ };
4468
+ }
4469
+ //#endregion
4470
+ //#region src/commands/bucket/key-list.ts
4471
+ /** The `bucket key list` command. */
4472
+ const TITLE$11 = "Listing access keys for bucket.";
4473
+ function listPresentations$7(result) {
4474
+ const rows = bucketKeyRows(result.keys);
4475
+ return {
4476
+ next: () => [],
4477
+ human: () => [
4478
+ {
4479
+ kind: "summary",
4480
+ status: "info",
4481
+ text: TITLE$11
4482
+ },
4483
+ {
4484
+ kind: "fields",
4485
+ rows: [{
4486
+ label: "bucket",
4487
+ value: result.bucketId
4488
+ }]
4489
+ },
4490
+ ...rows.length === 0 ? [{
4491
+ kind: "summary",
4492
+ status: "info",
4493
+ text: "No keys found."
4494
+ }] : [{
4495
+ kind: "table",
4496
+ columns: [
4497
+ "Name",
4498
+ "Id",
4499
+ "Role",
4500
+ "Hint",
4501
+ "Created"
4502
+ ],
4503
+ rows
4504
+ }]
4505
+ ],
4506
+ stdout: () => rows.map((row) => row.join(" ")),
4507
+ json: () => serializeBucketKeyList(result)
4508
+ };
4509
+ }
4510
+ const bucketKeyListCommand = defineCommand({
4511
+ args: { positionals: { bucketId: bucketPositional } },
4512
+ help: {
4513
+ summary: "List access keys for a bucket",
4514
+ examples: ["bucket key list bkt_123", "bucket key list bkt_123 --json"]
4515
+ },
4516
+ needs: { credentials: true },
4517
+ handler: async (args, ctx) => {
4518
+ try {
4519
+ const bucketId = args.positionals.bucketId.trim();
4520
+ if (!bucketId) throw usageError("Bucket id required", "Bucket key listing needs a bucket id.", "Pass the bucket id.", ["prisma bucket list"], "bucket");
4315
4521
  const result = {
4316
4522
  bucketId,
4317
- ...await resolveBucketProviderOnly(ctx).createKey({
4318
- bucketId,
4319
- name: args.flags.name?.trim() || void 0,
4320
- role: resolveKeyRole(args.flags.role),
4321
- signal: ctx.signal
4322
- })
4523
+ keys: await resolveBucketProviderOnly(ctx).listKeys(bucketId, { signal: ctx.signal })
4323
4524
  };
4324
- return ok(ctx.present({ data: result }, createPresentations$1(result)));
4525
+ return ok(ctx.present({ data: result }, listPresentations$7(result)));
4325
4526
  } catch (error) {
4326
4527
  const mapped = mapBucketOperationError(error);
4327
4528
  if (mapped) return notOk(mapped);
@@ -4330,824 +4531,1329 @@ const bucketKeyCreateCommand = defineCommand({
4330
4531
  }
4331
4532
  });
4332
4533
  //#endregion
4333
- //#region src/commands/bucket/key-delete.ts
4334
- /** The `bucket key delete` command. */
4335
- function deletePresentations$4(result) {
4534
+ //#region src/commands/bucket/list.ts
4535
+ /** The `bucket list` command. */
4536
+ const TITLE$10 = "Listing object-store buckets for the resolved project.";
4537
+ function listPresentations$6(result) {
4538
+ const rows = bucketRows(result.buckets);
4539
+ const stdoutRows = bucketStdoutRows(result.buckets);
4336
4540
  return {
4337
- stdout: () => [],
4338
- json: () => result,
4339
4541
  next: () => [],
4340
4542
  human: () => [
4341
4543
  {
4342
4544
  kind: "summary",
4343
- status: "ok",
4344
- text: "Deleting bucket access key."
4545
+ status: "info",
4546
+ text: TITLE$10
4345
4547
  },
4346
4548
  {
4347
4549
  kind: "fields",
4348
4550
  rows: [{
4349
- label: "key",
4350
- value: result.key.id
4351
- }]
4551
+ label: "project",
4552
+ value: result.projectName
4553
+ }, ...result.branchName ? [{
4554
+ label: "branch",
4555
+ value: result.branchName
4556
+ }] : []]
4352
4557
  },
4353
- {
4354
- kind: "list",
4355
- items: ["The access key was revoked and removed."]
4356
- }
4357
- ]
4558
+ ...rows.length === 0 ? [{
4559
+ kind: "summary",
4560
+ status: "info",
4561
+ text: "No buckets found."
4562
+ }] : [{
4563
+ kind: "table",
4564
+ columns: [
4565
+ "Name",
4566
+ "Id",
4567
+ "Status",
4568
+ "Branch",
4569
+ "Created"
4570
+ ],
4571
+ rows
4572
+ }]
4573
+ ],
4574
+ stdout: () => stdoutRows.map((row) => row.join(" ")),
4575
+ json: () => serializeBucketList(result)
4358
4576
  };
4359
4577
  }
4360
- const bucketKeyDeleteCommand = defineCommand({
4361
- args: { positionals: {
4362
- bucketId: bucketPositional,
4363
- keyId: positional.string({
4364
- brief: "Key id",
4365
- placeholder: "key-id"
4366
- })
4578
+ const bucketListCommand = defineCommand({
4579
+ args: { flags: {
4580
+ project: projectFlag$3,
4581
+ branch: branchFlag$2
4367
4582
  } },
4368
4583
  help: {
4369
- summary: "Revoke and delete a bucket access key",
4370
- examples: ["bucket key delete bkt_123 bkey_456"]
4584
+ summary: "List object-store buckets for the resolved project",
4585
+ examples: [
4586
+ "bucket list",
4587
+ "bucket list --branch preview",
4588
+ "bucket list --json"
4589
+ ]
4590
+ },
4591
+ needs: { credentials: true },
4592
+ handler: async (args, ctx) => {
4593
+ try {
4594
+ const { provider, projectId, projectName } = await resolveBucketContext(ctx, args.flags, "bucket list");
4595
+ const buckets = await provider.listBuckets({
4596
+ projectId,
4597
+ branchName: args.flags.branch,
4598
+ signal: ctx.signal
4599
+ });
4600
+ const result = {
4601
+ projectId,
4602
+ projectName,
4603
+ branchName: args.flags.branch ?? null,
4604
+ buckets
4605
+ };
4606
+ return ok(ctx.present({ data: result }, listPresentations$6(result)));
4607
+ } catch (error) {
4608
+ const mapped = mapBucketOperationError(error);
4609
+ if (mapped) return notOk(mapped);
4610
+ throw error;
4611
+ }
4612
+ }
4613
+ });
4614
+ //#endregion
4615
+ //#region src/lib/feedback.ts
4616
+ const FEEDBACK_TIMEOUT_MS = 3e3;
4617
+ const TIMEOUT_DETAIL = `The feedback service did not answer within ${FEEDBACK_TIMEOUT_MS / 1e3} seconds.`;
4618
+ function isTimeout(error) {
4619
+ return error instanceof Error && error.name === "TimeoutError";
4620
+ }
4621
+ function unreachableDetail(error) {
4622
+ if (isTimeout(error)) return TIMEOUT_DETAIL;
4623
+ return `The feedback service could not be reached${error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : ""}.`;
4624
+ }
4625
+ function unreadableBodyDetail(error) {
4626
+ return isTimeout(error) ? TIMEOUT_DETAIL : "The feedback service response could not be read.";
4627
+ }
4628
+ //#endregion
4629
+ //#region src/lib/version.ts
4630
+ const requireFromHere = createRequire(import.meta.url);
4631
+ /** The bundled entry sits one directory below the package root
4632
+ * (`dist/cli.js`); this source file sits two below (`src/lib/`). Both
4633
+ * are tried, nearest first, so the same code serves either. */
4634
+ const PACKAGE_JSON_CANDIDATES = ["../package.json", "../../package.json"];
4635
+ function readPackageMetadata() {
4636
+ for (const candidate of PACKAGE_JSON_CANDIDATES) try {
4637
+ const metadata = requireFromHere(candidate);
4638
+ if (metadata.version) return metadata;
4639
+ } catch {}
4640
+ return {};
4641
+ }
4642
+ function getCliVersion() {
4643
+ const pkg = readPackageMetadata();
4644
+ if (!pkg.version) throw new Error("CLI version metadata is missing from the installed package: the bundled package.json could not be read or did not contain a version field. Reinstall the CLI from the npm registry, or check your install path is intact.");
4645
+ return pkg.version;
4646
+ }
4647
+ function getCliName() {
4648
+ return CLI_NAME;
4649
+ }
4650
+ //#endregion
4651
+ //#region src/commands/feedback.ts
4652
+ const DEFAULT_FEEDBACK_ENDPOINT = "https://hiieirp2pwqnjvq9axzyg6d0.fra.prisma.build/feedback";
4653
+ const MAX_MESSAGE_LENGTH = 4e3;
4654
+ const MAX_EMAIL_LENGTH = 320;
4655
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4656
+ function sendFailedError(detail) {
4657
+ return new CliStructuredError("FEEDBACK.SEND_FAILED", "Feedback could not be delivered", {
4658
+ why: detail,
4659
+ nextActions: [{
4660
+ kind: "user-choice",
4661
+ label: "Check your network and rerun."
4662
+ }]
4663
+ });
4664
+ }
4665
+ function messageRequiredError() {
4666
+ return new CliStructuredError("FEEDBACK.MESSAGE_REQUIRED", "Feedback message required", {
4667
+ why: "The message argument is empty.",
4668
+ nextActions: [{
4669
+ kind: "user-choice",
4670
+ label: "Pass a non-empty message."
4671
+ }, {
4672
+ kind: "run-command",
4673
+ label: "Send feedback",
4674
+ command: `${CLI_NAME} feedback "the deploy flow is great"`
4675
+ }]
4676
+ });
4677
+ }
4678
+ function messageTooLongError(length) {
4679
+ return new CliStructuredError("FEEDBACK.MESSAGE_TOO_LONG", "Feedback message too long", {
4680
+ why: `The message is ${length} characters; the limit is ${MAX_MESSAGE_LENGTH}.`,
4681
+ nextActions: [{
4682
+ kind: "user-choice",
4683
+ label: "Shorten the message."
4684
+ }]
4685
+ });
4686
+ }
4687
+ function emailInvalidError(value) {
4688
+ return new CliStructuredError("FEEDBACK.EMAIL_INVALID", "Invalid email", {
4689
+ why: `"${value}" is not a valid email address of at most ${MAX_EMAIL_LENGTH} characters.`,
4690
+ nextActions: [{
4691
+ kind: "user-choice",
4692
+ label: "Pass a valid address with --email, or drop the flag to stay anonymous."
4693
+ }, {
4694
+ kind: "run-command",
4695
+ label: "Send feedback with a contact address",
4696
+ command: `${CLI_NAME} feedback "please add X" --email you@example.com`
4697
+ }]
4698
+ });
4699
+ }
4700
+ function feedbackPresentations(result) {
4701
+ return {
4702
+ stdout: () => [],
4703
+ json: () => result,
4704
+ next: () => [],
4705
+ human: () => [{
4706
+ kind: "summary",
4707
+ status: "ok",
4708
+ text: "Feedback sent. Thank you!"
4709
+ }, {
4710
+ kind: "fields",
4711
+ rows: [
4712
+ ...result.id ? [{
4713
+ label: "id",
4714
+ value: result.id
4715
+ }] : [],
4716
+ {
4717
+ label: "sent as",
4718
+ value: result.email ?? "anonymous"
4719
+ },
4720
+ {
4721
+ label: "included",
4722
+ value: `CLI ${result.context.cliVersion}, ${result.context.runtime.name} ${result.context.runtime.version}, ${result.context.platform} ${result.context.arch}`
4723
+ }
4724
+ ]
4725
+ }]
4726
+ };
4727
+ }
4728
+ async function readServiceError(response, signal) {
4729
+ let payload;
4730
+ try {
4731
+ payload = await response.json();
4732
+ } catch (error) {
4733
+ if (signal.aborted) throw error;
4734
+ return "";
4735
+ }
4736
+ return typeof payload?.error?.message === "string" ? ` (${payload.error.message})` : "";
4737
+ }
4738
+ async function sendFeedback(endpoint, body, signal) {
4739
+ try {
4740
+ return await fetch(endpoint, {
4741
+ method: "POST",
4742
+ headers: {
4743
+ "content-type": "application/json",
4744
+ "user-agent": `${CLI_NAME}/${getCliVersion()}`
4745
+ },
4746
+ body: JSON.stringify(body),
4747
+ signal: AbortSignal.any([signal, AbortSignal.timeout(FEEDBACK_TIMEOUT_MS)])
4748
+ });
4749
+ } catch (error) {
4750
+ if (signal.aborted) throw error;
4751
+ throw sendFailedError(unreachableDetail(error));
4752
+ }
4753
+ }
4754
+ async function readSubmissionId(response, signal) {
4755
+ let payload;
4756
+ try {
4757
+ payload = await response.json();
4758
+ } catch (error) {
4759
+ if (signal.aborted) throw error;
4760
+ if (!(error instanceof SyntaxError)) throw sendFailedError(unreadableBodyDetail(error));
4761
+ payload = null;
4762
+ }
4763
+ return typeof payload?.id === "string" ? payload.id : null;
4764
+ }
4765
+ async function postFeedback(endpoint, body, signal) {
4766
+ const response = await sendFeedback(endpoint, body, signal);
4767
+ if (!response.ok) throw sendFailedError(`The feedback service responded with HTTP ${response.status}${await readServiceError(response, signal)}.`);
4768
+ return readSubmissionId(response, signal);
4769
+ }
4770
+ const feedbackCommand = defineCommand({
4771
+ help: {
4772
+ summary: "Send feedback to the Prisma CLI team",
4773
+ description: "Anonymous unless --email is passed. Every submission includes the CLI\nversion, node version, and OS platform/arch, and nothing else.",
4774
+ examples: ["feedback \"the deploy flow is great\"", "feedback \"please add X\" --email you@example.com"]
4775
+ },
4776
+ args: {
4777
+ flags: { email: flag.string({
4778
+ brief: "Contact email if you want a reply; feedback is anonymous without it",
4779
+ placeholder: "address"
4780
+ }) },
4781
+ positionals: { message: positional.string({
4782
+ brief: "Feedback text (up to 4000 characters)",
4783
+ placeholder: "message"
4784
+ }) }
4371
4785
  },
4372
- needs: { credentials: true },
4373
4786
  handler: async (args, ctx) => {
4374
- try {
4375
- const bucketId = args.positionals.bucketId.trim();
4376
- const keyId = args.positionals.keyId.trim();
4377
- if (!bucketId || !keyId) throw usageError("Bucket id and key id required", "Bucket key deletion needs both a bucket id and a key id.", "Pass the bucket id and key id.", ["prisma-cli bucket key list <bucketId>"], "bucket");
4378
- await resolveBucketProviderOnly(ctx).deleteKey(bucketId, keyId, { signal: ctx.signal });
4379
- const result = { key: { id: keyId } };
4380
- return ok(ctx.present({ data: result }, deletePresentations$4(result)));
4381
- } catch (error) {
4382
- const mapped = mapBucketOperationError(error);
4383
- if (mapped) return notOk(mapped);
4384
- throw error;
4385
- }
4787
+ const message = args.positionals.message.trim();
4788
+ if (!message) throw messageRequiredError();
4789
+ if (message.length > MAX_MESSAGE_LENGTH) throw messageTooLongError(message.length);
4790
+ const email = args.flags.email?.trim();
4791
+ if (email !== void 0 && (email.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(email))) throw emailInvalidError(args.flags.email ?? "");
4792
+ const context = {
4793
+ cliVersion: getCliVersion(),
4794
+ runtime: { ...ctx.host.runtime },
4795
+ platform: ctx.host.platform,
4796
+ arch: ctx.host.arch
4797
+ };
4798
+ const result = {
4799
+ id: await postFeedback(ctx.env.PRISMA_CLI_FEEDBACK_URL || DEFAULT_FEEDBACK_ENDPOINT, {
4800
+ message,
4801
+ ...email ? { email } : {},
4802
+ meta: { ...context }
4803
+ }, ctx.signal),
4804
+ email: email ?? null,
4805
+ context
4806
+ };
4807
+ return ok(ctx.present({ data: result }, feedbackPresentations(result)));
4386
4808
  }
4387
4809
  });
4388
4810
  //#endregion
4389
- //#region src/output/patterns.ts
4390
- function serializeList(input) {
4391
- return {
4392
- context: input.context,
4393
- items: input.items.map((item) => ({
4394
- name: item.label,
4395
- id: item.id,
4396
- status: item.status
4397
- })),
4398
- count: input.items.length
4399
- };
4811
+ //#region src/adapters/git.ts
4812
+ const execFileAsync = promisify(execFile);
4813
+ async function readGitOriginRemote(cwd, signal) {
4814
+ try {
4815
+ const { stdout } = await execFileAsync("git", [
4816
+ "config",
4817
+ "--get",
4818
+ "remote.origin.url"
4819
+ ], {
4820
+ cwd,
4821
+ timeout: 5e3,
4822
+ signal
4823
+ });
4824
+ const remote = stdout.trim();
4825
+ return remote.length > 0 ? remote : null;
4826
+ } catch (error) {
4827
+ if (signal?.aborted || isAbortError(error)) throw error;
4828
+ return null;
4829
+ }
4400
4830
  }
4401
- //#endregion
4402
- //#region src/presenters/bucket.ts
4403
- function serializeBucketList(result) {
4831
+ function isAbortError(error) {
4832
+ return error instanceof Error && error.name === "AbortError";
4833
+ }
4834
+ function parseGitHubRepositoryUrl(value) {
4835
+ const input = value.trim();
4836
+ const shorthand = input.match(/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
4837
+ if (shorthand) return toGitHubRepositoryReference(shorthand[1], shorthand[2]);
4838
+ let parsed;
4839
+ try {
4840
+ parsed = new URL(input);
4841
+ } catch {
4842
+ return null;
4843
+ }
4844
+ if (parsed.hostname !== "github.com") return null;
4845
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:" && parsed.protocol !== "ssh:") return null;
4846
+ const parts = parsed.pathname.split("/").filter(Boolean);
4847
+ if (parts.length !== 2) return null;
4848
+ const [owner, rawName] = parts;
4849
+ return toGitHubRepositoryReference(owner, rawName.endsWith(".git") ? rawName.slice(0, -4) : rawName);
4850
+ }
4851
+ function toGitHubRepositoryReference(owner, name) {
4852
+ if (!owner || !name || owner.includes("/") || name.includes("/")) return null;
4404
4853
  return {
4405
- context: {
4406
- project: result.projectName,
4407
- ...result.branchName ? { branch: result.branchName } : {}
4408
- },
4409
- items: result.buckets.map((bucket) => ({
4410
- name: bucket.name,
4411
- id: bucket.id,
4412
- status: bucket.status
4413
- })),
4414
- count: result.buckets.length,
4415
- projectId: result.projectId,
4416
- branchName: result.branchName,
4417
- buckets: result.buckets
4854
+ provider: "github",
4855
+ owner,
4856
+ name,
4857
+ fullName: `${owner}/${name}`,
4858
+ url: `https://github.com/${owner}/${name}`
4418
4859
  };
4419
4860
  }
4420
- function serializeBucketKeyList(result) {
4861
+ //#endregion
4862
+ //#region src/presenters/project.ts
4863
+ function serializeProjectList(result) {
4421
4864
  return {
4422
4865
  ...serializeList({
4423
- context: { bucket: result.bucketId },
4424
- items: result.keys.map((key) => ({
4425
- noun: "key",
4426
- label: key.name,
4427
- id: key.id,
4866
+ context: { workspace: result.workspace.name },
4867
+ items: result.projects.map((project) => ({
4868
+ noun: "project",
4869
+ label: project.name,
4870
+ id: project.id,
4428
4871
  status: null
4429
4872
  }))
4430
4873
  }),
4431
- bucketId: result.bucketId,
4432
- keys: result.keys
4874
+ localBinding: result.localBinding ?? null
4875
+ };
4876
+ }
4877
+ function serializeProjectSetup(result) {
4878
+ return result;
4879
+ }
4880
+ function formatGitConnectionDetail(status) {
4881
+ switch (status) {
4882
+ case "active": return "GitHub branch automation is active for this project.";
4883
+ case "pending": return "GitHub branch automation is pending GitHub App installation.";
4884
+ case "archived": return "GitHub branch automation has been archived for this project.";
4885
+ default: return "GitHub repository is connected, but branch automation is not active.";
4886
+ }
4887
+ }
4888
+ //#endregion
4889
+ //#region src/commands/git/context.ts
4890
+ /** Workspace, project and the source-repository client for the
4891
+ * `git *` commands. */
4892
+ const projectFlag$2 = flag.string({
4893
+ brief: "Project id or name",
4894
+ placeholder: "id-or-name"
4895
+ });
4896
+ async function resolveGitContext(ctx, explicitProject, commandName) {
4897
+ const target = await resolvePinnedProject(ctx, await resolveActiveWorkspace(ctx), explicitProject, commandName);
4898
+ return {
4899
+ api: ctx.api,
4900
+ target
4901
+ };
4902
+ }
4903
+ //#endregion
4904
+ //#region src/commands/git/errors.ts
4905
+ const GIT_CODE_MAP = {
4906
+ USAGE_ERROR: "GIT.USAGE_ERROR",
4907
+ REPO_PROVIDER_UNSUPPORTED: "GIT.REPO_PROVIDER_UNSUPPORTED",
4908
+ REPO_ALREADY_CONNECTED: "GIT.REPO_ALREADY_CONNECTED",
4909
+ REPO_INSTALLATION_REQUIRED: "GIT.REPO_INSTALLATION_REQUIRED",
4910
+ REPO_NOT_ACCESSIBLE: "GIT.REPO_NOT_ACCESSIBLE",
4911
+ REPO_NOT_CONNECTED: "GIT.REPO_NOT_CONNECTED",
4912
+ REPO_CONNECTION_FAILED: "GIT.REPO_CONNECTION_FAILED"
4913
+ };
4914
+ /** The project-resolution codes the git commands can raise; they keep
4915
+ * the project group's dotted codes and copy. */
4916
+ const PROJECT_CODES$1 = new Set([
4917
+ "PROJECT_NOT_FOUND",
4918
+ "PROJECT_AMBIGUOUS",
4919
+ "PROJECT_SETUP_REQUIRED",
4920
+ "LOCAL_STATE_STALE",
4921
+ "LOCAL_PROJECT_WORKSPACE_MISMATCH"
4922
+ ]);
4923
+ const STALE_INTERACTIVE_SIGN_IN$1 = /, or rerun the command in a TTY to sign in interactively\./g;
4924
+ const TRACE_FLAG$1 = /--trace/g;
4925
+ /** `--trace` is gone in this CLI; the log level replaces it. Interactive
4926
+ * sign-in is gone too (R-S2b-2), so the legacy offer to rerun in a TTY
4927
+ * describes something this CLI cannot do; `auth login` is the whole remedy. */
4928
+ function portFixText$1(fix) {
4929
+ return fix.replace(TRACE_FLAG$1, "--log-level verbose").replace(STALE_INTERACTIVE_SIGN_IN$1, ".");
4930
+ }
4931
+ /** The install-required and not-accessible errors put the raw install
4932
+ * URL in their nextSteps beside real commands. A URL is not a command,
4933
+ * so it becomes an `open-url` action. */
4934
+ function nextStepAction(step) {
4935
+ if (step.startsWith("https://") || step.startsWith("http://")) return {
4936
+ kind: "open-url",
4937
+ label: step,
4938
+ url: step
4939
+ };
4940
+ const command = portCommandString(step);
4941
+ return {
4942
+ kind: "run-command",
4943
+ label: command,
4944
+ command
4433
4945
  };
4434
4946
  }
4435
- //#endregion
4436
- //#region src/commands/bucket/key-list.ts
4437
- /** The `bucket key list` command. */
4438
- const TITLE$11 = "Listing access keys for bucket.";
4439
- function listPresentations$6(result) {
4440
- const rows = bucketKeyRows(result.keys);
4947
+ function nextActionsFor$1(error) {
4948
+ return [...error.fix ? [{
4949
+ kind: "user-choice",
4950
+ label: portFixText$1(error.fix)
4951
+ }] : [], ...error.nextSteps.map(nextStepAction)];
4952
+ }
4953
+ /** The legacy errors branch their fix text on whether a browser was
4954
+ * opened. The engine's browser wait always shows the URL, so the
4955
+ * opened branch is the one that describes what this CLI does. */
4956
+ const BROWSER_OPENED = true;
4957
+ /**
4958
+ * The install wait's two terminal outcomes. The legacy constructors own
4959
+ * every copy string; the design drops `opened` from the meta they build
4960
+ * (`browserWait` does not report it and the URL is always shown), so
4961
+ * the structured error is assembled from their fields with the meta
4962
+ * d3 §3.8 pins.
4963
+ */
4964
+ function installWaitFailedError(repository, installUrl, inspectableInstallationCount) {
4965
+ const legacy = inspectableInstallationCount > 0 ? repoNotAccessibleError(repository, installUrl, BROWSER_OPENED) : repoInstallationRequiredError(repository, installUrl, BROWSER_OPENED);
4966
+ return new CliStructuredError(GIT_CODE_MAP[legacy.code], legacy.summary, {
4967
+ why: legacy.why ?? void 0,
4968
+ meta: {
4969
+ repository: repository.fullName,
4970
+ installUrl
4971
+ },
4972
+ nextActions: nextActionsFor$1(legacy)
4973
+ });
4974
+ }
4975
+ function mapGitOperationError(error) {
4976
+ if (!(error instanceof CliError)) return null;
4977
+ if (PROJECT_CODES$1.has(error.code)) return mapProjectOperationError(error);
4978
+ return new CliStructuredError(GIT_CODE_MAP[error.code] ?? `GIT.${error.code}`, error.summary, {
4979
+ why: error.why ?? void 0,
4980
+ meta: Object.keys(error.meta).length > 0 ? error.meta : void 0,
4981
+ nextActions: nextActionsFor$1(error)
4982
+ });
4983
+ }
4984
+ //#endregion
4985
+ //#region src/commands/git/connect.ts
4986
+ /** The `git connect` command. */
4987
+ /** The legacy wait line, printed once before the poll loop. */
4988
+ const WAIT_MESSAGE = "Waiting for GitHub App installation or repository access approval...";
4989
+ /**
4990
+ * The legacy `resolveInstalledRepository`: find the repository in the
4991
+ * workspace's GitHub App installations, and when it is not there yet,
4992
+ * send the user to an install intent and wait for them to finish. The
4993
+ * engine owns the announcement, the browser and the polling clock; this
4994
+ * only supplies the address, the cadence and the question being polled.
4995
+ */
4996
+ async function resolveInstalledRepository(ctx, api, workspaceId, repository) {
4997
+ const inspect = async (signal) => findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId, signal), repository, signal);
4998
+ const first = await inspect(ctx.signal);
4999
+ if (first.match) return first.match;
5000
+ const installUrl = await createGitHubInstallIntent(api, workspaceId, ctx.signal);
5001
+ let match = null;
5002
+ let inspectableInstallationCount = 0;
5003
+ try {
5004
+ await ctx.prompt.browserWait({
5005
+ url: installUrl,
5006
+ message: WAIT_MESSAGE,
5007
+ timeout: readPositiveIntegerEnv(ctx.env.PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS, GITHUB_INSTALL_POLL_TIMEOUT_MS),
5008
+ interval: readPositiveIntegerEnv(ctx.env.PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS, GITHUB_INSTALL_POLL_INTERVAL_MS),
5009
+ poll: async (signal) => {
5010
+ const lookup = await inspect(signal);
5011
+ match = lookup.match;
5012
+ inspectableInstallationCount = lookup.inspectableInstallationCount;
5013
+ return lookup.match !== null;
5014
+ }
5015
+ });
5016
+ } catch (error) {
5017
+ if (CliStructuredError.is(error) && error.code === "CLI.BROWSER_WAIT_TIMEOUT") throw installWaitFailedError(repository, installUrl, inspectableInstallationCount);
5018
+ throw error;
5019
+ }
5020
+ if (match === null) throw installWaitFailedError(repository, installUrl, inspectableInstallationCount);
5021
+ return match;
5022
+ }
5023
+ function connectPresentations(result) {
5024
+ const connection = result.repositoryConnection;
4441
5025
  return {
5026
+ stdout: () => [],
5027
+ json: () => result,
4442
5028
  next: () => [],
4443
5029
  human: () => [
4444
5030
  {
4445
5031
  kind: "summary",
4446
- status: "info",
4447
- text: TITLE$11
5032
+ status: "ok",
5033
+ text: "Connecting Git to the resolved project."
4448
5034
  },
4449
5035
  {
4450
5036
  kind: "fields",
4451
- rows: [{
4452
- label: "bucket",
4453
- value: result.bucketId
4454
- }]
5037
+ rows: [
5038
+ {
5039
+ label: "project",
5040
+ value: result.project.name
5041
+ },
5042
+ {
5043
+ label: "workspace",
5044
+ value: result.workspace.name
5045
+ },
5046
+ {
5047
+ label: "repository",
5048
+ value: connection.repository.fullName
5049
+ },
5050
+ {
5051
+ label: "status",
5052
+ value: connection.status
5053
+ }
5054
+ ]
4455
5055
  },
4456
- ...rows.length === 0 ? [{
4457
- kind: "summary",
4458
- status: "info",
4459
- text: "No keys found."
4460
- }] : [{
4461
- kind: "table",
4462
- columns: [
4463
- "Name",
4464
- "Id",
4465
- "Role",
4466
- "Hint",
4467
- "Created"
4468
- ],
4469
- rows
4470
- }]
4471
- ],
4472
- stdout: () => rows.map((row) => row.join(" ")),
4473
- json: () => serializeBucketKeyList(result)
5056
+ {
5057
+ kind: "list",
5058
+ items: [formatGitConnectionDetail(connection.status)]
5059
+ }
5060
+ ]
4474
5061
  };
4475
5062
  }
4476
- const bucketKeyListCommand = defineCommand({
4477
- args: { positionals: { bucketId: bucketPositional } },
5063
+ const gitConnectCommand = defineCommand({
5064
+ args: {
5065
+ positionals: { gitUrl: positional.optionalString({
5066
+ brief: "GitHub repository URL",
5067
+ placeholder: "git-url"
5068
+ }) },
5069
+ flags: { project: projectFlag$2 }
5070
+ },
4478
5071
  help: {
4479
- summary: "List access keys for a bucket",
4480
- examples: ["bucket key list bkt_123", "bucket key list bkt_123 --json"]
5072
+ summary: "Connect the resolved project to a GitHub repository",
5073
+ examples: [
5074
+ "git connect",
5075
+ "git connect git@github.com:prisma/prisma-cli.git",
5076
+ "git connect --project proj_123"
5077
+ ]
4481
5078
  },
4482
5079
  needs: { credentials: true },
4483
5080
  handler: async (args, ctx) => {
4484
5081
  try {
4485
- const bucketId = args.positionals.bucketId.trim();
4486
- if (!bucketId) throw usageError("Bucket id required", "Bucket key listing needs a bucket id.", "Pass the bucket id.", ["prisma-cli bucket list"], "bucket");
5082
+ const { api, target } = await resolveGitContext(ctx, args.flags.project, "git connect");
5083
+ const remoteUrl = args.positionals.gitUrl ?? await readGitOriginRemote(ctx.cwd, ctx.signal);
5084
+ if (!remoteUrl) throw usageError("Repository connection requires a GitHub repository URL", "No git-url was provided and the local repo does not have an origin remote.", `Pass a GitHub repository URL, or add a GitHub origin remote and rerun ${CLI_NAME} git connect.`, [`${CLI_NAME} git connect git@github.com:prisma/prisma-cli.git`], "project");
5085
+ const repository = parseGitHubRepositoryUrl(remoteUrl);
5086
+ if (!repository) throw unsupportedRepositoryProviderError();
5087
+ const existing = await readFirstSourceRepository(api, target.project.id, ctx.signal);
5088
+ if (existing) {
5089
+ const existingConnection = toRepositoryConnection(existing);
5090
+ if (!repositoryFullNamesMatch(existingConnection.repository.fullName, repository.fullName)) throw repoAlreadyConnectedError(existingConnection.repository.fullName);
5091
+ const idempotent = {
5092
+ ...target,
5093
+ repositoryConnection: existingConnection
5094
+ };
5095
+ return ok(ctx.present({ data: idempotent }, connectPresentations(idempotent)));
5096
+ }
5097
+ const installed = await resolveInstalledRepository(ctx, api, target.workspace.id, repository);
5098
+ const { data, error, response } = await api.POST("/v1/source-repositories", {
5099
+ body: {
5100
+ projectId: target.project.id,
5101
+ provider: "github",
5102
+ providerRepositoryId: installed.repository.id,
5103
+ installationId: installed.installation.id
5104
+ },
5105
+ signal: ctx.signal
5106
+ });
5107
+ if (error || !data) throw repoConnectionApiError("Failed to connect GitHub repository", response, error);
4487
5108
  const result = {
4488
- bucketId,
4489
- keys: await resolveBucketProviderOnly(ctx).listKeys(bucketId, { signal: ctx.signal })
5109
+ ...target,
5110
+ repositoryConnection: toRepositoryConnection(data.data)
4490
5111
  };
4491
- return ok(ctx.present({ data: result }, listPresentations$6(result)));
5112
+ return ok(ctx.present({ data: result }, connectPresentations(result)));
4492
5113
  } catch (error) {
4493
- const mapped = mapBucketOperationError(error);
5114
+ const mapped = mapGitOperationError(error);
4494
5115
  if (mapped) return notOk(mapped);
4495
5116
  throw error;
4496
5117
  }
4497
5118
  }
4498
5119
  });
4499
5120
  //#endregion
4500
- //#region src/commands/bucket/list.ts
4501
- /** The `bucket list` command. */
4502
- const TITLE$10 = "Listing object-store buckets for the resolved project.";
4503
- function listPresentations$5(result) {
4504
- const rows = bucketRows(result.buckets);
4505
- const stdoutRows = bucketStdoutRows(result.buckets);
5121
+ //#region src/commands/git/disconnect.ts
5122
+ /** The `git disconnect` command. */
5123
+ function disconnectPresentations(result) {
4506
5124
  return {
5125
+ stdout: () => [],
5126
+ json: () => result,
4507
5127
  next: () => [],
4508
5128
  human: () => [
4509
5129
  {
4510
5130
  kind: "summary",
4511
- status: "info",
4512
- text: TITLE$10
5131
+ status: "ok",
5132
+ text: "Disconnecting Git from the resolved project."
4513
5133
  },
4514
5134
  {
4515
5135
  kind: "fields",
4516
- rows: [{
4517
- label: "project",
4518
- value: result.projectName
4519
- }, ...result.branchName ? [{
4520
- label: "branch",
4521
- value: result.branchName
4522
- }] : []]
5136
+ rows: [
5137
+ {
5138
+ label: "project",
5139
+ value: result.project.name
5140
+ },
5141
+ {
5142
+ label: "workspace",
5143
+ value: result.workspace.name
5144
+ },
5145
+ {
5146
+ label: "repository",
5147
+ value: result.repositoryConnection.repository.fullName
5148
+ }
5149
+ ]
4523
5150
  },
4524
- ...rows.length === 0 ? [{
4525
- kind: "summary",
4526
- status: "info",
4527
- text: "No buckets found."
4528
- }] : [{
4529
- kind: "table",
4530
- columns: [
4531
- "Name",
4532
- "Id",
4533
- "Status",
4534
- "Branch",
4535
- "Created"
4536
- ],
4537
- rows
4538
- }]
4539
- ],
4540
- stdout: () => stdoutRows.map((row) => row.join(" ")),
4541
- json: () => serializeBucketList(result)
5151
+ {
5152
+ kind: "list",
5153
+ items: ["GitHub branch automation is no longer active for this project."]
5154
+ }
5155
+ ]
4542
5156
  };
4543
5157
  }
4544
- const bucketListCommand = defineCommand({
4545
- args: { flags: {
4546
- project: projectFlag$3,
4547
- branch: branchFlag$2
4548
- } },
5158
+ const gitDisconnectCommand = defineCommand({
5159
+ args: { flags: { project: projectFlag$2 } },
4549
5160
  help: {
4550
- summary: "List object-store buckets for the resolved project",
4551
- examples: [
4552
- "bucket list",
4553
- "bucket list --branch preview",
4554
- "bucket list --json"
4555
- ]
5161
+ summary: "Disconnect the GitHub repository from the resolved project",
5162
+ examples: ["git disconnect", "git disconnect --project proj_123"]
4556
5163
  },
4557
5164
  needs: { credentials: true },
4558
5165
  handler: async (args, ctx) => {
4559
5166
  try {
4560
- const { provider, projectId, projectName } = await resolveBucketContext(ctx, args.flags, "bucket list");
4561
- const buckets = await provider.listBuckets({
4562
- projectId,
4563
- branchName: args.flags.branch,
5167
+ const { api, target } = await resolveGitContext(ctx, args.flags.project, "git disconnect");
5168
+ const existing = await readFirstSourceRepository(api, target.project.id, ctx.signal);
5169
+ if (!existing) throw repoNotConnectedError();
5170
+ const { error, response } = await api.DELETE("/v1/source-repositories/{id}", {
5171
+ params: { path: { id: existing.id } },
4564
5172
  signal: ctx.signal
4565
5173
  });
5174
+ if (error) throw repoConnectionApiError("Failed to disconnect GitHub repository", response, error);
4566
5175
  const result = {
4567
- projectId,
4568
- projectName,
4569
- branchName: args.flags.branch ?? null,
4570
- buckets
5176
+ ...target,
5177
+ repositoryConnection: toRepositoryConnection(existing)
4571
5178
  };
4572
- return ok(ctx.present({ data: result }, listPresentations$5(result)));
4573
- } catch (error) {
4574
- const mapped = mapBucketOperationError(error);
4575
- if (mapped) return notOk(mapped);
4576
- throw error;
4577
- }
4578
- }
4579
- });
4580
- //#endregion
4581
- //#region src/lib/feedback.ts
4582
- const FEEDBACK_TIMEOUT_MS = 3e3;
4583
- const TIMEOUT_DETAIL = `The feedback service did not answer within ${FEEDBACK_TIMEOUT_MS / 1e3} seconds.`;
4584
- function isTimeout(error) {
4585
- return error instanceof Error && error.name === "TimeoutError";
4586
- }
4587
- function unreachableDetail(error) {
4588
- if (isTimeout(error)) return TIMEOUT_DETAIL;
4589
- return `The feedback service could not be reached${error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : ""}.`;
4590
- }
4591
- function unreadableBodyDetail(error) {
4592
- return isTimeout(error) ? TIMEOUT_DETAIL : "The feedback service response could not be read.";
4593
- }
5179
+ return ok(ctx.present({ data: result }, disconnectPresentations(result)));
5180
+ } catch (error) {
5181
+ const mapped = mapGitOperationError(error);
5182
+ if (mapped) return notOk(mapped);
5183
+ throw error;
5184
+ }
5185
+ }
5186
+ });
4594
5187
  //#endregion
4595
- //#region src/lib/version.ts
4596
- const requireFromHere = createRequire(import.meta.url);
4597
- /** The bundled entry sits one directory below the package root
4598
- * (`dist/cli.js`); this source file sits two below (`src/lib/`). Both
4599
- * are tried, nearest first, so the same code serves either. */
4600
- const PACKAGE_JSON_CANDIDATES = ["../package.json", "../../package.json"];
4601
- function readPackageMetadata() {
4602
- for (const candidate of PACKAGE_JSON_CANDIDATES) try {
4603
- const metadata = requireFromHere(candidate);
4604
- if (metadata.version) return metadata;
4605
- } catch {}
4606
- return {};
5188
+ //#region src/lib/skills/sync.ts
5189
+ /**
5190
+ * Brings the harness skill directories in line with the installed
5191
+ * source packages: copies each skill tree whose stamp does not match
5192
+ * the package it came from, and removes copies whose source package is
5193
+ * gone. A target directory that exists but is not this CLI's copy is
5194
+ * refused, never replaced. Doing nothing is the normal outcome and is
5195
+ * not an error.
5196
+ */
5197
+ async function syncSkills(status) {
5198
+ const synced = [];
5199
+ const refused = [];
5200
+ for (const skill of status.skills) {
5201
+ const dirs = skill.targets.filter((target) => target.state === "stale" || target.state === "absent").map((target) => target.dir);
5202
+ const refusedDirs = skill.targets.filter((target) => target.state === "unmanaged").map((target) => target.dir);
5203
+ if (refusedDirs.length > 0) refused.push({
5204
+ skill: skill.skill,
5205
+ dirs: refusedDirs
5206
+ });
5207
+ for (const target of skill.targets) if (target.state === "synced") await removeOldCliGitignore(path.join(status.projectRoot, target.dir, skill.skill, ".gitignore"));
5208
+ if (dirs.length === 0) continue;
5209
+ for (const dir of dirs) await replaceTree(skill.sourceDir, path.join(status.projectRoot, dir, skill.skill));
5210
+ synced.push({
5211
+ skill: skill.skill,
5212
+ library: skill.library,
5213
+ version: skill.version,
5214
+ dirs
5215
+ });
5216
+ }
5217
+ const pruned = [];
5218
+ for (const orphan of status.orphans) {
5219
+ for (const dir of orphan.dirs) await rm(path.join(status.projectRoot, dir, orphan.skill), {
5220
+ recursive: true,
5221
+ force: true
5222
+ });
5223
+ pruned.push({
5224
+ skill: orphan.skill,
5225
+ library: orphan.library,
5226
+ dirs: orphan.dirs
5227
+ });
5228
+ }
5229
+ return {
5230
+ projectRoot: status.projectRoot,
5231
+ packages: status.packages,
5232
+ synced,
5233
+ pruned,
5234
+ refused,
5235
+ checkDisabled: status.checkDisabled
5236
+ };
4607
5237
  }
4608
- function getCliVersion() {
4609
- const pkg = readPackageMetadata();
4610
- if (!pkg.version) throw new Error("CLI version metadata is missing from the installed package: the bundled package.json could not be read or did not contain a version field. Reinstall the CLI from the npm registry, or check your install path is intact.");
4611
- return pkg.version;
5238
+ const OLD_CLI_GITIGNORE = /^\*\r?\n?$/;
5239
+ async function removeOldCliGitignore(file) {
5240
+ let content;
5241
+ try {
5242
+ content = await readFile(file, "utf8");
5243
+ } catch {
5244
+ return;
5245
+ }
5246
+ if (OLD_CLI_GITIGNORE.test(content)) await rm(file, { force: true });
4612
5247
  }
4613
- function getCliName() {
4614
- return CLI_NAME;
5248
+ /**
5249
+ * Copies a skill tree over whatever is at the destination, so a skill
5250
+ * that lost a reference file between versions does not keep the stale
5251
+ * one. Files are read and written rather than handed to `fs.cp`,
5252
+ * because under Yarn PnP the source lives inside a zip and only the
5253
+ * patched read path can see it.
5254
+ */
5255
+ async function replaceTree(source, destination) {
5256
+ await rm(destination, {
5257
+ recursive: true,
5258
+ force: true
5259
+ });
5260
+ await copyTree(source, destination);
5261
+ }
5262
+ async function copyTree(source, destination) {
5263
+ await mkdir(destination, { recursive: true });
5264
+ for (const entry of await readdir(source, { withFileTypes: true })) {
5265
+ const from = path.join(source, entry.name);
5266
+ const to = path.join(destination, entry.name);
5267
+ if (entry.isDirectory()) {
5268
+ await copyTree(from, to);
5269
+ continue;
5270
+ }
5271
+ if (entry.isFile() || entry.isSymbolicLink()) await writeFile(to, await readFile(from));
5272
+ }
4615
5273
  }
4616
5274
  //#endregion
4617
- //#region src/commands/feedback.ts
4618
- const DEFAULT_FEEDBACK_ENDPOINT = "https://hiieirp2pwqnjvq9axzyg6d0.fra.prisma.build/feedback";
4619
- const MAX_MESSAGE_LENGTH = 4e3;
4620
- const MAX_EMAIL_LENGTH = 320;
4621
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4622
- function sendFailedError(detail) {
4623
- return new CliStructuredError("FEEDBACK.SEND_FAILED", "Feedback could not be delivered", {
4624
- why: detail,
4625
- nextActions: [{
4626
- kind: "user-choice",
4627
- label: "Check your network and rerun."
4628
- }]
4629
- });
4630
- }
4631
- function messageRequiredError() {
4632
- return new CliStructuredError("FEEDBACK.MESSAGE_REQUIRED", "Feedback message required", {
4633
- why: "The message argument is empty.",
4634
- nextActions: [{
4635
- kind: "user-choice",
4636
- label: "Pass a non-empty message."
4637
- }, {
4638
- kind: "run-command",
4639
- label: "Send feedback",
4640
- command: `${CLI_NAME} feedback "the deploy flow is great"`
4641
- }]
4642
- });
4643
- }
4644
- function messageTooLongError(length) {
4645
- return new CliStructuredError("FEEDBACK.MESSAGE_TOO_LONG", "Feedback message too long", {
4646
- why: `The message is ${length} characters; the limit is ${MAX_MESSAGE_LENGTH}.`,
4647
- nextActions: [{
4648
- kind: "user-choice",
4649
- label: "Shorten the message."
4650
- }]
4651
- });
4652
- }
4653
- function emailInvalidError(value) {
4654
- return new CliStructuredError("FEEDBACK.EMAIL_INVALID", "Invalid email", {
4655
- why: `"${value}" is not a valid email address of at most ${MAX_EMAIL_LENGTH} characters.`,
4656
- nextActions: [{
4657
- kind: "user-choice",
4658
- label: "Pass a valid address with --email, or drop the flag to stay anonymous."
5275
+ //#region src/commands/skills/presentation.ts
5276
+ function projectFields(projectRoot, checkDisabled) {
5277
+ return {
5278
+ kind: "fields",
5279
+ rows: [{
5280
+ label: "project",
5281
+ value: projectRoot
4659
5282
  }, {
4660
- kind: "run-command",
4661
- label: "Send feedback with a contact address",
4662
- command: `${CLI_NAME} feedback "please add X" --email you@example.com`
5283
+ label: "check",
5284
+ value: checkDisabled ? "disabled" : "enabled"
4663
5285
  }]
4664
- });
5286
+ };
4665
5287
  }
4666
- function feedbackPresentations(result) {
5288
+ /** Decision B: "up to date" may not over-claim — when directories were
5289
+ * refused, the summary says so in the same line, for sync and list
5290
+ * alike. */
5291
+ function unmanagedClause(count) {
5292
+ if (count === 0) return "";
5293
+ return count === 1 ? "; 1 directory is not managed by this CLI" : `; ${count} directories are not managed by this CLI`;
5294
+ }
5295
+ function syncSummary(result) {
5296
+ if (result.agents.length === 0) return "No agents are configured for skills.";
5297
+ if (result.packages.length === 0) return "No Prisma packages with agent skills are installed.";
5298
+ const refusedDirs = result.refused.reduce((count, skill) => count + skill.dirs.length, 0);
5299
+ if (result.synced.length === 0 && result.pruned.length === 0) return `Agent skills are up to date${unmanagedClause(refusedDirs)}.`;
5300
+ const synced = `${result.synced.length} skill${result.synced.length === 1 ? "" : "s"}`;
5301
+ return `${result.pruned.length === 0 ? `Synced ${synced}` : `Synced ${synced} and removed ${result.pruned.length}`}${unmanagedClause(refusedDirs)}.`;
5302
+ }
5303
+ function syncPresentations(result) {
5304
+ const syncedRows = result.synced.map((skill) => [
5305
+ skill.skill,
5306
+ skill.library,
5307
+ skill.version,
5308
+ skill.dirs.join(", ")
5309
+ ]);
5310
+ const prunedRows = result.pruned.map((skill) => [skill.skill, skill.dirs.join(", ")]);
5311
+ const refusedRows = result.refused.map((skill) => [skill.skill, skill.dirs.join(", ")]);
4667
5312
  return {
4668
- stdout: () => [],
4669
5313
  json: () => result,
4670
5314
  next: () => [],
4671
- human: () => [{
4672
- kind: "summary",
4673
- status: "ok",
4674
- text: "Feedback sent. Thank you!"
4675
- }, {
4676
- kind: "fields",
4677
- rows: [
4678
- ...result.id ? [{
4679
- label: "id",
4680
- value: result.id
4681
- }] : [],
4682
- {
4683
- label: "sent as",
4684
- value: result.email ?? "anonymous"
4685
- },
4686
- {
4687
- label: "included",
4688
- value: `CLI ${result.context.cliVersion}, ${result.context.runtime.name} ${result.context.runtime.version}, ${result.context.platform} ${result.context.arch}`
4689
- }
4690
- ]
4691
- }]
5315
+ human: () => [
5316
+ {
5317
+ kind: "summary",
5318
+ status: result.synced.length > 0 ? "ok" : "info",
5319
+ text: syncSummary(result)
5320
+ },
5321
+ projectFields(result.projectRoot, result.checkDisabled),
5322
+ ...syncedRows.length === 0 ? [] : [{
5323
+ kind: "table",
5324
+ columns: [
5325
+ "Skill",
5326
+ "Package",
5327
+ "Version",
5328
+ "Installed into"
5329
+ ],
5330
+ rows: syncedRows
5331
+ }],
5332
+ ...prunedRows.length === 0 ? [] : [{
5333
+ kind: "table",
5334
+ columns: ["Removed skill", "Removed from"],
5335
+ rows: prunedRows
5336
+ }],
5337
+ ...refusedRows.length === 0 ? [] : [{
5338
+ kind: "table",
5339
+ columns: ["Unmanaged skill", "Left untouched in"],
5340
+ rows: refusedRows
5341
+ }]
5342
+ ],
5343
+ stdout: () => syncedRows.map((row) => row.join(" "))
4692
5344
  };
4693
5345
  }
4694
- async function readServiceError(response, signal) {
4695
- let payload;
4696
- try {
4697
- payload = await response.json();
4698
- } catch (error) {
4699
- if (signal.aborted) throw error;
4700
- return "";
4701
- }
4702
- return typeof payload?.error?.message === "string" ? ` (${payload.error.message})` : "";
5346
+ function listSummary(result) {
5347
+ if (result.agents.length === 0) return "No agents are configured for skills.";
5348
+ if (result.skills.length === 0) return "No Prisma agent skills are available to sync.";
5349
+ if (!result.upToDate) return "Agent skills are out of date.";
5350
+ const unmanaged = result.skills.flatMap((skill) => skill.targets).filter((target) => target.state === "unmanaged").length;
5351
+ return `Agent skills are up to date${unmanagedClause(unmanaged)}.`;
4703
5352
  }
4704
- async function sendFeedback(endpoint, body, signal) {
4705
- try {
4706
- return await fetch(endpoint, {
4707
- method: "POST",
4708
- headers: {
4709
- "content-type": "application/json",
4710
- "user-agent": `${CLI_NAME}/${getCliVersion()}`
5353
+ function listPresentations$5(result) {
5354
+ const rows = result.skills.flatMap((skill) => skill.targets.map((target) => [
5355
+ skill.skill,
5356
+ skill.library,
5357
+ skill.version,
5358
+ target.dir,
5359
+ target.syncedVersion ?? "-",
5360
+ target.state
5361
+ ]));
5362
+ return {
5363
+ json: () => result,
5364
+ next: () => [],
5365
+ human: () => [
5366
+ {
5367
+ kind: "summary",
5368
+ status: "info",
5369
+ text: listSummary(result)
4711
5370
  },
4712
- body: JSON.stringify(body),
4713
- signal: AbortSignal.any([signal, AbortSignal.timeout(FEEDBACK_TIMEOUT_MS)])
4714
- });
4715
- } catch (error) {
4716
- if (signal.aborted) throw error;
4717
- throw sendFailedError(unreachableDetail(error));
4718
- }
5371
+ projectFields(result.projectRoot, result.checkDisabled),
5372
+ ...rows.length === 0 ? [] : [{
5373
+ kind: "table",
5374
+ columns: [
5375
+ "Skill",
5376
+ "Package",
5377
+ "Installed",
5378
+ "Directory",
5379
+ "Synced",
5380
+ "State"
5381
+ ],
5382
+ rows
5383
+ }]
5384
+ ],
5385
+ stdout: () => rows.map((row) => row.join(" "))
5386
+ };
4719
5387
  }
4720
- async function readSubmissionId(response, signal) {
4721
- let payload;
4722
- try {
4723
- payload = await response.json();
4724
- } catch (error) {
4725
- if (signal.aborted) throw error;
4726
- if (!(error instanceof SyntaxError)) throw sendFailedError(unreadableBodyDetail(error));
4727
- payload = null;
4728
- }
4729
- return typeof payload?.id === "string" ? payload.id : null;
5388
+ //#endregion
5389
+ //#region src/commands/skills/sync.ts
5390
+ function packageReports(packages) {
5391
+ return packages.map((installed) => ({
5392
+ package: installed.name,
5393
+ version: installed.version,
5394
+ conflictingVersions: installed.conflictingVersions
5395
+ }));
4730
5396
  }
4731
- async function postFeedback(endpoint, body, signal) {
4732
- const response = await sendFeedback(endpoint, body, signal);
4733
- if (!response.ok) throw sendFailedError(`The feedback service responded with HTTP ${response.status}${await readServiceError(response, signal)}.`);
4734
- return readSubmissionId(response, signal);
5397
+ /** Workspace members that pin different versions of the same
5398
+ * skill-bearing package: the highest wins, and the user hears about
5399
+ * it, because the losing members get a skill describing a version
5400
+ * they did not install. */
5401
+ function versionConflictDiagnostics(packages) {
5402
+ return packages.filter((installed) => installed.conflictingVersions.length > 1).map((installed) => ({
5403
+ code: "SKILLS.VERSION_CONFLICT",
5404
+ severity: "warn",
5405
+ summary: `Workspace members install different versions of ${installed.name} (${installed.conflictingVersions.join(", ")}); the skills for ${installed.version} were installed.`,
5406
+ nextActions: [{
5407
+ kind: "user-choice",
5408
+ label: `Pin one version of ${installed.name} across the workspace.`
5409
+ }]
5410
+ }));
4735
5411
  }
4736
- const feedbackCommand = defineCommand({
4737
- help: {
4738
- summary: "Send feedback to the Prisma CLI team",
4739
- description: "Anonymous unless --email is passed. Every submission includes the CLI\nversion, node version, and OS platform/arch, and nothing else.",
4740
- examples: ["feedback \"the deploy flow is great\"", "feedback \"please add X\" --email you@example.com"]
4741
- },
4742
- args: {
4743
- flags: { email: flag.string({
4744
- brief: "Contact email if you want a reply; feedback is anonymous without it",
4745
- placeholder: "address"
4746
- }) },
4747
- positionals: { message: positional.string({
4748
- brief: "Feedback text (up to 4000 characters)",
4749
- placeholder: "message"
4750
- }) }
4751
- },
4752
- handler: async (args, ctx) => {
4753
- const message = args.positionals.message.trim();
4754
- if (!message) throw messageRequiredError();
4755
- if (message.length > MAX_MESSAGE_LENGTH) throw messageTooLongError(message.length);
4756
- const email = args.flags.email?.trim();
4757
- if (email !== void 0 && (email.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(email))) throw emailInvalidError(args.flags.email ?? "");
4758
- const context = {
4759
- cliVersion: getCliVersion(),
4760
- runtime: { ...ctx.host.runtime },
4761
- platform: ctx.host.platform,
4762
- arch: ctx.host.arch
4763
- };
4764
- const result = {
4765
- id: await postFeedback(ctx.env.PRISMA_CLI_FEEDBACK_URL || DEFAULT_FEEDBACK_ENDPOINT, {
4766
- message,
4767
- ...email ? { email } : {},
4768
- meta: { ...context }
4769
- }, ctx.signal),
4770
- email: email ?? null,
4771
- context
4772
- };
4773
- return ok(ctx.present({ data: result }, feedbackPresentations(result)));
4774
- }
4775
- });
4776
- //#endregion
4777
- //#region src/adapters/git.ts
4778
- const execFileAsync = promisify(execFile);
4779
- async function readGitOriginRemote(cwd, signal) {
4780
- try {
4781
- const { stdout } = await execFileAsync("git", [
4782
- "config",
4783
- "--get",
4784
- "remote.origin.url"
4785
- ], {
4786
- cwd,
4787
- timeout: 5e3,
4788
- signal
4789
- });
4790
- const remote = stdout.trim();
4791
- return remote.length > 0 ? remote : null;
4792
- } catch (error) {
4793
- if (signal?.aborted || isAbortError(error)) throw error;
4794
- return null;
4795
- }
5412
+ /** Target directories that already hold a skill this CLI does not
5413
+ * manage: sync leaves them alone, and the user hears why the packaged
5414
+ * skill was not installed there. */
5415
+ function unmanagedDirectoryDiagnostics(refused) {
5416
+ return refused.flatMap((entry) => entry.dirs.map((dir) => ({
5417
+ code: "SKILLS.UNMANAGED_DIRECTORY",
5418
+ severity: "warn",
5419
+ summary: `${dir}/${entry.skill} is not managed by this CLI, so it was left untouched.`,
5420
+ nextActions: [{
5421
+ kind: "user-choice",
5422
+ label: `Move or remove ${dir}/${entry.skill}, then rerun skills sync to install the packaged skill.`
5423
+ }]
5424
+ })));
4796
5425
  }
4797
- function isAbortError(error) {
4798
- return error instanceof Error && error.name === "AbortError";
5426
+ function bothSwitchesError() {
5427
+ return new CliStructuredError("CLI.INVALID_ARGUMENTS", "--disable and --enable ask for opposite things, so only one may be given.", { nextActions: [{
5428
+ kind: "user-choice",
5429
+ label: "Run with --disable to silence the skills check, or --enable to restore it."
5430
+ }] });
4799
5431
  }
4800
- function parseGitHubRepositoryUrl(value) {
4801
- const input = value.trim();
4802
- const shorthand = input.match(/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
4803
- if (shorthand) return toGitHubRepositoryReference(shorthand[1], shorthand[2]);
4804
- let parsed;
4805
- try {
4806
- parsed = new URL(input);
4807
- } catch {
4808
- return null;
5432
+ const skillsSyncCommand = defineCommand({
5433
+ help: {
5434
+ summary: "Copy the agent skills from installed Prisma packages into this project",
5435
+ description: "Skills come from the Prisma packages the project installs, so they always describe the version in use. Sync copies them into the skill directories the agent harnesses read, and removes copies whose package is gone. It does nothing, and exits 0, when everything is already current.",
5436
+ examples: ["skills sync", "skills sync --disable"]
5437
+ },
5438
+ needs: { config: skillsConfigSection },
5439
+ args: { flags: {
5440
+ disable: flag.boolean({ brief: "Stop other commands reporting out-of-date skills in this project" }),
5441
+ enable: flag.boolean({ brief: "Undo --disable for this project" })
5442
+ } },
5443
+ handler: async (args, ctx) => {
5444
+ if (args.flags.disable && args.flags.enable) return notOk(bothSwitchesError());
5445
+ const outcome = await syncSkills(await readSkillsStatus(ctx.cwd, { agents: ctx.config.agents }));
5446
+ let optedOut = outcome.checkDisabled;
5447
+ if (args.flags.disable || args.flags.enable) {
5448
+ optedOut = args.flags.disable;
5449
+ await writeSkillsCheckDisabled(outcome.projectRoot, optedOut);
5450
+ }
5451
+ const checkDisabled = optedOut || !ctx.config.check;
5452
+ const result = {
5453
+ projectRoot: outcome.projectRoot,
5454
+ agents: ctx.config.agents,
5455
+ packages: packageReports(outcome.packages),
5456
+ synced: outcome.synced,
5457
+ pruned: outcome.pruned,
5458
+ refused: outcome.refused,
5459
+ checkDisabled
5460
+ };
5461
+ return ok(ctx.present({
5462
+ data: result,
5463
+ diagnostics: [...versionConflictDiagnostics(outcome.packages), ...unmanagedDirectoryDiagnostics(outcome.refused)]
5464
+ }, syncPresentations(result)));
4809
5465
  }
4810
- if (parsed.hostname !== "github.com") return null;
4811
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:" && parsed.protocol !== "ssh:") return null;
4812
- const parts = parsed.pathname.split("/").filter(Boolean);
4813
- if (parts.length !== 2) return null;
4814
- const [owner, rawName] = parts;
4815
- return toGitHubRepositoryReference(owner, rawName.endsWith(".git") ? rawName.slice(0, -4) : rawName);
5466
+ });
5467
+ //#endregion
5468
+ //#region src/commands/init.ts
5469
+ const POSTINSTALL_SCRIPT = "prisma skills sync || exit 0";
5470
+ function summary(status, text) {
5471
+ return {
5472
+ kind: "summary",
5473
+ status,
5474
+ text
5475
+ };
4816
5476
  }
4817
- function toGitHubRepositoryReference(owner, name) {
4818
- if (!owner || !name || owner.includes("/") || name.includes("/")) return null;
5477
+ const APPEND_ADVICE = {
5478
+ kind: "user-choice",
5479
+ label: `Append "${POSTINSTALL_SCRIPT}" to your postinstall script yourself to resync the skills on every install.`
5480
+ };
5481
+ function noPackageJsonDiagnostic() {
4819
5482
  return {
4820
- provider: "github",
4821
- owner,
4822
- name,
4823
- fullName: `${owner}/${name}`,
4824
- url: `https://github.com/${owner}/${name}`
5483
+ code: "INIT.NO_PACKAGE_JSON",
5484
+ severity: "warn",
5485
+ summary: "There is no package.json in this directory, so the postinstall hook was not added.",
5486
+ nextActions: [{
5487
+ kind: "user-choice",
5488
+ label: `Run ${CLI_NAME} init from the directory that holds your package.json.`
5489
+ }]
4825
5490
  };
4826
5491
  }
4827
- //#endregion
4828
- //#region src/presenters/project.ts
4829
- function serializeProjectList(result) {
5492
+ function unreadablePackageJsonDiagnostic() {
4830
5493
  return {
4831
- ...serializeList({
4832
- context: { workspace: result.workspace.name },
4833
- items: result.projects.map((project) => ({
4834
- noun: "project",
4835
- label: project.name,
4836
- id: project.id,
4837
- status: null
4838
- }))
4839
- }),
4840
- localBinding: result.localBinding ?? null
5494
+ code: "INIT.PACKAGE_JSON_UNREADABLE",
5495
+ severity: "warn",
5496
+ summary: "package.json could not be parsed, so the postinstall hook was not added.",
5497
+ nextActions: [APPEND_ADVICE]
4841
5498
  };
4842
5499
  }
4843
- function serializeProjectSetup(result) {
4844
- return result;
5500
+ function unwritablePackageJsonDiagnostic() {
5501
+ return {
5502
+ code: "INIT.PACKAGE_JSON_UNWRITABLE",
5503
+ severity: "warn",
5504
+ summary: "package.json could not be written, so the postinstall hook was not added.",
5505
+ nextActions: [APPEND_ADVICE]
5506
+ };
4845
5507
  }
4846
- function formatGitConnectionDetail(status) {
4847
- switch (status) {
4848
- case "active": return "GitHub branch automation is active for this project.";
4849
- case "pending": return "GitHub branch automation is pending GitHub App installation.";
4850
- case "archived": return "GitHub branch automation has been archived for this project.";
4851
- default: return "GitHub repository is connected, but branch automation is not active.";
4852
- }
5508
+ function scriptsNotAnObjectDiagnostic() {
5509
+ return {
5510
+ code: "INIT.SCRIPTS_NOT_AN_OBJECT",
5511
+ severity: "warn",
5512
+ summary: "The scripts field in package.json is not an object, so init left it alone.",
5513
+ nextActions: [APPEND_ADVICE]
5514
+ };
4853
5515
  }
4854
- //#endregion
4855
- //#region src/commands/git/context.ts
4856
- /** Workspace, project and the source-repository client for the
4857
- * `git *` commands. */
4858
- const projectFlag$2 = flag.string({
4859
- brief: "Project id or name",
4860
- placeholder: "id-or-name"
4861
- });
4862
- async function resolveGitContext(ctx, explicitProject, commandName) {
4863
- const target = await resolvePinnedProject(ctx, await resolveActiveWorkspace(ctx), explicitProject, commandName);
5516
+ function foreignPostinstallDiagnostic() {
4864
5517
  return {
4865
- api: ctx.api,
4866
- target
5518
+ code: "INIT.POSTINSTALL_KEPT",
5519
+ severity: "warn",
5520
+ summary: "package.json already has a postinstall script, so init left it alone.",
5521
+ nextActions: [APPEND_ADVICE]
4867
5522
  };
4868
5523
  }
4869
- //#endregion
4870
- //#region src/commands/git/errors.ts
4871
- const GIT_CODE_MAP = {
4872
- USAGE_ERROR: "GIT.USAGE_ERROR",
4873
- REPO_PROVIDER_UNSUPPORTED: "GIT.REPO_PROVIDER_UNSUPPORTED",
4874
- REPO_ALREADY_CONNECTED: "GIT.REPO_ALREADY_CONNECTED",
4875
- REPO_INSTALLATION_REQUIRED: "GIT.REPO_INSTALLATION_REQUIRED",
4876
- REPO_NOT_ACCESSIBLE: "GIT.REPO_NOT_ACCESSIBLE",
4877
- REPO_NOT_CONNECTED: "GIT.REPO_NOT_CONNECTED",
4878
- REPO_CONNECTION_FAILED: "GIT.REPO_CONNECTION_FAILED"
4879
- };
4880
- /** The project-resolution codes the git commands can raise; they keep
4881
- * the project group's dotted codes and copy. */
4882
- const PROJECT_CODES$1 = new Set([
4883
- "PROJECT_NOT_FOUND",
4884
- "PROJECT_AMBIGUOUS",
4885
- "PROJECT_SETUP_REQUIRED",
4886
- "LOCAL_STATE_STALE",
4887
- "LOCAL_PROJECT_WORKSPACE_MISMATCH"
4888
- ]);
4889
- const STALE_INTERACTIVE_SIGN_IN$1 = /, or rerun the command in a TTY to sign in interactively\./g;
4890
- const TRACE_FLAG$1 = /--trace/g;
4891
- /** `--trace` is gone in this CLI; the log level replaces it. Interactive
4892
- * sign-in is gone too (R-S2b-2), so the legacy offer to rerun in a TTY
4893
- * describes something this CLI cannot do; `auth login` is the whole remedy. */
4894
- function portFixText$1(fix) {
4895
- return fix.replace(TRACE_FLAG$1, "--log-level verbose").replace(STALE_INTERACTIVE_SIGN_IN$1, ".");
5524
+ function configSnippet(agents) {
5525
+ return `skills: { agents: [${agents.map((agent) => `"${agent}"`).join(", ")}] }`;
4896
5526
  }
4897
- /** The install-required and not-accessible errors put the raw install
4898
- * URL in their nextSteps beside real commands. A URL is not a command,
4899
- * so it becomes an `open-url` action. */
4900
- function nextStepAction(step) {
4901
- if (step.startsWith("https://") || step.startsWith("http://")) return {
4902
- kind: "open-url",
4903
- label: step,
4904
- url: step
5527
+ /** The same never-touch discipline as the postinstall step's
5528
+ * foreign-script rule: a prisma.config.ts the user already has is
5529
+ * theirs, and init only says what to add. */
5530
+ function configKeptDiagnostic(agents) {
5531
+ return {
5532
+ code: "INIT.CONFIG_KEPT",
5533
+ severity: "warn",
5534
+ summary: "prisma.config.ts already exists, so init left it alone instead of writing the skills section.",
5535
+ nextActions: [{
5536
+ kind: "user-choice",
5537
+ label: `Add ${configSnippet(agents)} to the object passed to definePrismaConfig in prisma.config.ts.`
5538
+ }]
4905
5539
  };
4906
- const command = portCommandString(step);
5540
+ }
5541
+ function configUnwritableDiagnostic(agents) {
4907
5542
  return {
4908
- kind: "run-command",
4909
- label: command,
4910
- command
5543
+ code: "INIT.CONFIG_UNWRITABLE",
5544
+ severity: "warn",
5545
+ summary: "prisma.config.ts could not be written, so init skipped it.",
5546
+ nextActions: [{
5547
+ kind: "user-choice",
5548
+ label: `Create a prisma.config.ts whose definePrismaConfig call carries ${configSnippet(agents)}.`
5549
+ }]
4911
5550
  };
4912
5551
  }
4913
- function nextActionsFor$1(error) {
4914
- return [...error.fix ? [{
4915
- kind: "user-choice",
4916
- label: portFixText$1(error.fix)
4917
- }] : [], ...error.nextSteps.map(nextStepAction)];
5552
+ function skillsSyncFailedDiagnostic(cause) {
5553
+ return {
5554
+ code: "INIT.SKILLS_SYNC_FAILED",
5555
+ severity: "warn",
5556
+ summary: `The agent skills could not be synced: ${cause instanceof Error ? cause.message : String(cause)}`,
5557
+ nextActions: [{
5558
+ kind: "run-command",
5559
+ label: "Retry the sync on its own",
5560
+ command: `${CLI_NAME} skills sync`
5561
+ }]
5562
+ };
4918
5563
  }
4919
- /** The legacy errors branch their fix text on whether a browser was
4920
- * opened. The engine's browser wait always shows the URL, so the
4921
- * opened branch is the one that describes what this CLI does. */
4922
- const BROWSER_OPENED = true;
4923
- /**
4924
- * The install wait's two terminal outcomes. The legacy constructors own
4925
- * every copy string; the design drops `opened` from the meta they build
4926
- * (`browserWait` does not report it and the URL is always shown), so
4927
- * the structured error is assembled from their fields with the meta
4928
- * d3 §3.8 pins.
4929
- */
4930
- function installWaitFailedError(repository, installUrl, inspectableInstallationCount) {
4931
- const legacy = inspectableInstallationCount > 0 ? repoNotAccessibleError(repository, installUrl, BROWSER_OPENED) : repoInstallationRequiredError(repository, installUrl, BROWSER_OPENED);
4932
- return new CliStructuredError(GIT_CODE_MAP[legacy.code], legacy.summary, {
4933
- why: legacy.why ?? void 0,
4934
- meta: {
4935
- repository: repository.fullName,
4936
- installUrl
5564
+ const FIRST_INDENT = /\n([ \t]+)"/;
5565
+ /** The indentation the file already uses, so the rewrite matches it. */
5566
+ function detectIndent(source) {
5567
+ return FIRST_INDENT.exec(source)?.[1] ?? " ";
5568
+ }
5569
+ const BOM = "";
5570
+ function isPlainObject(value) {
5571
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5572
+ }
5573
+ function parseManifestObject(source) {
5574
+ try {
5575
+ const parsed = JSON.parse(source);
5576
+ return isPlainObject(parsed) ? parsed : null;
5577
+ } catch {
5578
+ return null;
5579
+ }
5580
+ }
5581
+ function renderManifest(manifest, source, bom, crlf) {
5582
+ let rewritten = JSON.stringify(manifest, null, detectIndent(source));
5583
+ if (crlf) rewritten = rewritten.replaceAll("\n", "\r\n");
5584
+ const eol = crlf ? "\r\n" : "\n";
5585
+ return `${bom}${rewritten}${source.endsWith("\n") ? eol : ""}`;
5586
+ }
5587
+ async function addPostinstallHook(cwd) {
5588
+ const manifestPath = path.join(cwd, "package.json");
5589
+ let raw;
5590
+ try {
5591
+ raw = await readFile(manifestPath, "utf8");
5592
+ } catch {
5593
+ return {
5594
+ report: {
5595
+ outcome: "skipped",
5596
+ script: null
5597
+ },
5598
+ line: summary("warn", "No package.json here; postinstall hook skipped."),
5599
+ diagnostics: [noPackageJsonDiagnostic()]
5600
+ };
5601
+ }
5602
+ const bom = raw.startsWith(BOM) ? BOM : "";
5603
+ const source = bom === "" ? raw : raw.slice(1);
5604
+ const crlf = source.includes("\r\n");
5605
+ const manifest = parseManifestObject(source);
5606
+ if (manifest === null) return {
5607
+ report: {
5608
+ outcome: "skipped",
5609
+ script: null
4937
5610
  },
4938
- nextActions: nextActionsFor$1(legacy)
4939
- });
5611
+ line: summary("warn", "package.json could not be parsed; postinstall hook skipped."),
5612
+ diagnostics: [unreadablePackageJsonDiagnostic()]
5613
+ };
5614
+ if (manifest.scripts !== void 0 && !isPlainObject(manifest.scripts)) return {
5615
+ report: {
5616
+ outcome: "kept",
5617
+ script: null
5618
+ },
5619
+ line: summary("warn", "The scripts field in package.json is not an object; left untouched."),
5620
+ diagnostics: [scriptsNotAnObjectDiagnostic()]
5621
+ };
5622
+ const scripts = manifest.scripts ?? {};
5623
+ const existing = scripts.postinstall;
5624
+ if (existing === "prisma skills sync || exit 0") return {
5625
+ report: {
5626
+ outcome: "exists",
5627
+ script: POSTINSTALL_SCRIPT
5628
+ },
5629
+ line: summary("info", "The postinstall hook is already in package.json."),
5630
+ diagnostics: []
5631
+ };
5632
+ if (existing !== void 0) return {
5633
+ report: {
5634
+ outcome: "kept",
5635
+ script: typeof existing === "string" ? existing : null
5636
+ },
5637
+ line: summary("warn", "package.json has its own postinstall script; left untouched."),
5638
+ diagnostics: [foreignPostinstallDiagnostic()]
5639
+ };
5640
+ manifest.scripts = {
5641
+ ...scripts,
5642
+ postinstall: POSTINSTALL_SCRIPT
5643
+ };
5644
+ try {
5645
+ await writeFile(manifestPath, renderManifest(manifest, source, bom, crlf), "utf8");
5646
+ } catch {
5647
+ return {
5648
+ report: {
5649
+ outcome: "skipped",
5650
+ script: null
5651
+ },
5652
+ line: summary("warn", "package.json could not be written; postinstall hook skipped."),
5653
+ diagnostics: [unwritablePackageJsonDiagnostic()]
5654
+ };
5655
+ }
5656
+ return {
5657
+ report: {
5658
+ outcome: "added",
5659
+ script: POSTINSTALL_SCRIPT
5660
+ },
5661
+ line: summary("ok", `Added "postinstall": "${POSTINSTALL_SCRIPT}" to package.json.`),
5662
+ diagnostics: []
5663
+ };
4940
5664
  }
4941
- function mapGitOperationError(error) {
4942
- if (!(error instanceof CliError)) return null;
4943
- if (PROJECT_CODES$1.has(error.code)) return mapProjectOperationError(error);
4944
- return new CliStructuredError(GIT_CODE_MAP[error.code] ?? `GIT.${error.code}`, error.summary, {
4945
- why: error.why ?? void 0,
4946
- meta: Object.keys(error.meta).length > 0 ? error.meta : void 0,
4947
- nextActions: nextActionsFor$1(error)
4948
- });
5665
+ /** What the scaffold contains: the effective agents list, spelled the
5666
+ * way a user would write it by hand. */
5667
+ function renderConfigScaffold(agents) {
5668
+ return [
5669
+ "import { definePrismaConfig } from \"prisma/config\";",
5670
+ "",
5671
+ "export default definePrismaConfig({",
5672
+ " skills: {",
5673
+ ` agents: [${agents.map((agent) => `"${agent}"`).join(", ")}],`,
5674
+ " },",
5675
+ "});",
5676
+ ""
5677
+ ].join("\n");
4949
5678
  }
4950
- //#endregion
4951
- //#region src/commands/git/connect.ts
4952
- /** The `git connect` command. */
4953
- /** The legacy wait line, printed once before the poll loop. */
4954
- const WAIT_MESSAGE = "Waiting for GitHub App installation or repository access approval...";
4955
- /**
4956
- * The legacy `resolveInstalledRepository`: find the repository in the
4957
- * workspace's GitHub App installations, and when it is not there yet,
4958
- * send the user to an install intent and wait for them to finish. The
4959
- * engine owns the announcement, the browser and the polling clock; this
4960
- * only supplies the address, the cadence and the question being polled.
4961
- */
4962
- async function resolveInstalledRepository(ctx, api, workspaceId, repository) {
4963
- const inspect = async (signal) => findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId, signal), repository, signal);
4964
- const first = await inspect(ctx.signal);
4965
- if (first.match) return first.match;
4966
- const installUrl = await createGitHubInstallIntent(api, workspaceId, ctx.signal);
4967
- let match = null;
4968
- let inspectableInstallationCount = 0;
5679
+ async function scaffoldConfigStep(cwd, agents, agentsConfigured) {
5680
+ const configPath = path.join(cwd, "prisma.config.ts");
5681
+ let existing = null;
4969
5682
  try {
4970
- await ctx.prompt.browserWait({
4971
- url: installUrl,
4972
- message: WAIT_MESSAGE,
4973
- timeout: readPositiveIntegerEnv(ctx.env.PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS, GITHUB_INSTALL_POLL_TIMEOUT_MS),
4974
- interval: readPositiveIntegerEnv(ctx.env.PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS, GITHUB_INSTALL_POLL_INTERVAL_MS),
4975
- poll: async (signal) => {
4976
- const lookup = await inspect(signal);
4977
- match = lookup.match;
4978
- inspectableInstallationCount = lookup.inspectableInstallationCount;
4979
- return lookup.match !== null;
4980
- }
5683
+ existing = await readFile(configPath, "utf8");
5684
+ } catch {
5685
+ existing = null;
5686
+ }
5687
+ if (existing !== null) return {
5688
+ report: {
5689
+ outcome: "exists",
5690
+ agents: null
5691
+ },
5692
+ line: agentsConfigured ? summary("info", "prisma.config.ts already configures skills.agents.") : summary("warn", "prisma.config.ts already exists; left untouched."),
5693
+ diagnostics: agentsConfigured ? [] : [configKeptDiagnostic(agents)]
5694
+ };
5695
+ try {
5696
+ await writeFile(configPath, renderConfigScaffold(agents), {
5697
+ encoding: "utf8",
5698
+ flag: "wx"
4981
5699
  });
4982
- } catch (error) {
4983
- if (CliStructuredError.is(error) && error.code === "CLI.BROWSER_WAIT_TIMEOUT") throw installWaitFailedError(repository, installUrl, inspectableInstallationCount);
4984
- throw error;
5700
+ } catch {
5701
+ return {
5702
+ report: {
5703
+ outcome: "skipped",
5704
+ agents: null
5705
+ },
5706
+ line: summary("warn", "prisma.config.ts could not be written; skipped."),
5707
+ diagnostics: [configUnwritableDiagnostic(agents)]
5708
+ };
4985
5709
  }
4986
- if (match === null) throw installWaitFailedError(repository, installUrl, inspectableInstallationCount);
4987
- return match;
4988
- }
4989
- function connectPresentations(result) {
4990
- const connection = result.repositoryConnection;
4991
5710
  return {
4992
- stdout: () => [],
4993
- json: () => result,
4994
- next: () => [],
4995
- human: () => [
4996
- {
4997
- kind: "summary",
4998
- status: "ok",
4999
- text: "Connecting Git to the resolved project."
5711
+ report: {
5712
+ outcome: "created",
5713
+ agents
5714
+ },
5715
+ line: summary("ok", `Created prisma.config.ts with ${configSnippet(agents)}.`),
5716
+ diagnostics: []
5717
+ };
5718
+ }
5719
+ async function syncSkillsStep(cwd, agents, checkEnabledByConfig) {
5720
+ try {
5721
+ const outcome = await syncSkills(await readSkillsStatus(cwd, { agents }));
5722
+ const result = {
5723
+ projectRoot: outcome.projectRoot,
5724
+ agents,
5725
+ packages: packageReports(outcome.packages),
5726
+ synced: outcome.synced,
5727
+ pruned: outcome.pruned,
5728
+ refused: outcome.refused,
5729
+ checkDisabled: outcome.checkDisabled || !checkEnabledByConfig
5730
+ };
5731
+ return {
5732
+ report: {
5733
+ outcome: result.synced.length > 0 || result.pruned.length > 0 ? "synced" : "up-to-date",
5734
+ sync: result
5000
5735
  },
5001
- {
5002
- kind: "fields",
5003
- rows: [
5004
- {
5005
- label: "project",
5006
- value: result.project.name
5007
- },
5008
- {
5009
- label: "workspace",
5010
- value: result.workspace.name
5011
- },
5012
- {
5013
- label: "repository",
5014
- value: connection.repository.fullName
5015
- },
5016
- {
5017
- label: "status",
5018
- value: connection.status
5019
- }
5020
- ]
5736
+ line: null,
5737
+ diagnostics: [...versionConflictDiagnostics(outcome.packages), ...unmanagedDirectoryDiagnostics(outcome.refused)]
5738
+ };
5739
+ } catch (cause) {
5740
+ return {
5741
+ report: {
5742
+ outcome: "failed",
5743
+ sync: null
5021
5744
  },
5022
- {
5023
- kind: "list",
5024
- items: [formatGitConnectionDetail(connection.status)]
5025
- }
5026
- ]
5027
- };
5745
+ line: summary("warn", "The agent skills could not be synced."),
5746
+ diagnostics: [skillsSyncFailedDiagnostic(cause)]
5747
+ };
5748
+ }
5028
5749
  }
5029
- const gitConnectCommand = defineCommand({
5030
- args: {
5031
- positionals: { gitUrl: positional.optionalString({
5032
- brief: "GitHub repository URL",
5033
- placeholder: "git-url"
5034
- }) },
5035
- flags: { project: projectFlag$2 }
5750
+ const SKIPPED_POSTINSTALL = {
5751
+ report: {
5752
+ outcome: "skipped",
5753
+ script: null
5036
5754
  },
5037
- help: {
5038
- summary: "Connect the resolved project to a GitHub repository",
5039
- examples: [
5040
- "git connect",
5041
- "git connect git@github.com:prisma/prisma-cli.git",
5042
- "git connect --project proj_123"
5043
- ]
5755
+ line: summary("info", "Skipped the postinstall hook (--no-postinstall)."),
5756
+ diagnostics: []
5757
+ };
5758
+ const SKIPPED_SKILLS = {
5759
+ report: {
5760
+ outcome: "skipped",
5761
+ sync: null
5044
5762
  },
5045
- needs: { credentials: true },
5046
- handler: async (args, ctx) => {
5047
- try {
5048
- const { api, target } = await resolveGitContext(ctx, args.flags.project, "git connect");
5049
- const remoteUrl = args.positionals.gitUrl ?? await readGitOriginRemote(ctx.cwd, ctx.signal);
5050
- if (!remoteUrl) throw usageError("Repository connection requires a GitHub repository URL", "No git-url was provided and the local repo does not have an origin remote.", `Pass a GitHub repository URL, or add a GitHub origin remote and rerun ${CLI_NAME} git connect.`, [`${CLI_NAME} git connect git@github.com:prisma/prisma-cli.git`], "project");
5051
- const repository = parseGitHubRepositoryUrl(remoteUrl);
5052
- if (!repository) throw unsupportedRepositoryProviderError();
5053
- const existing = await readFirstSourceRepository(api, target.project.id, ctx.signal);
5054
- if (existing) {
5055
- const existingConnection = toRepositoryConnection(existing);
5056
- if (!repositoryFullNamesMatch(existingConnection.repository.fullName, repository.fullName)) throw repoAlreadyConnectedError(existingConnection.repository.fullName);
5057
- const idempotent = {
5058
- ...target,
5059
- repositoryConnection: existingConnection
5060
- };
5061
- return ok(ctx.present({ data: idempotent }, connectPresentations(idempotent)));
5062
- }
5063
- const installed = await resolveInstalledRepository(ctx, api, target.workspace.id, repository);
5064
- const { data, error, response } = await api.POST("/v1/source-repositories", {
5065
- body: {
5066
- projectId: target.project.id,
5067
- provider: "github",
5068
- providerRepositoryId: installed.repository.id,
5069
- installationId: installed.installation.id
5070
- },
5071
- signal: ctx.signal
5072
- });
5073
- if (error || !data) throw repoConnectionApiError("Failed to connect GitHub repository", response, error);
5074
- const result = {
5075
- ...target,
5076
- repositoryConnection: toRepositoryConnection(data.data)
5077
- };
5078
- return ok(ctx.present({ data: result }, connectPresentations(result)));
5079
- } catch (error) {
5080
- const mapped = mapGitOperationError(error);
5081
- if (mapped) return notOk(mapped);
5082
- throw error;
5083
- }
5763
+ line: summary("info", "Skipped the skills sync (--skills=none)."),
5764
+ diagnostics: []
5765
+ };
5766
+ const SKIP_SENTINEL = "none";
5767
+ function invalidSkillsFlagError(problem) {
5768
+ return new CliStructuredError("CLI.INVALID_ARGUMENTS", problem, { nextActions: [{
5769
+ kind: "user-choice",
5770
+ label: `Pass --skills a comma-separated list of agents (${KNOWN_AGENTS.join(", ")}), or --skills=${SKIP_SENTINEL} to record that no agent skills are wanted.`
5771
+ }] });
5772
+ }
5773
+ /** `--skills`: absent defers to the config's agents (every known agent
5774
+ * when there is no config); `none` records the choice — the scaffold
5775
+ * gets `agents: []` and the sync is skipped; otherwise a
5776
+ * comma-separated list of agent names. */
5777
+ function parseSkillsFlag(raw, configured) {
5778
+ if (raw === void 0) return {
5779
+ kind: "agents",
5780
+ agents: configured
5781
+ };
5782
+ const names = raw.split(",").map((name) => name.trim()).filter((name) => name !== "");
5783
+ if (names.includes(SKIP_SENTINEL)) return names.length === 1 ? { kind: "skip" } : {
5784
+ kind: "invalid",
5785
+ error: invalidSkillsFlagError(`--skills=${SKIP_SENTINEL} records that no agent skills are wanted, so it cannot be combined with agent names.`)
5786
+ };
5787
+ const agents = [];
5788
+ for (const name of names) {
5789
+ if (!isKnownAgent(name)) return {
5790
+ kind: "invalid",
5791
+ error: invalidSkillsFlagError(`--skills names '${name}', which this CLI does not know. The known agents are ${KNOWN_AGENTS.join(", ")}.`)
5792
+ };
5793
+ if (!agents.includes(name)) agents.push(name);
5084
5794
  }
5085
- });
5086
- //#endregion
5087
- //#region src/commands/git/disconnect.ts
5088
- /** The `git disconnect` command. */
5089
- function disconnectPresentations(result) {
5795
+ if (agents.length === 0) return {
5796
+ kind: "invalid",
5797
+ error: invalidSkillsFlagError("--skills was given no agent names.")
5798
+ };
5799
+ return {
5800
+ kind: "agents",
5801
+ agents
5802
+ };
5803
+ }
5804
+ function initPresentations(result, postinstall, config, skills) {
5090
5805
  return {
5091
- stdout: () => [],
5092
5806
  json: () => result,
5093
5807
  next: () => [],
5094
- human: () => [
5095
- {
5096
- kind: "summary",
5097
- status: "ok",
5098
- text: "Disconnecting Git from the resolved project."
5099
- },
5100
- {
5101
- kind: "fields",
5102
- rows: [
5103
- {
5104
- label: "project",
5105
- value: result.project.name
5106
- },
5107
- {
5108
- label: "workspace",
5109
- value: result.workspace.name
5110
- },
5111
- {
5112
- label: "repository",
5113
- value: result.repositoryConnection.repository.fullName
5114
- }
5115
- ]
5116
- },
5117
- {
5118
- kind: "list",
5119
- items: ["GitHub branch automation is no longer active for this project."]
5120
- }
5121
- ]
5808
+ stdout: () => [],
5809
+ human: (ui) => {
5810
+ const skillsBlocks = result.skills.sync === null ? [] : syncPresentations(result.skills.sync).human(ui);
5811
+ return [
5812
+ ...postinstall.line === null ? [] : [postinstall.line],
5813
+ ...config.line === null ? [] : [config.line],
5814
+ ...skills.line === null ? skillsBlocks : [skills.line]
5815
+ ];
5816
+ }
5122
5817
  };
5123
5818
  }
5124
- const gitDisconnectCommand = defineCommand({
5125
- args: { flags: { project: projectFlag$2 } },
5819
+ const initCommand = defineCommand({
5126
5820
  help: {
5127
- summary: "Disconnect the GitHub repository from the resolved project",
5128
- examples: ["git disconnect", "git disconnect --project proj_123"]
5821
+ summary: "Prepare this repository for Prisma development",
5822
+ description: "Runs locally and calls no platform API. Adds a postinstall script to package.json that keeps the Prisma agent skills in sync on every install, scaffolds a prisma.config.ts recording which agents to install skills for, then syncs the skills once now. Everything lands in the current directory; a prisma.config.ts or postinstall script that already exists is never edited. Rerunning is safe: each step reports what is already done.",
5823
+ examples: [
5824
+ "init",
5825
+ "init --skills=claude,cursor",
5826
+ "init --skills=none",
5827
+ "init --no-postinstall"
5828
+ ]
5129
5829
  },
5130
- needs: { credentials: true },
5830
+ needs: { config: skillsConfigSection },
5831
+ args: { flags: {
5832
+ postinstall: flag.optionalBoolean({ brief: "Add the skills-sync postinstall hook (--no-postinstall skips)" }),
5833
+ skills: flag.string({
5834
+ brief: `Agents to install skills for (comma-separated: ${KNOWN_AGENTS.join(", ")}); '${SKIP_SENTINEL}' records that no agent skills are wanted`,
5835
+ placeholder: "agents"
5836
+ })
5837
+ } },
5131
5838
  handler: async (args, ctx) => {
5132
- try {
5133
- const { api, target } = await resolveGitContext(ctx, args.flags.project, "git disconnect");
5134
- const existing = await readFirstSourceRepository(api, target.project.id, ctx.signal);
5135
- if (!existing) throw repoNotConnectedError();
5136
- const { error, response } = await api.DELETE("/v1/source-repositories/{id}", {
5137
- params: { path: { id: existing.id } },
5138
- signal: ctx.signal
5139
- });
5140
- if (error) throw repoConnectionApiError("Failed to disconnect GitHub repository", response, error);
5141
- const result = {
5142
- ...target,
5143
- repositoryConnection: toRepositoryConnection(existing)
5144
- };
5145
- return ok(ctx.present({ data: result }, disconnectPresentations(result)));
5146
- } catch (error) {
5147
- const mapped = mapGitOperationError(error);
5148
- if (mapped) return notOk(mapped);
5149
- throw error;
5150
- }
5839
+ const skillsFlag = parseSkillsFlag(args.flags.skills, ctx.config.agents);
5840
+ if (skillsFlag.kind === "invalid") return notOk(skillsFlag.error);
5841
+ const postinstall = args.flags.postinstall === false ? SKIPPED_POSTINSTALL : await addPostinstallHook(ctx.cwd);
5842
+ const config = await scaffoldConfigStep(ctx.cwd, skillsFlag.kind === "skip" ? [] : skillsFlag.agents, ctx.config.agentsConfigured);
5843
+ const skills = skillsFlag.kind === "skip" ? SKIPPED_SKILLS : await syncSkillsStep(ctx.cwd, skillsFlag.agents, ctx.config.check);
5844
+ const result = {
5845
+ postinstall: postinstall.report,
5846
+ config: config.report,
5847
+ skills: skills.report
5848
+ };
5849
+ return ok(ctx.present({
5850
+ data: result,
5851
+ diagnostics: [
5852
+ ...postinstall.diagnostics,
5853
+ ...config.diagnostics,
5854
+ ...skills.diagnostics
5855
+ ]
5856
+ }, initPresentations(result, postinstall, config, skills)));
5151
5857
  }
5152
5858
  });
5153
5859
  //#endregion
@@ -5188,7 +5894,7 @@ function parseBackupLimit(value, formatCommand) {
5188
5894
  }
5189
5895
  async function resolveDatabase(provider, target, databaseRef, branchName, signal) {
5190
5896
  const ref = databaseRef.trim();
5191
- if (!ref) throw usageError("Database id or name required", "This command needs a database id or name.", "Pass a database id or name.", ["prisma-cli database list"], "database");
5897
+ if (!ref) throw usageError("Database id or name required", "This command needs a database id or name.", "Pass a database id or name.", ["prisma database list"], "database");
5192
5898
  const databases = await provider.listDatabases({
5193
5899
  projectId: target.project.id,
5194
5900
  branchName,
@@ -5214,7 +5920,7 @@ function databaseRemovedDuringResolutionError(database, projectName) {
5214
5920
  why: `"${database.name}" (${database.id}) was listed for project "${projectName}", but reading it returned 404. It was most likely removed while this command was running.`,
5215
5921
  fix: "Re-run the command, or list the project's databases to see what is there now.",
5216
5922
  exitCode: 1,
5217
- nextSteps: ["prisma-cli database list"]
5923
+ nextSteps: ["prisma database list"]
5218
5924
  });
5219
5925
  }
5220
5926
  function ensureProjectId(database, projectId) {
@@ -5240,9 +5946,9 @@ function databaseNotFoundError(databaseRef, projectName, branchName) {
5240
5946
  domain: "database",
5241
5947
  summary: "Database not found",
5242
5948
  why: `No database matched "${databaseRef}"${projectName ? ` in project "${projectName}"${branchName ? ` on branch "${branchName}"` : ""}` : ""}.`,
5243
- fix: "Pass a database id or name from prisma-cli database list.",
5949
+ fix: "Pass a database id or name from prisma database list.",
5244
5950
  exitCode: 1,
5245
- nextSteps: ["prisma-cli database list"]
5951
+ nextSteps: ["prisma database list"]
5246
5952
  });
5247
5953
  }
5248
5954
  function databaseAmbiguousError(databaseRef, matches, branchName) {
@@ -5253,7 +5959,7 @@ function databaseAmbiguousError(databaseRef, matches, branchName) {
5253
5959
  why: branchName ? `Multiple databases matched "${databaseRef}" on branch "${branchName}".` : `Multiple databases matched "${databaseRef}".`,
5254
5960
  fix: "Pass the database id, or pass --branch <git-name> to narrow the match.",
5255
5961
  exitCode: 1,
5256
- nextSteps: ["prisma-cli database list"],
5962
+ nextSteps: ["prisma database list"],
5257
5963
  meta: { matches: matches.map((database) => ({
5258
5964
  id: database.id,
5259
5965
  name: database.name,
@@ -5489,9 +6195,9 @@ function normalizeCreatedDatabase(database, fallbackProjectId) {
5489
6195
  domain: "database",
5490
6196
  summary: "Created database did not return a connection string",
5491
6197
  why: "The Management API created the database but did not include the one-time connection payload.",
5492
- fix: "Create a connection explicitly with prisma-cli database connection create <database>.",
6198
+ fix: "Create a connection explicitly with prisma database connection create <database>.",
5493
6199
  exitCode: 1,
5494
- nextSteps: [`prisma-cli database connection create ${database.id}`]
6200
+ nextSteps: [`prisma database connection create ${database.id}`]
5495
6201
  });
5496
6202
  return {
5497
6203
  database: normalizeDatabase(database, fallbackProjectId),
@@ -5507,7 +6213,7 @@ function normalizeCreatedConnection(connection, fallbackDatabaseId) {
5507
6213
  why: "Database connection strings are one-time-view secrets, but the Management API did not include one in this create response.",
5508
6214
  fix: "Create another database connection and store the returned URL immediately.",
5509
6215
  exitCode: 1,
5510
- nextSteps: [`prisma-cli database connection create ${fallbackDatabaseId}`]
6216
+ nextSteps: [`prisma database connection create ${fallbackDatabaseId}`]
5511
6217
  });
5512
6218
  return {
5513
6219
  connection: normalizeConnection(connection, fallbackDatabaseId),
@@ -5773,7 +6479,7 @@ function portCommandReferences(text) {
5773
6479
  }
5774
6480
  function portPostgresCommand(command) {
5775
6481
  const named = portCommandReferences(command);
5776
- return named.startsWith(`prisma-cli `) ? named : `${CLI_NAME} ${named}`;
6482
+ return named.startsWith(`prisma `) ? named : `${CLI_NAME} ${named}`;
5777
6483
  }
5778
6484
  const STALE_INTERACTIVE_SIGN_IN = /, or rerun the command in a TTY to sign in interactively\./g;
5779
6485
  const TRACE_FLAG = /--trace/g;
@@ -7597,9 +8303,9 @@ const projectCreateCommand = defineCommand({
7597
8303
  * cancelled. */
7598
8304
  if (ctx.signal.aborted) throw ctx.signal.reason;
7599
8305
  throw projectCreateFailedError(error, name, workspace, {
7600
- nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"],
8306
+ nextSteps: ["prisma project list", "prisma project link <id-or-name>"],
7601
8307
  permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
7602
- fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
8308
+ fallbackFix: "Retry the command, or choose an existing Project with prisma project link <id-or-name>."
7603
8309
  });
7604
8310
  });
7605
8311
  const result = await bindDirectoryToProject(ctx, workspace, {
@@ -7701,9 +8407,9 @@ function positionalHint(command) {
7701
8407
  return "";
7702
8408
  }
7703
8409
  function resolveEnvScope(flags, options) {
7704
- if (flags.roleName && flags.branchName) throw usageError(`prisma-cli project env ${options.command} accepts either --role or --branch`, "--role targets a project-level config map; --branch targets a preview branch override.", "Pass exactly one scope flag.", [`prisma-cli project env ${options.command} ${positionalHint(options.command)}--role preview`, `prisma-cli project env ${options.command} ${positionalHint(options.command)}--branch feature/foo`], "app");
8410
+ if (flags.roleName && flags.branchName) throw usageError(`prisma project env ${options.command} accepts either --role or --branch`, "--role targets a project-level config map; --branch targets a preview branch override.", "Pass exactly one scope flag.", [`prisma project env ${options.command} ${positionalHint(options.command)}--role preview`, `prisma project env ${options.command} ${positionalHint(options.command)}--branch feature/foo`], "app");
7705
8411
  if (flags.roleName) {
7706
- if (!VALID_ROLES.has(flags.roleName)) throw usageError(`Unknown role "${flags.roleName}"`, "--role accepts production or preview.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} --role production`, `prisma-cli project env ${options.command} --role preview`], "app");
8412
+ if (!VALID_ROLES.has(flags.roleName)) throw usageError(`Unknown role "${flags.roleName}"`, "--role accepts production or preview.", "Pass --role production or --role preview.", [`prisma project env ${options.command} --role production`, `prisma project env ${options.command} --role preview`], "app");
7707
8413
  return {
7708
8414
  kind: "role",
7709
8415
  role: flags.roleName
@@ -7715,16 +8421,16 @@ function resolveEnvScope(flags, options) {
7715
8421
  };
7716
8422
  if (options.requireExplicit) {
7717
8423
  const positional = positionalHint(options.command);
7718
- throw usageError(`prisma-cli project env ${options.command} requires --role or --branch`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production, --role preview, or --branch <git-name>.", [
7719
- `prisma-cli project env ${options.command} ${positional}--role production`,
7720
- `prisma-cli project env ${options.command} ${positional}--role preview`,
7721
- `prisma-cli project env ${options.command} ${positional}--branch feature/foo`
8424
+ throw usageError(`prisma project env ${options.command} requires --role or --branch`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production, --role preview, or --branch <git-name>.", [
8425
+ `prisma project env ${options.command} ${positional}--role production`,
8426
+ `prisma project env ${options.command} ${positional}--role preview`,
8427
+ `prisma project env ${options.command} ${positional}--branch feature/foo`
7722
8428
  ], "app");
7723
8429
  }
7724
8430
  return null;
7725
8431
  }
7726
8432
  function parseKeyValuePositional(raw, command, env = process.env) {
7727
- if (!raw) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE`, "No KEY=VALUE positional argument was supplied.", "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
8433
+ if (!raw) throw usageError(`prisma project env ${command} requires KEY=VALUE`, "No KEY=VALUE positional argument was supplied.", "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
7728
8434
  const separatorIndex = raw.indexOf("=");
7729
8435
  if (separatorIndex === -1) {
7730
8436
  if (KEY_SHAPE.test(raw)) {
@@ -7734,14 +8440,14 @@ function parseKeyValuePositional(raw, command, env = process.env) {
7734
8440
  key: raw,
7735
8441
  value
7736
8442
  };
7737
- throw usageError(`Value for "${raw}" was not provided`, `No KEY=VALUE assignment was supplied, and ${raw} is not set in the current environment.`, "Pass KEY=VALUE or export the variable before running the command.", [`prisma-cli project env ${command} ${raw}=value --role production`, `${raw}=value prisma-cli project env ${command} ${raw} --role production`], "app");
8443
+ throw usageError(`Value for "${raw}" was not provided`, `No KEY=VALUE assignment was supplied, and ${raw} is not set in the current environment.`, "Pass KEY=VALUE or export the variable before running the command.", [`prisma project env ${command} ${raw}=value --role production`, `${raw}=value prisma project env ${command} ${raw} --role production`], "app");
7738
8444
  }
7739
- throw usageError(`KEY=VALUE argument is missing the = separator`, `"${raw}" does not contain an = character.`, "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
8445
+ throw usageError(`KEY=VALUE argument is missing the = separator`, `"${raw}" does not contain an = character.`, "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
7740
8446
  }
7741
8447
  const key = raw.slice(0, separatorIndex);
7742
8448
  const value = raw.slice(separatorIndex + 1);
7743
8449
  validateKey(key, command);
7744
- if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env delete to delete a variable.`, [`prisma-cli project env ${command} ${key}=value --role production`], "app");
8450
+ if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma project env delete to delete a variable.`, [`prisma project env ${command} ${key}=value --role production`], "app");
7745
8451
  return {
7746
8452
  key,
7747
8453
  value
@@ -7749,9 +8455,9 @@ function parseKeyValuePositional(raw, command, env = process.env) {
7749
8455
  }
7750
8456
  const KEY_SHAPE = /^[A-Z_][A-Z0-9_]*$/;
7751
8457
  function validateKey(key, command) {
7752
- if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY=value --role production`], "app");
8458
+ if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma project env ${command} STRIPE_KEY=value --role production`], "app");
7753
8459
  if (key.length > 256) throw usageError(`Variable key "${key}" exceeds the 256-character limit`, "Env-var keys are capped at 256 characters by the platform.", "Use a shorter key.", [], "app");
7754
- if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY=value --role production`], "app");
8460
+ if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma project env ${command} STRIPE_KEY=value --role production`], "app");
7755
8461
  }
7756
8462
  function formatScopeLabel(scope) {
7757
8463
  if (scope.kind === "role") return scope.role;
@@ -7766,7 +8472,7 @@ async function readEnvFileAssignments(cwd, filePath, command) {
7766
8472
  try {
7767
8473
  contents = await readFile(resolvedPath, "utf8");
7768
8474
  } catch (error) {
7769
- 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");
8475
+ 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 project env ${command} --file .env --role preview`], "app");
7770
8476
  }
7771
8477
  return parseEnvFileContents(contents, filePath, command);
7772
8478
  }
@@ -7889,15 +8595,15 @@ function formatDescriptorLabel(scope) {
7889
8595
  //#endregion
7890
8596
  //#region src/controllers/app-env.ts
7891
8597
  function resolveEnvWriteSource(rawAssignment, filePath, command) {
7892
- if (filePath !== void 0 && rawAssignment !== void 0) throw usageError(`prisma-cli project env ${command} accepts either KEY=VALUE or --file`, "The command received both a positional assignment and a dotenv file path.", "Pass one input source.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
8598
+ if (filePath !== void 0 && rawAssignment !== void 0) throw usageError(`prisma project env ${command} accepts either KEY=VALUE or --file`, "The command received both a positional assignment and a dotenv file path.", "Pass one input source.", [`prisma project env ${command} KEY=value --role preview`, `prisma project env ${command} --file .env --role preview`], "app");
7893
8599
  if (filePath !== void 0) {
7894
- if (filePath.length === 0) throw usageError(`prisma-cli project env ${command} --file requires a path`, "The --file flag was passed without a file path.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
8600
+ if (filePath.length === 0) throw usageError(`prisma project env ${command} --file requires a path`, "The --file flag was passed without a file path.", "Pass a readable dotenv file path.", [`prisma project env ${command} --file .env --role preview`], "app");
7895
8601
  return {
7896
8602
  kind: "file",
7897
8603
  filePath
7898
8604
  };
7899
8605
  }
7900
- if (rawAssignment === void 0) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE or --file`, "No environment variable input was supplied.", "Pass a single KEY=VALUE assignment or a dotenv file path.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
8606
+ if (rawAssignment === void 0) throw usageError(`prisma project env ${command} requires KEY=VALUE or --file`, "No environment variable input was supplied.", "Pass a single KEY=VALUE assignment or a dotenv file path.", [`prisma project env ${command} KEY=value --role preview`, `prisma project env ${command} --file .env --role preview`], "app");
7901
8607
  return {
7902
8608
  kind: "single",
7903
8609
  rawAssignment
@@ -7934,7 +8640,7 @@ async function resolveScopeToApi(client, projectId, scope, options) {
7934
8640
  why: "Production variables are project-level only; branch overrides apply to preview branches.",
7935
8641
  fix: "Use --role production for the production branch.",
7936
8642
  exitCode: 1,
7937
- nextSteps: ["prisma-cli project env list --role production"]
8643
+ nextSteps: ["prisma project env list --role production"]
7938
8644
  });
7939
8645
  return {
7940
8646
  scope,
@@ -8020,7 +8726,7 @@ async function resolveExistingBranch(client, projectId, branchName, signal) {
8020
8726
  why: "Branch update, list, and delete commands only target existing preview branches.",
8021
8727
  fix: "Create the branch by deploying it, or use `project env add --branch` to create its first override.",
8022
8728
  exitCode: 1,
8023
- nextSteps: [`prisma-cli project env add KEY=value --branch ${branchName}`]
8729
+ nextSteps: [`prisma project env add KEY=value --branch ${branchName}`]
8024
8730
  });
8025
8731
  return branch;
8026
8732
  }
@@ -8034,7 +8740,7 @@ async function resolveOrCreateBranch(client, projectId, branchName, signal) {
8034
8740
  why: "Creating the first branch would make it the project default, but branch overrides are preview-only.",
8035
8741
  fix: "Create or deploy the default branch first, then add the branch override.",
8036
8742
  exitCode: 1,
8037
- nextSteps: ["prisma-cli git connect <repository-url>"]
8743
+ nextSteps: ["prisma git connect <repository-url>"]
8038
8744
  });
8039
8745
  const { data, error, response } = await client.POST("/v1/projects/{projectId}/branches", {
8040
8746
  params: { path: { projectId } },
@@ -8257,7 +8963,7 @@ function envFileApplyFailedError(command, filePath, scope, failedKey, writtenVar
8257
8963
  why: writtenKeys.length === 0 ? `No variables were written before ${failedKey} failed. Cause: ${cause}` : `Written keys before failure: ${formatKeyList(writtenKeys)}. Cause: ${cause}`,
8258
8964
  fix: "Inspect the target scope, then retry the remaining keys once the API issue is resolved.",
8259
8965
  exitCode: 1,
8260
- nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`, retryStepForApplyFailure(command, filePath, scope, writtenKeys)],
8966
+ nextSteps: [`prisma project env list ${formatScopeFlag(scope)}`, retryStepForApplyFailure(command, filePath, scope, writtenKeys)],
8261
8967
  meta: {
8262
8968
  file: filePath,
8263
8969
  failedKey,
@@ -8266,9 +8972,9 @@ function envFileApplyFailedError(command, filePath, scope, failedKey, writtenVar
8266
8972
  });
8267
8973
  }
8268
8974
  function retryStepForApplyFailure(command, filePath, scope, writtenKeys) {
8269
- if (command === "update") return `prisma-cli project env update --file ${filePath} ${formatScopeFlag(scope)}`;
8270
- if (writtenKeys.length === 0) return `prisma-cli project env add --file ${filePath} ${formatScopeFlag(scope)}`;
8271
- return `prisma-cli project env add --file <remaining.env> ${formatScopeFlag(scope)}`;
8975
+ if (command === "update") return `prisma project env update --file ${filePath} ${formatScopeFlag(scope)}`;
8976
+ if (writtenKeys.length === 0) return `prisma project env add --file ${filePath} ${formatScopeFlag(scope)}`;
8977
+ return `prisma project env add --file <remaining.env> ${formatScopeFlag(scope)}`;
8272
8978
  }
8273
8979
  function splitFileNextSteps(filePath, scope, options) {
8274
8980
  const scopeFlag = formatScopeFlag(scope);
@@ -8276,15 +8982,15 @@ function splitFileNextSteps(filePath, scope, options) {
8276
8982
  const newFile = `${filePath}.new`;
8277
8983
  if (options.first === "update-existing") return [
8278
8984
  `# existing keys: ${formatKeyList(options.existingKeys)}`,
8279
- `prisma-cli project env update --file ${existingFile} ${scopeFlag}`,
8985
+ `prisma project env update --file ${existingFile} ${scopeFlag}`,
8280
8986
  "# new keys only",
8281
- `prisma-cli project env add --file ${newFile} ${scopeFlag}`
8987
+ `prisma project env add --file ${newFile} ${scopeFlag}`
8282
8988
  ];
8283
8989
  return [
8284
8990
  `# missing keys: ${formatKeyList(options.missingKeys)}`,
8285
- `prisma-cli project env add --file ${newFile} ${scopeFlag}`,
8991
+ `prisma project env add --file ${newFile} ${scopeFlag}`,
8286
8992
  "# existing keys only",
8287
- `prisma-cli project env update --file ${existingFile} ${scopeFlag}`
8993
+ `prisma project env update --file ${existingFile} ${scopeFlag}`
8288
8994
  ];
8289
8995
  }
8290
8996
  function formatKeyList(keys) {
@@ -8362,7 +9068,7 @@ function requireEnvScope(flags, command) {
8362
9068
  requireExplicit: true,
8363
9069
  command
8364
9070
  });
8365
- if (!scope) throw usageError(`prisma-cli project env ${command} requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env ${command} KEY=value --role production`], "app");
9071
+ if (!scope) throw usageError(`prisma project env ${command} requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma project env ${command} KEY=value --role production`], "app");
8366
9072
  return scope;
8367
9073
  }
8368
9074
  /** Workspace, pinned project and the API scope every env write needs. */
@@ -8511,7 +9217,7 @@ const projectEnvAddCommand = defineCommand({
8511
9217
  "project env add --file .env --role preview",
8512
9218
  "project env add DATABASE_URL=postgresql://branch --branch feature/foo",
8513
9219
  "project env add --file .env.local --branch feature/foo",
8514
- "API_URL=https://api.example prisma-cli project env add API_URL --project proj_123 --role preview"
9220
+ "API_URL=https://api.example prisma project env add API_URL --project proj_123 --role preview"
8515
9221
  ]
8516
9222
  },
8517
9223
  needs: { credentials: true },
@@ -8545,9 +9251,9 @@ const projectEnvAddCommand = defineCommand({
8545
9251
  domain: "app",
8546
9252
  summary: `Variable "${input.key}" already exists in ${formatScopeLabel(scope)}`,
8547
9253
  why: "A variable with this key already exists in the targeted scope.",
8548
- fix: "Use `prisma-cli project env update` to change an existing variable's value.",
9254
+ fix: "Use `prisma project env update` to change an existing variable's value.",
8549
9255
  exitCode: 1,
8550
- nextSteps: [`prisma-cli project env update ${input.key}=<new-value> ${formatScopeFlag$1(scope)}`]
9256
+ nextSteps: [`prisma project env update ${input.key}=<new-value> ${formatScopeFlag$1(scope)}`]
8551
9257
  });
8552
9258
  const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(ctx.api, projectId, input.key, {
8553
9259
  descriptor: {
@@ -8650,9 +9356,9 @@ const projectEnvDeleteCommand = defineCommand({
8650
9356
  domain: "app",
8651
9357
  summary: `Variable "${key}" not found in ${formatScopeLabel(scope)}`,
8652
9358
  why: "No variable with this key exists in the targeted scope, so there is nothing to delete.",
8653
- fix: "Run prisma-cli project env list with the same scope to see the available variables.",
9359
+ fix: "Run prisma project env list with the same scope to see the available variables.",
8654
9360
  exitCode: 1,
8655
- nextSteps: [`prisma-cli project env list ${formatScopeFlag$1(scope)}`]
9361
+ nextSteps: [`prisma project env list ${formatScopeFlag$1(scope)}`]
8656
9362
  });
8657
9363
  const { error, response } = await ctx.api.DELETE("/v1/environment-variables/{envVarId}", {
8658
9364
  params: { path: { envVarId: existing.id } },
@@ -8835,9 +9541,9 @@ const projectEnvUpdateCommand = defineCommand({
8835
9541
  domain: "app",
8836
9542
  summary: `Variable "${input.key}" not found in ${formatScopeLabel(scope)}`,
8837
9543
  why: "No variable with this key exists in the targeted scope.",
8838
- fix: "Use `prisma-cli project env add` to create a new variable.",
9544
+ fix: "Use `prisma project env add` to create a new variable.",
8839
9545
  exitCode: 1,
8840
- nextSteps: [`prisma-cli project env add ${input.key}=<value> ${formatScopeFlag$1(scope)}`]
9546
+ nextSteps: [`prisma project env add ${input.key}=<value> ${formatScopeFlag$1(scope)}`]
8841
9547
  });
8842
9548
  const { data, error, response } = await ctx.api.PATCH("/v1/environment-variables/{envVarId}", {
8843
9549
  params: { path: { envVarId: existing.id } },
@@ -8864,7 +9570,7 @@ const projectEnvUpdateCommand = defineCommand({
8864
9570
  const CREATE_CHOICE = "__create__";
8865
9571
  const CANCEL_CHOICE = "__cancel__";
8866
9572
  function setupCanceledError() {
8867
- return usageError("Project setup canceled", "Project link needs a Project before it can continue.", "Choose an existing Project or create a new one, then rerun project link.", ["prisma-cli project link <id-or-name>", "prisma-cli project create <name>"], "project");
9573
+ return usageError("Project setup canceled", "Project link needs a Project before it can continue.", "Choose an existing Project or create a new one, then rerun project link.", ["prisma project link <id-or-name>", "prisma project create <name>"], "project");
8868
9574
  }
8869
9575
  function choiceOptions(projects) {
8870
9576
  const sorted = sortProjects(projects);
@@ -8897,12 +9603,12 @@ async function createProjectForLink(ctx, workspace, projectName) {
8897
9603
  if (ctx.signal.aborted) throw ctx.signal.reason;
8898
9604
  throw projectCreateFailedError(error, projectName, workspace, {
8899
9605
  nextSteps: [
8900
- "prisma-cli project list",
8901
- "prisma-cli project link <id-or-name>",
8902
- `prisma-cli project create ${formatCommandArgument(projectName)}`
9606
+ "prisma project list",
9607
+ "prisma project link <id-or-name>",
9608
+ `prisma project create ${formatCommandArgument(projectName)}`
8903
9609
  ],
8904
9610
  permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
8905
- fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
9611
+ fallbackFix: "Retry the command, or choose an existing Project with prisma project link <id-or-name>."
8906
9612
  });
8907
9613
  });
8908
9614
  return {
@@ -9226,7 +9932,7 @@ function showPresentations$1(result, cwd, env) {
9226
9932
  stdout: () => stdoutFieldRows(result, cwd).map((row) => `${row.label}: ${row.value}`),
9227
9933
  next: () => result.project === null ? toNextActions(buildProjectSetupNextActions({
9228
9934
  commandName: "project show",
9229
- retryCommand: "prisma-cli project show <id-or-name>",
9935
+ retryCommand: "prisma project show <id-or-name>",
9230
9936
  suggestedProjectName: result.suggestedProjectName,
9231
9937
  reason: "This directory is not linked to a Prisma Project. Package and directory names can suggest setup defaults, but they do not select a Project."
9232
9938
  })) : []
@@ -9499,12 +10205,12 @@ function formatDomainFailureFix(domain) {
9499
10205
  if (domain.status !== "failed") return null;
9500
10206
  const dnsRecord = domain.dnsRecords[0];
9501
10207
  if (domain.failureCategory === "dns") {
9502
- if (dnsRecord) return `Add ${dnsRecord.type} ${dnsRecord.name} -> ${dnsRecord.value}, then run prisma-cli app domain retry ${domain.hostname}.`;
9503
- return `DNS verification failed, but the platform did not return a DNS record. Run prisma-cli app domain show ${domain.hostname} later, then retry when the DNS target is available.`;
10208
+ if (dnsRecord) return `Add ${dnsRecord.type} ${dnsRecord.name} -> ${dnsRecord.value}, then run prisma service domain retry ${domain.hostname}.`;
10209
+ return `DNS verification failed, but the platform did not return a DNS record. Run prisma service domain show ${domain.hostname} later, then retry when the DNS target is available.`;
9504
10210
  }
9505
- if (domain.failureCategory === "acme") return `Retry TLS issuance with prisma-cli app domain retry ${domain.hostname}. Contact support if it fails again.`;
9506
- if (domain.failureCategory === "storage") return `Retry provisioning with prisma-cli app domain retry ${domain.hostname}. Contact support if it fails again.`;
9507
- return `Run prisma-cli app domain retry ${domain.hostname}. Contact support if it fails again.`;
10211
+ if (domain.failureCategory === "acme") return `Retry TLS issuance with prisma service domain retry ${domain.hostname}. Contact support if it fails again.`;
10212
+ if (domain.failureCategory === "storage") return `Retry provisioning with prisma service domain retry ${domain.hostname}. Contact support if it fails again.`;
10213
+ return `Run prisma service domain retry ${domain.hostname}. Contact support if it fails again.`;
9508
10214
  }
9509
10215
  //#endregion
9510
10216
  //#region src/commands/service/errors.ts
@@ -9530,42 +10236,24 @@ function toEngineNextAction(action) {
9530
10236
  ...action.reason !== void 0 ? { reason: action.reason } : {}
9531
10237
  };
9532
10238
  }
9533
- /**
9534
- * The binary name legacy error copy is written in. It is fixed, not
9535
- * `CLI_NAME`: these strings are inputs to the rewriting below, and a
9536
- * renamed binary must still recognise them.
9537
- */
9538
- const LEGACY_CLI_NAME = "prisma-cli";
9539
10239
  const CNAME_HINT = /\bcname(?:s)?\s+to\b/;
9540
10240
  const PRISMA_BUILD_HOST = /\b((?:[a-z0-9-]+\.)+prisma\.build)\b/i;
9541
10241
  /**
9542
- * The rename surface for copy that flows through legacy error builders:
9543
- * command lines and the "app target" noun in prose.
9544
- */
9545
- function renameAppCopy(text) {
9546
- return text.replaceAll(`${LEGACY_CLI_NAME} app `, `${CLI_NAME} service `).replaceAll("App target", "Service target").replaceAll("app target", "service target");
9547
- }
9548
- /** A legacy `nextSteps` command line as this binary spells it. */
9549
- function toCurrentCommandLine(legacyStep) {
9550
- const renamed = renameAppCopy(legacyStep);
9551
- return renamed.startsWith(`${LEGACY_CLI_NAME} `) ? `${CLI_NAME} ${renamed.slice(11)}` : renamed;
9552
- }
9553
- /**
9554
10242
  * Maps a legacy CliError onto the engine error protocol: the flat code
9555
10243
  * becomes `SERVICE.<code>`, the free-text fix becomes a user-choice
9556
- * action carried alongside any typed legacy actions, and nextSteps that
9557
- * are command lines become run-command actions. Copy passes through the
9558
- * rename surface.
10244
+ * action carried alongside any typed legacy actions, and each nextSteps
10245
+ * command line becomes a run-command action. Copy passes through
10246
+ * unchanged: the producers write the commands a user types today.
9559
10247
  */
9560
10248
  function fromLegacyCliError(error) {
9561
- const fixAction = error.fix ? [adviceAction(renameAppCopy(error.fix))] : [];
9562
- const nextActions = error.nextActions.length > 0 ? [...error.nextActions.map(toEngineNextAction), ...fixAction] : [...fixAction, ...error.nextSteps.filter((step) => step.startsWith(`${LEGACY_CLI_NAME} `)).map((step) => ({
10249
+ const fixAction = error.fix ? [adviceAction(error.fix)] : [];
10250
+ const nextActions = error.nextActions.length > 0 ? [...error.nextActions.map(toEngineNextAction), ...fixAction] : [...fixAction, ...error.nextSteps.map((step) => ({
9563
10251
  kind: "run-command",
9564
10252
  label: "Run",
9565
- command: toCurrentCommandLine(step)
10253
+ command: step
9566
10254
  }))];
9567
- return new CliStructuredError(`SERVICE.${error.code}`, renameAppCopy(error.summary), {
9568
- ...error.why ? { why: renameAppCopy(error.why) } : {},
10255
+ return new CliStructuredError(`SERVICE.${error.code}`, error.summary, {
10256
+ ...error.why ? { why: error.why } : {},
9569
10257
  nextActions,
9570
10258
  ...error.where ? { where: { path: error.where } } : {},
9571
10259
  ...Object.keys(error.meta).length > 0 ? { meta: error.meta } : {},
@@ -9716,7 +10404,7 @@ function domainVerificationFailedError(hostname, domain) {
9716
10404
  return new CliStructuredError("SERVICE.DOMAIN_VERIFICATION_FAILED", `Custom domain "${hostname}" failed verification`, {
9717
10405
  why,
9718
10406
  nextActions: [
9719
- ...guidance ? [adviceAction(renameAppCopy(guidance))] : [],
10407
+ ...guidance ? [adviceAction(guidance)] : [],
9720
10408
  runCommandAction("Show the domain", `service domain show ${hostname} --service <name>`),
9721
10409
  runCommandAction("Retry verification", `service domain retry ${hostname} --service <name>`)
9722
10410
  ]
@@ -11979,6 +12667,54 @@ const serviceVersionStopCommand = defineCommand({
11979
12667
  }
11980
12668
  });
11981
12669
  //#endregion
12670
+ //#region src/commands/skills/family.ts
12671
+ /** Skill delivery is product-agnostic — the same two commands serve the
12672
+ * ORM's skills and Composer's — so it is its own family rather than
12673
+ * part of either product's. */
12674
+ const skillsCommandFamily = defineCommandFamily({
12675
+ configSection: skillsConfigSection,
12676
+ commands: {
12677
+ sync: skillsSyncCommand,
12678
+ list: defineCommand({
12679
+ help: {
12680
+ summary: "Show which Prisma agent skills are installed in this project",
12681
+ examples: ["skills list", "skills list --json"]
12682
+ },
12683
+ needs: { config: skillsConfigSection },
12684
+ handler: async (_args, ctx) => {
12685
+ const status = await readSkillsStatus(ctx.cwd, { agents: ctx.config.agents });
12686
+ const result = {
12687
+ projectRoot: status.projectRoot,
12688
+ agents: ctx.config.agents,
12689
+ packages: packageReports(status.packages),
12690
+ skills: status.skills.map((skill) => ({
12691
+ skill: skill.skill,
12692
+ library: skill.library,
12693
+ version: skill.version,
12694
+ upToDate: skill.upToDate,
12695
+ targets: skill.targets.map((target) => ({
12696
+ dir: target.dir,
12697
+ syncedVersion: target.syncedVersion,
12698
+ state: target.state
12699
+ }))
12700
+ })),
12701
+ orphaned: status.orphans.map((orphan) => ({
12702
+ skill: orphan.skill,
12703
+ library: orphan.library,
12704
+ dirs: orphan.dirs
12705
+ })),
12706
+ checkDisabled: status.checkDisabled || !ctx.config.check,
12707
+ upToDate: status.upToDate
12708
+ };
12709
+ return ok(ctx.present({
12710
+ data: result,
12711
+ diagnostics: versionConflictDiagnostics(status.packages)
12712
+ }, listPresentations$5(result)));
12713
+ }
12714
+ })
12715
+ }
12716
+ });
12717
+ //#endregion
11982
12718
  //#region src/cli.ts
11983
12719
  const platformCommandFamily = defineCommandFamily({ commands: {
11984
12720
  login: authLoginCommand,
@@ -12071,13 +12807,13 @@ const cliGroups = {
12071
12807
  service: { brief: "Manage services and their versions for a project" },
12072
12808
  "service domain": { brief: "Manage custom domains for a service" },
12073
12809
  "service version": { brief: "Manage the versions of a service" },
12074
- agent: { brief: "Manage Prisma skills for AI coding agents" },
12075
12810
  "auth workspace": { brief: "Manage local workspace sessions" },
12076
12811
  contract: { brief: "Define and emit your application data contract" },
12077
12812
  db: { brief: "Verify, sign and update your database against the contract" },
12078
12813
  migration: { brief: "Plan, inspect and scaffold on-disk migrations" },
12079
12814
  "migration ref": { brief: "Manage named refs that point at contracts" },
12080
12815
  orm: { brief: "Initialize a Prisma ORM project" },
12816
+ skills: { brief: "Keep this project's Prisma agent skills current" },
12081
12817
  ...telemetry.groups
12082
12818
  };
12083
12819
  const mountedCommands = {
@@ -12160,9 +12896,9 @@ const mountedCommands = {
12160
12896
  "migration ref delete": ormCommandFamily$1.commands["migration ref delete"],
12161
12897
  "migration ref list": ormCommandFamily$1.commands["migration ref list"],
12162
12898
  "migration ref set": ormCommandFamily$1.commands["migration ref set"],
12163
- "agent install": agentInstallCommand,
12164
- "agent update": agentUpdateCommand,
12165
- "agent status": agentStatusCommand,
12899
+ init: initCommand,
12900
+ "skills sync": skillsCommandFamily.commands.sync,
12901
+ "skills list": skillsCommandFamily.commands.list,
12166
12902
  feedback: feedbackCommand,
12167
12903
  ...telemetry.commands
12168
12904
  };
@@ -12173,7 +12909,8 @@ function buildCli() {
12173
12909
  commandFamilies: [
12174
12910
  platformCommandFamily,
12175
12911
  composerCommandFamily,
12176
- ormCommandFamily$1
12912
+ ormCommandFamily$1,
12913
+ skillsCommandFamily
12177
12914
  ],
12178
12915
  groups: cliGroups,
12179
12916
  commands: mountedCommands,
@@ -13370,6 +14107,81 @@ async function assembleRuntime(proc) {
13370
14107
  host: describeHost(proc)
13371
14108
  };
13372
14109
  }
14110
+ async function maybeWriteSkillsStaleNotice(runtime) {
14111
+ if (isSuppressedByInvocation(runtime)) return;
14112
+ try {
14113
+ if (await readSkillsCheckDisabled(runtime.cwd)) return;
14114
+ const status = await readSkillsStatus(runtime.cwd, {
14115
+ orphans: false,
14116
+ checkDisabled: false
14117
+ });
14118
+ if (status.upToDate) return;
14119
+ const config = await readProjectSkillsConfig(runtime.cwd, configPathFromArgv(runtime.argv));
14120
+ if (config !== null && !config.check) return;
14121
+ const agents = config?.agents ?? DEFAULT_AGENTS;
14122
+ if (agents.length === 0) return;
14123
+ const notice = renderStaleNotice(status, agentSkillDirs(agents));
14124
+ if (notice !== null) runtime.stderr.write(notice);
14125
+ } catch {
14126
+ return;
14127
+ }
14128
+ }
14129
+ /** The first skill with a stale or never-synced copy in one of the
14130
+ * configured directories — what the check names in its one line. */
14131
+ function firstOutdatedSkillIn(status, dirs) {
14132
+ return status.skills.find((skill) => skill.targets.some((target) => dirs.includes(target.dir) && (target.state === "stale" || target.state === "absent"))) ?? null;
14133
+ }
14134
+ function renderStaleNotice(status, dirs) {
14135
+ const outdated = firstOutdatedSkillIn(status, dirs);
14136
+ if (outdated === null) return null;
14137
+ const synced = outdated.targets.find((target) => dirs.includes(target.dir) && target.state === "stale")?.syncedVersion;
14138
+ return `Prisma agent skills are out of date (installed ${outdated.library} ${outdated.version}, synced ${synced ?? "none"}). Run: ${getCliName()} skills sync\n`;
14139
+ }
14140
+ /** The shared flags that take a separate value, so the word after them
14141
+ * is that value rather than the command being invoked. */
14142
+ const FLAGS_TAKING_A_VALUE = new Set([
14143
+ "--format",
14144
+ "--log-level",
14145
+ "--config",
14146
+ "--confirm"
14147
+ ]);
14148
+ /** The first word of the invocation — the group, or the command when it
14149
+ * is mounted top-level — skipping the shared flags that may precede it. */
14150
+ function invokedGroup(argv) {
14151
+ for (let index = 0; index < argv.length; index += 1) {
14152
+ const token = argv[index];
14153
+ if (!token.startsWith("-")) return token;
14154
+ if (FLAGS_TAKING_A_VALUE.has(token)) index += 1;
14155
+ }
14156
+ }
14157
+ /** Tokens before a bare `--`; everything after it is positional data,
14158
+ * never a flag. */
14159
+ function flagTokens(argv) {
14160
+ const end = argv.indexOf("--");
14161
+ return end === -1 ? argv : argv.slice(0, end);
14162
+ }
14163
+ /** The off switches that cost nothing to read. */
14164
+ function isSuppressedByInvocation(runtime) {
14165
+ const env = runtime.env;
14166
+ if (env["PRISMA_SKILLS_CHECK"] === "0") return true;
14167
+ if (env.CI || env.GITHUB_ACTIONS) return true;
14168
+ const argv = flagTokens(runtime.argv);
14169
+ const group = invokedGroup(argv);
14170
+ if (group === "skills" || group === "init") return true;
14171
+ if (argv.includes("--json") || argv.includes("--quiet") || argv.includes("-q")) return true;
14172
+ if (argv.includes("--version")) return true;
14173
+ return argv.some((token, index) => token === "--format=json" || token === "--format" && argv[index + 1] === "json");
14174
+ }
14175
+ /** The file an explicit --config names, so the check reads the same
14176
+ * config the command did. Discovery is otherwise cwd-only. */
14177
+ function configPathFromArgv(argv) {
14178
+ const tokens = flagTokens(argv);
14179
+ for (let index = 0; index < tokens.length; index += 1) {
14180
+ const token = tokens[index];
14181
+ if (token === "--config") return tokens[index + 1];
14182
+ if (token.startsWith("--config=")) return token.slice(9);
14183
+ }
14184
+ }
13373
14185
  //#endregion
13374
14186
  //#region src/update-check.ts
13375
14187
  const UPDATE_CHECK_FILE_NAME = "update-check.json";
@@ -13546,54 +14358,8 @@ function isAtLeastIntervalAgo(value) {
13546
14358
  return Number.isNaN(timestamp) || Date.now() - timestamp >= NOTIFICATION_INTERVAL_MS;
13547
14359
  }
13548
14360
  function isInstalledVersionStale(installedVersion, latestVersion) {
13549
- const installed = parseVersion(installedVersion);
13550
- const latest = parseVersion(latestVersion);
13551
- if (!installed || !latest) return false;
13552
- return compareVersions(installed, latest) < 0;
13553
- }
13554
- function parseVersion(version) {
13555
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version);
13556
- if (!match) return null;
13557
- return {
13558
- major: Number(match[1]),
13559
- minor: Number(match[2]),
13560
- patch: Number(match[3]),
13561
- prerelease: match[4]?.split(".") ?? []
13562
- };
13563
- }
13564
- function compareVersions(left, right) {
13565
- for (const key of [
13566
- "major",
13567
- "minor",
13568
- "patch"
13569
- ]) {
13570
- const diff = left[key] - right[key];
13571
- if (diff !== 0) return diff;
13572
- }
13573
- return comparePrerelease(left.prerelease, right.prerelease);
13574
- }
13575
- function comparePrerelease(left, right) {
13576
- if (left.length === 0 && right.length === 0) return 0;
13577
- if (left.length === 0) return 1;
13578
- if (right.length === 0) return -1;
13579
- const count = Math.max(left.length, right.length);
13580
- for (let index = 0; index < count; index += 1) {
13581
- const leftPart = left[index];
13582
- const rightPart = right[index];
13583
- if (leftPart === void 0) return -1;
13584
- if (rightPart === void 0) return 1;
13585
- const diff = comparePrereleasePart(leftPart, rightPart);
13586
- if (diff !== 0) return diff;
13587
- }
13588
- return 0;
13589
- }
13590
- function comparePrereleasePart(left, right) {
13591
- const leftNumber = /^\d+$/.test(left) ? Number(left) : null;
13592
- const rightNumber = /^\d+$/.test(right) ? Number(right) : null;
13593
- if (leftNumber !== null && rightNumber !== null) return leftNumber - rightNumber;
13594
- if (leftNumber !== null) return -1;
13595
- if (rightNumber !== null) return 1;
13596
- return left.localeCompare(right);
14361
+ const order = compareVersionStrings(installedVersion, latestVersion);
14362
+ return order !== null && order < 0;
13597
14363
  }
13598
14364
  async function fetchLatestVersion(registryUrl, fetchImpl) {
13599
14365
  const controller = new AbortController();
@@ -13632,7 +14398,14 @@ async function main(proc, buildCliForRun = buildCli) {
13632
14398
  stderr: proc.stderr
13633
14399
  });
13634
14400
  const runtime = await assembleRuntime(proc);
13635
- return cli.run(proc.argv.slice(2), runtime);
14401
+ const exitCode = await cli.run(proc.argv.slice(2), runtime);
14402
+ await maybeWriteSkillsStaleNotice({
14403
+ env: proc.env,
14404
+ argv: proc.argv.slice(2),
14405
+ cwd: proc.cwd(),
14406
+ stderr: proc.stderr
14407
+ });
14408
+ return exitCode;
13636
14409
  }
13637
14410
  //#endregion
13638
14411
  //#region src/bin.ts