metheus-governance-mcp-cli 0.2.36 → 0.2.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/cli.mjs +233 -4
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -103,6 +103,11 @@ Antigravity note:
|
|
|
103
103
|
Cursor note:
|
|
104
104
|
- this CLI manages MCP registration via Cursor global MCP config (`~/.cursor/mcp.json`).
|
|
105
105
|
|
|
106
|
+
Tool naming compatibility:
|
|
107
|
+
- Claude/Codex/Gemini keep canonical MCP tool names (`project.summary`, `ctxpack.merge.brief`, ...).
|
|
108
|
+
- Cursor/Antigravity sessions may receive safe aliases (`project_summary`, `ctxpack_merge_brief`, ...).
|
|
109
|
+
- Proxy maps alias calls back to canonical names automatically.
|
|
110
|
+
|
|
106
111
|
Local bootstrap tools exposed by proxy:
|
|
107
112
|
|
|
108
113
|
- `project.summary`
|
|
@@ -239,6 +244,7 @@ npm publish --access public
|
|
|
239
244
|
|
|
240
245
|
```bash
|
|
241
246
|
npm run check
|
|
247
|
+
npm run test:compat
|
|
242
248
|
npm run publish:dry
|
|
243
249
|
```
|
|
244
250
|
|
|
@@ -255,3 +261,25 @@ If npm account uses 2FA, pass OTP:
|
|
|
255
261
|
```bash
|
|
256
262
|
node release.mjs --otp <6-digit-code>
|
|
257
263
|
```
|
|
264
|
+
|
|
265
|
+
## Regression and smoke checks
|
|
266
|
+
|
|
267
|
+
Local compatibility selftest (no network):
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
npm run test:compat
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Proxy smoke test (initialize -> tools/list -> project summary -> ctxpack local sync):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npm run smoke:proxy -- --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --workspace-dir <workspace_path>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Optional:
|
|
280
|
+
- `--client cursor-vscode` (default) or `--client antigravity`
|
|
281
|
+
- `--base-url https://metheus.gesiaplatform.com/governance/mcp`
|
|
282
|
+
|
|
283
|
+
Workspace fallback guardrail:
|
|
284
|
+
- If `METHEUS_WORKSPACE_DIR` is accidentally set to a non-existing boolean suffix path like `C:\code_test\true`,
|
|
285
|
+
proxy now recovers to the parent workspace (`C:\code_test`) instead of pinning to the bad path.
|
package/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import process from "node:process";
|
|
6
7
|
import readline from "node:readline";
|
|
@@ -42,6 +43,7 @@ function printUsage() {
|
|
|
42
43
|
` ${cmd} setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--workspace-fallback-dir <path>] [--name <server_name>]`,
|
|
43
44
|
` ${cmd} doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>]`,
|
|
44
45
|
` ${cmd} proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--include-drafts <true|false>] [--auto-pull-on-conflict <true|false>] [--timeout-seconds <n>]`,
|
|
46
|
+
` ${cmd} selftest [--json <true|false>]`,
|
|
45
47
|
` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path|auto>] [--paths <csv>] [--timeout-seconds <n>]`,
|
|
46
48
|
` ${cmd} auth status`,
|
|
47
49
|
` ${cmd} auth login [--base-url <url>] [--flow <auto|device|callback|manual>] [--keycloak-url <url>] [--realm <name>] [--client-id <id>] [--open-browser <true|false>] [--callback-port <n>] [--timeout-seconds <n>] [--manual <true|false>]`,
|
|
@@ -604,7 +606,7 @@ function extractWeakWorkspaceCandidateFromRequest(requestObj) {
|
|
|
604
606
|
}
|
|
605
607
|
|
|
606
608
|
function extractStrongWorkspaceCandidateFromEnv() {
|
|
607
|
-
const
|
|
609
|
+
const rawCandidates = [
|
|
608
610
|
process.env.METHEUS_WORKSPACE_DIR,
|
|
609
611
|
process.env.METHEUS_WORKSPACE_URI,
|
|
610
612
|
process.env.CODEX_WORKSPACE_DIR,
|
|
@@ -612,8 +614,12 @@ function extractStrongWorkspaceCandidateFromEnv() {
|
|
|
612
614
|
process.env.CLAUDE_WORKSPACE_DIR,
|
|
613
615
|
process.env.CLAUDE_WORKSPACE_URI,
|
|
614
616
|
process.env.CLAUDE_PROJECT_DIR,
|
|
615
|
-
]
|
|
616
|
-
|
|
617
|
+
];
|
|
618
|
+
for (const rawCandidate of rawCandidates) {
|
|
619
|
+
const normalized = sanitizeWorkspaceFallbackEnvCandidate(rawCandidate);
|
|
620
|
+
if (normalized) return normalized;
|
|
621
|
+
}
|
|
622
|
+
return "";
|
|
617
623
|
}
|
|
618
624
|
|
|
619
625
|
function extractWorkspaceCandidateFromEnv() {
|
|
@@ -635,6 +641,35 @@ function extractWeakWorkspaceCandidateFromEnv() {
|
|
|
635
641
|
return sanitizeWorkspaceCandidate(rawCandidate);
|
|
636
642
|
}
|
|
637
643
|
|
|
644
|
+
function sanitizeWorkspaceFallbackEnvCandidate(rawCandidate) {
|
|
645
|
+
const input = String(rawCandidate || "").trim();
|
|
646
|
+
if (!input) return "";
|
|
647
|
+
|
|
648
|
+
const direct = sanitizeWorkspaceCandidate(input);
|
|
649
|
+
if (direct && fs.existsSync(direct)) {
|
|
650
|
+
return direct;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const fileCandidate = fileURIToLocalPath(input);
|
|
654
|
+
const candidate = firstNonEmptyString([fileCandidate, input]);
|
|
655
|
+
if (!candidate) return direct;
|
|
656
|
+
|
|
657
|
+
const resolved = resolveWorkspaceDir(candidate);
|
|
658
|
+
if (!resolved || isEditorInstallDirectory(resolved)) {
|
|
659
|
+
return direct;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const leaf = path.basename(resolved).trim().toLowerCase();
|
|
663
|
+
if ((leaf === "true" || leaf === "false") && !fs.existsSync(resolved)) {
|
|
664
|
+
const parent = sanitizeWorkspaceCandidate(path.dirname(resolved));
|
|
665
|
+
if (parent && fs.existsSync(parent)) {
|
|
666
|
+
return parent;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return direct;
|
|
671
|
+
}
|
|
672
|
+
|
|
638
673
|
function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs) {
|
|
639
674
|
const strongRequestCandidate = extractStrongWorkspaceCandidateFromRequest(requestObj, toolArgs);
|
|
640
675
|
const strongEnvCandidate = extractStrongWorkspaceCandidateFromEnv();
|
|
@@ -3491,6 +3526,83 @@ function ensureArray(value) {
|
|
|
3491
3526
|
return Array.isArray(value) ? value : [];
|
|
3492
3527
|
}
|
|
3493
3528
|
|
|
3529
|
+
function normalizeSafeToolAliasName(rawName) {
|
|
3530
|
+
const base = String(rawName || "")
|
|
3531
|
+
.trim()
|
|
3532
|
+
.replace(/[^a-zA-Z0-9_]/g, "_")
|
|
3533
|
+
.replace(/_+/g, "_")
|
|
3534
|
+
.replace(/^_+|_+$/g, "");
|
|
3535
|
+
if (!base) return "";
|
|
3536
|
+
if (/^[0-9]/.test(base)) return `tool_${base}`;
|
|
3537
|
+
return base;
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
function buildToolAliasMaps(tools) {
|
|
3541
|
+
const canonicalNames = new Set(
|
|
3542
|
+
ensureArray(tools)
|
|
3543
|
+
.map((tool) => String(tool?.name || "").trim())
|
|
3544
|
+
.filter(Boolean),
|
|
3545
|
+
);
|
|
3546
|
+
const aliasToCanonical = new Map();
|
|
3547
|
+
const canonicalToAlias = new Map();
|
|
3548
|
+
const reserved = new Set(canonicalNames);
|
|
3549
|
+
|
|
3550
|
+
for (const canonicalName of canonicalNames) {
|
|
3551
|
+
const aliasBase = normalizeSafeToolAliasName(canonicalName);
|
|
3552
|
+
if (!aliasBase || aliasBase === canonicalName) continue;
|
|
3553
|
+
let alias = aliasBase;
|
|
3554
|
+
let counter = 2;
|
|
3555
|
+
while (reserved.has(alias) || aliasToCanonical.has(alias)) {
|
|
3556
|
+
alias = `${aliasBase}_${counter}`;
|
|
3557
|
+
counter += 1;
|
|
3558
|
+
}
|
|
3559
|
+
aliasToCanonical.set(alias, canonicalName);
|
|
3560
|
+
canonicalToAlias.set(canonicalName, alias);
|
|
3561
|
+
reserved.add(alias);
|
|
3562
|
+
}
|
|
3563
|
+
return { aliasToCanonical, canonicalToAlias };
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
function applyToolAliasesToToolsListResponse(responseObj, canonicalToAlias) {
|
|
3567
|
+
const result = safeObject(responseObj.result);
|
|
3568
|
+
const tools = ensureArray(result.tools);
|
|
3569
|
+
result.tools = tools.map((tool) => {
|
|
3570
|
+
const safeTool = safeObject(tool);
|
|
3571
|
+
const currentName = String(safeTool.name || "").trim();
|
|
3572
|
+
const aliasName = canonicalToAlias.get(currentName);
|
|
3573
|
+
if (!aliasName) return safeTool;
|
|
3574
|
+
return { ...safeTool, name: aliasName };
|
|
3575
|
+
});
|
|
3576
|
+
responseObj.result = result;
|
|
3577
|
+
return responseObj;
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
function rewriteAliasedToolCallToCanonical(requestObj, aliasToCanonical) {
|
|
3581
|
+
if (!isJsonRpcMethod(requestObj, "tools/call")) return requestObj;
|
|
3582
|
+
const params = safeObject(requestObj.params);
|
|
3583
|
+
const currentName = String(params.name ?? params.tool_name ?? params.toolName ?? "").trim();
|
|
3584
|
+
if (!currentName) return requestObj;
|
|
3585
|
+
const canonicalName = String(aliasToCanonical.get(currentName) || "").trim();
|
|
3586
|
+
if (!canonicalName) return requestObj;
|
|
3587
|
+
|
|
3588
|
+
const nextParams = { ...params, name: canonicalName };
|
|
3589
|
+
if (Object.prototype.hasOwnProperty.call(nextParams, "tool_name")) {
|
|
3590
|
+
nextParams.tool_name = canonicalName;
|
|
3591
|
+
}
|
|
3592
|
+
if (Object.prototype.hasOwnProperty.call(nextParams, "toolName")) {
|
|
3593
|
+
nextParams.toolName = canonicalName;
|
|
3594
|
+
}
|
|
3595
|
+
return { ...requestObj, params: nextParams };
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
function shouldUseSafeToolAliasesForClient(initParamsRaw) {
|
|
3599
|
+
const initParams = safeObject(initParamsRaw);
|
|
3600
|
+
const clientInfo = safeObject(initParams.clientInfo);
|
|
3601
|
+
const name = String(clientInfo.name || "").trim().toLowerCase();
|
|
3602
|
+
if (!name) return false;
|
|
3603
|
+
return name.includes("cursor") || name.includes("antigravity");
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3494
3606
|
function injectWorkspaceDirIntoToolSchemas(tools) {
|
|
3495
3607
|
const workspaceDirProp = {
|
|
3496
3608
|
type: "string",
|
|
@@ -4044,6 +4156,9 @@ async function runProxy(flags) {
|
|
|
4044
4156
|
let lastRefreshError = "";
|
|
4045
4157
|
let sessionWorkspaceDir = "";
|
|
4046
4158
|
let sessionWorkspaceTrusted = false;
|
|
4159
|
+
let sessionUseSafeToolAliases = false;
|
|
4160
|
+
let sessionToolAliasToCanonical = new Map();
|
|
4161
|
+
let sessionToolCanonicalToAlias = new Map();
|
|
4047
4162
|
|
|
4048
4163
|
// Proxy-initiated requests (e.g., roots/list) pending client responses.
|
|
4049
4164
|
const pendingProxyRequests = new Map(); // id → callback(responseObj)
|
|
@@ -4122,7 +4237,7 @@ async function runProxy(flags) {
|
|
|
4122
4237
|
const line = String(lineRaw || "").trim();
|
|
4123
4238
|
if (!line) return;
|
|
4124
4239
|
|
|
4125
|
-
|
|
4240
|
+
let requestObj = tryJsonParse(line);
|
|
4126
4241
|
if (requestObj == null) {
|
|
4127
4242
|
writeProxyJson(jsonRpcError(null, -32700, "parse error"));
|
|
4128
4243
|
return;
|
|
@@ -4175,6 +4290,13 @@ async function runProxy(flags) {
|
|
|
4175
4290
|
return;
|
|
4176
4291
|
}
|
|
4177
4292
|
|
|
4293
|
+
if (isJsonRpcMethod(requestObj, "initialize") && shouldUseSafeToolAliasesForClient(requestObj?.params)) {
|
|
4294
|
+
sessionUseSafeToolAliases = true;
|
|
4295
|
+
}
|
|
4296
|
+
if (sessionUseSafeToolAliases) {
|
|
4297
|
+
requestObj = rewriteAliasedToolCallToCanonical(requestObj, sessionToolAliasToCanonical);
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4178
4300
|
const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
|
|
4179
4301
|
let strongRequestWorkspaceCandidate = "";
|
|
4180
4302
|
let weakRequestWorkspaceCandidate = "";
|
|
@@ -4430,6 +4552,13 @@ async function runProxy(flags) {
|
|
|
4430
4552
|
}
|
|
4431
4553
|
if (isJsonRpcMethod(requestObj, "tools/list")) {
|
|
4432
4554
|
patched = appendLocalToolToToolsList(patched);
|
|
4555
|
+
if (sessionUseSafeToolAliases) {
|
|
4556
|
+
const tools = ensureArray(safeObject(patched.result).tools);
|
|
4557
|
+
const aliasMaps = buildToolAliasMaps(tools);
|
|
4558
|
+
sessionToolAliasToCanonical = aliasMaps.aliasToCanonical;
|
|
4559
|
+
sessionToolCanonicalToAlias = aliasMaps.canonicalToAlias;
|
|
4560
|
+
patched = applyToolAliasesToToolsListResponse(patched, sessionToolCanonicalToAlias);
|
|
4561
|
+
}
|
|
4433
4562
|
} else if (isJsonRpcMethod(requestObj, "initialize")) {
|
|
4434
4563
|
patched = appendProjectHintToInitialize(patched, args);
|
|
4435
4564
|
// Log initialize params for workspace debugging.
|
|
@@ -5005,6 +5134,102 @@ function runSetup(flags) {
|
|
|
5005
5134
|
runSetupInternal(flags, { ensureOnly: false });
|
|
5006
5135
|
}
|
|
5007
5136
|
|
|
5137
|
+
function runSelftest(flags = {}) {
|
|
5138
|
+
const jsonMode = boolFromRaw(flags.json, false);
|
|
5139
|
+
const checks = [];
|
|
5140
|
+
const push = (name, ok, detail = "") => checks.push({ name, ok: Boolean(ok), detail: String(detail || "") });
|
|
5141
|
+
|
|
5142
|
+
const aliasMaps = buildToolAliasMaps([
|
|
5143
|
+
{ name: "project.summary" },
|
|
5144
|
+
{ name: "ctxpack.merge.brief" },
|
|
5145
|
+
{ name: "workitem.list" },
|
|
5146
|
+
]);
|
|
5147
|
+
const summaryAlias = String(aliasMaps.canonicalToAlias.get("project.summary") || "");
|
|
5148
|
+
const roundTrip = rewriteAliasedToolCallToCanonical(
|
|
5149
|
+
{
|
|
5150
|
+
jsonrpc: "2.0",
|
|
5151
|
+
id: 1,
|
|
5152
|
+
method: "tools/call",
|
|
5153
|
+
params: {
|
|
5154
|
+
name: summaryAlias,
|
|
5155
|
+
arguments: {},
|
|
5156
|
+
},
|
|
5157
|
+
},
|
|
5158
|
+
aliasMaps.aliasToCanonical,
|
|
5159
|
+
);
|
|
5160
|
+
const roundTripOk = summaryAlias === "project_summary" && String(roundTrip?.params?.name || "") === "project.summary";
|
|
5161
|
+
push(
|
|
5162
|
+
"alias_roundtrip_project_summary",
|
|
5163
|
+
roundTripOk,
|
|
5164
|
+
`alias=${summaryAlias || "(none)"} canonical=${String(roundTrip?.params?.name || "(none)")}`,
|
|
5165
|
+
);
|
|
5166
|
+
|
|
5167
|
+
const collisionMaps = buildToolAliasMaps([{ name: "a.b" }, { name: "a_b" }]);
|
|
5168
|
+
const aliasDot = String(collisionMaps.canonicalToAlias.get("a.b") || "");
|
|
5169
|
+
const aliasUnderscore = String(collisionMaps.canonicalToAlias.get("a_b") || "");
|
|
5170
|
+
const collisionOk = aliasDot === "a_b_2" && aliasUnderscore === "";
|
|
5171
|
+
push(
|
|
5172
|
+
"alias_collision_safe_suffix",
|
|
5173
|
+
collisionOk,
|
|
5174
|
+
`a.b->${aliasDot || "(none)"} a_b->${aliasUnderscore || "(none)"}`,
|
|
5175
|
+
);
|
|
5176
|
+
|
|
5177
|
+
let tempRoot = "";
|
|
5178
|
+
try {
|
|
5179
|
+
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-selftest-"));
|
|
5180
|
+
const parent = path.join(tempRoot, "workspace");
|
|
5181
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
5182
|
+
const suspicious = path.join(parent, "true");
|
|
5183
|
+
const recovered = sanitizeWorkspaceFallbackEnvCandidate(suspicious);
|
|
5184
|
+
const recoveredOk = normalizedPathForCompare(recovered) === normalizedPathForCompare(parent);
|
|
5185
|
+
push(
|
|
5186
|
+
"workspace_env_true_suffix_recovery",
|
|
5187
|
+
recoveredOk,
|
|
5188
|
+
`input=${suspicious} recovered=${recovered || "(none)"}`,
|
|
5189
|
+
);
|
|
5190
|
+
|
|
5191
|
+
fs.mkdirSync(suspicious, { recursive: true });
|
|
5192
|
+
const keptExisting = sanitizeWorkspaceFallbackEnvCandidate(suspicious);
|
|
5193
|
+
const keptExistingOk = normalizedPathForCompare(keptExisting) === normalizedPathForCompare(suspicious);
|
|
5194
|
+
push(
|
|
5195
|
+
"workspace_env_existing_true_dir_kept",
|
|
5196
|
+
keptExistingOk,
|
|
5197
|
+
`input=${suspicious} recovered=${keptExisting || "(none)"}`,
|
|
5198
|
+
);
|
|
5199
|
+
} catch (err) {
|
|
5200
|
+
push("workspace_env_guard_test_setup", false, String(err?.message || err));
|
|
5201
|
+
} finally {
|
|
5202
|
+
if (tempRoot) {
|
|
5203
|
+
try {
|
|
5204
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
5205
|
+
} catch {
|
|
5206
|
+
// ignore selftest cleanup error
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
const failed = checks.filter((row) => !row.ok);
|
|
5212
|
+
const payload = {
|
|
5213
|
+
ok: failed.length === 0,
|
|
5214
|
+
pass_count: checks.length - failed.length,
|
|
5215
|
+
fail_count: failed.length,
|
|
5216
|
+
checks,
|
|
5217
|
+
};
|
|
5218
|
+
|
|
5219
|
+
if (jsonMode) {
|
|
5220
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
5221
|
+
} else {
|
|
5222
|
+
process.stdout.write(`Selftest: ${payload.ok ? "PASS" : "FAIL"} (${payload.pass_count}/${checks.length})\n`);
|
|
5223
|
+
for (const row of checks) {
|
|
5224
|
+
process.stdout.write(`- ${row.ok ? "PASS" : "FAIL"} ${row.name}${row.detail ? ` :: ${row.detail}` : ""}\n`);
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
if (!payload.ok) {
|
|
5229
|
+
process.exitCode = 1;
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5008
5233
|
async function runBootstrap(flags) {
|
|
5009
5234
|
process.stdout.write("Bootstrap start.\n");
|
|
5010
5235
|
let resolved = resolveCurrentAccessToken();
|
|
@@ -5065,6 +5290,10 @@ async function main() {
|
|
|
5065
5290
|
await runDoctor(flags);
|
|
5066
5291
|
return;
|
|
5067
5292
|
}
|
|
5293
|
+
if (command === "selftest") {
|
|
5294
|
+
runSelftest(flags);
|
|
5295
|
+
return;
|
|
5296
|
+
}
|
|
5068
5297
|
if (command === "auth") {
|
|
5069
5298
|
await runAuth(rest);
|
|
5070
5299
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metheus-governance-mcp-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
4
4
|
"description": "Metheus Governance MCP CLI (setup + stdio proxy)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"check": "node --check cli.mjs && node --check release.mjs",
|
|
14
|
+
"test:compat": "node cli.mjs selftest --json",
|
|
15
|
+
"smoke:proxy": "node scripts/smoke-proxy.mjs",
|
|
14
16
|
"pack:dry": "npm pack --dry-run",
|
|
15
17
|
"publish:dry": "node release.mjs --dry-run",
|
|
16
18
|
"publish:public": "node release.mjs"
|