@prisma/cli 8.0.0-rc.8 → 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.
- package/dist/cli.js +2471 -1698
- package/dist/sender.js +1 -1
- package/package.json +8 -7
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
|
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/
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
116
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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/
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
153
|
-
return (
|
|
88
|
+
function sameWorkspaceId(left, right) {
|
|
89
|
+
return stripWorkspacePrefix(left) === stripWorkspacePrefix(right);
|
|
154
90
|
}
|
|
155
91
|
//#endregion
|
|
156
|
-
//#region src/
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
123
|
+
workspaceId: credential.workspaceId,
|
|
124
|
+
accessToken: credential.token,
|
|
125
|
+
refreshToken: credential.refreshToken
|
|
190
126
|
};
|
|
191
127
|
}
|
|
192
|
-
function
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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/
|
|
1768
|
-
const
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2898
|
+
fix: "Pass a project id or name from prisma project list.",
|
|
2866
2899
|
exitCode: 1,
|
|
2867
|
-
nextSteps: ["prisma
|
|
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
|
|
2876
|
-
if (firstMatch) nextSteps.push(`prisma
|
|
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
|
|
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
|
|
2920
|
-
"prisma
|
|
2921
|
-
"prisma
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3132
|
-
if (commandName) commands.push(`prisma
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
3446
|
+
fix: "Run prisma git connect before disconnecting.",
|
|
3414
3447
|
exitCode: 1,
|
|
3415
|
-
nextSteps: ["prisma
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
3719
|
-
*
|
|
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
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
-
|
|
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 },
|
|
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/
|
|
4334
|
-
/** The `bucket
|
|
4335
|
-
|
|
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: "
|
|
4344
|
-
text:
|
|
4545
|
+
status: "info",
|
|
4546
|
+
text: TITLE$10
|
|
4345
4547
|
},
|
|
4346
4548
|
{
|
|
4347
4549
|
kind: "fields",
|
|
4348
4550
|
rows: [{
|
|
4349
|
-
label: "
|
|
4350
|
-
value: result.
|
|
4351
|
-
}
|
|
4551
|
+
label: "project",
|
|
4552
|
+
value: result.projectName
|
|
4553
|
+
}, ...result.branchName ? [{
|
|
4554
|
+
label: "branch",
|
|
4555
|
+
value: result.branchName
|
|
4556
|
+
}] : []]
|
|
4352
4557
|
},
|
|
4353
|
-
{
|
|
4354
|
-
kind: "
|
|
4355
|
-
|
|
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
|
|
4361
|
-
args: {
|
|
4362
|
-
|
|
4363
|
-
|
|
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: "
|
|
4370
|
-
examples: [
|
|
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
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
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/
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
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
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
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
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
}
|
|
4409
|
-
|
|
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
|
-
|
|
4861
|
+
//#endregion
|
|
4862
|
+
//#region src/presenters/project.ts
|
|
4863
|
+
function serializeProjectList(result) {
|
|
4421
4864
|
return {
|
|
4422
4865
|
...serializeList({
|
|
4423
|
-
context: {
|
|
4424
|
-
items: result.
|
|
4425
|
-
noun: "
|
|
4426
|
-
label:
|
|
4427
|
-
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
|
-
|
|
4432
|
-
|
|
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
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
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: "
|
|
4447
|
-
text:
|
|
5032
|
+
status: "ok",
|
|
5033
|
+
text: "Connecting Git to the resolved project."
|
|
4448
5034
|
},
|
|
4449
5035
|
{
|
|
4450
5036
|
kind: "fields",
|
|
4451
|
-
rows: [
|
|
4452
|
-
|
|
4453
|
-
|
|
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
|
-
|
|
4457
|
-
kind: "
|
|
4458
|
-
|
|
4459
|
-
|
|
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
|
|
4477
|
-
args: {
|
|
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: "
|
|
4480
|
-
examples: [
|
|
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
|
|
4486
|
-
|
|
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
|
-
|
|
4489
|
-
|
|
5109
|
+
...target,
|
|
5110
|
+
repositoryConnection: toRepositoryConnection(data.data)
|
|
4490
5111
|
};
|
|
4491
|
-
return ok(ctx.present({ data: result },
|
|
5112
|
+
return ok(ctx.present({ data: result }, connectPresentations(result)));
|
|
4492
5113
|
} catch (error) {
|
|
4493
|
-
const mapped =
|
|
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/
|
|
4501
|
-
/** The `
|
|
4502
|
-
|
|
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: "
|
|
4512
|
-
text:
|
|
5131
|
+
status: "ok",
|
|
5132
|
+
text: "Disconnecting Git from the resolved project."
|
|
4513
5133
|
},
|
|
4514
5134
|
{
|
|
4515
5135
|
kind: "fields",
|
|
4516
|
-
rows: [
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
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
|
-
|
|
4525
|
-
kind: "
|
|
4526
|
-
|
|
4527
|
-
|
|
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
|
|
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: "
|
|
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 {
|
|
4561
|
-
const
|
|
4562
|
-
|
|
4563
|
-
|
|
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
|
-
|
|
4568
|
-
|
|
4569
|
-
branchName: args.flags.branch ?? null,
|
|
4570
|
-
buckets
|
|
5176
|
+
...target,
|
|
5177
|
+
repositoryConnection: toRepositoryConnection(existing)
|
|
4571
5178
|
};
|
|
4572
|
-
return ok(ctx.present({ data: result },
|
|
4573
|
-
} catch (error) {
|
|
4574
|
-
const mapped =
|
|
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/
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
*
|
|
4599
|
-
*
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
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
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
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
|
-
|
|
4614
|
-
|
|
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/
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
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
|
-
|
|
4661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
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
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
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
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
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
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
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
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
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
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
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
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
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
|
|
4798
|
-
return
|
|
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
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
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
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
return
|
|
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
|
-
|
|
4818
|
-
|
|
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
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
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
|
-
|
|
4828
|
-
//#region src/presenters/project.ts
|
|
4829
|
-
function serializeProjectList(result) {
|
|
5492
|
+
function unreadablePackageJsonDiagnostic() {
|
|
4830
5493
|
return {
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
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
|
|
4844
|
-
return
|
|
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
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4866
|
-
|
|
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
|
-
|
|
4870
|
-
|
|
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
|
|
4898
|
-
*
|
|
4899
|
-
*
|
|
4900
|
-
function
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
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
|
-
|
|
5540
|
+
}
|
|
5541
|
+
function configUnwritableDiagnostic(agents) {
|
|
4907
5542
|
return {
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
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
|
|
4914
|
-
return
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
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
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
return
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
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
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
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
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
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
|
|
4983
|
-
|
|
4984
|
-
|
|
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
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
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
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
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
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
]
|
|
5027
|
-
};
|
|
5745
|
+
line: summary("warn", "The agent skills could not be synced."),
|
|
5746
|
+
diagnostics: [skillsSyncFailedDiagnostic(cause)]
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5028
5749
|
}
|
|
5029
|
-
const
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
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
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
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
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
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
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
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
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
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
|
|
5125
|
-
args: { flags: { project: projectFlag$2 } },
|
|
5819
|
+
const initCommand = defineCommand({
|
|
5126
5820
|
help: {
|
|
5127
|
-
summary: "
|
|
5128
|
-
|
|
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: {
|
|
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
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
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
|
|
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
|
|
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
|
|
5949
|
+
fix: "Pass a database id or name from prisma database list.",
|
|
5244
5950
|
exitCode: 1,
|
|
5245
|
-
nextSteps: ["prisma
|
|
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
|
|
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
|
|
6198
|
+
fix: "Create a connection explicitly with prisma database connection create <database>.",
|
|
5493
6199
|
exitCode: 1,
|
|
5494
|
-
nextSteps: [`prisma
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7719
|
-
`prisma
|
|
7720
|
-
`prisma
|
|
7721
|
-
`prisma
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8270
|
-
if (writtenKeys.length === 0) return `prisma
|
|
8271
|
-
return `prisma
|
|
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
|
|
8985
|
+
`prisma project env update --file ${existingFile} ${scopeFlag}`,
|
|
8280
8986
|
"# new keys only",
|
|
8281
|
-
`prisma
|
|
8987
|
+
`prisma project env add --file ${newFile} ${scopeFlag}`
|
|
8282
8988
|
];
|
|
8283
8989
|
return [
|
|
8284
8990
|
`# missing keys: ${formatKeyList(options.missingKeys)}`,
|
|
8285
|
-
`prisma
|
|
8991
|
+
`prisma project env add --file ${newFile} ${scopeFlag}`,
|
|
8286
8992
|
"# existing keys only",
|
|
8287
|
-
`prisma
|
|
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
|
|
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
|
|
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
|
|
9254
|
+
fix: "Use `prisma project env update` to change an existing variable's value.",
|
|
8549
9255
|
exitCode: 1,
|
|
8550
|
-
nextSteps: [`prisma
|
|
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
|
|
9359
|
+
fix: "Run prisma project env list with the same scope to see the available variables.",
|
|
8654
9360
|
exitCode: 1,
|
|
8655
|
-
nextSteps: [`prisma
|
|
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
|
|
9544
|
+
fix: "Use `prisma project env add` to create a new variable.",
|
|
8839
9545
|
exitCode: 1,
|
|
8840
|
-
nextSteps: [`prisma
|
|
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
|
|
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
|
|
8901
|
-
"prisma
|
|
8902
|
-
`prisma
|
|
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
|
|
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
|
|
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
|
|
9503
|
-
return `DNS verification failed, but the platform did not return a DNS record. Run prisma
|
|
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
|
|
9506
|
-
if (domain.failureCategory === "storage") return `Retry provisioning with prisma
|
|
9507
|
-
return `Run prisma
|
|
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
|
|
9557
|
-
*
|
|
9558
|
-
*
|
|
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(
|
|
9562
|
-
const nextActions = error.nextActions.length > 0 ? [...error.nextActions.map(toEngineNextAction), ...fixAction] : [...fixAction, ...error.nextSteps.
|
|
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:
|
|
10253
|
+
command: step
|
|
9566
10254
|
}))];
|
|
9567
|
-
return new CliStructuredError(`SERVICE.${error.code}`,
|
|
9568
|
-
...error.why ? { 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(
|
|
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
|
-
|
|
12164
|
-
"
|
|
12165
|
-
"
|
|
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
|
|
13550
|
-
|
|
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
|
-
|
|
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
|