@mutmutco/cli 3.88.0 → 3.90.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/main.cjs +136 -48
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -4040,6 +4040,56 @@ var execFileP2 = (file, args, options = {}) => (
|
|
|
4040
4040
|
rawExecFileP2(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
|
|
4041
4041
|
);
|
|
4042
4042
|
var GIT_TIMEOUT_MS = DEFAULT_EXEC_TIMEOUT_MS;
|
|
4043
|
+
var ExecDeadlineError = class extends Error {
|
|
4044
|
+
constructor(step, timeoutMs, elapsedMs, killed) {
|
|
4045
|
+
super(
|
|
4046
|
+
`\`${step}\` did not finish within ${Math.round(timeoutMs / 1e3)}s (gave up after ${Math.round(elapsedMs / 1e3)}s). It stopped responding and did not exit when asked to, so the timeout alone could not end it; ${killed ? "its process tree was force-terminated" : "its process tree could NOT be terminated and may still be running"}.`
|
|
4047
|
+
);
|
|
4048
|
+
this.step = step;
|
|
4049
|
+
this.timeoutMs = timeoutMs;
|
|
4050
|
+
this.elapsedMs = elapsedMs;
|
|
4051
|
+
this.killed = killed;
|
|
4052
|
+
this.name = "ExecDeadlineError";
|
|
4053
|
+
}
|
|
4054
|
+
step;
|
|
4055
|
+
timeoutMs;
|
|
4056
|
+
elapsedMs;
|
|
4057
|
+
killed;
|
|
4058
|
+
};
|
|
4059
|
+
function killProcessTree(pid) {
|
|
4060
|
+
try {
|
|
4061
|
+
if (process.platform === "win32") {
|
|
4062
|
+
(0, import_node_child_process3.execFileSync)("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
4063
|
+
} else {
|
|
4064
|
+
process.kill(pid, "SIGKILL");
|
|
4065
|
+
}
|
|
4066
|
+
return true;
|
|
4067
|
+
} catch {
|
|
4068
|
+
return false;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
function execFileHard(file, args, options) {
|
|
4072
|
+
const { timeout, step, ...rest } = options;
|
|
4073
|
+
const started = Date.now();
|
|
4074
|
+
return new Promise((resolve5, reject) => {
|
|
4075
|
+
const child2 = (0, import_node_child_process3.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...rest, timeout: 0 }, (error, stdout, stderr) => {
|
|
4076
|
+
clearTimeout(timer);
|
|
4077
|
+
if (expired) return;
|
|
4078
|
+
if (error) reject(error);
|
|
4079
|
+
else resolve5({ stdout: String(stdout), stderr: String(stderr) });
|
|
4080
|
+
});
|
|
4081
|
+
let expired = false;
|
|
4082
|
+
const timer = setTimeout(() => {
|
|
4083
|
+
expired = true;
|
|
4084
|
+
const killed = child2.pid ? killProcessTree(child2.pid) : false;
|
|
4085
|
+
child2.stdout?.destroy();
|
|
4086
|
+
child2.stderr?.destroy();
|
|
4087
|
+
child2.unref();
|
|
4088
|
+
reject(new ExecDeadlineError(step, timeout, Date.now() - started, killed));
|
|
4089
|
+
}, timeout);
|
|
4090
|
+
timer.unref?.();
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4043
4093
|
var cachedGithubLogin;
|
|
4044
4094
|
async function githubLogin() {
|
|
4045
4095
|
cachedGithubLogin ??= execFileP2("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
@@ -8872,7 +8922,11 @@ function hasUserInstallRecord(file, pluginId) {
|
|
|
8872
8922
|
var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
8873
8923
|
var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
8874
8924
|
function runHostBin(bin, args, opts) {
|
|
8875
|
-
|
|
8925
|
+
const { step, timeout } = opts;
|
|
8926
|
+
const shared = { timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024 };
|
|
8927
|
+
const file = isWin ? "cmd.exe" : bin;
|
|
8928
|
+
const argv = isWin ? ["/c", bin, ...args] : args;
|
|
8929
|
+
return step ? execFileHard(file, argv, { ...shared, step }) : execFileP2(file, argv, shared);
|
|
8876
8930
|
}
|
|
8877
8931
|
function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
8878
8932
|
if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path14.join)(home, ".codex");
|
|
@@ -9050,27 +9104,14 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
9050
9104
|
) : (0, import_node_fs15.existsSync)((0, import_node_path14.join)(root, "plugins", "cache", "mutmutco", "mmi"))
|
|
9051
9105
|
};
|
|
9052
9106
|
}
|
|
9053
|
-
async function
|
|
9054
|
-
try {
|
|
9055
|
-
await runHostBin("claude", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
9056
|
-
return true;
|
|
9057
|
-
} catch {
|
|
9058
|
-
return false;
|
|
9059
|
-
}
|
|
9060
|
-
}
|
|
9061
|
-
async function runCodexPlugin(args) {
|
|
9107
|
+
async function runPluginCli(bin, args, log) {
|
|
9062
9108
|
try {
|
|
9063
|
-
await runHostBin(
|
|
9109
|
+
await runHostBin(bin, args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS, step: `${bin} ${args.join(" ")}` });
|
|
9064
9110
|
return true;
|
|
9065
|
-
} catch {
|
|
9066
|
-
|
|
9067
|
-
}
|
|
9068
|
-
}
|
|
9069
|
-
async function runKiloPlugin(args) {
|
|
9070
|
-
try {
|
|
9071
|
-
await runHostBin("kilo", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
9072
|
-
return true;
|
|
9073
|
-
} catch {
|
|
9111
|
+
} catch (error) {
|
|
9112
|
+
if (error instanceof ExecDeadlineError) {
|
|
9113
|
+
log(` \u2717 ${error.message} Recover with: mmi-cli plugin heal`);
|
|
9114
|
+
}
|
|
9074
9115
|
return false;
|
|
9075
9116
|
}
|
|
9076
9117
|
}
|
|
@@ -9131,7 +9172,8 @@ async function installCursorPluginCheckout(env = process.env) {
|
|
|
9131
9172
|
});
|
|
9132
9173
|
} else {
|
|
9133
9174
|
await runHostBin("gh", ["repo", "clone", "mutmutco/MMI-Hub", staged, "--", "--branch", "main", "--depth", "1"], {
|
|
9134
|
-
timeout: CLAUDE_PLUGIN_TIMEOUT_MS
|
|
9175
|
+
timeout: CLAUDE_PLUGIN_TIMEOUT_MS,
|
|
9176
|
+
step: "gh repo clone mutmutco/MMI-Hub"
|
|
9135
9177
|
});
|
|
9136
9178
|
}
|
|
9137
9179
|
if (!cursorPluginTreeHealthy(staged)) {
|
|
@@ -9161,7 +9203,8 @@ async function installCursorPluginCheckout(env = process.env) {
|
|
|
9161
9203
|
async function marketplaceAddRefSupported(bin) {
|
|
9162
9204
|
try {
|
|
9163
9205
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
9164
|
-
timeout: CLAUDE_PLUGIN_TIMEOUT_MS
|
|
9206
|
+
timeout: CLAUDE_PLUGIN_TIMEOUT_MS,
|
|
9207
|
+
step: `${bin} plugin marketplace add --help`
|
|
9165
9208
|
});
|
|
9166
9209
|
return marketplaceAddSupportsRef(`${stdout}
|
|
9167
9210
|
${stderr}`);
|
|
@@ -9191,7 +9234,7 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
9191
9234
|
if (token === "kilo") {
|
|
9192
9235
|
log(" \u21BB reinstalling the MMI plugin via `kilo plugin` (install \u2192 server() provisions the skills)\u2026");
|
|
9193
9236
|
for (const step of descriptor.healSteps) {
|
|
9194
|
-
const ok = await
|
|
9237
|
+
const ok = await runPluginCli("kilo", [...step.args], log);
|
|
9195
9238
|
if (healStepAborts(step, ok)) return false;
|
|
9196
9239
|
}
|
|
9197
9240
|
return true;
|
|
@@ -9207,7 +9250,7 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
9207
9250
|
const pins = token === "claude" ? captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]) : /* @__PURE__ */ new Map();
|
|
9208
9251
|
try {
|
|
9209
9252
|
for (const step of steps) {
|
|
9210
|
-
const ok =
|
|
9253
|
+
const ok = await runPluginCli(token, [...step.args], log);
|
|
9211
9254
|
if (healStepAborts(step, ok)) return false;
|
|
9212
9255
|
}
|
|
9213
9256
|
} finally {
|
|
@@ -24037,27 +24080,41 @@ function decideStage(inputs) {
|
|
|
24037
24080
|
var import_node_net2 = require("node:net");
|
|
24038
24081
|
var STAGE_LIVE_HUB_REPO = "mutmutco/MMI-Hub";
|
|
24039
24082
|
var IP_ECHO_URL = "https://api.ipify.org";
|
|
24083
|
+
var IP6_ECHO_URL = "https://api6.ipify.org";
|
|
24040
24084
|
var IP_DETECT_TIMEOUT_MS = 1e4;
|
|
24041
24085
|
function validStageLiveIp(ip) {
|
|
24042
24086
|
return (0, import_node_net2.isIP)(ip.trim()) !== 0;
|
|
24043
24087
|
}
|
|
24044
24088
|
async function detectPublicIp(fetchImpl = fetch) {
|
|
24089
|
+
return detectPublicIpFrom(IP_ECHO_URL, fetchImpl);
|
|
24090
|
+
}
|
|
24091
|
+
async function detectCallerIps(fetchImpl = fetch) {
|
|
24092
|
+
const [v4, v6] = await Promise.allSettled([
|
|
24093
|
+
detectPublicIp(fetchImpl),
|
|
24094
|
+
detectPublicIpFrom(IP6_ECHO_URL, fetchImpl)
|
|
24095
|
+
]);
|
|
24096
|
+
if (v4.status === "rejected") throw v4.reason;
|
|
24097
|
+
const ip = v4.value;
|
|
24098
|
+
const candidate = v6.status === "fulfilled" ? v6.value : void 0;
|
|
24099
|
+
return { ip, ip6: candidate && (0, import_node_net2.isIP)(candidate) === 6 ? candidate : void 0 };
|
|
24100
|
+
}
|
|
24101
|
+
async function detectPublicIpFrom(url, fetchImpl) {
|
|
24045
24102
|
let res;
|
|
24046
24103
|
try {
|
|
24047
|
-
res = await fetchImpl(
|
|
24104
|
+
res = await fetchImpl(url, { signal: AbortSignal.timeout(IP_DETECT_TIMEOUT_MS) });
|
|
24048
24105
|
} catch (e) {
|
|
24049
|
-
throw new Error(`public IP detection failed (${
|
|
24106
|
+
throw new Error(`public IP detection failed (${url}): ${e.message}`);
|
|
24050
24107
|
}
|
|
24051
|
-
if (!res.ok) throw new Error(`public IP detection failed: HTTP ${res.status} from ${
|
|
24108
|
+
if (!res.ok) throw new Error(`public IP detection failed: HTTP ${res.status} from ${url}`);
|
|
24052
24109
|
const ip = (await res.text()).trim();
|
|
24053
|
-
if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${
|
|
24110
|
+
if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${url}: "${ip.slice(0, 80)}"`);
|
|
24054
24111
|
return ip;
|
|
24055
24112
|
}
|
|
24056
24113
|
function stageLiveUpSteps(t) {
|
|
24057
24114
|
return [
|
|
24058
|
-
{ label: `detect your public
|
|
24115
|
+
{ label: `detect your public IPv4 and IPv6 (${IP_ECHO_URL} + ${IP6_ECHO_URL}, bounded, in parallel)` },
|
|
24059
24116
|
{ label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the Hub backend (tenant-deploy)` },
|
|
24060
|
-
{ label: `gate ${t.host} to your
|
|
24117
|
+
{ label: `gate ${t.host} to your IPv4 and your IPv6 /64 at the Cloudflare edge via the Hub backend (tenant-control cf-gate-allow)` },
|
|
24061
24118
|
{ label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
|
|
24062
24119
|
];
|
|
24063
24120
|
}
|
|
@@ -24069,11 +24126,14 @@ function stageLiveDownSteps(t) {
|
|
|
24069
24126
|
}
|
|
24070
24127
|
async function runStageLiveUp(deps, t) {
|
|
24071
24128
|
if (!t.ref?.trim()) throw new Error("stage --live: cannot resolve the current branch to deploy");
|
|
24072
|
-
const
|
|
24129
|
+
const detected = await deps.detectIp();
|
|
24130
|
+
const ip = detected.ip.trim();
|
|
24073
24131
|
if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
|
|
24132
|
+
const ip6 = detected.ip6?.trim() || void 0;
|
|
24074
24133
|
if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
|
|
24075
24134
|
await deps.deployDev({ repo: t.repo, ref: t.ref });
|
|
24076
|
-
await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip });
|
|
24135
|
+
await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip, ip6 });
|
|
24136
|
+
const allowed = ip6 ? `${ip} and your IPv6 /64 prefix (from ${ip6})` : `${ip} (IPv4 only \u2014 no public IPv6 detected)`;
|
|
24077
24137
|
return {
|
|
24078
24138
|
command: "stage --live",
|
|
24079
24139
|
mode: "up",
|
|
@@ -24081,10 +24141,11 @@ async function runStageLiveUp(deps, t) {
|
|
|
24081
24141
|
repo: t.repo,
|
|
24082
24142
|
ref: t.ref,
|
|
24083
24143
|
ip,
|
|
24144
|
+
ip6,
|
|
24084
24145
|
dispatched: ["tenant-deploy.yml", "tenant-control.yml"],
|
|
24085
24146
|
// #2656: the gate now writes a skip+block pair and self-verifies from the runner (a non-allowed vantage),
|
|
24086
24147
|
// so a gate that failed to close fails the run RED. The stage is private only once that gate run is green.
|
|
24087
|
-
message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${
|
|
24148
|
+
message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${allowed}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions and treat the stage as private ONLY once the cf-gate-allow run is green (it self-verifies the host is blocked) \u2014 tear down with: mmi-cli stage --live --down --apply`
|
|
24088
24149
|
};
|
|
24089
24150
|
}
|
|
24090
24151
|
async function runStageLiveDown(deps, t) {
|
|
@@ -24221,13 +24282,13 @@ function registerStageCommands(program3) {
|
|
|
24221
24282
|
}
|
|
24222
24283
|
const rcDeps = registryClientDeps(await loadConfig());
|
|
24223
24284
|
const deps = {
|
|
24224
|
-
detectIp: () =>
|
|
24285
|
+
detectIp: () => detectCallerIps(),
|
|
24225
24286
|
deployDev: async ({ repo, ref }) => {
|
|
24226
24287
|
const res = await tenantDeploy({ repo, stage: "dev", ref }, rcDeps);
|
|
24227
24288
|
if (!res.ok) throw new Error(`dev deploy dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
24228
24289
|
},
|
|
24229
|
-
control: async ({ repo, action, host, ip }) => {
|
|
24230
|
-
const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
|
|
24290
|
+
control: async ({ repo, action, host, ip, ip6 }) => {
|
|
24291
|
+
const res = await tenantControl({ repo, stage: "dev", action, host, ip, ip6 }, rcDeps);
|
|
24231
24292
|
if (!res.ok) throw new Error(`runtime tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
24232
24293
|
}
|
|
24233
24294
|
};
|
|
@@ -27862,13 +27923,16 @@ function registerSessionReport(program3) {
|
|
|
27862
27923
|
// src/doctor-render.ts
|
|
27863
27924
|
var RESTART_LINE = "\u21BB Restart Claude to finish.";
|
|
27864
27925
|
var VERBOSE_INDENT = " ";
|
|
27926
|
+
function checkGlyph(check) {
|
|
27927
|
+
if (!check.ok) return "\u2717";
|
|
27928
|
+
return check.verified === false ? "?" : "\u2713";
|
|
27929
|
+
}
|
|
27865
27930
|
function renderCheckLine(check) {
|
|
27866
|
-
|
|
27867
|
-
|
|
27868
|
-
}
|
|
27869
|
-
|
|
27870
|
-
return
|
|
27871
|
-
${VERBOSE_INDENT}\u2192 ${check.fix}` : head;
|
|
27931
|
+
const head = check.detail ? `${checkGlyph(check)} ${check.label} \u2014 ${check.detail}` : `${checkGlyph(check)} ${check.label}`;
|
|
27932
|
+
const lines = [head];
|
|
27933
|
+
if (check.fix) lines.push(`${VERBOSE_INDENT}\u2192 ${check.fix}`);
|
|
27934
|
+
if (check.command) lines.push(`${VERBOSE_INDENT}${check.command}`);
|
|
27935
|
+
return lines.join("\n");
|
|
27872
27936
|
}
|
|
27873
27937
|
function renderReport(checks, opts) {
|
|
27874
27938
|
const lines = checks.map(renderCheckLine);
|
|
@@ -27877,8 +27941,14 @@ function renderReport(checks, opts) {
|
|
|
27877
27941
|
}
|
|
27878
27942
|
function renderTally(checks) {
|
|
27879
27943
|
const total = checks.length;
|
|
27880
|
-
const
|
|
27881
|
-
|
|
27944
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
27945
|
+
const unverified = checks.filter((c) => c.ok && c.verified === false).length;
|
|
27946
|
+
const healthy = total - failed - unverified;
|
|
27947
|
+
if (failed === 0 && unverified === 0) return `\u2713 all ${total} checks healthy`;
|
|
27948
|
+
const parts = [`\u2713 ${healthy} verified healthy`];
|
|
27949
|
+
if (unverified > 0) parts.push(`? ${unverified} unverified`);
|
|
27950
|
+
if (failed > 0) parts.push(`\u2717 ${failed} failed`);
|
|
27951
|
+
return `${parts.join(" \xB7 ")} \u2014 ${total} checks`;
|
|
27882
27952
|
}
|
|
27883
27953
|
function doctorReportExitCode(checks) {
|
|
27884
27954
|
return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
|
|
@@ -28704,6 +28774,11 @@ function checkAwsIdentity(probe) {
|
|
|
28704
28774
|
const arn = probe.callerArn?.trim();
|
|
28705
28775
|
return {
|
|
28706
28776
|
ok: !arn || !arn.endsWith(":root"),
|
|
28777
|
+
// #4074: the row's claim is "this caller is not root", and with no ARN resolved there is no caller to
|
|
28778
|
+
// make it about. `awsCallerArn` returns undefined for ANY failure — expired SSO, no `aws` binary, a
|
|
28779
|
+
// timeout — so an empty result is "I could not look", never "I looked and it is fine". The evidence
|
|
28780
|
+
// line below has said exactly that since #3485; the glyph now says it too.
|
|
28781
|
+
verified: Boolean(arn),
|
|
28707
28782
|
id: "aws-identity",
|
|
28708
28783
|
label: "aws identity",
|
|
28709
28784
|
fix: "use a non-root IAM user/session profile (set AWS_PROFILE or run: aws sso login), then verify `aws sts get-caller-identity` is not :root",
|
|
@@ -28738,13 +28813,25 @@ function checkCodexHookTrust(probe, displayName = "Codex") {
|
|
|
28738
28813
|
if (probe.trusted) {
|
|
28739
28814
|
return { id: "codex-hook-trust", ok: true, label, detail: "trusted", verbose: evidence };
|
|
28740
28815
|
}
|
|
28816
|
+
if (probe.requiredCount === 0) {
|
|
28817
|
+
return {
|
|
28818
|
+
id: "codex-hook-trust",
|
|
28819
|
+
ok: true,
|
|
28820
|
+
verified: false,
|
|
28821
|
+
warn: true,
|
|
28822
|
+
label,
|
|
28823
|
+
detail: "no approval rows to read \u2014 the hook bundle could not be checked",
|
|
28824
|
+
fix: `run \`mmi-cli plugin heal\`, restart ${displayName}, then review and trust MMI under \`/hooks\``,
|
|
28825
|
+
verbose: evidence
|
|
28826
|
+
};
|
|
28827
|
+
}
|
|
28741
28828
|
return {
|
|
28742
28829
|
id: "codex-hook-trust",
|
|
28743
28830
|
ok: true,
|
|
28744
28831
|
warn: true,
|
|
28745
28832
|
label,
|
|
28746
|
-
detail:
|
|
28747
|
-
fix:
|
|
28833
|
+
detail: `${probe.trustedCount}/${probe.requiredCount} approval rows present`,
|
|
28834
|
+
fix: `${displayName} does not allow silent hook approval; run \`/hooks\`, review the MMI commands, and trust them`,
|
|
28748
28835
|
verbose: evidence
|
|
28749
28836
|
};
|
|
28750
28837
|
}
|
|
@@ -28757,8 +28844,9 @@ function checkCliVersion(input, releasedNote) {
|
|
|
28757
28844
|
if (!report.releasedVersion) {
|
|
28758
28845
|
return {
|
|
28759
28846
|
id: "cli-version",
|
|
28760
|
-
ok:
|
|
28761
|
-
|
|
28847
|
+
ok: true,
|
|
28848
|
+
verified: false,
|
|
28849
|
+
warn: true,
|
|
28762
28850
|
label: "mmi-cli",
|
|
28763
28851
|
detail: `${report.currentVersion} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
28764
28852
|
fix: "check it directly: `mmi-cli --version` against `npm view @mutmutco/cli version`; update with `npm install -g @mutmutco/cli@<released>`",
|
package/package.json
CHANGED