@the-meridian/cli 1.2.0 → 1.4.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 CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { realpathSync } from "fs";
5
- import { fileURLToPath } from "url";
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/db.ts
9
- import { readFileSync as readFileSync7 } from "fs";
10
- import { join as join6 } from "path";
9
+ import { readFileSync as readFileSync6 } from "fs";
10
+ import { join as join5 } from "path";
11
11
 
12
12
  // src/version.ts
13
13
  import { createRequire } from "module";
@@ -81,9 +81,9 @@ var devDatabaseSchema = z.object({
81
81
  /** Set by the API only while `status` is `error`; surfaced verbatim to the developer. */
82
82
  error_message: z.string().nullish(),
83
83
  /**
84
- * How the CLI reaches the database: `gateway` (the Meridian database
85
- * gateway with a per-developer certificate) or `proxy` (cloud-sql-proxy).
86
- * Absent from an API older than the gateway, which means `proxy`.
84
+ * How the CLI reaches the database. The API always reports `gateway` (the
85
+ * Meridian database gateway with a per-developer certificate) since
86
+ * MER-823; the CLI no longer reads it.
87
87
  */
88
88
  access: devDatabaseAccessSchema.nullish()
89
89
  }).loose();
@@ -111,11 +111,6 @@ var devCredentialsSchema = z.object({
111
111
  /** Echoed from the (nullable) dev-database column, so it can be null. */
112
112
  instance_connection_name: z.string().nullish()
113
113
  }).loose();
