@mutmutco/cli 2.6.0 → 2.8.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/README.md +1 -1
- package/dist/index.cjs +419 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ mmi-cli doctor --json
|
|
|
36
36
|
KB curation without echoing the plan body.
|
|
37
37
|
`mmi-cli plan` remains a compatibility alias.
|
|
38
38
|
- `mmi-cli secrets where|list|get|set|edit|rm|use|grant|revoke` manages two-tier project/org secrets without logging values; `where` prints the vault layout + well-known keys, and values move over TLS in the request body — never an argument.
|
|
39
|
-
- `mmi-cli project list|get|resolve|set` reads
|
|
39
|
+
- `mmi-cli project list|get|resolve|doctor|heal|readiness|set` reads and repairs Hub-owned v2 readiness state. `doctor --v2 --json` diagnoses central deploy/secrets readiness, `heal --v2 --apply` fixes only registry-owned defaults, and `readiness --update-issue` updates the repo's v2 readiness issue; `set` is master-only.
|
|
40
40
|
- `mmi-cli registry org` reads org-level constants from the registry (`ORG#config`).
|
|
41
41
|
- `mmi-cli oauth plan|verify` prints a repo's canonical Google OAuth URI set (read from the registry) and verifies the client is port-agnostic.
|
|
42
42
|
- `mmi-cli issue create` creates typed, prioritized GitHub issues (priority sets the board field, not a label) and queues related-issue discovery.
|
package/dist/index.cjs
CHANGED
|
@@ -3341,6 +3341,7 @@ function resumeCue() {
|
|
|
3341
3341
|
|
|
3342
3342
|
// src/saga-note.ts
|
|
3343
3343
|
var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
|
|
3344
|
+
var ROUTE_LEVEL_403 = "saga API route-level 403 from GitHubAuthorizer cache/policy";
|
|
3344
3345
|
function agentSurface() {
|
|
3345
3346
|
const surface = process.env.MMI_AGENT_SURFACE || "claude";
|
|
3346
3347
|
if (AGENT_SURFACE_TOKENS.includes(surface)) return surface;
|
|
@@ -3372,6 +3373,10 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
3372
3373
|
anchorForce: o.anchorForce || void 0
|
|
3373
3374
|
};
|
|
3374
3375
|
}
|
|
3376
|
+
function formatCaptureFailure(status, message) {
|
|
3377
|
+
if (status === 403 && message === "Forbidden") return `saga: ${ROUTE_LEVEL_403} (HTTP 403)`;
|
|
3378
|
+
return `saga: HTTP ${status}`;
|
|
3379
|
+
}
|
|
3375
3380
|
|
|
3376
3381
|
// src/version-lag.ts
|
|
3377
3382
|
var VERSION_LABEL = "installed plugin/adapter cache freshness";
|
|
@@ -4246,6 +4251,13 @@ function ghError(e) {
|
|
|
4246
4251
|
}
|
|
4247
4252
|
|
|
4248
4253
|
// src/gc.ts
|
|
4254
|
+
function buildRemoteBranchCleanupReport(branch, input) {
|
|
4255
|
+
if (!input.attempted) return { name: branch, status: "not-attempted", reason: input.reason };
|
|
4256
|
+
if (input.existsAfter === true) return { name: branch, status: "failed", reason: "still-present-after-delete" };
|
|
4257
|
+
if (input.existedBefore === false && input.existsAfter === false) return { name: branch, status: "already-gone" };
|
|
4258
|
+
if (input.existsAfter === false) return { name: branch, status: "deleted" };
|
|
4259
|
+
return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
|
|
4260
|
+
}
|
|
4249
4261
|
var DEFAULT_PROTECTED = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
|
|
4250
4262
|
function groupedPrs(prs) {
|
|
4251
4263
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4353,6 +4365,63 @@ function branchMissingFromList(branch, stdout) {
|
|
|
4353
4365
|
const names = stdout.split(/\r?\n/).map((line) => line.replace(/^\*\s*/, "").trim()).filter(Boolean);
|
|
4354
4366
|
return !names.includes(branch);
|
|
4355
4367
|
}
|
|
4368
|
+
function shellQuote(value) {
|
|
4369
|
+
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
4370
|
+
}
|
|
4371
|
+
function safeWorktreeRemoveCommand(safeCwd, targetPath) {
|
|
4372
|
+
const prefix = safeCwd ? `git -C ${shellQuote(safeCwd)}` : "git";
|
|
4373
|
+
return `${prefix} worktree remove --force ${shellQuote(targetPath)}`;
|
|
4374
|
+
}
|
|
4375
|
+
function errorMessage(error) {
|
|
4376
|
+
return error instanceof Error ? error.message : String(error);
|
|
4377
|
+
}
|
|
4378
|
+
async function cleanupPrMergeLocalBranch(branch, options) {
|
|
4379
|
+
const report = {
|
|
4380
|
+
branch,
|
|
4381
|
+
localBranch: { name: branch, status: "not-attempted", reason: branch ? "pending" : "missing-branch" }
|
|
4382
|
+
};
|
|
4383
|
+
if (!branch) return report;
|
|
4384
|
+
let afterWorktrees = [];
|
|
4385
|
+
try {
|
|
4386
|
+
afterWorktrees = parseWorktreePorcelain(await options.execGit(["worktree", "list", "--porcelain"]));
|
|
4387
|
+
} catch (e) {
|
|
4388
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-list-failed", error: errorMessage(e) };
|
|
4389
|
+
return report;
|
|
4390
|
+
}
|
|
4391
|
+
const beforeWorktrees = options.beforeWorktrees ?? [];
|
|
4392
|
+
const wtPath = selectPrMergeCleanupWorktree(branch, beforeWorktrees, afterWorktrees, options.startingPath);
|
|
4393
|
+
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath);
|
|
4394
|
+
const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
|
|
4395
|
+
if (wtPath) {
|
|
4396
|
+
try {
|
|
4397
|
+
await git(["worktree", "remove", "--force", wtPath]);
|
|
4398
|
+
report.worktree = { path: wtPath, status: "removed" };
|
|
4399
|
+
} catch (e) {
|
|
4400
|
+
report.worktree = {
|
|
4401
|
+
path: wtPath,
|
|
4402
|
+
status: "failed",
|
|
4403
|
+
error: errorMessage(e),
|
|
4404
|
+
safeCleanupCommand: safeWorktreeRemoveCommand(safeCwd, wtPath)
|
|
4405
|
+
};
|
|
4406
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-failed" };
|
|
4407
|
+
return report;
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
const current = (await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || "").trim();
|
|
4411
|
+
if (branch === current) {
|
|
4412
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "current-branch" };
|
|
4413
|
+
return report;
|
|
4414
|
+
}
|
|
4415
|
+
try {
|
|
4416
|
+
await git(["branch", "-D", branch]);
|
|
4417
|
+
report.localBranch = { name: branch, status: "deleted" };
|
|
4418
|
+
} catch (e) {
|
|
4419
|
+
const remaining = await git(["branch", "--list", branch]).catch(() => "");
|
|
4420
|
+
report.localBranch = branchMissingFromList(branch, remaining) ? { name: branch, status: "already-gone" } : { name: branch, status: "failed", error: errorMessage(e) };
|
|
4421
|
+
}
|
|
4422
|
+
if (wtPath) await git(["worktree", "prune"]).catch(() => "");
|
|
4423
|
+
return report;
|
|
4424
|
+
}
|
|
4356
4425
|
function formatGcPlan(plan2, apply) {
|
|
4357
4426
|
const lines = [`mmi-cli gc: ${apply ? "apply" : "dry-run"}`];
|
|
4358
4427
|
if (!plan2.branches.length && !plan2.trackingRefs.length) lines.push("nothing to clean");
|
|
@@ -4403,8 +4472,9 @@ function trainPlan(command) {
|
|
|
4403
4472
|
{ label: "verify current branch is development", gated: true },
|
|
4404
4473
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
4405
4474
|
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
4475
|
+
{ label: "for Hub distribution changes, verify the release bump is already landed before touching rc", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
4406
4476
|
{ label: "merge development to rc", gated: true },
|
|
4407
|
-
{ label: "
|
|
4477
|
+
{ label: "trigger the deploy path for this repo model", command: "tenant-container: gh workflow run tenant-deploy.yml ...; hub-serverless: no manual dispatch, deploy.yml auto-fires on rc push", gated: true }
|
|
4408
4478
|
];
|
|
4409
4479
|
}
|
|
4410
4480
|
if (command === "release") {
|
|
@@ -4414,8 +4484,9 @@ function trainPlan(command) {
|
|
|
4414
4484
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
4415
4485
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
4416
4486
|
{ label: "merge rc to main", gated: true },
|
|
4487
|
+
{ label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
4417
4488
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
4418
|
-
{ label: "
|
|
4489
|
+
{ label: "trigger the deploy path for this repo model", command: "tenant-container: gh workflow run tenant-deploy.yml ...; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release", gated: true },
|
|
4419
4490
|
{ label: "roll development forward", gated: true }
|
|
4420
4491
|
];
|
|
4421
4492
|
}
|
|
@@ -5695,6 +5766,24 @@ async function fetchProjectBySlug(slug, deps) {
|
|
|
5695
5766
|
return null;
|
|
5696
5767
|
}
|
|
5697
5768
|
}
|
|
5769
|
+
async function fetchDeployStatusBySlug(slug, deps) {
|
|
5770
|
+
if (!deps.baseUrl || !slug) return null;
|
|
5771
|
+
const token = await deps.token();
|
|
5772
|
+
if (!token) return null;
|
|
5773
|
+
const doFetch = deps.fetch ?? fetch;
|
|
5774
|
+
try {
|
|
5775
|
+
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
|
|
5776
|
+
method: "GET",
|
|
5777
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
5778
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
5779
|
+
});
|
|
5780
|
+
if (!res.ok) return null;
|
|
5781
|
+
const body = await res.json();
|
|
5782
|
+
return body?.stages ?? null;
|
|
5783
|
+
} catch {
|
|
5784
|
+
return null;
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5698
5787
|
async function fetchOrgConfig(deps) {
|
|
5699
5788
|
if (!deps.baseUrl) return null;
|
|
5700
5789
|
const token = await deps.token();
|
|
@@ -5741,6 +5830,173 @@ async function upsertProject(slug, patch, deps) {
|
|
|
5741
5830
|
return postJson(`/projects/${encodeURIComponent(slug)}`, patch, deps);
|
|
5742
5831
|
}
|
|
5743
5832
|
|
|
5833
|
+
// src/project-readiness.ts
|
|
5834
|
+
var STAGES = ["dev", "rc", "main"];
|
|
5835
|
+
var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
5836
|
+
function slugOfRepo(repoOrSlug) {
|
|
5837
|
+
return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
|
|
5838
|
+
}
|
|
5839
|
+
function repoFrom(repoOrSlug, slug) {
|
|
5840
|
+
return repoOrSlug.includes("/") ? repoOrSlug : `mutmutco/${slug}`;
|
|
5841
|
+
}
|
|
5842
|
+
function defaultSubdomain(slug) {
|
|
5843
|
+
return slug.replace(/^[a-z]+-/, "");
|
|
5844
|
+
}
|
|
5845
|
+
function defaultDeployModel(meta, repo) {
|
|
5846
|
+
if (meta?.deployModel) return meta.deployModel;
|
|
5847
|
+
if (meta?.class === "content") return "content";
|
|
5848
|
+
if (repo.toLowerCase().endsWith("/mmi-hub")) return "hub-serverless";
|
|
5849
|
+
return "tenant-container";
|
|
5850
|
+
}
|
|
5851
|
+
function stageRequiredSecrets(stage2, meta) {
|
|
5852
|
+
const contract = meta.requiredRuntimeSecrets;
|
|
5853
|
+
const extra = Array.isArray(contract) ? contract : Array.isArray(contract?.[stage2]) ? contract[stage2] ?? [] : [];
|
|
5854
|
+
return [.../* @__PURE__ */ new Set([...DEFAULT_RUNTIME_SECRET_NAMES, ...extra])];
|
|
5855
|
+
}
|
|
5856
|
+
function stageKey(stage2, key) {
|
|
5857
|
+
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
5858
|
+
}
|
|
5859
|
+
function appGapsFor(meta, model, slug) {
|
|
5860
|
+
if (model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
5861
|
+
const gaps = [
|
|
5862
|
+
"Ensure Docker/compose runs from the central release directory and consumes the Hub-generated .env.",
|
|
5863
|
+
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
5864
|
+
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
5865
|
+
];
|
|
5866
|
+
if (slug === "mmi-katip") {
|
|
5867
|
+
gaps.push("Katip-specific app plan: declare Google Workspace env requirements, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
|
|
5868
|
+
}
|
|
5869
|
+
if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
|
|
5870
|
+
return gaps;
|
|
5871
|
+
}
|
|
5872
|
+
function contractByStage(contract) {
|
|
5873
|
+
if (Array.isArray(contract)) return { dev: contract, rc: contract, main: contract };
|
|
5874
|
+
return contract ?? {};
|
|
5875
|
+
}
|
|
5876
|
+
function ensureStageNames(names, required) {
|
|
5877
|
+
return [.../* @__PURE__ */ new Set([...names ?? [], ...required])];
|
|
5878
|
+
}
|
|
5879
|
+
function sameNames(a, b) {
|
|
5880
|
+
const left = new Set(a ?? []);
|
|
5881
|
+
const right = new Set(b ?? []);
|
|
5882
|
+
if (left.size !== right.size) return false;
|
|
5883
|
+
return [...left].every((name) => right.has(name));
|
|
5884
|
+
}
|
|
5885
|
+
function sameStageContract(a, b) {
|
|
5886
|
+
return STAGES.every((stage2) => sameNames(a[stage2], b[stage2]));
|
|
5887
|
+
}
|
|
5888
|
+
function buildV2HealPatch(repoOrSlug, meta) {
|
|
5889
|
+
const slug = slugOfRepo(repoOrSlug);
|
|
5890
|
+
const repo = repoFrom(repoOrSlug, slug);
|
|
5891
|
+
const sub = defaultSubdomain(slug);
|
|
5892
|
+
const patch = {};
|
|
5893
|
+
const model = defaultDeployModel(meta, repo);
|
|
5894
|
+
if (!meta?.class) patch.class = "deployable";
|
|
5895
|
+
if (!meta?.deployModel) patch.deployModel = model;
|
|
5896
|
+
if (!meta?.vaultPath) patch.vaultPath = `/mmi-future/${slug}`;
|
|
5897
|
+
if (!meta?.kbPointer) patch.kbPointer = `kb/projects/${slug}.md`;
|
|
5898
|
+
if (!meta?.oauth) patch.oauth = { subdomains: [sub], callbackPath: "/auth/callback" };
|
|
5899
|
+
if (!meta?.edgeDomains && model === "tenant-container") {
|
|
5900
|
+
patch.edgeDomains = {
|
|
5901
|
+
dev: `dev.${sub}.mutatismutandis.co`,
|
|
5902
|
+
rc: `rc.${sub}.mutatismutandis.co`,
|
|
5903
|
+
main: `${sub}.mutatismutandis.co`
|
|
5904
|
+
};
|
|
5905
|
+
}
|
|
5906
|
+
if (slug === "mmi-katip") {
|
|
5907
|
+
const required = ["GOOGLE_IMPERSONATE_USER", "GOOGLE_ADMIN_USER"];
|
|
5908
|
+
const current = contractByStage(meta?.requiredRuntimeSecrets);
|
|
5909
|
+
const next = {
|
|
5910
|
+
dev: ensureStageNames(current.dev, required),
|
|
5911
|
+
rc: ensureStageNames(current.rc, required),
|
|
5912
|
+
main: ensureStageNames(current.main, required)
|
|
5913
|
+
};
|
|
5914
|
+
if (!sameStageContract(current, next)) {
|
|
5915
|
+
patch.requiredRuntimeSecrets = next;
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
return { slug, patch, appOwnedGaps: appGapsFor(meta, model, slug) };
|
|
5919
|
+
}
|
|
5920
|
+
async function buildV2Doctor(repoOrSlug, deps) {
|
|
5921
|
+
const slug = slugOfRepo(repoOrSlug);
|
|
5922
|
+
const repo = repoFrom(repoOrSlug, slug);
|
|
5923
|
+
const meta = await deps.getProject(slug);
|
|
5924
|
+
const model = defaultDeployModel(meta, repo);
|
|
5925
|
+
const autoHeal = buildV2HealPatch(repo, meta);
|
|
5926
|
+
if (!meta) {
|
|
5927
|
+
const emptySecrets = {
|
|
5928
|
+
dev: { required: [], present: [], missing: [] },
|
|
5929
|
+
rc: { required: [], present: [], missing: [] },
|
|
5930
|
+
main: { required: [], present: [], missing: [] }
|
|
5931
|
+
};
|
|
5932
|
+
const emptyCoords = {
|
|
5933
|
+
dev: { ok: false },
|
|
5934
|
+
rc: { ok: false },
|
|
5935
|
+
main: { ok: false }
|
|
5936
|
+
};
|
|
5937
|
+
return {
|
|
5938
|
+
ok: false,
|
|
5939
|
+
repo,
|
|
5940
|
+
slug,
|
|
5941
|
+
hubOwned: { meta: { ok: false, missing: [`PROJECT#${slug}/META`] }, deployCoords: emptyCoords, secrets: emptySecrets },
|
|
5942
|
+
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
5943
|
+
appOwnedGaps: autoHeal.appOwnedGaps
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
const presentSecrets = new Set(await deps.listSecrets(repo));
|
|
5947
|
+
const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => [stage2, { ok: await deps.hasDeployCoords(slug, stage2) }])));
|
|
5948
|
+
const secrets2 = Object.fromEntries(STAGES.map((stage2) => {
|
|
5949
|
+
const required = stageRequiredSecrets(stage2, meta).map((key) => stageKey(stage2, key));
|
|
5950
|
+
const present = required.filter((key) => presentSecrets.has(key));
|
|
5951
|
+
const missing = required.filter((key) => !presentSecrets.has(key));
|
|
5952
|
+
return [stage2, { required, present, missing }];
|
|
5953
|
+
}));
|
|
5954
|
+
const metaMissing = ["class", "deployModel", "vaultPath", "kbPointer", "oauth"].filter((key) => meta[key] === void 0);
|
|
5955
|
+
const ok = metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets2).every((v) => v.missing.length === 0);
|
|
5956
|
+
return {
|
|
5957
|
+
ok,
|
|
5958
|
+
repo,
|
|
5959
|
+
slug,
|
|
5960
|
+
class: meta.class,
|
|
5961
|
+
deployModel: model,
|
|
5962
|
+
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, secrets: secrets2 },
|
|
5963
|
+
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
5964
|
+
appOwnedGaps: autoHeal.appOwnedGaps
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
5967
|
+
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
5968
|
+
const start = "<!-- mmi-v2-readiness:start -->";
|
|
5969
|
+
const end = "<!-- mmi-v2-readiness:end -->";
|
|
5970
|
+
const stageLines = STAGES.map((stage2) => {
|
|
5971
|
+
const coords = report.hubOwned.deployCoords[stage2].ok ? "coords ok" : "coords missing/unverified";
|
|
5972
|
+
const missing = report.hubOwned.secrets[stage2].missing;
|
|
5973
|
+
return `- ${stage2}: ${coords}; ${missing.length ? `missing ${missing.join(", ")}` : "required secret names present"}`;
|
|
5974
|
+
});
|
|
5975
|
+
const section = [
|
|
5976
|
+
start,
|
|
5977
|
+
"## Hub v2 readiness",
|
|
5978
|
+
"",
|
|
5979
|
+
`Repo: ${report.repo}`,
|
|
5980
|
+
`Deploy model: ${report.deployModel ?? "(unresolved)"}`,
|
|
5981
|
+
`Overall: ${report.ok ? "ready" : "not ready"}`,
|
|
5982
|
+
"",
|
|
5983
|
+
"### Hub-owned diagnosis",
|
|
5984
|
+
`- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
|
|
5985
|
+
...stageLines,
|
|
5986
|
+
"",
|
|
5987
|
+
"### Auto-heal applied / available",
|
|
5988
|
+
...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
|
|
5989
|
+
"",
|
|
5990
|
+
"### App-owned implementation plan",
|
|
5991
|
+
...report.appOwnedGaps.map((x) => `- ${x}`),
|
|
5992
|
+
end
|
|
5993
|
+
].join("\n");
|
|
5994
|
+
const re = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
5995
|
+
return re.test(existingBody) ? existingBody.replace(re, section) : `${existingBody.trim()}
|
|
5996
|
+
|
|
5997
|
+
${section}`.trim();
|
|
5998
|
+
}
|
|
5999
|
+
|
|
5744
6000
|
// src/kb.ts
|
|
5745
6001
|
var DEFAULT_KB = { owner: "mutmutco", repo: "MM-KB", ref: "main" };
|
|
5746
6002
|
function resolveKbSource(rawBase) {
|
|
@@ -5887,7 +6143,8 @@ async function planPull(deps, slug, opts = {}) {
|
|
|
5887
6143
|
deps.log(`pulled ${slug} \u2192 ${planPath(slug)}`);
|
|
5888
6144
|
}
|
|
5889
6145
|
async function planList(deps, opts = {}) {
|
|
5890
|
-
const
|
|
6146
|
+
const project2 = opts.project ?? (opts.quiet ? await deps.project() : void 0);
|
|
6147
|
+
const qs = project2 ? `?${new URLSearchParams({ project: project2 }).toString()}` : "";
|
|
5891
6148
|
let res;
|
|
5892
6149
|
try {
|
|
5893
6150
|
res = await deps.fetch(`${deps.apiUrl}/plan/list${qs}`, {
|
|
@@ -6063,15 +6320,15 @@ async function secretsList(deps, opts) {
|
|
|
6063
6320
|
const { secrets: secrets2 } = await res.json();
|
|
6064
6321
|
deps.log(formatSecretList(secrets2 ?? []));
|
|
6065
6322
|
}
|
|
6066
|
-
var
|
|
6323
|
+
var DEFAULT_RUNTIME_SECRET_NAMES2 = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
6067
6324
|
function stringList(v) {
|
|
6068
6325
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && isValidSecretKey(x)) : [];
|
|
6069
6326
|
}
|
|
6070
6327
|
function requiredRuntimeSecretNames(stage2, contract) {
|
|
6071
6328
|
const extra = Array.isArray(contract) ? stringList(contract) : stringList(contract?.[stage2]);
|
|
6072
|
-
return [.../* @__PURE__ */ new Set([...
|
|
6329
|
+
return [.../* @__PURE__ */ new Set([...DEFAULT_RUNTIME_SECRET_NAMES2, ...extra])];
|
|
6073
6330
|
}
|
|
6074
|
-
function
|
|
6331
|
+
function stageKey2(stage2, key) {
|
|
6075
6332
|
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
6076
6333
|
}
|
|
6077
6334
|
async function secretsPreflight(deps, opts) {
|
|
@@ -6094,7 +6351,7 @@ async function secretsPreflight(deps, opts) {
|
|
|
6094
6351
|
}
|
|
6095
6352
|
const { secrets: secrets2 } = await res.json();
|
|
6096
6353
|
const present = new Set((secrets2 ?? []).map((s) => s.key));
|
|
6097
|
-
const required = opts.required.map((key) =>
|
|
6354
|
+
const required = opts.required.map((key) => stageKey2(opts.stage, key));
|
|
6098
6355
|
const missing = required.filter((key) => !present.has(key));
|
|
6099
6356
|
if (missing.length) {
|
|
6100
6357
|
deps.log(`missing ${missing.join(", ")}`);
|
|
@@ -6218,7 +6475,7 @@ var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
|
6218
6475
|
var SSM_ENVS = ["dev", "rc", "main"];
|
|
6219
6476
|
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
6220
6477
|
var uniq = (xs) => [...new Set(xs)];
|
|
6221
|
-
function
|
|
6478
|
+
function defaultSubdomain2(slug) {
|
|
6222
6479
|
const i = slug.indexOf("-");
|
|
6223
6480
|
return i === -1 ? slug : slug.slice(i + 1);
|
|
6224
6481
|
}
|
|
@@ -6247,7 +6504,7 @@ function oauthSsmKeys() {
|
|
|
6247
6504
|
}
|
|
6248
6505
|
function parseOauthConfig(mmiConfig, slug) {
|
|
6249
6506
|
const raw = mmiConfig?.oauth ?? {};
|
|
6250
|
-
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [
|
|
6507
|
+
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
|
|
6251
6508
|
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
6252
6509
|
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
6253
6510
|
if (!callbackPath.startsWith("/")) {
|
|
@@ -6407,7 +6664,15 @@ async function postCapture(capture, quiet = false) {
|
|
|
6407
6664
|
// duplicate the note. Backend capture-latency root cause tracked in #255.
|
|
6408
6665
|
signal: AbortSignal.timeout(2e4)
|
|
6409
6666
|
});
|
|
6410
|
-
|
|
6667
|
+
let message = "";
|
|
6668
|
+
if (!res.ok) {
|
|
6669
|
+
try {
|
|
6670
|
+
const body = await res.clone().json();
|
|
6671
|
+
message = typeof body.message === "string" ? body.message : "";
|
|
6672
|
+
} catch {
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
if (!quiet) console.log(res.ok ? "noted" : formatCaptureFailure(res.status, message));
|
|
6411
6676
|
} catch (e) {
|
|
6412
6677
|
if (!quiet) console.error(`mmi-cli saga: ${e.message}`);
|
|
6413
6678
|
}
|
|
@@ -6469,34 +6734,6 @@ async function applyGcPlan(plan2, remote) {
|
|
|
6469
6734
|
await execFileP3("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
|
|
6470
6735
|
}
|
|
6471
6736
|
}
|
|
6472
|
-
async function cleanupLocalBranch(branch, before = {}) {
|
|
6473
|
-
const result = { branchDeleted: false };
|
|
6474
|
-
if (!branch) return result;
|
|
6475
|
-
const { stdout } = await execFileP3("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
6476
|
-
const afterWorktrees = parseWorktreePorcelain(stdout);
|
|
6477
|
-
const wtPath = selectPrMergeCleanupWorktree(branch, before.worktrees ?? [], afterWorktrees, before.startingPath);
|
|
6478
|
-
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...before.worktrees ?? []], wtPath);
|
|
6479
|
-
const git = (args) => safeCwd ? execFileP3("git", ["-C", safeCwd, ...args], { timeout: GIT_TIMEOUT_MS }) : execFileP3("git", args, { timeout: GIT_TIMEOUT_MS });
|
|
6480
|
-
if (wtPath) {
|
|
6481
|
-
await git(["worktree", "remove", "--force", wtPath]).catch(() => {
|
|
6482
|
-
});
|
|
6483
|
-
result.worktreeRemoved = wtPath;
|
|
6484
|
-
}
|
|
6485
|
-
const current = safeCwd ? ((await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => ({ stdout: "" }))).stdout || "").trim() : await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]) || "";
|
|
6486
|
-
if (branch !== current) {
|
|
6487
|
-
await git(["branch", "-D", branch]).then(() => {
|
|
6488
|
-
result.branchDeleted = true;
|
|
6489
|
-
}).catch(() => {
|
|
6490
|
-
});
|
|
6491
|
-
if (!result.branchDeleted) {
|
|
6492
|
-
const remaining = await git(["branch", "--list", branch]).catch(() => ({ stdout: "" }));
|
|
6493
|
-
result.branchDeleted = branchMissingFromList(branch, remaining.stdout || "");
|
|
6494
|
-
}
|
|
6495
|
-
}
|
|
6496
|
-
if (wtPath) await git(["worktree", "prune"]).catch(() => {
|
|
6497
|
-
});
|
|
6498
|
-
return result;
|
|
6499
|
-
}
|
|
6500
6737
|
function resolveVersion() {
|
|
6501
6738
|
try {
|
|
6502
6739
|
const manifest = (0, import_node_path4.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
@@ -6542,39 +6779,41 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
6542
6779
|
}
|
|
6543
6780
|
}
|
|
6544
6781
|
try {
|
|
6545
|
-
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
6546
6782
|
log(` \u21BB updating mmi-cli ${report.currentVersion} \u2192 ${target}\u2026`);
|
|
6547
|
-
await
|
|
6783
|
+
await runHostBin("npm", ["install", "-g", "@mutmutco/cli@latest"], { timeout: NPM_UPDATE_TIMEOUT_MS });
|
|
6548
6784
|
return { ...report, ok: true };
|
|
6549
6785
|
} catch {
|
|
6550
6786
|
return report;
|
|
6551
6787
|
}
|
|
6552
6788
|
}
|
|
6553
|
-
|
|
6789
|
+
async function requireFreshTrainCli(commandName) {
|
|
6790
|
+
const report = buildVersionLagReport({
|
|
6791
|
+
currentVersion: resolveVersion(),
|
|
6792
|
+
repoVersion: readRepoVersion(),
|
|
6793
|
+
releasedVersion: await fetchReleasedVersion()
|
|
6794
|
+
});
|
|
6795
|
+
if (report.ok) return;
|
|
6796
|
+
const target = report.staleAgainst === "released" ? `released ${report.releasedVersion}` : `repo ${report.repoVersion}`;
|
|
6797
|
+
throw new Error(`running mmi-cli ${report.currentVersion} is stale against ${target}; run doctor/update first so ${commandName} uses the current train path`);
|
|
6798
|
+
}
|
|
6799
|
+
var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
6800
|
+
function runHostBin(bin, args, opts) {
|
|
6801
|
+
return isWin ? execFileP3("cmd.exe", ["/c", bin, ...args], opts) : execFileP3(bin, args, opts);
|
|
6802
|
+
}
|
|
6554
6803
|
async function runClaudePlugin(args) {
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
} catch (err) {
|
|
6561
|
-
if (err.code === "ENOENT") continue;
|
|
6562
|
-
return false;
|
|
6563
|
-
}
|
|
6804
|
+
try {
|
|
6805
|
+
await runHostBin("claude", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
6806
|
+
return true;
|
|
6807
|
+
} catch {
|
|
6808
|
+
return false;
|
|
6564
6809
|
}
|
|
6565
|
-
return false;
|
|
6566
6810
|
}
|
|
6567
6811
|
async function applyClaudePluginHeal(surface, log) {
|
|
6568
6812
|
if (surface !== "claude-cli" && surface !== "claude-vscode") return false;
|
|
6569
6813
|
log(" \u21BB updating the MMI plugin via `claude plugin` (marketplace \u2192 update \u2192 enable)\u2026");
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
["plugin", "enable", "mmi@mmi"]
|
|
6574
|
-
];
|
|
6575
|
-
for (const step of steps) {
|
|
6576
|
-
if (!await runClaudePlugin(step)) return false;
|
|
6577
|
-
}
|
|
6814
|
+
if (!await runClaudePlugin(["plugin", "marketplace", "update", "mmi"])) return false;
|
|
6815
|
+
if (!await runClaudePlugin(["plugin", "update", "mmi@mmi"])) return false;
|
|
6816
|
+
await runClaudePlugin(["plugin", "enable", "mmi@mmi"]);
|
|
6578
6817
|
return true;
|
|
6579
6818
|
}
|
|
6580
6819
|
var program2 = new Command();
|
|
@@ -7030,6 +7269,47 @@ function reportWrite(label, res) {
|
|
|
7030
7269
|
const detail = res.body?.error ?? "";
|
|
7031
7270
|
fail(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
7032
7271
|
}
|
|
7272
|
+
async function v2ReadinessDeps(cfg) {
|
|
7273
|
+
const reg = registryClientDeps(cfg);
|
|
7274
|
+
return {
|
|
7275
|
+
getProject: (slug) => fetchProjectBySlug(slug, reg),
|
|
7276
|
+
hasDeployCoords: async (slug, stage2) => {
|
|
7277
|
+
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
7278
|
+
return Boolean(status?.[stage2]);
|
|
7279
|
+
},
|
|
7280
|
+
listSecrets: async (targetRepo2) => {
|
|
7281
|
+
const apiUrl = cfg.sagaApiUrl;
|
|
7282
|
+
if (!apiUrl) return [];
|
|
7283
|
+
const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
|
|
7284
|
+
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
|
|
7285
|
+
method: "GET",
|
|
7286
|
+
headers: await sagaHeaders(),
|
|
7287
|
+
signal: AbortSignal.timeout(8e3)
|
|
7288
|
+
});
|
|
7289
|
+
if (!res.ok) return [];
|
|
7290
|
+
const body = await res.json();
|
|
7291
|
+
return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
|
|
7292
|
+
}
|
|
7293
|
+
};
|
|
7294
|
+
}
|
|
7295
|
+
async function updateV2ReadinessIssue(repo, report, healed) {
|
|
7296
|
+
const title = "v2 readiness: central deploy + secrets alignment";
|
|
7297
|
+
const freshBody = renderReadinessIssueBody("", report, { healed });
|
|
7298
|
+
const list = await execFileP3("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
|
|
7299
|
+
const issues = JSON.parse(list.stdout || "[]");
|
|
7300
|
+
const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
|
|
7301
|
+
if (!existing) {
|
|
7302
|
+
const created = await execFileP3("gh", ["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"], { timeout: 2e4 });
|
|
7303
|
+
const url = created.stdout.trim();
|
|
7304
|
+
const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
|
|
7305
|
+
return { number, url, action: "created" };
|
|
7306
|
+
}
|
|
7307
|
+
const view = await execFileP3("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
|
|
7308
|
+
const current = JSON.parse(view.stdout || "{}");
|
|
7309
|
+
const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
|
|
7310
|
+
await execFileP3("gh", ["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody], { timeout: 2e4 });
|
|
7311
|
+
return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
|
|
7312
|
+
}
|
|
7033
7313
|
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
|
|
7034
7314
|
project.command("list").description("list all projects (identity + board, never deploy coords)").option("--json", "machine-readable output").action(async (o) => {
|
|
7035
7315
|
const cfg = await loadConfig();
|
|
@@ -7058,6 +7338,38 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
7058
7338
|
}
|
|
7059
7339
|
fail(msg);
|
|
7060
7340
|
});
|
|
7341
|
+
project.command("doctor <owner/repo>").description("diagnose Hub v2 readiness for a repo without reading product repo files").option("--v2", "run the v2 central deploy/secrets readiness contract").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
7342
|
+
if (!o.v2) return fail("project doctor: pass --v2");
|
|
7343
|
+
const cfg = await loadConfig();
|
|
7344
|
+
const report = await buildV2Doctor(repo, await v2ReadinessDeps(cfg));
|
|
7345
|
+
console.log(JSON.stringify(report));
|
|
7346
|
+
if (!report.ok) process.exitCode = 1;
|
|
7347
|
+
});
|
|
7348
|
+
project.command("heal <owner/repo>").description("repair Hub-owned v2 readiness state only; never product repo files").option("--v2", "run the v2 central deploy/secrets readiness contract").option("--apply", "apply the Hub-owned registry patch").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
7349
|
+
if (!o.v2) return fail("project heal: pass --v2");
|
|
7350
|
+
const cfg = await loadConfig();
|
|
7351
|
+
const slug = slugOf(repo);
|
|
7352
|
+
const meta = await fetchProjectBySlug(slug, registryClientDeps(cfg));
|
|
7353
|
+
const plan2 = buildV2HealPatch(repo, meta);
|
|
7354
|
+
if (!o.apply) {
|
|
7355
|
+
console.log(JSON.stringify({ ok: true, slug, dryRun: true, patch: plan2.patch, appOwnedGaps: plan2.appOwnedGaps }));
|
|
7356
|
+
return;
|
|
7357
|
+
}
|
|
7358
|
+
if (!Object.keys(plan2.patch).length) {
|
|
7359
|
+
console.log(JSON.stringify({ ok: true, slug, applied: [], appOwnedGaps: plan2.appOwnedGaps }));
|
|
7360
|
+
return;
|
|
7361
|
+
}
|
|
7362
|
+
const res = await upsertProject(slug, plan2.patch, registryClientDeps(cfg));
|
|
7363
|
+
if (!res.ok) return reportWrite("project heal", res);
|
|
7364
|
+
console.log(JSON.stringify({ ok: true, slug, applied: Object.keys(plan2.patch), appOwnedGaps: plan2.appOwnedGaps, result: res.body }));
|
|
7365
|
+
});
|
|
7366
|
+
project.command("readiness <owner/repo>").description("update the repo v2 readiness issue with Hub diagnosis and app-owned tasks").option("--update-issue", "create/update the bounded v2 readiness issue section").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
7367
|
+
if (!o.updateIssue) return fail("project readiness: pass --update-issue");
|
|
7368
|
+
const cfg = await loadConfig();
|
|
7369
|
+
const report = await buildV2Doctor(repo, await v2ReadinessDeps(cfg));
|
|
7370
|
+
const issue2 = await updateV2ReadinessIssue(repo, report, []);
|
|
7371
|
+
console.log(JSON.stringify({ ok: true, repo, issue: issue2, ready: report.ok }));
|
|
7372
|
+
});
|
|
7061
7373
|
project.command("set <owner/repo>").description("MASTER-ONLY: upsert a project META (idempotent merge; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--deploy-model <model>", "hub-serverless | serverless | tenant-container | content (release-train deploy path, #514)").option("--var <KEY=VALUE...>", "META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
7062
7374
|
const cfg = await loadConfig();
|
|
7063
7375
|
const slug = slugOf(repoOrSlug);
|
|
@@ -7217,6 +7529,15 @@ pr.command("create").description("create a PR and print {number,url} JSON").requ
|
|
|
7217
7529
|
const created = await ghCreate(buildPrArgs({ title: o.title, body: o.body, base: o.base, head: o.head, repo: o.repo }));
|
|
7218
7530
|
console.log(JSON.stringify(created));
|
|
7219
7531
|
});
|
|
7532
|
+
async function remoteBranchExists(branch) {
|
|
7533
|
+
if (!branch) return void 0;
|
|
7534
|
+
try {
|
|
7535
|
+
const { stdout } = await execFileP3("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS });
|
|
7536
|
+
return stdout.trim().length > 0;
|
|
7537
|
+
} catch {
|
|
7538
|
+
return void 0;
|
|
7539
|
+
}
|
|
7540
|
+
}
|
|
7220
7541
|
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
7221
7542
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
7222
7543
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
@@ -7225,11 +7546,42 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
7225
7546
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
7226
7547
|
(await execFileP3("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
7227
7548
|
);
|
|
7549
|
+
const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
|
|
7550
|
+
let remoteDeleteAttempted = false;
|
|
7551
|
+
let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
|
|
7228
7552
|
await execFileP3("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GC_GH_TIMEOUT_MS }).catch((e) => {
|
|
7229
|
-
|
|
7553
|
+
const message = String(e.message || "");
|
|
7554
|
+
if (/already been merged/i.test(message)) {
|
|
7555
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
7556
|
+
return;
|
|
7557
|
+
}
|
|
7558
|
+
if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
|
|
7559
|
+
});
|
|
7560
|
+
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
7561
|
+
const remoteAfter = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
|
|
7562
|
+
const remoteBranch = buildRemoteBranchCleanupReport(headRef, {
|
|
7563
|
+
attempted: remoteDeleteAttempted,
|
|
7564
|
+
existedBefore: remoteBefore,
|
|
7565
|
+
existsAfter: remoteAfter,
|
|
7566
|
+
reason: remoteNotAttemptedReason
|
|
7567
|
+
});
|
|
7568
|
+
const localCleanup = repoArgs.length ? {
|
|
7569
|
+
branch: headRef,
|
|
7570
|
+
localBranch: { name: headRef, status: "not-attempted", reason: "repo-option" },
|
|
7571
|
+
worktree: void 0
|
|
7572
|
+
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
7573
|
+
beforeWorktrees,
|
|
7574
|
+
startingPath,
|
|
7575
|
+
execGit: async (args) => (await execFileP3("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
7230
7576
|
});
|
|
7231
|
-
|
|
7232
|
-
|
|
7577
|
+
console.log(JSON.stringify({
|
|
7578
|
+
merged: number,
|
|
7579
|
+
branch: headRef,
|
|
7580
|
+
method: method.slice(2),
|
|
7581
|
+
remoteBranch,
|
|
7582
|
+
localBranch: localCleanup.localBranch,
|
|
7583
|
+
worktree: localCleanup.worktree
|
|
7584
|
+
}));
|
|
7233
7585
|
});
|
|
7234
7586
|
async function runBoardRead(o) {
|
|
7235
7587
|
try {
|
|
@@ -7427,6 +7779,11 @@ program2.command("stage-live").description("explain that remote rc/live environm
|
|
|
7427
7779
|
});
|
|
7428
7780
|
for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
7429
7781
|
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--apply", commandName === "hotfix" ? "reserved; hotfix uses the /hotfix skill PR path" : "execute the guarded master-only train after explicit approval").action(async (o) => {
|
|
7782
|
+
try {
|
|
7783
|
+
await requireFreshTrainCli(commandName);
|
|
7784
|
+
} catch (e) {
|
|
7785
|
+
return fail(`${commandName}: ${e.message}`);
|
|
7786
|
+
}
|
|
7430
7787
|
if (o.apply) {
|
|
7431
7788
|
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
7432
7789
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|