@mutmutco/cli 2.15.0 → 2.17.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 +1186 -212
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3393,7 +3393,7 @@ var program = new Command();
|
|
|
3393
3393
|
|
|
3394
3394
|
// src/index.ts
|
|
3395
3395
|
var import_promises2 = require("node:fs/promises");
|
|
3396
|
-
var
|
|
3396
|
+
var import_node_fs7 = require("node:fs");
|
|
3397
3397
|
var import_node_crypto3 = require("node:crypto");
|
|
3398
3398
|
|
|
3399
3399
|
// src/rules-sync.ts
|
|
@@ -3473,7 +3473,10 @@ function buildSessionStartPlan(verbs) {
|
|
|
3473
3473
|
parallel: [
|
|
3474
3474
|
{ name: "rules sync", run: verbs.rulesSync },
|
|
3475
3475
|
{ name: "saga show", run: verbs.sagaShow },
|
|
3476
|
-
{ name: "saga health", run: verbs.sagaHealth }
|
|
3476
|
+
{ name: "saga health", run: verbs.sagaHealth },
|
|
3477
|
+
// whoami (#879): the resolved human lands in the banner so agents act --for them without asking.
|
|
3478
|
+
// Identity reads are memoized process-wide, so this adds no extra gh/Hub round-trip.
|
|
3479
|
+
{ name: "whoami", run: verbs.whoami }
|
|
3477
3480
|
],
|
|
3478
3481
|
sequential: [{ name: "doctor", run: verbs.doctor }]
|
|
3479
3482
|
};
|
|
@@ -3488,6 +3491,29 @@ function northstarPointer() {
|
|
|
3488
3491
|
return "North Stars: run `mmi-cli northstar relevant` to load plans relevant to your task (`northstar list` for all).";
|
|
3489
3492
|
}
|
|
3490
3493
|
|
|
3494
|
+
// src/whoami.ts
|
|
3495
|
+
async function resolveWhoami(deps) {
|
|
3496
|
+
let session;
|
|
3497
|
+
try {
|
|
3498
|
+
session = await deps.hubSession();
|
|
3499
|
+
} catch {
|
|
3500
|
+
session = void 0;
|
|
3501
|
+
}
|
|
3502
|
+
if (session?.login) return { login: session.login, source: "hub-session", sessionExpiresAt: session.expiresAt };
|
|
3503
|
+
let ghLogin;
|
|
3504
|
+
try {
|
|
3505
|
+
ghLogin = await deps.ghLogin();
|
|
3506
|
+
} catch {
|
|
3507
|
+
ghLogin = void 0;
|
|
3508
|
+
}
|
|
3509
|
+
if (ghLogin) return { login: ghLogin, source: "github", sessionExpiresAt: session?.expiresAt };
|
|
3510
|
+
return { source: "unknown" };
|
|
3511
|
+
}
|
|
3512
|
+
function whoamiLine(report) {
|
|
3513
|
+
if (!report.login) return null;
|
|
3514
|
+
return `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3491
3517
|
// src/saga-capture.ts
|
|
3492
3518
|
function parseHookInput(stdin) {
|
|
3493
3519
|
try {
|
|
@@ -3501,7 +3527,7 @@ function parseHookInput(stdin) {
|
|
|
3501
3527
|
// src/index.ts
|
|
3502
3528
|
var import_node_child_process6 = require("node:child_process");
|
|
3503
3529
|
var import_node_util6 = require("node:util");
|
|
3504
|
-
var
|
|
3530
|
+
var import_node_path8 = require("node:path");
|
|
3505
3531
|
var import_node_os3 = require("node:os");
|
|
3506
3532
|
|
|
3507
3533
|
// src/saga-head-maintainer.ts
|
|
@@ -3823,6 +3849,49 @@ function defaultHubUrl() {
|
|
|
3823
3849
|
return process.env.MMI_HUB_URL || DEFAULT_HUB_URL;
|
|
3824
3850
|
}
|
|
3825
3851
|
|
|
3852
|
+
// src/client-version.ts
|
|
3853
|
+
var import_node_fs3 = require("node:fs");
|
|
3854
|
+
var import_node_path4 = require("node:path");
|
|
3855
|
+
|
|
3856
|
+
// ../infra/compat.mjs
|
|
3857
|
+
var CLIENT_VERSION_HEADER = "x-client-version";
|
|
3858
|
+
function parseSemver(s) {
|
|
3859
|
+
if (typeof s !== "string") return null;
|
|
3860
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+]|$)/.exec(s.trim());
|
|
3861
|
+
if (!m) return null;
|
|
3862
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
|
|
3863
|
+
}
|
|
3864
|
+
function versionAtLeast(v, min) {
|
|
3865
|
+
const a = parseSemver(v);
|
|
3866
|
+
const b = parseSemver(min);
|
|
3867
|
+
if (!a || !b) return false;
|
|
3868
|
+
if (a.major !== b.major) return a.major > b.major;
|
|
3869
|
+
if (a.minor !== b.minor) return a.minor > b.minor;
|
|
3870
|
+
return a.patch >= b.patch;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// src/client-version.ts
|
|
3874
|
+
function resolveClientVersion() {
|
|
3875
|
+
try {
|
|
3876
|
+
const manifest = (0, import_node_path4.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
3877
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(manifest, "utf8")).version || "0.0.0";
|
|
3878
|
+
} catch {
|
|
3879
|
+
try {
|
|
3880
|
+
const pkg = (0, import_node_path4.join)(__dirname, "..", "package.json");
|
|
3881
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
3882
|
+
} catch {
|
|
3883
|
+
return "0.0.0";
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
function clientVersionHeaders() {
|
|
3888
|
+
return { [CLIENT_VERSION_HEADER]: resolveClientVersion() };
|
|
3889
|
+
}
|
|
3890
|
+
function upgradeRequiredError(res, body) {
|
|
3891
|
+
const minVersion = body && typeof body === "object" && typeof body.minVersion === "string" ? body.minVersion : "a newer version";
|
|
3892
|
+
return `Hub requires mmi-cli >= ${minVersion} \u2014 run mmi-cli doctor (installed ${resolveClientVersion()})`;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3826
3895
|
// src/saga-health.ts
|
|
3827
3896
|
function buildHealth(i) {
|
|
3828
3897
|
const problems = [];
|
|
@@ -3885,6 +3954,30 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3885
3954
|
throw lastErr;
|
|
3886
3955
|
}
|
|
3887
3956
|
|
|
3957
|
+
// src/clean-exit.ts
|
|
3958
|
+
function globalDispatcher() {
|
|
3959
|
+
const g = globalThis;
|
|
3960
|
+
const sym = Object.getOwnPropertySymbols(g).find(
|
|
3961
|
+
(s) => s.description === "undici.globalDispatcher.1" || s.description?.startsWith("undici.globalDispatcher.")
|
|
3962
|
+
);
|
|
3963
|
+
return sym ? g[sym] : void 0;
|
|
3964
|
+
}
|
|
3965
|
+
function destroyHttpPool() {
|
|
3966
|
+
try {
|
|
3967
|
+
const dispatcher = globalDispatcher();
|
|
3968
|
+
if (dispatcher?.destroy) {
|
|
3969
|
+
void dispatcher.destroy();
|
|
3970
|
+
return true;
|
|
3971
|
+
}
|
|
3972
|
+
} catch {
|
|
3973
|
+
}
|
|
3974
|
+
return false;
|
|
3975
|
+
}
|
|
3976
|
+
function hardExit(code) {
|
|
3977
|
+
destroyHttpPool();
|
|
3978
|
+
process.exit(code);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3888
3981
|
// src/saga-note.ts
|
|
3889
3982
|
var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
|
|
3890
3983
|
var ROUTE_LEVEL_403 = "saga API route-level 403 from HubSessionAuthorizer/session policy";
|
|
@@ -5020,6 +5113,47 @@ function buildRemoteBranchCleanupReport(branch, input) {
|
|
|
5020
5113
|
if (input.existsAfter === false) return { name: branch, status: "deleted" };
|
|
5021
5114
|
return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
|
|
5022
5115
|
}
|
|
5116
|
+
async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input) {
|
|
5117
|
+
const existsAfter = input.attempted ? await deps.exists(branch, { prune: true }) : void 0;
|
|
5118
|
+
return buildRemoteBranchCleanupReport(branch, {
|
|
5119
|
+
attempted: input.attempted,
|
|
5120
|
+
existedBefore: input.existedBefore,
|
|
5121
|
+
existsAfter,
|
|
5122
|
+
reason: input.reason
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
function summarizePrMergeCleanupStatus(input) {
|
|
5126
|
+
if (input.remoteBranch.status === "failed") return "warnings";
|
|
5127
|
+
if (input.localBranch.status === "failed") return "warnings";
|
|
5128
|
+
if (input.localBranch.reason === "worktree-removal-failed") return "warnings";
|
|
5129
|
+
if (input.worktree?.status === "failed") return "warnings";
|
|
5130
|
+
return "clean";
|
|
5131
|
+
}
|
|
5132
|
+
function buildPrMergeResultPayload(input) {
|
|
5133
|
+
return {
|
|
5134
|
+
mergeStatus: "merged",
|
|
5135
|
+
merged: input.number,
|
|
5136
|
+
branch: input.branch,
|
|
5137
|
+
method: input.method,
|
|
5138
|
+
cleanupStatus: summarizePrMergeCleanupStatus({
|
|
5139
|
+
remoteBranch: input.remoteBranch,
|
|
5140
|
+
localBranch: input.localCleanup.localBranch,
|
|
5141
|
+
worktree: input.localCleanup.worktree
|
|
5142
|
+
}),
|
|
5143
|
+
remoteBranch: input.remoteBranch,
|
|
5144
|
+
localBranch: input.localCleanup.localBranch,
|
|
5145
|
+
worktree: input.localCleanup.worktree
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
5148
|
+
async function checkRemoteBranchExists(branch, deps, options = {}) {
|
|
5149
|
+
if (!branch) return void 0;
|
|
5150
|
+
try {
|
|
5151
|
+
if (options.prune) await deps.execGit(["fetch", "origin", "--prune"]).catch(() => void 0);
|
|
5152
|
+
return (await deps.execGit(["ls-remote", "--heads", "origin", branch])).trim().length > 0;
|
|
5153
|
+
} catch {
|
|
5154
|
+
return void 0;
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5023
5157
|
var DEFAULT_PROTECTED = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
|
|
5024
5158
|
function groupedPrs(prs) {
|
|
5025
5159
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -5221,7 +5355,7 @@ function branchMissingFromList(branch, stdout) {
|
|
|
5221
5355
|
return !names.includes(branch);
|
|
5222
5356
|
}
|
|
5223
5357
|
function shellQuote(value) {
|
|
5224
|
-
return `
|
|
5358
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
5225
5359
|
}
|
|
5226
5360
|
function safeWorktreeRemoveCommand(safeCwd, targetPath) {
|
|
5227
5361
|
const prefix = safeCwd ? `git -C ${shellQuote(safeCwd)}` : "git";
|
|
@@ -5247,7 +5381,11 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
5247
5381
|
const wtPath = selectPrMergeCleanupWorktree(branch, beforeWorktrees, afterWorktrees, options.startingPath);
|
|
5248
5382
|
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath);
|
|
5249
5383
|
const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
|
|
5250
|
-
|
|
5384
|
+
const mainWorktreePath = beforeWorktrees[0]?.path ?? afterWorktrees[0]?.path;
|
|
5385
|
+
const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
|
|
5386
|
+
if (wtPath && mainWorktreeTarget) {
|
|
5387
|
+
report.worktree = { path: wtPath, status: "not-attempted", reason: "main-worktree" };
|
|
5388
|
+
} else if (wtPath) {
|
|
5251
5389
|
let stageTeardown;
|
|
5252
5390
|
if (options.teardownWorktreeStage) {
|
|
5253
5391
|
try {
|
|
@@ -5315,6 +5453,43 @@ function formatGcPlan(plan2, apply) {
|
|
|
5315
5453
|
}
|
|
5316
5454
|
|
|
5317
5455
|
// src/command-plans.ts
|
|
5456
|
+
function parseTrainTag(tag) {
|
|
5457
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(tag);
|
|
5458
|
+
return m ? { major: +m[1], minor: +m[2], patch: +m[3], rc: m[4] ? +m[4] : null } : null;
|
|
5459
|
+
}
|
|
5460
|
+
function cmpTrainRelease(a, b) {
|
|
5461
|
+
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
5462
|
+
}
|
|
5463
|
+
function trainPlanTargetsFromTags(tags, explicitRaw) {
|
|
5464
|
+
const all = tags.map(parseTrainTag).filter((v) => Boolean(v));
|
|
5465
|
+
const release = all.find((v) => v.rc === null) ?? { major: 0, minor: 0, patch: 0, rc: null };
|
|
5466
|
+
const targets = {};
|
|
5467
|
+
let explicit = null;
|
|
5468
|
+
if (explicitRaw) {
|
|
5469
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(String(explicitRaw).trim());
|
|
5470
|
+
if (!m) {
|
|
5471
|
+
targets.plannedReleaseError = `MMI_RELEASE_VERSION must be X.Y.Z (got ${explicitRaw})`;
|
|
5472
|
+
} else {
|
|
5473
|
+
explicit = { major: +m[1], minor: +m[2], patch: +m[3], rc: null };
|
|
5474
|
+
if (cmpTrainRelease(explicit, release) <= 0) {
|
|
5475
|
+
targets.plannedReleaseError = `MMI_RELEASE_VERSION ${explicitRaw} must be ahead of the latest release v${release.major}.${release.minor}.${release.patch}`;
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
if (!targets.plannedReleaseError) {
|
|
5480
|
+
const planned = explicit ?? { major: release.major, minor: release.minor + 1, patch: 0, rc: null };
|
|
5481
|
+
targets.plannedRelease = `v${planned.major}.${planned.minor}.${planned.patch}`;
|
|
5482
|
+
}
|
|
5483
|
+
let rcs = all.filter((v) => v.rc !== null);
|
|
5484
|
+
if (explicit) rcs = rcs.filter((v) => v.major === explicit.major && v.minor === explicit.minor && v.patch === explicit.patch);
|
|
5485
|
+
rcs.sort((a, b) => cmpTrainRelease(b, a) || (b.rc ?? 0) - (a.rc ?? 0));
|
|
5486
|
+
if (rcs[0]) {
|
|
5487
|
+
targets.existingRcRelease = `v${rcs[0].major}.${rcs[0].minor}.${rcs[0].patch}`;
|
|
5488
|
+
} else {
|
|
5489
|
+
targets.existingRcReleaseError = explicit ? `no rc tag for ${explicit.major}.${explicit.minor}.${explicit.patch} to release` : "no rc tag to release";
|
|
5490
|
+
}
|
|
5491
|
+
return targets;
|
|
5492
|
+
}
|
|
5318
5493
|
function stagePlan(stage2 = {}, stops = true) {
|
|
5319
5494
|
return [
|
|
5320
5495
|
...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
|
|
@@ -5343,20 +5518,49 @@ function stageLivePlan() {
|
|
|
5343
5518
|
{ label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli rcand && mmi-cli release && mmi-cli hotfix", gated: true }
|
|
5344
5519
|
];
|
|
5345
5520
|
}
|
|
5346
|
-
function
|
|
5521
|
+
function rcandVersionStep(targets) {
|
|
5522
|
+
const planned = targets.plannedRelease ? `plannedRelease=${targets.plannedRelease}` : "plannedRelease=(unresolved)";
|
|
5523
|
+
const existing = targets.existingRcRelease ? `existingRcRelease=${targets.existingRcRelease}` : "existingRcRelease=(none resolved yet)";
|
|
5524
|
+
return {
|
|
5525
|
+
label: `resolve train version targets (${planned}; ${existing})`,
|
|
5526
|
+
command: "plannedRelease: node scripts/next-version.mjs cycle; existing rc release only: node scripts/next-version.mjs release",
|
|
5527
|
+
gated: true
|
|
5528
|
+
};
|
|
5529
|
+
}
|
|
5530
|
+
function trainPlan(command, options = {}) {
|
|
5531
|
+
const isHub = options.repo?.toLowerCase() === "mutmutco/mmi-hub";
|
|
5347
5532
|
if (command === "rcand") {
|
|
5533
|
+
if (isHub) {
|
|
5534
|
+
return [
|
|
5535
|
+
{ label: "MMI-Hub releases skip rc; use /release from development instead", command: "mmi-cli release --apply", gated: true }
|
|
5536
|
+
];
|
|
5537
|
+
}
|
|
5348
5538
|
return [
|
|
5349
5539
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
5350
5540
|
{ label: "verify current branch is development", gated: true },
|
|
5541
|
+
rcandVersionStep(options),
|
|
5351
5542
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5352
5543
|
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
5353
|
-
{ 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 },
|
|
5544
|
+
{ 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; if missing: mmi-cli train prep --apply", gated: true },
|
|
5354
5545
|
{ label: "merge development to rc", gated: true },
|
|
5355
|
-
{ label: "trigger the deploy path for this repo model, returning
|
|
5546
|
+
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml auto-fires on rc push, correlate/watch that run", gated: true },
|
|
5356
5547
|
{ label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli tenant redeploy <owner/repo> rc --watch", gated: true }
|
|
5357
5548
|
];
|
|
5358
5549
|
}
|
|
5359
5550
|
if (command === "release") {
|
|
5551
|
+
if (isHub) {
|
|
5552
|
+
return [
|
|
5553
|
+
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
5554
|
+
{ label: "verify current branch is development", gated: true },
|
|
5555
|
+
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5556
|
+
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5557
|
+
{ label: "Hub releases skip rc; verify the distribution bump is already landed on development", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5558
|
+
{ label: "merge development to main", gated: true },
|
|
5559
|
+
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5560
|
+
{ label: "trigger the Hub deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release", gated: true },
|
|
5561
|
+
{ label: "roll development forward", gated: true }
|
|
5562
|
+
];
|
|
5563
|
+
}
|
|
5360
5564
|
return [
|
|
5361
5565
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
5362
5566
|
{ label: "verify current branch is rc", gated: true },
|
|
@@ -5364,9 +5568,9 @@ function trainPlan(command) {
|
|
|
5364
5568
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5365
5569
|
{ label: "verify every main-only hotfix commit is covered by the rc candidate", command: "node scripts/hotfix-coverage.mjs", gated: true },
|
|
5366
5570
|
{ label: "merge rc to main", gated: true },
|
|
5367
|
-
{ 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 },
|
|
5571
|
+
{ 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; if missing: mmi-cli train prep --apply", gated: true },
|
|
5368
5572
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5369
|
-
{ label: "trigger the deploy path for this repo model, returning
|
|
5573
|
+
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs", gated: true },
|
|
5370
5574
|
{ label: "roll development forward", gated: true }
|
|
5371
5575
|
];
|
|
5372
5576
|
}
|
|
@@ -5453,9 +5657,9 @@ function stalePosixFields(config, shell2) {
|
|
|
5453
5657
|
}
|
|
5454
5658
|
function sanitizeLocalStage(local, stale) {
|
|
5455
5659
|
if (!stale.length) return local;
|
|
5456
|
-
const
|
|
5457
|
-
for (const field of stale) delete
|
|
5458
|
-
return
|
|
5660
|
+
const clean4 = { ...local };
|
|
5661
|
+
for (const field of stale) delete clean4[field];
|
|
5662
|
+
return clean4;
|
|
5459
5663
|
}
|
|
5460
5664
|
function staleNote(staleFields, outcome) {
|
|
5461
5665
|
const list = staleFields.join(", ");
|
|
@@ -5718,6 +5922,20 @@ function buildPluginConfigDriftCheck(input) {
|
|
|
5718
5922
|
}
|
|
5719
5923
|
var GITIGNORE_BLOCK_LABEL = "org .gitignore managed block (.playwright-mcp/, .claude/worktrees/, scratch *.png)";
|
|
5720
5924
|
var GITIGNORE_BLOCK_FIX = "run `mmi-cli doctor` to auto-insert the `# >>> mmi-managed >>>` block (or copy it from MMI-Hub's .gitignore)";
|
|
5925
|
+
var GITIGNORE_BLOCK_DEFERRED_FIX = "run `mmi-cli doctor --apply` (without --no-repo-writes) after the release train to write the org-managed .gitignore block, then stage & commit .gitignore";
|
|
5926
|
+
function applyGitignoreRepoWritePolicy(check, repoWritesAllowed) {
|
|
5927
|
+
if (repoWritesAllowed || check.ok || !check.contentToWrite) return check;
|
|
5928
|
+
return { ...check, fix: GITIGNORE_BLOCK_DEFERRED_FIX };
|
|
5929
|
+
}
|
|
5930
|
+
function decideGitignoreRepair(check, flags) {
|
|
5931
|
+
if (check.ok || !check.contentToWrite) return { action: "none", check };
|
|
5932
|
+
if (!flags.repoWritesAllowed) {
|
|
5933
|
+
if (!flags.repairFull) return { action: "none", check };
|
|
5934
|
+
return { action: "suppress", check: applyGitignoreRepoWritePolicy(check, false) };
|
|
5935
|
+
}
|
|
5936
|
+
if (flags.repairFull) return { action: "write", check };
|
|
5937
|
+
return { action: "none", check };
|
|
5938
|
+
}
|
|
5721
5939
|
function buildGitignoreManagedBlockCheck(input) {
|
|
5722
5940
|
const base = { ok: true, label: GITIGNORE_BLOCK_LABEL, fix: GITIGNORE_BLOCK_FIX };
|
|
5723
5941
|
if (!input.isOrgRepo) return base;
|
|
@@ -5812,6 +6030,57 @@ function pluginRecoveryFix(surface) {
|
|
|
5812
6030
|
return "npm install -g @mutmutco/cli@latest";
|
|
5813
6031
|
}
|
|
5814
6032
|
}
|
|
6033
|
+
var PLUGIN_UPDATE_RECIPES = {
|
|
6034
|
+
claude: ["claude plugin update mmi@mmi"],
|
|
6035
|
+
codex: ["codex plugin marketplace upgrade mmi", "codex plugin list # verify mmi@mmi shows the new version"],
|
|
6036
|
+
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
6037
|
+
};
|
|
6038
|
+
function highestSemver(versions) {
|
|
6039
|
+
return versions.reduce((best, v) => {
|
|
6040
|
+
if (!isSemverVersion(v)) return best;
|
|
6041
|
+
if (best === void 0) return v;
|
|
6042
|
+
return compareVersions(v, best) > 0 ? v : best;
|
|
6043
|
+
}, void 0);
|
|
6044
|
+
}
|
|
6045
|
+
function buildPluginUpdateReport(input) {
|
|
6046
|
+
const claudePlugin = highestSemver(input.claudePluginVersions ?? []);
|
|
6047
|
+
const codexMarketplace = highestSemver(input.codexPluginVersions ?? []);
|
|
6048
|
+
const codexActiveCache = highestSemver(input.codexCacheVersions ?? []);
|
|
6049
|
+
return {
|
|
6050
|
+
versions: {
|
|
6051
|
+
...isSemverVersion(input.cliVersion) ? { cli: input.cliVersion } : {},
|
|
6052
|
+
...claudePlugin ? { claudePlugin } : {},
|
|
6053
|
+
...codexMarketplace ? { codexMarketplace } : {},
|
|
6054
|
+
...codexActiveCache ? { codexActiveCache } : {},
|
|
6055
|
+
...isSemverVersion(input.releasedVersion) ? { released: input.releasedVersion } : {}
|
|
6056
|
+
},
|
|
6057
|
+
recipes: PLUGIN_UPDATE_RECIPES
|
|
6058
|
+
};
|
|
6059
|
+
}
|
|
6060
|
+
function renderPluginUpdateReport(report) {
|
|
6061
|
+
const v = report.versions;
|
|
6062
|
+
const show = (x) => x ?? "unknown";
|
|
6063
|
+
return [
|
|
6064
|
+
"MMI versions:",
|
|
6065
|
+
` CLI: ${show(v.cli)}`,
|
|
6066
|
+
` Claude plugin: ${show(v.claudePlugin)}`,
|
|
6067
|
+
` Codex marketplace: ${show(v.codexMarketplace)}`,
|
|
6068
|
+
` Codex active cache: ${show(v.codexActiveCache)}`,
|
|
6069
|
+
` latest release: ${show(v.released)}`,
|
|
6070
|
+
"Update recipes (per surface):",
|
|
6071
|
+
` Claude: ${report.recipes.claude.join(" ; ")}`,
|
|
6072
|
+
` Codex: ${report.recipes.codex.join(" ; ")}`,
|
|
6073
|
+
` npm CLI: ${report.recipes.cli.join(" ; ")}`
|
|
6074
|
+
];
|
|
6075
|
+
}
|
|
6076
|
+
function buildDoctorJsonPayload(input) {
|
|
6077
|
+
return {
|
|
6078
|
+
ok: input.checks.every((c) => c.ok),
|
|
6079
|
+
checks: input.checks,
|
|
6080
|
+
updateReport: input.updateReport,
|
|
6081
|
+
...input.resources.length ? { resources: input.resources } : {}
|
|
6082
|
+
};
|
|
6083
|
+
}
|
|
5815
6084
|
var INSTALLED_PLUGIN_VERSION_LABEL = "installed MMI plugin version (vs latest release)";
|
|
5816
6085
|
function isSemverVersion(v) {
|
|
5817
6086
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
@@ -5922,16 +6191,25 @@ function buildCursorPluginInstallCheck(input) {
|
|
|
5922
6191
|
}
|
|
5923
6192
|
return { ...base, cacheRoot: input.cacheRoot, pins: input.pins };
|
|
5924
6193
|
}
|
|
6194
|
+
var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
|
|
6195
|
+
function buildHubCompatCheck(input) {
|
|
6196
|
+
const label = "Hub compatibility (client version vs Hub minimum)";
|
|
6197
|
+
const min = input.versionInfo?.minClientVersion;
|
|
6198
|
+
if (!input.isOrgRepo || !min || !parseSemver(min) || !parseSemver(input.installedVersion)) {
|
|
6199
|
+
return { ok: true, label, fix: HUB_COMPAT_FIX };
|
|
6200
|
+
}
|
|
6201
|
+
return { ok: versionAtLeast(input.installedVersion, min), label: `${label}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
|
|
6202
|
+
}
|
|
5925
6203
|
|
|
5926
6204
|
// src/stage-runner.ts
|
|
5927
6205
|
var import_node_child_process5 = require("node:child_process");
|
|
5928
|
-
var
|
|
5929
|
-
var
|
|
6206
|
+
var import_node_fs4 = require("node:fs");
|
|
6207
|
+
var import_node_path5 = require("node:path");
|
|
5930
6208
|
var import_node_net = require("node:net");
|
|
5931
6209
|
var import_node_util5 = require("node:util");
|
|
5932
6210
|
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
|
|
5933
6211
|
function stageStatePath(cwd = process.cwd()) {
|
|
5934
|
-
return (0,
|
|
6212
|
+
return (0, import_node_path5.join)(cwd, "tmp", "stage", "state.json");
|
|
5935
6213
|
}
|
|
5936
6214
|
var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
|
|
5937
6215
|
function posixOnlyShellProblems(command, field, platform = process.platform) {
|
|
@@ -5993,9 +6271,9 @@ async function shell(command, cwd, timeoutMs) {
|
|
|
5993
6271
|
});
|
|
5994
6272
|
}
|
|
5995
6273
|
function readState(path2) {
|
|
5996
|
-
if (!(0,
|
|
6274
|
+
if (!(0, import_node_fs4.existsSync)(path2)) return null;
|
|
5997
6275
|
try {
|
|
5998
|
-
return JSON.parse((0,
|
|
6276
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(path2, "utf8"));
|
|
5999
6277
|
} catch {
|
|
6000
6278
|
return null;
|
|
6001
6279
|
}
|
|
@@ -6047,7 +6325,7 @@ async function stopStage(opts = {}) {
|
|
|
6047
6325
|
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
6048
6326
|
}
|
|
6049
6327
|
await killTree(state.pid);
|
|
6050
|
-
(0,
|
|
6328
|
+
(0, import_node_fs4.rmSync)(statePath, { force: true });
|
|
6051
6329
|
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
6052
6330
|
}
|
|
6053
6331
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -6056,7 +6334,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
6056
6334
|
const cwd = opts.cwd ?? process.cwd();
|
|
6057
6335
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
6058
6336
|
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
6059
|
-
(0,
|
|
6337
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
6060
6338
|
let stagePort;
|
|
6061
6339
|
if (config.portRange) {
|
|
6062
6340
|
const [s, e] = config.portRange;
|
|
@@ -6066,9 +6344,9 @@ async function startStage(config = {}, opts = {}) {
|
|
|
6066
6344
|
}
|
|
6067
6345
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
6068
6346
|
if (config.ensureEnv) {
|
|
6069
|
-
const target = (0,
|
|
6070
|
-
const example = (0,
|
|
6071
|
-
if (!(0,
|
|
6347
|
+
const target = (0, import_node_path5.join)(cwd, config.ensureEnv.target);
|
|
6348
|
+
const example = (0, import_node_path5.join)(cwd, config.ensureEnv.example);
|
|
6349
|
+
if (!(0, import_node_fs4.existsSync)(target) && (0, import_node_fs4.existsSync)(example)) (0, import_node_fs4.copyFileSync)(example, target);
|
|
6072
6350
|
}
|
|
6073
6351
|
const extraEnv = {};
|
|
6074
6352
|
for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
|
|
@@ -6092,12 +6370,12 @@ async function startStage(config = {}, opts = {}) {
|
|
|
6092
6370
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
6093
6371
|
port: stagePort
|
|
6094
6372
|
};
|
|
6095
|
-
(0,
|
|
6373
|
+
(0, import_node_fs4.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
6096
6374
|
try {
|
|
6097
6375
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
6098
6376
|
} catch (e) {
|
|
6099
6377
|
await killTree(state.pid);
|
|
6100
|
-
(0,
|
|
6378
|
+
(0, import_node_fs4.rmSync)(statePath, { force: true });
|
|
6101
6379
|
throw e;
|
|
6102
6380
|
}
|
|
6103
6381
|
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
@@ -6167,14 +6445,42 @@ function requireValue(value, label) {
|
|
|
6167
6445
|
if (!value) throw new Error(`${label} could not be resolved`);
|
|
6168
6446
|
return value;
|
|
6169
6447
|
}
|
|
6170
|
-
function releaseTagFromRcTag(tag) {
|
|
6171
|
-
return tag.replace(/-rc\.\d+$/, "");
|
|
6172
|
-
}
|
|
6173
6448
|
async function verifyHubDistributionVersion(deps, model, releaseTag) {
|
|
6174
6449
|
if (model !== "hub-serverless") return;
|
|
6175
6450
|
await deps.run("node", ["scripts/release-distribution.mjs", "verify", releaseTag, "--skip-npm-view"]);
|
|
6176
6451
|
}
|
|
6177
|
-
|
|
6452
|
+
async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedSha) {
|
|
6453
|
+
const out = await deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName,targetCommitish"]);
|
|
6454
|
+
let release;
|
|
6455
|
+
try {
|
|
6456
|
+
release = JSON.parse(out);
|
|
6457
|
+
} catch {
|
|
6458
|
+
throw new Error(`Release ${tag} metadata was not valid JSON`);
|
|
6459
|
+
}
|
|
6460
|
+
if (release.tagName !== tag) {
|
|
6461
|
+
throw new Error(`Release metadata tag is ${String(release.tagName || "(missing)")}, expected ${tag}`);
|
|
6462
|
+
}
|
|
6463
|
+
if (release.targetCommitish !== expectedTarget) {
|
|
6464
|
+
throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
|
|
6465
|
+
}
|
|
6466
|
+
const tagSha = requireValue(clean(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
|
|
6467
|
+
if (tagSha !== expectedSha) {
|
|
6468
|
+
throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
|
|
6469
|
+
}
|
|
6470
|
+
const branchOut = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
|
|
6471
|
+
const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
|
|
6472
|
+
if (branchSha !== expectedSha) {
|
|
6473
|
+
throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
var ORG_SPINE_FILES = [
|
|
6477
|
+
"AGENTS.md",
|
|
6478
|
+
"CLAUDE.md",
|
|
6479
|
+
"GEMINI.md",
|
|
6480
|
+
".claude/settings.json",
|
|
6481
|
+
".claude/output-styles/mmi-plain.md",
|
|
6482
|
+
".cursor/rules/mmi-plain-language.mdc"
|
|
6483
|
+
];
|
|
6178
6484
|
function isSpinePath(path2) {
|
|
6179
6485
|
return ORG_SPINE_FILES.includes(path2);
|
|
6180
6486
|
}
|
|
@@ -6243,6 +6549,9 @@ async function requireBranch(deps, branch) {
|
|
|
6243
6549
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
6244
6550
|
}
|
|
6245
6551
|
var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
6552
|
+
function isHubControlRepo(repo) {
|
|
6553
|
+
return repo.toLowerCase() === HUB_REPO2.toLowerCase();
|
|
6554
|
+
}
|
|
6246
6555
|
var CORRELATE_ATTEMPTS = 5;
|
|
6247
6556
|
var CORRELATE_DELAY_MS = 1500;
|
|
6248
6557
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
@@ -6280,6 +6589,42 @@ async function correlateTenantRun(deps, since) {
|
|
|
6280
6589
|
}
|
|
6281
6590
|
return {};
|
|
6282
6591
|
}
|
|
6592
|
+
async function correlateWorkflowRun(deps, args) {
|
|
6593
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6594
|
+
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
6595
|
+
let lastError;
|
|
6596
|
+
let parsedAnyResponse = false;
|
|
6597
|
+
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
6598
|
+
if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
|
|
6599
|
+
const listArgs = [
|
|
6600
|
+
"run",
|
|
6601
|
+
"list",
|
|
6602
|
+
"--repo",
|
|
6603
|
+
HUB_REPO2,
|
|
6604
|
+
"--workflow",
|
|
6605
|
+
args.workflow,
|
|
6606
|
+
"--event",
|
|
6607
|
+
args.event,
|
|
6608
|
+
...args.branch ? ["--branch", args.branch] : [],
|
|
6609
|
+
"--limit",
|
|
6610
|
+
"10",
|
|
6611
|
+
"--json",
|
|
6612
|
+
"databaseId,url,event,createdAt,status,conclusion,headSha"
|
|
6613
|
+
];
|
|
6614
|
+
let rows;
|
|
6615
|
+
try {
|
|
6616
|
+
rows = JSON.parse(await deps.run("gh", listArgs));
|
|
6617
|
+
parsedAnyResponse = true;
|
|
6618
|
+
} catch {
|
|
6619
|
+
lastError = new Error(`could not list ${args.workflow} runs`);
|
|
6620
|
+
continue;
|
|
6621
|
+
}
|
|
6622
|
+
const match = rows.filter((r) => r.event === args.event && r.headSha === args.headSha && typeof r.databaseId === "number").map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
|
|
6623
|
+
if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
|
|
6624
|
+
}
|
|
6625
|
+
if (!parsedAnyResponse && lastError) throw lastError;
|
|
6626
|
+
return {};
|
|
6627
|
+
}
|
|
6283
6628
|
async function watchTenantRun(deps, runId) {
|
|
6284
6629
|
if (runId == null) return "pending";
|
|
6285
6630
|
try {
|
|
@@ -6289,6 +6634,16 @@ async function watchTenantRun(deps, runId) {
|
|
|
6289
6634
|
return "failure";
|
|
6290
6635
|
}
|
|
6291
6636
|
}
|
|
6637
|
+
async function watchWorkflowRun(deps, workflow, run) {
|
|
6638
|
+
if (run.runId == null) return { workflow, conclusion: "pending" };
|
|
6639
|
+
const conclusion = await watchTenantRun(deps, run.runId);
|
|
6640
|
+
return { workflow, runId: run.runId, runUrl: run.runUrl, conclusion };
|
|
6641
|
+
}
|
|
6642
|
+
function aggregateWorkflowRuns(runs) {
|
|
6643
|
+
if (runs.some((r) => r.conclusion === "failure")) return "failure";
|
|
6644
|
+
if (runs.some((r) => r.conclusion === "pending")) return "pending";
|
|
6645
|
+
return "success";
|
|
6646
|
+
}
|
|
6292
6647
|
function isNotFoundError(e) {
|
|
6293
6648
|
const msg = `${e.message ?? e} ${String(e.stderr ?? "")}`;
|
|
6294
6649
|
return /HTTP 404|Not Found|\(404\)/i.test(msg);
|
|
@@ -6395,7 +6750,19 @@ async function ensureTagPushed(deps, tag, sha) {
|
|
|
6395
6750
|
await deps.run("git", ["push", "origin", tag]);
|
|
6396
6751
|
return `tag ${tag} pushed at ${sha.slice(0, 7)}`;
|
|
6397
6752
|
}
|
|
6398
|
-
async function
|
|
6753
|
+
async function resolveRcResumeTag(deps, base, sha) {
|
|
6754
|
+
const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
|
|
6755
|
+
const onSha = clean(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
|
|
6756
|
+
const unique = [...new Set(onSha)];
|
|
6757
|
+
if (unique.length === 0) return { tag: null };
|
|
6758
|
+
const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
|
|
6759
|
+
const sorted = unique.sort((a, b) => rcNum(b) - rcNum(a));
|
|
6760
|
+
const newest = sorted[0];
|
|
6761
|
+
const leftovers = sorted.slice(1);
|
|
6762
|
+
const note = leftovers.length > 0 ? `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} (newest of ${sorted.length}); harmless leftover tag(s) on the same SHA: ${leftovers.join(", ")}` : `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} instead of minting a fresh rc`;
|
|
6763
|
+
return { tag: newest, note };
|
|
6764
|
+
}
|
|
6765
|
+
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha) {
|
|
6399
6766
|
if (model === "tenant-container") {
|
|
6400
6767
|
const since = (deps.now ?? Date.now)();
|
|
6401
6768
|
await deps.run("gh", [
|
|
@@ -6418,9 +6785,30 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
|
|
|
6418
6785
|
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
6419
6786
|
}
|
|
6420
6787
|
if (model === "hub-serverless") {
|
|
6788
|
+
const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
|
|
6789
|
+
if (!watch) return { note, deployStatus: "pending" };
|
|
6790
|
+
if (!autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
6791
|
+
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
6792
|
+
const targets = ref === "rc" ? [{ workflow: "deploy.yml", event: "push", branch: "rc" }] : [
|
|
6793
|
+
{ workflow: "deploy.yml", event: "release" },
|
|
6794
|
+
{ workflow: "publish.yml", event: "release" }
|
|
6795
|
+
];
|
|
6796
|
+
const workflowRuns = [];
|
|
6797
|
+
for (const target of targets) {
|
|
6798
|
+
try {
|
|
6799
|
+
const run = await correlateWorkflowRun(deps, { ...target, since, headSha: autoRunHeadSha });
|
|
6800
|
+
workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run));
|
|
6801
|
+
} catch {
|
|
6802
|
+
workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
const primary = workflowRuns[0];
|
|
6421
6806
|
return {
|
|
6422
|
-
note
|
|
6423
|
-
|
|
6807
|
+
note,
|
|
6808
|
+
runId: primary?.runId,
|
|
6809
|
+
runUrl: primary?.runUrl,
|
|
6810
|
+
workflowRuns,
|
|
6811
|
+
deployStatus: aggregateWorkflowRuns(workflowRuns)
|
|
6424
6812
|
};
|
|
6425
6813
|
}
|
|
6426
6814
|
return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
|
|
@@ -6455,18 +6843,85 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6455
6843
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
6456
6844
|
);
|
|
6457
6845
|
const deployModel2 = await preflight(deps, ctx, "rc");
|
|
6458
|
-
|
|
6459
|
-
|
|
6846
|
+
if (isHubControlRepo(ctx.repo) && deployModel2 === "hub-serverless") {
|
|
6847
|
+
throw new Error("MMI-Hub releases directly from development to main; run mmi-cli release --apply from development instead of rcand");
|
|
6848
|
+
}
|
|
6849
|
+
const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
6850
|
+
await verifyHubDistributionVersion(deps, deployModel2, releaseBase);
|
|
6460
6851
|
await deps.run("git", ["checkout", "rc"]);
|
|
6461
6852
|
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
6462
6853
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
6463
6854
|
const rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
6855
|
+
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
6856
|
+
const tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
6857
|
+
const resumeNote = resume.tag ? resume.note : void 0;
|
|
6464
6858
|
await ensureTagPushed(deps, tag2, rcSha);
|
|
6465
6859
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
6466
6860
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks2);
|
|
6861
|
+
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
6467
6862
|
await deps.run("git", ["push", "origin", "rc"]);
|
|
6468
|
-
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch);
|
|
6469
|
-
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
|
|
6863
|
+
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince2, rcSha);
|
|
6864
|
+
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
|
|
6865
|
+
}
|
|
6866
|
+
if (isHubControlRepo(ctx.repo)) {
|
|
6867
|
+
await requireBranch(deps, "development");
|
|
6868
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6869
|
+
ensurePositiveCount(
|
|
6870
|
+
await deps.run("git", ["rev-list", "--count", "origin/main..origin/development"]),
|
|
6871
|
+
"nothing to release: origin/development is not ahead of origin/main"
|
|
6872
|
+
);
|
|
6873
|
+
const deployModel2 = await preflight(deps, ctx, "main");
|
|
6874
|
+
if (deployModel2 !== "hub-serverless") {
|
|
6875
|
+
throw new Error(`MMI-Hub direct release requires deployModel=hub-serverless, got ${deployModel2}`);
|
|
6876
|
+
}
|
|
6877
|
+
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
6878
|
+
await verifyHubDistributionVersion(deps, deployModel2, tag2);
|
|
6879
|
+
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
6880
|
+
const predictedNonSpine2 = predicted2.filter((f) => !isSpinePath(f));
|
|
6881
|
+
if (predictedNonSpine2.length > 0) {
|
|
6882
|
+
throw new Error(
|
|
6883
|
+
`development -> main merge would conflict on non-spine path(s): ${predictedNonSpine2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
|
|
6884
|
+
);
|
|
6885
|
+
}
|
|
6886
|
+
await deps.run("git", ["checkout", "main"]);
|
|
6887
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
6888
|
+
if (predicted2.length === 0) {
|
|
6889
|
+
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
6890
|
+
} else {
|
|
6891
|
+
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs");
|
|
6892
|
+
}
|
|
6893
|
+
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
6894
|
+
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
6895
|
+
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
6896
|
+
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
6897
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
6898
|
+
const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
6899
|
+
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
6900
|
+
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
6901
|
+
const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2);
|
|
6902
|
+
await deps.run("git", ["checkout", "development"]);
|
|
6903
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6904
|
+
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
6905
|
+
await deps.run("git", ["push", "origin", "development"]);
|
|
6906
|
+
return {
|
|
6907
|
+
...ctx,
|
|
6908
|
+
command,
|
|
6909
|
+
stage: "main",
|
|
6910
|
+
ref: "main",
|
|
6911
|
+
tag: tag2,
|
|
6912
|
+
deployModel: deployModel2,
|
|
6913
|
+
promoted: true,
|
|
6914
|
+
checks: checks2,
|
|
6915
|
+
dispatch: d2.note,
|
|
6916
|
+
runId: d2.runId,
|
|
6917
|
+
runUrl: d2.runUrl,
|
|
6918
|
+
workflowRuns: d2.workflowRuns,
|
|
6919
|
+
deployStatus: d2.deployStatus,
|
|
6920
|
+
rcRetirement: "not-applicable",
|
|
6921
|
+
rcRetirementNote: "Hub releases skip rc; no rc runtime to retire",
|
|
6922
|
+
announceNote: announceNote2,
|
|
6923
|
+
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
6924
|
+
};
|
|
6470
6925
|
}
|
|
6471
6926
|
await requireBranch(deps, "rc");
|
|
6472
6927
|
ensurePositiveCount(
|
|
@@ -6496,13 +6951,17 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6496
6951
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
6497
6952
|
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
6498
6953
|
await deps.run("git", ["push", "origin", "main"]);
|
|
6499
|
-
|
|
6500
|
-
const
|
|
6954
|
+
const autoRunSince = (deps.now ?? Date.now)();
|
|
6955
|
+
const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
6956
|
+
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
6957
|
+
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
6958
|
+
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha);
|
|
6501
6959
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
6502
6960
|
await deps.run("git", ["checkout", "development"]);
|
|
6503
6961
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6504
6962
|
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
6505
6963
|
await deps.run("git", ["push", "origin", "development"]);
|
|
6964
|
+
const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
|
|
6506
6965
|
return {
|
|
6507
6966
|
...ctx,
|
|
6508
6967
|
command,
|
|
@@ -6515,11 +6974,47 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6515
6974
|
dispatch: d.note,
|
|
6516
6975
|
runId: d.runId,
|
|
6517
6976
|
runUrl: d.runUrl,
|
|
6977
|
+
workflowRuns: d.workflowRuns,
|
|
6518
6978
|
deployStatus: d.deployStatus,
|
|
6519
6979
|
rcRetirement: retirement.status,
|
|
6520
|
-
rcRetirementNote: retirement.note
|
|
6980
|
+
rcRetirementNote: retirement.note,
|
|
6981
|
+
rcRetirementCategory: retirement.category,
|
|
6982
|
+
announceNote,
|
|
6983
|
+
release: { tag, url: releaseUrl, targetSha: releaseSha },
|
|
6984
|
+
environments
|
|
6521
6985
|
};
|
|
6522
6986
|
}
|
|
6987
|
+
async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
|
|
6988
|
+
if (model !== "tenant-container") return void 0;
|
|
6989
|
+
const domains = deps.fetchEdgeDomains ? await deps.fetchEdgeDomains(ctx.slug).catch(() => null) : null;
|
|
6990
|
+
const mainDomains = domains?.main;
|
|
6991
|
+
const rcDomains = domains?.rc;
|
|
6992
|
+
const healthStatus = deployStatus === "success" ? "success" : deployStatus === "failure" ? "failure" : "pending";
|
|
6993
|
+
const main = { healthStatus };
|
|
6994
|
+
if (mainDomains?.length) {
|
|
6995
|
+
main.domains = mainDomains;
|
|
6996
|
+
main.healthUrl = `https://${mainDomains[0]}/`;
|
|
6997
|
+
}
|
|
6998
|
+
const rc = { retirement: retirement.status };
|
|
6999
|
+
if (retirement.category) rc.retirementCategory = retirement.category;
|
|
7000
|
+
if (rcDomains?.length) rc.domains = rcDomains;
|
|
7001
|
+
return { main, rc };
|
|
7002
|
+
}
|
|
7003
|
+
var RETIRE_CATEGORIES = /* @__PURE__ */ new Set([
|
|
7004
|
+
"retired",
|
|
7005
|
+
"retired-edge-pending",
|
|
7006
|
+
"ssm-command-failed",
|
|
7007
|
+
"wait-timeout",
|
|
7008
|
+
"transport-failed"
|
|
7009
|
+
]);
|
|
7010
|
+
function retireCategoryFrom(text) {
|
|
7011
|
+
try {
|
|
7012
|
+
const c = JSON.parse(text).category;
|
|
7013
|
+
return typeof c === "string" && RETIRE_CATEGORIES.has(c) ? c : void 0;
|
|
7014
|
+
} catch {
|
|
7015
|
+
return void 0;
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
6523
7018
|
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
6524
7019
|
if (model !== "tenant-container") {
|
|
6525
7020
|
return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
|
|
@@ -6544,16 +7039,28 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
6544
7039
|
}
|
|
6545
7040
|
const out = await deps.runSelf(["tenant", "control", ctx.repo, "rc", "retire"]);
|
|
6546
7041
|
let commandId = "";
|
|
7042
|
+
let category = retireCategoryFrom(out);
|
|
6547
7043
|
try {
|
|
6548
7044
|
commandId = String(JSON.parse(out).commandId ?? "");
|
|
6549
7045
|
} catch {
|
|
6550
7046
|
}
|
|
7047
|
+
if (category === "retired-edge-pending") {
|
|
7048
|
+
return {
|
|
7049
|
+
status: "retired",
|
|
7050
|
+
category,
|
|
7051
|
+
note: `rc runtime retired; edge vhost reconcile pending (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept`
|
|
7052
|
+
};
|
|
7053
|
+
}
|
|
7054
|
+
category = category ?? "retired";
|
|
6551
7055
|
return {
|
|
6552
7056
|
status: "retired",
|
|
7057
|
+
category,
|
|
6553
7058
|
note: `rc runtime retired (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
6554
7059
|
};
|
|
6555
7060
|
} catch (e) {
|
|
6556
|
-
|
|
7061
|
+
const err = e;
|
|
7062
|
+
const category = retireCategoryFrom(err.stdout ?? "") ?? "transport-failed";
|
|
7063
|
+
return { status: "failed", category, note: `rc retirement failed (the release itself succeeded): ${err.message}` };
|
|
6557
7064
|
}
|
|
6558
7065
|
}
|
|
6559
7066
|
async function runTenantRedeploy(deps, options) {
|
|
@@ -6579,14 +7086,42 @@ async function runTenantRedeploy(deps, options) {
|
|
|
6579
7086
|
throw new Error(`${repo} is ${deployModel}, not tenant-container \u2014 there is no central tenant-deploy run to retry (its deploy fires from its own workflow)`);
|
|
6580
7087
|
}
|
|
6581
7088
|
const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
|
|
6582
|
-
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, deployStatus: d.deployStatus };
|
|
7089
|
+
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
|
|
7090
|
+
}
|
|
7091
|
+
|
|
7092
|
+
// src/train-prep.ts
|
|
7093
|
+
function clean2(text) {
|
|
7094
|
+
return text.trim();
|
|
7095
|
+
}
|
|
7096
|
+
function normalizeVersion2(target) {
|
|
7097
|
+
const version = target.replace(/^v/, "");
|
|
7098
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
7099
|
+
throw new Error(`invalid train prep target ${target || "(empty)"}`);
|
|
7100
|
+
}
|
|
7101
|
+
return version;
|
|
7102
|
+
}
|
|
7103
|
+
function parseChangedFiles(out) {
|
|
7104
|
+
return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
7105
|
+
}
|
|
7106
|
+
async function runTrainPrep(deps, options = {}) {
|
|
7107
|
+
const target = clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"]));
|
|
7108
|
+
const version = normalizeVersion2(target);
|
|
7109
|
+
if (!options.apply) {
|
|
7110
|
+
return { target, version, applied: false, command: "mmi-cli train prep --apply", stagedFiles: [] };
|
|
7111
|
+
}
|
|
7112
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", target]);
|
|
7113
|
+
const stagedFiles = parseChangedFiles(await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"]));
|
|
7114
|
+
if (stagedFiles.length === 0) throw new Error("no locked distribution files returned by release-distribution changed-files");
|
|
7115
|
+
await deps.run("git", ["add", "--", ...stagedFiles]);
|
|
7116
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "verify", target, "--skip-npm-view"]);
|
|
7117
|
+
return { target, version, applied: true, command: "mmi-cli pr create --base development --head <current-branch>", stagedFiles };
|
|
6583
7118
|
}
|
|
6584
7119
|
|
|
6585
7120
|
// src/hotfix-apply.ts
|
|
6586
7121
|
var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
|
|
6587
7122
|
var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
6588
7123
|
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
6589
|
-
function
|
|
7124
|
+
function clean3(out) {
|
|
6590
7125
|
return out.trim();
|
|
6591
7126
|
}
|
|
6592
7127
|
function sleeper(deps) {
|
|
@@ -6640,7 +7175,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
6640
7175
|
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
6641
7176
|
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
6642
7177
|
}
|
|
6643
|
-
const sha =
|
|
7178
|
+
const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
6644
7179
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
6645
7180
|
return { sha, label: sha.slice(0, 7) };
|
|
6646
7181
|
}
|
|
@@ -6667,7 +7202,7 @@ async function runHotfixStart(deps, options) {
|
|
|
6667
7202
|
};
|
|
6668
7203
|
}
|
|
6669
7204
|
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
6670
|
-
const remoteBranch =
|
|
7205
|
+
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
6671
7206
|
if (remoteBranch) {
|
|
6672
7207
|
await deps.run("git", ["checkout", branch]);
|
|
6673
7208
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
@@ -6693,7 +7228,7 @@ async function runHotfixStart(deps, options) {
|
|
|
6693
7228
|
}
|
|
6694
7229
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
6695
7230
|
}
|
|
6696
|
-
const prUrl =
|
|
7231
|
+
const prUrl = clean3(await deps.run("gh", [
|
|
6697
7232
|
"pr",
|
|
6698
7233
|
"create",
|
|
6699
7234
|
"--repo",
|
|
@@ -6748,7 +7283,7 @@ async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
|
6748
7283
|
}
|
|
6749
7284
|
return { workflow, conclusion: "not-found" };
|
|
6750
7285
|
}
|
|
6751
|
-
async function runHotfixRelease(deps, versionInput) {
|
|
7286
|
+
async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
6752
7287
|
const ctx = await buildTrainApplyContext(deps);
|
|
6753
7288
|
const { tag, version } = normalizeHotfixVersion(versionInput);
|
|
6754
7289
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -6772,18 +7307,22 @@ async function runHotfixRelease(deps, versionInput) {
|
|
|
6772
7307
|
releaseExists = true;
|
|
6773
7308
|
} catch {
|
|
6774
7309
|
}
|
|
7310
|
+
let announceNote;
|
|
6775
7311
|
if (releaseExists) {
|
|
6776
7312
|
releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
|
|
6777
7313
|
} else {
|
|
6778
|
-
const tagCommit =
|
|
7314
|
+
const tagCommit = clean3(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
|
|
6779
7315
|
await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
|
|
6780
7316
|
releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
|
|
7317
|
+
if (deps.announce) {
|
|
7318
|
+
announceNote = (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note;
|
|
7319
|
+
}
|
|
6781
7320
|
}
|
|
6782
7321
|
const runs = [];
|
|
6783
7322
|
for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
|
|
6784
7323
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
6785
7324
|
}
|
|
6786
|
-
const previousRef =
|
|
7325
|
+
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
6787
7326
|
let verifyNote;
|
|
6788
7327
|
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
6789
7328
|
try {
|
|
@@ -6794,9 +7333,9 @@ async function runHotfixRelease(deps, versionInput) {
|
|
|
6794
7333
|
} finally {
|
|
6795
7334
|
if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]);
|
|
6796
7335
|
}
|
|
6797
|
-
return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote };
|
|
7336
|
+
return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote, announceNote };
|
|
6798
7337
|
}
|
|
6799
|
-
function
|
|
7338
|
+
function versionAtLeast2(actual, wanted) {
|
|
6800
7339
|
const pa = actual.split(".").map(Number);
|
|
6801
7340
|
const pw = wanted.split(".").map(Number);
|
|
6802
7341
|
if (pa.length < 3 || pa.some(Number.isNaN) || pw.length < 3 || pw.some(Number.isNaN)) return false;
|
|
@@ -6851,9 +7390,9 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
6851
7390
|
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
6852
7391
|
}
|
|
6853
7392
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
6854
|
-
const branchExists = Boolean(
|
|
7393
|
+
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
6855
7394
|
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
6856
|
-
const remoteTag =
|
|
7395
|
+
const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
6857
7396
|
const tagPushed = Boolean(remoteTag);
|
|
6858
7397
|
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
6859
7398
|
let releaseExists = false;
|
|
@@ -6887,7 +7426,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
6887
7426
|
}
|
|
6888
7427
|
}
|
|
6889
7428
|
}
|
|
6890
|
-
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(
|
|
7429
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
6891
7430
|
const devVersion = await deps.run("git", ["show", "origin/development:cli/package.json"]).then(
|
|
6892
7431
|
(out) => {
|
|
6893
7432
|
try {
|
|
@@ -6898,12 +7437,138 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
6898
7437
|
},
|
|
6899
7438
|
() => "unknown"
|
|
6900
7439
|
);
|
|
6901
|
-
const devDistribution = { version: devVersion, aligned:
|
|
7440
|
+
const devDistribution = { version: devVersion, aligned: versionAtLeast2(devVersion, version) };
|
|
6902
7441
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion, devDistribution };
|
|
6903
7442
|
}
|
|
6904
7443
|
|
|
7444
|
+
// src/release-announce.ts
|
|
7445
|
+
var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
|
|
7446
|
+
var SSM_REGION = "eu-central-1";
|
|
7447
|
+
var SSM_TOKEN_PARAM = "/mmi-future/shared/SLACK_BOT_TOKEN";
|
|
7448
|
+
var SSM_CHANNEL_PARAM = "/mmi-future/shared/SLACK_ALERTS_CHANNEL";
|
|
7449
|
+
var SLACK_TIMEOUT_MS = 1e4;
|
|
7450
|
+
var MAX_BULLETS = 6;
|
|
7451
|
+
var MAX_SUMMARY_LINES = 8;
|
|
7452
|
+
function escapeMrkdwn(text) {
|
|
7453
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7454
|
+
}
|
|
7455
|
+
function bulletToLine(raw) {
|
|
7456
|
+
const m = /^[*-]\s+(.*)$/.exec(raw.trim());
|
|
7457
|
+
if (!m) return null;
|
|
7458
|
+
let text = m[1].trim();
|
|
7459
|
+
const tail = /\s+by\s+@([\w[\]-]+)\s+in\s+(\S+)\s*$/.exec(text);
|
|
7460
|
+
let link = "";
|
|
7461
|
+
if (tail) {
|
|
7462
|
+
const author = tail[1].toLowerCase();
|
|
7463
|
+
if (author.includes("dependabot") || author.includes("[bot]") || author === "github-actions") return null;
|
|
7464
|
+
const url = tail[2];
|
|
7465
|
+
const pr2 = /\/pull\/(\d+)$/.exec(url);
|
|
7466
|
+
link = pr2 ? ` (<${url}|#${pr2[1]}>)` : "";
|
|
7467
|
+
text = text.slice(0, tail.index).trim();
|
|
7468
|
+
}
|
|
7469
|
+
if (!text) return null;
|
|
7470
|
+
return `\u2022 ${escapeMrkdwn(text)}${link}`;
|
|
7471
|
+
}
|
|
7472
|
+
function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
|
|
7473
|
+
const lines = [];
|
|
7474
|
+
let bullets = 0;
|
|
7475
|
+
let dropped = 0;
|
|
7476
|
+
let section = "";
|
|
7477
|
+
let pendingHeader = "";
|
|
7478
|
+
for (const raw of notesMd.split("\n")) {
|
|
7479
|
+
const header = /^(#{2,3})\s+(.*)$/.exec(raw.trim());
|
|
7480
|
+
if (header) {
|
|
7481
|
+
section = header[2].trim();
|
|
7482
|
+
if (/what's changed/i.test(section) || /new contributors/i.test(section)) {
|
|
7483
|
+
pendingHeader = "";
|
|
7484
|
+
continue;
|
|
7485
|
+
}
|
|
7486
|
+
pendingHeader = `*${escapeMrkdwn(section)}*`;
|
|
7487
|
+
continue;
|
|
7488
|
+
}
|
|
7489
|
+
if (/new contributors/i.test(section)) continue;
|
|
7490
|
+
const line = bulletToLine(raw);
|
|
7491
|
+
if (!line) continue;
|
|
7492
|
+
if (bullets >= maxBullets) {
|
|
7493
|
+
dropped += 1;
|
|
7494
|
+
continue;
|
|
7495
|
+
}
|
|
7496
|
+
if (pendingHeader) {
|
|
7497
|
+
lines.push(pendingHeader);
|
|
7498
|
+
pendingHeader = "";
|
|
7499
|
+
}
|
|
7500
|
+
lines.push(line);
|
|
7501
|
+
bullets += 1;
|
|
7502
|
+
}
|
|
7503
|
+
if (dropped > 0) lines.push(`\u2026and ${dropped} more`);
|
|
7504
|
+
return lines;
|
|
7505
|
+
}
|
|
7506
|
+
function formatAnnouncement(args) {
|
|
7507
|
+
const name = args.repo.split("/")[1] ?? args.repo;
|
|
7508
|
+
return [
|
|
7509
|
+
`:rocket: *${name} ${args.tag} released*`,
|
|
7510
|
+
"",
|
|
7511
|
+
...args.lines,
|
|
7512
|
+
"",
|
|
7513
|
+
`<${args.releaseUrl}|Release notes>`
|
|
7514
|
+
].join("\n");
|
|
7515
|
+
}
|
|
7516
|
+
function summaryFileLines(content) {
|
|
7517
|
+
return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) => escapeMrkdwn(l.replace(/^[•*-]\s*/, ""))).filter(Boolean).map((l) => `\u2022 ${l}`);
|
|
7518
|
+
}
|
|
7519
|
+
async function ssmParameter(deps, name, decrypt) {
|
|
7520
|
+
const args = [
|
|
7521
|
+
"ssm",
|
|
7522
|
+
"get-parameter",
|
|
7523
|
+
"--region",
|
|
7524
|
+
SSM_REGION,
|
|
7525
|
+
"--name",
|
|
7526
|
+
name,
|
|
7527
|
+
"--query",
|
|
7528
|
+
"Parameter.Value",
|
|
7529
|
+
"--output",
|
|
7530
|
+
"text",
|
|
7531
|
+
...decrypt ? ["--with-decryption"] : []
|
|
7532
|
+
];
|
|
7533
|
+
const value = (await deps.run("aws", args)).trim();
|
|
7534
|
+
if (!value || value === "None") throw new Error(`SSM parameter ${name} is empty`);
|
|
7535
|
+
return value;
|
|
7536
|
+
}
|
|
7537
|
+
async function announceRelease(deps, args) {
|
|
7538
|
+
if (args.repo !== ANNOUNCE_REPO) {
|
|
7539
|
+
return { status: "skipped", note: `announce is Hub-only \u2014 ${args.repo} skipped` };
|
|
7540
|
+
}
|
|
7541
|
+
try {
|
|
7542
|
+
const viewRaw = await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body,url"]);
|
|
7543
|
+
const view = JSON.parse(viewRaw);
|
|
7544
|
+
const releaseUrl = view.url ?? `https://github.com/${args.repo}/releases/tag/${args.tag}`;
|
|
7545
|
+
let lines = [];
|
|
7546
|
+
if (args.summaryFile) {
|
|
7547
|
+
if (!deps.readFile) throw new Error("summary file given but deps.readFile is missing");
|
|
7548
|
+
lines = summaryFileLines(await deps.readFile(args.summaryFile));
|
|
7549
|
+
}
|
|
7550
|
+
if (lines.length === 0) lines = curateReleaseNotes(view.body ?? "");
|
|
7551
|
+
if (lines.length === 0) lines = ["\u2022 maintenance release \u2014 see the notes for details"];
|
|
7552
|
+
const token = await ssmParameter(deps, SSM_TOKEN_PARAM, true);
|
|
7553
|
+
const channel = await ssmParameter(deps, SSM_CHANNEL_PARAM, false);
|
|
7554
|
+
const text = formatAnnouncement({ repo: args.repo, tag: args.tag, lines, releaseUrl });
|
|
7555
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
7556
|
+
const res = await fetchImpl("https://slack.com/api/chat.postMessage", {
|
|
7557
|
+
method: "POST",
|
|
7558
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json; charset=utf-8" },
|
|
7559
|
+
body: JSON.stringify({ channel, text, unfurl_links: false }),
|
|
7560
|
+
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
7561
|
+
});
|
|
7562
|
+
const json = await res.json().catch(() => ({}));
|
|
7563
|
+
if (!json.ok) throw new Error(`slack postMessage failed: ${json.error ?? `http ${res.status}`}`);
|
|
7564
|
+
return { status: "announced", note: `announced ${args.tag} to the alerts channel` };
|
|
7565
|
+
} catch (e) {
|
|
7566
|
+
return { status: "failed", note: `announce failed (release unaffected): ${e.message}` };
|
|
7567
|
+
}
|
|
7568
|
+
}
|
|
7569
|
+
|
|
6905
7570
|
// src/port-registry.ts
|
|
6906
|
-
var
|
|
7571
|
+
var import_node_fs5 = require("node:fs");
|
|
6907
7572
|
|
|
6908
7573
|
// ../infra/port-geometry.mjs
|
|
6909
7574
|
var PORT_BLOCK = 100;
|
|
@@ -6917,8 +7582,8 @@ function nextPortBlock(registry2) {
|
|
|
6917
7582
|
return [base, base + PORT_SPAN];
|
|
6918
7583
|
}
|
|
6919
7584
|
function loadPortRegistry(path2) {
|
|
6920
|
-
if (!(0,
|
|
6921
|
-
const raw = JSON.parse((0,
|
|
7585
|
+
if (!(0, import_node_fs5.existsSync)(path2)) return {};
|
|
7586
|
+
const raw = JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8"));
|
|
6922
7587
|
const out = {};
|
|
6923
7588
|
for (const [key, value] of Object.entries(raw)) {
|
|
6924
7589
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -6932,9 +7597,9 @@ function ensurePortRange(repo, path2) {
|
|
|
6932
7597
|
const existing = registry2[repo];
|
|
6933
7598
|
if (existing) return existing;
|
|
6934
7599
|
const range = nextPortBlock(registry2);
|
|
6935
|
-
const raw = (0,
|
|
7600
|
+
const raw = (0, import_node_fs5.existsSync)(path2) ? JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8")) : {};
|
|
6936
7601
|
raw[repo] = range;
|
|
6937
|
-
(0,
|
|
7602
|
+
(0, import_node_fs5.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
6938
7603
|
return range;
|
|
6939
7604
|
}
|
|
6940
7605
|
function portCursorSeed(registry2) {
|
|
@@ -7518,8 +8183,8 @@ function renderBootstrapVerifyReport(report) {
|
|
|
7518
8183
|
|
|
7519
8184
|
// src/hub-auth.ts
|
|
7520
8185
|
var import_node_crypto2 = require("node:crypto");
|
|
7521
|
-
var
|
|
7522
|
-
var
|
|
8186
|
+
var import_node_fs6 = require("node:fs");
|
|
8187
|
+
var import_node_path6 = require("node:path");
|
|
7523
8188
|
var import_node_os2 = require("node:os");
|
|
7524
8189
|
var REFRESH_WINDOW_MS = 10 * 60 * 1e3;
|
|
7525
8190
|
var EXCHANGE_TIMEOUT_MS = 8e3;
|
|
@@ -7533,15 +8198,15 @@ function tokenFingerprint(token) {
|
|
|
7533
8198
|
function defaultHubSessionCachePath(env = process.env) {
|
|
7534
8199
|
if (env.MMI_HUB_SESSION_CACHE) return env.MMI_HUB_SESSION_CACHE;
|
|
7535
8200
|
if (process.platform === "win32") {
|
|
7536
|
-
const base2 = env.LOCALAPPDATA || (0,
|
|
7537
|
-
return (0,
|
|
8201
|
+
const base2 = env.LOCALAPPDATA || (0, import_node_path6.join)((0, import_node_os2.homedir)(), "AppData", "Local");
|
|
8202
|
+
return (0, import_node_path6.join)(base2, "MMI Future", "mmi-cli", "hub-session.json");
|
|
7538
8203
|
}
|
|
7539
|
-
const base = env.XDG_STATE_HOME || (0,
|
|
7540
|
-
return (0,
|
|
8204
|
+
const base = env.XDG_STATE_HOME || (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".mmi");
|
|
8205
|
+
return (0, import_node_path6.join)(base, "mmi-cli", "hub-session.json");
|
|
7541
8206
|
}
|
|
7542
8207
|
function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
7543
8208
|
try {
|
|
7544
|
-
const session = JSON.parse((0,
|
|
8209
|
+
const session = JSON.parse((0, import_node_fs6.readFileSync)(path2, "utf8"));
|
|
7545
8210
|
if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return null;
|
|
7546
8211
|
if (session.githubTokenFingerprint !== githubTokenFingerprint) return null;
|
|
7547
8212
|
if (new Date(session.expiresAt).getTime() <= now.getTime() + REFRESH_WINDOW_MS) return null;
|
|
@@ -7551,16 +8216,16 @@ function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
|
7551
8216
|
}
|
|
7552
8217
|
}
|
|
7553
8218
|
function writeCache(path2, session) {
|
|
7554
|
-
(0,
|
|
8219
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
|
|
7555
8220
|
const tmp = `${path2}.${process.pid}.${Date.now()}.tmp`;
|
|
7556
|
-
(0,
|
|
8221
|
+
(0, import_node_fs6.writeFileSync)(tmp, JSON.stringify(session, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
7557
8222
|
try {
|
|
7558
|
-
(0,
|
|
8223
|
+
(0, import_node_fs6.chmodSync)(tmp, 384);
|
|
7559
8224
|
} catch {
|
|
7560
8225
|
}
|
|
7561
|
-
(0,
|
|
8226
|
+
(0, import_node_fs6.renameSync)(tmp, path2);
|
|
7562
8227
|
try {
|
|
7563
|
-
(0,
|
|
8228
|
+
(0, import_node_fs6.chmodSync)(path2, 384);
|
|
7564
8229
|
} catch {
|
|
7565
8230
|
}
|
|
7566
8231
|
}
|
|
@@ -7578,7 +8243,7 @@ async function hubAuthSession(deps) {
|
|
|
7578
8243
|
const res = await fetchWithRetry(
|
|
7579
8244
|
deps.fetch ?? fetch,
|
|
7580
8245
|
`${apiUrl}/auth/session`,
|
|
7581
|
-
{ method: "POST", headers: { Authorization: `Bearer ${ghToken}` } },
|
|
8246
|
+
{ method: "POST", headers: { ...clientVersionHeaders(), Authorization: `Bearer ${ghToken}` } },
|
|
7582
8247
|
{ attempts: EXCHANGE_ATTEMPTS, timeoutMs: EXCHANGE_TIMEOUT_MS }
|
|
7583
8248
|
);
|
|
7584
8249
|
if (!res.ok) return void 0;
|
|
@@ -7733,9 +8398,11 @@ var PROJECTS_ENVELOPE_KEY = "projects";
|
|
|
7733
8398
|
|
|
7734
8399
|
// src/registry-client.ts
|
|
7735
8400
|
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
8401
|
+
var WAITED_TENANT_CONTROL_TIMEOUT_MS = 13e3;
|
|
7736
8402
|
var RETRY_ATTEMPTS = 3;
|
|
7737
8403
|
function retriedFetch(deps, url, init) {
|
|
7738
|
-
|
|
8404
|
+
const headers = { ...clientVersionHeaders(), ...init.headers };
|
|
8405
|
+
return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
|
|
7739
8406
|
attempts: RETRY_ATTEMPTS,
|
|
7740
8407
|
timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
7741
8408
|
});
|
|
@@ -7749,6 +8416,7 @@ async function fetchTrainAuthority(repo, deps) {
|
|
|
7749
8416
|
method: "GET",
|
|
7750
8417
|
headers: { Authorization: `Bearer ${token}` }
|
|
7751
8418
|
});
|
|
8419
|
+
if (res.status === 426) return { ok: false, error: upgradeRequiredError(res, await res.json().catch(() => null)) };
|
|
7752
8420
|
if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
|
|
7753
8421
|
const body = await res.json();
|
|
7754
8422
|
if (typeof body?.train !== "boolean" || !body.role) return { ok: false, error: "malformed train-authority response" };
|
|
@@ -7789,6 +8457,7 @@ async function fetchProjectBySlugChecked(slug, deps) {
|
|
|
7789
8457
|
headers: { Authorization: `Bearer ${token}` }
|
|
7790
8458
|
});
|
|
7791
8459
|
if (res.status === 404) return { ok: true, project: null };
|
|
8460
|
+
if (res.status === 426) return { ok: false, error: upgradeRequiredError(res, await res.json().catch(() => null)) };
|
|
7792
8461
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
7793
8462
|
return { ok: true, project: await res.json() };
|
|
7794
8463
|
} catch (e) {
|
|
@@ -7831,14 +8500,17 @@ async function fetchOrgConfig(deps) {
|
|
|
7831
8500
|
return null;
|
|
7832
8501
|
}
|
|
7833
8502
|
}
|
|
7834
|
-
async function postJson(pathSuffix, payload, deps, method = "POST") {
|
|
8503
|
+
async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
|
|
7835
8504
|
if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
|
|
7836
8505
|
const token = await deps.token();
|
|
7837
8506
|
if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
|
|
8507
|
+
const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
8508
|
+
const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
|
|
8509
|
+
const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
|
|
7838
8510
|
try {
|
|
7839
|
-
const res = await
|
|
8511
|
+
const res = await send(`${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
|
|
7840
8512
|
method,
|
|
7841
|
-
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8513
|
+
headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
7842
8514
|
body: JSON.stringify(payload)
|
|
7843
8515
|
});
|
|
7844
8516
|
let body = null;
|
|
@@ -7861,10 +8533,15 @@ async function attestAppGaps(slug, repo, deps) {
|
|
|
7861
8533
|
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
7862
8534
|
}
|
|
7863
8535
|
async function tenantControl(payload, deps) {
|
|
7864
|
-
|
|
8536
|
+
const noRetry = payload.action === "retire";
|
|
8537
|
+
const timeoutMs = payload.wait ? WAITED_TENANT_CONTROL_TIMEOUT_MS : void 0;
|
|
8538
|
+
return postJson("/tenant-control", payload, deps, "POST", { noRetry, timeoutMs });
|
|
7865
8539
|
}
|
|
7866
8540
|
|
|
7867
8541
|
// src/project-readiness.ts
|
|
8542
|
+
function dnsErrorToResolution(code) {
|
|
8543
|
+
return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
|
|
8544
|
+
}
|
|
7868
8545
|
var STAGES = ["dev", "rc", "main"];
|
|
7869
8546
|
var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
7870
8547
|
function slugOfRepo(repoOrSlug) {
|
|
@@ -7882,8 +8559,17 @@ function projectRequiresGoogleOAuth(meta, model) {
|
|
|
7882
8559
|
if (projectType !== "web-app") return false;
|
|
7883
8560
|
return Boolean(meta.oauth && typeof meta.oauth === "object");
|
|
7884
8561
|
}
|
|
7885
|
-
function
|
|
7886
|
-
|
|
8562
|
+
function declaresNoPublicEdge(meta) {
|
|
8563
|
+
const ed = meta?.edgeDomains;
|
|
8564
|
+
return Boolean(ed && typeof ed === "object" && !Array.isArray(ed) && Object.keys(ed).length === 0);
|
|
8565
|
+
}
|
|
8566
|
+
function isNoEdgeTenantWorker(meta, model) {
|
|
8567
|
+
return model === "tenant-container" && declaresNoPublicEdge(meta);
|
|
8568
|
+
}
|
|
8569
|
+
function projectRequiresDeployCoords(model, stage2, meta) {
|
|
8570
|
+
if (model !== "tenant-container") return false;
|
|
8571
|
+
if (stage2 && isNoEdgeTenantWorker(meta, model)) return stage2 === "main";
|
|
8572
|
+
return true;
|
|
7887
8573
|
}
|
|
7888
8574
|
function projectRequiresDeployState(model, stage2) {
|
|
7889
8575
|
return model === "hub-serverless" && stage2 !== "dev";
|
|
@@ -7892,6 +8578,7 @@ function stageRequiredSecrets(stage2, meta) {
|
|
|
7892
8578
|
const contract = meta.requiredRuntimeSecrets;
|
|
7893
8579
|
const extra = !Array.isArray(contract) && Array.isArray(contract?.[stage2]) ? contract[stage2] ?? [] : [];
|
|
7894
8580
|
const model = resolveDeployModel(meta, meta.repos?.[0] ?? "");
|
|
8581
|
+
if (isNoEdgeTenantWorker(meta, model) && stage2 !== "main") return [];
|
|
7895
8582
|
const defaults = projectRequiresGoogleOAuth(meta, model) ? DEFAULT_RUNTIME_SECRET_NAMES : [];
|
|
7896
8583
|
return [.../* @__PURE__ */ new Set([...defaults, ...extra])];
|
|
7897
8584
|
}
|
|
@@ -7947,6 +8634,9 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
7947
8634
|
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
7948
8635
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
7949
8636
|
];
|
|
8637
|
+
if (isNoEdgeTenantWorker(meta, model)) {
|
|
8638
|
+
gaps.unshift("No public edge declared (`edgeDomains: {}`): worker/outbound-only tenant; skip Cloudflare edge auto-heal, OAuth defaults, and dev/rc remote deploy coords unless META explicitly adds them.");
|
|
8639
|
+
}
|
|
7950
8640
|
if (contractUndeclared) {
|
|
7951
8641
|
gaps.unshift(CONTRACT_UNDECLARED_LINE);
|
|
7952
8642
|
}
|
|
@@ -7956,6 +8646,31 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
7956
8646
|
if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
|
|
7957
8647
|
return gaps;
|
|
7958
8648
|
}
|
|
8649
|
+
function edgeDomainsByStage(meta) {
|
|
8650
|
+
const ed = meta?.edgeDomains;
|
|
8651
|
+
if (!ed || typeof ed !== "object" || Array.isArray(ed)) return {};
|
|
8652
|
+
const out = {};
|
|
8653
|
+
for (const stage2 of STAGES) {
|
|
8654
|
+
const v = ed[stage2];
|
|
8655
|
+
if (typeof v === "string" && v.trim()) out[stage2] = v.trim();
|
|
8656
|
+
}
|
|
8657
|
+
return out;
|
|
8658
|
+
}
|
|
8659
|
+
async function probeEdgeDomains(meta, resolveDns) {
|
|
8660
|
+
const byStage = edgeDomainsByStage(meta);
|
|
8661
|
+
const results = await Promise.all(
|
|
8662
|
+
Object.entries(byStage).map(async ([stage2, host]) => {
|
|
8663
|
+
let resolved;
|
|
8664
|
+
try {
|
|
8665
|
+
resolved = await resolveDns(host);
|
|
8666
|
+
} catch {
|
|
8667
|
+
resolved = void 0;
|
|
8668
|
+
}
|
|
8669
|
+
return resolved === false ? { stage: stage2, host } : void 0;
|
|
8670
|
+
})
|
|
8671
|
+
);
|
|
8672
|
+
return results.filter((r) => r !== void 0);
|
|
8673
|
+
}
|
|
7959
8674
|
function contractByStage(contract) {
|
|
7960
8675
|
return contract && !Array.isArray(contract) ? contract : {};
|
|
7961
8676
|
}
|
|
@@ -8086,7 +8801,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
8086
8801
|
secretsError = e?.message || "secrets list failed";
|
|
8087
8802
|
}
|
|
8088
8803
|
const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
|
|
8089
|
-
const required = projectRequiresDeployCoords(model);
|
|
8804
|
+
const required = projectRequiresDeployCoords(model, stage2, meta);
|
|
8090
8805
|
return [stage2, { required, ok: required ? await deps.hasDeployCoords(slug, stage2) : true }];
|
|
8091
8806
|
})));
|
|
8092
8807
|
const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
|
|
@@ -8101,6 +8816,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
8101
8816
|
}));
|
|
8102
8817
|
const metaMissing = ["class", "projectType", "deployModel", "vaultPath", "kbPointer"].filter((key) => meta[key] === void 0);
|
|
8103
8818
|
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets2).every((v) => v.missing.length === 0);
|
|
8819
|
+
const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
|
|
8104
8820
|
return {
|
|
8105
8821
|
ok,
|
|
8106
8822
|
repo,
|
|
@@ -8112,6 +8828,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
8112
8828
|
secretsError,
|
|
8113
8829
|
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
8114
8830
|
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
8831
|
+
...edgeDomainWarnings.length ? { edgeDomainWarnings } : {},
|
|
8115
8832
|
appAttested: appAttestationOf(meta) ?? void 0
|
|
8116
8833
|
};
|
|
8117
8834
|
}
|
|
@@ -8139,6 +8856,9 @@ function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
|
8139
8856
|
`- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
|
|
8140
8857
|
...stageLines,
|
|
8141
8858
|
...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
|
|
8859
|
+
...(report.edgeDomainWarnings ?? []).map(
|
|
8860
|
+
(w) => `- \u26A0 edge domain does not resolve in DNS (advisory): ${w.stage} \u2192 ${w.host}; verify the registry edgeDomains value against the live public host`
|
|
8861
|
+
),
|
|
8142
8862
|
"",
|
|
8143
8863
|
"### Auto-heal applied / available",
|
|
8144
8864
|
...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
|
|
@@ -8180,6 +8900,29 @@ function parseRuntimeSecretsVar(raw) {
|
|
|
8180
8900
|
}
|
|
8181
8901
|
return out;
|
|
8182
8902
|
}
|
|
8903
|
+
function parseEdgeDomainsVar(raw) {
|
|
8904
|
+
let parsed;
|
|
8905
|
+
try {
|
|
8906
|
+
parsed = JSON.parse(raw);
|
|
8907
|
+
} catch {
|
|
8908
|
+
throw new Error('project set: edgeDomains must be JSON, e.g. {"dev":"dev.example.co","rc":"rc.example.co","main":"example.co"}');
|
|
8909
|
+
}
|
|
8910
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8911
|
+
throw new Error("project set: edgeDomains must be a {dev,rc,main} map of domain strings");
|
|
8912
|
+
}
|
|
8913
|
+
const map = parsed;
|
|
8914
|
+
const out = {};
|
|
8915
|
+
for (const [stage2, domain] of Object.entries(map)) {
|
|
8916
|
+
if (!RUNTIME_SECRET_STAGES.includes(stage2)) {
|
|
8917
|
+
throw new Error(`project set: edgeDomains stage "${stage2}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
8918
|
+
}
|
|
8919
|
+
if (typeof domain !== "string" || !domain.trim()) {
|
|
8920
|
+
throw new Error(`project set: edgeDomains.${stage2} must be a non-empty domain string`);
|
|
8921
|
+
}
|
|
8922
|
+
out[stage2] = domain.trim();
|
|
8923
|
+
}
|
|
8924
|
+
return out;
|
|
8925
|
+
}
|
|
8183
8926
|
function buildProjectSetPatch(input) {
|
|
8184
8927
|
const patch = {};
|
|
8185
8928
|
if (input.class) {
|
|
@@ -8211,6 +8954,8 @@ function buildProjectSetPatch(input) {
|
|
|
8211
8954
|
patch[key] = n;
|
|
8212
8955
|
} else if (key === "requiredRuntimeSecrets") {
|
|
8213
8956
|
patch[key] = parseRuntimeSecretsVar(raw);
|
|
8957
|
+
} else if (key === "edgeDomains") {
|
|
8958
|
+
patch[key] = parseEdgeDomainsVar(raw);
|
|
8214
8959
|
} else {
|
|
8215
8960
|
patch[key] = raw;
|
|
8216
8961
|
}
|
|
@@ -8252,8 +8997,8 @@ function resolveKbSource(rawBase) {
|
|
|
8252
8997
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
8253
8998
|
}
|
|
8254
8999
|
function buildKbGetArgs(src, path2) {
|
|
8255
|
-
const
|
|
8256
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
9000
|
+
const clean4 = path2.replace(/^\/+/, "");
|
|
9001
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
8257
9002
|
}
|
|
8258
9003
|
function buildKbTreeArgs(src) {
|
|
8259
9004
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -8270,7 +9015,7 @@ function parseKbTree(stdout, prefix) {
|
|
|
8270
9015
|
}
|
|
8271
9016
|
|
|
8272
9017
|
// src/plan.ts
|
|
8273
|
-
var
|
|
9018
|
+
var import_node_path7 = require("node:path");
|
|
8274
9019
|
|
|
8275
9020
|
// src/frontmatter.ts
|
|
8276
9021
|
function splitFrontmatter(content) {
|
|
@@ -8353,8 +9098,8 @@ function rankPlansByRelevance(plans, signals, opts = {}) {
|
|
|
8353
9098
|
|
|
8354
9099
|
// src/plan.ts
|
|
8355
9100
|
var PLANS_DIR = "plans";
|
|
8356
|
-
var META_FILE = (0,
|
|
8357
|
-
var planPath = (slug) => (0,
|
|
9101
|
+
var META_FILE = (0, import_node_path7.join)(PLANS_DIR, ".plan-meta.json");
|
|
9102
|
+
var planPath = (slug) => (0, import_node_path7.join)(PLANS_DIR, `${slug}.md`);
|
|
8358
9103
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
8359
9104
|
function parseMeta(raw) {
|
|
8360
9105
|
if (!raw) return {};
|
|
@@ -8402,6 +9147,10 @@ ${next.join("\n")}
|
|
|
8402
9147
|
---
|
|
8403
9148
|
${body.replace(/^\n+/, "")}`;
|
|
8404
9149
|
}
|
|
9150
|
+
async function httpFailMessage(op, res) {
|
|
9151
|
+
if (res.status === 426) return upgradeRequiredError(res, await res.json().catch(() => null));
|
|
9152
|
+
return `plan ${op} failed: HTTP ${res.status}`;
|
|
9153
|
+
}
|
|
8405
9154
|
async function planPush(deps, slug, opts = {}) {
|
|
8406
9155
|
const raw = deps.readLocal(slug);
|
|
8407
9156
|
if (raw == null) {
|
|
@@ -8433,7 +9182,7 @@ async function planPush(deps, slug, opts = {}) {
|
|
|
8433
9182
|
deps.err(staleHint(slug));
|
|
8434
9183
|
return false;
|
|
8435
9184
|
} else {
|
|
8436
|
-
deps.err(
|
|
9185
|
+
deps.err(await httpFailMessage("push", res));
|
|
8437
9186
|
return false;
|
|
8438
9187
|
}
|
|
8439
9188
|
}
|
|
@@ -8457,7 +9206,7 @@ async function planPull(deps, slug, opts = {}) {
|
|
|
8457
9206
|
return false;
|
|
8458
9207
|
}
|
|
8459
9208
|
if (!res.ok) {
|
|
8460
|
-
deps.err(
|
|
9209
|
+
deps.err(await httpFailMessage("pull", res));
|
|
8461
9210
|
return false;
|
|
8462
9211
|
}
|
|
8463
9212
|
const doc = await res.json();
|
|
@@ -8475,6 +9224,7 @@ async function fetchPlanList(deps, project2) {
|
|
|
8475
9224
|
headers: await deps.headers(),
|
|
8476
9225
|
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
8477
9226
|
});
|
|
9227
|
+
if (res.status === 426) throw new Error(upgradeRequiredError(res, await res.json().catch(() => null)));
|
|
8478
9228
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
8479
9229
|
const { plans } = await res.json();
|
|
8480
9230
|
return plans ?? [];
|
|
@@ -8530,7 +9280,7 @@ async function planDelete(deps, slug, opts = {}) {
|
|
|
8530
9280
|
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
8531
9281
|
});
|
|
8532
9282
|
if (!res.ok) {
|
|
8533
|
-
deps.err(
|
|
9283
|
+
deps.err(await httpFailMessage("delete", res));
|
|
8534
9284
|
return;
|
|
8535
9285
|
}
|
|
8536
9286
|
deps.removeLocal(slug);
|
|
@@ -8617,6 +9367,52 @@ function formatVaultPointer(p) {
|
|
|
8617
9367
|
}
|
|
8618
9368
|
var TIMEOUT_MS2 = 8e3;
|
|
8619
9369
|
var repoOf = (slug) => `${OWNER2}/${slug}`;
|
|
9370
|
+
var RECALL_REGIONS = ["us-east-1", "us-west-2", "eu-central-1", "ap-northeast-1"];
|
|
9371
|
+
var PROVIDER_VERIFY_TIMEOUT_MS = 8e3;
|
|
9372
|
+
function secretLeafName(key) {
|
|
9373
|
+
return key.split("/").pop() ?? key;
|
|
9374
|
+
}
|
|
9375
|
+
function providerForSecretKey(key) {
|
|
9376
|
+
return secretLeafName(key) === "RECALL_API_KEY" ? "recall" : void 0;
|
|
9377
|
+
}
|
|
9378
|
+
function recallUsageUrl(region) {
|
|
9379
|
+
const end = /* @__PURE__ */ new Date();
|
|
9380
|
+
const start = new Date(end.getTime() - 60 * 60 * 1e3);
|
|
9381
|
+
const qs = new URLSearchParams({ start: start.toISOString(), end: end.toISOString() });
|
|
9382
|
+
return `https://${region}.recall.ai/api/v1/billing/usage/?${qs.toString()}`;
|
|
9383
|
+
}
|
|
9384
|
+
async function verifyRecallApiKey(deps, value) {
|
|
9385
|
+
const checked = [];
|
|
9386
|
+
for (const region of RECALL_REGIONS) {
|
|
9387
|
+
try {
|
|
9388
|
+
const res = await deps.fetch(recallUsageUrl(region), {
|
|
9389
|
+
method: "GET",
|
|
9390
|
+
headers: { Authorization: `Token ${value}` },
|
|
9391
|
+
signal: AbortSignal.timeout(PROVIDER_VERIFY_TIMEOUT_MS)
|
|
9392
|
+
});
|
|
9393
|
+
if (res.ok) {
|
|
9394
|
+
return { ok: true, provider: "recall", message: `validated RECALL_API_KEY with Recall (${region})`, checked };
|
|
9395
|
+
}
|
|
9396
|
+
checked.push(`${region}: HTTP ${res.status}`);
|
|
9397
|
+
} catch (e) {
|
|
9398
|
+
checked.push(`${region}: ${e.message}`);
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
9401
|
+
return {
|
|
9402
|
+
ok: false,
|
|
9403
|
+
provider: "recall",
|
|
9404
|
+
message: `Recall rejected the key in all configured regions (${checked.join("; ")})`,
|
|
9405
|
+
checked
|
|
9406
|
+
};
|
|
9407
|
+
}
|
|
9408
|
+
async function verifySecretValue(deps, key, value, _opts) {
|
|
9409
|
+
const provider = providerForSecretKey(key);
|
|
9410
|
+
if (!provider) {
|
|
9411
|
+
return { ok: false, message: `no provider verifier configured for ${secretLeafName(key)}`, checked: [] };
|
|
9412
|
+
}
|
|
9413
|
+
if (provider === "recall") return verifyRecallApiKey(deps, value);
|
|
9414
|
+
return { ok: false, provider, message: `no provider verifier configured for ${secretLeafName(key)}`, checked: [] };
|
|
9415
|
+
}
|
|
8620
9416
|
async function vaultSlug(deps, opts) {
|
|
8621
9417
|
return (opts.repo ? opts.repo.split("/").pop() : await deps.slug()).toLowerCase();
|
|
8622
9418
|
}
|
|
@@ -8646,6 +9442,10 @@ function errorDetail(body) {
|
|
|
8646
9442
|
const error = typeof body.error === "string" ? body.error : "";
|
|
8647
9443
|
return error ? `: ${error}` : "";
|
|
8648
9444
|
}
|
|
9445
|
+
async function upgradeMessage(res, body) {
|
|
9446
|
+
if (res.status !== 426) return null;
|
|
9447
|
+
return upgradeRequiredError(res, body ?? await readJsonBody(res));
|
|
9448
|
+
}
|
|
8649
9449
|
async function fetchSecretValue(deps, key, opts) {
|
|
8650
9450
|
if (!isValidSecretKey(key)) return null;
|
|
8651
9451
|
const repo = await targetRepo(deps, opts);
|
|
@@ -8678,7 +9478,7 @@ async function secretsList(deps, opts) {
|
|
|
8678
9478
|
return;
|
|
8679
9479
|
}
|
|
8680
9480
|
if (!res.ok) {
|
|
8681
|
-
deps.err(`secrets list failed: HTTP ${res.status}${await readErr(res)}`);
|
|
9481
|
+
deps.err(await upgradeMessage(res) ?? `secrets list failed: HTTP ${res.status}${await readErr(res)}`);
|
|
8682
9482
|
return;
|
|
8683
9483
|
}
|
|
8684
9484
|
const { secrets: secrets2 } = await res.json();
|
|
@@ -8711,7 +9511,7 @@ async function secretsPreflight(deps, opts) {
|
|
|
8711
9511
|
return false;
|
|
8712
9512
|
}
|
|
8713
9513
|
if (!res.ok) {
|
|
8714
|
-
deps.err(`secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
|
|
9514
|
+
deps.err(await upgradeMessage(res) ?? `secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
|
|
8715
9515
|
return false;
|
|
8716
9516
|
}
|
|
8717
9517
|
const { secrets: secrets2 } = await res.json();
|
|
@@ -8746,7 +9546,7 @@ async function secretsGet(deps, key, opts) {
|
|
|
8746
9546
|
return false;
|
|
8747
9547
|
}
|
|
8748
9548
|
deps.err(
|
|
8749
|
-
res.status === 403 ? `secrets get: not authorized for ${key} (HTTP 403)${errorDetail(body)}` : `secrets get failed: HTTP ${res.status}${errorDetail(body)}`
|
|
9549
|
+
await upgradeMessage(res, body) ?? (res.status === 403 ? `secrets get: not authorized for ${key} (HTTP 403)${errorDetail(body)}` : `secrets get failed: HTTP ${res.status}${errorDetail(body)}`)
|
|
8750
9550
|
);
|
|
8751
9551
|
return false;
|
|
8752
9552
|
}
|
|
@@ -8754,6 +9554,28 @@ async function secretsGet(deps, key, opts) {
|
|
|
8754
9554
|
deps.log(value ?? "");
|
|
8755
9555
|
return true;
|
|
8756
9556
|
}
|
|
9557
|
+
async function secretsVerify(deps, key, opts) {
|
|
9558
|
+
if (!isValidSecretKey(key)) {
|
|
9559
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
9560
|
+
return false;
|
|
9561
|
+
}
|
|
9562
|
+
if (!providerForSecretKey(key)) {
|
|
9563
|
+
deps.err(`no provider verifier configured for ${secretLeafName(key)}`);
|
|
9564
|
+
return false;
|
|
9565
|
+
}
|
|
9566
|
+
const value = await fetchSecretValue(deps, key, opts);
|
|
9567
|
+
if (!value) {
|
|
9568
|
+
deps.err(`secrets verify: could not read ${key}; no value was printed`);
|
|
9569
|
+
return false;
|
|
9570
|
+
}
|
|
9571
|
+
const result = await verifySecretValue(deps, key, value, opts);
|
|
9572
|
+
if (result.ok) {
|
|
9573
|
+
deps.log(result.message);
|
|
9574
|
+
return true;
|
|
9575
|
+
}
|
|
9576
|
+
deps.err(result.message);
|
|
9577
|
+
return false;
|
|
9578
|
+
}
|
|
8757
9579
|
async function secretsRequest(deps, key, opts) {
|
|
8758
9580
|
if (!isValidSecretKey(key)) {
|
|
8759
9581
|
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
@@ -8774,7 +9596,7 @@ async function secretsRequest(deps, key, opts) {
|
|
|
8774
9596
|
});
|
|
8775
9597
|
const body = await readJsonBody(res);
|
|
8776
9598
|
if (!res.ok) {
|
|
8777
|
-
deps.err(`secrets request failed: HTTP ${res.status}${errorDetail(body)}`);
|
|
9599
|
+
deps.err(await upgradeMessage(res, body) ?? `secrets request failed: HTTP ${res.status}${errorDetail(body)}`);
|
|
8778
9600
|
return false;
|
|
8779
9601
|
}
|
|
8780
9602
|
if (opts.json) {
|
|
@@ -8804,21 +9626,34 @@ async function putSecret(deps, key, value, opts) {
|
|
|
8804
9626
|
});
|
|
8805
9627
|
if (!res.ok) {
|
|
8806
9628
|
deps.err(
|
|
8807
|
-
res.status === 403 ? `secrets set: not authorized to write ${key} (HTTP 403)${await readErr(res)}` : `secrets set failed: HTTP ${res.status}${await readErr(res)}`
|
|
9629
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets set: not authorized to write ${key} (HTTP 403)${await readErr(res)}` : `secrets set failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8808
9630
|
);
|
|
8809
9631
|
return false;
|
|
8810
9632
|
}
|
|
9633
|
+
const provider = providerForSecretKey(key);
|
|
9634
|
+
if (provider) {
|
|
9635
|
+
const result = await verifySecretValue(deps, key, value, opts);
|
|
9636
|
+
if (!result.ok) {
|
|
9637
|
+
deps.err(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier), but ${result.message}`);
|
|
9638
|
+
return false;
|
|
9639
|
+
}
|
|
9640
|
+
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier); ${result.message}`);
|
|
9641
|
+
return true;
|
|
9642
|
+
}
|
|
8811
9643
|
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
8812
9644
|
return true;
|
|
8813
9645
|
}
|
|
8814
9646
|
async function secretsSet(deps, key, opts) {
|
|
8815
|
-
if (!isValidSecretKey(key))
|
|
9647
|
+
if (!isValidSecretKey(key)) {
|
|
9648
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
9649
|
+
return false;
|
|
9650
|
+
}
|
|
8816
9651
|
const value = await deps.readSecretValue(`value for ${key} (input hidden; will not be echoed): `);
|
|
8817
9652
|
if (!value) {
|
|
8818
9653
|
deps.err("secrets set: empty value \u2014 aborted (nothing written)");
|
|
8819
|
-
return;
|
|
9654
|
+
return false;
|
|
8820
9655
|
}
|
|
8821
|
-
|
|
9656
|
+
return putSecret(deps, key, value, opts);
|
|
8822
9657
|
}
|
|
8823
9658
|
async function secretsEdit(deps, key, opts) {
|
|
8824
9659
|
return secretsSet(deps, key, opts);
|
|
@@ -8834,7 +9669,7 @@ async function secretsRemove(deps, key, opts) {
|
|
|
8834
9669
|
});
|
|
8835
9670
|
if (!res.ok) {
|
|
8836
9671
|
deps.err(
|
|
8837
|
-
res.status === 403 ? `secrets rm: not authorized to remove ${key} (HTTP 403)${await readErr(res)}` : `secrets rm failed: HTTP ${res.status}${await readErr(res)}`
|
|
9672
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets rm: not authorized to remove ${key} (HTTP 403)${await readErr(res)}` : `secrets rm failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8838
9673
|
);
|
|
8839
9674
|
return;
|
|
8840
9675
|
}
|
|
@@ -8849,7 +9684,7 @@ async function secretsGrant(deps, repo, login, key, _opts) {
|
|
|
8849
9684
|
});
|
|
8850
9685
|
if (!res.ok) {
|
|
8851
9686
|
deps.err(
|
|
8852
|
-
res.status === 403 ? `secrets grant: master-admin only (HTTP 403)${await readErr(res)}` : `secrets grant failed: HTTP ${res.status}${await readErr(res)}`
|
|
9687
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets grant: master-admin only (HTTP 403)${await readErr(res)}` : `secrets grant failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8853
9688
|
);
|
|
8854
9689
|
return;
|
|
8855
9690
|
}
|
|
@@ -8864,7 +9699,7 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
|
|
|
8864
9699
|
});
|
|
8865
9700
|
if (!res.ok) {
|
|
8866
9701
|
deps.err(
|
|
8867
|
-
res.status === 403 ? `secrets revoke: master-admin only (HTTP 403)${await readErr(res)}` : `secrets revoke failed: HTTP ${res.status}${await readErr(res)}`
|
|
9702
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets revoke: master-admin only (HTTP 403)${await readErr(res)}` : `secrets revoke failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8868
9703
|
);
|
|
8869
9704
|
return;
|
|
8870
9705
|
}
|
|
@@ -9001,7 +9836,8 @@ async function awsCallerArn() {
|
|
|
9001
9836
|
async function hubHeaders(extra = {}) {
|
|
9002
9837
|
const cfg = await loadConfig();
|
|
9003
9838
|
const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
|
|
9004
|
-
|
|
9839
|
+
const base = { ...clientVersionHeaders(), ...extra };
|
|
9840
|
+
return t ? { ...base, Authorization: `Bearer ${t}` } : base;
|
|
9005
9841
|
}
|
|
9006
9842
|
async function loadConfig() {
|
|
9007
9843
|
let file = {};
|
|
@@ -9064,7 +9900,7 @@ function sessionDeps() {
|
|
|
9064
9900
|
env: process.env,
|
|
9065
9901
|
readPersisted: () => {
|
|
9066
9902
|
try {
|
|
9067
|
-
return (0,
|
|
9903
|
+
return (0, import_node_fs7.readFileSync)(SESSION_FILE, "utf8");
|
|
9068
9904
|
} catch {
|
|
9069
9905
|
return null;
|
|
9070
9906
|
}
|
|
@@ -9077,8 +9913,8 @@ function sessionDeps() {
|
|
|
9077
9913
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
9078
9914
|
function persistSession(id) {
|
|
9079
9915
|
try {
|
|
9080
|
-
(0,
|
|
9081
|
-
(0,
|
|
9916
|
+
(0, import_node_fs7.mkdirSync)(".mmi", { recursive: true });
|
|
9917
|
+
(0, import_node_fs7.writeFileSync)(SESSION_FILE, id, "utf8");
|
|
9082
9918
|
} catch {
|
|
9083
9919
|
}
|
|
9084
9920
|
}
|
|
@@ -9197,22 +10033,20 @@ async function applyGcPlan(plan2, remote) {
|
|
|
9197
10033
|
}
|
|
9198
10034
|
return result;
|
|
9199
10035
|
}
|
|
9200
|
-
function
|
|
10036
|
+
async function fetchHubVersionInfo(baseUrl) {
|
|
10037
|
+
if (!baseUrl) return null;
|
|
9201
10038
|
try {
|
|
9202
|
-
const
|
|
9203
|
-
|
|
10039
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/version`, { signal: AbortSignal.timeout(4e3) });
|
|
10040
|
+
if (!res.ok) return null;
|
|
10041
|
+
const body = await res.json();
|
|
10042
|
+
return body && typeof body === "object" ? body : null;
|
|
9204
10043
|
} catch {
|
|
9205
|
-
|
|
9206
|
-
const pkg = (0, import_node_path7.join)(__dirname, "..", "package.json");
|
|
9207
|
-
return JSON.parse((0, import_node_fs6.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
9208
|
-
} catch {
|
|
9209
|
-
return "0.0.0";
|
|
9210
|
-
}
|
|
10044
|
+
return null;
|
|
9211
10045
|
}
|
|
9212
10046
|
}
|
|
9213
10047
|
function readRepoVersion() {
|
|
9214
10048
|
try {
|
|
9215
|
-
return JSON.parse((0,
|
|
10049
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path8.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
9216
10050
|
} catch {
|
|
9217
10051
|
return void 0;
|
|
9218
10052
|
}
|
|
@@ -9251,7 +10085,7 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
9251
10085
|
}
|
|
9252
10086
|
async function requireFreshTrainCli(commandName) {
|
|
9253
10087
|
const report = buildVersionLagReport({
|
|
9254
|
-
currentVersion:
|
|
10088
|
+
currentVersion: resolveClientVersion(),
|
|
9255
10089
|
repoVersion: readRepoVersion(),
|
|
9256
10090
|
releasedVersion: await fetchReleasedVersion()
|
|
9257
10091
|
});
|
|
@@ -9281,7 +10115,7 @@ async function applyClaudePluginHeal(surface, log) {
|
|
|
9281
10115
|
return true;
|
|
9282
10116
|
}
|
|
9283
10117
|
var program2 = new Command();
|
|
9284
|
-
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(
|
|
10118
|
+
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
|
|
9285
10119
|
async function runRulesSync(opts, io = consoleIo) {
|
|
9286
10120
|
const cfg = await loadConfig();
|
|
9287
10121
|
if (isRulesSource(cfg.orgRulesSource)) {
|
|
@@ -9291,7 +10125,13 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
9291
10125
|
const base = resolveRulesBase(cfg.orgRulesSource, DEFAULT_RULES_SOURCE);
|
|
9292
10126
|
const token = await githubToken();
|
|
9293
10127
|
let changed = 0;
|
|
9294
|
-
const files = [
|
|
10128
|
+
const files = [
|
|
10129
|
+
"AGENTS.md",
|
|
10130
|
+
"CLAUDE.md",
|
|
10131
|
+
".claude/settings.json",
|
|
10132
|
+
".claude/output-styles/mmi-plain.md",
|
|
10133
|
+
".cursor/rules/mmi-plain-language.mdc"
|
|
10134
|
+
];
|
|
9295
10135
|
const fetched = await Promise.all(files.map(async (file) => {
|
|
9296
10136
|
try {
|
|
9297
10137
|
const url = `${base}/${file}`;
|
|
@@ -9309,10 +10149,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
9309
10149
|
for (const entry of fetched) {
|
|
9310
10150
|
if ("error" in entry) continue;
|
|
9311
10151
|
const { file, source } = entry;
|
|
9312
|
-
const current = (0,
|
|
10152
|
+
const current = (0, import_node_fs7.existsSync)(file) ? await (0, import_promises2.readFile)(file, "utf8") : null;
|
|
9313
10153
|
if (needsUpdate(source, current)) {
|
|
9314
10154
|
const slash = file.lastIndexOf("/");
|
|
9315
|
-
if (slash > 0) (0,
|
|
10155
|
+
if (slash > 0) (0, import_node_fs7.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
9316
10156
|
await (0, import_promises2.writeFile)(file, normalizeEol(source), "utf8");
|
|
9317
10157
|
changed++;
|
|
9318
10158
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -9322,7 +10162,7 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
9322
10162
|
return failures.length === 0;
|
|
9323
10163
|
}
|
|
9324
10164
|
var rules = program2.command("rules").description("org rules delivery");
|
|
9325
|
-
rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch AGENTS.md / CLAUDE.md / .claude/settings.json from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
|
|
10165
|
+
rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch the org-delivered files (AGENTS.md / CLAUDE.md / .claude/settings.json / output style / Cursor rule) from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
|
|
9326
10166
|
if (!await runRulesSync(opts)) process.exitCode = 1;
|
|
9327
10167
|
});
|
|
9328
10168
|
async function runDocsSync(opts, io = consoleIo) {
|
|
@@ -9338,7 +10178,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
9338
10178
|
return null;
|
|
9339
10179
|
}
|
|
9340
10180
|
},
|
|
9341
|
-
localContent: async (f) => (0,
|
|
10181
|
+
localContent: async (f) => (0, import_node_fs7.existsSync)(f) ? await (0, import_promises2.readFile)(f, "utf8") : null,
|
|
9342
10182
|
writeDoc: async (f, c) => {
|
|
9343
10183
|
await (0, import_promises2.writeFile)(f, c, "utf8");
|
|
9344
10184
|
}
|
|
@@ -9486,6 +10326,18 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
9486
10326
|
io.log(`saga health: ${report.ok ? "OK" : "NOT OK"}`);
|
|
9487
10327
|
if (report.problems.length) io.log(report.problems.map((p) => ` - ${p}`).join("\n"));
|
|
9488
10328
|
}
|
|
10329
|
+
async function runWhoami(io = consoleIo) {
|
|
10330
|
+
const cfg = await loadConfig();
|
|
10331
|
+
const report = await resolveWhoami({
|
|
10332
|
+
hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
10333
|
+
ghLogin: githubLogin
|
|
10334
|
+
});
|
|
10335
|
+
io.log(JSON.stringify(report));
|
|
10336
|
+
return report;
|
|
10337
|
+
}
|
|
10338
|
+
program2.command("whoami").description('resolve the logged-in human: {login, source, sessionExpiresAt} JSON; source "unknown" (exit 0) when neither the Hub session nor gh can name them').action(async () => {
|
|
10339
|
+
await runWhoami();
|
|
10340
|
+
});
|
|
9489
10341
|
saga.command("health").option("--json", "machine-readable output").option("--banner", "one-line SessionStart banner; silent when healthy").option("--quiet", "suppress detail output").description("zero-write health check: auth, backend reachability, resolved key").action((o) => runSagaHealth(o));
|
|
9490
10342
|
program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
9491
10343
|
if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
|
|
@@ -9614,7 +10466,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
9614
10466
|
}
|
|
9615
10467
|
}
|
|
9616
10468
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
9617
|
-
const ensureDir = () => (0,
|
|
10469
|
+
const ensureDir = () => (0, import_node_fs7.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
9618
10470
|
return {
|
|
9619
10471
|
apiUrl: cfg.sagaApiUrl,
|
|
9620
10472
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
@@ -9622,31 +10474,31 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
9622
10474
|
project: async () => (await sagaKey(cfg)).project,
|
|
9623
10475
|
readLocal: (slug) => {
|
|
9624
10476
|
try {
|
|
9625
|
-
return (0,
|
|
10477
|
+
return (0, import_node_fs7.readFileSync)(planPath(slug), "utf8");
|
|
9626
10478
|
} catch {
|
|
9627
10479
|
return null;
|
|
9628
10480
|
}
|
|
9629
10481
|
},
|
|
9630
10482
|
writeLocal: (slug, content) => {
|
|
9631
10483
|
ensureDir();
|
|
9632
|
-
(0,
|
|
10484
|
+
(0, import_node_fs7.writeFileSync)(planPath(slug), content, "utf8");
|
|
9633
10485
|
},
|
|
9634
10486
|
removeLocal: (slug) => {
|
|
9635
10487
|
try {
|
|
9636
|
-
(0,
|
|
10488
|
+
(0, import_node_fs7.rmSync)(planPath(slug));
|
|
9637
10489
|
} catch {
|
|
9638
10490
|
}
|
|
9639
10491
|
},
|
|
9640
10492
|
readMetaRaw: () => {
|
|
9641
10493
|
try {
|
|
9642
|
-
return (0,
|
|
10494
|
+
return (0, import_node_fs7.readFileSync)(META_FILE, "utf8");
|
|
9643
10495
|
} catch {
|
|
9644
10496
|
return null;
|
|
9645
10497
|
}
|
|
9646
10498
|
},
|
|
9647
10499
|
writeMetaRaw: (raw) => {
|
|
9648
10500
|
ensureDir();
|
|
9649
|
-
(0,
|
|
10501
|
+
(0, import_node_fs7.writeFileSync)(META_FILE, raw, "utf8");
|
|
9650
10502
|
},
|
|
9651
10503
|
log: (m) => io.log(m),
|
|
9652
10504
|
err: (m) => io.err(m),
|
|
@@ -9787,8 +10639,18 @@ secrets.command("request <key>").description("approved escalation: create a Hub
|
|
|
9787
10639
|
const ok = await secretsRequest(d, key, o);
|
|
9788
10640
|
if (!ok) process.exitCode = 1;
|
|
9789
10641
|
}));
|
|
9790
|
-
secrets.command("
|
|
9791
|
-
|
|
10642
|
+
secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
10643
|
+
const ok = await secretsVerify(d, key, o);
|
|
10644
|
+
if (!ok) process.exitCode = 1;
|
|
10645
|
+
}));
|
|
10646
|
+
secrets.command("set <key>").description("write/rotate a secret; value is read from stdin (never an argument)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
10647
|
+
const ok = await secretsSet(d, key, o);
|
|
10648
|
+
if (!ok) process.exitCode = 1;
|
|
10649
|
+
}));
|
|
10650
|
+
secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
10651
|
+
const ok = await secretsEdit(d, key, o);
|
|
10652
|
+
if (!ok) process.exitCode = 1;
|
|
10653
|
+
}));
|
|
9792
10654
|
secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
9793
10655
|
secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
|
|
9794
10656
|
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
@@ -9811,7 +10673,13 @@ function reportWrite(label, res) {
|
|
|
9811
10673
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
9812
10674
|
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, plus rc-only retire) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action) => {
|
|
9813
10675
|
const cfg = await loadConfig();
|
|
9814
|
-
const
|
|
10676
|
+
const wait = action === "status" || action === "retire";
|
|
10677
|
+
const res = await tenantControl({ repo, stage: stage2, action, wait }, registryClientDeps(cfg));
|
|
10678
|
+
const body = res.body;
|
|
10679
|
+
if (!res.ok && body?.category) {
|
|
10680
|
+
console.log(JSON.stringify(body));
|
|
10681
|
+
return fail(`tenant control ${stage2} ${action}: ${body.category}`);
|
|
10682
|
+
}
|
|
9815
10683
|
reportWrite("tenant control", res);
|
|
9816
10684
|
});
|
|
9817
10685
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
|
|
@@ -9823,9 +10691,18 @@ tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the cen
|
|
|
9823
10691
|
return fail(`tenant redeploy: ${e.message}`);
|
|
9824
10692
|
}
|
|
9825
10693
|
});
|
|
10694
|
+
async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
10695
|
+
const { lookup } = await import("node:dns/promises");
|
|
10696
|
+
const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
|
|
10697
|
+
const timeout = new Promise((resolve) => {
|
|
10698
|
+
setTimeout(() => resolve(void 0), timeoutMs).unref?.();
|
|
10699
|
+
});
|
|
10700
|
+
return Promise.race([probe, timeout]);
|
|
10701
|
+
}
|
|
9826
10702
|
async function v2ReadinessDeps(cfg) {
|
|
9827
10703
|
const reg = registryClientDeps(cfg);
|
|
9828
10704
|
return {
|
|
10705
|
+
resolveDns: (host) => resolveDnsBounded(host),
|
|
9829
10706
|
// Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
|
|
9830
10707
|
getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
|
|
9831
10708
|
hasDeployCoords: async (slug, stage2) => {
|
|
@@ -10255,19 +11132,15 @@ pr.command("create").description("create a PR and print {number,url} JSON").requ
|
|
|
10255
11132
|
const created = await ghCreate(buildPrArgs({ title: o.title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
10256
11133
|
console.log(JSON.stringify(created));
|
|
10257
11134
|
});
|
|
10258
|
-
async function remoteBranchExists(branch) {
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
return stdout.trim().length > 0;
|
|
10263
|
-
} catch {
|
|
10264
|
-
return void 0;
|
|
10265
|
-
}
|
|
11135
|
+
async function remoteBranchExists(branch, options = {}) {
|
|
11136
|
+
return checkRemoteBranchExists(branch, {
|
|
11137
|
+
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
11138
|
+
}, options);
|
|
10266
11139
|
}
|
|
10267
11140
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
10268
11141
|
function teardownWorktreeStage(worktreePath) {
|
|
10269
11142
|
return runWorktreeStageTeardown(worktreePath, {
|
|
10270
|
-
hasStageState: (wt) => (0,
|
|
11143
|
+
hasStageState: (wt) => (0, import_node_fs7.existsSync)(stageStatePath(wt)),
|
|
10271
11144
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
10272
11145
|
listComposeProjects: async () => {
|
|
10273
11146
|
const { stdout } = await execFileP4("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -10300,11 +11173,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
10300
11173
|
if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
|
|
10301
11174
|
});
|
|
10302
11175
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
10303
|
-
const
|
|
10304
|
-
|
|
11176
|
+
const remoteBranch = repoArgs.length ? buildRemoteBranchCleanupReport(headRef, {
|
|
11177
|
+
attempted: false,
|
|
11178
|
+
reason: remoteNotAttemptedReason
|
|
11179
|
+
}) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
11180
|
+
exists: remoteBranchExists
|
|
11181
|
+
}, {
|
|
10305
11182
|
attempted: remoteDeleteAttempted,
|
|
10306
11183
|
existedBefore: remoteBefore,
|
|
10307
|
-
existsAfter: remoteAfter,
|
|
10308
11184
|
reason: remoteNotAttemptedReason
|
|
10309
11185
|
});
|
|
10310
11186
|
const localCleanup = repoArgs.length ? {
|
|
@@ -10317,14 +11193,13 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
10317
11193
|
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
10318
11194
|
teardownWorktreeStage
|
|
10319
11195
|
});
|
|
10320
|
-
console.log(JSON.stringify({
|
|
10321
|
-
|
|
11196
|
+
console.log(JSON.stringify(buildPrMergeResultPayload({
|
|
11197
|
+
number,
|
|
10322
11198
|
branch: headRef,
|
|
10323
11199
|
method: method.slice(2),
|
|
10324
11200
|
remoteBranch,
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
}));
|
|
11201
|
+
localCleanup
|
|
11202
|
+
})));
|
|
10328
11203
|
});
|
|
10329
11204
|
async function runBoardRead(o) {
|
|
10330
11205
|
try {
|
|
@@ -10454,7 +11329,7 @@ function rawValues(flag) {
|
|
|
10454
11329
|
return out;
|
|
10455
11330
|
}
|
|
10456
11331
|
function printLine(value) {
|
|
10457
|
-
(0,
|
|
11332
|
+
(0, import_node_fs7.writeSync)(1, `${value}
|
|
10458
11333
|
`);
|
|
10459
11334
|
}
|
|
10460
11335
|
function stageKeepAlive() {
|
|
@@ -10471,8 +11346,8 @@ async function resolveStage() {
|
|
|
10471
11346
|
local,
|
|
10472
11347
|
shell: shellFor(),
|
|
10473
11348
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
10474
|
-
hasCompose: (0,
|
|
10475
|
-
hasEnvExample: (0,
|
|
11349
|
+
hasCompose: (0, import_node_fs7.existsSync)((0, import_node_path8.join)(process.cwd(), "docker-compose.yml")),
|
|
11350
|
+
hasEnvExample: (0, import_node_fs7.existsSync)((0, import_node_path8.join)(process.cwd(), ".env.example"))
|
|
10476
11351
|
});
|
|
10477
11352
|
}
|
|
10478
11353
|
function stageStepsFor(res, stops = true) {
|
|
@@ -10495,9 +11370,9 @@ function reportedStageUrl(res, result) {
|
|
|
10495
11370
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
10496
11371
|
}
|
|
10497
11372
|
program2.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (committed-file fallback)").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
10498
|
-
const path2 = (0,
|
|
11373
|
+
const path2 = (0, import_node_path8.join)(process.cwd(), "infra", "port-ranges.json");
|
|
10499
11374
|
const allocate = async (seed) => {
|
|
10500
|
-
const { stdout } = await execFileP4("node", [(0,
|
|
11375
|
+
const { stdout } = await execFileP4("node", [(0, import_node_path8.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
10501
11376
|
const parsed = JSON.parse(stdout);
|
|
10502
11377
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
10503
11378
|
return parsed.range;
|
|
@@ -10626,26 +11501,82 @@ function trainApplyDeps() {
|
|
|
10626
11501
|
trainAuthority: async (repo) => {
|
|
10627
11502
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
10628
11503
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
11504
|
+
},
|
|
11505
|
+
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
11506
|
+
announce: (args) => announceRelease({
|
|
11507
|
+
run: async (file, cmdArgs) => (await execFileP4(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
11508
|
+
readFile: (path2) => (0, import_promises2.readFile)(path2, "utf8")
|
|
11509
|
+
}, args),
|
|
11510
|
+
fetchEdgeDomains: async (slug) => {
|
|
11511
|
+
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
11512
|
+
const ed = proj?.edgeDomains;
|
|
11513
|
+
if (!ed) return null;
|
|
11514
|
+
const toArr = (v) => v ? [v] : void 0;
|
|
11515
|
+
return { rc: toArr(ed.rc), main: toArr(ed.main) };
|
|
10629
11516
|
}
|
|
10630
11517
|
};
|
|
10631
11518
|
}
|
|
11519
|
+
function trainPrepDeps() {
|
|
11520
|
+
return {
|
|
11521
|
+
run: async (file, args) => {
|
|
11522
|
+
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : GIT_TIMEOUT_MS;
|
|
11523
|
+
return (await execFileP4(file, args, { timeout })).stdout;
|
|
11524
|
+
}
|
|
11525
|
+
};
|
|
11526
|
+
}
|
|
11527
|
+
function formatWorkflowRun(r) {
|
|
11528
|
+
const ref = r.runUrl ?? (r.runId != null ? String(r.runId) : "unresolved");
|
|
11529
|
+
return `${r.workflow} ${ref} ${r.conclusion.toUpperCase()}`;
|
|
11530
|
+
}
|
|
10632
11531
|
function renderDeployLine(d) {
|
|
10633
11532
|
const parts = [d.dispatch];
|
|
10634
|
-
if (d.
|
|
11533
|
+
if (d.workflowRuns?.length) parts.push(`runs: ${d.workflowRuns.map(formatWorkflowRun).join(", ")}`);
|
|
11534
|
+
else if (d.runUrl) parts.push(`run ${d.runUrl}`);
|
|
10635
11535
|
if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
|
|
10636
11536
|
else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
|
|
10637
11537
|
else if (d.runId != null) parts.push(`deploy: dispatched (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
10638
11538
|
return parts.join("; ");
|
|
10639
11539
|
}
|
|
10640
11540
|
function renderTrainApply(commandName, r) {
|
|
10641
|
-
|
|
10642
|
-
|
|
11541
|
+
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
11542
|
+
if (r.resumeNote) base = `${base}; ${r.resumeNote}`;
|
|
11543
|
+
if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
|
|
11544
|
+
return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
|
|
10643
11545
|
}
|
|
10644
11546
|
function renderTenantRedeploy(r) {
|
|
10645
11547
|
return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
10646
11548
|
}
|
|
11549
|
+
async function resolveRcandPlanTargets() {
|
|
11550
|
+
try {
|
|
11551
|
+
const tags = (await execFileP4("git", ["tag", "--list", "v*", "--sort=-v:refname"], { timeout: 1e4 })).stdout.trim().split("\n").filter(Boolean);
|
|
11552
|
+
return trainPlanTargetsFromTags(tags, process.env.MMI_RELEASE_VERSION);
|
|
11553
|
+
} catch (e) {
|
|
11554
|
+
return { plannedReleaseError: e.message, existingRcReleaseError: e.message };
|
|
11555
|
+
}
|
|
11556
|
+
}
|
|
11557
|
+
function renderTrainPrep(r) {
|
|
11558
|
+
if (!r.applied) {
|
|
11559
|
+
return `mmi-cli train prep: target ${r.target}; run ${r.command} to prepare and stage locked distribution files`;
|
|
11560
|
+
}
|
|
11561
|
+
return [
|
|
11562
|
+
`mmi-cli train prep: prepared ${r.target}`,
|
|
11563
|
+
` - staged locked files: ${r.stagedFiles.join(", ")}`,
|
|
11564
|
+
" - stopped before promotion; no push, merge, tag, release, or deploy was run",
|
|
11565
|
+
` - next: ${r.command}`
|
|
11566
|
+
].join("\n");
|
|
11567
|
+
}
|
|
11568
|
+
var trainCmd = program2.command("train").description("release train helpers that stop before promotion");
|
|
11569
|
+
trainCmd.command("prep").description("prepare and stage the Hub distribution bump for the next release train").option("--json", "machine-readable output").option("--apply", "run release-distribution prepare, exact-file stage, verify, then stop").action(async (o) => {
|
|
11570
|
+
try {
|
|
11571
|
+
await requireFreshTrainCli("train prep");
|
|
11572
|
+
const result = await runTrainPrep(trainPrepDeps(), { apply: o.apply });
|
|
11573
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainPrep(result));
|
|
11574
|
+
} catch (e) {
|
|
11575
|
+
fail(`train prep: ${e.message}`);
|
|
11576
|
+
}
|
|
11577
|
+
});
|
|
10647
11578
|
for (const commandName of ["rcand", "release"]) {
|
|
10648
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the
|
|
11579
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").action(async (o) => {
|
|
10649
11580
|
try {
|
|
10650
11581
|
await requireFreshTrainCli(commandName);
|
|
10651
11582
|
} catch (e) {
|
|
@@ -10653,14 +11584,18 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
10653
11584
|
}
|
|
10654
11585
|
if (o.apply) {
|
|
10655
11586
|
try {
|
|
10656
|
-
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch });
|
|
11587
|
+
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
|
|
10657
11588
|
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
|
|
10658
11589
|
} catch (e) {
|
|
10659
11590
|
return fail(`${commandName}: ${e.message}`);
|
|
10660
11591
|
}
|
|
10661
11592
|
}
|
|
10662
|
-
const
|
|
10663
|
-
|
|
11593
|
+
const repo = await resolveRepo();
|
|
11594
|
+
const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
|
|
11595
|
+
const steps = trainPlan(commandName, { ...targets ?? {}, repo });
|
|
11596
|
+
console.log(
|
|
11597
|
+
o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps)
|
|
11598
|
+
);
|
|
10664
11599
|
});
|
|
10665
11600
|
}
|
|
10666
11601
|
function renderHotfixStart(r) {
|
|
@@ -10674,6 +11609,7 @@ function renderHotfixRelease(r) {
|
|
|
10674
11609
|
` - ${r.releaseNote}`,
|
|
10675
11610
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
10676
11611
|
` - ${r.verifyNote}`,
|
|
11612
|
+
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
10677
11613
|
` - next: mmi-cli hotfix status ${r.tag} (no back-merge \u2014 development already has the fix; align its distribution manifests by PR if status says behind)`
|
|
10678
11614
|
].join("\n");
|
|
10679
11615
|
}
|
|
@@ -10706,7 +11642,7 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
|
|
|
10706
11642
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli hotfix: dry-run plan", steps));
|
|
10707
11643
|
});
|
|
10708
11644
|
hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
|
|
10709
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version), o.json, renderHotfixRelease));
|
|
11645
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
|
|
10710
11646
|
hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
|
|
10711
11647
|
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
|
|
10712
11648
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
@@ -10726,7 +11662,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
10726
11662
|
const report = await verifyBootstrap(repo, o.class, {
|
|
10727
11663
|
client: defaultGitHubClient(),
|
|
10728
11664
|
projectMeta: meta,
|
|
10729
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
11665
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs7.existsSync)(path2) ? (0, import_node_fs7.readFileSync)(path2, "utf8") : null,
|
|
10730
11666
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
10731
11667
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
10732
11668
|
requiredGcpApis: (() => {
|
|
@@ -10767,12 +11703,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
10767
11703
|
return fail(`bootstrap apply: ${e.message}`);
|
|
10768
11704
|
}
|
|
10769
11705
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
10770
|
-
if (!(0,
|
|
10771
|
-
const manifest = loadBootstrapSeeds((0,
|
|
11706
|
+
if (!(0, import_node_fs7.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
11707
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs7.readFileSync)(manifestPath, "utf8"));
|
|
10772
11708
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
10773
11709
|
const slug = parsedRepo.slug;
|
|
10774
11710
|
const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
|
|
10775
|
-
const readFile2 = (p) => (0,
|
|
11711
|
+
const readFile2 = (p) => (0, import_node_fs7.existsSync)(p) ? (0, import_node_fs7.readFileSync)(p, "utf8") : null;
|
|
10776
11712
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
10777
11713
|
const vars = {};
|
|
10778
11714
|
for (const value of rawValues("--var")) {
|
|
@@ -10896,16 +11832,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
10896
11832
|
if (o.class !== "deployable" && o.class !== "content") return fail("access audit: --class must be deployable or content");
|
|
10897
11833
|
targets = [{ repo: o.repo, class: o.class }];
|
|
10898
11834
|
} else {
|
|
10899
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
11835
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs7.existsSync)("projects.json") ? (0, import_node_fs7.readFileSync)("projects.json", "utf8") : null;
|
|
10900
11836
|
if (!projectsJson) return fail("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
|
|
10901
|
-
const fanoutJson = (0,
|
|
11837
|
+
const fanoutJson = (0, import_node_fs7.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs7.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
10902
11838
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
10903
11839
|
}
|
|
10904
11840
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
10905
|
-
const fileMatrix = (0,
|
|
11841
|
+
const fileMatrix = (0, import_node_fs7.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs7.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
10906
11842
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
10907
11843
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
10908
|
-
const fileContracts = (0,
|
|
11844
|
+
const fileContracts = (0, import_node_fs7.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs7.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
10909
11845
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
10910
11846
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
10911
11847
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -10914,20 +11850,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
10914
11850
|
var isWin = process.platform === "win32";
|
|
10915
11851
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
10916
11852
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
10917
|
-
return (0,
|
|
11853
|
+
return (0, import_node_path8.join)((0, import_node_os3.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
10918
11854
|
};
|
|
10919
11855
|
function readInstalledPlugins() {
|
|
10920
11856
|
try {
|
|
10921
|
-
return JSON.parse((0,
|
|
11857
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(installedPluginsPath(), "utf8"));
|
|
10922
11858
|
} catch {
|
|
10923
11859
|
return null;
|
|
10924
11860
|
}
|
|
10925
11861
|
}
|
|
10926
11862
|
function installedPluginSources() {
|
|
10927
11863
|
return ["claude", "codex"].map((surface) => {
|
|
10928
|
-
const recordPath = (0,
|
|
11864
|
+
const recordPath = (0, import_node_path8.join)((0, import_node_os3.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
10929
11865
|
try {
|
|
10930
|
-
return { surface, installed: JSON.parse((0,
|
|
11866
|
+
return { surface, installed: JSON.parse((0, import_node_fs7.readFileSync)(recordPath, "utf8")), recordPath };
|
|
10931
11867
|
} catch {
|
|
10932
11868
|
return { surface, installed: null, recordPath };
|
|
10933
11869
|
}
|
|
@@ -10935,7 +11871,7 @@ function installedPluginSources() {
|
|
|
10935
11871
|
}
|
|
10936
11872
|
function readClaudeSettings() {
|
|
10937
11873
|
try {
|
|
10938
|
-
return JSON.parse((0,
|
|
11874
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path8.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
10939
11875
|
} catch {
|
|
10940
11876
|
return null;
|
|
10941
11877
|
}
|
|
@@ -10957,7 +11893,7 @@ function writeProjectInstallRecord(record) {
|
|
|
10957
11893
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
10958
11894
|
list.push(record);
|
|
10959
11895
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
10960
|
-
(0,
|
|
11896
|
+
(0, import_node_fs7.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
10961
11897
|
`, "utf8");
|
|
10962
11898
|
return true;
|
|
10963
11899
|
} catch {
|
|
@@ -10970,9 +11906,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
10970
11906
|
if (!file) return false;
|
|
10971
11907
|
if (!file.plugins) file.plugins = {};
|
|
10972
11908
|
const path2 = installedPluginsPath();
|
|
10973
|
-
(0,
|
|
11909
|
+
(0, import_node_fs7.copyFileSync)(path2, `${path2}.bak`);
|
|
10974
11910
|
file.plugins[pluginId] = records;
|
|
10975
|
-
(0,
|
|
11911
|
+
(0, import_node_fs7.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
10976
11912
|
`, "utf8");
|
|
10977
11913
|
return true;
|
|
10978
11914
|
} catch {
|
|
@@ -10980,26 +11916,26 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
10980
11916
|
}
|
|
10981
11917
|
}
|
|
10982
11918
|
function cursorPluginCacheRoot() {
|
|
10983
|
-
return (0,
|
|
11919
|
+
return (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
|
|
10984
11920
|
}
|
|
10985
11921
|
function cursorPluginCachePinSnapshots() {
|
|
10986
11922
|
const root = cursorPluginCacheRoot();
|
|
10987
11923
|
try {
|
|
10988
|
-
return (0,
|
|
10989
|
-
const path2 = (0,
|
|
10990
|
-
const pluginJson = (0,
|
|
10991
|
-
const hooksJson = (0,
|
|
11924
|
+
return (0, import_node_fs7.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
11925
|
+
const path2 = (0, import_node_path8.join)(root, entry.name);
|
|
11926
|
+
const pluginJson = (0, import_node_path8.join)(path2, ".cursor-plugin", "plugin.json");
|
|
11927
|
+
const hooksJson = (0, import_node_path8.join)(path2, "hooks", "hooks.json");
|
|
10992
11928
|
let isEmpty = true;
|
|
10993
11929
|
try {
|
|
10994
|
-
isEmpty = (0,
|
|
11930
|
+
isEmpty = (0, import_node_fs7.readdirSync)(path2).length === 0;
|
|
10995
11931
|
} catch {
|
|
10996
11932
|
isEmpty = true;
|
|
10997
11933
|
}
|
|
10998
11934
|
return {
|
|
10999
11935
|
name: entry.name,
|
|
11000
11936
|
path: path2,
|
|
11001
|
-
hasPluginJson: (0,
|
|
11002
|
-
hasHooksJson: (0,
|
|
11937
|
+
hasPluginJson: (0, import_node_fs7.existsSync)(pluginJson),
|
|
11938
|
+
hasHooksJson: (0, import_node_fs7.existsSync)(hooksJson),
|
|
11003
11939
|
isEmpty
|
|
11004
11940
|
};
|
|
11005
11941
|
});
|
|
@@ -11008,19 +11944,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
11008
11944
|
}
|
|
11009
11945
|
}
|
|
11010
11946
|
function hubCheckoutForCursorSeed() {
|
|
11011
|
-
const manifest = (0,
|
|
11012
|
-
return (0,
|
|
11947
|
+
const manifest = (0, import_node_path8.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
11948
|
+
return (0, import_node_fs7.existsSync)(manifest) ? process.cwd() : void 0;
|
|
11013
11949
|
}
|
|
11014
11950
|
function mmiPluginCacheRootSnapshots() {
|
|
11015
11951
|
const roots = [
|
|
11016
|
-
{ surface: "claude", root: (0,
|
|
11017
|
-
{ surface: "codex", root: (0,
|
|
11952
|
+
{ surface: "claude", root: (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
11953
|
+
{ surface: "codex", root: (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
11018
11954
|
];
|
|
11019
11955
|
return roots.flatMap(({ surface, root }) => {
|
|
11020
11956
|
try {
|
|
11021
|
-
const entries = (0,
|
|
11957
|
+
const entries = (0, import_node_fs7.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
11022
11958
|
name: entry.name,
|
|
11023
|
-
path: (0,
|
|
11959
|
+
path: (0, import_node_path8.join)(root, entry.name),
|
|
11024
11960
|
isDirectory: entry.isDirectory()
|
|
11025
11961
|
}));
|
|
11026
11962
|
return [{ surface, root, entries }];
|
|
@@ -11030,10 +11966,10 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
11030
11966
|
});
|
|
11031
11967
|
}
|
|
11032
11968
|
function uniqueQuarantineTarget(path2) {
|
|
11033
|
-
if (!(0,
|
|
11969
|
+
if (!(0, import_node_fs7.existsSync)(path2)) return path2;
|
|
11034
11970
|
for (let i = 1; i < 100; i += 1) {
|
|
11035
11971
|
const candidate = `${path2}-${i}`;
|
|
11036
|
-
if (!(0,
|
|
11972
|
+
if (!(0, import_node_fs7.existsSync)(candidate)) return candidate;
|
|
11037
11973
|
}
|
|
11038
11974
|
return `${path2}-${Date.now()}`;
|
|
11039
11975
|
}
|
|
@@ -11041,27 +11977,27 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
11041
11977
|
let moved = 0;
|
|
11042
11978
|
for (const move of plan2) {
|
|
11043
11979
|
try {
|
|
11044
|
-
if (!(0,
|
|
11980
|
+
if (!(0, import_node_fs7.existsSync)(move.from)) continue;
|
|
11045
11981
|
const target = uniqueQuarantineTarget(move.to);
|
|
11046
|
-
(0,
|
|
11047
|
-
(0,
|
|
11982
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.dirname)(target), { recursive: true });
|
|
11983
|
+
(0, import_node_fs7.renameSync)(move.from, target);
|
|
11048
11984
|
moved += 1;
|
|
11049
11985
|
} catch {
|
|
11050
11986
|
}
|
|
11051
11987
|
}
|
|
11052
11988
|
return moved;
|
|
11053
11989
|
}
|
|
11054
|
-
var gitignorePath = () => (0,
|
|
11990
|
+
var gitignorePath = () => (0, import_node_path8.join)(process.cwd(), ".gitignore");
|
|
11055
11991
|
function readGitignore() {
|
|
11056
11992
|
try {
|
|
11057
|
-
return (0,
|
|
11993
|
+
return (0, import_node_fs7.readFileSync)(gitignorePath(), "utf8");
|
|
11058
11994
|
} catch {
|
|
11059
11995
|
return null;
|
|
11060
11996
|
}
|
|
11061
11997
|
}
|
|
11062
11998
|
function writeGitignore(content) {
|
|
11063
11999
|
try {
|
|
11064
|
-
(0,
|
|
12000
|
+
(0, import_node_fs7.writeFileSync)(gitignorePath(), content, "utf8");
|
|
11065
12001
|
return true;
|
|
11066
12002
|
} catch {
|
|
11067
12003
|
return false;
|
|
@@ -11075,6 +12011,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11075
12011
|
}
|
|
11076
12012
|
const repairLocal = !opts.json || Boolean(opts.apply);
|
|
11077
12013
|
const repairFull = !opts.json && !opts.banner || Boolean(opts.apply);
|
|
12014
|
+
const repoWritesAllowed = !opts.noRepoWrites;
|
|
11078
12015
|
const checks = [];
|
|
11079
12016
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
11080
12017
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
@@ -11099,13 +12036,13 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11099
12036
|
let onPath = pathProbe;
|
|
11100
12037
|
if (!onPath) {
|
|
11101
12038
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
11102
|
-
if (root && (0,
|
|
12039
|
+
if (root && (0, import_node_fs7.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
11103
12040
|
}
|
|
11104
12041
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
11105
12042
|
const surface = detectSurface(process.env);
|
|
11106
12043
|
const reloadHint = reloadAction(surface);
|
|
11107
12044
|
let versionReport = buildVersionLagReport({
|
|
11108
|
-
currentVersion:
|
|
12045
|
+
currentVersion: resolveClientVersion(),
|
|
11109
12046
|
repoVersion: readRepoVersion(),
|
|
11110
12047
|
releasedVersion
|
|
11111
12048
|
});
|
|
@@ -11113,6 +12050,13 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11113
12050
|
if (!versionReport.ok) versionReport = { ...versionReport, fix: pluginRecoveryFix(surface) };
|
|
11114
12051
|
checks.push(versionReport);
|
|
11115
12052
|
checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
|
|
12053
|
+
checks.push(
|
|
12054
|
+
buildHubCompatCheck({
|
|
12055
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
12056
|
+
versionInfo: await fetchHubVersionInfo(cfg.sagaApiUrl),
|
|
12057
|
+
installedVersion: resolveClientVersion()
|
|
12058
|
+
})
|
|
12059
|
+
);
|
|
11116
12060
|
checks.push(buildAwsCrossAccountCheck({ callerArn }));
|
|
11117
12061
|
let cloneOk = cloneProbe;
|
|
11118
12062
|
if (!cloneOk && repairFull) {
|
|
@@ -11141,13 +12085,18 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11141
12085
|
}
|
|
11142
12086
|
checks.push(pluginCheck);
|
|
11143
12087
|
let gitignoreCheck = buildGitignoreManagedBlockCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), content: readGitignore() });
|
|
11144
|
-
|
|
12088
|
+
const gitignoreDecision = decideGitignoreRepair(gitignoreCheck, { repoWritesAllowed, repairFull });
|
|
12089
|
+
gitignoreCheck = gitignoreDecision.check;
|
|
12090
|
+
if (gitignoreDecision.action === "suppress") {
|
|
12091
|
+
io.err(" \u23F8 skipped (--no-repo-writes): org-managed .gitignore block repair would dirty the working tree");
|
|
12092
|
+
io.err(` apply it after the release train: ${gitignoreCheck.fix}`);
|
|
12093
|
+
} else if (gitignoreDecision.action === "write") {
|
|
11145
12094
|
if (writeGitignore(gitignoreCheck.contentToWrite)) {
|
|
11146
|
-
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
11147
12095
|
const drift = gitignoreCheck.seeded ? "inserted the org-managed block" : [
|
|
11148
12096
|
gitignoreCheck.added?.length ? `added ${gitignoreCheck.added.join(", ")}` : "",
|
|
11149
12097
|
gitignoreCheck.removed?.length ? `removed ${gitignoreCheck.removed.join(", ")}` : ""
|
|
11150
12098
|
].filter(Boolean).join("; ") || "normalized the block";
|
|
12099
|
+
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
11151
12100
|
io.err(` \u21BB repaired: org-managed .gitignore block \u2014 ${drift}`);
|
|
11152
12101
|
io.err(" this is an org-managed update (not unrelated churn) \u2014 stage & commit .gitignore so it stops recurring");
|
|
11153
12102
|
}
|
|
@@ -11186,7 +12135,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11186
12135
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
11187
12136
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11188
12137
|
roots: mmiPluginCacheRootSnapshots(),
|
|
11189
|
-
activeVersion:
|
|
12138
|
+
activeVersion: resolveClientVersion(),
|
|
11190
12139
|
releasedVersion,
|
|
11191
12140
|
installedVersions: installedPluginVersions(installed)
|
|
11192
12141
|
});
|
|
@@ -11201,7 +12150,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11201
12150
|
...buildMmiPluginCacheCleanupCheck({
|
|
11202
12151
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11203
12152
|
roots: mmiPluginCacheRootSnapshots(),
|
|
11204
|
-
activeVersion:
|
|
12153
|
+
activeVersion: resolveClientVersion(),
|
|
11205
12154
|
releasedVersion
|
|
11206
12155
|
}),
|
|
11207
12156
|
...moved > 0 ? { cleanedCount: moved } : {}
|
|
@@ -11214,29 +12163,44 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11214
12163
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11215
12164
|
surface,
|
|
11216
12165
|
cacheRoot: cursorCacheRoot,
|
|
11217
|
-
cacheRootExists: (0,
|
|
12166
|
+
cacheRootExists: (0, import_node_fs7.existsSync)(cursorCacheRoot),
|
|
11218
12167
|
pins: cursorPluginCachePinSnapshots() ?? [],
|
|
11219
12168
|
hubCheckout: hubCheckoutForCursorSeed()
|
|
11220
12169
|
})
|
|
11221
12170
|
);
|
|
11222
12171
|
const gaps = checks.filter((c) => !c.ok);
|
|
11223
|
-
const resources = doctorResourcesForGaps(gaps);
|
|
11224
|
-
if (opts.json) {
|
|
11225
|
-
io.log(JSON.stringify({ ok: gaps.length === 0, checks, ...resources.length ? { resources } : {} }, null, 2));
|
|
11226
|
-
return;
|
|
11227
|
-
}
|
|
11228
12172
|
if (opts.banner) {
|
|
11229
12173
|
if (gaps.length) io.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
|
|
11230
12174
|
return;
|
|
11231
12175
|
}
|
|
12176
|
+
const cacheRoots = mmiPluginCacheRootSnapshots();
|
|
12177
|
+
const cacheVersionsFor = (s) => cacheRoots.filter((r) => r.surface === s).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
12178
|
+
const sourceVersions = (s) => installedPluginVersions(installedPluginSources().find((src) => src.surface === s)?.installed ?? null);
|
|
12179
|
+
const updateReport = buildPluginUpdateReport({
|
|
12180
|
+
cliVersion: resolveClientVersion(),
|
|
12181
|
+
claudePluginVersions: sourceVersions("claude"),
|
|
12182
|
+
codexPluginVersions: sourceVersions("codex"),
|
|
12183
|
+
codexCacheVersions: cacheVersionsFor("codex"),
|
|
12184
|
+
releasedVersion
|
|
12185
|
+
});
|
|
12186
|
+
const resources = doctorResourcesForGaps(gaps);
|
|
12187
|
+
if (opts.json) {
|
|
12188
|
+
io.log(JSON.stringify(buildDoctorJsonPayload({ checks, updateReport, resources }), null, 2));
|
|
12189
|
+
return;
|
|
12190
|
+
}
|
|
11232
12191
|
for (const c of checks) io.log(c.ok ? `\u2713 ${c.label}` : `\u2717 ${c.label}
|
|
11233
12192
|
\u2192 ${c.fix}`);
|
|
11234
12193
|
for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
|
|
12194
|
+
io.log("");
|
|
12195
|
+
for (const line of renderPluginUpdateReport(updateReport)) io.log(line);
|
|
11235
12196
|
io.log(gaps.length ? `
|
|
11236
12197
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
11237
12198
|
}
|
|
11238
|
-
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install) and
|
|
11239
|
-
|
|
12199
|
+
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for release-train prep)").action((opts) => (
|
|
12200
|
+
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
12201
|
+
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
12202
|
+
));
|
|
12203
|
+
program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor) in one process; docs sync runs detached").action(async () => {
|
|
11240
12204
|
try {
|
|
11241
12205
|
const hook = parseHookInput(await readStdin());
|
|
11242
12206
|
if (hook.session_id) persistSession(hook.session_id);
|
|
@@ -11248,6 +12212,16 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
11248
12212
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
11249
12213
|
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
11250
12214
|
sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
|
|
12215
|
+
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
12216
|
+
// when unknown — a missing gh login must not noise or fail the banner.
|
|
12217
|
+
whoami: async (io) => {
|
|
12218
|
+
const report = await resolveWhoami({
|
|
12219
|
+
hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
12220
|
+
ghLogin: githubLogin
|
|
12221
|
+
});
|
|
12222
|
+
const line = whoamiLine(report);
|
|
12223
|
+
if (line) io.log(line);
|
|
12224
|
+
},
|
|
11251
12225
|
doctor: (io) => runDoctor({ banner: true }, io)
|
|
11252
12226
|
});
|
|
11253
12227
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
@@ -11255,7 +12229,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
11255
12229
|
});
|
|
11256
12230
|
function fail(msg) {
|
|
11257
12231
|
console.error(`mmi-cli ${msg}`);
|
|
11258
|
-
|
|
12232
|
+
hardExit(1);
|
|
11259
12233
|
}
|
|
11260
12234
|
process.on("unhandledRejection", (reason) => fail(reason instanceof Error ? reason.message : String(reason)));
|
|
11261
12235
|
process.on("uncaughtException", (err) => fail(err instanceof Error ? err.message : String(err)));
|