114
- var proxyTokenSchema = z.object({
115
- access_token: z.string(),
116
- expires_at: z.string().nullish(),
117
- instance_connection_name: z.string().nullish()
118
- }).loose();
119
114
  var migrationRunInputSchema = z.object({
120
115
  command: z.string(),
121
116
  status: z.enum(["success", "failed", "rolled_back"]),
@@ -350,13 +345,6 @@ var ApiClient = class _ApiClient {
350
345
  `/app/cli/apps/${encodePathSegment(appId)}/dev-database/credentials`
351
346
  );
352
347
  }
353
- async issueProxyToken(appId) {
354
- return this.request(
355
- proxyTokenSchema,
356
- "POST",
357
- `/app/cli/apps/${encodePathSegment(appId)}/dev-database/proxy-token`
358
- );
359
- }
360
348
  /**
361
349
  * Signs `publicKeyPem` (SPKI) as this developer's client certificate for the
362
350
  * database gateway. Each call replaces the previous certificate server-side.
@@ -592,9 +580,6 @@ function meridianHome() {
592
580
  function globalConfigPath() {
593
581
  return join(meridianHome(), "config.json");
594
582
  }
595
- function proxyBinDir() {
596
- return join(meridianHome(), "bin");
597
- }
598
583
  function projectStateDir(root) {
599
584
  return join(root, ".meridian");
600
585
  }
@@ -688,6 +673,16 @@ function resolveApiBaseUrl(override) {
688
673
  }
689
674
  return normalizeApiBaseUrl(DEFAULT_API_BASE_URL);
690
675
  }
676
+ function storedTokenApiBaseUrl(config = readConfig()) {
677
+ return normalizeApiBaseUrl(
678
+ config.api_base_url !== void 0 && config.api_base_url !== "" ? config.api_base_url : DEFAULT_API_BASE_URL
679
+ );
680
+ }
681
+ function tokenMismatchError(baseUrl, tokenBaseUrl) {
682
+ return new CliError(`Your Meridian CLI token was issued by ${tokenBaseUrl}, not ${baseUrl}, so it was not sent.`, {
683
+ hint: `Unset MERIDIAN_API_URL (or --api-url) to use ${tokenBaseUrl}, or run \`meridian login\` to sign in to ${baseUrl}.`
684
+ });
685
+ }
691
686
  function getAuth() {
692
687
  const config = readConfig();
693
688
  if (config.token === void 0) return null;
@@ -726,8 +721,13 @@ function workingDirectory() {
726
721
  function createApiClient(options = {}) {
727
722
  const requireAuth = options.requireAuth ?? true;
728
723
  const baseUrl = resolveApiBaseUrl(options.apiUrl ?? getGlobalOptions().apiUrl);
729
- const token = options.token ?? getAuth()?.token;
724
+ const storedToken = getAuth()?.token;
725
+ const token = options.token ?? storedToken;
730
726
  if (requireAuth && token === void 0) throw notLoggedInError();
727
+ if (token !== void 0 && token === storedToken) {
728
+ const tokenBaseUrl = storedTokenApiBaseUrl();
729
+ if (tokenBaseUrl !== baseUrl) throw tokenMismatchError(baseUrl, tokenBaseUrl);
730
+ }
731
731
  return new ApiClient({
732
732
  baseUrl,
733
733
  userAgent: USER_AGENT,
@@ -747,8 +747,8 @@ function connectionInfoFromCredentials(credentials, proxy) {
747
747
  };
748
748
  }
749
749
  var createSqlConnection = async (info, options) => {
750
- const { createConnection: createConnection2 } = await import("mysql2/promise");
751
- const connection = await createConnection2({
750
+ const { createConnection } = await import("mysql2/promise");
751
+ const connection = await createConnection({
752
752
  host: info.host,
753
753
  port: info.port,
754
754
  user: info.user,
@@ -832,7 +832,7 @@ function toDatabaseError(error2, info) {
832
832
  if (code === "ECONNREFUSED") {
833
833
  return build(
834
834
  `Could not reach ${target} (${code}).`,
835
- "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."
835
+ "Nothing is listening on that port. `meridian dev` and `meridian migrate` start the database connection for you; start one of them first if you are connecting by hand."
836
836
  );
837
837
  }
838
838
  if (code === "ETIMEDOUT" || code === "PROTOCOL_SEQUENCE_TIMEOUT") {
@@ -2569,7 +2569,7 @@ async function findFreePort(options = {}) {
2569
2569
  }
2570
2570
  const ephemeral = await bindEphemeralPort(host);
2571
2571
  if (ephemeral === null) {
2572
- throw new CliError("Could not find a free local port for the Cloud SQL Auth Proxy.", {
2572
+ throw new CliError("Could not find a free local port for the database gateway forwarder.", {
2573
2573
  hint: "Close other applications using local ports and try again."
2574
2574
  });
2575
2575
  }
@@ -2782,513 +2782,6 @@ function diagnoseMigrationFailure(root) {
2782
2782
  return offender === null ? null : foreignDialectExplanation(offender);
2783
2783
  }
2784
2784
 
2785
- // src/lib/proxy/binary.ts
2786
- import { createHash as createHash2 } from "crypto";
2787
- import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
2788
- import { join as join4 } from "path";
2789
- var PROXY_VERSION = "2.24.1";
2790
- var PROXY_CHECKSUMS = Object.freeze({
2791
- "cloud-sql-proxy.darwin.amd64": "67a38444d03fe77a8b11036f96fe5d2379640088bb6e338737fb48d97c58cf48",
2792
- "cloud-sql-proxy.darwin.arm64": "fd374545705f2aa687d8e525a2efcbb9d356cc8af60d4dfa1a330cb59a7f9450",
2793
- "cloud-sql-proxy.linux.386": "cf731f4becfb5a2de61f2998b2d660c5fb8f3067cbd3b86f5783ae9049c06f24",
2794
- "cloud-sql-proxy.linux.amd64": "fae2766aac9d614a2bdef2f2a7778f3d054f3acd5ff07a81a9e300bd471512eb",
2795
- "cloud-sql-proxy.linux.arm64": "da49fd73b0d33c0ab8e5dfc19dfb104cdd969ebd355eef8fd3b079c20a723c82",
2796
- "cloud-sql-proxy.x64.exe": "01ec54c69dbb8118c0b181c33ff1469a5cdb2dddb49eb86b0b173f6c55dea0bc",
2797
- "cloud-sql-proxy.x86.exe": "98c3b1897c80f2bc0b241b4452c18557a00cd86f89e4d7449c4ff1316e243c44"
2798
- });
2799
- var ASSET_NAME_BY_PLATFORM_ARCH = Object.freeze({
2800
- "darwin:arm64": "cloud-sql-proxy.darwin.arm64",
2801
- "darwin:x64": "cloud-sql-proxy.darwin.amd64",
2802
- "linux:x64": "cloud-sql-proxy.linux.amd64",
2803
- "linux:arm64": "cloud-sql-proxy.linux.arm64",
2804
- "linux:ia32": "cloud-sql-proxy.linux.386",
2805
- "win32:x64": "cloud-sql-proxy.x64.exe",
2806
- "win32:ia32": "cloud-sql-proxy.x86.exe"
2807
- });
2808
- function defaultVersion() {
2809
- const override = process.env.MERIDIAN_PROXY_VERSION;
2810
- if (override !== void 0 && override !== "") return override;
2811
- return PROXY_VERSION;
2812
- }
2813
- function resolveProxyAsset(options) {
2814
- const platform = options?.platform ?? process.platform;
2815
- const arch = options?.arch ?? process.arch;
2816
- const version = options?.version ?? defaultVersion();
2817
- const assetName = ASSET_NAME_BY_PLATFORM_ARCH[`${platform}:${arch}`];
2818
- if (assetName === void 0) {
2819
- throw new CliError(`The Cloud SQL Auth Proxy has no published build for ${platform}/${arch}.`, {
2820
- hint: "Set MERIDIAN_PROXY_PATH to the path of a proxy binary you install yourself."
2821
- });
2822
- }
2823
- const downloadUrl = `https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v${version}/${assetName}`;
2824
- const sha256Value = version === PROXY_VERSION ? PROXY_CHECKSUMS[assetName] : void 0;
2825
- const fileName = `cloud-sql-proxy-${version}-${platform}-${arch}${platform === "win32" ? ".exe" : ""}`;
2826
- return {
2827
- version,
2828
- assetName,
2829
- downloadUrl,
2830
- ...sha256Value !== void 0 ? { sha256: sha256Value } : {},
2831
- fileName
2832
- };
2833
- }
2834
- function sha256(data) {
2835
- return createHash2("sha256").update(data).digest("hex");
2836
- }
2837
- var CHECKSUM_ROW_PATTERN = /\[([^\]]+)]\([^)]*\)\s*\|\s*([0-9a-fA-F]{64})/g;
2838
- function parseChecksumTable(releaseBody) {
2839
- const table = {};
2840
- for (const match of releaseBody.matchAll(CHECKSUM_ROW_PATTERN)) {
2841
- const name = match[1];
2842
- const hash = match[2];
2843
- if (name !== void 0 && hash !== void 0) table[name] = hash.toLowerCase();
2844
- }
2845
- return table;
2846
- }
2847
- var RELEASE_API_URL = "https://api.github.com/repos/GoogleCloudPlatform/cloud-sql-proxy/releases/tags";
2848
- var checksumCache = /* @__PURE__ */ new Map();
2849
- async function resolveExpectedChecksum(asset, fetchImpl) {
2850
- if (asset.sha256 !== void 0) return asset.sha256;
2851
- const cacheKey = `${asset.version}:${asset.assetName}`;
2852
- const cached = checksumCache.get(cacheKey);
2853
- if (cached !== void 0) return cached;
2854
- const skipAllowed = process.env.MERIDIAN_PROXY_SKIP_CHECKSUM === "1";
2855
- const url = `${RELEASE_API_URL}/v${asset.version}`;
2856
- let response;
2857
- try {
2858
- response = await fetchImpl(url, { headers: { Accept: "application/vnd.github+json" } });
2859
- } catch (error2) {
2860
- if (skipAllowed) return void 0;
2861
- throw new CliError(`Could not reach ${url} to look up the checksum for cloud-sql-proxy v${asset.version}.`, {
2862
- hint: "Check your network connection, or set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk.",
2863
- cause: error2
2864
- });
2865
- }
2866
- if (!response.ok) {
2867
- if (skipAllowed) return void 0;
2868
- throw new CliError(
2869
- `Could not determine the sha256 checksum for cloud-sql-proxy v${asset.version} (GitHub release lookup for ${url} returned HTTP ${response.status}).`,
2870
- {
2871
- hint: "Set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk."
2872
- }
2873
- );
2874
- }
2875
- const rawBody = await response.text();
2876
- let releaseNotes = rawBody;
2877
- try {
2878
- const parsed = JSON.parse(rawBody);
2879
- if (typeof parsed === "object" && parsed !== null && "body" in parsed && typeof parsed.body === "string") {
2880
- releaseNotes = parsed.body;
2881
- }
2882
- } catch {
2883
- }
2884
- const checksum = parseChecksumTable(releaseNotes)[asset.assetName];
2885
- if (checksum !== void 0) {
2886
- checksumCache.set(cacheKey, checksum);
2887
- return checksum;
2888
- }
2889
- if (skipAllowed) return void 0;
2890
- throw new CliError(
2891
- `Could not find a published sha256 checksum for ${asset.assetName} in the cloud-sql-proxy v${asset.version} release notes.`,
2892
- { hint: "Set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk." }
2893
- );
2894
- }
2895
- function parseContentLength(header) {
2896
- if (header === null) return void 0;
2897
- const value = Number(header);
2898
- return Number.isFinite(value) ? value : void 0;
2899
- }
2900
- async function readResponseBody(response, onProgress) {
2901
- const total = parseContentLength(response.headers.get("content-length"));
2902
- if (response.body === null) {
2903
- const buffer = Buffer.from(await response.arrayBuffer());
2904
- onProgress?.(buffer.byteLength, total);
2905
- return buffer;
2906
- }
2907
- const reader = response.body.getReader();
2908
- const chunks = [];
2909
- let received = 0;
2910
- for (; ; ) {
2911
- const { done, value } = await reader.read();
2912
- if (done) break;
2913
- if (value !== void 0) {
2914
- chunks.push(value);
2915
- received += value.byteLength;
2916
- onProgress?.(received, total);
2917
- }
2918
- }
2919
- return Buffer.concat(chunks);
2920
- }
2921
- async function ensureProxyBinary(options = {}) {
2922
- const overridePath = process.env.MERIDIAN_PROXY_PATH;
2923
- if (overridePath !== void 0 && overridePath !== "") {
2924
- if (!existsSync4(overridePath)) {
2925
- throw new CliError(`MERIDIAN_PROXY_PATH is set to ${overridePath}, but that file does not exist.`, {
2926
- hint: "Fix or unset MERIDIAN_PROXY_PATH."
2927
- });
2928
- }
2929
- return overridePath;
2930
- }
2931
- const asset = resolveProxyAsset(options.version !== void 0 ? { version: options.version } : {});
2932
- const dir = options.dir ?? proxyBinDir();
2933
- const fetchImpl = options.fetchImpl ?? fetch;
2934
- const targetPath = join4(dir, asset.fileName);
2935
- mkdirSync4(dir, { recursive: true });
2936
- if (!options.force && existsSync4(targetPath)) {
2937
- const cached = readFileSync6(targetPath);
2938
- const expected2 = await resolveExpectedChecksum(asset, fetchImpl);
2939
- if (expected2 === void 0 || sha256(cached) === expected2) return targetPath;
2940
- rmSync2(targetPath, { force: true });
2941
- }
2942
- const response = await fetchImpl(asset.downloadUrl);
2943
- if (!response.ok) {
2944
- throw new CliError(
2945
- `Failed to download the Cloud SQL Auth Proxy v${asset.version} from ${asset.downloadUrl} (HTTP ${response.status}).`,
2946
- {
2947
- hint: "Check your network connection (and any corporate proxy settings), or set MERIDIAN_PROXY_PATH to a proxy binary you install yourself."
2948
- }
2949
- );
2950
- }
2951
- const buffer = await readResponseBody(response, options.onProgress);
2952
- const expected = await resolveExpectedChecksum(asset, fetchImpl);
2953
- const actual = sha256(buffer);
2954
- if (expected !== void 0 && actual !== expected) {
2955
- throw new CliError(
2956
- `Checksum mismatch for ${asset.assetName}: expected ${expected.slice(0, 12)}..., got ${actual.slice(0, 12)}....`,
2957
- {
2958
- hint: "This can happen after a corrupted download or an untrusted network. Try again; if it persists, report this."
2959
- }
2960
- );
2961
- }
2962
- const tmpPath = join4(dir, `.${asset.fileName}.${process.pid}.tmp`);
2963
- writeFileSync5(tmpPath, buffer);
2964
- chmodSync3(tmpPath, 493);
2965
- renameSync3(tmpPath, targetPath);
2966
- return targetPath;
2967
- }
2968
-
2969
- // src/lib/proxy/proxy.ts
2970
- import { spawn } from "child_process";
2971
- import { createConnection } from "net";
2972
- var PROBE_INTERVAL_MS = 150;
2973
- var KILL_GRACE_MS = 2e3;
2974
- var CRASH_BACKOFFS_MS = [200, 1e3, 3e3];
2975
- var MAX_CRASH_RESTARTS = CRASH_BACKOFFS_MS.length;
2976
- var REFRESH_RETRY_BACKOFFS_MS2 = [3e4, 6e4, 12e4];
2977
- var MIN_REFRESH_DELAY_MS2 = 6e4;
2978
- var LOG_RING_SIZE = 20;
2979
- var STDERR_TAIL_LINES = 10;
2980
- function buildProxyArgs(input2) {
2981
- return ["--address", input2.host, "--port", String(input2.port), input2.instanceConnectionName];
2982
- }
2983
- function buildProxyEnv(accessToken, base = process.env) {
2984
- return { ...base, CSQL_PROXY_TOKEN: accessToken };
2985
- }
2986
- function defaultProbe(host, port) {
2987
- return new Promise((resolve2) => {
2988
- const socket = createConnection({ host, port });
2989
- const finish = (ok) => {
2990
- socket.removeAllListeners();
2991
- socket.destroy();
2992
- resolve2(ok);
2993
- };
2994
- socket.once("connect", () => finish(true));
2995
- socket.once("error", () => finish(false));
2996
- socket.setTimeout(1e3, () => finish(false));
2997
- });
2998
- }
2999
- function computeRefreshDelayMs(token, refreshSkewMs, assumedLifetimeMs) {
3000
- let expiresAtMs;
3001
- if (token.expires_at !== void 0) {
3002
- const parsed = new Date(token.expires_at).getTime();
3003
- if (!Number.isNaN(parsed)) expiresAtMs = parsed;
3004
- }
3005
- const effectiveExpiryMs = expiresAtMs ?? Date.now() + assumedLifetimeMs;
3006
- return Math.max(effectiveExpiryMs - Date.now() - refreshSkewMs, MIN_REFRESH_DELAY_MS2);
3007
- }
3008
- function buildStartupFailureError(code, signal, lastLines) {
3009
- const tail = lastLines.slice(-STDERR_TAIL_LINES).join("\n");
3010
- const lower = tail.toLowerCase();
3011
- let hint2 = "Run `meridian dev` again; if this keeps happening, check the instance connection name and your network.";
3012
- if (lower.includes("iam") || lower.includes("permission") || lower.includes("forbidden")) {
3013
- hint2 = "The Meridian API's service account may be missing roles/cloudsql.client on the dev Cloud SQL instance. Contact Meridian support.";
3014
- } else if (lower.includes("invalid token") || lower.includes("401") || lower.includes("unauthorized")) {
3015
- hint2 = "The access token appears to have expired. Retry `meridian dev` to mint a fresh one.";
3016
- } else if (lower.includes("address already in use")) {
3017
- hint2 = "Another process is already listening on that port. Free it up, or retry with a different port.";
3018
- }
3019
- const summary = `The Cloud SQL Auth Proxy exited before it was ready (code ${code ?? "null"}, signal ${signal ?? "null"}).`;
3020
- return new CliError(tail === "" ? summary : `${summary}
3021
- ${tail}`, { hint: hint2 });
3022
- }
3023
- function waitForReady(child, probeImpl, host, port, timeoutMs, setTimeoutImpl, getLastLines) {
3024
- return new Promise((resolve2, reject) => {
3025
- let settled = false;
3026
- let pollTimer;
3027
- let timeoutTimer;
3028
- const cleanup = () => {
3029
- if (pollTimer !== void 0) clearTimeout(pollTimer);
3030
- if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
3031
- child.off("exit", onExit);
3032
- };
3033
- function onExit(code, signal) {
3034
- if (settled) return;
3035
- settled = true;
3036
- cleanup();
3037
- reject(buildStartupFailureError(code, signal, getLastLines()));
3038
- }
3039
- function poll() {
3040
- if (settled) return;
3041
- probeImpl(host, port).then((ok) => {
3042
- if (settled) return;
3043
- if (ok) {
3044
- settled = true;
3045
- cleanup();
3046
- resolve2();
3047
- } else {
3048
- pollTimer = setTimeoutImpl(poll, PROBE_INTERVAL_MS);
3049
- }
3050
- }).catch(() => {
3051
- if (!settled) pollTimer = setTimeoutImpl(poll, PROBE_INTERVAL_MS);
3052
- });
3053
- }
3054
- child.on("exit", onExit);
3055
- timeoutTimer = setTimeoutImpl(() => {
3056
- if (settled) return;
3057
- settled = true;
3058
- cleanup();
3059
- reject(
3060
- new CliError(`Timed out after ${timeoutMs}ms waiting for the Cloud SQL Auth Proxy to accept connections.`, {
3061
- hint: "The instance may be slow to reach, or readyTimeoutMs may need to be raised."
3062
- })
3063
- );
3064
- }, timeoutMs);
3065
- poll();
3066
- });
3067
- }
3068
- async function startProxy(options) {
3069
- const host = options.host ?? "127.0.0.1";
3070
- const spawnImpl = options.spawnImpl ?? spawn;
3071
- const probeImpl = options.probeImpl ?? defaultProbe;
3072
- const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
3073
- const refreshSkewMs = options.refreshSkewMs ?? 5 * 6e4;
3074
- const assumedLifetimeMs = options.assumedLifetimeMs ?? 55 * 6e4;
3075
- const readyTimeoutMs = options.readyTimeoutMs ?? 3e4;
3076
- const emit = (event) => options.onEvent?.(event);
3077
- let stopped = false;
3078
- let running = false;
3079
- let expectingExit = false;
3080
- let currentToken = options.token;
3081
- let child;
3082
- let refreshTimer;
3083
- let crashTimer;
3084
- let refreshBackoffIndex = 0;
3085
- let crashRestartsRemaining = MAX_CRASH_RESTARTS;
3086
- let attemptCounter = 0;
3087
- let cleanupFns = [];
3088
- const knownTokens = /* @__PURE__ */ new Set();
3089
- const logRing = [];
3090
- function trackToken(token) {
3091
- if (token.access_token !== "") knownTokens.add(token.access_token);
3092
- }
3093
- function redact(text) {
3094
- let result = text;
3095
- for (const token of knownTokens) result = result.split(token).join("<redacted>");
3096
- return result;
3097
- }
3098
- function pushLog(line) {
3099
- const safeLine = redact(line);
3100
- logRing.push(safeLine);
3101
- if (logRing.length > LOG_RING_SIZE) logRing.shift();
3102
- emit({ type: "log", line: safeLine });
3103
- }
3104
- function wireOutput(c) {
3105
- for (const stream of [c.stdout, c.stderr]) {
3106
- if (stream === null) continue;
3107
- let buffer = "";
3108
- const onData = (chunk) => {
3109
- buffer += chunk.toString();
3110
- const lines = buffer.split("\n");
3111
- buffer = lines.pop() ?? "";
3112
- for (const line of lines) pushLog(line);
3113
- };
3114
- stream.on("data", onData);
3115
- cleanupFns.push(() => stream.off("data", onData));
3116
- }
3117
- }
3118
- function wireExit(c) {
3119
- c.once("exit", (code, signal) => {
3120
- if (child !== c) return;
3121
- const wasRunning = running;
3122
- const wasExpected = expectingExit;
3123
- running = false;
3124
- expectingExit = false;
3125
- emit({ type: "exit", code, signal });
3126
- if (stopped || wasExpected || !wasRunning) return;
3127
- void handleCrash();
3128
- });
3129
- }
3130
- async function spawnChild() {
3131
- attemptCounter += 1;
3132
- emit({ type: "starting", attempt: attemptCounter });
3133
- const args = buildProxyArgs({
3134
- instanceConnectionName: options.instanceConnectionName,
3135
- host,
3136
- port: options.port
3137
- });
3138
- const c = spawnImpl(options.binaryPath, args, {
3139
- stdio: ["ignore", "pipe", "pipe"],
3140
- env: buildProxyEnv(currentToken.access_token, options.env)
3141
- });
3142
- child = c;
3143
- wireOutput(c);
3144
- wireExit(c);
3145
- try {
3146
- await waitForReady(c, probeImpl, host, options.port, readyTimeoutMs, setTimeoutImpl, () => logRing.slice());
3147
- } catch (error2) {
3148
- expectingExit = true;
3149
- await killChild(c);
3150
- if (child === c) child = void 0;
3151
- throw error2;
3152
- }
3153
- emit({ type: "ready", port: options.port });
3154
- }
3155
- function killChild(c) {
3156
- if (c.exitCode !== null || c.signalCode !== null) return Promise.resolve();
3157
- return new Promise((resolve2) => {
3158
- let settled = false;
3159
- let killTimer;
3160
- const onExit = () => {
3161
- if (settled) return;
3162
- settled = true;
3163
- if (killTimer !== void 0) clearTimeout(killTimer);
3164
- resolve2();
3165
- };
3166
- c.once("exit", onExit);
3167
- c.kill("SIGTERM");
3168
- killTimer = setTimeoutImpl(() => {
3169
- if (!settled) c.kill("SIGKILL");
3170
- }, KILL_GRACE_MS);
3171
- });
3172
- }
3173
- function scheduleRefresh(token) {
3174
- if (stopped || options.refreshToken === void 0) return;
3175
- if (refreshTimer !== void 0) clearTimeout(refreshTimer);
3176
- const delayMs = computeRefreshDelayMs(token, refreshSkewMs, assumedLifetimeMs);
3177
- refreshTimer = setTimeoutImpl(() => {
3178
- refreshTimer = void 0;
3179
- void handleRefreshFire();
3180
- }, delayMs);
3181
- }
3182
- function scheduleRefreshRetry() {
3183
- if (stopped) return;
3184
- const delayMs = REFRESH_RETRY_BACKOFFS_MS2[Math.min(refreshBackoffIndex, REFRESH_RETRY_BACKOFFS_MS2.length - 1)] ?? 12e4;
3185
- refreshBackoffIndex += 1;
3186
- if (refreshTimer !== void 0) clearTimeout(refreshTimer);
3187
- refreshTimer = setTimeoutImpl(() => {
3188
- refreshTimer = void 0;
3189
- void handleRefreshFire();
3190
- }, delayMs);
3191
- }
3192
- async function handleRefreshFire() {
3193
- if (stopped || options.refreshToken === void 0) return;
3194
- let newToken;
3195
- try {
3196
- newToken = await options.refreshToken();
3197
- } catch (error2) {
3198
- emit({ type: "error", message: redact(`Token refresh failed: ${describeError(error2)}`) });
3199
- scheduleRefreshRetry();
3200
- return;
3201
- }
3202
- if (stopped) return;
3203
- trackToken(newToken);
3204
- currentToken = newToken;
3205
- expectingExit = true;
3206
- emit({ type: "restarting", reason: "token-refresh" });
3207
- try {
3208
- const previous = child;
3209
- if (previous !== void 0) await killChild(previous);
3210
- await spawnChild();
3211
- } catch (error2) {
3212
- emit({ type: "error", message: redact(`Restart after token refresh failed: ${describeError(error2)}`) });
3213
- scheduleRefreshRetry();
3214
- return;
3215
- }
3216
- running = true;
3217
- refreshBackoffIndex = 0;
3218
- emit({ type: "token-refreshed", ...newToken.expires_at !== void 0 ? { expiresAt: newToken.expires_at } : {} });
3219
- scheduleRefresh(newToken);
3220
- }
3221
- async function handleCrash() {
3222
- if (stopped) return;
3223
- if (crashRestartsRemaining <= 0) {
3224
- const tail = logRing.slice(-STDERR_TAIL_LINES).join("\n");
3225
- emit({
3226
- type: "error",
3227
- message: `The Cloud SQL Auth Proxy crashed repeatedly and could not be restarted.${tail === "" ? "" : `
3228
- ${tail}`}`
3229
- });
3230
- return;
3231
- }
3232
- const attemptIndex = MAX_CRASH_RESTARTS - crashRestartsRemaining;
3233
- const delayMs = CRASH_BACKOFFS_MS[attemptIndex] ?? (CRASH_BACKOFFS_MS[CRASH_BACKOFFS_MS.length - 1] ?? 3e3);
3234
- crashRestartsRemaining -= 1;
3235
- emit({ type: "restarting", reason: "crash" });
3236
- crashTimer = setTimeoutImpl(() => {
3237
- crashTimer = void 0;
3238
- if (stopped) return;
3239
- spawnChild().then(() => {
3240
- running = true;
3241
- }).catch((error2) => {
3242
- emit({ type: "error", message: redact(describeError(error2)) });
3243
- void handleCrash();
3244
- });
3245
- }, delayMs);
3246
- }
3247
- async function refresh() {
3248
- if (stopped) return;
3249
- if (options.refreshToken === void 0) {
3250
- throw new CliError("Cannot refresh the Cloud SQL Auth Proxy token: no refreshToken() was configured.");
3251
- }
3252
- if (refreshTimer !== void 0) {
3253
- clearTimeout(refreshTimer);
3254
- refreshTimer = void 0;
3255
- }
3256
- refreshBackoffIndex = 0;
3257
- await handleRefreshFire();
3258
- }
3259
- async function stop() {
3260
- if (stopped) return;
3261
- stopped = true;
3262
- if (refreshTimer !== void 0) clearTimeout(refreshTimer);
3263
- refreshTimer = void 0;
3264
- if (crashTimer !== void 0) clearTimeout(crashTimer);
3265
- crashTimer = void 0;
3266
- for (const cleanup of cleanupFns) cleanup();
3267
- cleanupFns = [];
3268
- const current2 = child;
3269
- if (current2 !== void 0) {
3270
- expectingExit = true;
3271
- await killChild(current2);
3272
- }
3273
- running = false;
3274
- child = void 0;
3275
- }
3276
- trackToken(currentToken);
3277
- await spawnChild();
3278
- running = true;
3279
- scheduleRefresh(currentToken);
3280
- return {
3281
- host,
3282
- port: options.port,
3283
- instanceConnectionName: options.instanceConnectionName,
3284
- get running() {
3285
- return running;
3286
- },
3287
- refresh,
3288
- stop
3289
- };
3290
- }
3291
-
3292
2785
  // src/lib/signals.ts
3293
2786
  var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
3294
2787
  var SIGNAL_EXIT_CODES = {
@@ -3379,8 +2872,6 @@ function defaultSleep(ms) {
3379
2872
  }
3380
2873
  function resolveDeps(overrides) {
3381
2874
  return {
3382
- ensureProxyBinary,
3383
- startProxy,
3384
2875
  issueGatewayCertificate,
3385
2876
  startGatewayForwarder,
3386
2877
  findFreePort,
@@ -3453,15 +2944,6 @@ async function ensureDevDatabaseReady(client, appId, options = {}) {
3453
2944
  { success: (db) => `Developer database "${db.database}" is ready.` }
3454
2945
  );
3455
2946
  }
3456
- function toProxyToken(token) {
3457
- return {
3458
- access_token: token.access_token,
3459
- ...typeof token.expires_at === "string" ? { expires_at: token.expires_at } : {}
3460
- };
3461
- }
3462
- function formatMb(bytes) {
3463
- return (bytes / (1024 * 1024)).toFixed(1);
3464
- }
3465
2947
  function firstNonEmpty(...values) {
3466
2948
  for (const value of values) {
3467
2949
  if (typeof value === "string" && value.trim() !== "") return value;
@@ -3483,32 +2965,6 @@ function logEnvPatch(result, databaseUrl, wroteShadow) {
3483
2965
  }
3484
2966
  if (result.backedUp) detail(`Backed up the previous file to ${result.path}.backup`);
3485
2967
  }
3486
- function handleProxyEvent(event) {
3487
- switch (event.type) {
3488
- case "starting":
3489
- debug(`Starting the Cloud SQL Auth Proxy (attempt ${event.attempt}).`);
3490
- return;
3491
- case "ready":
3492
- debug(`Cloud SQL Auth Proxy listening on port ${event.port}.`);
3493
- return;
3494
- case "log":
3495
- debug(event.line);
3496
- return;
3497
- case "token-refreshed":
3498
- step("Refreshed the Cloud SQL Auth Proxy token.");
3499
- return;
3500
- case "restarting":
3501
- if (event.reason === "crash") warn("The Cloud SQL Auth Proxy crashed; restarting it.");
3502
- else debug("Restarting the Cloud SQL Auth Proxy with a refreshed token...");
3503
- return;
3504
- case "exit":
3505
- debug(`Cloud SQL Auth Proxy exited (code ${event.code ?? "null"}, signal ${event.signal ?? "null"}).`);
3506
- return;
3507
- case "error":
3508
- warn(`Cloud SQL Auth Proxy error: ${event.message} Restart \`meridian dev\` if the database becomes unreachable.`);
3509
- return;
3510
- }
3511
- }
3512
2968
  async function pickLocalPort(options, deps) {
3513
2969
  const findPortOptions = { start: DEFAULT_PORT_START };
3514
2970
  if (options.port !== void 0) findPortOptions.preferred = options.port;
@@ -3520,50 +2976,6 @@ async function pickLocalPort(options, deps) {
3520
2976
  }
3521
2977
  return port;
3522
2978
  }
3523
- async function startProxyEndpoint(client, appId, devDatabase, credentials, options, deps) {
3524
- const proxyToken = await withSpinner(
3525
- "Requesting a Cloud SQL Auth Proxy token...",
3526
- () => client.issueProxyToken(appId),
3527
- { success: "Issued a Cloud SQL Auth Proxy token." }
3528
- );
3529
- const binaryPath = await withSpinner(
3530
- "Preparing the Cloud SQL Auth Proxy...",
3531
- (spinner2) => {
3532
- const ensureOptions = {
3533
- onProgress: (received, total) => {
3534
- spinner2.update(
3535
- total !== void 0 ? `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)}/${formatMb(total)} MB)` : `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)} MB)`
3536
- );
3537
- }
3538
- };
3539
- return deps.ensureProxyBinary(ensureOptions);
3540
- },
3541
- { success: "Cloud SQL Auth Proxy is ready." }
3542
- );
3543
- const port = await pickLocalPort(options, deps);
3544
- const instanceConnectionName = firstNonEmpty(
3545
- credentials.instance_connection_name,
3546
- proxyToken.instance_connection_name,
3547
- devDatabase.instance_connection_name
3548
- );
3549
- if (instanceConnectionName === void 0) {
3550
- throw new CliError("The Meridian API did not return a Cloud SQL instance connection name.", {
3551
- hint: "Try again, or contact Meridian support if this persists."
3552
- });
3553
- }
3554
- return withSpinner(
3555
- `Starting the Cloud SQL Auth Proxy on 127.0.0.1:${port}...`,
3556
- () => deps.startProxy({
3557
- binaryPath,
3558
- instanceConnectionName,
3559
- token: toProxyToken(proxyToken),
3560
- port,
3561
- ...options.refreshToken !== false ? { refreshToken: async () => toProxyToken(await client.issueProxyToken(appId)) } : {},
3562
- onEvent: handleProxyEvent
3563
- }),
3564
- { success: `Cloud SQL Auth Proxy listening on 127.0.0.1:${port}.` }
3565
- );
3566
- }
3567
2979
  function handleForwarderEvent(event) {
3568
2980
  switch (event.type) {
3569
2981
  case "ready":
@@ -3616,8 +3028,8 @@ async function startDevSession(options = {}) {
3616
3028
  success: (creds) => `Fetched credentials for database "${creds.database}"${typeof creds.expires_at === "string" ? ` (expire ${creds.expires_at})` : ""}.`
3617
3029
  }
3618
3030
  );
3619
- const access = devDatabase.access === "gateway" ? "gateway" : "proxy";
3620
- proxy = access === "gateway" ? await startGatewayEndpoint(client, appId, credentials, options, deps) : await startProxyEndpoint(client, appId, devDatabase, credentials, options, deps);
3031
+ const access = "gateway";
3032
+ proxy = await startGatewayEndpoint(client, appId, credentials, options, deps);
3621
3033
  const connection = connectionInfoFromCredentials(credentials, { host: proxy.host, port: proxy.port });
3622
3034
  if (options.waitForDb !== false) {
3623
3035
  await withSpinner(
@@ -3918,7 +3330,7 @@ function registerDbCredentialCommand(db) {
3918
3330
 
3919
3331
  // src/commands/db-shell-repl.ts
3920
3332
  import { accessSync, constants as fsConstants } from "fs";
3921
- import { delimiter as delimiter2, join as join5 } from "path";
3333
+ import { delimiter as delimiter2, join as join4 } from "path";
3922
3334
  import { createInterface } from "readline";
3923
3335
  function candidateNames(name) {
3924
3336
  if (process.platform !== "win32") return [name];
@@ -3933,7 +3345,7 @@ function findExecutable(name) {
3933
3345
  const candidates = candidateNames(name);
3934
3346
  for (const dir of dirs) {
3935
3347
  for (const candidate of candidates) {
3936
- const fullPath = join5(dir, candidate);
3348
+ const fullPath = join4(dir, candidate);
3937
3349
  try {
3938
3350
  accessSync(fullPath, fsConstants.X_OK);
3939
3351
  return fullPath;
@@ -4068,7 +3480,7 @@ function defaultDbDeps() {
4068
3480
  function hasSeedConfigured(root) {
4069
3481
  let parsed;
4070
3482
  try {
4071
- parsed = JSON.parse(readFileSync7(join6(root, "package.json"), "utf8"));
3483
+ parsed = JSON.parse(readFileSync6(join5(root, "package.json"), "utf8"));
4072
3484
  } catch {
4073
3485
  return false;
4074
3486
  }
@@ -4247,7 +3659,7 @@ function registerDbCommand(program) {
4247
3659
  db.action(() => {
4248
3660
  db.outputHelp();
4249
3661
  });
4250
- db.command("reset").description("Drop and recreate every table in the developer 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) => {
3662
+ db.command("reset").description("Drop and recreate every table in the developer 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 database gateway forwarder", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to update (defaults to .env)").action(async (opts) => {
4251
3663
  await runAndSetExitCode2(
4252
3664
  () => runDbReset({
4253
3665
  ...opts.migrate === true ? { migrate: true } : {},
@@ -4257,7 +3669,7 @@ function registerDbCommand(program) {
4257
3669
  })
4258
3670
  );
4259
3671
  });
4260
- 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) => {
3672
+ db.command("seed").description("Run `npx prisma db seed` with the database connection up and DATABASE_URL set.").option("--port <port>", "local port for the database gateway forwarder", (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) => {
4261
3673
  await runAndSetExitCode2(
4262
3674
  () => runDbSeed({
4263
3675
  ...opts.port !== void 0 ? { port: opts.port } : {},
@@ -4266,7 +3678,7 @@ function registerDbCommand(program) {
4266
3678
  })
4267
3679
  );
4268
3680
  });
4269
- db.command("shell").description("Open a SQL shell against the developer 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) => {
3681
+ db.command("shell").description("Open a SQL shell against the developer database (the `mysql` client if available, otherwise a built-in REPL).").option("--port <port>", "local port for the database gateway forwarder", (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) => {
4270
3682
  await runAndSetExitCode2(
4271
3683
  () => runDbShell({
4272
3684
  ...opts.port !== void 0 ? { port: opts.port } : {},
@@ -4405,7 +3817,7 @@ async function runDev(options = {}) {
4405
3817
  }
4406
3818
  }
4407
3819
  function registerDevCommand(program) {
4408
- program.command("dev").description("Run the managed developer 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) => {
3820
+ program.command("dev").description("Run the managed developer database proxy and your app's dev server together.").option("--port <port>", "preferred local port for the database gateway forwarder").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) => {
4409
3821
  const port = parsePortOption(opts.port);
4410
3822
  const code = await runDev({
4411
3823
  ...command.length > 0 ? { command } : {},
@@ -4418,8 +3830,8 @@ function registerDevCommand(program) {
4418
3830
  }
4419
3831
 
4420
3832
  // src/commands/link.ts
4421
- import { existsSync as existsSync5 } from "fs";
4422
- import { join as join7 } from "path";
3833
+ import { existsSync as existsSync4 } from "fs";
3834
+ import { join as join6 } from "path";
4423
3835
  function defaultDeps3() {
4424
3836
  return { createClient: createApiClient };
4425
3837
  }
@@ -4438,7 +3850,7 @@ async function runLink(options = {}) {
4438
3850
  const deps = { ...defaultDeps3(), ...options.deps };
4439
3851
  const json = isJsonOutput();
4440
3852
  const root = resolveProjectRoot(workingDirectory()) ?? workingDirectory();
4441
- if (!existsSync5(join7(root, "package.json"))) {
3853
+ if (!existsSync4(join6(root, "package.json"))) {
4442
3854
  log(`No package.json found in ${style.path(root)}.`);
4443
3855
  const proceed = await confirmAction({
4444
3856
  message: "Link this directory to a Meridian app anyway?",
@@ -4656,7 +4068,8 @@ async function runLogin(options = {}) {
4656
4068
  const json = isJsonOutput();
4657
4069
  if (options.force !== true) {
4658
4070
  const auth = getAuth();
4659
- if (auth !== null) {
4071
+ const sameApi = auth !== null && storedTokenApiBaseUrl() === resolveApiBaseUrl(getGlobalOptions().apiUrl);
4072
+ if (auth !== null && sameApi) {
4660
4073
  const client2 = deps.createClient({ requireAuth: false, token: auth.token });
4661
4074
  try {
4662
4075
  const me = await client2.getMe();
@@ -4683,7 +4096,9 @@ async function runLogin(options = {}) {
4683
4096
  hostname,
4684
4097
  onCode: async (code) => {
4685
4098
  const target = code.verification_uri_complete ?? code.verification_uri;
4686
- if (!json) {
4099
+ if (json) {
4100
+ hint(`Enter the code ${formatUserCode(code.user_code)} at ${code.verification_uri} to approve this login.`);
4101
+ } else {
4687
4102
  log();
4688
4103
  log(` ${style.bold(formatUserCode(code.user_code))}`);
4689
4104
  log();
@@ -4709,12 +4124,11 @@ async function runLogin(options = {}) {
4709
4124
  });
4710
4125
  wait.stop();
4711
4126
  const user = toConfigUser(tokenResponse.user);
4712
- const storeApiBaseUrl = client.baseUrl !== DEFAULT_API_BASE_URL;
4713
4127
  updateConfig({
4714
4128
  token: tokenResponse.token,
4715
4129
  ...tokenResponse.expires_at !== void 0 ? { token_expires_at: tokenResponse.expires_at } : {},
4716
4130
  user,
4717
- ...storeApiBaseUrl ? { api_base_url: client.baseUrl } : {}
4131
+ api_base_url: client.baseUrl
4718
4132
  });
4719
4133
  if (json) {
4720
4134
  printLoginJson(user, tokenResponse.expires_at, client.baseUrl);
@@ -4752,7 +4166,7 @@ function registerLogoutCommand(program) {
4752
4166
 
4753
4167
  // src/lib/db/snapshot.ts
4754
4168
  import { mkdir, readFile, readdir, rm, stat } from "fs/promises";
4755
- import { join as join8 } from "path";
4169
+ import { join as join7 } from "path";
4756
4170
  function snapshotFileName(date = /* @__PURE__ */ new Date()) {
4757
4171
  return `${date.toISOString().replace(/[:.]/g, "-")}.sql`;
4758
4172
  }
@@ -4771,7 +4185,7 @@ async function createSnapshot(info, options) {
4771
4185
  }
4772
4186
  const dump = options.dumpImpl ?? await loadMysqldump();
4773
4187
  await mkdir(options.dir, { recursive: true });
4774
- const path = join8(options.dir, snapshotFileName(createdAt));
4188
+ const path = join7(options.dir, snapshotFileName(createdAt));
4775
4189
  try {
4776
4190
  await dump({
4777
4191
  connection: {
@@ -4977,7 +4391,7 @@ async function listSnapshots(dir) {
4977
4391
  const names = entries.filter((name) => name.endsWith(".sql")).sort().reverse();
4978
4392
  const results = [];
4979
4393
  for (const name of names) {
4980
- const path = join8(dir, name);
4394
+ const path = join7(dir, name);
4981
4395
  const { size } = await stat(path);
4982
4396
  results.push({ path, name, bytes: size, createdAt: parseSnapshotDate(name) });
4983
4397
  }
@@ -5173,7 +4587,7 @@ function registerMigrateCommand(program) {
5173
4587
  ).option("--skip-snapshot", "run without a pre-migration snapshot (no rollback is possible)").option(
5174
4588
  "--allow-foreign-migrations",
5175
4589
  "run even when prisma/migrations was generated for SQLite or Postgres and cannot apply to MySQL"
5176
- ).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) => {
4590
+ ).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 database gateway forwarder").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) => {
5177
4591
  const keepSnapshots = parseIntegerOption(opts.keepSnapshots, "--keep-snapshots", { min: 0 });
5178
4592
  const port = parsePortOption(opts.port);
5179
4593
  const exitCode = await runMigrate({
@@ -5189,6 +4603,208 @@ function registerMigrateCommand(program) {
5189
4603
  });
5190
4604
  }
5191
4605
 
4606
+ // src/commands/skills.ts
4607
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
4608
+ import { dirname as dirname5, join as join8, relative as relative2 } from "path";
4609
+ import { fileURLToPath } from "url";
4610
+ var MCP_SERVER_URL = "https://mcp.the-meridian.ai/mcp";
4611
+ var SERVER_NAME = "meridian";
4612
+ var AGENT_IDS = ["claude", "codex", "cursor"];
4613
+ var AGENTS = {
4614
+ claude: {
4615
+ label: "Claude Code",
4616
+ skillDir: ".claude/skills/meridian",
4617
+ markers: [".claude", "CLAUDE.md", ".mcp.json"],
4618
+ mcpConfig: ".mcp.json",
4619
+ connectStep: "In Claude Code, run `/mcp` and approve Meridian in the browser."
4620
+ },
4621
+ codex: {
4622
+ label: "Codex",
4623
+ skillDir: ".agents/skills/meridian",
4624
+ markers: [".codex", ".agents", "AGENTS.md"],
4625
+ mcpConfig: ".codex/config.toml",
4626
+ connectStep: "Trust this project in Codex, then run `codex mcp login meridian`."
4627
+ },
4628
+ cursor: {
4629
+ label: "Cursor",
4630
+ skillDir: ".cursor/skills/meridian",
4631
+ markers: [".cursor", ".cursorrules"],
4632
+ mcpConfig: ".cursor/mcp.json",
4633
+ connectStep: "In Cursor, open Customize in the sidebar (Settings > Tools & MCP before Cursor 3) and connect Meridian."
4634
+ }
4635
+ };
4636
+ function bundledSkillDir() {
4637
+ const candidates = [
4638
+ new URL("./skill", import.meta.url),
4639
+ new URL("../../../skill/plugins/meridian/skills/meridian", import.meta.url)
4640
+ ].map((url) => fileURLToPath(url));
4641
+ const found = candidates.find((dir) => existsSync5(join8(dir, "SKILL.md")));
4642
+ if (found === void 0) {
4643
+ throw new CliError("This build of the Meridian CLI does not include the agent skill.", {
4644
+ hint: "Reinstall it with `npm install -g @the-meridian/cli@latest`."
4645
+ });
4646
+ }
4647
+ return found;
4648
+ }
4649
+ function parseAgents(raw) {
4650
+ const names = raw.split(",").map((name) => name.trim().toLowerCase()).filter((name) => name !== "");
4651
+ if (names.includes("all")) return [...AGENT_IDS];
4652
+ const unknown = names.filter((name) => !AGENT_IDS.includes(name));
4653
+ if (unknown.length > 0 || names.length === 0) {
4654
+ throw new CliError(`Unknown agent: ${unknown.join(", ") || raw}.`, {
4655
+ hint: `Use one or more of ${AGENT_IDS.join(", ")}, comma-separated, or \`all\`.`
4656
+ });
4657
+ }
4658
+ return [...new Set(names)];
4659
+ }
4660
+ function detectAgents(root) {
4661
+ const detected = AGENT_IDS.filter((id) => AGENTS[id].markers.some((marker) => existsSync5(join8(root, marker))));
4662
+ return detected.length > 0 ? detected : [...AGENT_IDS];
4663
+ }
4664
+ function listFiles(dir, base = dir) {
4665
+ return readdirSync2(dir, { withFileTypes: true }).flatMap((entry) => {
4666
+ const path = join8(dir, entry.name);
4667
+ return entry.isDirectory() ? listFiles(path, base) : [relative2(base, path)];
4668
+ }).sort();
4669
+ }
4670
+ function sameTree(a, b) {
4671
+ const filesA = listFiles(a);
4672
+ const filesB = listFiles(b);
4673
+ if (filesA.join("\n") !== filesB.join("\n")) return false;
4674
+ return filesA.every((file) => readFileSync7(join8(a, file)).equals(readFileSync7(join8(b, file))));
4675
+ }
4676
+ async function installSkill(root, agent, sourceDir, confirmImpl) {
4677
+ const target = join8(root, AGENTS[agent].skillDir);
4678
+ if (existsSync5(target)) {
4679
+ if (sameTree(sourceDir, target)) return "unchanged";
4680
+ const replace = await confirmAction({
4681
+ message: `Replace ${AGENTS[agent].skillDir} with this CLI's version? Changes made in that folder are lost.`,
4682
+ default: true,
4683
+ ...confirmImpl !== void 0 ? { promptImpl: confirmImpl } : {}
4684
+ });
4685
+ if (!replace) return "skipped";
4686
+ rmSync2(target, { recursive: true, force: true });
4687
+ cpSync(sourceDir, target, { recursive: true });
4688
+ return "updated";
4689
+ }
4690
+ mkdirSync4(dirname5(target), { recursive: true });
4691
+ cpSync(sourceDir, target, { recursive: true });
4692
+ return "created";
4693
+ }
4694
+ function readJsonObject(path) {
4695
+ if (!existsSync5(path)) return {};
4696
+ let parsed;
4697
+ try {
4698
+ parsed = JSON.parse(readFileSync7(path, "utf8"));
4699
+ } catch {
4700
+ throw new CliError(`${path} is not valid JSON, so the Meridian MCP server was not added to it.`, {
4701
+ hint: "Fix the file, or rerun with `--no-mcp` and add the server by hand."
4702
+ });
4703
+ }
4704
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4705
+ throw new CliError(`${path} does not hold a JSON object, so the Meridian MCP server was not added to it.`, {
4706
+ hint: "Fix the file, or rerun with `--no-mcp` and add the server by hand."
4707
+ });
4708
+ }
4709
+ return parsed;
4710
+ }
4711
+ function mergeJsonServer(path, server) {
4712
+ const config = readJsonObject(path);
4713
+ const existing = config.mcpServers;
4714
+ const servers = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
4715
+ const current2 = servers[SERVER_NAME];
4716
+ if (current2 !== void 0) {
4717
+ const url = current2?.url;
4718
+ return url === MCP_SERVER_URL ? "present" : "conflict";
4719
+ }
4720
+ config.mcpServers = { ...servers, [SERVER_NAME]: server };
4721
+ mkdirSync4(dirname5(path), { recursive: true });
4722
+ writeFileSync5(path, `${JSON.stringify(config, null, 2)}
4723
+ `);
4724
+ return "added";
4725
+ }
4726
+ function mergeTomlServer(path) {
4727
+ const current2 = existsSync5(path) ? readFileSync7(path, "utf8") : "";
4728
+ if (/^\s*\[mcp_servers\.meridian\]\s*$/m.test(current2)) {
4729
+ return current2.includes(`"${MCP_SERVER_URL}"`) ? "present" : "conflict";
4730
+ }
4731
+ const separator = current2 === "" || current2.endsWith("\n\n") ? "" : current2.endsWith("\n") ? "\n" : "\n\n";
4732
+ mkdirSync4(dirname5(path), { recursive: true });
4733
+ writeFileSync5(path, `${current2}${separator}[mcp_servers.meridian]
4734
+ url = "${MCP_SERVER_URL}"
4735
+ `);
4736
+ return "added";
4737
+ }
4738
+ function installMcp(root, agent) {
4739
+ const path = join8(root, AGENTS[agent].mcpConfig);
4740
+ switch (agent) {
4741
+ case "claude":
4742
+ return mergeJsonServer(path, { type: "http", url: MCP_SERVER_URL });
4743
+ case "cursor":
4744
+ return mergeJsonServer(path, { url: MCP_SERVER_URL });
4745
+ case "codex":
4746
+ return mergeTomlServer(path);
4747
+ }
4748
+ }
4749
+ var SKILL_MESSAGES = {
4750
+ created: "installed",
4751
+ updated: "updated",
4752
+ unchanged: "already up to date",
4753
+ skipped: "left as it was"
4754
+ };
4755
+ async function runSkillsInstall(options = {}) {
4756
+ const json = isJsonOutput();
4757
+ const root = readProject(workingDirectory())?.root ?? workingDirectory();
4758
+ const sourceDir = options.deps?.sourceDir ?? bundledSkillDir();
4759
+ const agents = options.agents ?? detectAgents(root);
4760
+ const result = { root, skills: [], mcp: [] };
4761
+ for (const agent of agents) {
4762
+ const status = await installSkill(root, agent, sourceDir, options.deps?.confirmImpl);
4763
+ result.skills.push({ agent, path: AGENTS[agent].skillDir, status });
4764
+ }
4765
+ if (options.mcp !== false) {
4766
+ for (const agent of agents) {
4767
+ result.mcp.push({ agent, path: AGENTS[agent].mcpConfig, status: installMcp(root, agent) });
4768
+ }
4769
+ }
4770
+ if (json) {
4771
+ printJson(result);
4772
+ return result;
4773
+ }
4774
+ for (const skill of result.skills) {
4775
+ success(`${AGENTS[skill.agent].label}: skill ${SKILL_MESSAGES[skill.status]} in ${skill.path}`);
4776
+ }
4777
+ for (const entry of result.mcp) {
4778
+ const label = AGENTS[entry.agent].label;
4779
+ if (entry.status === "conflict") {
4780
+ warn(`${label}: ${entry.path} already has a "${SERVER_NAME}" server with another URL. It was left alone.`);
4781
+ } else {
4782
+ success(`${label}: Meridian MCP server ${entry.status === "added" ? "added to" : "already in"} ${entry.path}`);
4783
+ }
4784
+ }
4785
+ const toConnect = result.mcp.filter((entry) => entry.status === "added");
4786
+ if (toConnect.length > 0) {
4787
+ log();
4788
+ for (const entry of toConnect) hint(AGENTS[entry.agent].connectStep);
4789
+ }
4790
+ hint("Commit these folders so your teammates' agents get the skill too.");
4791
+ return result;
4792
+ }
4793
+ function registerSkillsCommand(program) {
4794
+ const skills = program.command("skills").description("Install the Meridian agent skill for Claude Code, Codex and Cursor.");
4795
+ skills.action(() => {
4796
+ skills.outputHelp();
4797
+ });
4798
+ skills.command("install").description(
4799
+ "Write the Meridian skill into this project and add the Meridian MCP server to each agent's config. Run it again after upgrading the CLI to update the skill."
4800
+ ).option("--agent <agents>", `agents to install for: ${AGENT_IDS.join(", ")} or all (default: the ones this project uses)`).option("--no-mcp", "install the skill only, without the MCP server config").action(async (opts) => {
4801
+ await runSkillsInstall({
4802
+ ...opts.agent !== void 0 ? { agents: parseAgents(opts.agent) } : {},
4803
+ ...opts.mcp === false ? { mcp: false } : {}
4804
+ });
4805
+ });
4806
+ }
4807
+
5192
4808
  // src/commands/unlink.ts
5193
4809
  async function runUnlink(options = {}) {
5194
4810
  const deps = { ...options.deps };
@@ -5421,6 +5037,7 @@ function buildProgram() {
5421
5037
  registerDevCommand(program);
5422
5038
  registerMigrateCommand(program);
5423
5039
  registerDbCommand(program);
5040
+ registerSkillsCommand(program);
5424
5041
  addGlobalFlagsToSubcommands(program);
5425
5042
  return program;
5426
5043
  }
@@ -5436,7 +5053,7 @@ function isMainModule() {
5436
5053
  const entry = process.argv[1];
5437
5054
  if (entry === void 0) return false;
5438
5055
  try {
5439
- return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
5056
+ return realpathSync(entry) === realpathSync(fileURLToPath2(import.meta.url));
5440
5057
  } catch {
5441
5058
  return false;
5442
5059
  }