@wardby/cli 0.1.0 → 0.2.1
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/README.md +183 -96
- package/deploy/local/docker-compose.yml +5 -5
- package/dist/claude-coding-worker/driver.d.ts +1 -0
- package/dist/claude-coding-worker/driver.js +4 -1
- package/dist/claude-coding-worker/main.js +3 -2
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -0
- package/dist/cli.js +0 -0
- package/dist/coding/protocol.d.ts +28 -3
- package/dist/coding/protocol.js +53 -2
- package/dist/coding-worker/artifact.d.ts +12 -0
- package/dist/coding-worker/artifact.js +48 -6
- package/dist/coding-worker/driver.d.ts +1 -0
- package/dist/coding-worker/driver.js +4 -1
- package/dist/coding-worker/errors.d.ts +7 -0
- package/dist/coding-worker/errors.js +18 -0
- package/dist/coding-worker/main.js +3 -2
- package/dist/core/budget-groups.d.ts +1 -1
- package/dist/core/coding-queue.d.ts +1 -1
- package/dist/core/datastores.d.ts +1 -1
- package/dist/core/db.d.ts +36 -2
- package/dist/core/db.js +69 -2
- package/dist/core/dispatch.d.ts +16 -1
- package/dist/core/dispatch.js +33 -5
- package/dist/core/http-runtime.js +3 -4
- package/dist/core/lease.d.ts +1 -1
- package/dist/core/private-directory.d.ts +20 -0
- package/dist/core/private-directory.js +36 -0
- package/dist/core/reconciler.d.ts +1 -1
- package/dist/core/runner.d.ts +1 -1
- package/dist/core/scheduler.d.ts +1 -1
- package/dist/core/secrets.d.ts +1 -1
- package/dist/core/subagent-memory-tools.d.ts +1 -1
- package/dist/core/webhooks.d.ts +1 -1
- package/dist/env.js +8 -1
- package/dist/generated/prisma/browser.d.ts +228 -0
- package/dist/generated/prisma/browser.js +17 -0
- package/dist/generated/prisma/client.d.ts +247 -0
- package/dist/generated/prisma/client.js +34 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +809 -0
- package/dist/generated/prisma/commonInputTypes.js +10 -0
- package/dist/generated/prisma/enums.d.ts +53 -0
- package/dist/generated/prisma/enums.js +54 -0
- package/dist/generated/prisma/internal/class.d.ts +462 -0
- package/dist/generated/prisma/internal/class.js +49 -0
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +3325 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +455 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +449 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +426 -0
- package/dist/generated/prisma/models/Agent.d.ts +3267 -0
- package/dist/generated/prisma/models/Agent.js +1 -0
- package/dist/generated/prisma/models/AgentDatastore.d.ts +1210 -0
- package/dist/generated/prisma/models/AgentDatastore.js +1 -0
- package/dist/generated/prisma/models/AgentMemory.d.ts +986 -0
- package/dist/generated/prisma/models/AgentMemory.js +1 -0
- package/dist/generated/prisma/models/AgentSecret.d.ts +1212 -0
- package/dist/generated/prisma/models/AgentSecret.js +1 -0
- package/dist/generated/prisma/models/AgentSubAgent.d.ts +1217 -0
- package/dist/generated/prisma/models/AgentSubAgent.js +1 -0
- package/dist/generated/prisma/models/AgentTool.d.ts +1309 -0
- package/dist/generated/prisma/models/AgentTool.js +1 -0
- package/dist/generated/prisma/models/AuthFormChallenge.d.ts +1298 -0
- package/dist/generated/prisma/models/AuthFormChallenge.js +1 -0
- package/dist/generated/prisma/models/AuthLoginKey.d.ts +1245 -0
- package/dist/generated/prisma/models/AuthLoginKey.js +1 -0
- package/dist/generated/prisma/models/AuthRateLimit.d.ts +979 -0
- package/dist/generated/prisma/models/AuthRateLimit.js +1 -0
- package/dist/generated/prisma/models/AuthSession.d.ts +1376 -0
- package/dist/generated/prisma/models/AuthSession.js +1 -0
- package/dist/generated/prisma/models/AuthUser.d.ts +1645 -0
- package/dist/generated/prisma/models/AuthUser.js +1 -0
- package/dist/generated/prisma/models/BudgetGroup.d.ts +1548 -0
- package/dist/generated/prisma/models/BudgetGroup.js +1 -0
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +1430 -0
- package/dist/generated/prisma/models/CodingAgentProfile.js +1 -0
- package/dist/generated/prisma/models/CodingProxyRequest.d.ts +1749 -0
- package/dist/generated/prisma/models/CodingProxyRequest.js +1 -0
- package/dist/generated/prisma/models/CodingProxySession.d.ts +1505 -0
- package/dist/generated/prisma/models/CodingProxySession.js +1 -0
- package/dist/generated/prisma/models/CodingRun.d.ts +2402 -0
- package/dist/generated/prisma/models/CodingRun.js +1 -0
- package/dist/generated/prisma/models/Datastore.d.ts +1434 -0
- package/dist/generated/prisma/models/Datastore.js +1 -0
- package/dist/generated/prisma/models/DatastoreEntry.d.ts +1314 -0
- package/dist/generated/prisma/models/DatastoreEntry.js +1 -0
- package/dist/generated/prisma/models/OAuthAuthorizationCode.d.ts +1526 -0
- package/dist/generated/prisma/models/OAuthAuthorizationCode.js +1 -0
- package/dist/generated/prisma/models/OAuthAuthorizationRequest.d.ts +1350 -0
- package/dist/generated/prisma/models/OAuthAuthorizationRequest.js +1 -0
- package/dist/generated/prisma/models/OAuthClient.d.ts +1289 -0
- package/dist/generated/prisma/models/OAuthClient.js +1 -0
- package/dist/generated/prisma/models/OAuthFamily.d.ts +1540 -0
- package/dist/generated/prisma/models/OAuthFamily.js +1 -0
- package/dist/generated/prisma/models/OAuthGrant.d.ts +1280 -0
- package/dist/generated/prisma/models/OAuthGrant.js +1 -0
- package/dist/generated/prisma/models/Principal.d.ts +1790 -0
- package/dist/generated/prisma/models/Principal.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +2281 -0
- package/dist/generated/prisma/models/Run.js +1 -0
- package/dist/generated/prisma/models/SchedulerLease.d.ts +970 -0
- package/dist/generated/prisma/models/SchedulerLease.js +1 -0
- package/dist/generated/prisma/models/Secret.d.ts +1399 -0
- package/dist/generated/prisma/models/Secret.js +1 -0
- package/dist/generated/prisma/models/SecretElicitationOutcome.d.ts +973 -0
- package/dist/generated/prisma/models/SecretElicitationOutcome.js +1 -0
- package/dist/generated/prisma/models/Task.d.ts +1176 -0
- package/dist/generated/prisma/models/Task.js +1 -0
- package/dist/generated/prisma/models/Tool.d.ts +1478 -0
- package/dist/generated/prisma/models/Tool.js +1 -0
- package/dist/generated/prisma/models/Webhook.d.ts +1386 -0
- package/dist/generated/prisma/models/Webhook.js +1 -0
- package/dist/generated/prisma/models.d.ts +32 -0
- package/dist/generated/prisma/models.js +1 -0
- package/dist/import/create.d.ts +1 -1
- package/dist/import/create.js +1 -1
- package/dist/import/index.d.ts +1 -1
- package/dist/mcp/auth/ownership.d.ts +19 -19
- package/dist/mcp/auth/principal.d.ts +1 -1
- package/dist/mcp/auth/resource-server.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/browser.js +20 -6
- package/dist/mcp/auth/self-hosted/cli.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/credentials.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/rate-limit.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/session.d.ts +1 -1
- package/dist/mcp/context.d.ts +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/shared/page-style.d.ts +16 -0
- package/dist/mcp/shared/page-style.js +129 -0
- package/dist/mcp/tasks/manager.d.ts +1 -1
- package/dist/mcp/tools/agents.js +1 -1
- package/dist/mcp/tools/datastore.js +1 -1
- package/dist/mcp/tools/scheduling.js +1 -1
- package/dist/mcp/tools/secret-elicitation-form.d.ts +1 -1
- package/dist/mcp/tools/secret-elicitation-form.js +10 -28
- package/dist/mcp/tools/secret-elicitation.d.ts +1 -1
- package/dist/mcp/tools/subagents.js +1 -1
- package/dist/mcp/tools/tools.js +1 -1
- package/dist/mcp/transport/streamable-http.d.ts +1 -1
- package/dist/mcp/unattended-schedules.d.ts +1 -1
- package/dist/mcp/webhooks/ingress.d.ts +1 -1
- package/dist/providers/auth/index.d.ts +1 -1
- package/dist/providers/auth/self-hosted.d.ts +2 -2
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -1
- package/dist/providers/coding-proxy/runtime.d.ts +1 -1
- package/dist/providers/datastore/postgres.d.ts +1 -1
- package/dist/providers/executor/build.d.ts +1 -1
- package/dist/providers/executor/composition.d.ts +1 -1
- package/dist/providers/executor/container.d.ts +1 -1
- package/dist/providers/executor/container.js +10 -3
- package/dist/providers/executor/dbos.d.ts +1 -1
- package/dist/providers/jobs/docker.d.ts +11 -0
- package/dist/providers/jobs/docker.js +34 -18
- package/dist/providers/jobs/kubernetes.js +5 -10
- package/dist/providers/jobs/types.d.ts +2 -0
- package/dist/providers/memory/postgres.d.ts +1 -1
- package/dist/providers/vcs/git.js +2 -1
- package/dist/quickstart/config.d.ts +30 -0
- package/dist/quickstart/config.js +106 -0
- package/dist/quickstart/index.d.ts +5 -0
- package/dist/quickstart/index.js +527 -0
- package/dist/quickstart/migrate.d.ts +23 -0
- package/dist/quickstart/migrate.js +125 -0
- package/dist/wardby-bin.js +35 -9
- package/package.json +22 -9
- package/prisma/migrate.config.mjs +20 -0
- package/prisma/schema.prisma +10 -2
- package/dist/providers/auth/release-gate.d.ts +0 -1
- package/dist/providers/auth/release-gate.js +0 -4
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { composeProjectName, defaultModel, ensureWardbyIgnored, generatedSecret, providerKeyName, quickstartPaths, readQuickstartEnv, readQuickstartState, resolveProjectDir, writeQuickstartEnv, writeQuickstartState, } from "./config.js";
|
|
9
|
+
import { FETCH_NOTICE, migrateFailureReason, prismaMigrateInvocation, REGISTRY_FAILURE_MESSAGE, runPrismaMigrateInvocation, } from "./migrate.js";
|
|
10
|
+
const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
11
|
+
const composeFile = join(packageRoot, "deploy/local/docker-compose.yml");
|
|
12
|
+
const wardbyBin = join(packageRoot, "bin/wardby.js");
|
|
13
|
+
const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
14
|
+
const DEMO_AGENT = "hello-wardby";
|
|
15
|
+
const DEMO_PROMPT = "You are the Wardby quickstart agent. Reply with a short greeting, then explain in one sentence that Wardby admitted this run against an explicit maximum budget. Do not call tools.";
|
|
16
|
+
function runCommand(command, args, options = {}) {
|
|
17
|
+
const spawnOptions = {
|
|
18
|
+
cwd: options.cwd,
|
|
19
|
+
env: options.env,
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
stdio: options.inherit ? "inherit" : "pipe",
|
|
22
|
+
};
|
|
23
|
+
const result = spawnSync(command, args, spawnOptions);
|
|
24
|
+
if (result.error) {
|
|
25
|
+
return { status: 1, stdout: result.stdout ?? "", stderr: result.error.message };
|
|
26
|
+
}
|
|
27
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
28
|
+
}
|
|
29
|
+
function commandFailure(label, result) {
|
|
30
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
31
|
+
throw new Error(`${label} failed${detail ? `: ${detail}` : ""}`);
|
|
32
|
+
}
|
|
33
|
+
function runtimeEnv(paths, values = readQuickstartEnv(paths)) {
|
|
34
|
+
return { ...process.env, ...values, WARDBY_PROJECT_DIR: paths.projectDir };
|
|
35
|
+
}
|
|
36
|
+
function composeArgs(paths, state, args) {
|
|
37
|
+
return [
|
|
38
|
+
"compose",
|
|
39
|
+
"--project-name",
|
|
40
|
+
state.composeProject,
|
|
41
|
+
"--env-file",
|
|
42
|
+
paths.envFile,
|
|
43
|
+
"--file",
|
|
44
|
+
composeFile,
|
|
45
|
+
...args,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
function runCompose(paths, state, args, options = {}) {
|
|
49
|
+
return runCommand("docker", composeArgs(paths, state, args), {
|
|
50
|
+
cwd: paths.projectDir,
|
|
51
|
+
env: runtimeEnv(paths),
|
|
52
|
+
inherit: options.inherit,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function nodeMajor() {
|
|
56
|
+
return Number(process.versions.node.split(".")[0]);
|
|
57
|
+
}
|
|
58
|
+
function assertPrerequisites() {
|
|
59
|
+
if (nodeMajor() < 24)
|
|
60
|
+
throw new Error(`Node.js 24 or newer is required; found ${process.versions.node}.`);
|
|
61
|
+
const docker = runCommand("docker", ["--version"]);
|
|
62
|
+
if (docker.status !== 0)
|
|
63
|
+
throw new Error("Docker is not installed or is not on PATH.");
|
|
64
|
+
const compose = runCommand("docker", ["compose", "version"]);
|
|
65
|
+
if (compose.status !== 0)
|
|
66
|
+
throw new Error("Docker Compose v2 is required (`docker compose`).");
|
|
67
|
+
const daemon = runCommand("docker", ["info"]);
|
|
68
|
+
if (daemon.status !== 0)
|
|
69
|
+
throw new Error("Docker is installed but its daemon is not available.");
|
|
70
|
+
}
|
|
71
|
+
async function portAvailable(port) {
|
|
72
|
+
return await new Promise((resolve) => {
|
|
73
|
+
const server = createServer();
|
|
74
|
+
server.unref();
|
|
75
|
+
server.once("error", () => resolve(false));
|
|
76
|
+
server.listen({ host: "0.0.0.0", port }, () => server.close(() => resolve(true)));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function selectPostgresPort(firstPort = 55432) {
|
|
80
|
+
for (let port = firstPort; port <= 55532; port += 1) {
|
|
81
|
+
if (await portAvailable(port))
|
|
82
|
+
return port;
|
|
83
|
+
}
|
|
84
|
+
throw new Error("No available local port was found between 55432 and 55532.");
|
|
85
|
+
}
|
|
86
|
+
async function startPostgres(paths, state, config) {
|
|
87
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
88
|
+
const up = runCompose(paths, state, ["up", "--detach", "--wait", "--wait-timeout", "90"]);
|
|
89
|
+
if (up.status === 0)
|
|
90
|
+
return;
|
|
91
|
+
const output = `${up.stderr}\n${up.stdout}`;
|
|
92
|
+
if (!/port is already allocated|address already in use|bind for .* failed/i.test(output)) {
|
|
93
|
+
commandFailure("PostgreSQL startup", up);
|
|
94
|
+
}
|
|
95
|
+
// Docker Desktop's port forwarder is not always visible to a host socket
|
|
96
|
+
// probe. Clean up the failed container and converge on the next port.
|
|
97
|
+
runCompose(paths, state, ["down"]);
|
|
98
|
+
state.postgresPort = await selectPostgresPort(state.postgresPort + 1);
|
|
99
|
+
config.WARDBY_POSTGRES_PORT = String(state.postgresPort);
|
|
100
|
+
config.DATABASE_URL = databaseUrl(config, state.postgresPort);
|
|
101
|
+
writeQuickstartEnv(paths, config);
|
|
102
|
+
writeQuickstartState(paths, state);
|
|
103
|
+
console.log(`! Port was occupied; retrying PostgreSQL on ${state.postgresPort}`);
|
|
104
|
+
}
|
|
105
|
+
throw new Error("PostgreSQL startup could not find a usable port after 10 attempts.");
|
|
106
|
+
}
|
|
107
|
+
function databaseUrl(config, port) {
|
|
108
|
+
const user = encodeURIComponent(config.WARDBY_POSTGRES_USER);
|
|
109
|
+
const password = encodeURIComponent(config.WARDBY_POSTGRES_PASSWORD);
|
|
110
|
+
const database = encodeURIComponent(config.WARDBY_POSTGRES_DB);
|
|
111
|
+
return `postgresql://${user}:${password}@127.0.0.1:${port}/${database}`;
|
|
112
|
+
}
|
|
113
|
+
async function promptLine(question, fallback) {
|
|
114
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
115
|
+
try {
|
|
116
|
+
const answer = (await terminal.question(question)).trim();
|
|
117
|
+
return answer || fallback || "";
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
terminal.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function promptYesNo(question, defaultYes) {
|
|
124
|
+
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
125
|
+
const answer = (await promptLine(`${question}${suffix}`)).toLowerCase();
|
|
126
|
+
if (!answer)
|
|
127
|
+
return defaultYes;
|
|
128
|
+
return answer === "y" || answer === "yes";
|
|
129
|
+
}
|
|
130
|
+
async function promptSecret(question) {
|
|
131
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
132
|
+
throw new Error("A provider key is required in the environment when stdin is not an interactive terminal.");
|
|
133
|
+
}
|
|
134
|
+
process.stdout.write(question);
|
|
135
|
+
process.stdin.setRawMode(true);
|
|
136
|
+
process.stdin.resume();
|
|
137
|
+
process.stdin.setEncoding("utf8");
|
|
138
|
+
return await new Promise((resolve, reject) => {
|
|
139
|
+
let value = "";
|
|
140
|
+
const finish = (error) => {
|
|
141
|
+
process.stdin.off("data", onData);
|
|
142
|
+
process.stdin.setRawMode(false);
|
|
143
|
+
process.stdin.pause();
|
|
144
|
+
process.stdout.write("\n");
|
|
145
|
+
if (error)
|
|
146
|
+
reject(error);
|
|
147
|
+
else
|
|
148
|
+
resolve(value);
|
|
149
|
+
};
|
|
150
|
+
const onData = (chunk) => {
|
|
151
|
+
for (const character of chunk) {
|
|
152
|
+
if (character === "\u0003")
|
|
153
|
+
return finish(new Error("cancelled"));
|
|
154
|
+
if (character === "\r" || character === "\n")
|
|
155
|
+
return finish();
|
|
156
|
+
if (character === "\u007f" || character === "\b")
|
|
157
|
+
value = value.slice(0, -1);
|
|
158
|
+
else if (character >= " ")
|
|
159
|
+
value += character;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
process.stdin.on("data", onData);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function parseProvider(value) {
|
|
166
|
+
if (value === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
if (value !== "openai" && value !== "anthropic") {
|
|
169
|
+
throw new Error(`--provider must be openai or anthropic; got "${value}".`);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function parseClient(value) {
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
return undefined;
|
|
176
|
+
if (!["none", "codex", "claude", "both"].includes(value)) {
|
|
177
|
+
throw new Error(`--client must be none, codex, claude, or both; got "${value}".`);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
async function chooseProvider(requested, previous, existing, nonInteractive) {
|
|
182
|
+
if (requested)
|
|
183
|
+
return requested;
|
|
184
|
+
if (previous)
|
|
185
|
+
return previous.provider;
|
|
186
|
+
if (existing.LLM_PROVIDER === "openai" || existing.LLM_PROVIDER === "anthropic")
|
|
187
|
+
return existing.LLM_PROVIDER;
|
|
188
|
+
if (process.env.OPENAI_API_KEY || existing.OPENAI_API_KEY)
|
|
189
|
+
return "openai";
|
|
190
|
+
if (process.env.ANTHROPIC_API_KEY || existing.ANTHROPIC_API_KEY)
|
|
191
|
+
return "anthropic";
|
|
192
|
+
if (nonInteractive)
|
|
193
|
+
throw new Error("--provider is required in non-interactive mode.");
|
|
194
|
+
const answer = (await promptLine("Model provider: [1] OpenAI, [2] Anthropic [1] ", "1")).toLowerCase();
|
|
195
|
+
if (answer === "1" || answer === "openai")
|
|
196
|
+
return "openai";
|
|
197
|
+
if (answer === "2" || answer === "anthropic")
|
|
198
|
+
return "anthropic";
|
|
199
|
+
throw new Error(`Unknown provider selection "${answer}".`);
|
|
200
|
+
}
|
|
201
|
+
function applyMigrations(paths) {
|
|
202
|
+
const invocation = prismaMigrateInvocation("deploy", runtimeEnv(paths));
|
|
203
|
+
if (!invocation.local)
|
|
204
|
+
console.log(FETCH_NOTICE);
|
|
205
|
+
const result = runPrismaMigrateInvocation(invocation, paths.projectDir);
|
|
206
|
+
if (result.registryFailure) {
|
|
207
|
+
const detail = /^npm (?:error|ERR!) code .*$/m.exec(result.stderr)?.[0] ?? result.stderr.trim().split("\n")[0];
|
|
208
|
+
throw new Error(`${REGISTRY_FAILURE_MESSAGE}${detail ? ` (${detail})` : ""}`);
|
|
209
|
+
}
|
|
210
|
+
if (result.status !== 0)
|
|
211
|
+
commandFailure("Database migration", result);
|
|
212
|
+
}
|
|
213
|
+
async function seedDemo(paths, state, budget) {
|
|
214
|
+
const { createPrismaClient } = await import("../core/db.js");
|
|
215
|
+
const db = createPrismaClient(readQuickstartEnv(paths).DATABASE_URL);
|
|
216
|
+
try {
|
|
217
|
+
const existing = await db.agent.findUnique({ where: { name: DEMO_AGENT } });
|
|
218
|
+
if (existing && existing.systemPrompt !== DEMO_PROMPT) {
|
|
219
|
+
throw new Error(`An agent named "${DEMO_AGENT}" already exists and was not created by quickstart; rename it or use --skip-demo.`);
|
|
220
|
+
}
|
|
221
|
+
if (existing) {
|
|
222
|
+
await db.agent.update({
|
|
223
|
+
where: { name: DEMO_AGENT },
|
|
224
|
+
data: { model: state.model, budgetUsd: budget, maxTurns: 2 },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
await db.agent.create({
|
|
229
|
+
data: {
|
|
230
|
+
name: DEMO_AGENT,
|
|
231
|
+
model: state.model,
|
|
232
|
+
budgetUsd: budget,
|
|
233
|
+
maxTurns: 2,
|
|
234
|
+
systemPrompt: DEMO_PROMPT,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await db.$disconnect();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function runDemo(paths) {
|
|
244
|
+
const result = runCommand(process.execPath, [wardbyBin, "run", DEMO_AGENT], {
|
|
245
|
+
cwd: paths.projectDir,
|
|
246
|
+
env: runtimeEnv(paths),
|
|
247
|
+
inherit: true,
|
|
248
|
+
});
|
|
249
|
+
if (result.status !== 0)
|
|
250
|
+
throw new Error("The sample agent run failed; run `wardby doctor` for diagnostics.");
|
|
251
|
+
}
|
|
252
|
+
function clientInstalled(command) {
|
|
253
|
+
return runCommand(command, ["--version"]).status === 0;
|
|
254
|
+
}
|
|
255
|
+
function configureOneMcpClient(client, paths) {
|
|
256
|
+
if (!clientInstalled(client)) {
|
|
257
|
+
console.warn(`! ${client} is not installed; skipped its MCP configuration.`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const existing = runCommand(client, ["mcp", "get", "wardby"], { cwd: paths.projectDir });
|
|
261
|
+
if (existing.status === 0) {
|
|
262
|
+
console.log(`✓ ${client} already has an MCP server named wardby.`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const packageSpec = `@wardby/cli@${packageJson.version}`;
|
|
266
|
+
const args = client === "codex"
|
|
267
|
+
? [
|
|
268
|
+
"mcp",
|
|
269
|
+
"add",
|
|
270
|
+
"--env",
|
|
271
|
+
`WARDBY_PROJECT_DIR=${paths.projectDir}`,
|
|
272
|
+
"wardby",
|
|
273
|
+
"--",
|
|
274
|
+
"npx",
|
|
275
|
+
"--yes",
|
|
276
|
+
packageSpec,
|
|
277
|
+
"mcp",
|
|
278
|
+
]
|
|
279
|
+
: [
|
|
280
|
+
"mcp",
|
|
281
|
+
"add",
|
|
282
|
+
"--scope",
|
|
283
|
+
"local",
|
|
284
|
+
"-e",
|
|
285
|
+
`WARDBY_PROJECT_DIR=${paths.projectDir}`,
|
|
286
|
+
"wardby",
|
|
287
|
+
"--",
|
|
288
|
+
"npx",
|
|
289
|
+
"--yes",
|
|
290
|
+
packageSpec,
|
|
291
|
+
"mcp",
|
|
292
|
+
];
|
|
293
|
+
const result = runCommand(client, args, { cwd: paths.projectDir });
|
|
294
|
+
if (result.status !== 0)
|
|
295
|
+
commandFailure(`${client} MCP configuration`, result);
|
|
296
|
+
console.log(`✓ Configured Wardby MCP for ${client}.`);
|
|
297
|
+
}
|
|
298
|
+
function configureMcpClients(client, paths) {
|
|
299
|
+
if (client === "codex" || client === "both")
|
|
300
|
+
configureOneMcpClient("codex", paths);
|
|
301
|
+
if (client === "claude" || client === "both")
|
|
302
|
+
configureOneMcpClient("claude", paths);
|
|
303
|
+
}
|
|
304
|
+
async function chooseClient(nonInteractive) {
|
|
305
|
+
if (nonInteractive)
|
|
306
|
+
return "none";
|
|
307
|
+
const answer = (await promptLine("Configure MCP: [n]one, [c]odex, c[l]aude, [b]oth [n] ", "n")).toLowerCase();
|
|
308
|
+
if (answer === "" || answer === "n" || answer === "none")
|
|
309
|
+
return "none";
|
|
310
|
+
if (answer === "c" || answer === "codex")
|
|
311
|
+
return "codex";
|
|
312
|
+
if (answer === "l" || answer === "claude")
|
|
313
|
+
return "claude";
|
|
314
|
+
if (answer === "b" || answer === "both")
|
|
315
|
+
return "both";
|
|
316
|
+
throw new Error(`Unknown MCP client selection "${answer}".`);
|
|
317
|
+
}
|
|
318
|
+
export async function quickstartCommand(args) {
|
|
319
|
+
const { values } = parseArgs({
|
|
320
|
+
args,
|
|
321
|
+
options: {
|
|
322
|
+
provider: { type: "string" },
|
|
323
|
+
model: { type: "string" },
|
|
324
|
+
budget: { type: "string" },
|
|
325
|
+
client: { type: "string" },
|
|
326
|
+
"non-interactive": { type: "boolean" },
|
|
327
|
+
"skip-demo": { type: "boolean" },
|
|
328
|
+
yes: { type: "boolean", short: "y" },
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const nonInteractive = values["non-interactive"] ?? false;
|
|
332
|
+
const skipDemo = values["skip-demo"] ?? false;
|
|
333
|
+
const budget = values.budget === undefined ? 1 : Number(values.budget);
|
|
334
|
+
if (!Number.isFinite(budget) || budget <= 0)
|
|
335
|
+
throw new Error(`--budget must be positive; got "${values.budget}".`);
|
|
336
|
+
if (nonInteractive && !skipDemo && !values.yes) {
|
|
337
|
+
throw new Error("Non-interactive demo execution requires --yes because it may incur provider charges.");
|
|
338
|
+
}
|
|
339
|
+
console.log("Wardby quickstart\n");
|
|
340
|
+
assertPrerequisites();
|
|
341
|
+
console.log(`✓ Node.js ${process.versions.node}`);
|
|
342
|
+
console.log("✓ Docker and Docker Compose are available");
|
|
343
|
+
const projectDir = resolveProjectDir();
|
|
344
|
+
const paths = quickstartPaths(projectDir);
|
|
345
|
+
const previous = readQuickstartState(paths);
|
|
346
|
+
const existing = readQuickstartEnv(paths);
|
|
347
|
+
const provider = await chooseProvider(parseProvider(values.provider), previous, existing, nonInteractive);
|
|
348
|
+
const model = values.model ?? (previous?.provider === provider ? previous.model : defaultModel(provider));
|
|
349
|
+
const keyName = providerKeyName(provider);
|
|
350
|
+
let providerKey = process.env[keyName] || existing[keyName];
|
|
351
|
+
if (!providerKey && !skipDemo) {
|
|
352
|
+
if (nonInteractive)
|
|
353
|
+
throw new Error(`${keyName} is required in non-interactive mode unless --skip-demo is used.`);
|
|
354
|
+
providerKey = await promptSecret(`${keyName}: `);
|
|
355
|
+
if (!providerKey)
|
|
356
|
+
throw new Error(`${keyName} cannot be empty.`);
|
|
357
|
+
}
|
|
358
|
+
const postgresPort = previous?.postgresPort ?? (await selectPostgresPort());
|
|
359
|
+
const postgresUser = existing.WARDBY_POSTGRES_USER || "wardby";
|
|
360
|
+
const postgresDatabase = existing.WARDBY_POSTGRES_DB || "wardby";
|
|
361
|
+
const postgresPassword = existing.WARDBY_POSTGRES_PASSWORD || generatedSecret();
|
|
362
|
+
const config = {
|
|
363
|
+
...existing,
|
|
364
|
+
LLM_PROVIDER: provider,
|
|
365
|
+
LOCAL_PRINCIPAL: existing.LOCAL_PRINCIPAL || "local",
|
|
366
|
+
MCP_TRANSPORT: "stdio",
|
|
367
|
+
SECRET_APP_KEY: existing.SECRET_APP_KEY || generatedSecret(),
|
|
368
|
+
WARDBY_POSTGRES_DB: postgresDatabase,
|
|
369
|
+
WARDBY_POSTGRES_PASSWORD: postgresPassword,
|
|
370
|
+
WARDBY_POSTGRES_PORT: String(postgresPort),
|
|
371
|
+
WARDBY_POSTGRES_USER: postgresUser,
|
|
372
|
+
};
|
|
373
|
+
config.DATABASE_URL = databaseUrl(config, postgresPort);
|
|
374
|
+
if (providerKey)
|
|
375
|
+
config[keyName] = providerKey;
|
|
376
|
+
ensureWardbyIgnored(projectDir);
|
|
377
|
+
writeQuickstartEnv(paths, config);
|
|
378
|
+
const state = {
|
|
379
|
+
version: 1,
|
|
380
|
+
projectDir,
|
|
381
|
+
composeProject: previous?.composeProject ?? composeProjectName(projectDir),
|
|
382
|
+
postgresPort,
|
|
383
|
+
provider,
|
|
384
|
+
model,
|
|
385
|
+
packageVersion: packageJson.version,
|
|
386
|
+
createdAt: previous?.createdAt ?? new Date().toISOString(),
|
|
387
|
+
};
|
|
388
|
+
writeQuickstartState(paths, state);
|
|
389
|
+
console.log(`✓ Configuration ready in ${paths.wardbyDir}`);
|
|
390
|
+
await startPostgres(paths, state, config);
|
|
391
|
+
console.log(`✓ PostgreSQL is healthy on 127.0.0.1:${state.postgresPort}`);
|
|
392
|
+
applyMigrations(paths);
|
|
393
|
+
console.log("✓ Database migrations applied");
|
|
394
|
+
if (!skipDemo) {
|
|
395
|
+
await seedDemo(paths, state, budget);
|
|
396
|
+
console.log(`✓ Sample agent "${DEMO_AGENT}" is ready`);
|
|
397
|
+
const approved = values.yes || (await promptYesNo(`Run it now with a maximum budget of $${budget.toFixed(2)}?`, true));
|
|
398
|
+
if (approved)
|
|
399
|
+
runDemo(paths);
|
|
400
|
+
else
|
|
401
|
+
console.log(`Skipped the billed run. Start it later with: npx @wardby/cli@latest run ${DEMO_AGENT}`);
|
|
402
|
+
}
|
|
403
|
+
const client = parseClient(values.client) ?? (await chooseClient(nonInteractive));
|
|
404
|
+
if (client !== "none")
|
|
405
|
+
configureMcpClients(client, paths);
|
|
406
|
+
console.log("\nWardby is ready.");
|
|
407
|
+
console.log(" npx @wardby/cli@latest status");
|
|
408
|
+
console.log(" npx @wardby/cli@latest doctor");
|
|
409
|
+
console.log(" npx @wardby/cli@latest down");
|
|
410
|
+
}
|
|
411
|
+
async function databaseHealthy(paths) {
|
|
412
|
+
const env = readQuickstartEnv(paths);
|
|
413
|
+
if (!env.DATABASE_URL)
|
|
414
|
+
return false;
|
|
415
|
+
const { createPrismaClient } = await import("../core/db.js");
|
|
416
|
+
const db = createPrismaClient(env.DATABASE_URL);
|
|
417
|
+
try {
|
|
418
|
+
await db.$queryRawUnsafe("SELECT 1");
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
await db.$disconnect();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export async function doctorCommand(args) {
|
|
429
|
+
parseArgs({ args, options: {} });
|
|
430
|
+
const paths = quickstartPaths(resolveProjectDir());
|
|
431
|
+
const checks = [];
|
|
432
|
+
checks.push(["Node.js 24 or newer", nodeMajor() >= 24, process.versions.node]);
|
|
433
|
+
checks.push(["Docker CLI", runCommand("docker", ["--version"]).status === 0]);
|
|
434
|
+
checks.push(["Docker Compose v2", runCommand("docker", ["compose", "version"]).status === 0]);
|
|
435
|
+
checks.push(["Docker daemon", runCommand("docker", ["info"]).status === 0]);
|
|
436
|
+
let state;
|
|
437
|
+
try {
|
|
438
|
+
state = readQuickstartState(paths);
|
|
439
|
+
checks.push(["Quickstart state", state !== undefined, paths.stateFile]);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
checks.push(["Quickstart state", false, error instanceof Error ? error.message : String(error)]);
|
|
443
|
+
}
|
|
444
|
+
const env = readQuickstartEnv(paths);
|
|
445
|
+
const key = state ? providerKeyName(state.provider) : undefined;
|
|
446
|
+
checks.push(["Private configuration", Object.keys(env).length > 0, paths.envFile]);
|
|
447
|
+
if (Object.keys(env).length > 0) {
|
|
448
|
+
const mode = statSync(paths.envFile).mode & 0o777;
|
|
449
|
+
checks.push(["Configuration permissions", (mode & 0o077) === 0, mode.toString(8)]);
|
|
450
|
+
checks.push(["Secret encryption key", /^[a-f0-9]{64}$/i.test(env.SECRET_APP_KEY ?? "")]);
|
|
451
|
+
if (key)
|
|
452
|
+
checks.push([`${key} configured`, Boolean(env[key] || process.env[key])]);
|
|
453
|
+
}
|
|
454
|
+
if (state) {
|
|
455
|
+
const ps = runCompose(paths, state, ["ps", "--status", "running", "--quiet"]);
|
|
456
|
+
checks.push(["PostgreSQL container", ps.status === 0 && ps.stdout.trim().length > 0]);
|
|
457
|
+
checks.push(["Database connection", await databaseHealthy(paths)]);
|
|
458
|
+
const invocation = prismaMigrateInvocation("status", runtimeEnv(paths));
|
|
459
|
+
if (!invocation.local)
|
|
460
|
+
console.log(FETCH_NOTICE);
|
|
461
|
+
const migrations = runPrismaMigrateInvocation(invocation, paths.projectDir);
|
|
462
|
+
checks.push([
|
|
463
|
+
"Database migrations",
|
|
464
|
+
migrations.status === 0,
|
|
465
|
+
migrations.status === 0 ? undefined : migrateFailureReason(migrations),
|
|
466
|
+
]);
|
|
467
|
+
}
|
|
468
|
+
let failed = false;
|
|
469
|
+
for (const [name, ok, detail] of checks) {
|
|
470
|
+
failed ||= !ok;
|
|
471
|
+
console.log(`${ok ? "✓" : "✗"} ${name}${detail ? ` (${detail})` : ""}`);
|
|
472
|
+
}
|
|
473
|
+
if (failed)
|
|
474
|
+
process.exitCode = 1;
|
|
475
|
+
}
|
|
476
|
+
export async function statusCommand(args) {
|
|
477
|
+
parseArgs({ args, options: {} });
|
|
478
|
+
const paths = quickstartPaths(resolveProjectDir());
|
|
479
|
+
const state = readQuickstartState(paths);
|
|
480
|
+
if (!state)
|
|
481
|
+
throw new Error(`Wardby is not initialized in ${paths.projectDir}; run \`wardby quickstart\`.`);
|
|
482
|
+
console.log(`project: ${state.projectDir}`);
|
|
483
|
+
console.log(`version: ${state.packageVersion}`);
|
|
484
|
+
console.log(`provider/model: ${state.provider} / ${state.model}`);
|
|
485
|
+
console.log(`postgres: 127.0.0.1:${state.postgresPort}`);
|
|
486
|
+
const ps = runCompose(paths, state, ["ps"]);
|
|
487
|
+
if (ps.status !== 0)
|
|
488
|
+
commandFailure("Docker Compose status", ps);
|
|
489
|
+
process.stdout.write(ps.stdout);
|
|
490
|
+
const healthy = await databaseHealthy(paths);
|
|
491
|
+
console.log(`database: ${healthy ? "healthy" : "unavailable"}`);
|
|
492
|
+
if (!healthy)
|
|
493
|
+
process.exitCode = 1;
|
|
494
|
+
}
|
|
495
|
+
export async function logsCommand(args) {
|
|
496
|
+
const { values } = parseArgs({
|
|
497
|
+
args,
|
|
498
|
+
options: {
|
|
499
|
+
follow: { type: "boolean", short: "f" },
|
|
500
|
+
tail: { type: "string" },
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
const paths = quickstartPaths(resolveProjectDir());
|
|
504
|
+
const state = readQuickstartState(paths);
|
|
505
|
+
if (!state)
|
|
506
|
+
throw new Error(`Wardby is not initialized in ${paths.projectDir}; run \`wardby quickstart\`.`);
|
|
507
|
+
const composeLogArgs = ["logs", "--tail", values.tail ?? "100"];
|
|
508
|
+
if (values.follow)
|
|
509
|
+
composeLogArgs.push("--follow");
|
|
510
|
+
const result = runCompose(paths, state, composeLogArgs, { inherit: true });
|
|
511
|
+
if (result.status !== 0)
|
|
512
|
+
throw new Error("Docker Compose logs failed.");
|
|
513
|
+
}
|
|
514
|
+
export async function downCommand(args) {
|
|
515
|
+
const { values } = parseArgs({ args, options: { volumes: { type: "boolean" } } });
|
|
516
|
+
const paths = quickstartPaths(resolveProjectDir());
|
|
517
|
+
const state = readQuickstartState(paths);
|
|
518
|
+
if (!state)
|
|
519
|
+
throw new Error(`Wardby is not initialized in ${paths.projectDir}; nothing to stop.`);
|
|
520
|
+
const composeDownArgs = ["down"];
|
|
521
|
+
if (values.volumes)
|
|
522
|
+
composeDownArgs.push("--volumes");
|
|
523
|
+
const result = runCompose(paths, state, composeDownArgs, { inherit: true });
|
|
524
|
+
if (result.status !== 0)
|
|
525
|
+
throw new Error("Docker Compose shutdown failed.");
|
|
526
|
+
console.log(values.volumes ? "Wardby stopped and its local database volume was removed." : "Wardby stopped; data retained.");
|
|
527
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const PRISMA_CLI_VERSION = "7.10.0";
|
|
2
|
+
export declare const FETCH_NOTICE = "Fetching the Prisma CLI (prisma@7.10.0)\u2026";
|
|
3
|
+
export declare const REGISTRY_FAILURE_MESSAGE: string;
|
|
4
|
+
export declare const migrateConfigFile: string;
|
|
5
|
+
export interface MigrateResult {
|
|
6
|
+
status: number;
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
registryFailure?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface MigrateInvocation {
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
local: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseLocalPrismaVersion(stdout: string): string | undefined;
|
|
18
|
+
export declare function probeLocalPrismaVersion(env: NodeJS.ProcessEnv): string | undefined;
|
|
19
|
+
export declare function prismaMigrateInvocation(subcommand: "deploy" | "status", env: NodeJS.ProcessEnv, probe?: (env: NodeJS.ProcessEnv) => string | undefined): MigrateInvocation;
|
|
20
|
+
export declare function classifyMigrateResult(result: MigrateResult, spawnError?: Error, local?: boolean): MigrateResult;
|
|
21
|
+
export declare function migrateFailureReason(result: MigrateResult): string;
|
|
22
|
+
export declare function runPrismaMigrateInvocation(invocation: MigrateInvocation, cwd: string): MigrateResult;
|
|
23
|
+
export declare function runPrismaMigrate(subcommand: "deploy" | "status", env: NodeJS.ProcessEnv, cwd: string, probe?: (env: NodeJS.ProcessEnv) => string | undefined): MigrateResult;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
// The installed npm package does not carry the Prisma CLI (a devDependency:
|
|
4
|
+
// its tree pulled advisories into every consumer install), so quickstart and
|
|
5
|
+
// doctor first look for a `prisma` executable on PATH and use it only if it
|
|
6
|
+
// reports exactly this version; anything else -- wrong version, not on PATH,
|
|
7
|
+
// a spawn error, a non-zero exit, or a timeout -- falls back to running the
|
|
8
|
+
// pinned version through npx, with a config file shipped in the package.
|
|
9
|
+
// Keep this equal to package.json's devDependencies.prisma (migrate.test.ts
|
|
10
|
+
// enforces it).
|
|
11
|
+
//
|
|
12
|
+
// Not supported on Windows: npx is `npx.cmd` there (and a local install would
|
|
13
|
+
// be `prisma.cmd`), which spawnSync can only run through a shell, and
|
|
14
|
+
// quickstart has no win32 handling anywhere else (Docker Compose paths,
|
|
15
|
+
// POSIX file modes) either.
|
|
16
|
+
export const PRISMA_CLI_VERSION = "7.10.0";
|
|
17
|
+
export const FETCH_NOTICE = `Fetching the Prisma CLI (prisma@${PRISMA_CLI_VERSION})…`;
|
|
18
|
+
export const REGISTRY_FAILURE_MESSAGE = `Could not download the Prisma CLI (prisma@${PRISMA_CLI_VERSION}) from the npm registry. ` +
|
|
19
|
+
"quickstart and doctor fetch it on demand. Check your internet connection or npm registry settings " +
|
|
20
|
+
"(.npmrc, proxy), then retry. You can also install it yourself " +
|
|
21
|
+
`(\`npm i -g prisma@${PRISMA_CLI_VERSION}\`) and rerun quickstart -- doctor benefits too.`;
|
|
22
|
+
export const migrateConfigFile = fileURLToPath(new URL("../../prisma/migrate.config.mjs", import.meta.url));
|
|
23
|
+
// A short timeout so a wedged or misbehaving local binary can't stall
|
|
24
|
+
// quickstart/doctor -- any hang past this falls back to npx.
|
|
25
|
+
const LOCAL_PRISMA_PROBE_TIMEOUT_MS = 5000;
|
|
26
|
+
// `prisma --version` prints a fixed-label, padded-column report, e.g.:
|
|
27
|
+
// prisma : 7.10.0
|
|
28
|
+
// @prisma/client : 7.10.0
|
|
29
|
+
// Operating System : darwin
|
|
30
|
+
// ...
|
|
31
|
+
// Several other lines carry their own version-shaped numbers (Node.js,
|
|
32
|
+
// TypeScript, the schema engine hash), so key off the "prisma" label
|
|
33
|
+
// specifically -- not the first x.y.z token anywhere in the output.
|
|
34
|
+
const LOCAL_VERSION_LINE = /^prisma\s*:\s*(\S+)$/m;
|
|
35
|
+
export function parseLocalPrismaVersion(stdout) {
|
|
36
|
+
return LOCAL_VERSION_LINE.exec(stdout)?.[1];
|
|
37
|
+
}
|
|
38
|
+
// Runs a local `prisma --version` under the given env's PATH and returns the
|
|
39
|
+
// version it reports, or undefined for anything that isn't a clean match:
|
|
40
|
+
// not on PATH, a spawn error, a non-zero exit, a timeout, or unparseable
|
|
41
|
+
// output. Never throws.
|
|
42
|
+
export function probeLocalPrismaVersion(env) {
|
|
43
|
+
const result = spawnSync("prisma", ["--version"], {
|
|
44
|
+
env,
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
stdio: "pipe",
|
|
47
|
+
timeout: LOCAL_PRISMA_PROBE_TIMEOUT_MS,
|
|
48
|
+
});
|
|
49
|
+
if (result.error || result.status !== 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
return parseLocalPrismaVersion(result.stdout ?? "");
|
|
52
|
+
}
|
|
53
|
+
// DATABASE_URL travels in the child's environment only -- never argv, where
|
|
54
|
+
// any local user could read it from the process table. `probe` is injectable
|
|
55
|
+
// so tests never depend on the developer's real PATH.
|
|
56
|
+
export function prismaMigrateInvocation(subcommand, env, probe = probeLocalPrismaVersion) {
|
|
57
|
+
const childEnv = {
|
|
58
|
+
...env,
|
|
59
|
+
PRISMA_HIDE_UPDATE_MESSAGE: "1",
|
|
60
|
+
npm_config_update_notifier: "false",
|
|
61
|
+
npm_config_fund: "false",
|
|
62
|
+
};
|
|
63
|
+
if (probe(childEnv) === PRISMA_CLI_VERSION) {
|
|
64
|
+
return {
|
|
65
|
+
command: "prisma",
|
|
66
|
+
args: ["migrate", subcommand, "--config", migrateConfigFile],
|
|
67
|
+
env: childEnv,
|
|
68
|
+
local: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
command: "npx",
|
|
73
|
+
args: ["--yes", `prisma@${PRISMA_CLI_VERSION}`, "migrate", subcommand, "--config", migrateConfigFile],
|
|
74
|
+
env: childEnv,
|
|
75
|
+
local: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// npm's own error lines when it cannot fetch a package (npm 7+ prints
|
|
79
|
+
// "npm error", older "npm ERR!"): network failures and registry HTTP / version
|
|
80
|
+
// errors. A failed migration is printed by Prisma itself and never carries
|
|
81
|
+
// these -- in particular a database ECONNREFUSED is Prisma's P1001, not an
|
|
82
|
+
// "npm error code" line.
|
|
83
|
+
const REGISTRY_ERROR = /^npm (?:error|ERR!) (?:code|errno) (?:ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ETARGET|E[45]\d\d)\b|^npm (?:error|ERR!) network /m;
|
|
84
|
+
// `local` marks that `result`/`spawnError` came from the locally installed
|
|
85
|
+
// binary rather than npx: nothing was fetched, so neither an npm-shaped
|
|
86
|
+
// error line nor a spawn error is a registry failure there -- a spawn error
|
|
87
|
+
// still gets a clear message (below), just not that one.
|
|
88
|
+
export function classifyMigrateResult(result, spawnError, local = false) {
|
|
89
|
+
if (spawnError) {
|
|
90
|
+
if (local)
|
|
91
|
+
return { ...result, status: 1, stderr: spawnError.message };
|
|
92
|
+
return { ...result, status: 1, stderr: spawnError.message, registryFailure: true };
|
|
93
|
+
}
|
|
94
|
+
if (!local && result.status !== 0 && REGISTRY_ERROR.test(result.stderr))
|
|
95
|
+
return { ...result, registryFailure: true };
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
// The one-line reason for a failed run: the registry message, or Prisma's own
|
|
99
|
+
// first error line (URLs masked, though Prisma prints host and database only).
|
|
100
|
+
export function migrateFailureReason(result) {
|
|
101
|
+
if (result.registryFailure)
|
|
102
|
+
return REGISTRY_FAILURE_MESSAGE;
|
|
103
|
+
const lines = `${result.stderr}\n${result.stdout}`
|
|
104
|
+
.split("\n")
|
|
105
|
+
.map((line) => line.trim())
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
// A line that starts with "Error", or that carries a Prisma error code anywhere.
|
|
108
|
+
const line = lines.find((candidate) => candidate.startsWith("Error") || /\bP\d{4}\b/.test(candidate)) ?? lines[0] ?? "no output";
|
|
109
|
+
return line.replace(/postgres(ql)?:\/\/[^\s`'"]+/gi, "<database url>");
|
|
110
|
+
}
|
|
111
|
+
// Runs an already-resolved invocation (from `prismaMigrateInvocation`). Split
|
|
112
|
+
// out so callers can decide whether to print `FETCH_NOTICE` -- which only
|
|
113
|
+
// applies to the npx path -- before running it.
|
|
114
|
+
export function runPrismaMigrateInvocation(invocation, cwd) {
|
|
115
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
116
|
+
cwd,
|
|
117
|
+
env: invocation.env,
|
|
118
|
+
encoding: "utf8",
|
|
119
|
+
stdio: "pipe",
|
|
120
|
+
});
|
|
121
|
+
return classifyMigrateResult({ status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" }, result.error, invocation.local);
|
|
122
|
+
}
|
|
123
|
+
export function runPrismaMigrate(subcommand, env, cwd, probe) {
|
|
124
|
+
return runPrismaMigrateInvocation(prismaMigrateInvocation(subcommand, env, probe), cwd);
|
|
125
|
+
}
|