@the-meridian/cli 0.1.0

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/index.js ADDED
@@ -0,0 +1,4319 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { realpathSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { Command } from "commander";
7
+
8
+ // src/commands/db.ts
9
+ import { readFileSync as readFileSync7 } from "fs";
10
+ import { join as join6 } from "path";
11
+
12
+ // src/version.ts
13
+ import { createRequire } from "module";
14
+ var VERSION = (() => {
15
+ try {
16
+ const require2 = createRequire(import.meta.url);
17
+ const pkg = require2("../package.json");
18
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
19
+ } catch {
20
+ return "0.0.0";
21
+ }
22
+ })();
23
+ var USER_AGENT = `meridian-cli/${VERSION} (${process.platform} ${process.arch})`;
24
+
25
+ // src/lib/api.ts
26
+ import { hostname as osHostname } from "os";
27
+ import { z as z2 } from "zod";
28
+
29
+ // src/lib/api-types.ts
30
+ import { z } from "zod";
31
+ var idSchema = z.union([z.number(), z.string()]);
32
+ var cliUserSchema = z.object({
33
+ id: idSchema,
34
+ name: z.string(),
35
+ email: z.string()
36
+ }).loose();
37
+ var orgSchema = z.object({
38
+ id: idSchema,
39
+ name: z.string(),
40
+ slug: z.string()
41
+ }).loose();
42
+ var appSummarySchema = z.object({
43
+ id: idSchema,
44
+ name: z.string(),
45
+ slug: z.string()
46
+ }).loose();
47
+ var meResponseSchema = z.object({
48
+ user: cliUserSchema,
49
+ orgs: z.array(orgSchema)
50
+ }).loose();
51
+ var deviceCodeResponseSchema = z.object({
52
+ device_code: z.string(),
53
+ user_code: z.string(),
54
+ verification_uri: z.string(),
55
+ verification_uri_complete: z.string().optional(),
56
+ interval: z.number().optional(),
57
+ expires_in: z.number().optional()
58
+ }).loose();
59
+ var deviceTokenResponseSchema = z.object({
60
+ token: z.string(),
61
+ expires_at: z.string().optional(),
62
+ user: cliUserSchema
63
+ }).loose();
64
+ var deviceInfoResponseSchema = z.object({
65
+ hostname: z.string(),
66
+ client_name: z.string(),
67
+ created_at: z.string().optional()
68
+ }).loose();
69
+ var devDatabaseStatusSchema = z.union([z.enum(["provisioning", "ready", "error"]), z.string()]);
70
+ var devDatabaseSchema = z.object({
71
+ id: idSchema,
72
+ status: devDatabaseStatusSchema,
73
+ database: z.string(),
74
+ /** Nullish for an API older than per-developer Prisma shadow databases. */
75
+ shadow_database: z.string().nullish(),
76
+ mysql_user: z.string(),
77
+ instance_connection_name: z.string().nullish(),
78
+ region: z.string().nullish(),
79
+ created_at: z.string().nullish(),
80
+ /** Set by the API only while `status` is `error`; surfaced verbatim to the developer. */
81
+ error_message: z.string().nullish()
82
+ }).loose();
83
+ var devCredentialsSchema = z.object({
84
+ host: z.string(),
85
+ port: z.coerce.number(),
86
+ database: z.string(),
87
+ /**
88
+ * The developer's Prisma shadow database, reachable with these same
89
+ * credentials. Nullish for an API older than shadow databases, in which
90
+ * case no SHADOW_DATABASE_URL is written.
91
+ */
92
+ shadow_database: z.string().nullish(),
93
+ username: z.string(),
94
+ password: z.string(),
95
+ expires_at: z.string().nullish(),
96
+ /** Echoed from the (nullable) dev-database column, so it can be null. */
97
+ instance_connection_name: z.string().nullish()
98
+ }).loose();
99
+ var proxyTokenSchema = z.object({
100
+ access_token: z.string(),
101
+ expires_at: z.string().nullish(),
102
+ instance_connection_name: z.string().nullish()
103
+ }).loose();
104
+ var migrationRunInputSchema = z.object({
105
+ command: z.string(),
106
+ status: z.enum(["success", "failed", "rolled_back"]),
107
+ started_at: z.string(),
108
+ finished_at: z.string(),
109
+ output_excerpt: z.string().optional()
110
+ });
111
+ var migrationRunSchema = z.object({
112
+ id: idSchema.nullish(),
113
+ command: z.string(),
114
+ status: z.string(),
115
+ started_at: z.string().nullish(),
116
+ finished_at: z.string().nullish(),
117
+ output_excerpt: z.string().nullish(),
118
+ created_at: z.string().nullish()
119
+ }).loose();
120
+
121
+ // src/lib/errors.ts
122
+ var CliError = class extends Error {
123
+ hint;
124
+ exitCode;
125
+ constructor(message, options = {}) {
126
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
127
+ this.name = "CliError";
128
+ this.hint = options.hint;
129
+ this.exitCode = options.exitCode ?? 1;
130
+ }
131
+ };
132
+ var ApiError = class extends CliError {
133
+ status;
134
+ body;
135
+ /** `error` field from the response body, e.g. "authorization_pending". */
136
+ errorCode;
137
+ constructor(message, init) {
138
+ super(message, init);
139
+ this.name = "ApiError";
140
+ this.status = init.status;
141
+ this.body = init.body;
142
+ this.errorCode = init.errorCode;
143
+ }
144
+ };
145
+ function isCliError(error2) {
146
+ return error2 instanceof CliError;
147
+ }
148
+ function isApiError(error2) {
149
+ return error2 instanceof ApiError;
150
+ }
151
+ function notLoggedInError() {
152
+ return new CliError("You are not logged in to Meridian.", {
153
+ hint: "Run `meridian login` to authenticate this machine."
154
+ });
155
+ }
156
+ function notLinkedError() {
157
+ return new CliError("This project is not linked to a Meridian app.", {
158
+ hint: "Run `meridian link` in your project root to pick an organization and app."
159
+ });
160
+ }
161
+ function sessionExpiredError(cause) {
162
+ return new CliError("Your Meridian CLI token is no longer valid (401).", {
163
+ hint: "Run `meridian login` again to issue a fresh token.",
164
+ ...cause !== void 0 ? { cause } : {}
165
+ });
166
+ }
167
+ function nonInteractiveError(what, override) {
168
+ return new CliError(`Cannot prompt for ${what} in a non-interactive shell.`, {
169
+ hint: `Set ${override} to answer it up front.`
170
+ });
171
+ }
172
+ function describeError(error2) {
173
+ if (error2 instanceof Error) return error2.message;
174
+ if (typeof error2 === "string") return error2;
175
+ return String(error2);
176
+ }
177
+
178
+ // src/lib/api.ts
179
+ var DEFAULT_TIMEOUT_MS = 3e4;
180
+ var DEFAULT_CLIENT_NAME = "meridian-cli";
181
+ var MAX_MIGRATION_COMMAND_LENGTH = 499;
182
+ var ApiClient = class _ApiClient {
183
+ baseUrl;
184
+ #token;
185
+ #fetchImpl;
186
+ #timeoutMs;
187
+ #userAgent;
188
+ constructor(options) {
189
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
190
+ this.#token = options.token;
191
+ this.#fetchImpl = options.fetchImpl ?? fetch;
192
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
193
+ this.#userAgent = options.userAgent;
194
+ }
195
+ get hasToken() {
196
+ return this.#token !== void 0;
197
+ }
198
+ /** Returns a new client carrying the given token; used right after login. */
199
+ withToken(token) {
200
+ return new _ApiClient({
201
+ baseUrl: this.baseUrl,
202
+ token,
203
+ fetchImpl: this.#fetchImpl,
204
+ timeoutMs: this.#timeoutMs,
205
+ ...this.#userAgent !== void 0 ? { userAgent: this.#userAgent } : {}
206
+ });
207
+ }
208
+ /**
209
+ * Performs one request and validates the response against `schema`. Pass
210
+ * `null` for endpoints whose body is irrelevant (e.g. logging a migration
211
+ * run) — the resolved value is then `undefined`.
212
+ */
213
+ async request(schema, method, path, options = {}) {
214
+ const auth = options.auth ?? true;
215
+ if (auth && this.#token === void 0) throw notLoggedInError();
216
+ const url = this.#buildUrl(path, options.query);
217
+ const headers = this.#buildHeaders(options.headers, options.body !== void 0, auth);
218
+ const response = await this.#fetchWithTimeout(url, method, headers, options.body, options.signal);
219
+ if (response.status === 404 && options.allow404 === true) {
220
+ await response.text().catch(() => "");
221
+ return null;
222
+ }
223
+ if (!response.ok) {
224
+ throw await toApiError(response, method, path);
225
+ }
226
+ if (schema === null) {
227
+ await response.text().catch(() => "");
228
+ return void 0;
229
+ }
230
+ const text = await response.text().catch(() => "");
231
+ const data = text.trim().length === 0 ? void 0 : parseJson(text, method, path);
232
+ return parseWithSchema(schema, data, method, path);
233
+ }
234
+ /** `POST /app/cli/device/code` — unauthenticated. */
235
+ async requestDeviceCode(input2 = {}) {
236
+ return this.request(deviceCodeResponseSchema, "POST", "/app/cli/device/code", {
237
+ auth: false,
238
+ body: {
239
+ client_name: input2.clientName ?? DEFAULT_CLIENT_NAME,
240
+ hostname: input2.hostname ?? osHostname()
241
+ }
242
+ });
243
+ }
244
+ /**
245
+ * `POST /app/cli/device/token` — a single poll attempt. On 428/429/410/403
246
+ * this resolves an `ApiError` whose `errorCode` is the RFC 8628-ish string
247
+ * from the body (`authorization_pending`, `slow_down`, ...); callers branch
248
+ * on that, not on `status`.
249
+ */
250
+ async pollDeviceTokenOnce(deviceCode) {
251
+ return this.request(deviceTokenResponseSchema, "POST", "/app/cli/device/token", {
252
+ auth: false,
253
+ body: { device_code: deviceCode }
254
+ });
255
+ }
256
+ /**
257
+ * `GET /app/cli/device/info` — the approval UI's endpoint, exposed here for
258
+ * completeness/tests. It sits behind `auth:api` (and rejects CLI tokens), so
259
+ * the token is attached when one is available rather than required.
260
+ */
261
+ async getDeviceInfo(userCode) {
262
+ return this.request(deviceInfoResponseSchema, "GET", "/app/cli/device/info", {
263
+ auth: this.hasToken,
264
+ query: { user_code: userCode }
265
+ });
266
+ }
267
+ async getMe() {
268
+ return this.request(meResponseSchema, "GET", "/app/cli/me");
269
+ }
270
+ async listApps(orgId) {
271
+ return this.request(z2.array(appSummarySchema), "GET", `/app/cli/orgs/${encodePathSegment(orgId)}/apps`);
272
+ }
273
+ /** Idempotent ensure/provision; may resolve with `status: "provisioning"` — poll `getDevDatabase` until `"ready"`. */
274
+ async ensureDevDatabase(appId) {
275
+ return this.request(devDatabaseSchema, "POST", `/app/cli/apps/${encodePathSegment(appId)}/dev-database`);
276
+ }
277
+ /** `null` when the app has no dev database provisioned yet (404). */
278
+ async getDevDatabase(appId) {
279
+ return this.request(
280
+ devDatabaseSchema,
281
+ "GET",
282
+ `/app/cli/apps/${encodePathSegment(appId)}/dev-database`,
283
+ { allow404: true }
284
+ );
285
+ }
286
+ async issueCredentials(appId) {
287
+ return this.request(
288
+ devCredentialsSchema,
289
+ "POST",
290
+ `/app/cli/apps/${encodePathSegment(appId)}/dev-database/credentials`
291
+ );
292
+ }
293
+ async issueProxyToken(appId) {
294
+ return this.request(
295
+ proxyTokenSchema,
296
+ "POST",
297
+ `/app/cli/apps/${encodePathSegment(appId)}/dev-database/proxy-token`
298
+ );
299
+ }
300
+ async resetDevDatabase(appId) {
301
+ return this.request(devDatabaseSchema, "POST", `/app/cli/apps/${encodePathSegment(appId)}/dev-database/reset`);
302
+ }
303
+ /** `command` is capped server-side at 500 characters, so a long `--` passthrough is trimmed here rather than rejected as a 422. */
304
+ async reportMigration(appId, run2) {
305
+ await this.request(null, "POST", `/app/cli/apps/${encodePathSegment(appId)}/dev-database/migrations`, {
306
+ body: { ...run2, command: truncate(run2.command, MAX_MIGRATION_COMMAND_LENGTH) }
307
+ });
308
+ }
309
+ async listMigrations(appId) {
310
+ return this.request(
311
+ z2.array(migrationRunSchema),
312
+ "GET",
313
+ `/app/cli/apps/${encodePathSegment(appId)}/dev-database/migrations`
314
+ );
315
+ }
316
+ #buildUrl(path, query) {
317
+ const url = new URL(`${this.baseUrl}${path}`);
318
+ if (query !== void 0) {
319
+ for (const [key, value] of Object.entries(query)) {
320
+ if (value !== void 0) url.searchParams.set(key, String(value));
321
+ }
322
+ }
323
+ return url.toString();
324
+ }
325
+ #buildHeaders(extra, hasBody, auth) {
326
+ const headers = { Accept: "application/json" };
327
+ if (hasBody) headers["Content-Type"] = "application/json";
328
+ if (auth && this.#token !== void 0) headers.Authorization = `Bearer ${this.#token}`;
329
+ if (this.#userAgent !== void 0) headers["User-Agent"] = this.#userAgent;
330
+ if (extra !== void 0) Object.assign(headers, extra);
331
+ return headers;
332
+ }
333
+ async #fetchWithTimeout(url, method, headers, body, externalSignal) {
334
+ const controller = new AbortController();
335
+ let timedOut = false;
336
+ const onExternalAbort = () => controller.abort();
337
+ if (externalSignal !== void 0) {
338
+ if (externalSignal.aborted) controller.abort();
339
+ else externalSignal.addEventListener("abort", onExternalAbort, { once: true });
340
+ }
341
+ const timeoutId = setTimeout(() => {
342
+ timedOut = true;
343
+ controller.abort();
344
+ }, this.#timeoutMs);
345
+ try {
346
+ return await this.#fetchImpl(url, {
347
+ method,
348
+ headers,
349
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
350
+ signal: controller.signal
351
+ });
352
+ } catch (error2) {
353
+ if (timedOut) {
354
+ throw new CliError(`Request to the Meridian API timed out after ${this.#timeoutMs}ms.`, {
355
+ hint: "The API may be slow or unreachable right now; try again in a moment.",
356
+ cause: error2
357
+ });
358
+ }
359
+ if (externalSignal?.aborted === true) throw error2;
360
+ throw new CliError(`Cannot reach the Meridian API at ${this.baseUrl}.`, {
361
+ hint: "Check your network connection, or set MERIDIAN_API_URL if you're targeting a different environment.",
362
+ cause: error2
363
+ });
364
+ } finally {
365
+ clearTimeout(timeoutId);
366
+ externalSignal?.removeEventListener("abort", onExternalAbort);
367
+ }
368
+ }
369
+ };
370
+ function encodePathSegment(id) {
371
+ return encodeURIComponent(String(id));
372
+ }
373
+ function parseJson(text, method, path) {
374
+ try {
375
+ return JSON.parse(text);
376
+ } catch (error2) {
377
+ throw new CliError(`The Meridian API returned a non-JSON response for ${method} ${path}.`, {
378
+ hint: "This usually means the CLI is out of date; try upgrading @the-meridian/cli.",
379
+ cause: error2
380
+ });
381
+ }
382
+ }
383
+ function parseWithSchema(schema, data, method, path) {
384
+ const result = schema.safeParse(data);
385
+ if (result.success) return result.data;
386
+ const issues = result.error.issues.slice(0, 3).map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`).join("; ");
387
+ throw new CliError(
388
+ `The Meridian API returned an unexpected payload for ${method} ${path}: ${truncate(issues, 300)}`,
389
+ {
390
+ hint: "This usually means the CLI is out of date; try upgrading @the-meridian/cli.",
391
+ cause: result.error
392
+ }
393
+ );
394
+ }
395
+ async function toApiError(response, method, path) {
396
+ const text = await response.text().catch(() => "");
397
+ let body;
398
+ if (text.trim().length === 0) body = void 0;
399
+ else {
400
+ try {
401
+ body = JSON.parse(text);
402
+ } catch {
403
+ body = text;
404
+ }
405
+ }
406
+ const errorCode = extractErrorCode(body);
407
+ const message = extractMessage(body) ?? `${method} ${path} failed with ${response.status} ${response.statusText}`.trim();
408
+ const hint2 = hintForError(response.status, errorCode);
409
+ return new ApiError(message, {
410
+ status: response.status,
411
+ body,
412
+ ...errorCode !== void 0 ? { errorCode } : {},
413
+ ...hint2 !== void 0 ? { hint: hint2 } : {}
414
+ });
415
+ }
416
+ function asRecord(value) {
417
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
418
+ }
419
+ function envelopeError(body) {
420
+ const record = asRecord(body);
421
+ return record === void 0 ? void 0 : asRecord(record.error);
422
+ }
423
+ function firstFieldError(bag) {
424
+ const fields = asRecord(bag);
425
+ if (fields === void 0) return void 0;
426
+ for (const value of Object.values(fields)) {
427
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
428
+ if (typeof value === "string") return value;
429
+ }
430
+ return void 0;
431
+ }
432
+ function extractMessage(body) {
433
+ const record = asRecord(body);
434
+ if (record === void 0) return void 0;
435
+ const envelope = envelopeError(body);
436
+ if (envelope !== void 0) {
437
+ const fieldError2 = firstFieldError(asRecord(envelope.details)?.fields);
438
+ if (fieldError2 !== void 0) return fieldError2;
439
+ if (typeof envelope.message === "string" && envelope.message.length > 0) return envelope.message;
440
+ if (typeof envelope.code === "string" && envelope.code.length > 0) return envelope.code;
441
+ }
442
+ const fieldError = firstFieldError(record.errors);
443
+ if (fieldError !== void 0) return fieldError;
444
+ if (typeof record.message === "string" && record.message.length > 0) return record.message;
445
+ if (typeof record.error === "string" && record.error.length > 0) return record.error;
446
+ return void 0;
447
+ }
448
+ function extractErrorCode(body) {
449
+ const envelope = envelopeError(body);
450
+ if (envelope !== void 0 && typeof envelope.code === "string") return envelope.code;
451
+ const record = asRecord(body);
452
+ return record !== void 0 && typeof record.error === "string" ? record.error : void 0;
453
+ }
454
+ var HINTS_BY_ERROR_CODE = {
455
+ dev_database_limit_reached: "This app has reached its allowance of development databases. Whoever owns the existing one can delete it, or an organization owner/admin can remove it from the Meridian dashboard.",
456
+ dev_database_over_storage_limit: "Run `meridian db reset` to drop and recreate the schema \u2014 reset and delete stay available while you're over the limit.",
457
+ FEATURE_NOT_ENTITLED: "Your organization's plan does not include managed development databases; upgrade it in the Meridian dashboard."
458
+ };
459
+ function hintForError(status, errorCode) {
460
+ if (errorCode !== void 0) {
461
+ const byCode = HINTS_BY_ERROR_CODE[errorCode];
462
+ if (byCode !== void 0) return byCode;
463
+ }
464
+ if (status === 401) return sessionExpiredError().hint;
465
+ if (status === 403) return "You may not have access to this organization or app; check your Meridian membership.";
466
+ if (status === 404) return "Run `meridian link` to select a valid organization and app.";
467
+ if (status === 409) {
468
+ return "The development database is not ready yet. Wait for provisioning to finish, then try again \u2014 `meridian dev` polls for you.";
469
+ }
470
+ if (status === 422) return "Check the request for validation errors and try again.";
471
+ if (status === 429) return "You're being rate limited by the Meridian API; wait a moment and try again.";
472
+ if (status >= 500) return "This looks like a server-side failure on the Meridian API; try again shortly.";
473
+ return void 0;
474
+ }
475
+ function truncate(text, max) {
476
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
477
+ }
478
+
479
+ // src/lib/config.ts
480
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
481
+ import { dirname } from "path";
482
+ import { z as z3 } from "zod";
483
+
484
+ // src/lib/paths.ts
485
+ import { homedir } from "os";
486
+ import { join } from "path";
487
+ function meridianHome() {
488
+ const override = process.env.MERIDIAN_HOME;
489
+ if (override !== void 0 && override !== "") return override;
490
+ return join(homedir(), ".meridian");
491
+ }
492
+ function globalConfigPath() {
493
+ return join(meridianHome(), "config.json");
494
+ }
495
+ function proxyBinDir() {
496
+ return join(meridianHome(), "bin");
497
+ }
498
+ function projectStateDir(root) {
499
+ return join(root, ".meridian");
500
+ }
501
+ function projectConfigPath(root) {
502
+ return join(projectStateDir(root), "project.json");
503
+ }
504
+ function snapshotsDir(root) {
505
+ return join(projectStateDir(root), "snapshots");
506
+ }
507
+ function envFilePath(root, fileName = ".env") {
508
+ return join(root, fileName);
509
+ }
510
+
511
+ // src/lib/config.ts
512
+ var DEFAULT_API_BASE_URL = "https://api.the-meridian.ai";
513
+ var configUserSchema = z3.object({
514
+ id: z3.union([z3.number(), z3.string()]),
515
+ name: z3.string(),
516
+ email: z3.string()
517
+ });
518
+ var globalConfigSchema = z3.object({
519
+ api_base_url: z3.string().exactOptional(),
520
+ token: z3.string().exactOptional(),
521
+ token_expires_at: z3.string().exactOptional(),
522
+ user: configUserSchema.exactOptional()
523
+ }).loose();
524
+ function readConfig() {
525
+ const path = globalConfigPath();
526
+ let raw;
527
+ try {
528
+ raw = readFileSync(path, "utf8");
529
+ } catch (error2) {
530
+ if (isEnoent(error2)) return {};
531
+ throw new CliError(`Could not read ${path}.`, {
532
+ hint: "Check the file's permissions, then run `meridian login` again.",
533
+ cause: error2
534
+ });
535
+ }
536
+ let parsed;
537
+ try {
538
+ parsed = JSON.parse(raw);
539
+ } catch (error2) {
540
+ throw new CliError(`${path} is not valid JSON.`, {
541
+ hint: "Fix or delete the file, then run `meridian login` again.",
542
+ cause: error2
543
+ });
544
+ }
545
+ const result = globalConfigSchema.safeParse(parsed);
546
+ if (!result.success) {
547
+ throw new CliError(`${path} does not look like a Meridian config file.`, {
548
+ hint: "Fix or delete the file, then run `meridian login` again.",
549
+ cause: result.error
550
+ });
551
+ }
552
+ return result.data;
553
+ }
554
+ function writeConfig(config) {
555
+ const path = globalConfigPath();
556
+ const dir = dirname(path);
557
+ mkdirSync(dir, { recursive: true, mode: 448 });
558
+ const tmpPath = `${path}.tmp`;
559
+ writeFileSync(tmpPath, `${JSON.stringify(config, null, 2)}
560
+ `, { mode: 384 });
561
+ renameSync(tmpPath, path);
562
+ chmodSync(path, 384);
563
+ }
564
+ function updateConfig(patch) {
565
+ const merged = { ...readConfig(), ...patch };
566
+ writeConfig(merged);
567
+ return merged;
568
+ }
569
+ function clearAuth() {
570
+ const current2 = readConfig();
571
+ const hadToken = current2.token !== void 0;
572
+ const next = {
573
+ ...current2.api_base_url !== void 0 ? { api_base_url: current2.api_base_url } : {}
574
+ };
575
+ writeConfig(next);
576
+ return hadToken;
577
+ }
578
+ function normalizeApiBaseUrl(url) {
579
+ return url.trim().replace(/\/+$/, "").replace(/\/app$/, "");
580
+ }
581
+ function resolveApiBaseUrl(override) {
582
+ if (override !== void 0 && override !== "") return normalizeApiBaseUrl(override);
583
+ const envValue = process.env.MERIDIAN_API_URL;
584
+ if (envValue !== void 0 && envValue !== "") return normalizeApiBaseUrl(envValue);
585
+ const config = readConfig();
586
+ if (config.api_base_url !== void 0 && config.api_base_url !== "") {
587
+ return normalizeApiBaseUrl(config.api_base_url);
588
+ }
589
+ return normalizeApiBaseUrl(DEFAULT_API_BASE_URL);
590
+ }
591
+ function getAuth() {
592
+ const config = readConfig();
593
+ if (config.token === void 0) return null;
594
+ return {
595
+ token: config.token,
596
+ ...config.token_expires_at !== void 0 ? { tokenExpiresAt: config.token_expires_at } : {},
597
+ ...config.user !== void 0 ? { user: config.user } : {}
598
+ };
599
+ }
600
+ function isEnoent(error2) {
601
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
602
+ }
603
+
604
+ // src/lib/context.ts
605
+ var defaults = {
606
+ json: false,
607
+ quiet: false,
608
+ verbose: false,
609
+ yes: false
610
+ };
611
+ var current = { ...defaults };
612
+ function setGlobalOptions(options) {
613
+ current = { ...current, ...options };
614
+ }
615
+ function getGlobalOptions() {
616
+ return current;
617
+ }
618
+ function isJsonOutput() {
619
+ return current.json;
620
+ }
621
+ function workingDirectory() {
622
+ return current.cwd ?? process.cwd();
623
+ }
624
+
625
+ // src/lib/client.ts
626
+ function createApiClient(options = {}) {
627
+ const requireAuth = options.requireAuth ?? true;
628
+ const baseUrl = resolveApiBaseUrl(options.apiUrl ?? getGlobalOptions().apiUrl);
629
+ const token = options.token ?? getAuth()?.token;
630
+ if (requireAuth && token === void 0) throw notLoggedInError();
631
+ return new ApiClient({
632
+ baseUrl,
633
+ userAgent: USER_AGENT,
634
+ ...token !== void 0 ? { token } : {},
635
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
636
+ });
637
+ }
638
+
639
+ // src/lib/db/connection.ts
640
+ function connectionInfoFromCredentials(credentials, proxy) {
641
+ return {
642
+ host: proxy.host ?? "127.0.0.1",
643
+ port: proxy.port,
644
+ user: credentials.username,
645
+ password: credentials.password,
646
+ database: credentials.database
647
+ };
648
+ }
649
+ var createSqlConnection = async (info, options) => {
650
+ const { createConnection: createConnection2 } = await import("mysql2/promise");
651
+ const connection = await createConnection2({
652
+ host: info.host,
653
+ port: info.port,
654
+ user: info.user,
655
+ password: info.password,
656
+ database: info.database,
657
+ ...options?.multipleStatements !== void 0 ? { multipleStatements: options.multipleStatements } : {}
658
+ });
659
+ return connection;
660
+ };
661
+ async function closeQuietly(connection) {
662
+ try {
663
+ await connection.end();
664
+ } catch {
665
+ connection.destroy?.();
666
+ }
667
+ }
668
+ async function withConnection(info, fn, options = {}) {
669
+ const factory = options.factory ?? createSqlConnection;
670
+ const factoryOptions = options.multipleStatements !== void 0 ? { multipleStatements: options.multipleStatements } : {};
671
+ const connection = await factory(info, factoryOptions);
672
+ try {
673
+ return await fn(connection);
674
+ } finally {
675
+ await closeQuietly(connection);
676
+ }
677
+ }
678
+ async function waitForDatabase(info, options = {}) {
679
+ const timeoutMs = options.timeoutMs ?? 3e4;
680
+ const intervalMs = options.intervalMs ?? 500;
681
+ const factory = options.factory ?? createSqlConnection;
682
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
683
+ const now = options.now ?? Date.now;
684
+ const deadline = now() + timeoutMs;
685
+ let lastError;
686
+ for (; ; ) {
687
+ try {
688
+ const connection = await factory(info);
689
+ try {
690
+ await connection.query("SELECT 1");
691
+ } finally {
692
+ await closeQuietly(connection);
693
+ }
694
+ return;
695
+ } catch (error2) {
696
+ lastError = error2;
697
+ }
698
+ if (now() >= deadline) throw toDatabaseError(lastError, info);
699
+ await sleep(intervalMs);
700
+ }
701
+ }
702
+ function extractErrorCode2(error2) {
703
+ if (typeof error2 === "object" && error2 !== null && "code" in error2) {
704
+ const code = error2.code;
705
+ if (typeof code === "string") return code;
706
+ }
707
+ return void 0;
708
+ }
709
+ function redactPassword(text, password) {
710
+ if (password === "") return text;
711
+ return text.split(password).join("***");
712
+ }
713
+ function toDatabaseError(error2, info) {
714
+ const target = `${info.host}:${info.port}/${info.database}`;
715
+ const code = extractErrorCode2(error2);
716
+ const build = (message, hint2) => new CliError(redactPassword(message, info.password), {
717
+ ...hint2 !== void 0 ? { hint: redactPassword(hint2, info.password) } : {},
718
+ cause: error2
719
+ });
720
+ if (code === "ER_ACCESS_DENIED_ERROR") {
721
+ return build(
722
+ `Access denied connecting to ${target} as "${info.user}".`,
723
+ "Database credentials are short-lived and rotate automatically \u2014 run `meridian dev` again to fetch fresh ones."
724
+ );
725
+ }
726
+ if (code === "ER_BAD_DB_ERROR") {
727
+ return build(
728
+ `Database "${info.database}" does not exist on ${target}.`,
729
+ "Run `meridian db reset` to recreate it."
730
+ );
731
+ }
732
+ if (code === "ECONNREFUSED") {
733
+ return build(
734
+ `Could not reach ${target} (${code}).`,
735
+ "Nothing is listening on that port. `meridian dev` and `meridian migrate` start the Cloud SQL Auth Proxy for you; start it first if you are connecting by hand."
736
+ );
737
+ }
738
+ if (code === "ETIMEDOUT" || code === "PROTOCOL_SEQUENCE_TIMEOUT") {
739
+ return build(
740
+ `Timed out waiting for MySQL at ${target} (${code}).`,
741
+ "The proxy is listening but the database never answered. Check that your network allows outbound connections to Cloud SQL (VPN, firewall, proxy), then retry with --verbose to see the proxy's own log output."
742
+ );
743
+ }
744
+ const detail2 = code ?? describeError(error2);
745
+ return build(`Database error connecting to ${target} as "${info.user}": ${detail2}`);
746
+ }
747
+
748
+ // src/lib/output.ts
749
+ import pc from "picocolors";
750
+ var state;
751
+ function defaultStreams() {
752
+ return { out: process.stdout, err: process.stderr };
753
+ }
754
+ function computeDefaultColor(streams) {
755
+ return Boolean(
756
+ !process.env.NO_COLOR && (process.env.FORCE_COLOR ? true : Boolean(streams.err.isTTY))
757
+ );
758
+ }
759
+ function getState() {
760
+ if (state === void 0) {
761
+ const streams = defaultStreams();
762
+ state = {
763
+ streams,
764
+ mode: { json: false, quiet: false, verbose: false, color: computeDefaultColor(streams) }
765
+ };
766
+ }
767
+ return state;
768
+ }
769
+ function configureOutput(options) {
770
+ const current2 = getState();
771
+ const streamsChanged = options.streams !== void 0;
772
+ const streams = options.streams ?? current2.streams;
773
+ const color = options.color ?? (streamsChanged ? computeDefaultColor(streams) : current2.mode.color);
774
+ state = {
775
+ streams,
776
+ mode: {
777
+ json: options.json ?? current2.mode.json,
778
+ quiet: options.quiet ?? current2.mode.quiet,
779
+ verbose: options.verbose ?? current2.mode.verbose,
780
+ color
781
+ }
782
+ };
783
+ }
784
+ function getOutputMode() {
785
+ return { ...getState().mode };
786
+ }
787
+ function colors() {
788
+ return pc.createColors(getOutputMode().color);
789
+ }
790
+ var style = {
791
+ bold(s) {
792
+ return colors().bold(s);
793
+ },
794
+ dim(s) {
795
+ return colors().dim(s);
796
+ },
797
+ cyan(s) {
798
+ return colors().cyan(s);
799
+ },
800
+ green(s) {
801
+ return colors().green(s);
802
+ },
803
+ yellow(s) {
804
+ return colors().yellow(s);
805
+ },
806
+ red(s) {
807
+ return colors().red(s);
808
+ },
809
+ /** Highlights a command the user should run, e.g. `meridian login`. */
810
+ command(s) {
811
+ return colors().cyan(colors().bold(s));
812
+ },
813
+ /** Highlights a path or URL. */
814
+ path(s) {
815
+ return colors().underline(s);
816
+ }
817
+ };
818
+ function highlightBackticks(message) {
819
+ return message.replace(/`([^`]+)`/g, (_match, code) => style.command(code));
820
+ }
821
+ function indentLines(message, prefix) {
822
+ return message.split("\n").map((line) => `${prefix}${line}`).join("\n");
823
+ }
824
+ function suppressed() {
825
+ const mode = getOutputMode();
826
+ return mode.json || mode.quiet;
827
+ }
828
+ function writeErr(text) {
829
+ getState().streams.err.write(text);
830
+ }
831
+ function writeErrLine(text) {
832
+ writeErr(`${text}
833
+ `);
834
+ }
835
+ function log(message = "") {
836
+ if (suppressed()) return;
837
+ writeErrLine(message);
838
+ }
839
+ function step(message) {
840
+ if (suppressed()) return;
841
+ writeErrLine(`${colors().cyan("\u2192")} ${message}`);
842
+ }
843
+ function success(message) {
844
+ if (suppressed()) return;
845
+ writeErrLine(`${colors().green("\u2714")} ${message}`);
846
+ }
847
+ function warn(message) {
848
+ writeErrLine(`${colors().yellow("\u26A0")} ${message}`);
849
+ }
850
+ function error(message) {
851
+ writeErrLine(`${colors().red("\u2716")} ${message}`);
852
+ }
853
+ function debug(message) {
854
+ const mode = getOutputMode();
855
+ if (suppressed() || !mode.verbose) return;
856
+ writeErrLine(colors().dim(`\xB7 ${message}`));
857
+ }
858
+ function detail(message) {
859
+ if (suppressed()) return;
860
+ writeErrLine(indentLines(message, " "));
861
+ }
862
+ function detailAlways(message) {
863
+ writeErrLine(indentLines(message, " "));
864
+ }
865
+ function keyValues(entries) {
866
+ if (suppressed()) return;
867
+ if (entries.length === 0) return;
868
+ const maxKeyLength = Math.max(...entries.map(([key]) => key.length));
869
+ for (const [key, value] of entries) {
870
+ writeErrLine(` ${colors().dim(key.padEnd(maxKeyLength))} ${value}`);
871
+ }
872
+ }
873
+ function hint(message) {
874
+ writeErrLine(indentLines(highlightBackticks(message), ` ${colors().dim("\u203A")} `));
875
+ }
876
+ function printJson(value) {
877
+ getState().streams.out.write(`${JSON.stringify(value, null, 2)}
878
+ `);
879
+ }
880
+ function printLine(message) {
881
+ getState().streams.out.write(`${message}
882
+ `);
883
+ }
884
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
885
+ var SPINNER_INTERVAL_MS = 80;
886
+ var HIDE_CURSOR = "\x1B[?25l";
887
+ var SHOW_CURSOR = "\x1B[?25h";
888
+ var CLEAR_LINE = "\r\x1B[K";
889
+ var SpinnerImpl = class {
890
+ #text;
891
+ #timer;
892
+ #frame = 0;
893
+ #running = false;
894
+ constructor(text) {
895
+ this.#text = text;
896
+ }
897
+ get text() {
898
+ return this.#text;
899
+ }
900
+ start(text) {
901
+ if (text !== void 0) this.#text = text;
902
+ if (getOutputMode().json || this.#running) return this;
903
+ this.#running = true;
904
+ if (!this.#animated()) {
905
+ this.#writeLine(`${colors().cyan("\u2192")} ${this.#text}`);
906
+ return this;
907
+ }
908
+ this.#writeRaw(HIDE_CURSOR);
909
+ this.#render();
910
+ this.#timer = setInterval(() => {
911
+ this.#frame = (this.#frame + 1) % SPINNER_FRAMES.length;
912
+ this.#render();
913
+ }, SPINNER_INTERVAL_MS);
914
+ this.#timer.unref();
915
+ return this;
916
+ }
917
+ update(text) {
918
+ this.#text = text;
919
+ if (getOutputMode().json || !this.#running || !this.#animated()) return this;
920
+ this.#render();
921
+ return this;
922
+ }
923
+ succeed(text) {
924
+ this.#finish(colors().green("\u2714"), text);
925
+ }
926
+ fail(text) {
927
+ this.#finish(colors().red("\u2716"), text);
928
+ }
929
+ stop() {
930
+ if (!this.#running) return;
931
+ this.#clearTimer();
932
+ if (this.#animated()) {
933
+ this.#writeRaw(CLEAR_LINE);
934
+ this.#writeRaw(SHOW_CURSOR);
935
+ }
936
+ this.#running = false;
937
+ }
938
+ #finish(mark, text) {
939
+ if (text !== void 0) this.#text = text;
940
+ if (getOutputMode().json) {
941
+ this.#running = false;
942
+ return;
943
+ }
944
+ const wasAnimated = this.#running && this.#animated();
945
+ this.#clearTimer();
946
+ if (wasAnimated) {
947
+ this.#writeRaw(CLEAR_LINE);
948
+ this.#writeRaw(SHOW_CURSOR);
949
+ }
950
+ this.#writeLine(`${mark} ${this.#text}`);
951
+ this.#running = false;
952
+ }
953
+ #clearTimer() {
954
+ if (this.#timer !== void 0) {
955
+ clearInterval(this.#timer);
956
+ this.#timer = void 0;
957
+ }
958
+ }
959
+ #render() {
960
+ const frame = SPINNER_FRAMES[this.#frame] ?? SPINNER_FRAMES[0];
961
+ const line = this.#truncate(`${colors().cyan(frame ?? "")} ${this.#text}`);
962
+ this.#writeRaw(`${CLEAR_LINE}${line}`);
963
+ }
964
+ #truncate(line) {
965
+ const columns = this.#columns();
966
+ if (columns === void 0 || line.length <= columns) return line;
967
+ return line.slice(0, Math.max(0, columns - 1));
968
+ }
969
+ #columns() {
970
+ const stream = getState().streams.err;
971
+ return typeof stream.columns === "number" ? stream.columns : void 0;
972
+ }
973
+ /** Full braille animation only makes sense on a TTY, outside quiet mode. */
974
+ #animated() {
975
+ const stream = getState().streams.err;
976
+ return stream.isTTY === true && !getOutputMode().quiet;
977
+ }
978
+ #writeLine(text) {
979
+ this.#writeRaw(`${text}
980
+ `);
981
+ }
982
+ #writeRaw(text) {
983
+ getState().streams.err.write(text);
984
+ }
985
+ };
986
+ function spinner(text) {
987
+ return new SpinnerImpl(text);
988
+ }
989
+ async function withSpinner(text, fn, options = {}) {
990
+ const active = spinner(text).start();
991
+ try {
992
+ const value = await fn(active);
993
+ const successText = typeof options.success === "function" ? options.success(value) : options.success;
994
+ active.succeed(successText);
995
+ return value;
996
+ } catch (err) {
997
+ active.fail(options.fail);
998
+ throw err;
999
+ }
1000
+ }
1001
+ function buildJsonError(err) {
1002
+ const payload = { message: err.message };
1003
+ if (err.hint !== void 0) payload.hint = err.hint;
1004
+ if (isApiError(err)) payload.status = err.status;
1005
+ return payload;
1006
+ }
1007
+ function truncate2(text, max) {
1008
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
1009
+ }
1010
+ function safeStringify(value) {
1011
+ try {
1012
+ return JSON.stringify(value);
1013
+ } catch {
1014
+ return String(value);
1015
+ }
1016
+ }
1017
+ function reportError(err) {
1018
+ const mode = getOutputMode();
1019
+ if (isApiError(err)) {
1020
+ error(err.message);
1021
+ if (err.hint !== void 0) hint(err.hint);
1022
+ detailAlways(`status: ${err.status}`);
1023
+ if (mode.verbose && err.body !== void 0) detailAlways(truncate2(safeStringify(err.body), 500));
1024
+ if (mode.json) printJson({ error: buildJsonError(err) });
1025
+ return err.exitCode;
1026
+ }
1027
+ if (isCliError(err)) {
1028
+ error(err.message);
1029
+ if (err.hint !== void 0) hint(err.hint);
1030
+ if (mode.verbose && err.stack !== void 0) detailAlways(err.stack);
1031
+ if (mode.json) printJson({ error: buildJsonError(err) });
1032
+ return err.exitCode;
1033
+ }
1034
+ const message = describeError(err);
1035
+ error(message);
1036
+ if (err instanceof Error && err.stack !== void 0) detailAlways(err.stack);
1037
+ if (mode.json) printJson({ error: { message } });
1038
+ return 1;
1039
+ }
1040
+
1041
+ // src/lib/options.ts
1042
+ function parseIntegerOption(raw, flag, range = {}) {
1043
+ if (raw === void 0) return void 0;
1044
+ const value = Number(raw);
1045
+ const { min, max } = range;
1046
+ const isValid = Number.isInteger(value) && (min === void 0 || value >= min) && (max === void 0 || value <= max);
1047
+ if (!isValid) {
1048
+ const bounds = min !== void 0 && max !== void 0 ? ` between ${min} and ${max}` : min !== void 0 ? ` of at least ${min}` : "";
1049
+ throw new CliError(`${flag} expects a whole number${bounds}, but got "${raw}".`, {
1050
+ hint: `Pass a valid value, e.g. \`${flag} ${min ?? 1}\`.`
1051
+ });
1052
+ }
1053
+ return value;
1054
+ }
1055
+ function parsePortOption(raw, flag = "--port") {
1056
+ return parseIntegerOption(raw, flag, { min: 1, max: 65535 });
1057
+ }
1058
+
1059
+ // src/lib/prompts.ts
1060
+ import { confirm, input, select } from "@inquirer/prompts";
1061
+ function isInteractive() {
1062
+ if (interactiveOverride !== void 0) return interactiveOverride;
1063
+ return process.stdin.isTTY === true && process.stderr.isTTY === true && !process.env.CI && !process.env.MERIDIAN_NON_INTERACTIVE;
1064
+ }
1065
+ function isAssumeYes() {
1066
+ if (assumeYesOverride !== void 0) return assumeYesOverride;
1067
+ return Boolean(process.env.MERIDIAN_YES);
1068
+ }
1069
+ var interactiveOverride;
1070
+ var assumeYesOverride;
1071
+ function configurePrompts(options) {
1072
+ if (options.assumeYes !== void 0) assumeYesOverride = options.assumeYes;
1073
+ if (options.interactive !== void 0) interactiveOverride = options.interactive;
1074
+ }
1075
+ function matchChoice(choices, raw) {
1076
+ const target = raw.trim().toLowerCase();
1077
+ return choices.find((choice) => {
1078
+ const value = String(choice.value).trim().toLowerCase();
1079
+ const name = choice.name.trim().toLowerCase();
1080
+ return value === target || name === target;
1081
+ });
1082
+ }
1083
+ function listChoiceNames(choices) {
1084
+ return choices.map((choice) => choice.name).join(", ");
1085
+ }
1086
+ function describeOverride(flag, envVar) {
1087
+ const parts = [flag, envVar].filter((part) => part !== void 0);
1088
+ return parts.length > 0 ? parts.join(" or ") : "the appropriate flag";
1089
+ }
1090
+ async function selectOne(options) {
1091
+ const { message, choices, envVar, answer, flag } = options;
1092
+ if (choices.length === 0) {
1093
+ throw new CliError("Nothing to choose from.");
1094
+ }
1095
+ if (answer !== void 0) {
1096
+ const match = matchChoice(choices, answer);
1097
+ if (match === void 0) {
1098
+ throw new CliError(`"${answer}" is not a valid choice for ${message}.`, {
1099
+ hint: `Valid options: ${listChoiceNames(choices)}.`
1100
+ });
1101
+ }
1102
+ return match.value;
1103
+ }
1104
+ if (envVar !== void 0) {
1105
+ const envValue = process.env[envVar];
1106
+ if (envValue !== void 0 && envValue !== "") {
1107
+ const match = matchChoice(choices, envValue);
1108
+ if (match === void 0) {
1109
+ throw new CliError(`${envVar}="${envValue}" is not a valid choice for ${message}.`, {
1110
+ hint: `Valid options: ${listChoiceNames(choices)}.`
1111
+ });
1112
+ }
1113
+ return match.value;
1114
+ }
1115
+ }
1116
+ if (choices.length === 1) {
1117
+ const only = choices[0];
1118
+ if (only !== void 0) return only.value;
1119
+ }
1120
+ if (!isInteractive()) {
1121
+ throw nonInteractiveError(message, describeOverride(flag, envVar));
1122
+ }
1123
+ const promptImpl = options.promptImpl ?? select;
1124
+ try {
1125
+ return await promptImpl(
1126
+ {
1127
+ message,
1128
+ choices: choices.map((choice) => ({
1129
+ value: choice.value,
1130
+ name: choice.name,
1131
+ ...choice.description !== void 0 ? { description: choice.description } : {}
1132
+ })),
1133
+ ...options.default !== void 0 ? { default: options.default } : {}
1134
+ },
1135
+ { output: process.stderr }
1136
+ );
1137
+ } catch (err) {
1138
+ throw toPromptError(err);
1139
+ }
1140
+ }
1141
+ function confirmOverrideText(flag) {
1142
+ return `${flag ?? "--yes"} (or MERIDIAN_YES)`;
1143
+ }
1144
+ async function confirmAction(options) {
1145
+ const assumeYes = options.assumeYes ?? isAssumeYes();
1146
+ if (assumeYes) return true;
1147
+ if (!isInteractive()) {
1148
+ throw nonInteractiveError(options.message, confirmOverrideText(options.flag));
1149
+ }
1150
+ const promptImpl = options.promptImpl ?? confirm;
1151
+ try {
1152
+ return await promptImpl(
1153
+ {
1154
+ message: options.message,
1155
+ ...options.default !== void 0 ? { default: options.default } : {}
1156
+ },
1157
+ { output: process.stderr }
1158
+ );
1159
+ } catch (err) {
1160
+ throw toPromptError(err);
1161
+ }
1162
+ }
1163
+ async function confirmDestructive(options) {
1164
+ const assumeYes = options.assumeYes ?? isAssumeYes();
1165
+ if (assumeYes) return true;
1166
+ if (!isInteractive()) {
1167
+ throw nonInteractiveError(options.message, confirmOverrideText(options.flag));
1168
+ }
1169
+ const promptImpl = options.promptImpl ?? input;
1170
+ let typed;
1171
+ try {
1172
+ typed = await promptImpl(
1173
+ { message: `${options.message} Type "${options.phrase}" to confirm:` },
1174
+ { output: process.stderr }
1175
+ );
1176
+ } catch (err) {
1177
+ throw toPromptError(err);
1178
+ }
1179
+ return typed.trim() === options.phrase;
1180
+ }
1181
+ function toPromptError(error2) {
1182
+ if (error2 instanceof Error && error2.name === "ExitPromptError") {
1183
+ return new CliError("Aborted.", { exitCode: 130, cause: error2 });
1184
+ }
1185
+ return error2;
1186
+ }
1187
+
1188
+ // src/lib/project.ts
1189
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
1190
+ import { dirname as dirname2, parse, resolve } from "path";
1191
+ import { z as z4 } from "zod";
1192
+ var projectConfigSchema = z4.object({
1193
+ org_id: z4.union([z4.number(), z4.string()]),
1194
+ app_id: z4.union([z4.number(), z4.string()]),
1195
+ app_name: z4.string().exactOptional(),
1196
+ dev_command: z4.string().exactOptional(),
1197
+ skip_datasource_check: z4.boolean().exactOptional()
1198
+ });
1199
+ var GITIGNORE_ENTRIES = ["snapshots/", "*.log", "tmp/"];
1200
+ function findProjectRoot(startDir) {
1201
+ let dir = resolve(startDir ?? process.cwd());
1202
+ const { root } = parse(dir);
1203
+ while (true) {
1204
+ if (existsSync(projectConfigPath(dir))) return dir;
1205
+ if (dir === root) return null;
1206
+ dir = dirname2(dir);
1207
+ }
1208
+ }
1209
+ function readProject(startDir) {
1210
+ const root = findProjectRoot(startDir);
1211
+ if (root === null) return null;
1212
+ const path = projectConfigPath(root);
1213
+ let raw;
1214
+ try {
1215
+ raw = readFileSync2(path, "utf8");
1216
+ } catch (error2) {
1217
+ if (isEnoent2(error2)) return null;
1218
+ throw new CliError(`Could not read ${path}.`, {
1219
+ hint: "Run `meridian link` to relink this project.",
1220
+ cause: error2
1221
+ });
1222
+ }
1223
+ let parsed;
1224
+ try {
1225
+ parsed = JSON.parse(raw);
1226
+ } catch (error2) {
1227
+ throw new CliError(`${path} is not valid JSON.`, {
1228
+ hint: "Run `meridian link` to relink this project.",
1229
+ cause: error2
1230
+ });
1231
+ }
1232
+ const result = projectConfigSchema.safeParse(parsed);
1233
+ if (!result.success) {
1234
+ throw new CliError(`${path} is not a valid Meridian project file.`, {
1235
+ hint: "Run `meridian link` to relink this project.",
1236
+ cause: result.error
1237
+ });
1238
+ }
1239
+ return { root, config: result.data };
1240
+ }
1241
+ function requireProject(startDir) {
1242
+ const project = readProject(startDir);
1243
+ if (project === null) throw notLinkedError();
1244
+ return project;
1245
+ }
1246
+ function writeProject(root, config) {
1247
+ const resolvedRoot = resolve(root);
1248
+ mkdirSync2(projectStateDir(resolvedRoot), { recursive: true });
1249
+ writeFileSync2(projectConfigPath(resolvedRoot), `${JSON.stringify(config, null, 2)}
1250
+ `);
1251
+ ensureProjectStateIgnore(resolvedRoot);
1252
+ }
1253
+ function removeProject(root) {
1254
+ const path = projectConfigPath(resolve(root));
1255
+ if (!existsSync(path)) return false;
1256
+ rmSync(path);
1257
+ return true;
1258
+ }
1259
+ function ensureProjectStateIgnore(root) {
1260
+ const resolvedRoot = resolve(root);
1261
+ const stateDir = projectStateDir(resolvedRoot);
1262
+ mkdirSync2(stateDir, { recursive: true });
1263
+ const gitignorePath = `${stateDir}/.gitignore`;
1264
+ let existingLines = [];
1265
+ if (existsSync(gitignorePath)) {
1266
+ existingLines = readFileSync2(gitignorePath, "utf8").split("\n");
1267
+ }
1268
+ const missing = GITIGNORE_ENTRIES.filter((entry) => !existingLines.includes(entry));
1269
+ if (existingLines.length > 0 && missing.length === 0) return;
1270
+ const lines = [...existingLines.filter((line) => line.trim() !== ""), ...missing];
1271
+ writeFileSync2(gitignorePath, `${lines.join("\n")}
1272
+ `);
1273
+ }
1274
+ function resolveProjectRoot(cwd) {
1275
+ if (cwd !== void 0) return findProjectRoot(cwd);
1276
+ return findProjectRoot();
1277
+ }
1278
+ function isEnoent2(error2) {
1279
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
1280
+ }
1281
+
1282
+ // src/lib/run.ts
1283
+ import { readFileSync as readFileSync3 } from "fs";
1284
+ import { delimiter, join as join2 } from "path";
1285
+ import crossSpawn from "cross-spawn";
1286
+ var MAX_OUTPUT_LENGTH = 8 * 1024;
1287
+ function toEnoentError(command, cause) {
1288
+ return new CliError(`Could not run \`${command}\`: command not found.`, {
1289
+ hint: `Make sure \`${command}\` is installed and on your PATH.`,
1290
+ cause
1291
+ });
1292
+ }
1293
+ function isEnoent3(error2) {
1294
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
1295
+ }
1296
+ function parseCommandString(command) {
1297
+ const tokens = [];
1298
+ let current2 = "";
1299
+ let hasCurrent = false;
1300
+ let quote = null;
1301
+ let i = 0;
1302
+ while (i < command.length) {
1303
+ const ch = command.charAt(i);
1304
+ if (quote !== null) {
1305
+ if (ch === quote) {
1306
+ quote = null;
1307
+ } else {
1308
+ current2 += ch;
1309
+ hasCurrent = true;
1310
+ }
1311
+ i++;
1312
+ continue;
1313
+ }
1314
+ if (ch === '"' || ch === "'") {
1315
+ quote = ch;
1316
+ hasCurrent = true;
1317
+ i++;
1318
+ continue;
1319
+ }
1320
+ if (ch === "\\" && i + 1 < command.length) {
1321
+ current2 += command.charAt(i + 1);
1322
+ hasCurrent = true;
1323
+ i += 2;
1324
+ continue;
1325
+ }
1326
+ if (ch === " " || ch === " ") {
1327
+ if (hasCurrent) {
1328
+ tokens.push(current2);
1329
+ current2 = "";
1330
+ hasCurrent = false;
1331
+ }
1332
+ i++;
1333
+ continue;
1334
+ }
1335
+ current2 += ch;
1336
+ hasCurrent = true;
1337
+ i++;
1338
+ }
1339
+ if (quote !== null) {
1340
+ throw new CliError(`Unbalanced ${quote} quote in command: ${command}`);
1341
+ }
1342
+ if (hasCurrent) tokens.push(current2);
1343
+ const [first, ...rest] = tokens;
1344
+ if (first === void 0) {
1345
+ throw new CliError("Cannot parse an empty command string.");
1346
+ }
1347
+ return { command: first, args: rest };
1348
+ }
1349
+ function attachOutputCapture(child, onData) {
1350
+ child.stdout?.on("data", onData);
1351
+ child.stderr?.on("data", onData);
1352
+ }
1353
+ function makeOutputCollector() {
1354
+ let output = "";
1355
+ return {
1356
+ append(chunk) {
1357
+ output += chunk.toString();
1358
+ if (output.length > MAX_OUTPUT_LENGTH) output = output.slice(output.length - MAX_OUTPUT_LENGTH);
1359
+ },
1360
+ get: () => output
1361
+ };
1362
+ }
1363
+ function runProcess(command, args, options) {
1364
+ const spawnFn = options.spawnImpl ?? crossSpawn;
1365
+ const inherit = options.inherit ?? true;
1366
+ return new Promise((resolve2, reject) => {
1367
+ const child = spawnFn(command, args, {
1368
+ cwd: options.cwd,
1369
+ env: options.env ?? process.env,
1370
+ stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"]
1371
+ });
1372
+ const collector = makeOutputCollector();
1373
+ if (!inherit) attachOutputCapture(child, collector.append);
1374
+ let settled = false;
1375
+ child.once("error", (error2) => {
1376
+ if (settled) return;
1377
+ settled = true;
1378
+ reject(isEnoent3(error2) ? toEnoentError(command, error2) : error2);
1379
+ });
1380
+ child.once("close", (code, signal) => {
1381
+ if (settled) return;
1382
+ settled = true;
1383
+ resolve2({ code: code ?? 0, signal, output: collector.get() });
1384
+ });
1385
+ });
1386
+ }
1387
+ function runNpx(args, options) {
1388
+ const baseEnv = options.env ?? process.env;
1389
+ const binDir = join2(options.cwd, "node_modules", ".bin");
1390
+ const path = baseEnv.PATH;
1391
+ const env = {
1392
+ ...baseEnv,
1393
+ PATH: path !== void 0 && path !== "" ? `${binDir}${delimiter}${path}` : binDir
1394
+ };
1395
+ return runProcess("npx", args, { ...options, env });
1396
+ }
1397
+ function trySignal(child, signal) {
1398
+ try {
1399
+ child.kill(signal);
1400
+ } catch (error2) {
1401
+ if (!isEsrch(error2)) throw error2;
1402
+ }
1403
+ }
1404
+ function isEsrch(error2) {
1405
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ESRCH";
1406
+ }
1407
+ function startProcess(command, args, options) {
1408
+ const spawnFn = options.spawnImpl ?? crossSpawn;
1409
+ const inherit = options.inherit ?? true;
1410
+ const env = { ...options.env ?? process.env, [DEV_GUARD_ENV]: "1" };
1411
+ const child = spawnFn(command, args, {
1412
+ cwd: options.cwd,
1413
+ env,
1414
+ stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"]
1415
+ });
1416
+ const collector = makeOutputCollector();
1417
+ if (!inherit) attachOutputCapture(child, collector.append);
1418
+ const exited = new Promise((resolve2, reject) => {
1419
+ child.once("error", (error2) => {
1420
+ reject(isEnoent3(error2) ? toEnoentError(command, error2) : error2);
1421
+ });
1422
+ child.once("close", (code, signal) => {
1423
+ resolve2({ code: code ?? 0, signal, output: collector.get() });
1424
+ });
1425
+ });
1426
+ let stopped = false;
1427
+ const stop = async (signal = "SIGTERM", graceMs = 5e3) => {
1428
+ if (stopped) {
1429
+ await exited.catch(() => void 0);
1430
+ return;
1431
+ }
1432
+ stopped = true;
1433
+ trySignal(child, signal);
1434
+ const killTimer = setTimeout(() => {
1435
+ trySignal(child, "SIGKILL");
1436
+ }, graceMs);
1437
+ killTimer.unref?.();
1438
+ try {
1439
+ await exited;
1440
+ } catch {
1441
+ } finally {
1442
+ clearTimeout(killTimer);
1443
+ }
1444
+ };
1445
+ return {
1446
+ get pid() {
1447
+ return child.pid;
1448
+ },
1449
+ exited,
1450
+ stop
1451
+ };
1452
+ }
1453
+ var DEV_GUARD_ENV = "MERIDIAN_DEV_ACTIVE";
1454
+ function isInsideDevSession(env = process.env) {
1455
+ return env[DEV_GUARD_ENV] === "1";
1456
+ }
1457
+ var RUNNER_PREFIXES = [["npx"], ["npm", "exec"], ["pnpm", "dlx"], ["yarn", "dlx"]];
1458
+ function stripRunnerPrefix(tokens) {
1459
+ for (const prefix of RUNNER_PREFIXES) {
1460
+ if (prefix.length <= tokens.length && prefix.every((part, i) => tokens[i] === part)) {
1461
+ return tokens.slice(prefix.length);
1462
+ }
1463
+ }
1464
+ return tokens;
1465
+ }
1466
+ function isMeridianBinary(token) {
1467
+ if (token === void 0) return false;
1468
+ return token === "meridian" || token.endsWith("/meridian") || token.endsWith("\\meridian");
1469
+ }
1470
+ function assertNotSelfInvoking(command) {
1471
+ let tokens;
1472
+ try {
1473
+ const parsed = parseCommandString(command);
1474
+ tokens = [parsed.command, ...parsed.args];
1475
+ } catch {
1476
+ return;
1477
+ }
1478
+ const rest = stripRunnerPrefix(tokens);
1479
+ if (isMeridianBinary(rest[0]) && rest[1] === "dev") {
1480
+ throw new CliError("`meridian dev` cannot run itself as the app's dev command.", {
1481
+ hint: "Set `dev_command` in .meridian/project.json to the underlying command (e.g. `next dev`), because `meridian dev` runs `npm run dev` by default."
1482
+ });
1483
+ }
1484
+ }
1485
+ function readPackageScript(root, name) {
1486
+ try {
1487
+ const raw = readFileSync3(join2(root, "package.json"), "utf8");
1488
+ const parsed = JSON.parse(raw);
1489
+ if (typeof parsed !== "object" || parsed === null) return null;
1490
+ const scripts = parsed.scripts;
1491
+ if (typeof scripts !== "object" || scripts === null) return null;
1492
+ const value = scripts[name];
1493
+ return typeof value === "string" ? value : null;
1494
+ } catch {
1495
+ return null;
1496
+ }
1497
+ }
1498
+
1499
+ // src/lib/env-file.ts
1500
+ import {
1501
+ chmodSync as chmodSync2,
1502
+ existsSync as existsSync2,
1503
+ mkdirSync as mkdirSync3,
1504
+ readFileSync as readFileSync4,
1505
+ renameSync as renameSync2,
1506
+ statSync,
1507
+ writeFileSync as writeFileSync3
1508
+ } from "fs";
1509
+ import { dirname as dirname3 } from "path";
1510
+ var CREATED_ENV_MODE = 384;
1511
+ var MANAGED_HEADER = "# Managed by the Meridian CLI \u2014 DATABASE_URL is rewritten by `meridian dev`.";
1512
+ var LINE_RE = /^([ \t]*)(export[ \t]+)?([A-Za-z_][A-Za-z0-9_]*)([ \t]*=[ \t]*)(.*)$/;
1513
+ function splitLines(content) {
1514
+ const lines = [];
1515
+ let start = 0;
1516
+ for (let i = 0; i < content.length; i++) {
1517
+ if (content.charAt(i) !== "\n") continue;
1518
+ const hasCr = i > start && content.charAt(i - 1) === "\r";
1519
+ const end = hasCr ? i - 1 : i;
1520
+ lines.push({ text: content.slice(start, end), eol: hasCr ? "\r\n" : "\n" });
1521
+ start = i + 1;
1522
+ }
1523
+ if (start < content.length) lines.push({ text: content.slice(start), eol: "" });
1524
+ return lines;
1525
+ }
1526
+ function detectDominantEol(lines) {
1527
+ let crlf = 0;
1528
+ let lf = 0;
1529
+ for (const line of lines) {
1530
+ if (line.eol === "\r\n") crlf++;
1531
+ else if (line.eol === "\n") lf++;
1532
+ }
1533
+ return crlf > lf ? "\r\n" : "\n";
1534
+ }
1535
+ function matchLine(text) {
1536
+ const match = LINE_RE.exec(text);
1537
+ if (match === null) return null;
1538
+ return {
1539
+ indent: match[1] ?? "",
1540
+ prefix: match[2] ?? "",
1541
+ key: match[3] ?? "",
1542
+ eq: match[4] ?? "",
1543
+ rest: match[5] ?? ""
1544
+ };
1545
+ }
1546
+ function unescapeDoubleQuoted(inner) {
1547
+ return inner.replace(/\\(.)/g, (_full, ch) => ch === "n" ? "\n" : ch);
1548
+ }
1549
+ function decodeEnvValue(raw) {
1550
+ const trimmed = raw.trim();
1551
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
1552
+ return unescapeDoubleQuoted(trimmed.slice(1, -1));
1553
+ }
1554
+ if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
1555
+ return trimmed.slice(1, -1);
1556
+ }
1557
+ return trimmed;
1558
+ }
1559
+ function formatEnvValue(value) {
1560
+ const needsQuoting = value === "" || /[\s#"'$]/.test(value);
1561
+ if (!needsQuoting) return value;
1562
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
1563
+ return `"${escaped}"`;
1564
+ }
1565
+ function upsertEnvContent(content, patch) {
1566
+ const lines = splitLines(content);
1567
+ const keys = [];
1568
+ const toAppend = [];
1569
+ let changed = false;
1570
+ for (const [key, value] of Object.entries(patch)) {
1571
+ let foundIndex = -1;
1572
+ for (let i = lines.length - 1; i >= 0; i--) {
1573
+ const line2 = lines[i];
1574
+ if (line2 === void 0) continue;
1575
+ const match2 = matchLine(line2.text);
1576
+ if (match2 !== null && match2.key === key) {
1577
+ foundIndex = i;
1578
+ break;
1579
+ }
1580
+ }
1581
+ if (foundIndex === -1) {
1582
+ toAppend.push({ key, value });
1583
+ keys.push({ key, next: value, action: "created" });
1584
+ changed = true;
1585
+ continue;
1586
+ }
1587
+ const line = lines[foundIndex];
1588
+ const match = line === void 0 ? null : matchLine(line.text);
1589
+ if (line === void 0 || match === null) continue;
1590
+ const previous = decodeEnvValue(match.rest);
1591
+ if (previous === value) {
1592
+ keys.push({ key, previous, next: value, action: "unchanged" });
1593
+ continue;
1594
+ }
1595
+ lines[foundIndex] = {
1596
+ text: `${match.indent}${match.prefix}${key}${match.eq}${formatEnvValue(value)}`,
1597
+ eol: line.eol
1598
+ };
1599
+ keys.push({ key, previous, next: value, action: "updated" });
1600
+ changed = true;
1601
+ }
1602
+ if (toAppend.length > 0) {
1603
+ const dominantEol = detectDominantEol(lines);
1604
+ const lastIndex = lines.length - 1;
1605
+ const lastLine = lines[lastIndex];
1606
+ if (lastLine !== void 0 && lastLine.eol === "") {
1607
+ lines[lastIndex] = { text: lastLine.text, eol: dominantEol };
1608
+ }
1609
+ for (const { key, value } of toAppend) {
1610
+ lines.push({ text: `${key}=${formatEnvValue(value)}`, eol: dominantEol });
1611
+ }
1612
+ }
1613
+ const nextContent = lines.map((line) => line.text + line.eol).join("");
1614
+ return { content: nextContent, changed, keys };
1615
+ }
1616
+ function isEnoent4(error2) {
1617
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
1618
+ }
1619
+ function writeExact(path, content, mode) {
1620
+ writeFileSync3(path, content, { mode });
1621
+ try {
1622
+ chmodSync2(path, mode);
1623
+ } catch {
1624
+ }
1625
+ }
1626
+ function patchEnvFile(envPath, patch, options = {}) {
1627
+ const backupEnabled = options.backup ?? true;
1628
+ let existing = null;
1629
+ let mode;
1630
+ try {
1631
+ existing = readFileSync4(envPath, "utf8");
1632
+ mode = statSync(envPath).mode & 511;
1633
+ } catch (error2) {
1634
+ if (!isEnoent4(error2)) {
1635
+ throw new CliError(`Could not read ${envPath}.`, {
1636
+ hint: "Check the file's permissions and try again.",
1637
+ cause: error2
1638
+ });
1639
+ }
1640
+ }
1641
+ const created = existing === null;
1642
+ const baseContent = existing !== null ? existing : `${MANAGED_HEADER}
1643
+ `;
1644
+ const targetMode = mode ?? CREATED_ENV_MODE;
1645
+ const { content: nextContent, changed, keys } = upsertEnvContent(baseContent, patch);
1646
+ let backedUp = false;
1647
+ if (existing !== null && changed && backupEnabled) {
1648
+ const backupPath = `${envPath}.backup`;
1649
+ if (!existsSync2(backupPath)) {
1650
+ writeExact(backupPath, existing, targetMode);
1651
+ backedUp = true;
1652
+ }
1653
+ }
1654
+ if (created || changed) {
1655
+ mkdirSync3(dirname3(envPath), { recursive: true });
1656
+ const tmpPath = `${envPath}.${process.pid}.tmp`;
1657
+ writeExact(tmpPath, nextContent, targetMode);
1658
+ renameSync2(tmpPath, envPath);
1659
+ }
1660
+ return { path: envPath, created, backedUp, changed: created || changed, keys };
1661
+ }
1662
+ function buildDatabaseUrl(input2) {
1663
+ const host = input2.host ?? "127.0.0.1";
1664
+ const user = encodeURIComponent(input2.username);
1665
+ const pass = encodeURIComponent(input2.password);
1666
+ const hasParams = input2.params !== void 0 && Object.keys(input2.params).length > 0;
1667
+ const query = hasParams ? `?${new URLSearchParams(input2.params).toString()}` : "";
1668
+ return `mysql://${user}:${pass}@${host}:${input2.port}/${input2.database}${query}`;
1669
+ }
1670
+ function redactDatabaseUrl(url) {
1671
+ return url.replace(/^(mysql:\/\/[^:@/]+):([^@]*)@/, "$1:****@");
1672
+ }
1673
+
1674
+ // src/lib/port.ts
1675
+ import { createServer } from "net";
1676
+ var DEFAULT_START_PORT = 3307;
1677
+ var DEFAULT_ATTEMPTS = 20;
1678
+ var MAX_PORT = 65535;
1679
+ function isPortFree(port, host = "127.0.0.1") {
1680
+ return new Promise((resolve2) => {
1681
+ const server = createServer();
1682
+ server.once("error", () => {
1683
+ resolve2(false);
1684
+ });
1685
+ server.once("listening", () => {
1686
+ server.close(() => resolve2(true));
1687
+ });
1688
+ server.listen({ port, host, exclusive: true });
1689
+ });
1690
+ }
1691
+ function bindEphemeralPort(host) {
1692
+ return new Promise((resolve2) => {
1693
+ const server = createServer();
1694
+ server.once("error", () => resolve2(null));
1695
+ server.listen({ port: 0, host, exclusive: true }, () => {
1696
+ const address = server.address();
1697
+ const port = typeof address === "object" && address !== null ? address.port : null;
1698
+ server.close(() => resolve2(port));
1699
+ });
1700
+ });
1701
+ }
1702
+ async function findFreePort(options = {}) {
1703
+ const host = options.host ?? "127.0.0.1";
1704
+ const start = options.start ?? DEFAULT_START_PORT;
1705
+ const attempts = options.attempts ?? DEFAULT_ATTEMPTS;
1706
+ if (options.preferred !== void 0 && await isPortFree(options.preferred, host)) {
1707
+ return options.preferred;
1708
+ }
1709
+ for (let offset = 0; offset < attempts; offset++) {
1710
+ const candidate = start + offset;
1711
+ if (candidate > MAX_PORT) break;
1712
+ if (await isPortFree(candidate, host)) return candidate;
1713
+ }
1714
+ const ephemeral = await bindEphemeralPort(host);
1715
+ if (ephemeral === null) {
1716
+ throw new CliError("Could not find a free local port for the Cloud SQL Auth Proxy.", {
1717
+ hint: "Close other applications using local ports and try again."
1718
+ });
1719
+ }
1720
+ return ephemeral;
1721
+ }
1722
+
1723
+ // src/lib/prisma.ts
1724
+ import { existsSync as existsSync3, readFileSync as readFileSync5, readdirSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1725
+ import { dirname as dirname4, join as join3, relative } from "path";
1726
+ var REQUIRED_URL_ENV = "DATABASE_URL";
1727
+ var REQUIRED_SHADOW_URL_ENV = "SHADOW_DATABASE_URL";
1728
+ var REQUIRED_PROVIDER = "mysql";
1729
+ var BYPASS_HINT = `Set \`"skip_datasource_check": true\` in .meridian/project.json to run anyway.`;
1730
+ function stripComments(source) {
1731
+ let out = "";
1732
+ let inString = false;
1733
+ let i = 0;
1734
+ while (i < source.length) {
1735
+ const ch = source.charAt(i);
1736
+ if (inString) {
1737
+ if (ch === "\\" && i + 1 < source.length) {
1738
+ out += ch + source.charAt(i + 1);
1739
+ i += 2;
1740
+ continue;
1741
+ }
1742
+ if (ch === '"') inString = false;
1743
+ out += ch;
1744
+ i++;
1745
+ continue;
1746
+ }
1747
+ if (ch === '"') {
1748
+ inString = true;
1749
+ out += ch;
1750
+ i++;
1751
+ continue;
1752
+ }
1753
+ if (ch === "/" && source.charAt(i + 1) === "/") {
1754
+ while (i < source.length && source.charAt(i) !== "\n") i++;
1755
+ continue;
1756
+ }
1757
+ out += ch;
1758
+ i++;
1759
+ }
1760
+ return out;
1761
+ }
1762
+ function isDirectory(path) {
1763
+ try {
1764
+ return statSync2(path).isDirectory();
1765
+ } catch {
1766
+ return false;
1767
+ }
1768
+ }
1769
+ function findPrismaSchemas(root) {
1770
+ const paths = [];
1771
+ const single = join3(root, "prisma", "schema.prisma");
1772
+ if (existsSync3(single)) paths.push(single);
1773
+ const folder = join3(root, "prisma", "schema");
1774
+ if (isDirectory(folder)) {
1775
+ for (const entry of readdirSync(folder).sort()) {
1776
+ if (entry.endsWith(".prisma")) paths.push(join3(folder, entry));
1777
+ }
1778
+ }
1779
+ const bare = join3(root, "schema.prisma");
1780
+ if (existsSync3(bare)) paths.push(bare);
1781
+ return paths;
1782
+ }
1783
+ function parseUrl(body, key) {
1784
+ const fromEnv = new RegExp(`\\b${key}\\s*=\\s*env\\(\\s*"([^"]*)"\\s*\\)`).exec(body);
1785
+ if (fromEnv?.[1] !== void 0) return { kind: "env", name: fromEnv[1] };
1786
+ const literal = new RegExp(`\\b${key}\\s*=\\s*"([^"]*)"`).exec(body);
1787
+ if (literal?.[1] !== void 0) return { kind: "literal", value: literal[1] };
1788
+ return null;
1789
+ }
1790
+ function parseDatasource(source) {
1791
+ const block = /datasource\s+(\w+)\s*\{([^}]*)\}/.exec(stripComments(source));
1792
+ if (block === null) return null;
1793
+ const name = block[1] ?? "db";
1794
+ const body = block[2] ?? "";
1795
+ const provider = /\bprovider\s*=\s*"([^"]*)"/.exec(body);
1796
+ return {
1797
+ name,
1798
+ provider: provider?.[1] ?? null,
1799
+ url: parseUrl(body, "url"),
1800
+ shadowUrl: parseUrl(body, "shadowDatabaseUrl")
1801
+ };
1802
+ }
1803
+ function readPrismaDatasource(root) {
1804
+ for (const path of findPrismaSchemas(root)) {
1805
+ let source;
1806
+ try {
1807
+ source = readFileSync5(path, "utf8");
1808
+ } catch {
1809
+ continue;
1810
+ }
1811
+ const parsed = parseDatasource(source);
1812
+ if (parsed !== null) return { ...parsed, schemaPath: relative(root, path) };
1813
+ }
1814
+ return null;
1815
+ }
1816
+ function missingShadowDatabaseUrl(root) {
1817
+ const datasource = readPrismaDatasource(root);
1818
+ if (datasource === null || datasource.shadowUrl !== null) return null;
1819
+ return datasource.schemaPath;
1820
+ }
1821
+ function alignAssignments(lines) {
1822
+ const parsed = lines.map((line) => /^(\s*)([A-Za-z_]\w*)\s*=\s*(.*)$/.exec(line));
1823
+ const widest = parsed.reduce((max, match) => Math.max(max, match?.[2]?.length ?? 0), 0);
1824
+ return lines.map((line, index) => {
1825
+ const match = parsed[index];
1826
+ if (match === void 0 || match === null) return line;
1827
+ return `${match[1]}${(match[2] ?? "").padEnd(widest)} = ${match[3] ?? ""}`;
1828
+ });
1829
+ }
1830
+ function addShadowDatabaseUrl(root) {
1831
+ const datasource = readPrismaDatasource(root);
1832
+ if (datasource === null) throw new CliError("No Prisma datasource found to add `shadowDatabaseUrl` to.");
1833
+ const absolute = join3(root, datasource.schemaPath);
1834
+ const source = readFileSync5(absolute, "utf8");
1835
+ const lines = source.split("\n");
1836
+ const open = lines.findIndex((line) => new RegExp(`^\\s*datasource\\s+${datasource.name}\\s*\\{`).test(line));
1837
+ if (open === -1) throw new CliError(`Could not locate the \`${datasource.name}\` datasource in ${datasource.schemaPath}.`);
1838
+ const close = lines.findIndex((line, index) => index > open && /^\s*\}/.test(line));
1839
+ if (close === -1) throw new CliError(`\`${datasource.schemaPath}\` has an unterminated datasource block.`);
1840
+ const urlLine = lines.findIndex((line, index) => index > open && index < close && /^\s*url\s*=/.test(line));
1841
+ const insertAfter = urlLine === -1 ? open : urlLine;
1842
+ const indent = /^(\s*)/.exec(lines[insertAfter] ?? "")?.[1];
1843
+ const body = [
1844
+ ...lines.slice(open + 1, insertAfter + 1),
1845
+ `${urlLine === -1 ? " " : indent}shadowDatabaseUrl = env("${REQUIRED_SHADOW_URL_ENV}")`,
1846
+ ...lines.slice(insertAfter + 1, close)
1847
+ ];
1848
+ const patched = [...lines.slice(0, open + 1), ...alignAssignments(body), ...lines.slice(close)];
1849
+ writeFileSync4(absolute, patched.join("\n"));
1850
+ return datasource.schemaPath;
1851
+ }
1852
+ function wrongUrlError(datasource, url) {
1853
+ const problem = url.kind === "literal" ? `hardcodes \`url = "${url.value}"\`` : `reads \`env("${url.name}")\`, but Meridian sets \`${REQUIRED_URL_ENV}\``;
1854
+ return new CliError(
1855
+ `\`${datasource.schemaPath}\` ${problem}, so this command would not touch the managed development database.`,
1856
+ {
1857
+ hint: `Change the \`${datasource.name}\` datasource to \`url = env("${REQUIRED_URL_ENV}")\` with \`provider = "${REQUIRED_PROVIDER}"\`. Migrations generated against another database use dialect-specific SQL and have to be regenerated. ${BYPASS_HINT}`
1858
+ }
1859
+ );
1860
+ }
1861
+ function wrongProviderError(datasource, provider) {
1862
+ return new CliError(
1863
+ `\`${datasource.schemaPath}\` sets \`provider = "${provider}"\`, but the managed development database is MySQL.`,
1864
+ {
1865
+ hint: `Change the \`${datasource.name}\` datasource to \`provider = "${REQUIRED_PROVIDER}"\` and regenerate any migrations generated for ${provider}, which use dialect-specific SQL. ${BYPASS_HINT}`
1866
+ }
1867
+ );
1868
+ }
1869
+ function assertManagedDatasource(root) {
1870
+ const datasource = readPrismaDatasource(root);
1871
+ if (datasource === null || datasource.url === null) return;
1872
+ if (datasource.url.kind !== "env" || datasource.url.name !== REQUIRED_URL_ENV) {
1873
+ throw wrongUrlError(datasource, datasource.url);
1874
+ }
1875
+ if (datasource.provider !== null && datasource.provider !== REQUIRED_PROVIDER) {
1876
+ throw wrongProviderError(datasource, datasource.provider);
1877
+ }
1878
+ }
1879
+ function assertShadowDatasource(root) {
1880
+ const datasource = readPrismaDatasource(root);
1881
+ if (datasource === null) return;
1882
+ const shadow = datasource.shadowUrl;
1883
+ if (shadow !== null && shadow.kind === "env" && shadow.name === REQUIRED_SHADOW_URL_ENV) return;
1884
+ const problem = shadow === null ? "declares no `shadowDatabaseUrl`" : shadow.kind === "literal" ? `hardcodes \`shadowDatabaseUrl = "${shadow.value}"\`` : `reads \`env("${shadow.name}")\` for its shadow database, but Meridian sets \`${REQUIRED_SHADOW_URL_ENV}\``;
1885
+ throw new CliError(`\`${datasource.schemaPath}\` ${problem}.`, {
1886
+ hint: `\`prisma migrate dev\` diffs your migrations in a throwaway shadow database, and your dev user cannot create one \u2014 so Meridian provisions it for you and exports \`${REQUIRED_SHADOW_URL_ENV}\`. Add \`shadowDatabaseUrl = env("${REQUIRED_SHADOW_URL_ENV}")\` to the \`${datasource.name}\` datasource. ${BYPASS_HINT}`
1887
+ });
1888
+ }
1889
+ var FOREIGN_DIALECT_DDL = /\b(?:CREATE\s+(?:UNIQUE\s+)?(?:TABLE|INDEX)|ALTER\s+TABLE|REFERENCES)\s+"/i;
1890
+ function migrationSqlPaths(root) {
1891
+ const schemas = findPrismaSchemas(root);
1892
+ const roots = new Set(schemas.map((schema) => join3(dirname4(schema), "migrations")));
1893
+ roots.add(join3(root, "prisma", "migrations"));
1894
+ const paths = [];
1895
+ for (const dir of roots) {
1896
+ if (!isDirectory(dir)) continue;
1897
+ for (const entry of readdirSync(dir).sort()) {
1898
+ const sql = join3(dir, entry, "migration.sql");
1899
+ if (existsSync3(sql)) paths.push(sql);
1900
+ }
1901
+ }
1902
+ return paths;
1903
+ }
1904
+ function findForeignDialectMigration(root) {
1905
+ const offender = migrationSqlPaths(root).find((path) => {
1906
+ try {
1907
+ return FOREIGN_DIALECT_DDL.test(readFileSync5(path, "utf8"));
1908
+ } catch {
1909
+ return false;
1910
+ }
1911
+ });
1912
+ return offender === void 0 ? null : relative(root, offender);
1913
+ }
1914
+ function foreignDialectExplanation(offender) {
1915
+ return `\`${offender}\` was generated for SQLite or Postgres \u2014 it quotes identifiers with double quotes, which MySQL reads as string literals. Migration SQL is dialect-specific and cannot be replayed against MySQL. If this history has only ever run locally, delete \`prisma/migrations\` and re-run to regenerate it for MySQL; run \`meridian db reset\` first if an earlier attempt already failed, since Prisma records that failure in \`_prisma_migrations\` and will not continue while it is there.`;
1916
+ }
1917
+ function assertMigrationDialect(root) {
1918
+ const offender = findForeignDialectMigration(root);
1919
+ if (offender === null) return;
1920
+ throw new CliError(`\`${offender}\` was written for a different database engine and cannot apply to MySQL.`, {
1921
+ hint: `${foreignDialectExplanation(offender)} Pass \`--allow-foreign-migrations\` to try anyway.`
1922
+ });
1923
+ }
1924
+ function diagnoseMigrationFailure(root) {
1925
+ const offender = findForeignDialectMigration(root);
1926
+ return offender === null ? null : foreignDialectExplanation(offender);
1927
+ }
1928
+
1929
+ // src/lib/proxy/binary.ts
1930
+ import { createHash } from "crypto";
1931
+ import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
1932
+ import { join as join4 } from "path";
1933
+ var PROXY_VERSION = "2.24.1";
1934
+ var PROXY_CHECKSUMS = Object.freeze({
1935
+ "cloud-sql-proxy.darwin.amd64": "67a38444d03fe77a8b11036f96fe5d2379640088bb6e338737fb48d97c58cf48",
1936
+ "cloud-sql-proxy.darwin.arm64": "fd374545705f2aa687d8e525a2efcbb9d356cc8af60d4dfa1a330cb59a7f9450",
1937
+ "cloud-sql-proxy.linux.386": "cf731f4becfb5a2de61f2998b2d660c5fb8f3067cbd3b86f5783ae9049c06f24",
1938
+ "cloud-sql-proxy.linux.amd64": "fae2766aac9d614a2bdef2f2a7778f3d054f3acd5ff07a81a9e300bd471512eb",
1939
+ "cloud-sql-proxy.linux.arm64": "da49fd73b0d33c0ab8e5dfc19dfb104cdd969ebd355eef8fd3b079c20a723c82",
1940
+ "cloud-sql-proxy.x64.exe": "01ec54c69dbb8118c0b181c33ff1469a5cdb2dddb49eb86b0b173f6c55dea0bc",
1941
+ "cloud-sql-proxy.x86.exe": "98c3b1897c80f2bc0b241b4452c18557a00cd86f89e4d7449c4ff1316e243c44"
1942
+ });
1943
+ var ASSET_NAME_BY_PLATFORM_ARCH = Object.freeze({
1944
+ "darwin:arm64": "cloud-sql-proxy.darwin.arm64",
1945
+ "darwin:x64": "cloud-sql-proxy.darwin.amd64",
1946
+ "linux:x64": "cloud-sql-proxy.linux.amd64",
1947
+ "linux:arm64": "cloud-sql-proxy.linux.arm64",
1948
+ "linux:ia32": "cloud-sql-proxy.linux.386",
1949
+ "win32:x64": "cloud-sql-proxy.x64.exe",
1950
+ "win32:ia32": "cloud-sql-proxy.x86.exe"
1951
+ });
1952
+ function defaultVersion() {
1953
+ const override = process.env.MERIDIAN_PROXY_VERSION;
1954
+ if (override !== void 0 && override !== "") return override;
1955
+ return PROXY_VERSION;
1956
+ }
1957
+ function resolveProxyAsset(options) {
1958
+ const platform = options?.platform ?? process.platform;
1959
+ const arch = options?.arch ?? process.arch;
1960
+ const version = options?.version ?? defaultVersion();
1961
+ const assetName = ASSET_NAME_BY_PLATFORM_ARCH[`${platform}:${arch}`];
1962
+ if (assetName === void 0) {
1963
+ throw new CliError(`The Cloud SQL Auth Proxy has no published build for ${platform}/${arch}.`, {
1964
+ hint: "Set MERIDIAN_PROXY_PATH to the path of a proxy binary you install yourself."
1965
+ });
1966
+ }
1967
+ const downloadUrl = `https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v${version}/${assetName}`;
1968
+ const sha256Value = version === PROXY_VERSION ? PROXY_CHECKSUMS[assetName] : void 0;
1969
+ const fileName = `cloud-sql-proxy-${version}-${platform}-${arch}${platform === "win32" ? ".exe" : ""}`;
1970
+ return {
1971
+ version,
1972
+ assetName,
1973
+ downloadUrl,
1974
+ ...sha256Value !== void 0 ? { sha256: sha256Value } : {},
1975
+ fileName
1976
+ };
1977
+ }
1978
+ function sha256(data) {
1979
+ return createHash("sha256").update(data).digest("hex");
1980
+ }
1981
+ var CHECKSUM_ROW_PATTERN = /\[([^\]]+)]\([^)]*\)\s*\|\s*([0-9a-fA-F]{64})/g;
1982
+ function parseChecksumTable(releaseBody) {
1983
+ const table = {};
1984
+ for (const match of releaseBody.matchAll(CHECKSUM_ROW_PATTERN)) {
1985
+ const name = match[1];
1986
+ const hash = match[2];
1987
+ if (name !== void 0 && hash !== void 0) table[name] = hash.toLowerCase();
1988
+ }
1989
+ return table;
1990
+ }
1991
+ var RELEASE_API_URL = "https://api.github.com/repos/GoogleCloudPlatform/cloud-sql-proxy/releases/tags";
1992
+ var checksumCache = /* @__PURE__ */ new Map();
1993
+ async function resolveExpectedChecksum(asset, fetchImpl) {
1994
+ if (asset.sha256 !== void 0) return asset.sha256;
1995
+ const cacheKey = `${asset.version}:${asset.assetName}`;
1996
+ const cached = checksumCache.get(cacheKey);
1997
+ if (cached !== void 0) return cached;
1998
+ const skipAllowed = process.env.MERIDIAN_PROXY_SKIP_CHECKSUM === "1";
1999
+ const url = `${RELEASE_API_URL}/v${asset.version}`;
2000
+ let response;
2001
+ try {
2002
+ response = await fetchImpl(url, { headers: { Accept: "application/vnd.github+json" } });
2003
+ } catch (error2) {
2004
+ if (skipAllowed) return void 0;
2005
+ throw new CliError(`Could not reach ${url} to look up the checksum for cloud-sql-proxy v${asset.version}.`, {
2006
+ hint: "Check your network connection, or set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk.",
2007
+ cause: error2
2008
+ });
2009
+ }
2010
+ if (!response.ok) {
2011
+ if (skipAllowed) return void 0;
2012
+ throw new CliError(
2013
+ `Could not determine the sha256 checksum for cloud-sql-proxy v${asset.version} (GitHub release lookup for ${url} returned HTTP ${response.status}).`,
2014
+ {
2015
+ hint: "Set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk."
2016
+ }
2017
+ );
2018
+ }
2019
+ const rawBody = await response.text();
2020
+ let releaseNotes = rawBody;
2021
+ try {
2022
+ const parsed = JSON.parse(rawBody);
2023
+ if (typeof parsed === "object" && parsed !== null && "body" in parsed && typeof parsed.body === "string") {
2024
+ releaseNotes = parsed.body;
2025
+ }
2026
+ } catch {
2027
+ }
2028
+ const checksum = parseChecksumTable(releaseNotes)[asset.assetName];
2029
+ if (checksum !== void 0) {
2030
+ checksumCache.set(cacheKey, checksum);
2031
+ return checksum;
2032
+ }
2033
+ if (skipAllowed) return void 0;
2034
+ throw new CliError(
2035
+ `Could not find a published sha256 checksum for ${asset.assetName} in the cloud-sql-proxy v${asset.version} release notes.`,
2036
+ { hint: "Set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk." }
2037
+ );
2038
+ }
2039
+ function parseContentLength(header) {
2040
+ if (header === null) return void 0;
2041
+ const value = Number(header);
2042
+ return Number.isFinite(value) ? value : void 0;
2043
+ }
2044
+ async function readResponseBody(response, onProgress) {
2045
+ const total = parseContentLength(response.headers.get("content-length"));
2046
+ if (response.body === null) {
2047
+ const buffer = Buffer.from(await response.arrayBuffer());
2048
+ onProgress?.(buffer.byteLength, total);
2049
+ return buffer;
2050
+ }
2051
+ const reader = response.body.getReader();
2052
+ const chunks = [];
2053
+ let received = 0;
2054
+ for (; ; ) {
2055
+ const { done, value } = await reader.read();
2056
+ if (done) break;
2057
+ if (value !== void 0) {
2058
+ chunks.push(value);
2059
+ received += value.byteLength;
2060
+ onProgress?.(received, total);
2061
+ }
2062
+ }
2063
+ return Buffer.concat(chunks);
2064
+ }
2065
+ async function ensureProxyBinary(options = {}) {
2066
+ const overridePath = process.env.MERIDIAN_PROXY_PATH;
2067
+ if (overridePath !== void 0 && overridePath !== "") {
2068
+ if (!existsSync4(overridePath)) {
2069
+ throw new CliError(`MERIDIAN_PROXY_PATH is set to ${overridePath}, but that file does not exist.`, {
2070
+ hint: "Fix or unset MERIDIAN_PROXY_PATH."
2071
+ });
2072
+ }
2073
+ return overridePath;
2074
+ }
2075
+ const asset = resolveProxyAsset(options.version !== void 0 ? { version: options.version } : {});
2076
+ const dir = options.dir ?? proxyBinDir();
2077
+ const fetchImpl = options.fetchImpl ?? fetch;
2078
+ const targetPath = join4(dir, asset.fileName);
2079
+ mkdirSync4(dir, { recursive: true });
2080
+ if (!options.force && existsSync4(targetPath)) {
2081
+ const cached = readFileSync6(targetPath);
2082
+ const expected2 = await resolveExpectedChecksum(asset, fetchImpl);
2083
+ if (expected2 === void 0 || sha256(cached) === expected2) return targetPath;
2084
+ rmSync2(targetPath, { force: true });
2085
+ }
2086
+ const response = await fetchImpl(asset.downloadUrl);
2087
+ if (!response.ok) {
2088
+ throw new CliError(
2089
+ `Failed to download the Cloud SQL Auth Proxy v${asset.version} from ${asset.downloadUrl} (HTTP ${response.status}).`,
2090
+ {
2091
+ hint: "Check your network connection (and any corporate proxy settings), or set MERIDIAN_PROXY_PATH to a proxy binary you install yourself."
2092
+ }
2093
+ );
2094
+ }
2095
+ const buffer = await readResponseBody(response, options.onProgress);
2096
+ const expected = await resolveExpectedChecksum(asset, fetchImpl);
2097
+ const actual = sha256(buffer);
2098
+ if (expected !== void 0 && actual !== expected) {
2099
+ throw new CliError(
2100
+ `Checksum mismatch for ${asset.assetName}: expected ${expected.slice(0, 12)}..., got ${actual.slice(0, 12)}....`,
2101
+ {
2102
+ hint: "This can happen after a corrupted download or an untrusted network. Try again; if it persists, report this."
2103
+ }
2104
+ );
2105
+ }
2106
+ const tmpPath = join4(dir, `.${asset.fileName}.${process.pid}.tmp`);
2107
+ writeFileSync5(tmpPath, buffer);
2108
+ chmodSync3(tmpPath, 493);
2109
+ renameSync3(tmpPath, targetPath);
2110
+ return targetPath;
2111
+ }
2112
+
2113
+ // src/lib/proxy/proxy.ts
2114
+ import { spawn } from "child_process";
2115
+ import { createConnection } from "net";
2116
+ var PROBE_INTERVAL_MS = 150;
2117
+ var KILL_GRACE_MS = 2e3;
2118
+ var CRASH_BACKOFFS_MS = [200, 1e3, 3e3];
2119
+ var MAX_CRASH_RESTARTS = CRASH_BACKOFFS_MS.length;
2120
+ var REFRESH_RETRY_BACKOFFS_MS = [3e4, 6e4, 12e4];
2121
+ var MIN_REFRESH_DELAY_MS = 6e4;
2122
+ var LOG_RING_SIZE = 20;
2123
+ var STDERR_TAIL_LINES = 10;
2124
+ function buildProxyArgs(input2) {
2125
+ return ["--address", input2.host, "--port", String(input2.port), input2.instanceConnectionName];
2126
+ }
2127
+ function buildProxyEnv(accessToken, base = process.env) {
2128
+ return { ...base, CSQL_PROXY_TOKEN: accessToken };
2129
+ }
2130
+ function defaultProbe(host, port) {
2131
+ return new Promise((resolve2) => {
2132
+ const socket = createConnection({ host, port });
2133
+ const finish = (ok) => {
2134
+ socket.removeAllListeners();
2135
+ socket.destroy();
2136
+ resolve2(ok);
2137
+ };
2138
+ socket.once("connect", () => finish(true));
2139
+ socket.once("error", () => finish(false));
2140
+ socket.setTimeout(1e3, () => finish(false));
2141
+ });
2142
+ }
2143
+ function computeRefreshDelayMs(token, refreshSkewMs, assumedLifetimeMs) {
2144
+ let expiresAtMs;
2145
+ if (token.expires_at !== void 0) {
2146
+ const parsed = new Date(token.expires_at).getTime();
2147
+ if (!Number.isNaN(parsed)) expiresAtMs = parsed;
2148
+ }
2149
+ const effectiveExpiryMs = expiresAtMs ?? Date.now() + assumedLifetimeMs;
2150
+ return Math.max(effectiveExpiryMs - Date.now() - refreshSkewMs, MIN_REFRESH_DELAY_MS);
2151
+ }
2152
+ function buildStartupFailureError(code, signal, lastLines) {
2153
+ const tail = lastLines.slice(-STDERR_TAIL_LINES).join("\n");
2154
+ const lower = tail.toLowerCase();
2155
+ let hint2 = "Run `meridian dev` again; if this keeps happening, check the instance connection name and your network.";
2156
+ if (lower.includes("iam") || lower.includes("permission") || lower.includes("forbidden")) {
2157
+ hint2 = "The Meridian API's service account may be missing roles/cloudsql.client on the dev Cloud SQL instance. Contact Meridian support.";
2158
+ } else if (lower.includes("invalid token") || lower.includes("401") || lower.includes("unauthorized")) {
2159
+ hint2 = "The access token appears to have expired. Retry `meridian dev` to mint a fresh one.";
2160
+ } else if (lower.includes("address already in use")) {
2161
+ hint2 = "Another process is already listening on that port. Free it up, or retry with a different port.";
2162
+ }
2163
+ const summary = `The Cloud SQL Auth Proxy exited before it was ready (code ${code ?? "null"}, signal ${signal ?? "null"}).`;
2164
+ return new CliError(tail === "" ? summary : `${summary}
2165
+ ${tail}`, { hint: hint2 });
2166
+ }
2167
+ function waitForReady(child, probeImpl, host, port, timeoutMs, setTimeoutImpl, getLastLines) {
2168
+ return new Promise((resolve2, reject) => {
2169
+ let settled = false;
2170
+ let pollTimer;
2171
+ let timeoutTimer;
2172
+ const cleanup = () => {
2173
+ if (pollTimer !== void 0) clearTimeout(pollTimer);
2174
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
2175
+ child.off("exit", onExit);
2176
+ };
2177
+ function onExit(code, signal) {
2178
+ if (settled) return;
2179
+ settled = true;
2180
+ cleanup();
2181
+ reject(buildStartupFailureError(code, signal, getLastLines()));
2182
+ }
2183
+ function poll() {
2184
+ if (settled) return;
2185
+ probeImpl(host, port).then((ok) => {
2186
+ if (settled) return;
2187
+ if (ok) {
2188
+ settled = true;
2189
+ cleanup();
2190
+ resolve2();
2191
+ } else {
2192
+ pollTimer = setTimeoutImpl(poll, PROBE_INTERVAL_MS);
2193
+ }
2194
+ }).catch(() => {
2195
+ if (!settled) pollTimer = setTimeoutImpl(poll, PROBE_INTERVAL_MS);
2196
+ });
2197
+ }
2198
+ child.on("exit", onExit);
2199
+ timeoutTimer = setTimeoutImpl(() => {
2200
+ if (settled) return;
2201
+ settled = true;
2202
+ cleanup();
2203
+ reject(
2204
+ new CliError(`Timed out after ${timeoutMs}ms waiting for the Cloud SQL Auth Proxy to accept connections.`, {
2205
+ hint: "The instance may be slow to reach, or readyTimeoutMs may need to be raised."
2206
+ })
2207
+ );
2208
+ }, timeoutMs);
2209
+ poll();
2210
+ });
2211
+ }
2212
+ async function startProxy(options) {
2213
+ const host = options.host ?? "127.0.0.1";
2214
+ const spawnImpl = options.spawnImpl ?? spawn;
2215
+ const probeImpl = options.probeImpl ?? defaultProbe;
2216
+ const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
2217
+ const refreshSkewMs = options.refreshSkewMs ?? 5 * 6e4;
2218
+ const assumedLifetimeMs = options.assumedLifetimeMs ?? 55 * 6e4;
2219
+ const readyTimeoutMs = options.readyTimeoutMs ?? 3e4;
2220
+ const emit = (event) => options.onEvent?.(event);
2221
+ let stopped = false;
2222
+ let running = false;
2223
+ let expectingExit = false;
2224
+ let currentToken = options.token;
2225
+ let child;
2226
+ let refreshTimer;
2227
+ let crashTimer;
2228
+ let refreshBackoffIndex = 0;
2229
+ let crashRestartsRemaining = MAX_CRASH_RESTARTS;
2230
+ let attemptCounter = 0;
2231
+ let cleanupFns = [];
2232
+ const knownTokens = /* @__PURE__ */ new Set();
2233
+ const logRing = [];
2234
+ function trackToken(token) {
2235
+ if (token.access_token !== "") knownTokens.add(token.access_token);
2236
+ }
2237
+ function redact(text) {
2238
+ let result = text;
2239
+ for (const token of knownTokens) result = result.split(token).join("<redacted>");
2240
+ return result;
2241
+ }
2242
+ function pushLog(line) {
2243
+ const safeLine = redact(line);
2244
+ logRing.push(safeLine);
2245
+ if (logRing.length > LOG_RING_SIZE) logRing.shift();
2246
+ emit({ type: "log", line: safeLine });
2247
+ }
2248
+ function wireOutput(c) {
2249
+ for (const stream of [c.stdout, c.stderr]) {
2250
+ if (stream === null) continue;
2251
+ let buffer = "";
2252
+ const onData = (chunk) => {
2253
+ buffer += chunk.toString();
2254
+ const lines = buffer.split("\n");
2255
+ buffer = lines.pop() ?? "";
2256
+ for (const line of lines) pushLog(line);
2257
+ };
2258
+ stream.on("data", onData);
2259
+ cleanupFns.push(() => stream.off("data", onData));
2260
+ }
2261
+ }
2262
+ function wireExit(c) {
2263
+ c.once("exit", (code, signal) => {
2264
+ if (child !== c) return;
2265
+ const wasRunning = running;
2266
+ const wasExpected = expectingExit;
2267
+ running = false;
2268
+ expectingExit = false;
2269
+ emit({ type: "exit", code, signal });
2270
+ if (stopped || wasExpected || !wasRunning) return;
2271
+ void handleCrash();
2272
+ });
2273
+ }
2274
+ async function spawnChild() {
2275
+ attemptCounter += 1;
2276
+ emit({ type: "starting", attempt: attemptCounter });
2277
+ const args = buildProxyArgs({
2278
+ instanceConnectionName: options.instanceConnectionName,
2279
+ host,
2280
+ port: options.port
2281
+ });
2282
+ const c = spawnImpl(options.binaryPath, args, {
2283
+ stdio: ["ignore", "pipe", "pipe"],
2284
+ env: buildProxyEnv(currentToken.access_token, options.env)
2285
+ });
2286
+ child = c;
2287
+ wireOutput(c);
2288
+ wireExit(c);
2289
+ try {
2290
+ await waitForReady(c, probeImpl, host, options.port, readyTimeoutMs, setTimeoutImpl, () => logRing.slice());
2291
+ } catch (error2) {
2292
+ expectingExit = true;
2293
+ await killChild(c);
2294
+ if (child === c) child = void 0;
2295
+ throw error2;
2296
+ }
2297
+ emit({ type: "ready", port: options.port });
2298
+ }
2299
+ function killChild(c) {
2300
+ if (c.exitCode !== null || c.signalCode !== null) return Promise.resolve();
2301
+ return new Promise((resolve2) => {
2302
+ let settled = false;
2303
+ let killTimer;
2304
+ const onExit = () => {
2305
+ if (settled) return;
2306
+ settled = true;
2307
+ if (killTimer !== void 0) clearTimeout(killTimer);
2308
+ resolve2();
2309
+ };
2310
+ c.once("exit", onExit);
2311
+ c.kill("SIGTERM");
2312
+ killTimer = setTimeoutImpl(() => {
2313
+ if (!settled) c.kill("SIGKILL");
2314
+ }, KILL_GRACE_MS);
2315
+ });
2316
+ }
2317
+ function scheduleRefresh(token) {
2318
+ if (stopped || options.refreshToken === void 0) return;
2319
+ if (refreshTimer !== void 0) clearTimeout(refreshTimer);
2320
+ const delayMs = computeRefreshDelayMs(token, refreshSkewMs, assumedLifetimeMs);
2321
+ refreshTimer = setTimeoutImpl(() => {
2322
+ refreshTimer = void 0;
2323
+ void handleRefreshFire();
2324
+ }, delayMs);
2325
+ }
2326
+ function scheduleRefreshRetry() {
2327
+ if (stopped) return;
2328
+ const delayMs = REFRESH_RETRY_BACKOFFS_MS[Math.min(refreshBackoffIndex, REFRESH_RETRY_BACKOFFS_MS.length - 1)] ?? 12e4;
2329
+ refreshBackoffIndex += 1;
2330
+ if (refreshTimer !== void 0) clearTimeout(refreshTimer);
2331
+ refreshTimer = setTimeoutImpl(() => {
2332
+ refreshTimer = void 0;
2333
+ void handleRefreshFire();
2334
+ }, delayMs);
2335
+ }
2336
+ async function handleRefreshFire() {
2337
+ if (stopped || options.refreshToken === void 0) return;
2338
+ let newToken;
2339
+ try {
2340
+ newToken = await options.refreshToken();
2341
+ } catch (error2) {
2342
+ emit({ type: "error", message: redact(`Token refresh failed: ${describeError(error2)}`) });
2343
+ scheduleRefreshRetry();
2344
+ return;
2345
+ }
2346
+ if (stopped) return;
2347
+ trackToken(newToken);
2348
+ currentToken = newToken;
2349
+ expectingExit = true;
2350
+ emit({ type: "restarting", reason: "token-refresh" });
2351
+ try {
2352
+ const previous = child;
2353
+ if (previous !== void 0) await killChild(previous);
2354
+ await spawnChild();
2355
+ } catch (error2) {
2356
+ emit({ type: "error", message: redact(`Restart after token refresh failed: ${describeError(error2)}`) });
2357
+ scheduleRefreshRetry();
2358
+ return;
2359
+ }
2360
+ running = true;
2361
+ refreshBackoffIndex = 0;
2362
+ emit({ type: "token-refreshed", ...newToken.expires_at !== void 0 ? { expiresAt: newToken.expires_at } : {} });
2363
+ scheduleRefresh(newToken);
2364
+ }
2365
+ async function handleCrash() {
2366
+ if (stopped) return;
2367
+ if (crashRestartsRemaining <= 0) {
2368
+ const tail = logRing.slice(-STDERR_TAIL_LINES).join("\n");
2369
+ emit({
2370
+ type: "error",
2371
+ message: `The Cloud SQL Auth Proxy crashed repeatedly and could not be restarted.${tail === "" ? "" : `
2372
+ ${tail}`}`
2373
+ });
2374
+ return;
2375
+ }
2376
+ const attemptIndex = MAX_CRASH_RESTARTS - crashRestartsRemaining;
2377
+ const delayMs = CRASH_BACKOFFS_MS[attemptIndex] ?? (CRASH_BACKOFFS_MS[CRASH_BACKOFFS_MS.length - 1] ?? 3e3);
2378
+ crashRestartsRemaining -= 1;
2379
+ emit({ type: "restarting", reason: "crash" });
2380
+ crashTimer = setTimeoutImpl(() => {
2381
+ crashTimer = void 0;
2382
+ if (stopped) return;
2383
+ spawnChild().then(() => {
2384
+ running = true;
2385
+ }).catch((error2) => {
2386
+ emit({ type: "error", message: redact(describeError(error2)) });
2387
+ void handleCrash();
2388
+ });
2389
+ }, delayMs);
2390
+ }
2391
+ async function refresh() {
2392
+ if (stopped) return;
2393
+ if (options.refreshToken === void 0) {
2394
+ throw new CliError("Cannot refresh the Cloud SQL Auth Proxy token: no refreshToken() was configured.");
2395
+ }
2396
+ if (refreshTimer !== void 0) {
2397
+ clearTimeout(refreshTimer);
2398
+ refreshTimer = void 0;
2399
+ }
2400
+ refreshBackoffIndex = 0;
2401
+ await handleRefreshFire();
2402
+ }
2403
+ async function stop() {
2404
+ if (stopped) return;
2405
+ stopped = true;
2406
+ if (refreshTimer !== void 0) clearTimeout(refreshTimer);
2407
+ refreshTimer = void 0;
2408
+ if (crashTimer !== void 0) clearTimeout(crashTimer);
2409
+ crashTimer = void 0;
2410
+ for (const cleanup of cleanupFns) cleanup();
2411
+ cleanupFns = [];
2412
+ const current2 = child;
2413
+ if (current2 !== void 0) {
2414
+ expectingExit = true;
2415
+ await killChild(current2);
2416
+ }
2417
+ running = false;
2418
+ child = void 0;
2419
+ }
2420
+ trackToken(currentToken);
2421
+ await spawnChild();
2422
+ running = true;
2423
+ scheduleRefresh(currentToken);
2424
+ return {
2425
+ host,
2426
+ port: options.port,
2427
+ instanceConnectionName: options.instanceConnectionName,
2428
+ get running() {
2429
+ return running;
2430
+ },
2431
+ refresh,
2432
+ stop
2433
+ };
2434
+ }
2435
+
2436
+ // src/lib/signals.ts
2437
+ var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
2438
+ var SIGNAL_EXIT_CODES = {
2439
+ SIGHUP: 129,
2440
+ SIGINT: 130,
2441
+ SIGTERM: 143
2442
+ };
2443
+ var FORCE_EXIT_CODE = 130;
2444
+ var DEFAULT_HANDLER_TIMEOUT_MS = 8e3;
2445
+ var handlers = [];
2446
+ var signalListeners;
2447
+ var isShuttingDown = false;
2448
+ var hasRun = false;
2449
+ var exitImpl = (code) => process.exit(code);
2450
+ function attachListeners() {
2451
+ if (signalListeners !== void 0) return;
2452
+ const listeners = /* @__PURE__ */ new Map();
2453
+ for (const signal of SIGNALS) {
2454
+ const listener = () => {
2455
+ void handleSignal(signal);
2456
+ };
2457
+ process.on(signal, listener);
2458
+ listeners.set(signal, listener);
2459
+ }
2460
+ signalListeners = listeners;
2461
+ }
2462
+ function detachListeners() {
2463
+ if (signalListeners === void 0) return;
2464
+ for (const [signal, listener] of signalListeners) {
2465
+ process.off(signal, listener);
2466
+ }
2467
+ signalListeners = void 0;
2468
+ }
2469
+ async function handleSignal(signal) {
2470
+ if (isShuttingDown) {
2471
+ exitImpl(FORCE_EXIT_CODE);
2472
+ return;
2473
+ }
2474
+ isShuttingDown = true;
2475
+ await runShutdownHandlers({ signal });
2476
+ process.exitCode = exitCodeForSignal(signal);
2477
+ }
2478
+ function runHandlerWithTimeout(handler, reason, timeoutMs) {
2479
+ return new Promise((resolve2) => {
2480
+ let settled = false;
2481
+ const timer = setTimeout(() => {
2482
+ if (settled) return;
2483
+ settled = true;
2484
+ resolve2();
2485
+ }, timeoutMs);
2486
+ timer.unref?.();
2487
+ Promise.resolve().then(() => handler(reason)).catch(() => {
2488
+ }).then(() => {
2489
+ if (settled) return;
2490
+ settled = true;
2491
+ clearTimeout(timer);
2492
+ resolve2();
2493
+ });
2494
+ });
2495
+ }
2496
+ function onShutdown(handler) {
2497
+ handlers.push(handler);
2498
+ attachListeners();
2499
+ let unregistered = false;
2500
+ return () => {
2501
+ if (unregistered) return;
2502
+ unregistered = true;
2503
+ const index = handlers.indexOf(handler);
2504
+ if (index !== -1) handlers.splice(index, 1);
2505
+ if (handlers.length === 0) detachListeners();
2506
+ };
2507
+ }
2508
+ async function runShutdownHandlers(reason = {}, timeoutMs = DEFAULT_HANDLER_TIMEOUT_MS) {
2509
+ if (hasRun) return;
2510
+ hasRun = true;
2511
+ const toRun = [...handlers].reverse();
2512
+ for (const handler of toRun) {
2513
+ await runHandlerWithTimeout(handler, reason, timeoutMs);
2514
+ }
2515
+ }
2516
+ function exitCodeForSignal(signal) {
2517
+ return SIGNAL_EXIT_CODES[signal] ?? 128;
2518
+ }
2519
+
2520
+ // src/lib/session.ts
2521
+ function defaultSleep(ms) {
2522
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2523
+ }
2524
+ function resolveDeps(overrides) {
2525
+ return {
2526
+ ensureProxyBinary,
2527
+ startProxy,
2528
+ findFreePort,
2529
+ patchEnvFile,
2530
+ waitForDatabase,
2531
+ sleep: defaultSleep,
2532
+ now: () => Date.now(),
2533
+ ...overrides
2534
+ };
2535
+ }
2536
+ var DEFAULT_POLL_TIMEOUT_MS = 5 * 6e4;
2537
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
2538
+ var NOT_FOUND_GRACE_MS = 3e4;
2539
+ var DEFAULT_PORT_START = 3307;
2540
+ var DEV_DATABASE_ID_ENV = "MERIDIAN_DEV_DATABASE_ID";
2541
+ function provisioningFailedError(devDatabase) {
2542
+ const reason = devDatabase.error_message;
2543
+ const detail2 = typeof reason === "string" && reason.trim() !== "" ? ` ${reason.trim()}` : "";
2544
+ return new CliError(`Provisioning the development database failed.${detail2}`, {
2545
+ hint: "Check the Meridian dashboard for details, or contact support if this keeps happening."
2546
+ });
2547
+ }
2548
+ function provisioningTimeoutError() {
2549
+ return new CliError("Timed out waiting for the development database to be provisioned.", {
2550
+ hint: "Try `meridian dev` again in a few minutes, and check the Meridian dashboard for its status."
2551
+ });
2552
+ }
2553
+ function provisioningNotFoundError() {
2554
+ return new CliError("The Meridian API never finished creating the development database.", {
2555
+ hint: "Try `meridian dev` again; if this keeps happening, contact Meridian support."
2556
+ });
2557
+ }
2558
+ async function pollDevDatabase(client, appId, options, spinner2) {
2559
+ const timeoutMs = options.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
2560
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2561
+ const sleep = options.sleep ?? defaultSleep;
2562
+ const now = options.now ?? Date.now;
2563
+ const start = now();
2564
+ const report = (status) => {
2565
+ const elapsedMs = now() - start;
2566
+ spinner2.update(`Provisioning the development database... (${status}, ${Math.round(elapsedMs / 1e3)}s)`);
2567
+ options.onStatus?.(status, elapsedMs);
2568
+ };
2569
+ let current2 = await client.ensureDevDatabase(appId);
2570
+ report(current2.status);
2571
+ if (current2.status === "ready") return current2;
2572
+ if (current2.status === "error") throw provisioningFailedError(current2);
2573
+ let notFoundSince;
2574
+ for (; ; ) {
2575
+ if (now() - start >= timeoutMs) throw provisioningTimeoutError();
2576
+ await sleep(intervalMs);
2577
+ const fetched = await client.getDevDatabase(appId);
2578
+ if (fetched === null) {
2579
+ if (notFoundSince === void 0) notFoundSince = now();
2580
+ if (now() - notFoundSince >= NOT_FOUND_GRACE_MS) throw provisioningNotFoundError();
2581
+ report("provisioning");
2582
+ continue;
2583
+ }
2584
+ notFoundSince = void 0;
2585
+ current2 = fetched;
2586
+ report(current2.status);
2587
+ if (current2.status === "ready") return current2;
2588
+ if (current2.status === "error") throw provisioningFailedError(current2);
2589
+ }
2590
+ }
2591
+ async function ensureDevDatabaseReady(client, appId, options = {}) {
2592
+ return withSpinner(
2593
+ "Provisioning the development database...",
2594
+ (spinner2) => pollDevDatabase(client, appId, options, spinner2),
2595
+ { success: (db) => `Development database "${db.database}" is ready.` }
2596
+ );
2597
+ }
2598
+ function toProxyToken(token) {
2599
+ return {
2600
+ access_token: token.access_token,
2601
+ ...typeof token.expires_at === "string" ? { expires_at: token.expires_at } : {}
2602
+ };
2603
+ }
2604
+ function formatMb(bytes) {
2605
+ return (bytes / (1024 * 1024)).toFixed(1);
2606
+ }
2607
+ function firstNonEmpty(...values) {
2608
+ for (const value of values) {
2609
+ if (typeof value === "string" && value.trim() !== "") return value;
2610
+ }
2611
+ return void 0;
2612
+ }
2613
+ function devDatabaseIdValue(devDatabase) {
2614
+ return firstNonEmpty(String(devDatabase.id));
2615
+ }
2616
+ function logEnvPatch(result, databaseUrl, wroteShadow) {
2617
+ const redacted = redactDatabaseUrl(databaseUrl);
2618
+ const names = wroteShadow ? "DATABASE_URL and SHADOW_DATABASE_URL" : "DATABASE_URL";
2619
+ if (result.created) {
2620
+ success(`Created ${result.path} with DATABASE_URL=${redacted}`);
2621
+ } else if (result.changed) {
2622
+ success(`Updated ${names} in ${result.path} (${redacted})`);
2623
+ } else {
2624
+ debug(`${result.path} already has the correct ${names}.`);
2625
+ }
2626
+ if (result.backedUp) detail(`Backed up the previous file to ${result.path}.backup`);
2627
+ }
2628
+ function handleProxyEvent(event) {
2629
+ switch (event.type) {
2630
+ case "starting":
2631
+ debug(`Starting the Cloud SQL Auth Proxy (attempt ${event.attempt}).`);
2632
+ return;
2633
+ case "ready":
2634
+ debug(`Cloud SQL Auth Proxy listening on port ${event.port}.`);
2635
+ return;
2636
+ case "log":
2637
+ debug(event.line);
2638
+ return;
2639
+ case "token-refreshed":
2640
+ step("Refreshed the Cloud SQL Auth Proxy token.");
2641
+ return;
2642
+ case "restarting":
2643
+ if (event.reason === "crash") warn("The Cloud SQL Auth Proxy crashed; restarting it.");
2644
+ else debug("Restarting the Cloud SQL Auth Proxy with a refreshed token...");
2645
+ return;
2646
+ case "exit":
2647
+ debug(`Cloud SQL Auth Proxy exited (code ${event.code ?? "null"}, signal ${event.signal ?? "null"}).`);
2648
+ return;
2649
+ case "error":
2650
+ warn(`Cloud SQL Auth Proxy error: ${event.message} Restart \`meridian dev\` if the database becomes unreachable.`);
2651
+ return;
2652
+ }
2653
+ }
2654
+ async function startDevSession(options = {}) {
2655
+ const deps = resolveDeps(options.deps);
2656
+ let proxy;
2657
+ try {
2658
+ const project = options.project ?? requireProject(workingDirectory());
2659
+ if (project.config.skip_datasource_check !== true) assertManagedDatasource(project.root);
2660
+ const client = options.client ?? createApiClient();
2661
+ const appId = project.config.app_id;
2662
+ const devDatabase = await ensureDevDatabaseReady(client, appId, { sleep: deps.sleep, now: deps.now });
2663
+ const credentials = await withSpinner(
2664
+ "Fetching database credentials...",
2665
+ () => client.issueCredentials(appId),
2666
+ {
2667
+ success: (creds) => `Fetched credentials for database "${creds.database}"${typeof creds.expires_at === "string" ? ` (expire ${creds.expires_at})` : ""}.`
2668
+ }
2669
+ );
2670
+ const proxyToken = await withSpinner(
2671
+ "Requesting a Cloud SQL Auth Proxy token...",
2672
+ () => client.issueProxyToken(appId),
2673
+ { success: "Issued a Cloud SQL Auth Proxy token." }
2674
+ );
2675
+ const binaryPath = await withSpinner(
2676
+ "Preparing the Cloud SQL Auth Proxy...",
2677
+ (spinner2) => {
2678
+ const ensureOptions = {
2679
+ onProgress: (received, total) => {
2680
+ spinner2.update(
2681
+ total !== void 0 ? `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)}/${formatMb(total)} MB)` : `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)} MB)`
2682
+ );
2683
+ }
2684
+ };
2685
+ return deps.ensureProxyBinary(ensureOptions);
2686
+ },
2687
+ { success: "Cloud SQL Auth Proxy is ready." }
2688
+ );
2689
+ const findPortOptions = { start: DEFAULT_PORT_START };
2690
+ if (options.port !== void 0) findPortOptions.preferred = options.port;
2691
+ const port = await deps.findFreePort(findPortOptions);
2692
+ if (options.port !== void 0 && port !== options.port) {
2693
+ throw new CliError(`Port ${options.port} is already in use.`, {
2694
+ hint: "Pick a different port with --port, or free up that port and try again."
2695
+ });
2696
+ }
2697
+ const instanceConnectionName = firstNonEmpty(
2698
+ credentials.instance_connection_name,
2699
+ proxyToken.instance_connection_name,
2700
+ devDatabase.instance_connection_name
2701
+ );
2702
+ if (instanceConnectionName === void 0) {
2703
+ throw new CliError("The Meridian API did not return a Cloud SQL instance connection name.", {
2704
+ hint: "Try again, or contact Meridian support if this persists."
2705
+ });
2706
+ }
2707
+ proxy = await withSpinner(
2708
+ `Starting the Cloud SQL Auth Proxy on 127.0.0.1:${port}...`,
2709
+ () => deps.startProxy({
2710
+ binaryPath,
2711
+ instanceConnectionName,
2712
+ token: toProxyToken(proxyToken),
2713
+ port,
2714
+ ...options.refreshToken !== false ? { refreshToken: async () => toProxyToken(await client.issueProxyToken(appId)) } : {},
2715
+ onEvent: handleProxyEvent
2716
+ }),
2717
+ { success: `Cloud SQL Auth Proxy listening on 127.0.0.1:${port}.` }
2718
+ );
2719
+ const connection = connectionInfoFromCredentials(credentials, { host: proxy.host, port: proxy.port });
2720
+ if (options.waitForDb !== false) {
2721
+ await withSpinner(
2722
+ "Waiting for MySQL to accept connections...",
2723
+ () => deps.waitForDatabase(connection),
2724
+ { success: "MySQL is accepting connections." }
2725
+ );
2726
+ }
2727
+ const databaseUrl = buildDatabaseUrl({
2728
+ username: credentials.username,
2729
+ password: credentials.password,
2730
+ port: proxy.port,
2731
+ database: credentials.database
2732
+ });
2733
+ const shadowDatabase = firstNonEmpty(credentials.shadow_database, devDatabase.shadow_database);
2734
+ const shadowDatabaseUrl = shadowDatabase === void 0 ? void 0 : buildDatabaseUrl({
2735
+ username: credentials.username,
2736
+ password: credentials.password,
2737
+ port: proxy.port,
2738
+ database: shadowDatabase
2739
+ });
2740
+ const devDatabaseId = devDatabaseIdValue(devDatabase);
2741
+ let envPatch;
2742
+ if (options.patchEnv !== false) {
2743
+ const envPath = envFilePath(project.root, options.envFile);
2744
+ const patch = {
2745
+ DATABASE_URL: databaseUrl,
2746
+ ...shadowDatabaseUrl !== void 0 ? { SHADOW_DATABASE_URL: shadowDatabaseUrl } : {}
2747
+ };
2748
+ envPatch = deps.patchEnvFile(envPath, patch, { backup: true });
2749
+ logEnvPatch(envPatch, databaseUrl, shadowDatabaseUrl !== void 0);
2750
+ }
2751
+ let unregisterShutdown;
2752
+ const stop = async () => {
2753
+ if (unregisterShutdown !== void 0) {
2754
+ const unregister = unregisterShutdown;
2755
+ unregisterShutdown = void 0;
2756
+ unregister();
2757
+ }
2758
+ if (proxy !== void 0) await proxy.stop();
2759
+ };
2760
+ if (options.handleSignals !== false) {
2761
+ unregisterShutdown = onShutdown(() => stop());
2762
+ }
2763
+ return {
2764
+ project,
2765
+ devDatabase,
2766
+ credentials,
2767
+ connection,
2768
+ databaseUrl,
2769
+ ...shadowDatabaseUrl !== void 0 ? { shadowDatabaseUrl } : {},
2770
+ proxy,
2771
+ env: {
2772
+ ...process.env,
2773
+ DATABASE_URL: databaseUrl,
2774
+ ...shadowDatabaseUrl !== void 0 ? { SHADOW_DATABASE_URL: shadowDatabaseUrl } : {},
2775
+ ...devDatabaseId !== void 0 ? { [DEV_DATABASE_ID_ENV]: devDatabaseId } : {}
2776
+ },
2777
+ ...envPatch !== void 0 ? { envPatch } : {},
2778
+ stop
2779
+ };
2780
+ } catch (error2) {
2781
+ if (proxy !== void 0) {
2782
+ await proxy.stop().catch(() => {
2783
+ });
2784
+ }
2785
+ if (isCliError(error2)) throw error2;
2786
+ throw new CliError(`Failed to start the Meridian development session: ${describeError(error2)}`, { cause: error2 });
2787
+ }
2788
+ }
2789
+
2790
+ // src/commands/db-shell-repl.ts
2791
+ import { accessSync, constants as fsConstants } from "fs";
2792
+ import { delimiter as delimiter2, join as join5 } from "path";
2793
+ import { createInterface } from "readline";
2794
+ function candidateNames(name) {
2795
+ if (process.platform !== "win32") return [name];
2796
+ if (/\.[^./\\]+$/.test(name)) return [name];
2797
+ const pathExt = process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM";
2798
+ const extensions = pathExt.split(";").filter((ext) => ext !== "");
2799
+ return [name, ...extensions.map((ext) => `${name}${ext.toLowerCase()}`)];
2800
+ }
2801
+ function findExecutable(name) {
2802
+ const pathValue = process.env.PATH ?? "";
2803
+ const dirs = pathValue.split(delimiter2).filter((dir) => dir !== "");
2804
+ const candidates = candidateNames(name);
2805
+ for (const dir of dirs) {
2806
+ for (const candidate of candidates) {
2807
+ const fullPath = join5(dir, candidate);
2808
+ try {
2809
+ accessSync(fullPath, fsConstants.X_OK);
2810
+ return fullPath;
2811
+ } catch {
2812
+ continue;
2813
+ }
2814
+ }
2815
+ }
2816
+ return null;
2817
+ }
2818
+ function isResultSetHeaderLike(value) {
2819
+ return typeof value === "object" && value !== null && "affectedRows" in value && typeof value.affectedRows === "number";
2820
+ }
2821
+ function interpretQueryResult(raw) {
2822
+ const first = Array.isArray(raw) ? raw[0] : raw;
2823
+ if (Array.isArray(first)) {
2824
+ return { kind: "rows", rows: first };
2825
+ }
2826
+ if (isResultSetHeaderLike(first)) {
2827
+ return { kind: "write", affectedRows: first.affectedRows };
2828
+ }
2829
+ return { kind: "write", affectedRows: 0 };
2830
+ }
2831
+ var MAX_CELL_WIDTH = 60;
2832
+ function cellText(value) {
2833
+ if (value === null || value === void 0) return "NULL";
2834
+ if (value instanceof Date) return value.toISOString();
2835
+ if (Buffer.isBuffer(value)) return value.toString("hex");
2836
+ return String(value);
2837
+ }
2838
+ function truncateCell(text) {
2839
+ return text.length > MAX_CELL_WIDTH ? `${text.slice(0, MAX_CELL_WIDTH - 1)}\u2026` : text;
2840
+ }
2841
+ function formatTable(rows) {
2842
+ if (rows.length === 0) return "(0 rows)";
2843
+ const columns = Object.keys(rows[0] ?? {});
2844
+ const cells = rows.map((row) => columns.map((column) => truncateCell(cellText(row[column]))));
2845
+ const widths = columns.map((column, i) => Math.max(column.length, ...cells.map((row) => row[i]?.length ?? 0)));
2846
+ const renderRow = (values) => values.map((value, i) => value.padEnd(widths[i] ?? 0)).join(" ");
2847
+ const lines = [
2848
+ renderRow(columns),
2849
+ renderRow(widths.map((width) => "-".repeat(width))),
2850
+ ...cells.map((row) => renderRow(row)),
2851
+ `(${rows.length} row${rows.length === 1 ? "" : "s"})`
2852
+ ];
2853
+ return lines.join("\n");
2854
+ }
2855
+ async function printQueryOutcome(raw, json) {
2856
+ const outcome = interpretQueryResult(raw);
2857
+ if (outcome.kind === "rows") {
2858
+ if (json) printJson(outcome.rows);
2859
+ else printLine(formatTable(outcome.rows));
2860
+ return;
2861
+ }
2862
+ if (json) printJson({ affected_rows: outcome.affectedRows });
2863
+ else printLine(`Query OK, ${outcome.affectedRows} row${outcome.affectedRows === 1 ? "" : "s"} affected.`);
2864
+ }
2865
+ async function runStatement(connection, connectionInfo, sql, json = false) {
2866
+ try {
2867
+ const raw = await connection.query(sql);
2868
+ await printQueryOutcome(raw, json);
2869
+ return true;
2870
+ } catch (err) {
2871
+ const dbError = toDatabaseError(err, connectionInfo);
2872
+ error(dbError.message);
2873
+ if (dbError.hint !== void 0) hint(dbError.hint);
2874
+ return false;
2875
+ }
2876
+ }
2877
+ async function executeStatement(connection, connectionInfo, sql, options = {}) {
2878
+ const ok = await runStatement(connection, connectionInfo, sql, options.json === true);
2879
+ return ok ? 0 : 1;
2880
+ }
2881
+ var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "\\q"]);
2882
+ function runRepl(options) {
2883
+ const { connection, connectionInfo, input: input2, output } = options;
2884
+ const rl = createInterface({ input: input2, output, prompt: `meridian(${connectionInfo.database})> ` });
2885
+ let buffer = "";
2886
+ let settled = false;
2887
+ return new Promise((resolve2) => {
2888
+ const finish = (code) => {
2889
+ if (settled) return;
2890
+ settled = true;
2891
+ rl.close();
2892
+ resolve2(code);
2893
+ };
2894
+ rl.on("line", (line) => {
2895
+ const trimmed = line.trim();
2896
+ if (buffer === "" && EXIT_COMMANDS.has(trimmed.toLowerCase())) {
2897
+ finish(0);
2898
+ return;
2899
+ }
2900
+ buffer = buffer === "" ? line : `${buffer}
2901
+ ${line}`;
2902
+ if (!trimmed.endsWith(";")) {
2903
+ if (!settled) rl.prompt();
2904
+ return;
2905
+ }
2906
+ const statement = buffer;
2907
+ buffer = "";
2908
+ void runStatement(connection, connectionInfo, statement).then(() => {
2909
+ if (!settled) rl.prompt();
2910
+ });
2911
+ });
2912
+ rl.on("SIGINT", () => {
2913
+ if (buffer !== "") {
2914
+ buffer = "";
2915
+ output.write("\n");
2916
+ if (!settled) rl.prompt();
2917
+ } else {
2918
+ finish(0);
2919
+ }
2920
+ });
2921
+ rl.once("close", () => finish(0));
2922
+ rl.prompt();
2923
+ });
2924
+ }
2925
+
2926
+ // src/commands/db.ts
2927
+ function defaultDbDeps() {
2928
+ return {
2929
+ startDevSession,
2930
+ ensureDevDatabaseReady,
2931
+ createApiClient,
2932
+ runNpx,
2933
+ runProcess,
2934
+ createConnection: createSqlConnection,
2935
+ findExecutable,
2936
+ requireProject
2937
+ };
2938
+ }
2939
+ function hasSeedConfigured(root) {
2940
+ let parsed;
2941
+ try {
2942
+ parsed = JSON.parse(readFileSync7(join6(root, "package.json"), "utf8"));
2943
+ } catch {
2944
+ return false;
2945
+ }
2946
+ const prismaSeed = parsed.prisma?.seed;
2947
+ if (typeof prismaSeed === "string" && prismaSeed !== "") return true;
2948
+ const seedScript = parsed.scripts?.seed;
2949
+ return typeof seedScript === "string" && seedScript !== "";
2950
+ }
2951
+ async function runDbReset(options = {}) {
2952
+ const deps = { ...defaultDbDeps(), ...options.deps };
2953
+ const json = isJsonOutput();
2954
+ const project = deps.requireProject(workingDirectory());
2955
+ const client = deps.createApiClient({});
2956
+ const appId = project.config.app_id;
2957
+ const devDatabase = await deps.ensureDevDatabaseReady(client, appId);
2958
+ const database = devDatabase.database;
2959
+ const confirmed = await confirmDestructive({
2960
+ message: `This will drop every table in ${database}. Type the database name to confirm.`,
2961
+ phrase: database,
2962
+ assumeYes: isAssumeYes(),
2963
+ ...deps.destructiveImpl !== void 0 ? { promptImpl: deps.destructiveImpl } : {}
2964
+ });
2965
+ if (!confirmed) {
2966
+ log("Aborted.");
2967
+ if (json) {
2968
+ printJson({
2969
+ reset: false,
2970
+ database,
2971
+ app_id: appId,
2972
+ migrated: false,
2973
+ seeded: false,
2974
+ aborted: true
2975
+ });
2976
+ }
2977
+ return 0;
2978
+ }
2979
+ await withSpinner(`Resetting ${database}...`, () => client.resetDevDatabase(appId), {
2980
+ success: `Reset ${database}.`
2981
+ });
2982
+ let wantMigrate = options.migrate === true;
2983
+ let wantSeed = options.seed === true;
2984
+ if (!wantMigrate && !wantSeed) {
2985
+ if (isInteractive()) {
2986
+ wantMigrate = await confirmAction({
2987
+ message: "Run `npx prisma migrate deploy` now?",
2988
+ default: false,
2989
+ ...deps.confirmImpl !== void 0 ? { promptImpl: deps.confirmImpl } : {}
2990
+ });
2991
+ if (hasSeedConfigured(project.root)) {
2992
+ wantSeed = await confirmAction({
2993
+ message: "Seed the database now?",
2994
+ default: false,
2995
+ ...deps.confirmImpl !== void 0 ? { promptImpl: deps.confirmImpl } : {}
2996
+ });
2997
+ }
2998
+ } else {
2999
+ hint("Run `npx prisma migrate deploy` and `npx prisma db seed` to finish setting up the database.");
3000
+ }
3001
+ }
3002
+ let migrated = false;
3003
+ let seeded = false;
3004
+ let followUpExitCode = 0;
3005
+ if (wantMigrate || wantSeed) {
3006
+ let session;
3007
+ try {
3008
+ session = await deps.startDevSession({
3009
+ ...options.port !== void 0 ? { port: options.port } : {},
3010
+ ...options.envFile !== void 0 ? { envFile: options.envFile } : {}
3011
+ });
3012
+ if (wantMigrate) {
3013
+ const result = await deps.runNpx(["prisma", "migrate", "deploy"], {
3014
+ cwd: session.project.root,
3015
+ env: session.env,
3016
+ inherit: true
3017
+ });
3018
+ migrated = result.code === 0;
3019
+ if (result.code !== 0) {
3020
+ error(`\`npx prisma migrate deploy\` exited with code ${result.code}.`);
3021
+ followUpExitCode = result.code;
3022
+ }
3023
+ }
3024
+ if (followUpExitCode === 0 && wantSeed) {
3025
+ const result = await deps.runNpx(["prisma", "db", "seed"], {
3026
+ cwd: session.project.root,
3027
+ env: session.env,
3028
+ inherit: true
3029
+ });
3030
+ seeded = result.code === 0;
3031
+ if (result.code !== 0) {
3032
+ error(`\`npx prisma db seed\` exited with code ${result.code}.`);
3033
+ hint("Make sure `prisma.seed` (or your Prisma 6 seed config) is set in package.json.");
3034
+ followUpExitCode = result.code;
3035
+ }
3036
+ }
3037
+ } finally {
3038
+ if (session !== void 0) await session.stop();
3039
+ }
3040
+ }
3041
+ if (json) {
3042
+ printJson({ reset: true, database, app_id: appId, migrated, seeded, aborted: false });
3043
+ }
3044
+ return followUpExitCode;
3045
+ }
3046
+ async function runDbSeed(options = {}) {
3047
+ const deps = { ...defaultDbDeps(), ...options.deps };
3048
+ const json = isJsonOutput();
3049
+ let session;
3050
+ try {
3051
+ session = await deps.startDevSession({
3052
+ ...options.port !== void 0 ? { port: options.port } : {},
3053
+ ...options.envFile !== void 0 ? { envFile: options.envFile } : {}
3054
+ });
3055
+ const args = ["prisma", "db", "seed", ...options.extraArgs ?? []];
3056
+ const result = await deps.runNpx(args, {
3057
+ cwd: session.project.root,
3058
+ env: session.env,
3059
+ inherit: true
3060
+ });
3061
+ if (result.code === 0) {
3062
+ success("Seeded the development database.");
3063
+ } else {
3064
+ error(`\`npx prisma db seed\` exited with code ${result.code}.`);
3065
+ hint("Make sure `prisma.seed` (or your Prisma 6 seed config) is set in package.json.");
3066
+ }
3067
+ if (json) printJson({ seeded: result.code === 0, exit_code: result.code });
3068
+ return result.code;
3069
+ } finally {
3070
+ if (session !== void 0) await session.stop();
3071
+ }
3072
+ }
3073
+ async function runDbShell(options = {}) {
3074
+ const deps = { ...defaultDbDeps(), ...options.deps };
3075
+ const json = isJsonOutput();
3076
+ let session;
3077
+ try {
3078
+ session = await deps.startDevSession({
3079
+ waitForDb: true,
3080
+ ...options.port !== void 0 ? { port: options.port } : {},
3081
+ ...options.envFile !== void 0 ? { envFile: options.envFile } : {},
3082
+ ...options.env === false ? { patchEnv: false } : {}
3083
+ });
3084
+ const info = session.connection;
3085
+ const wantRepl = options.repl === true;
3086
+ const mysqlPath = wantRepl ? null : deps.findExecutable(process.platform === "win32" ? "mysql.exe" : "mysql");
3087
+ if (mysqlPath !== null) {
3088
+ step(`Connecting to ${style.bold(info.database)} on ${info.host}:${info.port} with the mysql client...`);
3089
+ const args = ["--host", info.host, "--port", String(info.port), "--user", info.user, "--database", info.database];
3090
+ if (options.execute !== void 0) args.push(`--execute=${options.execute}`);
3091
+ const env = { ...session.env, MYSQL_PWD: info.password };
3092
+ const result = await deps.runProcess(mysqlPath, args, { cwd: session.project.root, env, inherit: true });
3093
+ return result.code;
3094
+ }
3095
+ step(`Connecting to ${style.bold(info.database)} on ${info.host}:${info.port}...`);
3096
+ return await withConnection(
3097
+ info,
3098
+ async (connection) => {
3099
+ if (options.execute !== void 0) {
3100
+ return executeStatement(connection, info, options.execute, { json });
3101
+ }
3102
+ return runRepl({ connection, connectionInfo: info, input: process.stdin, output: process.stdout });
3103
+ },
3104
+ { factory: deps.createConnection }
3105
+ );
3106
+ } finally {
3107
+ if (session !== void 0) await session.stop();
3108
+ }
3109
+ }
3110
+ async function runAndSetExitCode(fn) {
3111
+ const code = await fn();
3112
+ if (code !== 0) process.exitCode = code;
3113
+ }
3114
+ function registerDbCommand(program) {
3115
+ const db = program.command("db").description("Manage the Meridian-managed MySQL development database: reset, seed, or open a SQL shell.");
3116
+ db.action(() => {
3117
+ db.outputHelp();
3118
+ });
3119
+ db.command("reset").description("Drop and recreate every table in the development database (keeps the MySQL user).").option("--migrate", "run `npx prisma migrate deploy` afterwards, without asking").option("--seed", "run `npx prisma db seed` afterwards, without asking").option("--port <port>", "local port for the Cloud SQL Auth Proxy", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to update (defaults to .env)").action(async (opts) => {
3120
+ await runAndSetExitCode(
3121
+ () => runDbReset({
3122
+ ...opts.migrate === true ? { migrate: true } : {},
3123
+ ...opts.seed === true ? { seed: true } : {},
3124
+ ...opts.port !== void 0 ? { port: opts.port } : {},
3125
+ ...opts.envFile !== void 0 ? { envFile: opts.envFile } : {}
3126
+ })
3127
+ );
3128
+ });
3129
+ db.command("seed").description("Run `npx prisma db seed` with the Cloud SQL Auth Proxy up and DATABASE_URL set.").option("--port <port>", "local port for the Cloud SQL Auth Proxy", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to update (defaults to .env)").passThroughOptions().argument("[args...]", "extra arguments forwarded to `npx prisma db seed` (put them after `--`)").action(async (args, opts) => {
3130
+ await runAndSetExitCode(
3131
+ () => runDbSeed({
3132
+ ...opts.port !== void 0 ? { port: opts.port } : {},
3133
+ ...opts.envFile !== void 0 ? { envFile: opts.envFile } : {},
3134
+ ...args.length > 0 ? { extraArgs: args } : {}
3135
+ })
3136
+ );
3137
+ });
3138
+ db.command("shell").description("Open a SQL shell against the development database (the `mysql` client if available, otherwise a built-in REPL).").option("--port <port>", "local port for the Cloud SQL Auth Proxy", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to read/patch (defaults to .env)").option("--repl", "force the built-in SQL REPL even when `mysql` is on PATH").option("-e, --execute <sql>", "run one SQL statement and exit").option("--no-env", "do not patch DATABASE_URL into the project's .env file").action(async (opts) => {
3139
+ await runAndSetExitCode(
3140
+ () => runDbShell({
3141
+ ...opts.port !== void 0 ? { port: opts.port } : {},
3142
+ ...opts.envFile !== void 0 ? { envFile: opts.envFile } : {},
3143
+ ...opts.repl === true ? { repl: true } : {},
3144
+ ...opts.execute !== void 0 ? { execute: opts.execute } : {},
3145
+ ...opts.env === false ? { env: false } : {}
3146
+ })
3147
+ );
3148
+ });
3149
+ }
3150
+
3151
+ // src/commands/dev.ts
3152
+ function defaultDeps() {
3153
+ return { startDevSession, startProcess, readPackageScript, requireProject };
3154
+ }
3155
+ var RECURSION_HINT = "Set `dev_command` in `.meridian/project.json` to the underlying command (e.g. `next dev`), or run `meridian dev -- next dev` to bypass the script entirely.";
3156
+ function selfInvokingScriptError() {
3157
+ return new CliError('This project\'s "dev" script runs `meridian dev`, which would start it again in a loop.', {
3158
+ hint: RECURSION_HINT
3159
+ });
3160
+ }
3161
+ function nestedSessionError() {
3162
+ return new CliError("`meridian dev` is already running in this process tree.", {
3163
+ hint: `Use the dev server that session started, or stop it first. ${RECURSION_HINT}`
3164
+ });
3165
+ }
3166
+ function scriptReentersMeridianDev(script) {
3167
+ try {
3168
+ assertNotSelfInvoking(script);
3169
+ return false;
3170
+ } catch {
3171
+ return true;
3172
+ }
3173
+ }
3174
+ function resolvePassthroughCommand(argv) {
3175
+ const [command, ...args] = argv;
3176
+ return { command, args, display: argv.join(" ") };
3177
+ }
3178
+ function resolveConfiguredCommand(devCommand) {
3179
+ assertNotSelfInvoking(devCommand);
3180
+ const { command, args } = parseCommandString(devCommand);
3181
+ return { command, args, display: devCommand };
3182
+ }
3183
+ function noDevScriptError() {
3184
+ return new CliError('This project has no "dev" script.', {
3185
+ hint: "Run `meridian dev -- <your command>`, or set `dev_command` in `.meridian/project.json`."
3186
+ });
3187
+ }
3188
+ function resolveDefaultCommand(deps, root) {
3189
+ const script = deps.readPackageScript(root, "dev");
3190
+ if (script === null) throw noDevScriptError();
3191
+ if (scriptReentersMeridianDev(script)) throw selfInvokingScriptError();
3192
+ const display = "npm run dev";
3193
+ assertNotSelfInvoking(display);
3194
+ return { command: "npm", args: ["run", "dev"], display };
3195
+ }
3196
+ function resolveDevCommand(deps, project, passthrough) {
3197
+ if (passthrough !== void 0 && passthrough.length > 0) return resolvePassthroughCommand(passthrough);
3198
+ if (project.config.dev_command !== void 0) return resolveConfiguredCommand(project.config.dev_command);
3199
+ return resolveDefaultCommand(deps, project.root);
3200
+ }
3201
+ function appLabel(project) {
3202
+ return project.config.app_name ?? String(project.config.app_id);
3203
+ }
3204
+ function printDevSummary(session, noEnv) {
3205
+ const envEntry = noEnv ? "not touched (--no-env)" : session.envPatch !== void 0 ? session.envPatch.path : "unchanged";
3206
+ const entries = [
3207
+ ["App", appLabel(session.project)],
3208
+ ["Database", session.devDatabase.database],
3209
+ ["Proxy", `127.0.0.1:${session.proxy.port}`],
3210
+ ["Env file", envEntry],
3211
+ ["DATABASE_URL", redactDatabaseUrl(session.databaseUrl)]
3212
+ ];
3213
+ if (typeof session.credentials.expires_at === "string") {
3214
+ entries.push(["Credentials expire", session.credentials.expires_at]);
3215
+ }
3216
+ log();
3217
+ keyValues(entries);
3218
+ log();
3219
+ }
3220
+ function buildDevJsonPayload(session) {
3221
+ return {
3222
+ app: appLabel(session.project),
3223
+ database: session.devDatabase.database,
3224
+ proxy: { host: session.proxy.host, port: session.proxy.port },
3225
+ database_url: redactDatabaseUrl(session.databaseUrl),
3226
+ env_file: session.envPatch?.path ?? null,
3227
+ credentials_expire_at: session.credentials.expires_at ?? null
3228
+ };
3229
+ }
3230
+ async function runDev(options = {}) {
3231
+ const deps = { ...defaultDeps(), ...options.deps };
3232
+ if (isInsideDevSession()) throw nestedSessionError();
3233
+ const project = deps.requireProject(workingDirectory());
3234
+ const resolved = resolveDevCommand(deps, project, options.command);
3235
+ const json = isJsonOutput();
3236
+ const sessionOptions = {
3237
+ project,
3238
+ patchEnv: options.noEnv !== true,
3239
+ ...options.port !== void 0 ? { port: options.port } : {},
3240
+ ...options.envFile !== void 0 ? { envFile: options.envFile } : {}
3241
+ };
3242
+ const session = await deps.startDevSession(sessionOptions);
3243
+ let unregisterShutdown;
3244
+ try {
3245
+ if (json) {
3246
+ printJson(buildDevJsonPayload(session));
3247
+ } else {
3248
+ printDevSummary(session, options.noEnv === true);
3249
+ step(`Starting ${resolved.display}...`);
3250
+ }
3251
+ const child = deps.startProcess(resolved.command, resolved.args, {
3252
+ cwd: project.root,
3253
+ env: session.env,
3254
+ inherit: true
3255
+ });
3256
+ unregisterShutdown = onShutdown(async () => {
3257
+ await child.stop();
3258
+ await session.stop();
3259
+ });
3260
+ const result = await child.exited;
3261
+ unregisterShutdown();
3262
+ unregisterShutdown = void 0;
3263
+ await session.stop();
3264
+ if (result.signal !== null) return exitCodeForSignal(result.signal);
3265
+ if (result.code !== 0) log(style.dim(`${resolved.display} exited with code ${result.code}.`));
3266
+ return result.code;
3267
+ } catch (error2) {
3268
+ unregisterShutdown?.();
3269
+ await session.stop().catch(() => {
3270
+ });
3271
+ throw error2;
3272
+ }
3273
+ }
3274
+ function registerDevCommand(program) {
3275
+ program.command("dev").description("Run the managed dev database proxy and your app's dev server together.").option("--port <port>", "preferred local port for the Cloud SQL Auth Proxy").option("--env-file <file>", "the .env file to patch, relative to the project root (default .env)").option("--no-env", "don't touch any .env file; just print the DATABASE_URL for manual use").argument("[command...]", "run this instead of the app's dev script, e.g. `meridian dev -- next dev`").passThroughOptions().action(async (command, opts) => {
3276
+ const port = parsePortOption(opts.port);
3277
+ const code = await runDev({
3278
+ ...command.length > 0 ? { command } : {},
3279
+ ...port !== void 0 ? { port } : {},
3280
+ ...opts.envFile !== void 0 ? { envFile: opts.envFile } : {},
3281
+ ...opts.env === false ? { noEnv: true } : {}
3282
+ });
3283
+ process.exitCode = code;
3284
+ });
3285
+ }
3286
+
3287
+ // src/commands/link.ts
3288
+ import { existsSync as existsSync5 } from "fs";
3289
+ import { join as join7 } from "path";
3290
+ function defaultDeps2() {
3291
+ return { createClient: createApiClient };
3292
+ }
3293
+ function formatOrg(org) {
3294
+ return `${org.name} (${org.slug})`;
3295
+ }
3296
+ function formatApp(app) {
3297
+ return `${app.name} (${app.slug})`;
3298
+ }
3299
+ function resolveChoiceAnswer(choices, raw, keysOf) {
3300
+ const target = raw.trim().toLowerCase();
3301
+ const match = choices.find((choice) => keysOf(choice.value).some((key) => key.toLowerCase() === target));
3302
+ return match !== void 0 ? match.name : raw;
3303
+ }
3304
+ async function runLink(options = {}) {
3305
+ const deps = { ...defaultDeps2(), ...options.deps };
3306
+ const json = isJsonOutput();
3307
+ const root = resolveProjectRoot(workingDirectory()) ?? workingDirectory();
3308
+ if (!existsSync5(join7(root, "package.json"))) {
3309
+ log(`No package.json found in ${style.path(root)}.`);
3310
+ const proceed = await confirmAction({
3311
+ message: "Link this directory to a Meridian app anyway?",
3312
+ flag: "--force",
3313
+ ...options.force === true ? { assumeYes: true } : {},
3314
+ ...deps.confirmImpl !== void 0 ? { promptImpl: deps.confirmImpl } : {}
3315
+ });
3316
+ if (!proceed) throw new CliError("Aborted.");
3317
+ }
3318
+ const existing = readProject(root);
3319
+ if (existing !== null) {
3320
+ log("This directory is already linked:");
3321
+ keyValues([
3322
+ ["App", existing.config.app_name ?? String(existing.config.app_id)],
3323
+ ["Org", String(existing.config.org_id)]
3324
+ ]);
3325
+ const relink = await confirmAction({
3326
+ message: "Re-link this project?",
3327
+ flag: "--force",
3328
+ ...options.force === true ? { assumeYes: true } : {},
3329
+ ...deps.confirmImpl !== void 0 ? { promptImpl: deps.confirmImpl } : {}
3330
+ });
3331
+ if (!relink) throw new CliError("Aborted.");
3332
+ }
3333
+ const client = deps.createClient({});
3334
+ const me = await client.getMe();
3335
+ const orgs = me.orgs;
3336
+ if (orgs.length === 0) {
3337
+ throw new CliError("Your Meridian account has no organizations yet.", {
3338
+ hint: "Create one in the Meridian dashboard, then run `meridian link` again."
3339
+ });
3340
+ }
3341
+ const orgChoices = orgs.map((org2) => ({ value: org2, name: formatOrg(org2) }));
3342
+ const orgCandidate = options.org ?? process.env.MERIDIAN_ORG;
3343
+ const orgAnswer = orgCandidate !== void 0 ? resolveChoiceAnswer(orgChoices, orgCandidate, (org2) => [String(org2.id), org2.name, org2.slug]) : void 0;
3344
+ const org = await selectOne({
3345
+ message: "Select an organization",
3346
+ choices: orgChoices,
3347
+ envVar: "MERIDIAN_ORG",
3348
+ flag: "--org",
3349
+ ...orgAnswer !== void 0 ? { answer: orgAnswer } : {},
3350
+ ...deps.selectImpl !== void 0 ? { promptImpl: deps.selectImpl } : {}
3351
+ });
3352
+ log(`Organization: ${style.bold(formatOrg(org))}`);
3353
+ const apps = await client.listApps(org.id);
3354
+ if (apps.length === 0) {
3355
+ throw new CliError(`Organization "${org.name}" has no apps yet.`, {
3356
+ hint: "Create an app in the Meridian dashboard, then run `meridian link` again."
3357
+ });
3358
+ }
3359
+ const appChoices = apps.map((app2) => ({ value: app2, name: formatApp(app2) }));
3360
+ const appCandidate = options.app ?? process.env.MERIDIAN_APP;
3361
+ const appAnswer = appCandidate !== void 0 ? resolveChoiceAnswer(appChoices, appCandidate, (app2) => [String(app2.id), app2.name, app2.slug]) : void 0;
3362
+ const app = await selectOne({
3363
+ message: "Select an app",
3364
+ choices: appChoices,
3365
+ envVar: "MERIDIAN_APP",
3366
+ flag: "--app",
3367
+ ...appAnswer !== void 0 ? { answer: appAnswer } : {},
3368
+ ...deps.selectImpl !== void 0 ? { promptImpl: deps.selectImpl } : {}
3369
+ });
3370
+ log(`App: ${style.bold(formatApp(app))}`);
3371
+ const config = {
3372
+ org_id: org.id,
3373
+ app_id: app.id,
3374
+ app_name: app.name,
3375
+ ...options.devCommand !== void 0 ? { dev_command: options.devCommand } : {}
3376
+ };
3377
+ writeProject(root, config);
3378
+ if (json) {
3379
+ printJson({ ...config, root });
3380
+ return;
3381
+ }
3382
+ success(`Linked ${style.path(root)} to ${app.name}.`);
3383
+ keyValues([
3384
+ ["Org", formatOrg(org)],
3385
+ ["App", formatApp(app)],
3386
+ ["Root", root]
3387
+ ]);
3388
+ log();
3389
+ hint("Run `meridian dev` to start your local dev server against the managed database.");
3390
+ }
3391
+ function registerLinkCommand(program) {
3392
+ program.command("link").description("Link this project to a Meridian organization and app.").option("--org <idOrName>", "organization to link (id, name, or slug)").option("--app <idOrName>", "app to link (id, name, or slug)").option("--dev-command <cmd>", "override the command `meridian dev` runs (defaults to `npm run dev`)").option("--force", "overwrite an existing link, and skip the missing-package.json warning, without prompting").action(async (opts) => {
3393
+ await runLink({
3394
+ ...opts.org !== void 0 ? { org: opts.org } : {},
3395
+ ...opts.app !== void 0 ? { app: opts.app } : {},
3396
+ ...opts.devCommand !== void 0 ? { devCommand: opts.devCommand } : {},
3397
+ ...opts.force === true ? { force: true } : {}
3398
+ });
3399
+ });
3400
+ }
3401
+
3402
+ // src/commands/login.ts
3403
+ import { hostname as osHostname3 } from "os";
3404
+
3405
+ // src/auth/device-flow.ts
3406
+ import { hostname as osHostname2 } from "os";
3407
+ var DEFAULT_CLIENT_NAME2 = "meridian-cli";
3408
+ var DEFAULT_INTERVAL_SECONDS = 5;
3409
+ var DEFAULT_EXPIRES_IN_SECONDS = 900;
3410
+ var SLOW_DOWN_INCREMENT_MS = 5e3;
3411
+ var MAX_CONSECUTIVE_TRANSIENT_FAILURES = 3;
3412
+ var DEFAULT_MAX_WAIT_MS = 15 * 60 * 1e3;
3413
+ function defaultSleep2(ms) {
3414
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
3415
+ }
3416
+ function deniedError() {
3417
+ return new CliError("Authorization was denied in the browser.", {
3418
+ hint: "Run `meridian login` again if you want to retry."
3419
+ });
3420
+ }
3421
+ function expiredError(expiresInSeconds) {
3422
+ const minutes = Math.max(1, Math.round(expiresInSeconds / 60));
3423
+ return new CliError("The login request expired before it was approved.", {
3424
+ hint: `Run \`meridian login\` again and approve it in the browser within ${minutes} minute${minutes === 1 ? "" : "s"}.`
3425
+ });
3426
+ }
3427
+ function timedOutError() {
3428
+ return new CliError("Timed out waiting for the login to be approved.", {
3429
+ hint: "Run `meridian login` again and approve the request promptly in the browser."
3430
+ });
3431
+ }
3432
+ function invalidGrantError() {
3433
+ return new CliError("This login request is no longer valid.", {
3434
+ hint: "Run `meridian login` again to start a new one."
3435
+ });
3436
+ }
3437
+ function isTransientFailure(error2) {
3438
+ if (isApiError(error2)) return error2.status >= 500;
3439
+ return isCliError(error2);
3440
+ }
3441
+ async function runDeviceFlow(options) {
3442
+ const { client } = options;
3443
+ const sleep = options.sleep ?? defaultSleep2;
3444
+ const now = options.now ?? Date.now;
3445
+ const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
3446
+ const code = await client.requestDeviceCode({
3447
+ clientName: options.clientName ?? DEFAULT_CLIENT_NAME2,
3448
+ hostname: options.hostname ?? osHostname2()
3449
+ });
3450
+ await options.onCode?.(code);
3451
+ const expiresInSeconds = code.expires_in ?? DEFAULT_EXPIRES_IN_SECONDS;
3452
+ let intervalMs = Math.max(1, code.interval ?? DEFAULT_INTERVAL_SECONDS) * 1e3;
3453
+ const start = now();
3454
+ const deadline = Math.min(start + expiresInSeconds * 1e3, start + maxWaitMs);
3455
+ let attempt = 1;
3456
+ let consecutiveFailures = 0;
3457
+ while (true) {
3458
+ if (now() >= deadline) throw timedOutError();
3459
+ options.onPoll?.(attempt);
3460
+ try {
3461
+ return await client.pollDeviceTokenOnce(code.device_code);
3462
+ } catch (error2) {
3463
+ if (isApiError(error2)) {
3464
+ if (error2.errorCode === "authorization_pending") {
3465
+ consecutiveFailures = 0;
3466
+ await sleep(intervalMs);
3467
+ attempt += 1;
3468
+ continue;
3469
+ }
3470
+ if (error2.errorCode === "slow_down" || error2.status === 429) {
3471
+ intervalMs += SLOW_DOWN_INCREMENT_MS;
3472
+ options.onSlowDown?.(intervalMs);
3473
+ consecutiveFailures = 0;
3474
+ await sleep(intervalMs);
3475
+ attempt += 1;
3476
+ continue;
3477
+ }
3478
+ if (error2.errorCode === "access_denied") throw deniedError();
3479
+ if (error2.errorCode === "expired_token") throw expiredError(expiresInSeconds);
3480
+ if (error2.errorCode === "invalid_grant") throw invalidGrantError();
3481
+ }
3482
+ if (!isTransientFailure(error2)) throw error2;
3483
+ consecutiveFailures += 1;
3484
+ if (consecutiveFailures > MAX_CONSECUTIVE_TRANSIENT_FAILURES) throw error2;
3485
+ await sleep(intervalMs);
3486
+ attempt += 1;
3487
+ }
3488
+ }
3489
+ }
3490
+ function formatUserCode(code) {
3491
+ const cleaned = code.trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
3492
+ if (cleaned.length === 8) return `${cleaned.slice(0, 4)}-${cleaned.slice(4)}`;
3493
+ return cleaned;
3494
+ }
3495
+
3496
+ // src/commands/login.ts
3497
+ async function defaultOpenUrl(url) {
3498
+ const { default: open } = await import("open");
3499
+ await open(url);
3500
+ }
3501
+ function defaultDeps3() {
3502
+ return { createClient: createApiClient, runDeviceFlow, openUrl: defaultOpenUrl };
3503
+ }
3504
+ function shouldOpenBrowser(noBrowser) {
3505
+ if (noBrowser === true) return false;
3506
+ if (process.env.MERIDIAN_NO_BROWSER) return false;
3507
+ if (process.env.CI) return false;
3508
+ return isInteractive();
3509
+ }
3510
+ function toConfigUser(user) {
3511
+ return { id: user.id, name: user.name, email: user.email };
3512
+ }
3513
+ function printLoginJson(user, tokenExpiresAt, apiBaseUrl) {
3514
+ printJson({ user, token_expires_at: tokenExpiresAt, api_base_url: apiBaseUrl });
3515
+ }
3516
+ function hintUnlessLinked(json) {
3517
+ if (json) return;
3518
+ if (readProject(workingDirectory()) !== null) return;
3519
+ hint("Run `meridian link` to connect this project to a Meridian app.");
3520
+ }
3521
+ async function runLogin(options = {}) {
3522
+ const deps = { ...defaultDeps3(), ...options.deps };
3523
+ const json = isJsonOutput();
3524
+ if (options.force !== true) {
3525
+ const auth = getAuth();
3526
+ if (auth !== null) {
3527
+ const client2 = deps.createClient({ requireAuth: false, token: auth.token });
3528
+ try {
3529
+ const me = await client2.getMe();
3530
+ const user2 = toConfigUser(me.user);
3531
+ updateConfig({ user: user2 });
3532
+ if (json) {
3533
+ printLoginJson(user2, auth.tokenExpiresAt, client2.baseUrl);
3534
+ } else {
3535
+ log(`Already logged in as ${user2.name} <${user2.email}>`);
3536
+ hint("Run `meridian login --force` to re-authenticate.");
3537
+ }
3538
+ return;
3539
+ } catch (error2) {
3540
+ if (!(isApiError(error2) && error2.status === 401)) throw error2;
3541
+ }
3542
+ }
3543
+ }
3544
+ const client = deps.createClient({ requireAuth: false });
3545
+ const hostname = options.hostname ?? osHostname3();
3546
+ const wait = spinner("Waiting for approval in the browser\u2026");
3547
+ const startedAt = Date.now();
3548
+ const tokenResponse = await deps.runDeviceFlow({
3549
+ client,
3550
+ hostname,
3551
+ onCode: async (code) => {
3552
+ const target = code.verification_uri_complete ?? code.verification_uri;
3553
+ if (!json) {
3554
+ log();
3555
+ log(` ${style.bold(formatUserCode(code.user_code))}`);
3556
+ log();
3557
+ log(`Visit: ${style.path(code.verification_uri)}`);
3558
+ log("Waiting for you to approve this login in the browser\u2026");
3559
+ }
3560
+ let opened = false;
3561
+ if (shouldOpenBrowser(options.noBrowser)) {
3562
+ try {
3563
+ await deps.openUrl(target);
3564
+ opened = true;
3565
+ } catch {
3566
+ warn("Could not open the browser automatically.");
3567
+ }
3568
+ }
3569
+ if (!opened && !json) log(`Open this URL to continue: ${style.path(target)}`);
3570
+ if (!json) wait.start();
3571
+ },
3572
+ onPoll: () => {
3573
+ const elapsedSeconds = Math.round((Date.now() - startedAt) / 1e3);
3574
+ wait.update(`Waiting for approval in the browser\u2026 (${elapsedSeconds}s)`);
3575
+ }
3576
+ });
3577
+ wait.stop();
3578
+ const user = toConfigUser(tokenResponse.user);
3579
+ const storeApiBaseUrl = client.baseUrl !== DEFAULT_API_BASE_URL;
3580
+ updateConfig({
3581
+ token: tokenResponse.token,
3582
+ ...tokenResponse.expires_at !== void 0 ? { token_expires_at: tokenResponse.expires_at } : {},
3583
+ user,
3584
+ ...storeApiBaseUrl ? { api_base_url: client.baseUrl } : {}
3585
+ });
3586
+ if (json) {
3587
+ printLoginJson(user, tokenResponse.expires_at, client.baseUrl);
3588
+ } else {
3589
+ success(`Logged in as ${user.name} <${user.email}>`);
3590
+ hintUnlessLinked(json);
3591
+ }
3592
+ }
3593
+ function registerLoginCommand(program) {
3594
+ program.command("login").description("Authenticate this machine with Meridian via a device-code login in your browser.").option("--force", "re-authenticate even when a valid token is already stored").option("--no-browser", "print the verification URL instead of opening it").option("--hostname <name>", "hostname reported to Meridian for this device (defaults to the machine hostname)").action(async (opts) => {
3595
+ await runLogin({
3596
+ ...opts.force === true ? { force: true } : {},
3597
+ ...opts.browser === false ? { noBrowser: true } : {},
3598
+ ...typeof opts.hostname === "string" ? { hostname: opts.hostname } : {}
3599
+ });
3600
+ });
3601
+ }
3602
+
3603
+ // src/commands/logout.ts
3604
+ async function runLogout() {
3605
+ const hadToken = clearAuth();
3606
+ if (isJsonOutput()) {
3607
+ printJson({ logged_out: hadToken });
3608
+ return;
3609
+ }
3610
+ if (hadToken) success("Logged out.");
3611
+ else log("You were not logged in.");
3612
+ hint("`.meridian/project.json` is untouched; run `meridian unlink` to remove a linked project.");
3613
+ }
3614
+ function registerLogoutCommand(program) {
3615
+ program.command("logout").description("Clear the Meridian CLI token stored on this machine.").action(async () => {
3616
+ await runLogout();
3617
+ });
3618
+ }
3619
+
3620
+ // src/lib/db/snapshot.ts
3621
+ import { mkdir, readFile, readdir, rm, stat } from "fs/promises";
3622
+ import { join as join8 } from "path";
3623
+ function snapshotFileName(date = /* @__PURE__ */ new Date()) {
3624
+ return `${date.toISOString().replace(/[:.]/g, "-")}.sql`;
3625
+ }
3626
+ var SKIP_SNAPSHOT_HINT = "Re-run with --skip-snapshot to bypass snapshotting (not recommended before a schema-changing migration).";
3627
+ async function loadMysqldump() {
3628
+ const mod = await import("mysqldump");
3629
+ const dump = mod.default.default ?? mod.default;
3630
+ return dump;
3631
+ }
3632
+ async function createSnapshot(info, options) {
3633
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
3634
+ const createdAt = now();
3635
+ const tables = await countTables(info, options.factory);
3636
+ if (tables === 0) {
3637
+ return { path: null, bytes: 0, createdAt, tables: 0 };
3638
+ }
3639
+ const dump = options.dumpImpl ?? await loadMysqldump();
3640
+ await mkdir(options.dir, { recursive: true });
3641
+ const path = join8(options.dir, snapshotFileName(createdAt));
3642
+ try {
3643
+ await dump({
3644
+ connection: {
3645
+ host: info.host,
3646
+ port: info.port,
3647
+ user: info.user,
3648
+ password: info.password,
3649
+ database: info.database
3650
+ },
3651
+ dumpToFile: path,
3652
+ dump: {
3653
+ schema: { table: { ifNotExist: false, dropIfExist: true }, engine: false },
3654
+ data: { format: false, verbose: false },
3655
+ trigger: {}
3656
+ }
3657
+ });
3658
+ } catch (error2) {
3659
+ throw new CliError(`Could not snapshot the development database: ${describeError(error2)}`, {
3660
+ hint: SKIP_SNAPSHOT_HINT,
3661
+ cause: error2
3662
+ });
3663
+ }
3664
+ let bytes;
3665
+ try {
3666
+ bytes = (await stat(path)).size;
3667
+ } catch (error2) {
3668
+ throw new CliError("Could not snapshot the development database: the dump did not produce a file.", {
3669
+ hint: SKIP_SNAPSHOT_HINT,
3670
+ cause: error2
3671
+ });
3672
+ }
3673
+ if (bytes === 0) {
3674
+ throw new CliError("Could not snapshot the development database: the dump produced an empty file.", {
3675
+ hint: SKIP_SNAPSHOT_HINT
3676
+ });
3677
+ }
3678
+ return { path, bytes, createdAt, tables };
3679
+ }
3680
+ function quoteSqlString(value) {
3681
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
3682
+ }
3683
+ function readCount(result) {
3684
+ const groups = Array.isArray(result) ? result : [result];
3685
+ for (const group of groups) {
3686
+ const rows = Array.isArray(group) ? group : [group];
3687
+ for (const row of rows) {
3688
+ if (typeof row === "object" && row !== null && "count" in row) {
3689
+ const parsed = Number(row.count);
3690
+ if (Number.isFinite(parsed)) return parsed;
3691
+ }
3692
+ }
3693
+ }
3694
+ return void 0;
3695
+ }
3696
+ async function countTables(info, factory = createSqlConnection) {
3697
+ let connection;
3698
+ try {
3699
+ connection = await factory(info);
3700
+ } catch {
3701
+ return 1;
3702
+ }
3703
+ try {
3704
+ const result = await connection.query(
3705
+ `SELECT COUNT(*) AS \`count\` FROM information_schema.tables WHERE table_schema = ${quoteSqlString(info.database)}`
3706
+ );
3707
+ return readCount(result) ?? 1;
3708
+ } catch {
3709
+ return 1;
3710
+ } finally {
3711
+ try {
3712
+ await connection.end();
3713
+ } catch {
3714
+ connection.destroy?.();
3715
+ }
3716
+ }
3717
+ }
3718
+ async function restoreSnapshot(info, snapshotPath, options = {}) {
3719
+ const factory = options.factory ?? createSqlConnection;
3720
+ const sql = await readFile(snapshotPath, "utf8");
3721
+ const statements = splitSqlStatements(sql);
3722
+ const connection = await factory(info, { multipleStatements: true });
3723
+ try {
3724
+ await connection.query("SET FOREIGN_KEY_CHECKS=0");
3725
+ await connection.query("SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'");
3726
+ for (const [index, statement] of statements.entries()) {
3727
+ try {
3728
+ await connection.query(statement);
3729
+ } catch (error2) {
3730
+ throw new CliError(
3731
+ `Snapshot replay failed at statement ${index + 1} of ${statements.length}: ${statement.slice(0, 200)}`,
3732
+ {
3733
+ hint: `Inspect the failure, then replay the snapshot by hand with \`mysql < ${snapshotPath}\`.`,
3734
+ cause: error2
3735
+ }
3736
+ );
3737
+ }
3738
+ options.onProgress?.(index + 1, statements.length);
3739
+ }
3740
+ await connection.query("SET FOREIGN_KEY_CHECKS=1");
3741
+ } finally {
3742
+ await connection.end();
3743
+ }
3744
+ return { statements: statements.length };
3745
+ }
3746
+ function splitSqlStatements(sql) {
3747
+ const statements = [];
3748
+ let delimiter3 = ";";
3749
+ let buffer = "";
3750
+ let i = 0;
3751
+ const n = sql.length;
3752
+ const flush = () => {
3753
+ const trimmed = buffer.trim();
3754
+ buffer = "";
3755
+ if (trimmed !== "") statements.push(trimmed);
3756
+ };
3757
+ while (i < n) {
3758
+ if (buffer.trim() === "" && sql.slice(i, i + 10).toUpperCase() === "DELIMITER ") {
3759
+ const lineEnd = sql.indexOf("\n", i);
3760
+ const line = lineEnd === -1 ? sql.slice(i) : sql.slice(i, lineEnd);
3761
+ const newDelimiter = line.slice("DELIMITER".length).trim();
3762
+ if (newDelimiter !== "") {
3763
+ delimiter3 = newDelimiter;
3764
+ buffer = "";
3765
+ i = lineEnd === -1 ? n : lineEnd + 1;
3766
+ continue;
3767
+ }
3768
+ }
3769
+ const ch = sql[i];
3770
+ if (ch === "-" && sql[i + 1] === "-" && (sql[i + 2] === void 0 || /\s/.test(sql[i + 2] ?? ""))) {
3771
+ const lineEnd = sql.indexOf("\n", i);
3772
+ i = lineEnd === -1 ? n : lineEnd + 1;
3773
+ buffer += "\n";
3774
+ continue;
3775
+ }
3776
+ if (ch === "#") {
3777
+ const lineEnd = sql.indexOf("\n", i);
3778
+ i = lineEnd === -1 ? n : lineEnd + 1;
3779
+ buffer += "\n";
3780
+ continue;
3781
+ }
3782
+ if (ch === "/" && sql[i + 1] === "*") {
3783
+ const isExecutableHint = sql[i + 2] === "!";
3784
+ const end = sql.indexOf("*/", i + 2);
3785
+ const commentEnd = end === -1 ? n : end + 2;
3786
+ buffer += isExecutableHint ? sql.slice(i, commentEnd) : " ";
3787
+ i = commentEnd;
3788
+ continue;
3789
+ }
3790
+ if (ch === "'" || ch === '"' || ch === "`") {
3791
+ const quote = ch;
3792
+ let j = i + 1;
3793
+ let text = ch;
3794
+ while (j < n) {
3795
+ const c = sql[j];
3796
+ if (c === "\\" && quote !== "`") {
3797
+ text += sql.slice(j, j + 2);
3798
+ j += 2;
3799
+ continue;
3800
+ }
3801
+ if (c === quote) {
3802
+ if (sql[j + 1] === quote) {
3803
+ text += quote + quote;
3804
+ j += 2;
3805
+ continue;
3806
+ }
3807
+ text += quote;
3808
+ j += 1;
3809
+ break;
3810
+ }
3811
+ text += c;
3812
+ j += 1;
3813
+ }
3814
+ buffer += text;
3815
+ i = j;
3816
+ continue;
3817
+ }
3818
+ if (sql.startsWith(delimiter3, i)) {
3819
+ flush();
3820
+ i += delimiter3.length;
3821
+ continue;
3822
+ }
3823
+ buffer += ch;
3824
+ i += 1;
3825
+ }
3826
+ flush();
3827
+ return statements;
3828
+ }
3829
+ function isEnoent5(error2) {
3830
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
3831
+ }
3832
+ function parseSnapshotDate(name) {
3833
+ const iso = name.replace(/\.sql$/, "").replace(/T(\d\d)-(\d\d)-(\d\d)-(\d\d\d)Z$/, "T$1:$2:$3.$4Z");
3834
+ return new Date(iso);
3835
+ }
3836
+ async function listSnapshots(dir) {
3837
+ let entries;
3838
+ try {
3839
+ entries = await readdir(dir);
3840
+ } catch (error2) {
3841
+ if (isEnoent5(error2)) return [];
3842
+ throw error2;
3843
+ }
3844
+ const names = entries.filter((name) => name.endsWith(".sql")).sort().reverse();
3845
+ const results = [];
3846
+ for (const name of names) {
3847
+ const path = join8(dir, name);
3848
+ const { size } = await stat(path);
3849
+ results.push({ path, name, bytes: size, createdAt: parseSnapshotDate(name) });
3850
+ }
3851
+ return results;
3852
+ }
3853
+ async function pruneSnapshots(dir, keep) {
3854
+ const snapshots = await listSnapshots(dir);
3855
+ const toDelete = snapshots.slice(Math.max(keep, 0));
3856
+ for (const snapshot of toDelete) {
3857
+ await rm(snapshot.path, { force: true });
3858
+ }
3859
+ return toDelete.map((snapshot) => snapshot.path);
3860
+ }
3861
+
3862
+ // src/commands/migrate.ts
3863
+ var DEFAULT_KEEP_SNAPSHOTS = 5;
3864
+ var MAX_OUTPUT_EXCERPT = 4e3;
3865
+ function defaultDeps4() {
3866
+ return {
3867
+ startDevSession,
3868
+ createSnapshot,
3869
+ restoreSnapshot,
3870
+ pruneSnapshots,
3871
+ runNpx,
3872
+ createApiClient,
3873
+ requireProject: () => requireProject(workingDirectory()),
3874
+ assertShadowDatasource,
3875
+ assertMigrationDialect,
3876
+ missingShadowDatabaseUrl,
3877
+ addShadowDatabaseUrl,
3878
+ diagnoseMigrationFailure,
3879
+ now: () => /* @__PURE__ */ new Date()
3880
+ };
3881
+ }
3882
+ function resolveDeps2(overrides) {
3883
+ return { ...defaultDeps4(), ...overrides };
3884
+ }
3885
+ function formatSize(bytes) {
3886
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3887
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3888
+ }
3889
+ function excerptOf(output) {
3890
+ return output.length > MAX_OUTPUT_EXCERPT ? output.slice(-MAX_OUTPUT_EXCERPT) : output;
3891
+ }
3892
+ function shouldInheritStdio(json) {
3893
+ return !json && isInteractive();
3894
+ }
3895
+ async function reportMigrationSafely(client, appId, input2) {
3896
+ try {
3897
+ await client.reportMigration(appId, input2);
3898
+ } catch {
3899
+ warn("Could not report this migration run to Meridian.");
3900
+ }
3901
+ }
3902
+ async function runMigration(session, deps, ctx) {
3903
+ const { json, keepSnapshots, commandArgs, commandString, options } = ctx;
3904
+ const client = deps.createApiClient();
3905
+ const appId = session.project.config.app_id;
3906
+ const dir = snapshotsDir(session.project.root);
3907
+ let snapshot;
3908
+ if (options.skipSnapshot !== true) {
3909
+ snapshot = await withSpinner(
3910
+ "Snapshotting the development database...",
3911
+ () => deps.createSnapshot(session.connection, { dir, now: deps.now }),
3912
+ {
3913
+ success: (result) => result.path === null ? "Development database is empty \u2014 nothing to snapshot." : `Snapshot saved to ${style.path(result.path)} (${formatSize(result.bytes)}).`
3914
+ }
3915
+ );
3916
+ }
3917
+ const inherit = shouldInheritStdio(json);
3918
+ const startedAt = deps.now().toISOString();
3919
+ const runResult = await deps.runNpx(commandArgs, { cwd: session.project.root, env: session.env, inherit });
3920
+ const finishedAt = deps.now().toISOString();
3921
+ let status;
3922
+ let rolledBack = false;
3923
+ let exitCode;
3924
+ if (runResult.code === 0) {
3925
+ success("Migration applied.");
3926
+ status = "success";
3927
+ exitCode = 0;
3928
+ if (snapshot !== void 0) await deps.pruneSnapshots(dir, keepSnapshots);
3929
+ } else {
3930
+ exitCode = runResult.code !== 0 ? runResult.code : 1;
3931
+ status = "failed";
3932
+ if (snapshot === void 0) {
3933
+ warn(`\`${commandString}\` failed (exit code ${runResult.code}).`);
3934
+ hint("No snapshot was taken (--skip-snapshot), so there is nothing to roll back.");
3935
+ } else if (options.noRollback === true) {
3936
+ warn(`\`${commandString}\` failed (exit code ${runResult.code}); the database was left as-is (--no-rollback).`);
3937
+ if (snapshot.path === null) {
3938
+ hint("The database was empty before the migration; `meridian db reset` returns it to that state.");
3939
+ } else {
3940
+ detail(`Pre-migration snapshot: ${snapshot.path}`);
3941
+ hint(`Replay it by hand once the proxy is up (\`meridian dev\`): \`mysql < ${snapshot.path}\`.`);
3942
+ }
3943
+ } else {
3944
+ const snapshotPath = snapshot.path;
3945
+ warn(
3946
+ `\`${commandString}\` failed (exit code ${runResult.code}); ` + (snapshotPath === null ? "rolling the development database back to its pre-migration (empty) state..." : "rolling back the development database to the pre-migration snapshot...")
3947
+ );
3948
+ try {
3949
+ await client.resetDevDatabase(appId);
3950
+ if (snapshotPath === null) {
3951
+ success("Development database rolled back to its pre-migration (empty) state.");
3952
+ } else {
3953
+ await withSpinner(
3954
+ "Restoring the pre-migration snapshot...",
3955
+ (spinner2) => deps.restoreSnapshot(session.connection, snapshotPath, {
3956
+ onProgress: (executed, total) => spinner2.update(`Restoring the pre-migration snapshot... (${executed}/${total} statements)`)
3957
+ }),
3958
+ { success: (result) => `Restored the pre-migration snapshot (${result.statements} statements).` }
3959
+ );
3960
+ }
3961
+ status = "rolled_back";
3962
+ rolledBack = true;
3963
+ } catch {
3964
+ error("Automatic rollback failed. The development database may be in a partial state \u2014 treat it as unsafe.");
3965
+ if (snapshotPath === null) {
3966
+ hint("Recover by hand with `meridian db reset` \u2014 the database was empty before this migration.");
3967
+ } else {
3968
+ detail(`Pre-migration snapshot: ${snapshotPath}`);
3969
+ hint(
3970
+ `Recover by hand: start the proxy with \`meridian dev\`, then run \`mysql < ${snapshotPath}\` to replay the snapshot, or \`meridian db reset\` to start over from an empty schema.`
3971
+ );
3972
+ }
3973
+ }
3974
+ }
3975
+ const diagnosis = deps.diagnoseMigrationFailure(session.project.root);
3976
+ if (diagnosis !== null) hint(diagnosis);
3977
+ }
3978
+ const excerpt = excerptOf(runResult.output);
3979
+ await reportMigrationSafely(client, appId, {
3980
+ command: commandString,
3981
+ status,
3982
+ started_at: startedAt,
3983
+ finished_at: finishedAt,
3984
+ ...excerpt !== "" ? { output_excerpt: excerpt } : {}
3985
+ });
3986
+ if (json) {
3987
+ const payload = {
3988
+ status,
3989
+ command: commandString,
3990
+ started_at: startedAt,
3991
+ finished_at: finishedAt,
3992
+ exit_code: exitCode,
3993
+ snapshot: snapshot !== void 0 ? { path: snapshot.path, bytes: snapshot.bytes } : null,
3994
+ rolled_back: rolledBack,
3995
+ database: session.connection.database,
3996
+ app_id: appId
3997
+ };
3998
+ printJson(payload);
3999
+ }
4000
+ return exitCode;
4001
+ }
4002
+ async function ensureShadowDatasource(root, deps) {
4003
+ const schemaPath = deps.missingShadowDatabaseUrl(root);
4004
+ if (schemaPath !== null && (isAssumeYes() || isInteractive())) {
4005
+ const approved = await confirmAction({
4006
+ message: `\`prisma migrate dev\` needs a shadow database. Add \`shadowDatabaseUrl\` to ${schemaPath}?`,
4007
+ default: true
4008
+ });
4009
+ if (approved) {
4010
+ deps.addShadowDatabaseUrl(root);
4011
+ success(`Added \`shadowDatabaseUrl = env("SHADOW_DATABASE_URL")\` to ${style.path(schemaPath)}.`);
4012
+ }
4013
+ }
4014
+ deps.assertShadowDatasource(root);
4015
+ }
4016
+ async function runMigrate(options = {}) {
4017
+ const deps = resolveDeps2(options.deps);
4018
+ const json = isJsonOutput();
4019
+ const keepSnapshots = options.keepSnapshots ?? DEFAULT_KEEP_SNAPSHOTS;
4020
+ const commandArgs = ["prisma", "migrate", "dev", ...options.prismaArgs ?? []];
4021
+ const commandString = ["npx", ...commandArgs].join(" ");
4022
+ const project = deps.requireProject();
4023
+ if (project.config.skip_datasource_check !== true) await ensureShadowDatasource(project.root, deps);
4024
+ if (options.allowForeignMigrations !== true) deps.assertMigrationDialect(project.root);
4025
+ const sessionOptions = {
4026
+ ...options.port !== void 0 ? { port: options.port } : {},
4027
+ ...options.envFile !== void 0 ? { envFile: options.envFile } : {},
4028
+ project
4029
+ };
4030
+ const session = await deps.startDevSession(sessionOptions);
4031
+ try {
4032
+ return await runMigration(session, deps, { json, keepSnapshots, commandArgs, commandString, options });
4033
+ } finally {
4034
+ await session.stop();
4035
+ }
4036
+ }
4037
+ function registerMigrateCommand(program) {
4038
+ program.command("migrate").description(
4039
+ "Snapshot the development database, run `prisma migrate dev`, and roll back to the snapshot automatically if the migration fails."
4040
+ ).option("--skip-snapshot", "run without a pre-migration snapshot (no rollback is possible)").option(
4041
+ "--allow-foreign-migrations",
4042
+ "run even when prisma/migrations was generated for SQLite or Postgres and cannot apply to MySQL"
4043
+ ).option("--no-rollback", "keep a snapshot but do not automatically restore it after a failed migration").option("--keep-snapshots <n>", "number of recent snapshots to keep after a successful migration", "5").option("--port <port>", "preferred local port for the Cloud SQL Auth Proxy").option("--env-file <file>", "path (relative to the project root) of the .env file to patch with DATABASE_URL").argument("[prismaArgs...]", "extra arguments forwarded to `prisma migrate dev` verbatim, after --").passThroughOptions().action(async (prismaArgs, opts) => {
4044
+ const keepSnapshots = parseIntegerOption(opts.keepSnapshots, "--keep-snapshots", { min: 0 });
4045
+ const port = parsePortOption(opts.port);
4046
+ const exitCode = await runMigrate({
4047
+ prismaArgs,
4048
+ ...opts.skipSnapshot === true ? { skipSnapshot: true } : {},
4049
+ ...opts.allowForeignMigrations === true ? { allowForeignMigrations: true } : {},
4050
+ ...opts.rollback === false ? { noRollback: true } : {},
4051
+ ...keepSnapshots !== void 0 ? { keepSnapshots } : {},
4052
+ ...port !== void 0 ? { port } : {},
4053
+ ...opts.envFile !== void 0 ? { envFile: opts.envFile } : {}
4054
+ });
4055
+ process.exitCode = exitCode;
4056
+ });
4057
+ }
4058
+
4059
+ // src/commands/unlink.ts
4060
+ async function runUnlink(options = {}) {
4061
+ const deps = { ...options.deps };
4062
+ const json = isJsonOutput();
4063
+ const root = workingDirectory();
4064
+ const project = readProject(root);
4065
+ if (project === null) {
4066
+ if (json) printJson({ unlinked: false, root });
4067
+ else log("This directory is not linked to a Meridian app.");
4068
+ return;
4069
+ }
4070
+ const appLabel2 = project.config.app_name ?? String(project.config.app_id);
4071
+ const proceed = await confirmAction({
4072
+ message: `Unlink ${project.root} from ${appLabel2}?`,
4073
+ ...deps.confirmImpl !== void 0 ? { promptImpl: deps.confirmImpl } : {}
4074
+ });
4075
+ if (!proceed) throw new CliError("Aborted.");
4076
+ removeProject(project.root);
4077
+ if (json) {
4078
+ printJson({ unlinked: true, root: project.root });
4079
+ return;
4080
+ }
4081
+ success(`Unlinked ${project.root} from ${appLabel2}.`);
4082
+ hint("Snapshots in `.meridian/snapshots/` were left alone.");
4083
+ }
4084
+ function registerUnlinkCommand(program) {
4085
+ program.command("unlink").description("Remove this project's link to a Meridian organization and app.").action(async () => {
4086
+ await runUnlink();
4087
+ });
4088
+ }
4089
+
4090
+ // src/commands/whoami.ts
4091
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
4092
+ function defaultDeps5() {
4093
+ return { createClient: createApiClient };
4094
+ }
4095
+ function toConfigUser2(user) {
4096
+ return { id: user.id, name: user.name, email: user.email };
4097
+ }
4098
+ function describeExpiry(tokenExpiresAt) {
4099
+ if (tokenExpiresAt === void 0) return void 0;
4100
+ const expiry = new Date(tokenExpiresAt).getTime();
4101
+ if (Number.isNaN(expiry)) return void 0;
4102
+ const remainingMs = expiry - Date.now();
4103
+ if (remainingMs < 0) return "Your Meridian CLI token has expired.";
4104
+ if (remainingMs < SEVEN_DAYS_MS) return "Your Meridian CLI token expires soon.";
4105
+ return void 0;
4106
+ }
4107
+ function printHuman(result, project) {
4108
+ if (result.user !== null) {
4109
+ success(`Logged in as ${result.user.name} <${result.user.email}>`);
4110
+ } else {
4111
+ log("Logged in (cached user details unavailable offline).");
4112
+ }
4113
+ keyValues([
4114
+ ["User ID", result.user !== null ? String(result.user.id) : "unknown"],
4115
+ ["API URL", result.apiBaseUrl],
4116
+ ["Token expires", result.tokenExpiresAt ?? "never"]
4117
+ ]);
4118
+ const expiryWarning = describeExpiry(result.tokenExpiresAt);
4119
+ if (expiryWarning !== void 0) warn(expiryWarning);
4120
+ log();
4121
+ if (result.offline) {
4122
+ hint("Organization list requires network access; omitted in --offline mode.");
4123
+ } else if (result.orgs.length > 0) {
4124
+ step("Organizations");
4125
+ keyValues(result.orgs.map((org) => [org.name, org.slug]));
4126
+ } else {
4127
+ log("No organizations.");
4128
+ }
4129
+ log();
4130
+ if (project !== null) {
4131
+ step("Linked project");
4132
+ keyValues([
4133
+ ["App", project.config.app_name ?? String(project.config.app_id)],
4134
+ ["Org", String(project.config.org_id)],
4135
+ ["Root", project.root]
4136
+ ]);
4137
+ } else {
4138
+ hint("Run `meridian link` to connect this directory to a Meridian app.");
4139
+ }
4140
+ }
4141
+ function printJsonResult(result, project) {
4142
+ printJson({
4143
+ user: result.user,
4144
+ api_base_url: result.apiBaseUrl,
4145
+ token_expires_at: result.tokenExpiresAt,
4146
+ orgs: result.orgs,
4147
+ project: project === null ? null : {
4148
+ root: project.root,
4149
+ org_id: project.config.org_id,
4150
+ app_id: project.config.app_id,
4151
+ ...project.config.app_name !== void 0 ? { app_name: project.config.app_name } : {}
4152
+ }
4153
+ });
4154
+ }
4155
+ async function runWhoami(options = {}) {
4156
+ const deps = { ...defaultDeps5(), ...options.deps };
4157
+ const json = isJsonOutput();
4158
+ const auth = getAuth();
4159
+ if (auth === null) throw notLoggedInError();
4160
+ let result;
4161
+ if (options.offline === true) {
4162
+ result = {
4163
+ user: auth.user ?? null,
4164
+ apiBaseUrl: resolveApiBaseUrl(getGlobalOptions().apiUrl),
4165
+ tokenExpiresAt: auth.tokenExpiresAt,
4166
+ orgs: [],
4167
+ offline: true
4168
+ };
4169
+ } else {
4170
+ const client = deps.createClient({ token: auth.token, requireAuth: false });
4171
+ try {
4172
+ const me = await client.getMe();
4173
+ result = {
4174
+ user: toConfigUser2(me.user),
4175
+ apiBaseUrl: client.baseUrl,
4176
+ tokenExpiresAt: auth.tokenExpiresAt,
4177
+ orgs: me.orgs,
4178
+ offline: false
4179
+ };
4180
+ } catch (error2) {
4181
+ if (isApiError(error2) && error2.status === 401) throw sessionExpiredError(error2);
4182
+ throw error2;
4183
+ }
4184
+ }
4185
+ const project = readProject(workingDirectory());
4186
+ if (json) printJsonResult(result, project);
4187
+ else printHuman(result, project);
4188
+ }
4189
+ function registerWhoamiCommand(program) {
4190
+ program.command("whoami").description(
4191
+ "Show the logged-in Meridian user, org memberships, and any linked project.\nUse --offline to skip the API call and report only what is cached locally."
4192
+ ).option("--offline", "skip the API call; report only what is stored locally").action(async (opts) => {
4193
+ await runWhoami({ ...opts.offline === true ? { offline: true } : {} });
4194
+ });
4195
+ }
4196
+
4197
+ // src/lib/global-flags.ts
4198
+ import { Option } from "commander";
4199
+ var HELP_GROUP = "Global Options:";
4200
+ function globalOptions() {
4201
+ return [
4202
+ new Option("--api-url <url>", "Meridian API base URL (overrides MERIDIAN_API_URL and the stored config)"),
4203
+ new Option("--json", "print machine-readable JSON on stdout").default(false),
4204
+ new Option("-q, --quiet", "only print warnings and errors").default(false),
4205
+ new Option("--verbose", "print debug output, including stack traces").default(false),
4206
+ new Option("--no-color", "disable ANSI colors"),
4207
+ new Option("-y, --yes", "assume yes for every confirmation prompt").default(false),
4208
+ new Option("--no-input", "never prompt; fail instead (for CI)"),
4209
+ new Option("--cwd <dir>", "run as if started in this directory")
4210
+ ];
4211
+ }
4212
+ function addGlobalFlags(command) {
4213
+ for (const option of globalOptions()) {
4214
+ command.addOption(option.helpGroup(HELP_GROUP));
4215
+ }
4216
+ return command;
4217
+ }
4218
+ function addGlobalFlagsToSubcommands(parent) {
4219
+ for (const child of parent.commands) {
4220
+ if (child.commands.length > 0) addGlobalFlagsToSubcommands(child);
4221
+ else addGlobalFlags(child);
4222
+ }
4223
+ }
4224
+ function commandChain(command) {
4225
+ const chain = [];
4226
+ let node = command;
4227
+ while (node !== null) {
4228
+ chain.push(node);
4229
+ node = node.parent;
4230
+ }
4231
+ return chain;
4232
+ }
4233
+ function explicit(chain, key) {
4234
+ for (const command of chain) {
4235
+ if (command.getOptionValueSource(key) === "cli") return command.getOptionValue(key);
4236
+ }
4237
+ return void 0;
4238
+ }
4239
+ function resolveGlobalFlags(actionCommand) {
4240
+ const chain = commandChain(actionCommand);
4241
+ const apiUrl = explicit(chain, "apiUrl");
4242
+ const color = explicit(chain, "color");
4243
+ const input2 = explicit(chain, "input");
4244
+ const cwd = explicit(chain, "cwd");
4245
+ return {
4246
+ ...apiUrl !== void 0 ? { apiUrl } : {},
4247
+ json: explicit(chain, "json") === true,
4248
+ quiet: explicit(chain, "quiet") === true,
4249
+ verbose: explicit(chain, "verbose") === true,
4250
+ // `--no-color`/`--no-input` are only meaningful when asked for explicitly;
4251
+ // otherwise output and prompts auto-detect the terminal.
4252
+ ...color !== void 0 ? { color } : {},
4253
+ yes: explicit(chain, "yes") === true,
4254
+ ...input2 !== void 0 ? { interactive: input2 } : {},
4255
+ ...cwd !== void 0 ? { cwd } : {}
4256
+ };
4257
+ }
4258
+
4259
+ // src/index.ts
4260
+ function buildProgram() {
4261
+ const program = new Command();
4262
+ program.name("meridian").description(
4263
+ "Managed remote development databases for Meridian apps.\n\nRun `meridian login` once, `meridian link` per project, then `meridian dev`."
4264
+ ).version(VERSION, "-V, --version").enablePositionalOptions().showHelpAfterError("(run `meridian --help` for usage)");
4265
+ addGlobalFlags(program);
4266
+ program.hook("preAction", (_thisCommand, actionCommand) => {
4267
+ const flags = resolveGlobalFlags(actionCommand);
4268
+ setGlobalOptions(flags);
4269
+ configureOutput({
4270
+ json: flags.json === true,
4271
+ quiet: flags.quiet === true,
4272
+ verbose: flags.verbose === true,
4273
+ ...flags.color !== void 0 ? { color: flags.color } : {}
4274
+ });
4275
+ configurePrompts({
4276
+ // Only set when the flag is actually present: `--yes` has no negated
4277
+ // form, so passing `false` here would just be "flag absent" — and it
4278
+ // would override the MERIDIAN_YES fallback isAssumeYes() falls back to.
4279
+ ...flags.yes === true ? { assumeYes: true } : {},
4280
+ ...flags.interactive !== void 0 ? { interactive: flags.interactive } : {}
4281
+ });
4282
+ });
4283
+ registerLoginCommand(program);
4284
+ registerLogoutCommand(program);
4285
+ registerWhoamiCommand(program);
4286
+ registerLinkCommand(program);
4287
+ registerUnlinkCommand(program);
4288
+ registerDevCommand(program);
4289
+ registerMigrateCommand(program);
4290
+ registerDbCommand(program);
4291
+ addGlobalFlagsToSubcommands(program);
4292
+ return program;
4293
+ }
4294
+ async function run(argv = process.argv) {
4295
+ try {
4296
+ await buildProgram().parseAsync(argv);
4297
+ return process.exitCode === void 0 ? 0 : Number(process.exitCode);
4298
+ } catch (error2) {
4299
+ return reportError(error2);
4300
+ }
4301
+ }
4302
+ function isMainModule() {
4303
+ const entry = process.argv[1];
4304
+ if (entry === void 0) return false;
4305
+ try {
4306
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
4307
+ } catch {
4308
+ return false;
4309
+ }
4310
+ }
4311
+ if (isMainModule()) {
4312
+ const code = await run(process.argv);
4313
+ if (code !== 0) process.exitCode = code;
4314
+ }
4315
+ export {
4316
+ buildProgram,
4317
+ run
4318
+ };
4319
+ //# sourceMappingURL=index.js.map