@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/CHANGELOG.md +17 -0
- package/README.md +21 -17
- package/dist/index.js +252 -635
- package/dist/index.js.map +1 -1
- package/dist/skill/SKILL.md +72 -0
- package/dist/skill/references/cli.md +54 -0
- package/dist/skill/references/hosting.md +54 -0
- package/dist/skill/references/sdk.md +71 -0
- package/dist/skill/references/setup.md +68 -0
- package/package.json +4 -3
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
|
|
10
|
-
import { join as
|
|
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
|
|
85
|
-
* gateway with a per-developer certificate)
|
|
86
|
-
*
|
|
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
|
|
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
|
|
751
|
-
const connection = await
|
|
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
|
|
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
|
|
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 =
|
|
3620
|
-
proxy =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4422
|
-
import { join as
|
|
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 (!
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
5056
|
+
return realpathSync(entry) === realpathSync(fileURLToPath2(import.meta.url));
|
|
5440
5057
|
} catch {
|
|
5441
5058
|
return false;
|
|
5442
5059
|
}
|