@mutmutco/cli 3.51.0 → 3.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +1222 -549
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
DEFAULT_PRIORITY: () => DEFAULT_PRIORITY,
|
|
34
34
|
awsCallerArn: () => awsCallerArn,
|
|
35
35
|
classifyParseError: () => classifyParseError,
|
|
36
|
+
envHealLockPath: () => envHealLockPath,
|
|
36
37
|
gcPlan: () => gcPlan,
|
|
37
38
|
isOrgRegisteredRepo: () => isOrgRegisteredRepo,
|
|
38
39
|
registryClientDeps: () => registryClientDeps,
|
|
@@ -3409,8 +3410,8 @@ function useColor() {
|
|
|
3409
3410
|
var program = new Command();
|
|
3410
3411
|
|
|
3411
3412
|
// src/index.ts
|
|
3412
|
-
var
|
|
3413
|
-
var
|
|
3413
|
+
var import_promises8 = require("node:fs/promises");
|
|
3414
|
+
var import_node_fs32 = require("node:fs");
|
|
3414
3415
|
var import_node_child_process13 = require("node:child_process");
|
|
3415
3416
|
|
|
3416
3417
|
// src/cli-shared.ts
|
|
@@ -3820,7 +3821,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3820
3821
|
const attempts = opts.attempts ?? 3;
|
|
3821
3822
|
const baseDelayMs = opts.baseDelayMs ?? 250;
|
|
3822
3823
|
const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
|
|
3823
|
-
const
|
|
3824
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
3824
3825
|
let lastErr;
|
|
3825
3826
|
for (let i = 0; i < attempts; i++) {
|
|
3826
3827
|
const isLast = i === attempts - 1;
|
|
@@ -3828,14 +3829,14 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3828
3829
|
try {
|
|
3829
3830
|
const res = await fetchImpl(url, attemptInit);
|
|
3830
3831
|
if (!isLast && retryOn(res)) {
|
|
3831
|
-
await
|
|
3832
|
+
await sleep3(baseDelayMs * 2 ** i);
|
|
3832
3833
|
continue;
|
|
3833
3834
|
}
|
|
3834
3835
|
return res;
|
|
3835
3836
|
} catch (e) {
|
|
3836
3837
|
lastErr = e;
|
|
3837
3838
|
if (isLast) throw e;
|
|
3838
|
-
await
|
|
3839
|
+
await sleep3(baseDelayMs * 2 ** i);
|
|
3839
3840
|
}
|
|
3840
3841
|
}
|
|
3841
3842
|
throw lastErr;
|
|
@@ -3860,13 +3861,23 @@ function defaultHubSessionCachePath(env = process.env) {
|
|
|
3860
3861
|
const base = env.XDG_STATE_HOME || (0, import_node_path3.join)((0, import_node_os.homedir)(), ".local", "state");
|
|
3861
3862
|
return (0, import_node_path3.join)(base, "mmi-cli", "hub-session.json");
|
|
3862
3863
|
}
|
|
3864
|
+
function roleFromToken(token) {
|
|
3865
|
+
const part = token?.split(".")[1];
|
|
3866
|
+
if (!part) return void 0;
|
|
3867
|
+
try {
|
|
3868
|
+
const claims = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
3869
|
+
return claims.role === "owner" ? "owner" : void 0;
|
|
3870
|
+
} catch {
|
|
3871
|
+
return void 0;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3863
3874
|
function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
3864
3875
|
try {
|
|
3865
3876
|
const session = JSON.parse((0, import_node_fs3.readFileSync)(path2, "utf8"));
|
|
3866
3877
|
if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return null;
|
|
3867
3878
|
if (session.githubTokenFingerprint !== githubTokenFingerprint) return null;
|
|
3868
3879
|
if (new Date(session.expiresAt).getTime() <= now.getTime() + REFRESH_WINDOW_MS) return null;
|
|
3869
|
-
return session;
|
|
3880
|
+
return { ...session, role: roleFromToken(session.token) };
|
|
3870
3881
|
} catch {
|
|
3871
3882
|
return null;
|
|
3872
3883
|
}
|
|
@@ -3913,6 +3924,9 @@ async function hubAuthSession(deps) {
|
|
|
3913
3924
|
token: body.token,
|
|
3914
3925
|
expiresAt: body.expiresAt,
|
|
3915
3926
|
login: typeof body.login === "string" ? body.login : void 0,
|
|
3927
|
+
// #3513: from the SIGNED token, not the response body — one rule at every hop, so the cache
|
|
3928
|
+
// read and the network read cannot disagree about what counts as an assertion.
|
|
3929
|
+
role: roleFromToken(body.token),
|
|
3916
3930
|
apiUrl,
|
|
3917
3931
|
githubTokenFingerprint
|
|
3918
3932
|
};
|
|
@@ -5084,6 +5098,26 @@ var import_node_fs10 = require("node:fs");
|
|
|
5084
5098
|
|
|
5085
5099
|
// src/gc.ts
|
|
5086
5100
|
var import_node_path7 = require("node:path");
|
|
5101
|
+
var AGENT_ARTIFACT_PREFIXES = [".jerv/"];
|
|
5102
|
+
function classifyWorktreeDirt(porcelain) {
|
|
5103
|
+
const lines = porcelain.split("\n").map((l) => l.trimEnd()).filter((l) => l.length > 0);
|
|
5104
|
+
if (!lines.length) return "clean";
|
|
5105
|
+
if (lines.some((l) => l.slice(0, 2) !== "??")) return "tracked-changes";
|
|
5106
|
+
const isArtifact = (path2) => AGENT_ARTIFACT_PREFIXES.some((prefix) => (
|
|
5107
|
+
// Equals the prefix without its trailing slash (`.jerv`) or sits under it (`.jerv/attest/x.json`);
|
|
5108
|
+
// a sibling like `.jerv-lease.json` must NOT match by string-prefix alone.
|
|
5109
|
+
path2 === prefix.slice(0, -1) || path2.startsWith(prefix)
|
|
5110
|
+
));
|
|
5111
|
+
const untrackedPaths = lines.map((l) => {
|
|
5112
|
+
let path2 = l.slice(3);
|
|
5113
|
+
if (path2.length >= 2 && path2.startsWith('"') && path2.endsWith('"')) path2 = path2.slice(1, -1);
|
|
5114
|
+
return path2;
|
|
5115
|
+
});
|
|
5116
|
+
return untrackedPaths.every(isArtifact) ? "artifacts-only" : "untracked-files";
|
|
5117
|
+
}
|
|
5118
|
+
function isRemovableDirt(dirt) {
|
|
5119
|
+
return dirt === "clean" || dirt === "artifacts-only";
|
|
5120
|
+
}
|
|
5087
5121
|
var DEFERRED_SWEEP_COMMAND = "mmi-cli worktree gc sweep-deferred";
|
|
5088
5122
|
var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
|
|
5089
5123
|
var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
|
|
@@ -5144,7 +5178,7 @@ async function sweepDeferredWorktrees(store, deps) {
|
|
|
5144
5178
|
if (!entries.length) return { removed: [], stillDeferred: [], skipped: [] };
|
|
5145
5179
|
let live;
|
|
5146
5180
|
try {
|
|
5147
|
-
live =
|
|
5181
|
+
live = parseWorktreePorcelainEntries(await deps.git(["worktree", "list", "--porcelain"]));
|
|
5148
5182
|
} catch {
|
|
5149
5183
|
return { removed: [], stillDeferred: entries, skipped: [] };
|
|
5150
5184
|
}
|
|
@@ -5158,8 +5192,8 @@ async function sweepDeferredWorktrees(store, deps) {
|
|
|
5158
5192
|
continue;
|
|
5159
5193
|
}
|
|
5160
5194
|
if (current) {
|
|
5161
|
-
const
|
|
5162
|
-
if (
|
|
5195
|
+
const porcelain = await deps.git(["-C", entry.path, "status", "--porcelain"]).catch(() => void 0);
|
|
5196
|
+
if (!isRemovableDirt(porcelain === void 0 ? "tracked-changes" : classifyWorktreeDirt(porcelain))) {
|
|
5163
5197
|
stillDeferred.push(entry);
|
|
5164
5198
|
continue;
|
|
5165
5199
|
}
|
|
@@ -5179,7 +5213,7 @@ async function sweepDeferredWorktrees(store, deps) {
|
|
|
5179
5213
|
async function sweepDeferredWorktreesWithRetry(store, deps, opts = {}) {
|
|
5180
5214
|
const attempts = opts.attempts ?? 4;
|
|
5181
5215
|
const backoff = opts.backoffMs ?? [500, 2e3, 5e3, 1e4];
|
|
5182
|
-
const
|
|
5216
|
+
const sleep3 = opts.sleep ?? defaultSleep;
|
|
5183
5217
|
const removed = [];
|
|
5184
5218
|
const skipped = [];
|
|
5185
5219
|
let stillDeferred = [];
|
|
@@ -5189,7 +5223,7 @@ async function sweepDeferredWorktreesWithRetry(store, deps, opts = {}) {
|
|
|
5189
5223
|
skipped.push(...result.skipped);
|
|
5190
5224
|
stillDeferred = result.stillDeferred;
|
|
5191
5225
|
if (!stillDeferred.length) break;
|
|
5192
|
-
if (i < attempts - 1) await
|
|
5226
|
+
if (i < attempts - 1) await sleep3(backoff[Math.min(i, backoff.length - 1)]);
|
|
5193
5227
|
}
|
|
5194
5228
|
return { removed, stillDeferred, skipped };
|
|
5195
5229
|
}
|
|
@@ -5281,7 +5315,7 @@ function buildRemoteBranchCleanupReport(branch, input) {
|
|
|
5281
5315
|
return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
|
|
5282
5316
|
}
|
|
5283
5317
|
async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, options = {}) {
|
|
5284
|
-
const
|
|
5318
|
+
const sleep3 = options.sleep ?? defaultSleep;
|
|
5285
5319
|
const maxAttempts = options.maxAttempts ?? 5;
|
|
5286
5320
|
const backoff = [500, 1e3, 1500, 2e3];
|
|
5287
5321
|
if (!input.attempted) {
|
|
@@ -5292,7 +5326,7 @@ async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, option
|
|
|
5292
5326
|
}
|
|
5293
5327
|
let existsAfter = await deps.exists(branch, { prune: true });
|
|
5294
5328
|
for (let i = 1; i < maxAttempts && existsAfter === true; i++) {
|
|
5295
|
-
await
|
|
5329
|
+
await sleep3(backoff[Math.min(i - 1, backoff.length - 1)]);
|
|
5296
5330
|
existsAfter = await deps.exists(branch, { prune: true });
|
|
5297
5331
|
}
|
|
5298
5332
|
return buildRemoteBranchCleanupReport(branch, {
|
|
@@ -5688,7 +5722,7 @@ function parseBranchHeadRows(stdout) {
|
|
|
5688
5722
|
}
|
|
5689
5723
|
return out;
|
|
5690
5724
|
}
|
|
5691
|
-
function
|
|
5725
|
+
function parseWorktreePorcelainEntries(stdout) {
|
|
5692
5726
|
const out = [];
|
|
5693
5727
|
for (const block of stdout.split(/\r?\n(?=worktree )/)) {
|
|
5694
5728
|
let path2 = "";
|
|
@@ -5697,15 +5731,24 @@ function parseWorktreePorcelain(stdout) {
|
|
|
5697
5731
|
if (line.startsWith("worktree ")) path2 = line.slice("worktree ".length).trim();
|
|
5698
5732
|
if (line.startsWith("branch refs/heads/")) branch = line.slice("branch refs/heads/".length).trim();
|
|
5699
5733
|
}
|
|
5700
|
-
if (path2
|
|
5734
|
+
if (path2) out.push(branch ? { path: path2, branch } : { path: path2 });
|
|
5701
5735
|
}
|
|
5702
5736
|
return out;
|
|
5703
5737
|
}
|
|
5704
|
-
function
|
|
5705
|
-
return
|
|
5738
|
+
function parseWorktreePorcelain(stdout) {
|
|
5739
|
+
return parseWorktreePorcelainEntries(stdout).filter(
|
|
5740
|
+
(w) => typeof w.branch === "string"
|
|
5741
|
+
);
|
|
5706
5742
|
}
|
|
5707
|
-
function
|
|
5708
|
-
return
|
|
5743
|
+
function pathsAreCaseInsensitive(platform2 = process.platform) {
|
|
5744
|
+
return platform2 === "win32" || platform2 === "darwin";
|
|
5745
|
+
}
|
|
5746
|
+
function normPath(p, platform2 = process.platform) {
|
|
5747
|
+
const unified = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
5748
|
+
return pathsAreCaseInsensitive(platform2) ? unified.toLowerCase() : unified;
|
|
5749
|
+
}
|
|
5750
|
+
function samePath(a, b, platform2 = process.platform) {
|
|
5751
|
+
return normPath(a, platform2) === normPath(b, platform2);
|
|
5709
5752
|
}
|
|
5710
5753
|
function toNativePath(p) {
|
|
5711
5754
|
return process.platform === "win32" ? p.replace(/\//g, "\\") : p;
|
|
@@ -5753,7 +5796,7 @@ function selectWorktreeComposeProjects(worktreePath, projects) {
|
|
|
5753
5796
|
return names;
|
|
5754
5797
|
}
|
|
5755
5798
|
function deriveComposeProjectName(worktreePath) {
|
|
5756
|
-
const norm = normPath(worktreePath);
|
|
5799
|
+
const norm = normPath(worktreePath).toLowerCase();
|
|
5757
5800
|
if (!norm) return void 0;
|
|
5758
5801
|
const base = norm.slice(norm.lastIndexOf("/") + 1);
|
|
5759
5802
|
const name = base.replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
|
|
@@ -5862,9 +5905,10 @@ async function fastForwardCurrentBranch(git) {
|
|
|
5862
5905
|
}
|
|
5863
5906
|
async function returnCheckoutToBase(git, base, mergedBranch, currentBranch2) {
|
|
5864
5907
|
if (currentBranch2 === mergedBranch) {
|
|
5865
|
-
const
|
|
5866
|
-
|
|
5867
|
-
|
|
5908
|
+
const porcelain = await git(["status", "--porcelain"]).catch(() => void 0);
|
|
5909
|
+
const dirt = porcelain === void 0 ? "tracked-changes" : classifyWorktreeDirt(porcelain);
|
|
5910
|
+
if (!isRemovableDirt(dirt)) {
|
|
5911
|
+
return { report: { branch: base, switched: false, sync: "skipped", reason: dirt === "untracked-files" ? "untracked-files" : "dirty-worktree" }, canDeleteBranch: false };
|
|
5868
5912
|
}
|
|
5869
5913
|
try {
|
|
5870
5914
|
await git(["checkout", base]);
|
|
@@ -5947,20 +5991,22 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
5947
5991
|
return report;
|
|
5948
5992
|
}
|
|
5949
5993
|
const worktreeDirtyAtActTime = async (path2) => {
|
|
5950
|
-
if (options.pathExists && !options.pathExists(path2)) return
|
|
5951
|
-
if (options.isWorktreeDirty) return options.isWorktreeDirty(path2);
|
|
5994
|
+
if (options.pathExists && !options.pathExists(path2)) return void 0;
|
|
5995
|
+
if (options.isWorktreeDirty) return await options.isWorktreeDirty(path2) ? "tracked-changes" : void 0;
|
|
5952
5996
|
try {
|
|
5953
|
-
return (await options.execGit(["-C", path2, "status", "--porcelain"])
|
|
5997
|
+
return classifyWorktreeDirt(await options.execGit(["-C", path2, "status", "--porcelain"]));
|
|
5954
5998
|
} catch {
|
|
5955
|
-
return
|
|
5999
|
+
return "tracked-changes";
|
|
5956
6000
|
}
|
|
5957
6001
|
};
|
|
5958
6002
|
if (wtPath && mainWorktreeTarget) {
|
|
5959
6003
|
report.worktree = { path: wtPath, status: "not-attempted", reason: "main-worktree" };
|
|
5960
6004
|
} else if (wtPath) {
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
6005
|
+
const dirt = await worktreeDirtyAtActTime(wtPath);
|
|
6006
|
+
if (dirt !== void 0 && !isRemovableDirt(dirt)) {
|
|
6007
|
+
const reason = dirt === "untracked-files" ? "untracked-files" : "dirty-worktree";
|
|
6008
|
+
report.worktree = { path: wtPath, status: "not-attempted", reason };
|
|
6009
|
+
report.localBranch = { name: branch, status: "not-attempted", reason };
|
|
5964
6010
|
return report;
|
|
5965
6011
|
}
|
|
5966
6012
|
const removeDeps = {
|
|
@@ -6183,11 +6229,27 @@ function resolveReleaseTrack(meta, hints, repo) {
|
|
|
6183
6229
|
if (inferred) return inferred;
|
|
6184
6230
|
return "full";
|
|
6185
6231
|
}
|
|
6232
|
+
function metaWithDeclaredClass(meta, declaredClass) {
|
|
6233
|
+
if (!declaredClass) return meta;
|
|
6234
|
+
const stated = typeof meta?.releaseTrack === "string" || typeof meta?.class === "string" || typeof meta?.deployModel === "string";
|
|
6235
|
+
if (stated) return meta;
|
|
6236
|
+
return { ...meta ?? {}, class: declaredClass };
|
|
6237
|
+
}
|
|
6186
6238
|
function branchesForTrack(track) {
|
|
6187
6239
|
if (track === "trunk") return ["main"];
|
|
6188
6240
|
if (track === "direct") return ["development", "main"];
|
|
6189
6241
|
return ["development", "rc", "main"];
|
|
6190
6242
|
}
|
|
6243
|
+
function promotionBranchesForTrack(track) {
|
|
6244
|
+
if (track === "trunk") return [];
|
|
6245
|
+
if (track === "direct") return ["main"];
|
|
6246
|
+
return ["rc", "main"];
|
|
6247
|
+
}
|
|
6248
|
+
function isPromotionBase(base, track) {
|
|
6249
|
+
if (!base) return false;
|
|
6250
|
+
if (!track) return base !== "development";
|
|
6251
|
+
return promotionBranchesForTrack(track).includes(base);
|
|
6252
|
+
}
|
|
6191
6253
|
|
|
6192
6254
|
// src/readiness-audit.ts
|
|
6193
6255
|
var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
|
|
@@ -7443,7 +7505,7 @@ async function worktreeBranches() {
|
|
|
7443
7505
|
let dirty = true;
|
|
7444
7506
|
try {
|
|
7445
7507
|
const { stdout: status } = await execFileP2("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7446
|
-
dirty = status
|
|
7508
|
+
dirty = !isRemovableDirt(classifyWorktreeDirt(status));
|
|
7447
7509
|
} catch {
|
|
7448
7510
|
dirty = true;
|
|
7449
7511
|
}
|
|
@@ -7859,6 +7921,12 @@ function parseIssueSelector(selector, defaultRepo) {
|
|
|
7859
7921
|
if (local) return { repo: defaultRepo, number: Number(local[1]) };
|
|
7860
7922
|
throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
|
|
7861
7923
|
}
|
|
7924
|
+
function sameRepo(itemRepo, selectorKey) {
|
|
7925
|
+
const repo = itemRepo.toLowerCase();
|
|
7926
|
+
if (repo === selectorKey) return true;
|
|
7927
|
+
if (selectorKey.includes("/")) return false;
|
|
7928
|
+
return repo.slice(repo.indexOf("/") + 1) === selectorKey;
|
|
7929
|
+
}
|
|
7862
7930
|
function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
|
|
7863
7931
|
const empty = () => ({ userOwned: [], claimable: [], taken: [], unownedInFlight: [] });
|
|
7864
7932
|
const groups = { primary: empty(), secondary: empty() };
|
|
@@ -7866,7 +7934,7 @@ function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
|
|
|
7866
7934
|
const repoKey = currentRepo.toLowerCase();
|
|
7867
7935
|
for (const item of items) {
|
|
7868
7936
|
if (!ACTIVE_STATUSES.has(item.status)) continue;
|
|
7869
|
-
const scope = item.repository
|
|
7937
|
+
const scope = sameRepo(item.repository, repoKey) ? "primary" : "secondary";
|
|
7870
7938
|
const assignees = item.assignees.map((a) => a.toLowerCase());
|
|
7871
7939
|
const assignedToViewer = assignees.includes(viewerKey);
|
|
7872
7940
|
if (assignedToViewer) {
|
|
@@ -8413,11 +8481,11 @@ var defaultRetrySleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, m
|
|
|
8413
8481
|
async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
|
|
8414
8482
|
const attempts = Math.max(1, opts.attempts ?? 5);
|
|
8415
8483
|
const delayMs = opts.delayMs ?? 300;
|
|
8416
|
-
const
|
|
8484
|
+
const sleep3 = opts.sleep ?? defaultRetrySleep;
|
|
8417
8485
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
8418
8486
|
const itemId = (await fetchIssueProjectItem(client, cfg, selector)).item?.itemId;
|
|
8419
8487
|
if (itemId) return itemId;
|
|
8420
|
-
if (attempt < attempts - 1) await
|
|
8488
|
+
if (attempt < attempts - 1) await sleep3(delayMs * (attempt + 1));
|
|
8421
8489
|
}
|
|
8422
8490
|
return void 0;
|
|
8423
8491
|
}
|
|
@@ -9000,9 +9068,196 @@ async function gatherStaleWorktreeWarning(gitRun = defaultGitRun) {
|
|
|
9000
9068
|
}
|
|
9001
9069
|
}
|
|
9002
9070
|
|
|
9003
|
-
// src/
|
|
9071
|
+
// src/released-version-cache.ts
|
|
9004
9072
|
var import_node_fs12 = require("node:fs");
|
|
9005
9073
|
var import_node_path10 = require("node:path");
|
|
9074
|
+
var RELEASED_VERSION_CACHE_MS = 24 * 36e5;
|
|
9075
|
+
function releasedVersionCachePath(runtimeRoot) {
|
|
9076
|
+
return (0, import_node_path10.join)(runtimeRoot, "head-ts", ".released-version");
|
|
9077
|
+
}
|
|
9078
|
+
function readReleasedVersionCache(cachePath, now = Date.now(), read = import_node_fs12.readFileSync) {
|
|
9079
|
+
let parsed;
|
|
9080
|
+
try {
|
|
9081
|
+
parsed = JSON.parse(read(cachePath, "utf8"));
|
|
9082
|
+
} catch {
|
|
9083
|
+
return void 0;
|
|
9084
|
+
}
|
|
9085
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
9086
|
+
const { version, at } = parsed;
|
|
9087
|
+
if (typeof version !== "string" || !version.trim()) return void 0;
|
|
9088
|
+
if (typeof at !== "number" || !Number.isFinite(at)) return void 0;
|
|
9089
|
+
const age = now - at;
|
|
9090
|
+
if (age < 0 || age >= RELEASED_VERSION_CACHE_MS) return void 0;
|
|
9091
|
+
return { version, at };
|
|
9092
|
+
}
|
|
9093
|
+
function writeReleasedVersionCache(cachePath, version, now = Date.now()) {
|
|
9094
|
+
try {
|
|
9095
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(cachePath), { recursive: true });
|
|
9096
|
+
(0, import_node_fs12.writeFileSync)(cachePath, JSON.stringify({ version, at: now }), "utf8");
|
|
9097
|
+
} catch {
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
function cacheAgeHours(at, now = Date.now()) {
|
|
9101
|
+
return Math.max(0, Math.floor((now - at) / 36e5));
|
|
9102
|
+
}
|
|
9103
|
+
function cachedReadNote(cachedAt, now = Date.now()) {
|
|
9104
|
+
const hours = cacheAgeHours(cachedAt, now);
|
|
9105
|
+
return `(cached ${hours < 1 ? "<1" : hours}h ago \u2014 the banner reads npm at most once a day)`;
|
|
9106
|
+
}
|
|
9107
|
+
|
|
9108
|
+
// src/marketplace-autoupdate.ts
|
|
9109
|
+
var KNOWN_MARKETPLACES_RELATIVE = [".claude", "plugins", "known_marketplaces.json"];
|
|
9110
|
+
var MMI_MARKETPLACE_NAME = "mutmutco";
|
|
9111
|
+
function readFileSyncSafe(path2, read) {
|
|
9112
|
+
try {
|
|
9113
|
+
return read(path2, "utf8");
|
|
9114
|
+
} catch {
|
|
9115
|
+
return void 0;
|
|
9116
|
+
}
|
|
9117
|
+
}
|
|
9118
|
+
var AUTO_UPDATE_DEFAULT_ON = /* @__PURE__ */ new Set([
|
|
9119
|
+
"claude-plugins-official",
|
|
9120
|
+
"claude-code-marketplace",
|
|
9121
|
+
"claude-code-plugins",
|
|
9122
|
+
"anthropic-marketplace",
|
|
9123
|
+
"anthropic-plugins",
|
|
9124
|
+
"agent-skills",
|
|
9125
|
+
"anthropic-agent-skills"
|
|
9126
|
+
]);
|
|
9127
|
+
function resolveAutoUpdate(probe) {
|
|
9128
|
+
const { name, registered, declared, settingsDeclared } = probe;
|
|
9129
|
+
if (!registered) {
|
|
9130
|
+
return { name, effective: false, reason: "not registered \u2014 nothing to update" };
|
|
9131
|
+
}
|
|
9132
|
+
if (typeof declared === "boolean") {
|
|
9133
|
+
return { name, effective: declared, reason: `declared ${declared} in known_marketplaces.json` };
|
|
9134
|
+
}
|
|
9135
|
+
if (typeof settingsDeclared === "boolean") {
|
|
9136
|
+
return { name, effective: settingsDeclared, reason: `declared ${settingsDeclared} under extraKnownMarketplaces.${name}` };
|
|
9137
|
+
}
|
|
9138
|
+
if (AUTO_UPDATE_DEFAULT_ON.has(name.toLowerCase())) {
|
|
9139
|
+
return { name, effective: true, reason: "undeclared \u2014 an official Anthropic marketplace, on by default" };
|
|
9140
|
+
}
|
|
9141
|
+
return {
|
|
9142
|
+
name,
|
|
9143
|
+
effective: false,
|
|
9144
|
+
reason: "undeclared \u2014 third-party marketplaces are OFF by default, so nothing auto-updates and no update notice ever prints"
|
|
9145
|
+
};
|
|
9146
|
+
}
|
|
9147
|
+
function readKnownMarketplace(raw, name) {
|
|
9148
|
+
if (!raw) return { registered: false };
|
|
9149
|
+
let parsed;
|
|
9150
|
+
try {
|
|
9151
|
+
parsed = JSON.parse(raw);
|
|
9152
|
+
} catch {
|
|
9153
|
+
return { registered: false };
|
|
9154
|
+
}
|
|
9155
|
+
if (!parsed || typeof parsed !== "object") return { registered: false };
|
|
9156
|
+
const entry = parsed[name];
|
|
9157
|
+
if (!entry || typeof entry !== "object") return { registered: false };
|
|
9158
|
+
const { autoUpdate, source } = entry;
|
|
9159
|
+
const ref = source && typeof source === "object" ? source.ref : void 0;
|
|
9160
|
+
return {
|
|
9161
|
+
registered: true,
|
|
9162
|
+
...typeof autoUpdate === "boolean" ? { declared: autoUpdate } : {},
|
|
9163
|
+
...typeof ref === "string" && ref ? { ref } : {}
|
|
9164
|
+
};
|
|
9165
|
+
}
|
|
9166
|
+
function readSettingsAutoUpdate(raw, name) {
|
|
9167
|
+
if (!raw) return void 0;
|
|
9168
|
+
let parsed;
|
|
9169
|
+
try {
|
|
9170
|
+
parsed = JSON.parse(raw);
|
|
9171
|
+
} catch {
|
|
9172
|
+
return void 0;
|
|
9173
|
+
}
|
|
9174
|
+
const extra = parsed?.extraKnownMarketplaces;
|
|
9175
|
+
if (!extra || typeof extra !== "object") return void 0;
|
|
9176
|
+
const entry = extra[name];
|
|
9177
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
9178
|
+
const { autoUpdate } = entry;
|
|
9179
|
+
return typeof autoUpdate === "boolean" ? autoUpdate : void 0;
|
|
9180
|
+
}
|
|
9181
|
+
var AUTO_UPDATE_TOGGLE_STEPS = "`/plugin` \u2192 Marketplaces tab \u2192 select the marketplace \u2192 Enable auto-update";
|
|
9182
|
+
var CATALOG_CONTENT_REF = "main";
|
|
9183
|
+
var CATALOG_REF_PIN_STEPS = `add \`"ref": "${CATALOG_CONTENT_REF}"\` to this marketplace's \`source\` in ~/.claude/plugins/known_marketplaces.json, then restart Claude`;
|
|
9184
|
+
function resolveCatalogRef(probe) {
|
|
9185
|
+
const { name, registered, ref, autoUpdate } = probe;
|
|
9186
|
+
if (!registered) return void 0;
|
|
9187
|
+
if (ref) {
|
|
9188
|
+
const agrees = ref === CATALOG_CONTENT_REF;
|
|
9189
|
+
return {
|
|
9190
|
+
name,
|
|
9191
|
+
ref,
|
|
9192
|
+
autoUpdate,
|
|
9193
|
+
unpinned: false,
|
|
9194
|
+
agrees,
|
|
9195
|
+
reason: agrees ? `pinned to ${ref} \u2014 catalog and plugin content come from the same branch` : `pinned to ${ref} \u2014 plugin content is pinned to ${CATALOG_CONTENT_REF}, so the two disagree`
|
|
9196
|
+
};
|
|
9197
|
+
}
|
|
9198
|
+
return {
|
|
9199
|
+
name,
|
|
9200
|
+
autoUpdate,
|
|
9201
|
+
unpinned: true,
|
|
9202
|
+
agrees: false,
|
|
9203
|
+
reason: `no ref pinned \u2014 the catalog is served from the repo's default branch while plugin content is pinned to ${CATALOG_CONTENT_REF}`
|
|
9204
|
+
};
|
|
9205
|
+
}
|
|
9206
|
+
function checkMarketplaceAutoUpdate(probe) {
|
|
9207
|
+
if (!probe) return null;
|
|
9208
|
+
const evidence = [`${probe.name}: auto-update ${probe.effective ? "on" : "off"} \u2014 ${probe.reason}`];
|
|
9209
|
+
if (probe.effective) {
|
|
9210
|
+
return { ok: true, label: `marketplace auto-update (${probe.name})`, detail: "on", verbose: evidence };
|
|
9211
|
+
}
|
|
9212
|
+
return {
|
|
9213
|
+
ok: true,
|
|
9214
|
+
warn: true,
|
|
9215
|
+
label: `marketplace auto-update (${probe.name})`,
|
|
9216
|
+
detail: `off \u2014 this machine will not pick up a new plugin release on its own; turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`,
|
|
9217
|
+
verbose: evidence
|
|
9218
|
+
};
|
|
9219
|
+
}
|
|
9220
|
+
function checkMarketplaceCatalogRef(probe) {
|
|
9221
|
+
if (!probe) return null;
|
|
9222
|
+
const label = `marketplace catalog ref (${probe.name})`;
|
|
9223
|
+
const evidence = [`${probe.name}: ${probe.reason}`, `auto-update: ${probe.autoUpdate ? "on" : "off"}`];
|
|
9224
|
+
if (probe.agrees) {
|
|
9225
|
+
return { ok: true, label, detail: `${probe.ref}`, verbose: evidence };
|
|
9226
|
+
}
|
|
9227
|
+
const drift = probe.unpinned ? "unpinned" : `pinned to ${probe.ref}, not ${CATALOG_CONTENT_REF}`;
|
|
9228
|
+
if (probe.autoUpdate) {
|
|
9229
|
+
return {
|
|
9230
|
+
ok: false,
|
|
9231
|
+
label,
|
|
9232
|
+
fix: `${drift} while auto-update is ON \u2014 this machine installs plugin releases advertised by a branch nobody released; ${CATALOG_REF_PIN_STEPS}`,
|
|
9233
|
+
verbose: evidence
|
|
9234
|
+
};
|
|
9235
|
+
}
|
|
9236
|
+
return {
|
|
9237
|
+
ok: true,
|
|
9238
|
+
warn: true,
|
|
9239
|
+
label,
|
|
9240
|
+
detail: `${drift} \u2014 harmless while auto-update is off, and the thing to fix before turning it on; ${CATALOG_REF_PIN_STEPS}`,
|
|
9241
|
+
verbose: evidence
|
|
9242
|
+
};
|
|
9243
|
+
}
|
|
9244
|
+
function marketplaceRows(name, known, settings) {
|
|
9245
|
+
const { registered, declared, ref } = readKnownMarketplace(known, name);
|
|
9246
|
+
const autoUpdate = resolveAutoUpdate({
|
|
9247
|
+
name,
|
|
9248
|
+
registered,
|
|
9249
|
+
declared,
|
|
9250
|
+
settingsDeclared: readSettingsAutoUpdate(settings, name)
|
|
9251
|
+
});
|
|
9252
|
+
const rows = [checkMarketplaceAutoUpdate(autoUpdate), checkMarketplaceCatalogRef(
|
|
9253
|
+
resolveCatalogRef({ name, registered, ref, autoUpdate: autoUpdate.effective })
|
|
9254
|
+
)];
|
|
9255
|
+
return rows.filter((r) => r !== null);
|
|
9256
|
+
}
|
|
9257
|
+
|
|
9258
|
+
// src/hook-activity.ts
|
|
9259
|
+
var import_node_fs13 = require("node:fs");
|
|
9260
|
+
var import_node_path11 = require("node:path");
|
|
9006
9261
|
var DEFAULT_SURFACE = "claude";
|
|
9007
9262
|
function activityLogPath(cwd) {
|
|
9008
9263
|
return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
|
|
@@ -9015,16 +9270,16 @@ function appendHookActivity(cwd, entry) {
|
|
|
9015
9270
|
surface: DEFAULT_SURFACE,
|
|
9016
9271
|
...entry
|
|
9017
9272
|
};
|
|
9018
|
-
(0,
|
|
9019
|
-
(0,
|
|
9273
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path2), { recursive: true });
|
|
9274
|
+
(0, import_node_fs13.appendFileSync)(path2, `${JSON.stringify(line)}
|
|
9020
9275
|
`, "utf8");
|
|
9021
9276
|
} catch {
|
|
9022
9277
|
}
|
|
9023
9278
|
}
|
|
9024
9279
|
|
|
9025
9280
|
// src/worktree.ts
|
|
9026
|
-
var
|
|
9027
|
-
var
|
|
9281
|
+
var import_node_fs14 = require("node:fs");
|
|
9282
|
+
var import_node_path12 = require("node:path");
|
|
9028
9283
|
var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
9029
9284
|
var PKG = "package.json";
|
|
9030
9285
|
var LOCKFILE = "package-lock.json";
|
|
@@ -9032,21 +9287,21 @@ var NODE_MODULES = "node_modules";
|
|
|
9032
9287
|
var realFsProbe = {
|
|
9033
9288
|
isDir: (p) => {
|
|
9034
9289
|
try {
|
|
9035
|
-
return (0,
|
|
9290
|
+
return (0, import_node_fs14.statSync)(p).isDirectory();
|
|
9036
9291
|
} catch {
|
|
9037
9292
|
return false;
|
|
9038
9293
|
}
|
|
9039
9294
|
},
|
|
9040
9295
|
isFile: (p) => {
|
|
9041
9296
|
try {
|
|
9042
|
-
return (0,
|
|
9297
|
+
return (0, import_node_fs14.statSync)(p).isFile();
|
|
9043
9298
|
} catch {
|
|
9044
9299
|
return false;
|
|
9045
9300
|
}
|
|
9046
9301
|
},
|
|
9047
9302
|
listDirs: (p) => {
|
|
9048
9303
|
try {
|
|
9049
|
-
return (0,
|
|
9304
|
+
return (0, import_node_fs14.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9050
9305
|
} catch {
|
|
9051
9306
|
return [];
|
|
9052
9307
|
}
|
|
@@ -9054,12 +9309,12 @@ var realFsProbe = {
|
|
|
9054
9309
|
};
|
|
9055
9310
|
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
9056
9311
|
const factsFor = (dir) => {
|
|
9057
|
-
const abs = dir ? (0,
|
|
9312
|
+
const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
|
|
9058
9313
|
return {
|
|
9059
9314
|
dir,
|
|
9060
|
-
hasPackageJson: fs2.isFile((0,
|
|
9061
|
-
hasLockfile: fs2.isFile((0,
|
|
9062
|
-
hasNodeModules: fs2.isDir((0,
|
|
9315
|
+
hasPackageJson: fs2.isFile((0, import_node_path12.join)(abs, PKG)),
|
|
9316
|
+
hasLockfile: fs2.isFile((0, import_node_path12.join)(abs, LOCKFILE)),
|
|
9317
|
+
hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES))
|
|
9063
9318
|
};
|
|
9064
9319
|
};
|
|
9065
9320
|
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
@@ -9069,7 +9324,7 @@ function npmInstallTargets(dirs) {
|
|
|
9069
9324
|
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
9070
9325
|
}
|
|
9071
9326
|
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
9072
|
-
return fs2.isFile((0,
|
|
9327
|
+
return fs2.isFile((0, import_node_path12.join)(root, ".git"));
|
|
9073
9328
|
}
|
|
9074
9329
|
function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
9075
9330
|
if (!isLinkedWorktree(root, fs2)) return null;
|
|
@@ -9079,8 +9334,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
|
9079
9334
|
return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
|
|
9080
9335
|
}
|
|
9081
9336
|
function defaultCopyFile(from, to) {
|
|
9082
|
-
(0,
|
|
9083
|
-
(0,
|
|
9337
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
|
|
9338
|
+
(0, import_node_fs14.copyFileSync)(from, to);
|
|
9084
9339
|
}
|
|
9085
9340
|
async function provisionWorktree(worktreeRoot, deps) {
|
|
9086
9341
|
const fs2 = deps.fs ?? realFsProbe;
|
|
@@ -9092,7 +9347,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
9092
9347
|
const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
|
|
9093
9348
|
const installed = [];
|
|
9094
9349
|
for (const target of targets) {
|
|
9095
|
-
const cwd = target.dir ? (0,
|
|
9350
|
+
const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
|
|
9096
9351
|
log(`installing deps: ${target.command} in ${target.dir || "."}`);
|
|
9097
9352
|
await deps.runInstall(target.command, cwd);
|
|
9098
9353
|
installed.push(target);
|
|
@@ -9101,7 +9356,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
9101
9356
|
const copySkipped = [];
|
|
9102
9357
|
const primary = await deps.primaryCheckout();
|
|
9103
9358
|
for (const rel of LOCAL_ONLY_FILES) {
|
|
9104
|
-
const dest = (0,
|
|
9359
|
+
const dest = (0, import_node_path12.join)(worktreeRoot, rel);
|
|
9105
9360
|
if (fs2.isFile(dest)) {
|
|
9106
9361
|
copySkipped.push({ file: rel, reason: "already-present" });
|
|
9107
9362
|
continue;
|
|
@@ -9110,24 +9365,30 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
9110
9365
|
copySkipped.push({ file: rel, reason: "no-primary" });
|
|
9111
9366
|
continue;
|
|
9112
9367
|
}
|
|
9113
|
-
if (!fs2.isFile((0,
|
|
9368
|
+
if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
|
|
9114
9369
|
copySkipped.push({ file: rel, reason: "absent-in-primary" });
|
|
9115
9370
|
continue;
|
|
9116
9371
|
}
|
|
9117
|
-
copyFile((0,
|
|
9372
|
+
copyFile((0, import_node_path12.join)(primary, rel), dest);
|
|
9118
9373
|
copied.push(rel);
|
|
9119
9374
|
log(`copied local config: ${rel}`);
|
|
9120
9375
|
}
|
|
9121
9376
|
return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
|
|
9122
9377
|
}
|
|
9378
|
+
function capWorktreeDirName(name, max = 40) {
|
|
9379
|
+
if (name.length <= max) return name;
|
|
9380
|
+
const hard = name.slice(0, max);
|
|
9381
|
+
const lastDash = hard.lastIndexOf("-");
|
|
9382
|
+
return (lastDash >= max - 12 ? hard.slice(0, lastDash) : hard).replace(/-+$/, "");
|
|
9383
|
+
}
|
|
9123
9384
|
function defaultWorktreePath(repoRoot2, branch) {
|
|
9124
|
-
const safe = branch.replace(/[/\\]+/g, "-");
|
|
9125
|
-
return (0,
|
|
9385
|
+
const safe = capWorktreeDirName(branch.replace(/[/\\]+/g, "-"));
|
|
9386
|
+
return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path12.basename)(repoRoot2), safe);
|
|
9126
9387
|
}
|
|
9127
9388
|
async function primaryCheckoutRootOf(git) {
|
|
9128
9389
|
try {
|
|
9129
9390
|
const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
|
|
9130
|
-
return out ? (0,
|
|
9391
|
+
return out ? (0, import_node_path12.dirname)(out) : void 0;
|
|
9131
9392
|
} catch {
|
|
9132
9393
|
return void 0;
|
|
9133
9394
|
}
|
|
@@ -9183,7 +9444,15 @@ async function resolveWhoami(deps) {
|
|
|
9183
9444
|
} catch {
|
|
9184
9445
|
session = void 0;
|
|
9185
9446
|
}
|
|
9186
|
-
if (session?.login)
|
|
9447
|
+
if (session?.login) {
|
|
9448
|
+
return {
|
|
9449
|
+
login: session.login,
|
|
9450
|
+
source: "hub-session",
|
|
9451
|
+
sessionExpiresAt: session.expiresAt,
|
|
9452
|
+
// Exact-match, and only here. The `github` branch below deliberately never carries a role.
|
|
9453
|
+
...session.role === "owner" ? { role: "owner" } : {}
|
|
9454
|
+
};
|
|
9455
|
+
}
|
|
9187
9456
|
let ghLogin;
|
|
9188
9457
|
try {
|
|
9189
9458
|
ghLogin = await deps.ghLogin();
|
|
@@ -9198,6 +9467,10 @@ function whoamiLine(report, caveat) {
|
|
|
9198
9467
|
const line = `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.`;
|
|
9199
9468
|
return caveat?.trim() ? `${line} ${caveat.trim()}` : line;
|
|
9200
9469
|
}
|
|
9470
|
+
function authorityLine(report) {
|
|
9471
|
+
if (report.source !== "hub-session" || report.role !== "owner" || !report.login) return null;
|
|
9472
|
+
return `full authority: ${report.login} owns the org \u2014 doctrine, board, roles, matrix, skills and process are his to decide; never ask whether a change is his to make. Secret VALUES are still never echoed.`;
|
|
9473
|
+
}
|
|
9201
9474
|
|
|
9202
9475
|
// src/command-ladder-hint.ts
|
|
9203
9476
|
var COMMAND_LADDER_WRITES = [
|
|
@@ -9233,7 +9506,7 @@ function commandLadderHint() {
|
|
|
9233
9506
|
}
|
|
9234
9507
|
|
|
9235
9508
|
// src/index.ts
|
|
9236
|
-
var
|
|
9509
|
+
var import_node_path30 = require("node:path");
|
|
9237
9510
|
|
|
9238
9511
|
// src/merge-ci-policy.ts
|
|
9239
9512
|
function resolveMergeCiPolicy(input) {
|
|
@@ -9321,16 +9594,16 @@ function pollStateFromParsed(parsed) {
|
|
|
9321
9594
|
}
|
|
9322
9595
|
var PR_MERGEABLE_UNKNOWN_POLL_RETRIES = 4;
|
|
9323
9596
|
var PR_MERGEABLE_UNKNOWN_POLL_MS = 3e3;
|
|
9324
|
-
async function resolveSettledMergeableState(pollMergeable,
|
|
9597
|
+
async function resolveSettledMergeableState(pollMergeable, sleep3) {
|
|
9325
9598
|
for (let attempt = 1; attempt <= PR_MERGEABLE_UNKNOWN_POLL_RETRIES; attempt++) {
|
|
9326
9599
|
const state = await pollMergeable();
|
|
9327
9600
|
if (state !== "UNKNOWN") return state;
|
|
9328
|
-
if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await
|
|
9601
|
+
if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await sleep3(PR_MERGEABLE_UNKNOWN_POLL_MS);
|
|
9329
9602
|
}
|
|
9330
9603
|
return "UNKNOWN";
|
|
9331
9604
|
}
|
|
9332
9605
|
function conflictingPrMessage(baseBranch) {
|
|
9333
|
-
return `PR is CONFLICTING with ${baseBranch} \u2014 GitHub will not queue checks until the conflict is resolved. Resolve with: git fetch origin ${baseBranch} && git
|
|
9606
|
+
return `PR is CONFLICTING with ${baseBranch} \u2014 GitHub will not queue checks until the conflict is resolved. Resolve with: git fetch origin ${baseBranch} && git merge origin/${baseBranch} (merge, not rebase \u2014 a pushed branch cannot be rebased without a force-push, which doctrine forbids)`;
|
|
9334
9607
|
}
|
|
9335
9608
|
function conflictingResult(policy, baseBranch, waitedMs) {
|
|
9336
9609
|
return { policy, status: "conflicting", reason: conflictingPrMessage(baseBranch), detail: "conflicting", waitedMs };
|
|
@@ -9686,6 +9959,12 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9686
9959
|
// and `.cursor/rules/` because a repo may want its rules tracked despite the wall.
|
|
9687
9960
|
".codex/",
|
|
9688
9961
|
".agents/",
|
|
9962
|
+
// #3525: `jerv-cli fusion attest record` writes `.jerv/attest/<head>.attest.json` INTO the worktree,
|
|
9963
|
+
// so every worktree that has ever passed the fusion gate reports `?? .jerv/` forever. `worktree land`
|
|
9964
|
+
// refuses to clean a dirty worktree, which made the attestation artifact the gate requires the exact
|
|
9965
|
+
// thing blocking the cleanup that gate's own worktree needs. Org-universal: every repo runs the same
|
|
9966
|
+
// gate, so this belongs in the managed block rather than in one repo's own ignores.
|
|
9967
|
+
".jerv/",
|
|
9689
9968
|
// #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<RepoName>/<branch>` (outside the tree,
|
|
9690
9969
|
// via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
|
|
9691
9970
|
// doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
|
|
@@ -9755,6 +10034,112 @@ function planManagedGitignore(current) {
|
|
|
9755
10034
|
return { changed, content, added, removed };
|
|
9756
10035
|
}
|
|
9757
10036
|
|
|
10037
|
+
// src/docs-index-command.ts
|
|
10038
|
+
var import_node_fs15 = require("node:fs");
|
|
10039
|
+
var import_node_path13 = require("node:path");
|
|
10040
|
+
var DOCS_INDEX_PATH = "docs/index.md";
|
|
10041
|
+
function isRoutableDocsPath(relPath) {
|
|
10042
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
10043
|
+
return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
|
|
10044
|
+
}
|
|
10045
|
+
var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
|
|
10046
|
+
function plainInline(text) {
|
|
10047
|
+
return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
|
|
10048
|
+
}
|
|
10049
|
+
function extractDocMeta(relPath, raw) {
|
|
10050
|
+
const lines = raw.split(/\r?\n/);
|
|
10051
|
+
let i = 0;
|
|
10052
|
+
if (lines[i]?.trim() === "---") {
|
|
10053
|
+
i++;
|
|
10054
|
+
while (i < lines.length && lines[i].trim() !== "---") i++;
|
|
10055
|
+
if (i < lines.length) i++;
|
|
10056
|
+
}
|
|
10057
|
+
let title = "";
|
|
10058
|
+
let scope = "";
|
|
10059
|
+
for (; i < lines.length; i++) {
|
|
10060
|
+
const line = lines[i];
|
|
10061
|
+
const trimmed = line.trim();
|
|
10062
|
+
if (trimmed === "") continue;
|
|
10063
|
+
if (trimmed.startsWith("<!--")) {
|
|
10064
|
+
while (i < lines.length && !lines[i].includes("-->")) i++;
|
|
10065
|
+
continue;
|
|
10066
|
+
}
|
|
10067
|
+
if (!title && trimmed.startsWith("# ")) {
|
|
10068
|
+
title = trimmed.slice(2).trim();
|
|
10069
|
+
continue;
|
|
10070
|
+
}
|
|
10071
|
+
if (!title) {
|
|
10072
|
+
break;
|
|
10073
|
+
}
|
|
10074
|
+
if (trimmed.startsWith("#")) continue;
|
|
10075
|
+
scope = plainInline(trimmed);
|
|
10076
|
+
break;
|
|
10077
|
+
}
|
|
10078
|
+
if (!title) {
|
|
10079
|
+
const base = relPath.split("/").pop() ?? relPath;
|
|
10080
|
+
title = base.replace(/\.md$/, "");
|
|
10081
|
+
}
|
|
10082
|
+
return { path: relPath, title, scope };
|
|
10083
|
+
}
|
|
10084
|
+
function renderDocsIndex(entries) {
|
|
10085
|
+
const lines = [
|
|
10086
|
+
GENERATED_HEADER,
|
|
10087
|
+
"",
|
|
10088
|
+
"# Documentation index",
|
|
10089
|
+
"",
|
|
10090
|
+
"Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
|
|
10091
|
+
"can never diverge from the records it lists.",
|
|
10092
|
+
""
|
|
10093
|
+
];
|
|
10094
|
+
for (const entry of entries) {
|
|
10095
|
+
const link = `[${plainInline(entry.title)}](${entry.path})`;
|
|
10096
|
+
lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
|
|
10097
|
+
}
|
|
10098
|
+
return lines.join("\n") + "\n";
|
|
10099
|
+
}
|
|
10100
|
+
function sameContent(a, b) {
|
|
10101
|
+
const normalize = (text) => text.replace(/\r\n/g, "\n");
|
|
10102
|
+
return normalize(a) === normalize(b);
|
|
10103
|
+
}
|
|
10104
|
+
function docsIndex(deps, opts = {}) {
|
|
10105
|
+
const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
|
|
10106
|
+
const content = renderDocsIndex(entries);
|
|
10107
|
+
const current = deps.readIndex();
|
|
10108
|
+
const drift = current === null || !sameContent(current, content);
|
|
10109
|
+
let wrote = false;
|
|
10110
|
+
if (!opts.check && drift) {
|
|
10111
|
+
deps.writeIndex(content);
|
|
10112
|
+
wrote = true;
|
|
10113
|
+
}
|
|
10114
|
+
return { content, drift, wrote };
|
|
10115
|
+
}
|
|
10116
|
+
function walkMarkdown(dir) {
|
|
10117
|
+
const out = [];
|
|
10118
|
+
const stack = [dir];
|
|
10119
|
+
while (stack.length) {
|
|
10120
|
+
const current = stack.pop();
|
|
10121
|
+
for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
|
|
10122
|
+
const full = (0, import_node_path13.join)(current, entry.name);
|
|
10123
|
+
if (entry.isDirectory()) {
|
|
10124
|
+
stack.push(full);
|
|
10125
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
10126
|
+
out.push((0, import_node_path13.relative)(dir, full).split(import_node_path13.sep).join("/"));
|
|
10127
|
+
}
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
return out;
|
|
10131
|
+
}
|
|
10132
|
+
function createDocsIndexDeps(repoRoot2) {
|
|
10133
|
+
const docsDir = (0, import_node_path13.join)(repoRoot2, "docs");
|
|
10134
|
+
const indexPath = (0, import_node_path13.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
10135
|
+
return {
|
|
10136
|
+
listDocs: () => (0, import_node_fs15.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
|
|
10137
|
+
readDoc: (relPath) => (0, import_node_fs15.readFileSync)((0, import_node_path13.join)(docsDir, relPath), "utf8"),
|
|
10138
|
+
readIndex: () => (0, import_node_fs15.existsSync)(indexPath) ? (0, import_node_fs15.readFileSync)(indexPath, "utf8") : null,
|
|
10139
|
+
writeIndex: (content) => (0, import_node_fs15.writeFileSync)(indexPath, content, "utf8")
|
|
10140
|
+
};
|
|
10141
|
+
}
|
|
10142
|
+
|
|
9758
10143
|
// src/gate-budget.ts
|
|
9759
10144
|
var BLESSED_RUN_WITH_BUDGET_SHA = "ea2cf8710949dec53566c29836e29cd7515a4e23";
|
|
9760
10145
|
var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
|
|
@@ -10001,6 +10386,7 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
|
|
|
10001
10386
|
out.REPO_NAME ??= parsed.name;
|
|
10002
10387
|
out.REPO_SLUG ??= parsed.slug;
|
|
10003
10388
|
out.CLASS ??= cls;
|
|
10389
|
+
out.PROJECT_OWNER ??= parsed.owner;
|
|
10004
10390
|
const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
|
|
10005
10391
|
const runtime = out.GATE_RUNTIME === "python" ? "python" : "node";
|
|
10006
10392
|
for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime))) {
|
|
@@ -10079,10 +10465,10 @@ function labelsToPrune(orgLabelNames) {
|
|
|
10079
10465
|
const org = new Set(orgLabelNames);
|
|
10080
10466
|
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
10081
10467
|
}
|
|
10082
|
-
function resolveSeedContent(seed, vars,
|
|
10083
|
-
if (seed.source === "self") return
|
|
10468
|
+
function resolveSeedContent(seed, vars, readFile7) {
|
|
10469
|
+
if (seed.source === "self") return readFile7(seed.target);
|
|
10084
10470
|
if (seed.source.startsWith("seed:")) {
|
|
10085
|
-
const tmpl =
|
|
10471
|
+
const tmpl = readFile7(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
10086
10472
|
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
10087
10473
|
}
|
|
10088
10474
|
return null;
|
|
@@ -10201,6 +10587,26 @@ function boardFieldsQueryArgs(projectId) {
|
|
|
10201
10587
|
const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
|
|
10202
10588
|
return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
|
|
10203
10589
|
}
|
|
10590
|
+
function seededDocsIndex(seeds) {
|
|
10591
|
+
const docs2 = seeds.map((s) => ({ rel: s.path.replace(/\\/g, "/"), content: s.content })).filter((s) => s.rel.startsWith("docs/") && s.rel.endsWith(".md")).map((s) => ({ rel: s.rel.slice("docs/".length), content: s.content })).filter((s) => isRoutableDocsPath(s.rel));
|
|
10592
|
+
if (!docs2.length) return null;
|
|
10593
|
+
const entries = docs2.slice().sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0).map((s) => extractDocMeta(s.rel, s.content));
|
|
10594
|
+
return renderDocsIndex(entries);
|
|
10595
|
+
}
|
|
10596
|
+
function linkedProjectsQueryArgs(owner, name) {
|
|
10597
|
+
const query = "query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { projectsV2(first: 10) { nodes { id number title } } } }";
|
|
10598
|
+
return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
|
|
10599
|
+
}
|
|
10600
|
+
function pickLinkedProject(json) {
|
|
10601
|
+
const nodes = json?.data?.repository?.projectsV2?.nodes;
|
|
10602
|
+
if (!Array.isArray(nodes)) return void 0;
|
|
10603
|
+
const usable = nodes.filter(
|
|
10604
|
+
(n) => typeof n?.id === "string" && Boolean(n.id)
|
|
10605
|
+
);
|
|
10606
|
+
if (usable.length !== 1) return void 0;
|
|
10607
|
+
const only = usable[0];
|
|
10608
|
+
return typeof only.number === "number" && Number.isFinite(only.number) ? { id: only.id, number: only.number } : { id: only.id };
|
|
10609
|
+
}
|
|
10204
10610
|
function decideSeedPrAction(openPrs) {
|
|
10205
10611
|
const list = Array.isArray(openPrs) ? openPrs : [];
|
|
10206
10612
|
for (const pr2 of list) {
|
|
@@ -10748,8 +11154,8 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
|
10748
11154
|
if (first.mergeStatus !== "failed") return first;
|
|
10749
11155
|
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
|
|
10750
11156
|
if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
|
|
10751
|
-
const
|
|
10752
|
-
await
|
|
11157
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
11158
|
+
await sleep3(PR_LAND_MERGE_RETRY_DELAY_MS);
|
|
10753
11159
|
const retried = await deps.mergeAuto(prNumber, repo);
|
|
10754
11160
|
if (retried.mergeStatus !== "failed") return retried;
|
|
10755
11161
|
return { mergeStatus: "failed", error: `merge retry after transient failure also failed: ${retried.error ?? first.error ?? "unknown error"}` };
|
|
@@ -10759,13 +11165,13 @@ var AUTO_MERGE_CONFIRM_DELAY_MS = 3e3;
|
|
|
10759
11165
|
async function confirmAutoMergeEnqueued(deps, options) {
|
|
10760
11166
|
const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
|
|
10761
11167
|
const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
|
|
10762
|
-
const
|
|
11168
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
10763
11169
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
10764
11170
|
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
10765
11171
|
const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
|
|
10766
11172
|
if (stuck) return "enqueued";
|
|
10767
11173
|
if (attempt < retries - 1) {
|
|
10768
|
-
await
|
|
11174
|
+
await sleep3(delayMs);
|
|
10769
11175
|
await deps.reEnqueue().catch(() => {
|
|
10770
11176
|
});
|
|
10771
11177
|
}
|
|
@@ -10776,7 +11182,7 @@ async function confirmAutoMergeEnqueued(deps, options) {
|
|
|
10776
11182
|
async function readGhPrStateWithRetry(fetchState, options) {
|
|
10777
11183
|
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
10778
11184
|
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
10779
|
-
const
|
|
11185
|
+
const sleep3 = options?.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
10780
11186
|
let lastError = "empty state";
|
|
10781
11187
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
10782
11188
|
try {
|
|
@@ -10786,7 +11192,7 @@ async function readGhPrStateWithRetry(fetchState, options) {
|
|
|
10786
11192
|
} catch (e) {
|
|
10787
11193
|
lastError = String(e.message || "gh pr view failed");
|
|
10788
11194
|
}
|
|
10789
|
-
if (attempt < retries - 1) await
|
|
11195
|
+
if (attempt < retries - 1) await sleep3(delayMs);
|
|
10790
11196
|
}
|
|
10791
11197
|
return { ok: false, error: lastError };
|
|
10792
11198
|
}
|
|
@@ -10849,12 +11255,12 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
10849
11255
|
}
|
|
10850
11256
|
|
|
10851
11257
|
// src/wave-status.ts
|
|
10852
|
-
var
|
|
11258
|
+
var import_node_fs17 = require("node:fs");
|
|
10853
11259
|
|
|
10854
11260
|
// src/stage-runner.ts
|
|
10855
11261
|
var import_node_child_process7 = require("node:child_process");
|
|
10856
|
-
var
|
|
10857
|
-
var
|
|
11262
|
+
var import_node_fs16 = require("node:fs");
|
|
11263
|
+
var import_node_path14 = require("node:path");
|
|
10858
11264
|
var import_node_net = require("node:net");
|
|
10859
11265
|
var import_node_util6 = require("node:util");
|
|
10860
11266
|
|
|
@@ -10993,11 +11399,11 @@ function appendForceRecreate(up) {
|
|
|
10993
11399
|
return `${up.trimEnd()} --force-recreate`;
|
|
10994
11400
|
}
|
|
10995
11401
|
function stageStatePath(cwd = process.cwd()) {
|
|
10996
|
-
return (0,
|
|
11402
|
+
return (0, import_node_path14.join)(cwd, "tmp", "stage", "state.json");
|
|
10997
11403
|
}
|
|
10998
11404
|
function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
|
|
10999
|
-
const dir = (0,
|
|
11000
|
-
return (0,
|
|
11405
|
+
const dir = (0, import_node_path14.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path14.resolve)(cwd, gitCommonDir);
|
|
11406
|
+
return (0, import_node_path14.join)(dir, "mmi", "stage", "state.json");
|
|
11001
11407
|
}
|
|
11002
11408
|
function normPath2(path2) {
|
|
11003
11409
|
return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
@@ -11221,14 +11627,14 @@ function stageProcessEnv(stagePort, extraEnv) {
|
|
|
11221
11627
|
}
|
|
11222
11628
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
11223
11629
|
if (!config.ensureEnv) return;
|
|
11224
|
-
const target = (0,
|
|
11225
|
-
const example = (0,
|
|
11226
|
-
if (!(0,
|
|
11227
|
-
(0,
|
|
11228
|
-
} else if ((0,
|
|
11229
|
-
const stale = detectStaleEnvFile((0,
|
|
11230
|
-
exampleMtimeMs: (0,
|
|
11231
|
-
targetMtimeMs: (0,
|
|
11630
|
+
const target = (0, import_node_path14.join)(cwd, config.ensureEnv.target);
|
|
11631
|
+
const example = (0, import_node_path14.join)(cwd, config.ensureEnv.example);
|
|
11632
|
+
if (!(0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
|
|
11633
|
+
(0, import_node_fs16.copyFileSync)(example, target);
|
|
11634
|
+
} else if ((0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
|
|
11635
|
+
const stale = detectStaleEnvFile((0, import_node_fs16.readFileSync)(example, "utf8"), (0, import_node_fs16.readFileSync)(target, "utf8"), {
|
|
11636
|
+
exampleMtimeMs: (0, import_node_fs16.statSync)(example).mtimeMs,
|
|
11637
|
+
targetMtimeMs: (0, import_node_fs16.statSync)(target).mtimeMs
|
|
11232
11638
|
});
|
|
11233
11639
|
if (stale) {
|
|
11234
11640
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -11236,8 +11642,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
|
11236
11642
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
11237
11643
|
}
|
|
11238
11644
|
}
|
|
11239
|
-
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0,
|
|
11240
|
-
(0,
|
|
11645
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs16.existsSync)(target)) {
|
|
11646
|
+
(0, import_node_fs16.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs16.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
11241
11647
|
}
|
|
11242
11648
|
}
|
|
11243
11649
|
async function gitText(cwd, args) {
|
|
@@ -11267,20 +11673,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
|
|
|
11267
11673
|
return void 0;
|
|
11268
11674
|
}
|
|
11269
11675
|
function readState(path2) {
|
|
11270
|
-
if (!(0,
|
|
11676
|
+
if (!(0, import_node_fs16.existsSync)(path2)) return null;
|
|
11271
11677
|
try {
|
|
11272
|
-
return JSON.parse((0,
|
|
11678
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
|
|
11273
11679
|
} catch {
|
|
11274
11680
|
return null;
|
|
11275
11681
|
}
|
|
11276
11682
|
}
|
|
11277
11683
|
function mkdirFor(path2) {
|
|
11278
11684
|
const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
|
|
11279
|
-
(0,
|
|
11685
|
+
(0, import_node_fs16.mkdirSync)(dir, { recursive: true });
|
|
11280
11686
|
}
|
|
11281
11687
|
function writeState(path2, state) {
|
|
11282
11688
|
mkdirFor(path2);
|
|
11283
|
-
(0,
|
|
11689
|
+
(0, import_node_fs16.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
|
|
11284
11690
|
}
|
|
11285
11691
|
function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
11286
11692
|
const reservation = {
|
|
@@ -11300,7 +11706,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
|
|
|
11300
11706
|
await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
|
|
11301
11707
|
}
|
|
11302
11708
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
11303
|
-
(0,
|
|
11709
|
+
(0, import_node_fs16.rmSync)(path2, { force: true });
|
|
11304
11710
|
}
|
|
11305
11711
|
}
|
|
11306
11712
|
async function killTree(pid) {
|
|
@@ -11458,8 +11864,8 @@ async function runStage(config = {}, opts = {}) {
|
|
|
11458
11864
|
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
11459
11865
|
if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
|
|
11460
11866
|
} catch (e) {
|
|
11461
|
-
(0,
|
|
11462
|
-
if (globalStatePath && globalStatePath !== statePath) (0,
|
|
11867
|
+
(0, import_node_fs16.rmSync)(statePath, { force: true });
|
|
11868
|
+
if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs16.rmSync)(globalStatePath, { force: true });
|
|
11463
11869
|
throw e;
|
|
11464
11870
|
}
|
|
11465
11871
|
const started = await startStage(config, {
|
|
@@ -11480,9 +11886,9 @@ function parseNextFromHead(headText) {
|
|
|
11480
11886
|
}
|
|
11481
11887
|
function readStageSummary(worktreePath) {
|
|
11482
11888
|
const statePath = stageStatePath(worktreePath);
|
|
11483
|
-
if (!(0,
|
|
11889
|
+
if (!(0, import_node_fs17.existsSync)(statePath)) return void 0;
|
|
11484
11890
|
try {
|
|
11485
|
-
const state = JSON.parse((0,
|
|
11891
|
+
const state = JSON.parse((0, import_node_fs17.readFileSync)(statePath, "utf8"));
|
|
11486
11892
|
const port = typeof state.port === "number" ? state.port : void 0;
|
|
11487
11893
|
if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
|
|
11488
11894
|
return { port, url: typeof state.url === "string" ? state.url : void 0 };
|
|
@@ -11567,7 +11973,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
|
|
|
11567
11973
|
}
|
|
11568
11974
|
|
|
11569
11975
|
// src/index.ts
|
|
11570
|
-
var
|
|
11976
|
+
var import_node_os9 = require("node:os");
|
|
11571
11977
|
|
|
11572
11978
|
// src/attach-to-project.ts
|
|
11573
11979
|
async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
|
|
@@ -11601,7 +12007,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
11601
12007
|
// src/gh-create.ts
|
|
11602
12008
|
var import_promises = require("node:fs/promises");
|
|
11603
12009
|
var import_node_os3 = require("node:os");
|
|
11604
|
-
var
|
|
12010
|
+
var import_node_path15 = require("node:path");
|
|
11605
12011
|
var import_node_crypto3 = require("node:crypto");
|
|
11606
12012
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
11607
12013
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
@@ -11655,8 +12061,8 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
11655
12061
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
11656
12062
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
11657
12063
|
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
11658
|
-
const file = (0,
|
|
11659
|
-
await ensureDir((0,
|
|
12064
|
+
const file = (0, import_node_path15.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
12065
|
+
await ensureDir((0, import_node_path15.dirname)(file), { recursive: true }).catch(() => {
|
|
11660
12066
|
});
|
|
11661
12067
|
await write(file, args[i + 1], "utf8");
|
|
11662
12068
|
return {
|
|
@@ -11723,23 +12129,61 @@ function parseExistingPr(stderr) {
|
|
|
11723
12129
|
if (!match) return void 0;
|
|
11724
12130
|
return { number: Number(match[1]), url: match[0] };
|
|
11725
12131
|
}
|
|
11726
|
-
|
|
12132
|
+
var GH_CREATE_UPSTREAM_RETRIES = 3;
|
|
12133
|
+
var GH_CREATE_RETRY_BACKOFF_MS = [2e3, 5e3];
|
|
12134
|
+
function httpStatusCodes(stderr) {
|
|
12135
|
+
const out = [];
|
|
12136
|
+
for (const m of stderr.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) out.push(Number(m[1]));
|
|
12137
|
+
for (const m of stderr.matchAll(/\b(\d{3})\s+Internal Server Error\b/g)) out.push(Number(m[1]));
|
|
12138
|
+
return out;
|
|
12139
|
+
}
|
|
12140
|
+
function isUpstreamGitHubFault(stderr) {
|
|
12141
|
+
const codes = httpStatusCodes(stderr);
|
|
12142
|
+
if (codes.some((c) => c >= 400 && c < 500)) return false;
|
|
12143
|
+
if (codes.some((c) => c >= 500 && c < 600)) return true;
|
|
12144
|
+
return /Something went wrong while executing your query/.test(stderr) || /^\s*unexpected end of JSON input\s*$/m.test(stderr);
|
|
12145
|
+
}
|
|
12146
|
+
function mayRetryCreate(verb) {
|
|
12147
|
+
return verb === "pr";
|
|
12148
|
+
}
|
|
12149
|
+
function upstreamFaultMessage(verb, stderr) {
|
|
12150
|
+
const requestId = /\b([0-9A-F]{4}:[0-9A-F]{4,6}:[0-9A-F]+:[0-9A-F]+:[0-9A-F]+)\b/.exec(stderr)?.[1];
|
|
12151
|
+
const advice = mayRetryCreate(verb) ? "Retrying is the right action" : "The write may have completed server-side before the error \u2014 VERIFY before retrying, or a retry will duplicate it";
|
|
12152
|
+
return `gh ${verb} create failed: GitHub's API is failing (server-side 5xx) \u2014 this is NOT a problem with your diff or arguments. GitHub request ID: ${requestId ?? "unavailable"}. ${advice}; note githubstatus.com may still read green while this is happening.`;
|
|
12153
|
+
}
|
|
12154
|
+
async function ghCreate(args, deps = {}) {
|
|
12155
|
+
const exec = deps.exec ?? execFileP2;
|
|
12156
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
11727
12157
|
const swapped = await bodyArgsViaFile(args);
|
|
11728
12158
|
try {
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
12159
|
+
for (let attempt = 1; attempt <= GH_CREATE_UPSTREAM_RETRIES; attempt++) {
|
|
12160
|
+
try {
|
|
12161
|
+
const { stdout } = await exec("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
12162
|
+
return parseCreatedUrl(stdout);
|
|
12163
|
+
} catch (e) {
|
|
12164
|
+
const err = e;
|
|
12165
|
+
const errText = `${err.stderr ?? ""}
|
|
12166
|
+
${err.message ?? ""}`;
|
|
12167
|
+
const faultText = (err.stderr ?? "").trim() ? err.stderr : err.message ?? "";
|
|
12168
|
+
const existing = args[0] === "pr" ? parseExistingPr(errText) : void 0;
|
|
12169
|
+
if (existing) {
|
|
12170
|
+
await swapped.cleanup();
|
|
12171
|
+
console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
|
|
12172
|
+
return { ...existing, existing: true };
|
|
12173
|
+
}
|
|
12174
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
12175
|
+
if (isUpstreamGitHubFault(faultText) && mayRetryCreate(args[0]) && attempt < GH_CREATE_UPSTREAM_RETRIES) {
|
|
12176
|
+
const waitMs = GH_CREATE_RETRY_BACKOFF_MS[attempt - 1];
|
|
12177
|
+
console.warn(`gh ${args[0]} create: GitHub API server fault (attempt ${attempt}/${GH_CREATE_UPSTREAM_RETRIES}) \u2014 retrying in ${waitMs}ms`);
|
|
12178
|
+
await sleep3(waitMs);
|
|
12179
|
+
continue;
|
|
12180
|
+
}
|
|
12181
|
+
await swapped.cleanup();
|
|
12182
|
+
if (isUpstreamGitHubFault(faultText)) return fail(upstreamFaultMessage(args[0], faultText));
|
|
12183
|
+
return fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
12184
|
+
}
|
|
11739
12185
|
}
|
|
11740
|
-
|
|
11741
|
-
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
11742
|
-
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
12186
|
+
throw new Error("ghCreate: retry loop exhausted without a verdict");
|
|
11743
12187
|
} finally {
|
|
11744
12188
|
await swapped.cleanup();
|
|
11745
12189
|
}
|
|
@@ -12893,8 +13337,8 @@ function parseVerifyBroker(stdout) {
|
|
|
12893
13337
|
}
|
|
12894
13338
|
|
|
12895
13339
|
// src/train-apply.ts
|
|
12896
|
-
var
|
|
12897
|
-
var
|
|
13340
|
+
var import_node_fs18 = require("node:fs");
|
|
13341
|
+
var import_node_path16 = require("node:path");
|
|
12898
13342
|
function resolveDeployModel2(meta, repo) {
|
|
12899
13343
|
const m = meta?.deployModel;
|
|
12900
13344
|
if (isDeployModel(m)) return m;
|
|
@@ -13003,7 +13447,7 @@ function mergeTreeConflictFiles(stdout) {
|
|
|
13003
13447
|
return stdout.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
|
|
13004
13448
|
}
|
|
13005
13449
|
async function runMergeTreePreflight(deps, ours, theirs) {
|
|
13006
|
-
const
|
|
13450
|
+
const sleep3 = resolveSleep(deps);
|
|
13007
13451
|
let lastError;
|
|
13008
13452
|
for (let attempt = 0; attempt < MERGE_TREE_PREFLIGHT_ATTEMPTS; attempt++) {
|
|
13009
13453
|
try {
|
|
@@ -13013,7 +13457,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
|
|
|
13013
13457
|
lastError = e;
|
|
13014
13458
|
const files = mergeTreeConflictFiles(String(e.stdout ?? ""));
|
|
13015
13459
|
if (files.length > 0) return files;
|
|
13016
|
-
if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await
|
|
13460
|
+
if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await sleep3(MERGE_TREE_PREFLIGHT_DELAY_MS);
|
|
13017
13461
|
}
|
|
13018
13462
|
}
|
|
13019
13463
|
const msg = lastError?.message ?? String(lastError);
|
|
@@ -13217,7 +13661,7 @@ function isPersistentGitPushFailure(e) {
|
|
|
13217
13661
|
return /non-fast-forward|\[rejected\]|failed to push|protected branch|hook declined|permission denied|could not read Username|authentication failed|Authentication failed|403 Forbidden|GH006|GH013/i.test(msg);
|
|
13218
13662
|
}
|
|
13219
13663
|
async function runGitWithRetry(deps, args, shouldRetry) {
|
|
13220
|
-
const
|
|
13664
|
+
const sleep3 = resolveSleep(deps);
|
|
13221
13665
|
let lastError;
|
|
13222
13666
|
for (let attempt = 1; attempt <= GIT_NETWORK_ATTEMPTS; attempt++) {
|
|
13223
13667
|
try {
|
|
@@ -13225,7 +13669,7 @@ async function runGitWithRetry(deps, args, shouldRetry) {
|
|
|
13225
13669
|
} catch (e) {
|
|
13226
13670
|
lastError = e;
|
|
13227
13671
|
if (attempt >= GIT_NETWORK_ATTEMPTS || !shouldRetry(e)) throw e;
|
|
13228
|
-
await
|
|
13672
|
+
await sleep3(GIT_NETWORK_RETRY_DELAY_MS * attempt);
|
|
13229
13673
|
}
|
|
13230
13674
|
}
|
|
13231
13675
|
throw lastError;
|
|
@@ -13246,13 +13690,13 @@ var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project
|
|
|
13246
13690
|
var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
|
|
13247
13691
|
var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
|
|
13248
13692
|
async function correlateRun(deps, args) {
|
|
13249
|
-
const
|
|
13693
|
+
const sleep3 = resolveSleep(deps);
|
|
13250
13694
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
13251
13695
|
const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO3 : HUB_REPO3;
|
|
13252
13696
|
let lastError;
|
|
13253
13697
|
let parsedAnyResponse = false;
|
|
13254
13698
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
13255
|
-
if (attempt > 0) await
|
|
13699
|
+
if (attempt > 0) await sleep3(CORRELATE_DELAY_MS);
|
|
13256
13700
|
const listArgs = [
|
|
13257
13701
|
"run",
|
|
13258
13702
|
"list",
|
|
@@ -13445,13 +13889,13 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
13445
13889
|
const t = Date.parse(String(r.startedAt ?? ""));
|
|
13446
13890
|
return !Number.isFinite(t) || t >= freshThreshold;
|
|
13447
13891
|
};
|
|
13448
|
-
const
|
|
13892
|
+
const sleep3 = resolveSleep(deps);
|
|
13449
13893
|
let lastStatus = "not checked";
|
|
13450
13894
|
let lastError;
|
|
13451
13895
|
const everObserved = /* @__PURE__ */ new Set();
|
|
13452
13896
|
const autoSatisfied = /* @__PURE__ */ new Set();
|
|
13453
13897
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
13454
|
-
if (attempt > 0) await
|
|
13898
|
+
if (attempt > 0) await sleep3(TRAIN_CHECK_DELAY_MS);
|
|
13455
13899
|
let checkRuns;
|
|
13456
13900
|
let statuses;
|
|
13457
13901
|
try {
|
|
@@ -13585,14 +14029,14 @@ function isTransientDispatchFailure(e) {
|
|
|
13585
14029
|
return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
|
|
13586
14030
|
}
|
|
13587
14031
|
async function dispatchTenantDeployWithRetry(deps, input) {
|
|
13588
|
-
const
|
|
14032
|
+
const sleep3 = resolveSleep(deps);
|
|
13589
14033
|
for (let attempt = 1; ; attempt++) {
|
|
13590
14034
|
try {
|
|
13591
14035
|
await deps.dispatchTenantDeploy(input);
|
|
13592
14036
|
return;
|
|
13593
14037
|
} catch (e) {
|
|
13594
14038
|
if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
|
|
13595
|
-
await
|
|
14039
|
+
await sleep3(DISPATCH_RETRY_DELAY_MS * attempt);
|
|
13596
14040
|
}
|
|
13597
14041
|
}
|
|
13598
14042
|
}
|
|
@@ -13612,9 +14056,9 @@ var PUBLISH_LOG_RETRY_ATTEMPTS = 4;
|
|
|
13612
14056
|
var PUBLISH_LOG_RETRY_DELAY_MS = 6e3;
|
|
13613
14057
|
async function reconcilePublishFailure(deps, runId) {
|
|
13614
14058
|
if (runId == null) return "failure";
|
|
13615
|
-
const
|
|
14059
|
+
const sleep3 = resolveSleep(deps);
|
|
13616
14060
|
for (let attempt = 0; attempt < PUBLISH_LOG_RETRY_ATTEMPTS; attempt++) {
|
|
13617
|
-
if (attempt > 0) await
|
|
14061
|
+
if (attempt > 0) await sleep3(PUBLISH_LOG_RETRY_DELAY_MS);
|
|
13618
14062
|
const log = await fetchControlRunLog(deps, runId);
|
|
13619
14063
|
if (PUBLISH_IDEMPOTENT_MARKER.test(log)) return "success";
|
|
13620
14064
|
}
|
|
@@ -13802,17 +14246,17 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
13802
14246
|
return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
|
|
13803
14247
|
}
|
|
13804
14248
|
function readLocalGateWorkflows() {
|
|
13805
|
-
const dir = (0,
|
|
14249
|
+
const dir = (0, import_node_path16.join)(".github", "workflows");
|
|
13806
14250
|
let names;
|
|
13807
14251
|
try {
|
|
13808
|
-
names = (0,
|
|
14252
|
+
names = (0, import_node_fs18.readdirSync)(dir);
|
|
13809
14253
|
} catch {
|
|
13810
14254
|
return null;
|
|
13811
14255
|
}
|
|
13812
14256
|
const files = [];
|
|
13813
14257
|
for (const name of names.filter(isGateWorkflowPath)) {
|
|
13814
14258
|
try {
|
|
13815
|
-
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0,
|
|
14259
|
+
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, name), "utf8") });
|
|
13816
14260
|
} catch {
|
|
13817
14261
|
}
|
|
13818
14262
|
}
|
|
@@ -14408,13 +14852,13 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
14408
14852
|
note: `origin/rc moved past the released candidate (${releasedRcSha.slice(0, 7)} -> ${rcNow.slice(0, 7)}) \u2014 a new candidate is in flight; rc runtime left untouched`
|
|
14409
14853
|
};
|
|
14410
14854
|
}
|
|
14411
|
-
const
|
|
14855
|
+
const sleep3 = resolveSleep(deps);
|
|
14412
14856
|
let last;
|
|
14413
14857
|
for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
|
|
14414
14858
|
last = await attemptRetire(deps, ctx);
|
|
14415
14859
|
if (last.status === "retired") return last;
|
|
14416
14860
|
if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
|
|
14417
|
-
await
|
|
14861
|
+
await sleep3(RETIRE_BACKOFF_MS * attempt);
|
|
14418
14862
|
}
|
|
14419
14863
|
const f = last;
|
|
14420
14864
|
if (f.category === "wait-timeout") return f;
|
|
@@ -14945,9 +15389,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
|
|
|
14945
15389
|
return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
|
|
14946
15390
|
}
|
|
14947
15391
|
async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
14948
|
-
const
|
|
15392
|
+
const sleep3 = sleeper(deps);
|
|
14949
15393
|
for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
|
|
14950
|
-
if (attempt > 0) await
|
|
15394
|
+
if (attempt > 0) await sleep3(HOTFIX_RUN_FIND_DELAY_MS);
|
|
14951
15395
|
let rows;
|
|
14952
15396
|
try {
|
|
14953
15397
|
const out = await deps.run("gh", [
|
|
@@ -15085,7 +15529,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
15085
15529
|
try {
|
|
15086
15530
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
15087
15531
|
const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
|
|
15088
|
-
const
|
|
15532
|
+
const sleep3 = sleeper(deps);
|
|
15089
15533
|
let attempt = 0;
|
|
15090
15534
|
for (; ; ) {
|
|
15091
15535
|
attempt++;
|
|
@@ -15094,7 +15538,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
15094
15538
|
break;
|
|
15095
15539
|
} catch (err) {
|
|
15096
15540
|
if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
|
|
15097
|
-
await
|
|
15541
|
+
await sleep3(HOTFIX_VERIFY_RETRY_MS);
|
|
15098
15542
|
}
|
|
15099
15543
|
}
|
|
15100
15544
|
const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
|
|
@@ -15708,116 +16152,10 @@ function renderAccessReport(report) {
|
|
|
15708
16152
|
return lines.join("\n");
|
|
15709
16153
|
}
|
|
15710
16154
|
|
|
15711
|
-
// src/docs-index-command.ts
|
|
15712
|
-
var import_node_fs17 = require("node:fs");
|
|
15713
|
-
var import_node_path15 = require("node:path");
|
|
15714
|
-
var DOCS_INDEX_PATH = "docs/index.md";
|
|
15715
|
-
function isRoutableDocsPath(relPath) {
|
|
15716
|
-
const normalized = relPath.replace(/\\/g, "/");
|
|
15717
|
-
return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
|
|
15718
|
-
}
|
|
15719
|
-
var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
|
|
15720
|
-
function plainInline(text) {
|
|
15721
|
-
return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
|
|
15722
|
-
}
|
|
15723
|
-
function extractDocMeta(relPath, raw) {
|
|
15724
|
-
const lines = raw.split(/\r?\n/);
|
|
15725
|
-
let i = 0;
|
|
15726
|
-
if (lines[i]?.trim() === "---") {
|
|
15727
|
-
i++;
|
|
15728
|
-
while (i < lines.length && lines[i].trim() !== "---") i++;
|
|
15729
|
-
if (i < lines.length) i++;
|
|
15730
|
-
}
|
|
15731
|
-
let title = "";
|
|
15732
|
-
let scope = "";
|
|
15733
|
-
for (; i < lines.length; i++) {
|
|
15734
|
-
const line = lines[i];
|
|
15735
|
-
const trimmed = line.trim();
|
|
15736
|
-
if (trimmed === "") continue;
|
|
15737
|
-
if (trimmed.startsWith("<!--")) {
|
|
15738
|
-
while (i < lines.length && !lines[i].includes("-->")) i++;
|
|
15739
|
-
continue;
|
|
15740
|
-
}
|
|
15741
|
-
if (!title && trimmed.startsWith("# ")) {
|
|
15742
|
-
title = trimmed.slice(2).trim();
|
|
15743
|
-
continue;
|
|
15744
|
-
}
|
|
15745
|
-
if (!title) {
|
|
15746
|
-
break;
|
|
15747
|
-
}
|
|
15748
|
-
if (trimmed.startsWith("#")) continue;
|
|
15749
|
-
scope = plainInline(trimmed);
|
|
15750
|
-
break;
|
|
15751
|
-
}
|
|
15752
|
-
if (!title) {
|
|
15753
|
-
const base = relPath.split("/").pop() ?? relPath;
|
|
15754
|
-
title = base.replace(/\.md$/, "");
|
|
15755
|
-
}
|
|
15756
|
-
return { path: relPath, title, scope };
|
|
15757
|
-
}
|
|
15758
|
-
function renderDocsIndex(entries) {
|
|
15759
|
-
const lines = [
|
|
15760
|
-
GENERATED_HEADER,
|
|
15761
|
-
"",
|
|
15762
|
-
"# Documentation index",
|
|
15763
|
-
"",
|
|
15764
|
-
"Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
|
|
15765
|
-
"can never diverge from the records it lists.",
|
|
15766
|
-
""
|
|
15767
|
-
];
|
|
15768
|
-
for (const entry of entries) {
|
|
15769
|
-
const link = `[${plainInline(entry.title)}](${entry.path})`;
|
|
15770
|
-
lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
|
|
15771
|
-
}
|
|
15772
|
-
return lines.join("\n") + "\n";
|
|
15773
|
-
}
|
|
15774
|
-
function sameContent(a, b) {
|
|
15775
|
-
const normalize = (text) => text.replace(/\r\n/g, "\n");
|
|
15776
|
-
return normalize(a) === normalize(b);
|
|
15777
|
-
}
|
|
15778
|
-
function docsIndex(deps, opts = {}) {
|
|
15779
|
-
const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
|
|
15780
|
-
const content = renderDocsIndex(entries);
|
|
15781
|
-
const current = deps.readIndex();
|
|
15782
|
-
const drift = current === null || !sameContent(current, content);
|
|
15783
|
-
let wrote = false;
|
|
15784
|
-
if (!opts.check && drift) {
|
|
15785
|
-
deps.writeIndex(content);
|
|
15786
|
-
wrote = true;
|
|
15787
|
-
}
|
|
15788
|
-
return { content, drift, wrote };
|
|
15789
|
-
}
|
|
15790
|
-
function walkMarkdown(dir) {
|
|
15791
|
-
const out = [];
|
|
15792
|
-
const stack = [dir];
|
|
15793
|
-
while (stack.length) {
|
|
15794
|
-
const current = stack.pop();
|
|
15795
|
-
for (const entry of (0, import_node_fs17.readdirSync)(current, { withFileTypes: true })) {
|
|
15796
|
-
const full = (0, import_node_path15.join)(current, entry.name);
|
|
15797
|
-
if (entry.isDirectory()) {
|
|
15798
|
-
stack.push(full);
|
|
15799
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
15800
|
-
out.push((0, import_node_path15.relative)(dir, full).split(import_node_path15.sep).join("/"));
|
|
15801
|
-
}
|
|
15802
|
-
}
|
|
15803
|
-
}
|
|
15804
|
-
return out;
|
|
15805
|
-
}
|
|
15806
|
-
function createDocsIndexDeps(repoRoot2) {
|
|
15807
|
-
const docsDir = (0, import_node_path15.join)(repoRoot2, "docs");
|
|
15808
|
-
const indexPath = (0, import_node_path15.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
15809
|
-
return {
|
|
15810
|
-
listDocs: () => (0, import_node_fs17.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
|
|
15811
|
-
readDoc: (relPath) => (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(docsDir, relPath), "utf8"),
|
|
15812
|
-
readIndex: () => (0, import_node_fs17.existsSync)(indexPath) ? (0, import_node_fs17.readFileSync)(indexPath, "utf8") : null,
|
|
15813
|
-
writeIndex: (content) => (0, import_node_fs17.writeFileSync)(indexPath, content, "utf8")
|
|
15814
|
-
};
|
|
15815
|
-
}
|
|
15816
|
-
|
|
15817
16155
|
// src/doc-refs-core.ts
|
|
15818
16156
|
var import_node_child_process9 = require("node:child_process");
|
|
15819
|
-
var
|
|
15820
|
-
var
|
|
16157
|
+
var import_node_fs19 = require("node:fs");
|
|
16158
|
+
var import_node_path17 = require("node:path");
|
|
15821
16159
|
var PIN_RE = /<!--\s*pinned by\s+([^:]+?)\s*:\s*"([^"]+)"[^"]*-->/;
|
|
15822
16160
|
var PIN_MENTION_RE = /<!--\s*pinned by\b/;
|
|
15823
16161
|
var REF_RE = /`([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)(?::\d+)?`/g;
|
|
@@ -15851,7 +16189,7 @@ function extractPins(markdown) {
|
|
|
15851
16189
|
});
|
|
15852
16190
|
return pins;
|
|
15853
16191
|
}
|
|
15854
|
-
function checkPins(root,
|
|
16192
|
+
function checkPins(root, readFile7, docs2) {
|
|
15855
16193
|
const findings = [];
|
|
15856
16194
|
for (const [doc, markdown] of Object.entries(docs2)) {
|
|
15857
16195
|
for (const pin of extractPins(markdown)) {
|
|
@@ -15859,7 +16197,7 @@ function checkPins(root, readFile6, docs2) {
|
|
|
15859
16197
|
findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
|
|
15860
16198
|
continue;
|
|
15861
16199
|
}
|
|
15862
|
-
const source =
|
|
16200
|
+
const source = readFile7((0, import_node_path17.join)(root, pin.file));
|
|
15863
16201
|
if (source == null) {
|
|
15864
16202
|
findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
|
|
15865
16203
|
continue;
|
|
@@ -15920,17 +16258,17 @@ function checkRefs(root, deps, docs2) {
|
|
|
15920
16258
|
for (const [doc, markdown] of Object.entries(docs2)) {
|
|
15921
16259
|
for (const { ref, line } of extractRefs(markdown)) {
|
|
15922
16260
|
const first = ref.split("/")[0];
|
|
15923
|
-
if (!exists((0,
|
|
15924
|
-
if (!exists((0,
|
|
16261
|
+
if (!exists((0, import_node_path17.join)(root, first))) continue;
|
|
16262
|
+
if (!exists((0, import_node_path17.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
|
|
15925
16263
|
}
|
|
15926
|
-
const docDir =
|
|
16264
|
+
const docDir = import_node_path17.posix.dirname(doc);
|
|
15927
16265
|
for (const { target, line } of extractLinks(markdown)) {
|
|
15928
|
-
const resolved =
|
|
16266
|
+
const resolved = import_node_path17.posix.normalize(import_node_path17.posix.join(docDir === "." ? "" : docDir, target));
|
|
15929
16267
|
if (resolved.startsWith("..")) {
|
|
15930
16268
|
candidates.push({ kind: "missing-link", doc, line, detail: target, escapesRoot: true });
|
|
15931
16269
|
continue;
|
|
15932
16270
|
}
|
|
15933
|
-
if (!exists((0,
|
|
16271
|
+
if (!exists((0, import_node_path17.join)(root, resolved))) {
|
|
15934
16272
|
candidates.push({ kind: "missing-link", doc, line, detail: target, resolved });
|
|
15935
16273
|
}
|
|
15936
16274
|
}
|
|
@@ -15964,22 +16302,22 @@ function checkCommands(docs2, commandPaths) {
|
|
|
15964
16302
|
return { ok: findings.length === 0, findings };
|
|
15965
16303
|
}
|
|
15966
16304
|
function readFileOrNull(path2) {
|
|
15967
|
-
return (0,
|
|
16305
|
+
return (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null;
|
|
15968
16306
|
}
|
|
15969
16307
|
function walk(dir, root, out) {
|
|
15970
|
-
for (const entry of (0,
|
|
15971
|
-
const full = (0,
|
|
15972
|
-
if ((0,
|
|
16308
|
+
for (const entry of (0, import_node_fs19.readdirSync)(dir)) {
|
|
16309
|
+
const full = (0, import_node_path17.join)(dir, entry);
|
|
16310
|
+
if ((0, import_node_fs19.statSync)(full).isDirectory()) walk(full, root, out);
|
|
15973
16311
|
else if (entry.endsWith(".md")) out.push(full.slice(root.length + 1).replaceAll("\\", "/"));
|
|
15974
16312
|
}
|
|
15975
16313
|
return out;
|
|
15976
16314
|
}
|
|
15977
16315
|
function defaultListDocs(root) {
|
|
15978
|
-
const docsDir = (0,
|
|
15979
|
-
const docs2 = ((0,
|
|
16316
|
+
const docsDir = (0, import_node_path17.join)(root, "docs");
|
|
16317
|
+
const docs2 = ((0, import_node_fs19.existsSync)(docsDir) ? walk(docsDir, root, []) : []).filter(
|
|
15980
16318
|
(rel) => !SKIP_WALK.some((skip) => rel.startsWith(skip))
|
|
15981
16319
|
);
|
|
15982
|
-
return [...ROOT_DOCS.filter((rel) => (0,
|
|
16320
|
+
return [...ROOT_DOCS.filter((rel) => (0, import_node_fs19.existsSync)((0, import_node_path17.join)(root, rel))), ...docs2];
|
|
15983
16321
|
}
|
|
15984
16322
|
function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.execFileSync) {
|
|
15985
16323
|
const inRepo = relPaths.filter((p) => !p.startsWith(".."));
|
|
@@ -15997,16 +16335,16 @@ function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.exec
|
|
|
15997
16335
|
}
|
|
15998
16336
|
}
|
|
15999
16337
|
function runDocRefs(root, deps = {}) {
|
|
16000
|
-
const
|
|
16001
|
-
const exists = deps.exists ??
|
|
16338
|
+
const readFile7 = deps.readFile ?? readFileOrNull;
|
|
16339
|
+
const exists = deps.exists ?? import_node_fs19.existsSync;
|
|
16002
16340
|
const listDocs = deps.listDocs ?? defaultListDocs;
|
|
16003
16341
|
const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
|
|
16004
16342
|
const commandPaths = deps.commandPaths ?? null;
|
|
16005
16343
|
const docs2 = Object.fromEntries(
|
|
16006
|
-
listDocs(root).map((rel) => [rel,
|
|
16344
|
+
listDocs(root).map((rel) => [rel, readFile7((0, import_node_path17.join)(root, rel))]).filter(([, body]) => body != null)
|
|
16007
16345
|
);
|
|
16008
16346
|
const findings = [
|
|
16009
|
-
...checkPins(root,
|
|
16347
|
+
...checkPins(root, readFile7, docs2).findings,
|
|
16010
16348
|
...checkRefs(root, { exists, isIgnored }, docs2).findings
|
|
16011
16349
|
];
|
|
16012
16350
|
if (commandPaths == null) {
|
|
@@ -16078,7 +16416,28 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
16078
16416
|
if (!fetch2.ok) {
|
|
16079
16417
|
return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
|
|
16080
16418
|
}
|
|
16419
|
+
if (!isValidIsoDate(opts.today)) {
|
|
16420
|
+
throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
|
|
16421
|
+
}
|
|
16081
16422
|
if (fetch2.verdict === null) {
|
|
16423
|
+
const armedAt = opts.armedAt;
|
|
16424
|
+
if (armedAt && isValidIsoDate(armedAt)) {
|
|
16425
|
+
const cadenceDays = opts.cadenceDays ?? 7;
|
|
16426
|
+
const graceDays = opts.graceDays ?? 3;
|
|
16427
|
+
const sinceArmed = ageInDays(armedAt, opts.today);
|
|
16428
|
+
if (sinceArmed <= cadenceDays + graceDays) {
|
|
16429
|
+
return {
|
|
16430
|
+
ok: true,
|
|
16431
|
+
state: "awaiting-first-tick",
|
|
16432
|
+
line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
|
|
16433
|
+
};
|
|
16434
|
+
}
|
|
16435
|
+
return {
|
|
16436
|
+
ok: false,
|
|
16437
|
+
state: "missing",
|
|
16438
|
+
line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
|
|
16439
|
+
};
|
|
16440
|
+
}
|
|
16082
16441
|
return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
|
|
16083
16442
|
}
|
|
16084
16443
|
const verdict = fetch2.verdict;
|
|
@@ -16107,9 +16466,6 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
16107
16466
|
line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
|
|
16108
16467
|
};
|
|
16109
16468
|
}
|
|
16110
|
-
if (!isValidIsoDate(opts.today)) {
|
|
16111
|
-
throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
|
|
16112
|
-
}
|
|
16113
16469
|
const cadence = opts.cadenceDays ?? 7;
|
|
16114
16470
|
const grace = opts.graceDays ?? 3;
|
|
16115
16471
|
const age = ageInDays(verdict.date, opts.today);
|
|
@@ -16945,8 +17301,8 @@ function writeError(res) {
|
|
|
16945
17301
|
}
|
|
16946
17302
|
|
|
16947
17303
|
// src/secrets-commands.ts
|
|
16948
|
-
var
|
|
16949
|
-
var
|
|
17304
|
+
var import_node_fs20 = require("node:fs");
|
|
17305
|
+
var import_node_path18 = require("node:path");
|
|
16950
17306
|
var import_node_os5 = require("node:os");
|
|
16951
17307
|
|
|
16952
17308
|
// src/project-runtime.ts
|
|
@@ -17063,25 +17419,25 @@ var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
|
|
|
17063
17419
|
var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
|
|
17064
17420
|
function resolvePreflightRepoScope(optRepo, cwdRepo) {
|
|
17065
17421
|
const targetRepo2 = optRepo ? optRepo.includes("/") ? optRepo : `mutmutco/${optRepo}` : cwdRepo;
|
|
17066
|
-
const
|
|
17067
|
-
return { targetRepo: targetRepo2, sameRepo };
|
|
17422
|
+
const sameRepo2 = !optRepo || Boolean(cwdRepo) && Boolean(targetRepo2) && cwdRepo.toLowerCase() === targetRepo2.toLowerCase();
|
|
17423
|
+
return { targetRepo: targetRepo2, sameRepo: sameRepo2 };
|
|
17068
17424
|
}
|
|
17069
17425
|
function collectMap(value, previous = []) {
|
|
17070
17426
|
return [...previous, value];
|
|
17071
17427
|
}
|
|
17072
17428
|
async function decryptRailsCredentials(input) {
|
|
17073
|
-
const appDir = (0,
|
|
17429
|
+
const appDir = (0, import_node_path18.resolve)(input.appDir ?? process.cwd());
|
|
17074
17430
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
17075
17431
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
17076
|
-
const credentialsPath = (0,
|
|
17077
|
-
const masterKeyPath = (0,
|
|
17432
|
+
const credentialsPath = (0, import_node_path18.resolve)(appDir, credentialsFile);
|
|
17433
|
+
const masterKeyPath = (0, import_node_path18.resolve)(appDir, masterKeyFile);
|
|
17078
17434
|
const env = {
|
|
17079
17435
|
...process.env,
|
|
17080
17436
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
17081
17437
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
17082
17438
|
};
|
|
17083
|
-
if ((0,
|
|
17084
|
-
env.RAILS_MASTER_KEY = (0,
|
|
17439
|
+
if ((0, import_node_fs20.existsSync)(masterKeyPath)) {
|
|
17440
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs20.readFileSync)(masterKeyPath, "utf8").trim();
|
|
17085
17441
|
}
|
|
17086
17442
|
const script = [
|
|
17087
17443
|
'require "json"',
|
|
@@ -17091,9 +17447,9 @@ async function decryptRailsCredentials(input) {
|
|
|
17091
17447
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
17092
17448
|
"puts JSON.generate(config.config)"
|
|
17093
17449
|
].join("\n");
|
|
17094
|
-
const scriptDir = (0,
|
|
17095
|
-
const scriptPath = (0,
|
|
17096
|
-
(0,
|
|
17450
|
+
const scriptDir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
|
|
17451
|
+
const scriptPath = (0, import_node_path18.join)(scriptDir, "decrypt.rb");
|
|
17452
|
+
(0, import_node_fs20.writeFileSync)(scriptPath, script, "utf8");
|
|
17097
17453
|
try {
|
|
17098
17454
|
const args = ["exec", "ruby", scriptPath];
|
|
17099
17455
|
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
@@ -17105,7 +17461,7 @@ async function decryptRailsCredentials(input) {
|
|
|
17105
17461
|
});
|
|
17106
17462
|
return JSON.parse(stdout);
|
|
17107
17463
|
} finally {
|
|
17108
|
-
(0,
|
|
17464
|
+
(0, import_node_fs20.rmSync)(scriptDir, { recursive: true, force: true });
|
|
17109
17465
|
}
|
|
17110
17466
|
}
|
|
17111
17467
|
async function readSecretStdin() {
|
|
@@ -17195,7 +17551,7 @@ function registerSecretsCommands(program3) {
|
|
|
17195
17551
|
let body;
|
|
17196
17552
|
if (o.file) {
|
|
17197
17553
|
try {
|
|
17198
|
-
body = (0,
|
|
17554
|
+
body = (0, import_node_fs20.readFileSync)((0, import_node_path18.resolve)(o.file), "utf8");
|
|
17199
17555
|
} catch (e) {
|
|
17200
17556
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
17201
17557
|
}
|
|
@@ -17229,11 +17585,11 @@ function registerSecretsCommands(program3) {
|
|
|
17229
17585
|
const stage = o.stage;
|
|
17230
17586
|
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
|
|
17231
17587
|
const cwdRepo = repoFromRemoteUrl((await execFileP2("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
|
|
17232
|
-
const { sameRepo } = resolvePreflightRepoScope(o.repo, cwdRepo);
|
|
17588
|
+
const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
|
|
17233
17589
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
17234
17590
|
const fact = facts?.stages?.[stage] ?? null;
|
|
17235
|
-
const composeText =
|
|
17236
|
-
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(
|
|
17591
|
+
const composeText = sameRepo2 ? await readStageBranchCompose(branch) : null;
|
|
17592
|
+
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo2), force: false, repo });
|
|
17237
17593
|
if (!verdict.ok) {
|
|
17238
17594
|
d.err(`secrets preflight: ${verdict.reason}`);
|
|
17239
17595
|
filelessOk = false;
|
|
@@ -17300,7 +17656,7 @@ function registerSecretsCommands(program3) {
|
|
|
17300
17656
|
{
|
|
17301
17657
|
...d,
|
|
17302
17658
|
decryptRailsCredentials,
|
|
17303
|
-
removeFile: (path2) => (0,
|
|
17659
|
+
removeFile: (path2) => (0, import_node_fs20.unlinkSync)((0, import_node_path18.resolve)(o.appDir ?? process.cwd(), path2))
|
|
17304
17660
|
},
|
|
17305
17661
|
{
|
|
17306
17662
|
repo: o.repo,
|
|
@@ -17474,7 +17830,7 @@ function checkGithubPools(probe) {
|
|
|
17474
17830
|
}
|
|
17475
17831
|
|
|
17476
17832
|
// src/box-commands.ts
|
|
17477
|
-
var
|
|
17833
|
+
var import_node_fs21 = require("node:fs");
|
|
17478
17834
|
|
|
17479
17835
|
// src/box.ts
|
|
17480
17836
|
var BOX_KEYS = {
|
|
@@ -17677,7 +18033,7 @@ function registerBoxCommands(program3) {
|
|
|
17677
18033
|
}
|
|
17678
18034
|
if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
|
|
17679
18035
|
else if (o.ssh && o.script) {
|
|
17680
|
-
(0,
|
|
18036
|
+
(0, import_node_fs21.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
|
|
17681
18037
|
console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
|
|
17682
18038
|
} else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
17683
18039
|
${SSH_RECIPE_AGENT_NOTE}`);
|
|
@@ -17775,7 +18131,7 @@ function lambdaTargetFromArn(arn) {
|
|
|
17775
18131
|
}
|
|
17776
18132
|
function awsScheduleEntry(payload) {
|
|
17777
18133
|
if (!payload || typeof payload !== "object") return null;
|
|
17778
|
-
const { Name, ScheduleExpression, State, Target, Description } = payload;
|
|
18134
|
+
const { Name, ScheduleExpression, State, Target, Description, CreationDate } = payload;
|
|
17779
18135
|
if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
|
|
17780
18136
|
if (State !== "ENABLED" || isJervResource(Name)) return null;
|
|
17781
18137
|
let target = "";
|
|
@@ -17798,6 +18154,7 @@ function awsScheduleEntry(payload) {
|
|
|
17798
18154
|
}
|
|
17799
18155
|
const sourceBase = `aws scheduler schedule ${Name} (eu-central-1)`;
|
|
17800
18156
|
const source = qualifier && target ? `${sourceBase} \u2192 ${target}:${qualifier}` : sourceBase;
|
|
18157
|
+
const armedAt = armedAtFromCreationDate(CreationDate);
|
|
17801
18158
|
return {
|
|
17802
18159
|
name: Name,
|
|
17803
18160
|
cadence: ScheduleExpression,
|
|
@@ -17805,9 +18162,25 @@ function awsScheduleEntry(payload) {
|
|
|
17805
18162
|
llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
|
|
17806
18163
|
resolved: "live",
|
|
17807
18164
|
source,
|
|
17808
|
-
...scheduleId ? { scheduleId } : {}
|
|
18165
|
+
...scheduleId ? { scheduleId } : {},
|
|
18166
|
+
...armedAt ? { armedAt } : {}
|
|
17809
18167
|
};
|
|
17810
18168
|
}
|
|
18169
|
+
function armedAtFromCreationDate(creationDate) {
|
|
18170
|
+
if (creationDate instanceof Date) {
|
|
18171
|
+
return Number.isNaN(creationDate.getTime()) ? void 0 : creationDate.toISOString().slice(0, 10);
|
|
18172
|
+
}
|
|
18173
|
+
if (typeof creationDate === "string" && creationDate.trim()) {
|
|
18174
|
+
const parsed = Date.parse(creationDate);
|
|
18175
|
+
return Number.isNaN(parsed) ? void 0 : new Date(parsed).toISOString().slice(0, 10);
|
|
18176
|
+
}
|
|
18177
|
+
if (typeof creationDate === "number" && Number.isFinite(creationDate)) {
|
|
18178
|
+
const ms = creationDate > 1e12 ? creationDate : creationDate * 1e3;
|
|
18179
|
+
const date = new Date(ms);
|
|
18180
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString().slice(0, 10);
|
|
18181
|
+
}
|
|
18182
|
+
return void 0;
|
|
18183
|
+
}
|
|
17811
18184
|
function awsScheduleRefs(payload) {
|
|
17812
18185
|
if (!payload || typeof payload !== "object") return [];
|
|
17813
18186
|
const { Schedules: schedules } = payload;
|
|
@@ -17873,6 +18246,12 @@ function spliceDoc(docText, generatedSection) {
|
|
|
17873
18246
|
}
|
|
17874
18247
|
return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
|
|
17875
18248
|
}
|
|
18249
|
+
function driftClearableFrom(drift, repoName) {
|
|
18250
|
+
if (drift.class !== "file-vs-registry-stale") return false;
|
|
18251
|
+
const slash = drift.name.indexOf("/");
|
|
18252
|
+
if (slash <= 0) return false;
|
|
18253
|
+
return drift.name.slice(0, slash).toLowerCase() === repoName.toLowerCase();
|
|
18254
|
+
}
|
|
17876
18255
|
var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["cursor-agent"]);
|
|
17877
18256
|
function isHarbourLlmLauncher(executor) {
|
|
17878
18257
|
return HARBOUR_LLM_LAUNCHERS.has(executor);
|
|
@@ -18253,9 +18632,105 @@ function registerSchedulesCommands(program3) {
|
|
|
18253
18632
|
});
|
|
18254
18633
|
}
|
|
18255
18634
|
|
|
18256
|
-
// src/
|
|
18635
|
+
// src/file-lock.ts
|
|
18257
18636
|
var import_promises3 = require("node:fs/promises");
|
|
18258
|
-
var
|
|
18637
|
+
var import_node_path19 = require("node:path");
|
|
18638
|
+
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
18639
|
+
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
18640
|
+
var FileLockBusyError = class extends Error {
|
|
18641
|
+
lockPath;
|
|
18642
|
+
constructor(label, lockPath, maxWaitMs) {
|
|
18643
|
+
super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
|
|
18644
|
+
this.name = "FileLockBusyError";
|
|
18645
|
+
this.lockPath = lockPath;
|
|
18646
|
+
}
|
|
18647
|
+
};
|
|
18648
|
+
function resolveFileLockOpts(opts = {}) {
|
|
18649
|
+
return {
|
|
18650
|
+
staleMs: opts.staleMs ?? 3e4,
|
|
18651
|
+
retryMs: opts.retryMs ?? 50,
|
|
18652
|
+
maxWaitMs: opts.maxWaitMs ?? 5e3,
|
|
18653
|
+
label: opts.label ?? "file lock"
|
|
18654
|
+
};
|
|
18655
|
+
}
|
|
18656
|
+
async function acquireFileLock(lockPath, opts, deadline) {
|
|
18657
|
+
let immediateRetries = 0;
|
|
18658
|
+
for (; ; ) {
|
|
18659
|
+
let handle;
|
|
18660
|
+
try {
|
|
18661
|
+
handle = await (0, import_promises3.open)(lockPath, "wx");
|
|
18662
|
+
} catch (e) {
|
|
18663
|
+
const code = e.code;
|
|
18664
|
+
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
18665
|
+
const retryNow = async () => {
|
|
18666
|
+
if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
18667
|
+
if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
|
|
18668
|
+
};
|
|
18669
|
+
try {
|
|
18670
|
+
const age = Date.now() - (await (0, import_promises3.stat)(lockPath)).mtimeMs;
|
|
18671
|
+
if (age > opts.staleMs) {
|
|
18672
|
+
try {
|
|
18673
|
+
await (0, import_promises3.unlink)(lockPath);
|
|
18674
|
+
await retryNow();
|
|
18675
|
+
continue;
|
|
18676
|
+
} catch (unlinkError) {
|
|
18677
|
+
if (unlinkError instanceof FileLockBusyError) throw unlinkError;
|
|
18678
|
+
const unlinkCode = unlinkError.code;
|
|
18679
|
+
if (unlinkCode === "ENOENT") {
|
|
18680
|
+
await retryNow();
|
|
18681
|
+
continue;
|
|
18682
|
+
}
|
|
18683
|
+
}
|
|
18684
|
+
}
|
|
18685
|
+
} catch (statError) {
|
|
18686
|
+
if (statError instanceof FileLockBusyError) throw statError;
|
|
18687
|
+
if (statError.code !== "ENOENT") throw statError;
|
|
18688
|
+
await retryNow();
|
|
18689
|
+
continue;
|
|
18690
|
+
}
|
|
18691
|
+
if (Date.now() >= deadline) {
|
|
18692
|
+
throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
18693
|
+
}
|
|
18694
|
+
await sleep(opts.retryMs);
|
|
18695
|
+
continue;
|
|
18696
|
+
}
|
|
18697
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
18698
|
+
try {
|
|
18699
|
+
await handle.writeFile(token);
|
|
18700
|
+
} catch (e) {
|
|
18701
|
+
await handle.close().catch(() => void 0);
|
|
18702
|
+
throw e;
|
|
18703
|
+
}
|
|
18704
|
+
return { token, handle };
|
|
18705
|
+
}
|
|
18706
|
+
}
|
|
18707
|
+
async function fileLockHeldBy(lockPath, token) {
|
|
18708
|
+
try {
|
|
18709
|
+
return await (0, import_promises3.readFile)(lockPath, "utf8") === token;
|
|
18710
|
+
} catch {
|
|
18711
|
+
return false;
|
|
18712
|
+
}
|
|
18713
|
+
}
|
|
18714
|
+
async function releaseFileLock(lockPath, guard) {
|
|
18715
|
+
await guard.handle.close().catch(() => void 0);
|
|
18716
|
+
if (await fileLockHeldBy(lockPath, guard.token)) {
|
|
18717
|
+
await (0, import_promises3.unlink)(lockPath).catch(() => void 0);
|
|
18718
|
+
}
|
|
18719
|
+
}
|
|
18720
|
+
async function withFileLock(lockPath, opts, fn) {
|
|
18721
|
+
const resolved = resolveFileLockOpts(opts);
|
|
18722
|
+
await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
18723
|
+
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
18724
|
+
try {
|
|
18725
|
+
return await fn();
|
|
18726
|
+
} finally {
|
|
18727
|
+
await releaseFileLock(lockPath, guard);
|
|
18728
|
+
}
|
|
18729
|
+
}
|
|
18730
|
+
|
|
18731
|
+
// src/schedules-lift-command.ts
|
|
18732
|
+
var import_promises4 = require("node:fs/promises");
|
|
18733
|
+
var import_node_path20 = require("node:path");
|
|
18259
18734
|
|
|
18260
18735
|
// src/schedules-lift.ts
|
|
18261
18736
|
var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
|
|
@@ -18354,14 +18829,14 @@ var RegistryUnreachableError = class extends Error {
|
|
|
18354
18829
|
async function readWorkflowFiles(dir) {
|
|
18355
18830
|
let names;
|
|
18356
18831
|
try {
|
|
18357
|
-
names = await (0,
|
|
18832
|
+
names = await (0, import_promises4.readdir)(dir);
|
|
18358
18833
|
} catch {
|
|
18359
18834
|
return [];
|
|
18360
18835
|
}
|
|
18361
18836
|
const files = [];
|
|
18362
18837
|
for (const name of names.sort()) {
|
|
18363
18838
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
18364
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0,
|
|
18839
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises4.readFile)((0, import_node_path20.join)(dir, name), "utf8") });
|
|
18365
18840
|
}
|
|
18366
18841
|
return files;
|
|
18367
18842
|
}
|
|
@@ -18903,11 +19378,11 @@ function registerQueryCommands(program3) {
|
|
|
18903
19378
|
}
|
|
18904
19379
|
|
|
18905
19380
|
// src/bootstrap-commands.ts
|
|
18906
|
-
var
|
|
19381
|
+
var import_node_fs22 = require("node:fs");
|
|
18907
19382
|
|
|
18908
19383
|
// src/bootstrap-verify.ts
|
|
18909
19384
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
18910
|
-
var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md"];
|
|
19385
|
+
var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
|
|
18911
19386
|
var requiredIssueTemplates = [
|
|
18912
19387
|
".github/ISSUE_TEMPLATE/bug.yml",
|
|
18913
19388
|
".github/ISSUE_TEMPLATE/feature.yml",
|
|
@@ -19064,6 +19539,11 @@ function unfilledDocPlaceholders(text) {
|
|
|
19064
19539
|
for (const prompt of DOC_PLACEHOLDER_PROMPTS) if (text.includes(prompt)) found.add(prompt);
|
|
19065
19540
|
return [...found];
|
|
19066
19541
|
}
|
|
19542
|
+
function filledDocCheck(label, text, path2) {
|
|
19543
|
+
if (text === null) return { ok: false, label, detail: `${path2} not readable via API` };
|
|
19544
|
+
const unfilled = unfilledDocPlaceholders(text);
|
|
19545
|
+
return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
|
|
19546
|
+
}
|
|
19067
19547
|
async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
19068
19548
|
const branchesWanted = expectedBranches(repoClass, releaseTrack);
|
|
19069
19549
|
const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
|
|
@@ -19137,19 +19617,9 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
19137
19617
|
label: "README has Agent context section",
|
|
19138
19618
|
detail: readme === null ? "README.md not readable via API" : void 0
|
|
19139
19619
|
});
|
|
19140
|
-
|
|
19141
|
-
checks.push({
|
|
19142
|
-
ok: readmePlaceholders.length === 0,
|
|
19143
|
-
label: "README Agent context is filled (no template placeholders)",
|
|
19144
|
-
detail: readmePlaceholders.length ? `unfilled: ${readmePlaceholders.join(", ")}` : void 0
|
|
19145
|
-
});
|
|
19620
|
+
checks.push(filledDocCheck("README Agent context is filled (no template placeholders)", readme, "README.md"));
|
|
19146
19621
|
const architecture = await contentText(deps, repo, baseBranch, "architecture.md");
|
|
19147
|
-
|
|
19148
|
-
checks.push({
|
|
19149
|
-
ok: archPlaceholders.length === 0,
|
|
19150
|
-
label: "architecture.md is filled (no template placeholders)",
|
|
19151
|
-
detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
|
|
19152
|
-
});
|
|
19622
|
+
checks.push(filledDocCheck("architecture.md is filled (no template placeholders)", architecture, "architecture.md"));
|
|
19153
19623
|
const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
|
|
19154
19624
|
const labelNames = new Set(labels.map((l) => l.name));
|
|
19155
19625
|
for (const label of requiredLabels) {
|
|
@@ -19451,7 +19921,7 @@ function registerBootstrapCommands(program3) {
|
|
|
19451
19921
|
client: defaultGitHubClient(),
|
|
19452
19922
|
projectMeta: meta,
|
|
19453
19923
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
19454
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
19924
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
|
|
19455
19925
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
19456
19926
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
19457
19927
|
requiredGcpApis: (() => {
|
|
@@ -19472,7 +19942,11 @@ function registerBootstrapCommands(program3) {
|
|
|
19472
19942
|
return null;
|
|
19473
19943
|
}
|
|
19474
19944
|
}
|
|
19475
|
-
|
|
19945
|
+
// #3537: verify runs BEFORE registration by construction (skill Step 0a), so `meta` is null on every
|
|
19946
|
+
// fresh repo and `resolveReleaseTrack` falls through to `full` — which then outranks the operator's
|
|
19947
|
+
// own `--class content` inside expectedBranches, and the report demands a development and an rc the
|
|
19948
|
+
// track forbids. The declared class fills that gap only when the registry states nothing itself.
|
|
19949
|
+
}, resolveReleaseTrack(metaWithDeclaredClass(meta, o.class), void 0, repo));
|
|
19476
19950
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
19477
19951
|
if (!report.ok) process.exitCode = 1;
|
|
19478
19952
|
});
|
|
@@ -19502,12 +19976,12 @@ function registerBootstrapCommands(program3) {
|
|
|
19502
19976
|
return fail(`bootstrap apply: ${e.message}`);
|
|
19503
19977
|
}
|
|
19504
19978
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
19505
|
-
if (!(0,
|
|
19506
|
-
const manifest = loadBootstrapSeeds((0,
|
|
19979
|
+
if (!(0, import_node_fs22.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
|
|
19980
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
|
|
19507
19981
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
19508
19982
|
const slug = parsedRepo.slug;
|
|
19509
19983
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
19510
|
-
const
|
|
19984
|
+
const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
|
|
19511
19985
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
19512
19986
|
const rawVars = {};
|
|
19513
19987
|
for (const value of cmdOpts.var ?? []) {
|
|
@@ -19520,6 +19994,17 @@ function registerBootstrapCommands(program3) {
|
|
|
19520
19994
|
} catch {
|
|
19521
19995
|
}
|
|
19522
19996
|
const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack);
|
|
19997
|
+
if (!vars.PROJECT_ID) {
|
|
19998
|
+
try {
|
|
19999
|
+
const r = await gh(linkedProjectsQueryArgs(parsedRepo.owner, parsedRepo.name));
|
|
20000
|
+
const linked = pickLinkedProject(JSON.parse(r.stdout));
|
|
20001
|
+
if (linked) {
|
|
20002
|
+
vars.PROJECT_ID = linked.id;
|
|
20003
|
+
if (linked.number != null && vars.PROJECT_NUMBER == null) vars.PROJECT_NUMBER = String(linked.number);
|
|
20004
|
+
}
|
|
20005
|
+
} catch {
|
|
20006
|
+
}
|
|
20007
|
+
}
|
|
19523
20008
|
if (vars.PROJECT_ID) {
|
|
19524
20009
|
try {
|
|
19525
20010
|
const r = await gh(boardFieldsQueryArgs(vars.PROJECT_ID));
|
|
@@ -19561,6 +20046,7 @@ function registerBootstrapCommands(program3) {
|
|
|
19561
20046
|
}
|
|
19562
20047
|
}
|
|
19563
20048
|
}
|
|
20049
|
+
const docsForIndex = [];
|
|
19564
20050
|
for (const seed of manifest.seeds) {
|
|
19565
20051
|
if (!seed.classes.includes(o.class)) continue;
|
|
19566
20052
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
@@ -19585,15 +20071,41 @@ function registerBootstrapCommands(program3) {
|
|
|
19585
20071
|
}
|
|
19586
20072
|
const planned = planSeedAction(resolved, exists);
|
|
19587
20073
|
const isBlock = resolved.source === "managed-block";
|
|
19588
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
20074
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
|
|
19589
20075
|
const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
|
|
19590
20076
|
actions.push(action);
|
|
20077
|
+
const docBody = content ?? remoteContent;
|
|
20078
|
+
if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
|
|
20079
|
+
docsForIndex.push({ path: resolved.target, content: docBody });
|
|
20080
|
+
}
|
|
19591
20081
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
19592
20082
|
await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
|
|
19593
20083
|
applied.push(`${action.action} ${resolved.target}`);
|
|
19594
20084
|
if (seedPlan.mode === "pr") seededToBranch++;
|
|
19595
20085
|
}
|
|
19596
20086
|
}
|
|
20087
|
+
const indexContent = seededDocsIndex(docsForIndex);
|
|
20088
|
+
if (indexContent) {
|
|
20089
|
+
let indexCurrent = null;
|
|
20090
|
+
try {
|
|
20091
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(DOCS_INDEX_PATH)}?ref=${seedPlan.ref}`]);
|
|
20092
|
+
const parsed = JSON.parse(r.stdout);
|
|
20093
|
+
indexCurrent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : "";
|
|
20094
|
+
} catch {
|
|
20095
|
+
}
|
|
20096
|
+
const indexAction = indexCurrent === null ? "create" : "skip";
|
|
20097
|
+
actions.push({
|
|
20098
|
+
target: DOCS_INDEX_PATH,
|
|
20099
|
+
action: indexAction,
|
|
20100
|
+
ownership: "org",
|
|
20101
|
+
reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
|
|
20102
|
+
});
|
|
20103
|
+
if (o.execute && indexAction === "create") {
|
|
20104
|
+
await gh(contentPutArgs(repo, DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0));
|
|
20105
|
+
applied.push(`${indexAction} ${DOCS_INDEX_PATH}`);
|
|
20106
|
+
if (seedPlan.mode === "pr") seededToBranch++;
|
|
20107
|
+
}
|
|
20108
|
+
}
|
|
19597
20109
|
let seedPrUrl;
|
|
19598
20110
|
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
|
|
19599
20111
|
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
|
|
@@ -19640,7 +20152,7 @@ function registerBootstrapCommands(program3) {
|
|
|
19640
20152
|
}
|
|
19641
20153
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
19642
20154
|
if (rulesetSeed) {
|
|
19643
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
20155
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
|
|
19644
20156
|
if (rulesetContent) {
|
|
19645
20157
|
try {
|
|
19646
20158
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -19703,12 +20215,12 @@ LIVE apply to ${repo}:
|
|
|
19703
20215
|
}
|
|
19704
20216
|
|
|
19705
20217
|
// src/stage-commands.ts
|
|
19706
|
-
var
|
|
19707
|
-
var
|
|
20218
|
+
var import_node_fs24 = require("node:fs");
|
|
20219
|
+
var import_node_path22 = require("node:path");
|
|
19708
20220
|
|
|
19709
20221
|
// src/port-registry.ts
|
|
19710
|
-
var
|
|
19711
|
-
var
|
|
20222
|
+
var import_node_fs23 = require("node:fs");
|
|
20223
|
+
var import_node_path21 = require("node:path");
|
|
19712
20224
|
|
|
19713
20225
|
// ../infra/port-geometry.mjs
|
|
19714
20226
|
var PORT_BLOCK = 100;
|
|
@@ -19722,8 +20234,8 @@ function nextPortBlock(registry2) {
|
|
|
19722
20234
|
return [base, base + PORT_SPAN];
|
|
19723
20235
|
}
|
|
19724
20236
|
function loadPortRegistry(path2) {
|
|
19725
|
-
if (!(0,
|
|
19726
|
-
const raw = JSON.parse((0,
|
|
20237
|
+
if (!(0, import_node_fs23.existsSync)(path2)) return {};
|
|
20238
|
+
const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8"));
|
|
19727
20239
|
const out = {};
|
|
19728
20240
|
for (const [key, value] of Object.entries(raw)) {
|
|
19729
20241
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -19737,9 +20249,9 @@ function ensurePortRange(repo, path2) {
|
|
|
19737
20249
|
const existing = registry2[repo];
|
|
19738
20250
|
if (existing) return existing;
|
|
19739
20251
|
const range = nextPortBlock(registry2);
|
|
19740
|
-
const raw = (0,
|
|
20252
|
+
const raw = (0, import_node_fs23.existsSync)(path2) ? JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")) : {};
|
|
19741
20253
|
raw[repo] = range;
|
|
19742
|
-
(0,
|
|
20254
|
+
(0, import_node_fs23.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
19743
20255
|
return range;
|
|
19744
20256
|
}
|
|
19745
20257
|
function portCursorSeed(registry2) {
|
|
@@ -19761,22 +20273,22 @@ function existingPortRange(repo, registry2) {
|
|
|
19761
20273
|
return registry2[repo] ?? null;
|
|
19762
20274
|
}
|
|
19763
20275
|
function portRangeInfraAt(root, source) {
|
|
19764
|
-
const registryPath = (0,
|
|
19765
|
-
const ddbScriptPath = (0,
|
|
19766
|
-
if (!(0,
|
|
20276
|
+
const registryPath = (0, import_node_path21.join)(root, "infra", "port-ranges.json");
|
|
20277
|
+
const ddbScriptPath = (0, import_node_path21.join)(root, "infra", "port-ddb.mjs");
|
|
20278
|
+
if (!(0, import_node_fs23.existsSync)(registryPath) || !(0, import_node_fs23.existsSync)(ddbScriptPath)) return null;
|
|
19767
20279
|
return { root, source, registryPath, ddbScriptPath };
|
|
19768
20280
|
}
|
|
19769
20281
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
19770
20282
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
19771
20283
|
if (direct) return direct;
|
|
19772
|
-
for (let dir = cwd; ; dir = (0,
|
|
19773
|
-
const sibling = portRangeInfraAt((0,
|
|
20284
|
+
for (let dir = cwd; ; dir = (0, import_node_path21.dirname)(dir)) {
|
|
20285
|
+
const sibling = portRangeInfraAt((0, import_node_path21.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
19774
20286
|
if (sibling) return sibling;
|
|
19775
|
-
const parent = (0,
|
|
20287
|
+
const parent = (0, import_node_path21.dirname)(dir);
|
|
19776
20288
|
if (parent === dir) break;
|
|
19777
20289
|
}
|
|
19778
20290
|
if (packageDir) {
|
|
19779
|
-
const pkgRoot = (0,
|
|
20291
|
+
const pkgRoot = (0, import_node_path21.join)(packageDir, "..", "..");
|
|
19780
20292
|
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
19781
20293
|
if (pkgFrom) return pkgFrom;
|
|
19782
20294
|
}
|
|
@@ -19952,8 +20464,8 @@ function registerStageCommands(program3) {
|
|
|
19952
20464
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
19953
20465
|
return decideStage({
|
|
19954
20466
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
19955
|
-
hasCompose: (0,
|
|
19956
|
-
hasEnvExample: (0,
|
|
20467
|
+
hasCompose: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), "docker-compose.yml")),
|
|
20468
|
+
hasEnvExample: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), ".env.example"))
|
|
19957
20469
|
});
|
|
19958
20470
|
}
|
|
19959
20471
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -20388,9 +20900,9 @@ function registerBoardCommands(program3) {
|
|
|
20388
20900
|
}
|
|
20389
20901
|
|
|
20390
20902
|
// src/merge-cleanup.ts
|
|
20391
|
-
var
|
|
20392
|
-
var
|
|
20393
|
-
var
|
|
20903
|
+
var import_node_fs25 = require("node:fs");
|
|
20904
|
+
var import_promises6 = require("node:fs/promises");
|
|
20905
|
+
var import_node_path24 = require("node:path");
|
|
20394
20906
|
var import_node_child_process11 = require("node:child_process");
|
|
20395
20907
|
|
|
20396
20908
|
// src/board-advance.ts
|
|
@@ -20476,117 +20988,64 @@ function boardAdvanceFailureMessage(result) {
|
|
|
20476
20988
|
}
|
|
20477
20989
|
|
|
20478
20990
|
// src/deferred-registry-store.ts
|
|
20479
|
-
var
|
|
20480
|
-
var
|
|
20481
|
-
var
|
|
20991
|
+
var import_promises5 = require("node:fs/promises");
|
|
20992
|
+
var import_node_path23 = require("node:path");
|
|
20993
|
+
var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
20482
20994
|
async function atomicWrite(target, contents) {
|
|
20483
20995
|
const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
20484
|
-
await (0,
|
|
20996
|
+
await (0, import_promises5.writeFile)(tmp, contents, "utf8");
|
|
20485
20997
|
try {
|
|
20486
20998
|
await renameWithRetry(tmp, target);
|
|
20487
20999
|
} catch (e) {
|
|
20488
|
-
await (0,
|
|
21000
|
+
await (0, import_promises5.unlink)(tmp).catch(() => void 0);
|
|
20489
21001
|
throw e;
|
|
20490
21002
|
}
|
|
20491
21003
|
}
|
|
20492
21004
|
async function renameWithRetry(from, to, attempts = 5, backoffMs = 20) {
|
|
20493
21005
|
for (let i = 0; ; i++) {
|
|
20494
21006
|
try {
|
|
20495
|
-
await (0,
|
|
21007
|
+
await (0, import_promises5.rename)(from, to);
|
|
20496
21008
|
return;
|
|
20497
21009
|
} catch (e) {
|
|
20498
21010
|
const code = e.code;
|
|
20499
21011
|
if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") throw e;
|
|
20500
|
-
await
|
|
21012
|
+
await sleep2(backoffMs);
|
|
20501
21013
|
}
|
|
20502
21014
|
}
|
|
20503
21015
|
}
|
|
20504
21016
|
async function readStrict(registryPath) {
|
|
20505
21017
|
let text;
|
|
20506
21018
|
try {
|
|
20507
|
-
text = await (0,
|
|
21019
|
+
text = await (0, import_promises5.readFile)(registryPath, "utf8");
|
|
20508
21020
|
} catch (e) {
|
|
20509
21021
|
if (e.code === "ENOENT") return [];
|
|
20510
21022
|
throw e;
|
|
20511
21023
|
}
|
|
20512
21024
|
return parseDeferredWorktreesFile(text);
|
|
20513
21025
|
}
|
|
20514
|
-
|
|
20515
|
-
|
|
20516
|
-
|
|
20517
|
-
try {
|
|
20518
|
-
handle = await (0, import_promises4.open)(lockPath, "wx");
|
|
20519
|
-
} catch (e) {
|
|
20520
|
-
const code = e.code;
|
|
20521
|
-
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
20522
|
-
try {
|
|
20523
|
-
const age = Date.now() - (await (0, import_promises4.stat)(lockPath)).mtimeMs;
|
|
20524
|
-
if (age > opts.staleMs) {
|
|
20525
|
-
try {
|
|
20526
|
-
await (0, import_promises4.unlink)(lockPath);
|
|
20527
|
-
continue;
|
|
20528
|
-
} catch (unlinkError) {
|
|
20529
|
-
const code2 = unlinkError.code;
|
|
20530
|
-
if (code2 === "ENOENT") continue;
|
|
20531
|
-
}
|
|
20532
|
-
}
|
|
20533
|
-
} catch (statError) {
|
|
20534
|
-
if (statError.code !== "ENOENT") throw statError;
|
|
20535
|
-
continue;
|
|
20536
|
-
}
|
|
20537
|
-
if (Date.now() >= deadline) {
|
|
20538
|
-
throw new Error(`deferred registry lock busy: ${lockPath} held longer than ${opts.maxWaitMs}ms`);
|
|
20539
|
-
}
|
|
20540
|
-
await sleep(opts.retryMs);
|
|
20541
|
-
continue;
|
|
20542
|
-
}
|
|
20543
|
-
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
20544
|
-
try {
|
|
20545
|
-
await handle.writeFile(token);
|
|
20546
|
-
} catch (e) {
|
|
20547
|
-
await handle.close().catch(() => void 0);
|
|
20548
|
-
throw e;
|
|
20549
|
-
}
|
|
20550
|
-
return { token, handle };
|
|
20551
|
-
}
|
|
20552
|
-
}
|
|
20553
|
-
async function lockHeldBy(lockPath, token) {
|
|
20554
|
-
try {
|
|
20555
|
-
return await (0, import_promises4.readFile)(lockPath, "utf8") === token;
|
|
20556
|
-
} catch {
|
|
20557
|
-
return false;
|
|
20558
|
-
}
|
|
20559
|
-
}
|
|
20560
|
-
async function releaseLock(lockPath, guard) {
|
|
20561
|
-
await guard.handle.close().catch(() => void 0);
|
|
20562
|
-
if (await lockHeldBy(lockPath, guard.token)) {
|
|
20563
|
-
await (0, import_promises4.unlink)(lockPath).catch(() => void 0);
|
|
20564
|
-
}
|
|
20565
|
-
}
|
|
21026
|
+
var acquireLock = acquireFileLock;
|
|
21027
|
+
var lockHeldBy = fileLockHeldBy;
|
|
21028
|
+
var releaseLock = releaseFileLock;
|
|
20566
21029
|
function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
20567
|
-
const opts = {
|
|
20568
|
-
staleMs: lockOpts.staleMs ?? 3e4,
|
|
20569
|
-
retryMs: lockOpts.retryMs ?? 50,
|
|
20570
|
-
maxWaitMs: lockOpts.maxWaitMs ?? 5e3
|
|
20571
|
-
};
|
|
21030
|
+
const opts = resolveFileLockOpts({ ...lockOpts, label: "deferred registry lock" });
|
|
20572
21031
|
const lockPath = `${registryPath}.lock`;
|
|
20573
21032
|
return {
|
|
20574
21033
|
// Lenient read for the sweep's initial fetch: any error (missing/corrupt) yields an empty queue.
|
|
20575
21034
|
read: async () => {
|
|
20576
21035
|
try {
|
|
20577
|
-
return parseDeferredWorktreesFile(await (0,
|
|
21036
|
+
return parseDeferredWorktreesFile(await (0, import_promises5.readFile)(registryPath, "utf8"));
|
|
20578
21037
|
} catch {
|
|
20579
21038
|
return [];
|
|
20580
21039
|
}
|
|
20581
21040
|
},
|
|
20582
21041
|
// Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
|
|
20583
21042
|
write: async (entries) => {
|
|
20584
|
-
await (0,
|
|
21043
|
+
await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
|
|
20585
21044
|
await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
|
|
20586
21045
|
},
|
|
20587
21046
|
// Serialized read-modify-write under the repo-wide lock (#2846).
|
|
20588
21047
|
update: async (mutate) => {
|
|
20589
|
-
await (0,
|
|
21048
|
+
await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
|
|
20590
21049
|
const deadline = Date.now() + opts.maxWaitMs;
|
|
20591
21050
|
for (; ; ) {
|
|
20592
21051
|
const guard = await acquireLock(lockPath, opts, deadline);
|
|
@@ -20603,7 +21062,7 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
|
20603
21062
|
if (Date.now() >= deadline) {
|
|
20604
21063
|
throw new Error(`deferred registry lock contention: ${lockPath} lost to a concurrent holder past ${opts.maxWaitMs}ms`);
|
|
20605
21064
|
}
|
|
20606
|
-
await
|
|
21065
|
+
await sleep2(opts.retryMs);
|
|
20607
21066
|
}
|
|
20608
21067
|
}
|
|
20609
21068
|
};
|
|
@@ -20650,11 +21109,17 @@ function renderGcApplyResult(result) {
|
|
|
20650
21109
|
lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
|
|
20651
21110
|
lines.push(` tracking refs removed: ${result.removedTrackingRefs.length ? result.removedTrackingRefs.join(", ") : "none"}`);
|
|
20652
21111
|
lines.push(` dead sibling dirs removed: ${result.removedWorktreeDirs.length ? result.removedWorktreeDirs.join(", ") : "none"}`);
|
|
20653
|
-
lines.push(`
|
|
21112
|
+
lines.push(` stale registrations pruned: ${result.pruned ? "yes" : "no"} (clears registrations only \u2014 never deletes a directory)`);
|
|
20654
21113
|
if (result.failed.length) {
|
|
20655
21114
|
lines.push(` failed (${result.failed.length}):`);
|
|
20656
21115
|
for (const f of result.failed) lines.push(` - ${f}`);
|
|
20657
21116
|
}
|
|
21117
|
+
const removedNothing = result.removedBranches.length === 0 && result.removedRemoteBranches.length === 0 && result.removedTrackingRefs.length === 0 && result.removedWorktreeDirs.length === 0;
|
|
21118
|
+
if (removedNothing) {
|
|
21119
|
+
lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
|
|
21120
|
+
lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
|
|
21121
|
+
lines.push(" directory and re-run to clear its registration.");
|
|
21122
|
+
}
|
|
20658
21123
|
return lines.join("\n");
|
|
20659
21124
|
}
|
|
20660
21125
|
async function deleteReviewedRemoteBranch(remote, branch, expectedHeadOid) {
|
|
@@ -20713,7 +21178,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
20713
21178
|
return cleanupPrMergeLocalBranch(branch.branch, {
|
|
20714
21179
|
beforeWorktrees,
|
|
20715
21180
|
startingPath: branch.worktreePath,
|
|
20716
|
-
pathExists: (p) => (0,
|
|
21181
|
+
pathExists: (p) => (0, import_node_fs25.existsSync)(p),
|
|
20717
21182
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
20718
21183
|
teardownWorktreeStage,
|
|
20719
21184
|
deferredStore,
|
|
@@ -20735,12 +21200,12 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
20735
21200
|
const removeDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
20736
21201
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20737
21202
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
20738
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
21203
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path24.dirname)((0, import_node_path24.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
20739
21204
|
const siblingRoot = opts.root ? resolveExplicitScanRoot(opts.root, primaryRepoRoot) : siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
20740
21205
|
for (const wt of plan.worktreeDirs) {
|
|
20741
21206
|
try {
|
|
20742
21207
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
20743
|
-
realpath: (path2) => (0,
|
|
21208
|
+
realpath: (path2) => (0, import_node_fs25.realpathSync)(path2)
|
|
20744
21209
|
});
|
|
20745
21210
|
if (!cleanupTarget.ok) {
|
|
20746
21211
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -20901,13 +21366,13 @@ var realWorktreeDirRemover = {
|
|
|
20901
21366
|
probe: (p) => {
|
|
20902
21367
|
let st;
|
|
20903
21368
|
try {
|
|
20904
|
-
st = (0,
|
|
21369
|
+
st = (0, import_node_fs25.lstatSync)(p);
|
|
20905
21370
|
} catch {
|
|
20906
21371
|
return null;
|
|
20907
21372
|
}
|
|
20908
21373
|
if (st.isSymbolicLink()) return "link";
|
|
20909
21374
|
try {
|
|
20910
|
-
(0,
|
|
21375
|
+
(0, import_node_fs25.readlinkSync)(p);
|
|
20911
21376
|
return "link";
|
|
20912
21377
|
} catch {
|
|
20913
21378
|
}
|
|
@@ -20915,7 +21380,7 @@ var realWorktreeDirRemover = {
|
|
|
20915
21380
|
},
|
|
20916
21381
|
readdir: (p) => {
|
|
20917
21382
|
try {
|
|
20918
|
-
return (0,
|
|
21383
|
+
return (0, import_node_fs25.readdirSync)(p);
|
|
20919
21384
|
} catch {
|
|
20920
21385
|
return [];
|
|
20921
21386
|
}
|
|
@@ -20924,12 +21389,12 @@ var realWorktreeDirRemover = {
|
|
|
20924
21389
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
20925
21390
|
detachLink: (p) => {
|
|
20926
21391
|
try {
|
|
20927
|
-
(0,
|
|
21392
|
+
(0, import_node_fs25.rmdirSync)(p);
|
|
20928
21393
|
} catch {
|
|
20929
|
-
(0,
|
|
21394
|
+
(0, import_node_fs25.unlinkSync)(p);
|
|
20930
21395
|
}
|
|
20931
21396
|
},
|
|
20932
|
-
removeTree: (p) => (0,
|
|
21397
|
+
removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
20933
21398
|
};
|
|
20934
21399
|
async function resolvePrimaryCheckout(execGit) {
|
|
20935
21400
|
try {
|
|
@@ -20959,9 +21424,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
20959
21424
|
}
|
|
20960
21425
|
}
|
|
20961
21426
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
20962
|
-
if (!(0,
|
|
21427
|
+
if (!(0, import_node_fs25.existsSync)(statePath)) return false;
|
|
20963
21428
|
try {
|
|
20964
|
-
const state = JSON.parse((0,
|
|
21429
|
+
const state = JSON.parse((0, import_node_fs25.readFileSync)(statePath, "utf8"));
|
|
20965
21430
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
20966
21431
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
20967
21432
|
} catch {
|
|
@@ -21168,8 +21633,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
|
|
|
21168
21633
|
}
|
|
21169
21634
|
|
|
21170
21635
|
// src/worktree-lifecycle-commands.ts
|
|
21171
|
-
var
|
|
21172
|
-
var
|
|
21636
|
+
var import_node_fs26 = require("node:fs");
|
|
21637
|
+
var import_node_path25 = require("node:path");
|
|
21173
21638
|
var GH_TIMEOUT_MS = 2e4;
|
|
21174
21639
|
var DEFAULT_BASE = "origin/development";
|
|
21175
21640
|
var DEFAULT_REMOTE = "origin";
|
|
@@ -21272,7 +21737,7 @@ function classifyStaleLeaks(input) {
|
|
|
21272
21737
|
var defaultOrphanDirScanDeps = {
|
|
21273
21738
|
listDirs: (root) => {
|
|
21274
21739
|
try {
|
|
21275
|
-
return (0,
|
|
21740
|
+
return (0, import_node_fs26.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path25.join)(root, e.name));
|
|
21276
21741
|
} catch {
|
|
21277
21742
|
return [];
|
|
21278
21743
|
}
|
|
@@ -21328,13 +21793,13 @@ function registerWorktreeCommands(program3) {
|
|
|
21328
21793
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
21329
21794
|
return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
21330
21795
|
}
|
|
21331
|
-
const gitFile = (0,
|
|
21332
|
-
const isLinked = (0,
|
|
21796
|
+
const gitFile = (0, import_node_path25.join)(wtPath, ".git");
|
|
21797
|
+
const isLinked = (0, import_node_fs26.existsSync)(gitFile) && (0, import_node_fs26.statSync)(gitFile).isFile();
|
|
21333
21798
|
if (apply && !isLinked) {
|
|
21334
21799
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
21335
21800
|
}
|
|
21336
21801
|
const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
21337
|
-
const primaryCheckout = commonDir ? (0,
|
|
21802
|
+
const primaryCheckout = commonDir ? (0, import_node_path25.dirname)(commonDir) : wtPath;
|
|
21338
21803
|
const stageSummary = readStageSummary(wtPath);
|
|
21339
21804
|
const hasStage = stageSummary != null;
|
|
21340
21805
|
const steps = landSteps(branch, true, hasStage);
|
|
@@ -21445,10 +21910,10 @@ async function gatherWorktreeContext() {
|
|
|
21445
21910
|
if (s) stages.push({ path: wt.path, port: s.port });
|
|
21446
21911
|
}
|
|
21447
21912
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
21448
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
21913
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path25.dirname)((0, import_node_path25.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
21449
21914
|
const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
21450
21915
|
let orphanDirs = [];
|
|
21451
|
-
if ((0,
|
|
21916
|
+
if ((0, import_node_fs26.existsSync)(wtRoot)) {
|
|
21452
21917
|
orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
|
|
21453
21918
|
...defaultOrphanDirScanDeps,
|
|
21454
21919
|
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
@@ -21471,7 +21936,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
21471
21936
|
}
|
|
21472
21937
|
|
|
21473
21938
|
// src/issue-commands.ts
|
|
21474
|
-
var
|
|
21939
|
+
var import_node_fs27 = require("node:fs");
|
|
21475
21940
|
var import_node_crypto5 = require("node:crypto");
|
|
21476
21941
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
21477
21942
|
async function editIssue(client, options, deps = {}) {
|
|
@@ -21485,7 +21950,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
21485
21950
|
if (options.body !== void 0 || options.bodyFile !== void 0) {
|
|
21486
21951
|
let body;
|
|
21487
21952
|
if (options.bodyFile) {
|
|
21488
|
-
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
21953
|
+
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs27.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
21489
21954
|
body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
|
|
21490
21955
|
} else {
|
|
21491
21956
|
body = options.body ?? "";
|
|
@@ -21982,7 +22447,7 @@ function extendCreateCommand(issue2, batchAttach) {
|
|
|
21982
22447
|
if (opts.batch) {
|
|
21983
22448
|
let specs;
|
|
21984
22449
|
try {
|
|
21985
|
-
const raw = (0,
|
|
22450
|
+
const raw = (0, import_node_fs27.readFileSync)(opts.batch, "utf8");
|
|
21986
22451
|
specs = JSON.parse(raw);
|
|
21987
22452
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
21988
22453
|
} catch (e) {
|
|
@@ -22040,12 +22505,12 @@ ${lines}`);
|
|
|
22040
22505
|
}
|
|
22041
22506
|
|
|
22042
22507
|
// src/train-commands.ts
|
|
22043
|
-
var
|
|
22044
|
-
var
|
|
22508
|
+
var import_node_fs29 = require("node:fs");
|
|
22509
|
+
var import_node_path27 = require("node:path");
|
|
22045
22510
|
|
|
22046
22511
|
// src/plugin-guard-io.ts
|
|
22047
|
-
var
|
|
22048
|
-
var
|
|
22512
|
+
var import_node_fs28 = require("node:fs");
|
|
22513
|
+
var import_node_path26 = require("node:path");
|
|
22049
22514
|
var import_node_os6 = require("node:os");
|
|
22050
22515
|
|
|
22051
22516
|
// src/plugin-guard.ts
|
|
@@ -22175,11 +22640,11 @@ function runHostBin(bin, args, opts) {
|
|
|
22175
22640
|
}
|
|
22176
22641
|
var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
|
|
22177
22642
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
22178
|
-
return (0,
|
|
22643
|
+
return (0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
22179
22644
|
};
|
|
22180
22645
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
22181
22646
|
try {
|
|
22182
|
-
return JSON.parse((0,
|
|
22647
|
+
return JSON.parse((0, import_node_fs28.readFileSync)(installedPluginsPath2(surface), "utf8"));
|
|
22183
22648
|
} catch {
|
|
22184
22649
|
return null;
|
|
22185
22650
|
}
|
|
@@ -22187,13 +22652,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
|
22187
22652
|
function marketplaceCloneCandidates(surface, home) {
|
|
22188
22653
|
if (surface === "codex") {
|
|
22189
22654
|
return [
|
|
22190
|
-
(0,
|
|
22191
|
-
(0,
|
|
22655
|
+
(0, import_node_path26.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
|
|
22656
|
+
(0, import_node_path26.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
|
|
22192
22657
|
];
|
|
22193
22658
|
}
|
|
22194
|
-
return [(0,
|
|
22659
|
+
return [(0, import_node_path26.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
22195
22660
|
}
|
|
22196
|
-
function marketplaceClonePresent(surface, home, exists =
|
|
22661
|
+
function marketplaceClonePresent(surface, home, exists = import_node_fs28.existsSync) {
|
|
22197
22662
|
return marketplaceCloneCandidates(surface, home).some(exists);
|
|
22198
22663
|
}
|
|
22199
22664
|
async function fetchNpmReleasedVersion() {
|
|
@@ -22221,7 +22686,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
22221
22686
|
isOrgRepo,
|
|
22222
22687
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
22223
22688
|
marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
|
|
22224
|
-
pluginCachePresent: (0,
|
|
22689
|
+
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
22225
22690
|
};
|
|
22226
22691
|
}
|
|
22227
22692
|
function claudePluginGuardState(isOrgRepo) {
|
|
@@ -22356,7 +22821,7 @@ function formatTrainStatus(r) {
|
|
|
22356
22821
|
// src/train-commands.ts
|
|
22357
22822
|
function readRepoVersion() {
|
|
22358
22823
|
try {
|
|
22359
|
-
return JSON.parse((0,
|
|
22824
|
+
return JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
22360
22825
|
} catch {
|
|
22361
22826
|
return void 0;
|
|
22362
22827
|
}
|
|
@@ -22502,6 +22967,9 @@ function registerDeployCommands(program3) {
|
|
|
22502
22967
|
}
|
|
22503
22968
|
|
|
22504
22969
|
// src/discovery-commands.ts
|
|
22970
|
+
var import_node_fs30 = require("node:fs");
|
|
22971
|
+
var import_node_os7 = require("node:os");
|
|
22972
|
+
var import_node_path28 = require("node:path");
|
|
22505
22973
|
var GC_GH_TIMEOUT_MS3 = 2e4;
|
|
22506
22974
|
async function collectStatus() {
|
|
22507
22975
|
let branch = "";
|
|
@@ -22586,6 +23054,23 @@ async function recommendNext(deps) {
|
|
|
22586
23054
|
const top = sorted[0];
|
|
22587
23055
|
return { number: top.number, title: top.title, url: top.url, priority: top.priority, repo: top.repository };
|
|
22588
23056
|
}
|
|
23057
|
+
function onboardPluginGate(deps) {
|
|
23058
|
+
const { registered, declared, ref } = readKnownMarketplace(deps.readKnown(), MMI_MARKETPLACE_NAME);
|
|
23059
|
+
if (!registered) {
|
|
23060
|
+
return { ok: false, detail: `${MMI_MARKETPLACE_NAME} marketplace not registered on this machine` };
|
|
23061
|
+
}
|
|
23062
|
+
const autoUpdate = resolveAutoUpdate({
|
|
23063
|
+
name: MMI_MARKETPLACE_NAME,
|
|
23064
|
+
registered,
|
|
23065
|
+
declared,
|
|
23066
|
+
settingsDeclared: readSettingsAutoUpdate(deps.readSettings(), MMI_MARKETPLACE_NAME)
|
|
23067
|
+
}).effective;
|
|
23068
|
+
const catalog = resolveCatalogRef({ name: MMI_MARKETPLACE_NAME, registered, ref, autoUpdate });
|
|
23069
|
+
const gaps = [];
|
|
23070
|
+
if (!autoUpdate) gaps.push(`auto-update is off \u2014 turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`);
|
|
23071
|
+
if (catalog && !catalog.agrees) gaps.push(`the catalog is ${catalog.unpinned ? "unpinned" : `pinned to ${catalog.ref}`} \u2014 ${CATALOG_REF_PIN_STEPS}`);
|
|
23072
|
+
return gaps.length === 0 ? { ok: true, detail: `auto-update on, catalog pinned to ${catalog?.ref}` } : { ok: false, detail: gaps.join("; ") };
|
|
23073
|
+
}
|
|
22589
23074
|
async function collectOnboardStatus() {
|
|
22590
23075
|
const cfg = await loadConfig();
|
|
22591
23076
|
let track = "unknown";
|
|
@@ -22659,7 +23144,12 @@ async function collectOnboardStatus() {
|
|
|
22659
23144
|
else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
|
|
22660
23145
|
else nextCommand = "mmi-cli board read \u2014 no claimable items found";
|
|
22661
23146
|
}
|
|
22662
|
-
|
|
23147
|
+
const home = (0, import_node_os7.homedir)();
|
|
23148
|
+
const plugin = onboardPluginGate({
|
|
23149
|
+
readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
|
|
23150
|
+
readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
|
|
23151
|
+
});
|
|
23152
|
+
return { track, board, registry: registry2, secrets, plugin, nextCommand };
|
|
22663
23153
|
}
|
|
22664
23154
|
function registerDiscoveryCommands(program3) {
|
|
22665
23155
|
program3.command("status").description("unified repo snapshot: branch, worktrees, open PRs (yours), claimed board items, stage state").option("--json", "machine-readable output").action(async (o) => {
|
|
@@ -22703,6 +23193,7 @@ function registerDiscoveryCommands(program3) {
|
|
|
22703
23193
|
console.log(`Board: ${report.board.ok ? "\u2713" : "\u2717"} ${report.board.detail}`);
|
|
22704
23194
|
console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
|
|
22705
23195
|
console.log(`Secrets: ${report.secrets.ok ? "\u2713" : "\u2717"} ${report.secrets.detail}`);
|
|
23196
|
+
console.log(`Plugin: ${report.plugin.ok ? "\u2713" : "\u2717"} ${report.plugin.detail}`);
|
|
22706
23197
|
console.log(`
|
|
22707
23198
|
Next command: ${report.nextCommand}`);
|
|
22708
23199
|
}
|
|
@@ -22862,7 +23353,7 @@ function registerExplainCommand(program3) {
|
|
|
22862
23353
|
}
|
|
22863
23354
|
|
|
22864
23355
|
// src/pr-commands.ts
|
|
22865
|
-
var
|
|
23356
|
+
var import_promises7 = require("node:fs/promises");
|
|
22866
23357
|
var GC_GH_TIMEOUT_MS4 = 2e4;
|
|
22867
23358
|
var CHECKS_WATCH_POLL_MS = 15e3;
|
|
22868
23359
|
var CHECKS_WATCH_TIMEOUT_MS = 10 * 6e4;
|
|
@@ -23006,10 +23497,10 @@ function registerPrLifecycleCommands(program3) {
|
|
|
23006
23497
|
let body;
|
|
23007
23498
|
try {
|
|
23008
23499
|
if (o.title || o.titleFile) {
|
|
23009
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
23500
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
23010
23501
|
}
|
|
23011
23502
|
if (o.body || o.bodyFile) {
|
|
23012
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23503
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
23013
23504
|
}
|
|
23014
23505
|
} catch (e) {
|
|
23015
23506
|
return fail(`pr edit: ${e.message}`);
|
|
@@ -23039,7 +23530,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
23039
23530
|
}
|
|
23040
23531
|
let body;
|
|
23041
23532
|
try {
|
|
23042
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23533
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
23043
23534
|
} catch (e) {
|
|
23044
23535
|
return fail(`pr comment: ${e.message}`);
|
|
23045
23536
|
}
|
|
@@ -23443,7 +23934,7 @@ function renderDoctorText(checks, opts) {
|
|
|
23443
23934
|
return lines.join("\n");
|
|
23444
23935
|
}
|
|
23445
23936
|
function doctorReportExitCode(checks) {
|
|
23446
|
-
return checks.some((c) => !c.ok) ? 1 : 0;
|
|
23937
|
+
return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
|
|
23447
23938
|
}
|
|
23448
23939
|
|
|
23449
23940
|
// src/doctor-clean.ts
|
|
@@ -23477,6 +23968,7 @@ function checkGithubAuth(probe) {
|
|
|
23477
23968
|
};
|
|
23478
23969
|
}
|
|
23479
23970
|
function checkAwsIdentity(probe) {
|
|
23971
|
+
if (probe.probed === false) return null;
|
|
23480
23972
|
if (!probe.isOrgRepo) return null;
|
|
23481
23973
|
const arn = probe.callerArn?.trim();
|
|
23482
23974
|
return {
|
|
@@ -23485,7 +23977,11 @@ function checkAwsIdentity(probe) {
|
|
|
23485
23977
|
fix: "use a non-root IAM user/session profile (set AWS_PROFILE or run: aws sso login), then verify `aws sts get-caller-identity` is not :root",
|
|
23486
23978
|
// Name the caller. A green "aws identity" says only "not root" — it does not say WHO, and "no AWS
|
|
23487
23979
|
// configured at all" is also green here. Under --verbose those two must not look the same.
|
|
23488
|
-
|
|
23980
|
+
//
|
|
23981
|
+
// #3485: the empty case says "not resolved", never "not configured". `awsCallerArn` returns undefined
|
|
23982
|
+
// for ANY failure — expired SSO, no `aws` binary, a timeout — so asserting "no AWS identity configured"
|
|
23983
|
+
// is the same false-fact green this issue is about, one layer down. Evidence states what was observed.
|
|
23984
|
+
verbose: [`caller: ${arn || "(none resolved \u2014 no AWS configured, or the probe failed; either is allowed)"}`]
|
|
23489
23985
|
};
|
|
23490
23986
|
}
|
|
23491
23987
|
function checkRepoWorktrees(probe) {
|
|
@@ -23515,6 +24011,11 @@ function checkWorktreeRoots(probe) {
|
|
|
23515
24011
|
return {
|
|
23516
24012
|
ok: false,
|
|
23517
24013
|
label: "worktree roots",
|
|
24014
|
+
// #3485: report-only in EVERY mode means report-only in the exit code too. `worktree gc --root` does
|
|
24015
|
+
// exist and is the eventual fix, but this row is deliberately not a gate: the remedy is a human
|
|
24016
|
+
// review of directories that may hold unlanded work, and a size-based auto-sweep once nearly deleted
|
|
24017
|
+
// an unlanded correctness fix. Counting it made `mmi-cli doctor` exit 1 until someone swept by hand.
|
|
24018
|
+
reportOnly: true,
|
|
23518
24019
|
// The fix is a review, never a delete: `gc --root` still refuses anything it cannot classify as dead,
|
|
23519
24020
|
// and doctor itself — with or without --apply — never touches these directories.
|
|
23520
24021
|
fix: `${probe.stray.length} worktrees root(s) outside ${probe.authoritative} \u2014 ${named}; no gc scans them. Review, then sweep deliberately from the owning repo with \`mmi-cli worktree gc --root <path> --apply\` (report-only here: doctor never deletes these)${cutShort}`,
|
|
@@ -23530,7 +24031,8 @@ function checkClaudePlugin(probe) {
|
|
|
23530
24031
|
const { installed, released, guardState } = probe;
|
|
23531
24032
|
const evidence = [
|
|
23532
24033
|
`installed: ${installed ?? "(none)"}`,
|
|
23533
|
-
|
|
24034
|
+
// #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
|
|
24035
|
+
`released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
|
|
23534
24036
|
`resolvable: ${guardState}`
|
|
23535
24037
|
];
|
|
23536
24038
|
const trigger = pluginHealTrigger(probe);
|
|
@@ -23552,15 +24054,16 @@ function checkClaudePlugin(probe) {
|
|
|
23552
24054
|
verbose: evidence
|
|
23553
24055
|
};
|
|
23554
24056
|
}
|
|
24057
|
+
if (!released) return null;
|
|
23555
24058
|
return { ok: true, label: installed ? `Claude plugin ${installed}` : "Claude plugin", verbose: evidence };
|
|
23556
24059
|
}
|
|
23557
|
-
function checkCliVersion(input) {
|
|
24060
|
+
function checkCliVersion(input, releasedNote) {
|
|
23558
24061
|
const report = buildVersionLagReport(input);
|
|
23559
24062
|
const evidence = [
|
|
23560
24063
|
`running: ${report.currentVersion}`,
|
|
23561
|
-
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`
|
|
24064
|
+
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
|
|
23562
24065
|
];
|
|
23563
|
-
if (!report.releasedVersion) return
|
|
24066
|
+
if (!report.releasedVersion) return null;
|
|
23564
24067
|
if (report.ok) return { ok: true, label: `mmi-cli ${report.currentVersion}`, verbose: evidence };
|
|
23565
24068
|
return {
|
|
23566
24069
|
ok: false,
|
|
@@ -23595,6 +24098,12 @@ function checkPluginCache(input) {
|
|
|
23595
24098
|
return {
|
|
23596
24099
|
ok: false,
|
|
23597
24100
|
label: "plugin cache",
|
|
24101
|
+
// #3485: deliberately NOT `reportOnly`, and not in the closed set docs/doctor-contract.md enumerates.
|
|
24102
|
+
// A checker argued it qualifies because no `doctor --apply` clears it; I tagged it, then reverted.
|
|
24103
|
+
// `mmi-cli plugin prune --apply` clears it in one command and a re-run goes green, so this is an
|
|
24104
|
+
// ordinary red the operator is expected to act on. "The doctor cannot heal it automatically" was never
|
|
24105
|
+
// the bar — see the contract for why report-only is a per-row ruling rather than a rule you can apply
|
|
24106
|
+
// from here.
|
|
23598
24107
|
// Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
|
|
23599
24108
|
// on its own so a cache with zero versioned dirs still gets an honest ✗.
|
|
23600
24109
|
detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
|
|
@@ -23623,10 +24132,14 @@ function checkSchedules(probe) {
|
|
|
23623
24132
|
...probe.incomplete.map((i) => `incomplete: ${i}`),
|
|
23624
24133
|
...probe.drift.map((d) => `drift: ${d}`)
|
|
23625
24134
|
];
|
|
24135
|
+
const clearableHere = probe.clearableHere ?? 0;
|
|
23626
24136
|
if (probe.incomplete.length) {
|
|
23627
24137
|
return {
|
|
23628
24138
|
ok: false,
|
|
23629
24139
|
label: "schedules",
|
|
24140
|
+
// #3485: ruled report-only — an unreadable source is a read failure somewhere in the org, and
|
|
24141
|
+
// nothing in this checkout clears it.
|
|
24142
|
+
reportOnly: true,
|
|
23630
24143
|
detail: `${probe.incomplete.length} source(s) unreadable`,
|
|
23631
24144
|
fix: "run `mmi-cli org schedules` for the full report \u2014 the notebook may be missing armed entries",
|
|
23632
24145
|
verbose: evidence
|
|
@@ -23636,8 +24149,14 @@ function checkSchedules(probe) {
|
|
|
23636
24149
|
return {
|
|
23637
24150
|
ok: false,
|
|
23638
24151
|
label: "schedules",
|
|
24152
|
+
// #3485 ruled this row report-only because the drift classes are owned by other repos. #3492 makes
|
|
24153
|
+
// that conditional rather than blanket: when a finding IS clearable from this checkout — a
|
|
24154
|
+
// `file-vs-registry-stale` row for this repo, one `org schedules register` away — the row gates
|
|
24155
|
+
// like any other actionable red. Exempting a fault the operator can fix in one command is the
|
|
24156
|
+
// tolerated-red failure the tier exists to prevent, not an instance of it.
|
|
24157
|
+
...clearableHere > 0 ? {} : { reportOnly: true },
|
|
23639
24158
|
detail: `${probe.drift.length} drift finding(s)`,
|
|
23640
|
-
fix: "run `mmi-cli org schedules` \u2014 each drift line names its per-class remedy (file/registry/live mismatch or harbour enforcement), applied in the owning repo",
|
|
24159
|
+
fix: clearableHere > 0 ? `${clearableHere} of ${probe.drift.length} clearable from this repo \u2014 from an up-to-date checkout run \`mmi-cli org schedules register\` here, then \`mmi-cli org schedules\` for the rest (each line names its own per-class remedy, applied in the owning repo)` : "run `mmi-cli org schedules` \u2014 each drift line names its per-class remedy (file/registry/live mismatch or harbour enforcement), applied in the owning repo",
|
|
23641
24160
|
verbose: evidence
|
|
23642
24161
|
};
|
|
23643
24162
|
}
|
|
@@ -23652,6 +24171,10 @@ function checkDocsAudit(probe) {
|
|
|
23652
24171
|
return {
|
|
23653
24172
|
ok: false,
|
|
23654
24173
|
label: "docs-audit",
|
|
24174
|
+
// #3485: ruled report-only — the remedy is re-running or re-arming the janitor in the repo that
|
|
24175
|
+
// owns it. `mmi-cli docs audit record` can write a verdict from here, but hand-writing one to clear
|
|
24176
|
+
// a dead-man check defeats the check (#3067 pillar 4: silence is an alarm, never a success).
|
|
24177
|
+
reportOnly: true,
|
|
23655
24178
|
fix: `${probe.detail} \u2014 re-run the janitor in the owning repo or re-arm its schedule (the remedy never lives in this doctor)`,
|
|
23656
24179
|
verbose: [probe.detail]
|
|
23657
24180
|
};
|
|
@@ -23705,13 +24228,15 @@ function gcReapable(plan) {
|
|
|
23705
24228
|
return plan.branches.length + plan.trackingRefs.length + plan.worktreeDirs.length;
|
|
23706
24229
|
}
|
|
23707
24230
|
async function runDoctorClean(opts, io, deps) {
|
|
23708
|
-
const
|
|
23709
|
-
const applyRepo = apply && opts.repoWrites !== false;
|
|
24231
|
+
const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
|
|
24232
|
+
const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
|
|
24233
|
+
const probeReleased = !opts.fast || Boolean(opts.self);
|
|
24234
|
+
const probeAws = !opts.fast && !opts.banner;
|
|
23710
24235
|
const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
|
|
23711
24236
|
deps.githubLogin(),
|
|
23712
24237
|
deps.isOrgRepo(),
|
|
23713
|
-
|
|
23714
|
-
|
|
24238
|
+
probeReleased ? deps.releasedVersion() : Promise.resolve(void 0),
|
|
24239
|
+
probeAws ? deps.awsCallerArn() : Promise.resolve(void 0),
|
|
23715
24240
|
// #3365. Skipped on the banner/preflight lanes, which run on every SessionStart and must not pay a
|
|
23716
24241
|
// network round-trip — and on a bare `--fast`, whose contract is offline-safe. It DOES run on
|
|
23717
24242
|
// `--self`, which rides the fast lane but is the interactive "is my setup actually fine?" diagnostic,
|
|
@@ -23726,7 +24251,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23726
24251
|
if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
|
|
23727
24252
|
checks.push(...checkGithubPools(await deps.githubPools()));
|
|
23728
24253
|
}
|
|
23729
|
-
const aws = checkAwsIdentity({ isOrgRepo, callerArn });
|
|
24254
|
+
const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
|
|
23730
24255
|
if (aws) checks.push(aws);
|
|
23731
24256
|
checks.push(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
|
|
23732
24257
|
if (isOrgRepo) {
|
|
@@ -23745,9 +24270,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23745
24270
|
checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
23746
24271
|
}
|
|
23747
24272
|
}
|
|
23748
|
-
const
|
|
24273
|
+
const releasedNote = deps.releasedVersionNote?.();
|
|
24274
|
+
const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo), releasedNote };
|
|
23749
24275
|
const healTrigger = pluginHealTrigger(pluginProbe);
|
|
23750
|
-
if (
|
|
24276
|
+
if (applyEnv && deps.healPlugin && healTrigger) {
|
|
23751
24277
|
const heal = await deps.healPlugin();
|
|
23752
24278
|
const label = healTrigger === "behind" ? `Claude plugin ${installed} \u2192 ${released}` : "Claude plugin \u2014 reinstalled";
|
|
23753
24279
|
const healEvidence = [
|
|
@@ -23764,18 +24290,25 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23764
24290
|
} : {
|
|
23765
24291
|
ok: false,
|
|
23766
24292
|
label,
|
|
23767
|
-
|
|
24293
|
+
// #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
|
|
24294
|
+
// not fix what it found — but it is not this invocation's to clear. The other process is doing it;
|
|
24295
|
+
// waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
|
|
24296
|
+
// genuine gap and still gates.
|
|
24297
|
+
...heal.skipped ? { reportOnly: true } : {},
|
|
24298
|
+
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes` : `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then restart Claude`,
|
|
23768
24299
|
verbose: healEvidence
|
|
23769
24300
|
});
|
|
23770
|
-
restartPending = true;
|
|
24301
|
+
if (!heal.skipped) restartPending = true;
|
|
23771
24302
|
} else {
|
|
23772
24303
|
const plugin = checkClaudePlugin(pluginProbe);
|
|
23773
|
-
|
|
23774
|
-
|
|
24304
|
+
if (plugin) {
|
|
24305
|
+
checks.push(plugin);
|
|
24306
|
+
if (!plugin.ok) restartPending = true;
|
|
24307
|
+
}
|
|
23775
24308
|
}
|
|
23776
24309
|
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
23777
24310
|
const cliReport = buildVersionLagReport(cliInput);
|
|
23778
|
-
if (
|
|
24311
|
+
if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
|
|
23779
24312
|
const heal = await deps.updateCli(cliReport.releasedVersion);
|
|
23780
24313
|
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
23781
24314
|
checks.push(heal.ok ? {
|
|
@@ -23786,17 +24319,48 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23786
24319
|
} : {
|
|
23787
24320
|
ok: false,
|
|
23788
24321
|
label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
23789
|
-
|
|
24322
|
+
// #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
|
|
24323
|
+
...heal.skipped ? { reportOnly: true } : {},
|
|
24324
|
+
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
|
|
23790
24325
|
verbose: healEvidence
|
|
23791
24326
|
});
|
|
23792
24327
|
} else {
|
|
23793
|
-
|
|
24328
|
+
const cli = checkCliVersion(cliInput, releasedNote);
|
|
24329
|
+
if (cli) checks.push(cli);
|
|
24330
|
+
}
|
|
24331
|
+
const cacheProbe = deps.pluginCache();
|
|
24332
|
+
const cacheStale = cacheProbe.stale.length > 0 || cacheProbe.staging.length > 0;
|
|
24333
|
+
if (applyEnv && deps.prunePluginCache && cacheStale) {
|
|
24334
|
+
const pruned = await deps.prunePluginCache();
|
|
24335
|
+
const removed = pruned.removed ?? [];
|
|
24336
|
+
const pruneEvidence = [
|
|
24337
|
+
`cached versions: ${cacheProbe.cached}`,
|
|
24338
|
+
`stale: ${cacheProbe.stale.length ? cacheProbe.stale.join(", ") : "(none)"}`,
|
|
24339
|
+
`orphaned staging dirs: ${cacheProbe.staging.length ? cacheProbe.staging.join(", ") : "(none)"}`,
|
|
24340
|
+
`prune: ${pruned.detail}`,
|
|
24341
|
+
...removed.map((r) => `removed: ${r}`)
|
|
24342
|
+
];
|
|
24343
|
+
checks.push(pruned.ok ? {
|
|
24344
|
+
ok: true,
|
|
24345
|
+
label: "plugin cache",
|
|
24346
|
+
detail: removed.length ? `pruned ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}` : "nothing to prune",
|
|
24347
|
+
verbose: pruneEvidence
|
|
24348
|
+
} : {
|
|
24349
|
+
ok: false,
|
|
24350
|
+
label: "plugin cache",
|
|
24351
|
+
// Same split as the other env heals (#3489): a contention skip is not this run's gap.
|
|
24352
|
+
...pruned.skipped ? { reportOnly: true } : {},
|
|
24353
|
+
fix: pruned.skipped ? `${pruned.detail} \u2014 another doctor on this machine is pruning it now; re-run once it finishes` : `prune failed (${pruned.detail}) \u2014 run \`mmi-cli plugin prune --apply\``,
|
|
24354
|
+
verbose: pruneEvidence
|
|
24355
|
+
});
|
|
24356
|
+
} else {
|
|
24357
|
+
checks.push(checkPluginCache(cacheProbe));
|
|
23794
24358
|
}
|
|
23795
|
-
checks.push(checkPluginCache(deps.pluginCache()));
|
|
23796
24359
|
if (deps.sessionPayload) {
|
|
23797
24360
|
const payload = checkSessionPayload(deps.sessionPayload());
|
|
23798
24361
|
if (payload) checks.push(payload);
|
|
23799
24362
|
}
|
|
24363
|
+
if (deps.marketplaceRows) checks.push(...deps.marketplaceRows());
|
|
23800
24364
|
if (!opts.fast && !opts.banner && !opts.preflight) {
|
|
23801
24365
|
const probe = await deps.schedulesNotebook().catch((e) => ({
|
|
23802
24366
|
armed: 0,
|
|
@@ -23822,7 +24386,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23822
24386
|
const roots = checkWorktreeRoots(probe);
|
|
23823
24387
|
if (roots) checks.push(roots);
|
|
23824
24388
|
}
|
|
23825
|
-
if (isOrgRepo && !opts.fast && !opts.banner) {
|
|
24389
|
+
if (isOrgRepo && !opts.fast && !opts.banner && !opts.preflight) {
|
|
23826
24390
|
try {
|
|
23827
24391
|
checks.push(checkTrainSync(await deps.syncTrain()));
|
|
23828
24392
|
} catch (e) {
|
|
@@ -23924,17 +24488,17 @@ function parseOriginRepo(remoteUrl) {
|
|
|
23924
24488
|
}
|
|
23925
24489
|
function ghHostsConfigPath(env, platform2) {
|
|
23926
24490
|
const sep2 = platform2 === "win32" ? "\\" : "/";
|
|
23927
|
-
const
|
|
24491
|
+
const join26 = (...parts) => parts.join(sep2);
|
|
23928
24492
|
const explicit = env.GH_CONFIG_DIR?.trim();
|
|
23929
|
-
if (explicit) return
|
|
24493
|
+
if (explicit) return join26(explicit, "hosts.yml");
|
|
23930
24494
|
if (platform2 === "win32") {
|
|
23931
24495
|
const appData = (env.AppData ?? env.APPDATA)?.trim();
|
|
23932
|
-
return appData ?
|
|
24496
|
+
return appData ? join26(appData, "GitHub CLI", "hosts.yml") : void 0;
|
|
23933
24497
|
}
|
|
23934
24498
|
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
23935
|
-
if (xdg) return
|
|
24499
|
+
if (xdg) return join26(xdg, "gh", "hosts.yml");
|
|
23936
24500
|
const home = env.HOME?.trim();
|
|
23937
|
-
return home ?
|
|
24501
|
+
return home ? join26(home, ".config", "gh", "hosts.yml") : void 0;
|
|
23938
24502
|
}
|
|
23939
24503
|
function parseGhHostsAccounts(yaml, host = "github.com") {
|
|
23940
24504
|
let hostIndent = null;
|
|
@@ -23984,9 +24548,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
23984
24548
|
}
|
|
23985
24549
|
|
|
23986
24550
|
// src/doctor-io.ts
|
|
23987
|
-
var
|
|
23988
|
-
var
|
|
23989
|
-
var
|
|
24551
|
+
var import_node_fs31 = require("node:fs");
|
|
24552
|
+
var import_node_os8 = require("node:os");
|
|
24553
|
+
var import_node_path29 = require("node:path");
|
|
23990
24554
|
var import_node_child_process12 = require("node:child_process");
|
|
23991
24555
|
var import_node_util8 = require("node:util");
|
|
23992
24556
|
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
|
|
@@ -23994,7 +24558,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
|
23994
24558
|
function installedClaudePluginVersion() {
|
|
23995
24559
|
try {
|
|
23996
24560
|
const file = JSON.parse(
|
|
23997
|
-
(0,
|
|
24561
|
+
(0, import_node_fs31.readFileSync)((0, import_node_path29.join)((0, import_node_os8.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
23998
24562
|
);
|
|
23999
24563
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
24000
24564
|
if (versions.length === 0) return void 0;
|
|
@@ -24015,13 +24579,13 @@ function worktreeRootSync() {
|
|
|
24015
24579
|
}
|
|
24016
24580
|
var gitignorePath = () => {
|
|
24017
24581
|
const root = worktreeRootSync();
|
|
24018
|
-
return root === null ? null : (0,
|
|
24582
|
+
return root === null ? null : (0, import_node_path29.join)(root, ".gitignore");
|
|
24019
24583
|
};
|
|
24020
24584
|
function readGitignore() {
|
|
24021
24585
|
const path2 = gitignorePath();
|
|
24022
24586
|
if (path2 === null) return null;
|
|
24023
24587
|
try {
|
|
24024
|
-
return (0,
|
|
24588
|
+
return (0, import_node_fs31.readFileSync)(path2, "utf8");
|
|
24025
24589
|
} catch {
|
|
24026
24590
|
return null;
|
|
24027
24591
|
}
|
|
@@ -24030,7 +24594,7 @@ function writeGitignore(content) {
|
|
|
24030
24594
|
const path2 = gitignorePath();
|
|
24031
24595
|
if (path2 === null) return false;
|
|
24032
24596
|
try {
|
|
24033
|
-
(0,
|
|
24597
|
+
(0, import_node_fs31.writeFileSync)(path2, content, "utf8");
|
|
24034
24598
|
return true;
|
|
24035
24599
|
} catch {
|
|
24036
24600
|
return false;
|
|
@@ -24054,7 +24618,7 @@ async function repoRoot() {
|
|
|
24054
24618
|
}
|
|
24055
24619
|
function hasRepoLocalWorktrees() {
|
|
24056
24620
|
const root = worktreeRootSync();
|
|
24057
|
-
return root !== null && (0,
|
|
24621
|
+
return root !== null && (0, import_node_fs31.existsSync)((0, import_node_path29.join)(root, ".worktrees"));
|
|
24058
24622
|
}
|
|
24059
24623
|
|
|
24060
24624
|
// src/index.ts
|
|
@@ -24089,13 +24653,61 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
24089
24653
|
function ghMultiAccountCaveat(announcedLogin) {
|
|
24090
24654
|
try {
|
|
24091
24655
|
const hostsPath = ghHostsConfigPath(process.env, process.platform);
|
|
24092
|
-
if (!hostsPath || !(0,
|
|
24093
|
-
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0,
|
|
24656
|
+
if (!hostsPath || !(0, import_node_fs32.existsSync)(hostsPath)) return void 0;
|
|
24657
|
+
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs32.readFileSync)(hostsPath, "utf8")));
|
|
24094
24658
|
} catch {
|
|
24095
24659
|
return void 0;
|
|
24096
24660
|
}
|
|
24097
24661
|
}
|
|
24098
|
-
|
|
24662
|
+
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
24663
|
+
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
24664
|
+
function envHealLockPath(home) {
|
|
24665
|
+
return (0, import_node_path30.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
24666
|
+
}
|
|
24667
|
+
async function withEnvHealLock(what, run) {
|
|
24668
|
+
try {
|
|
24669
|
+
return await withFileLock(
|
|
24670
|
+
envHealLockPath((0, import_node_os9.homedir)()),
|
|
24671
|
+
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
24672
|
+
run
|
|
24673
|
+
);
|
|
24674
|
+
} catch (e) {
|
|
24675
|
+
if (e instanceof FileLockBusyError) {
|
|
24676
|
+
return { ok: false, skipped: true, detail: `${what} skipped \u2014 ${e.message}` };
|
|
24677
|
+
}
|
|
24678
|
+
return { ok: false, detail: `${what} could not take the env-heal lock \u2014 ${e.message}` };
|
|
24679
|
+
}
|
|
24680
|
+
}
|
|
24681
|
+
function docsJanitorScheduleId(repo) {
|
|
24682
|
+
return `${repo.split("/").pop()}/docs-janitor`;
|
|
24683
|
+
}
|
|
24684
|
+
function throttledReleasedVersion() {
|
|
24685
|
+
let note;
|
|
24686
|
+
return {
|
|
24687
|
+
note: () => note,
|
|
24688
|
+
read: async () => {
|
|
24689
|
+
const cachePath = releasedVersionCachePath(repoRuntimeStatePath(process.cwd()));
|
|
24690
|
+
const cached = readReleasedVersionCache(cachePath);
|
|
24691
|
+
if (cached) {
|
|
24692
|
+
note = cachedReadNote(cached.at);
|
|
24693
|
+
return cached.version;
|
|
24694
|
+
}
|
|
24695
|
+
const live = await fetchNpmReleasedVersion();
|
|
24696
|
+
if (live) writeReleasedVersionCache(cachePath, live);
|
|
24697
|
+
note = void 0;
|
|
24698
|
+
return live;
|
|
24699
|
+
}
|
|
24700
|
+
};
|
|
24701
|
+
}
|
|
24702
|
+
function mmiDoctorDeps(opts = {}) {
|
|
24703
|
+
const throttled = opts.throttleReleasedRead ? throttledReleasedVersion() : void 0;
|
|
24704
|
+
let notebook;
|
|
24705
|
+
const notebookOnce = () => notebook ??= fetchNotebook();
|
|
24706
|
+
const docsJanitorArmedAt = async (repo) => {
|
|
24707
|
+
const wanted = docsJanitorScheduleId(repo);
|
|
24708
|
+
const { entries } = await notebookOnce();
|
|
24709
|
+
return entries.find((e) => e.scheduleId === wanted)?.armedAt;
|
|
24710
|
+
};
|
|
24099
24711
|
return {
|
|
24100
24712
|
githubLogin,
|
|
24101
24713
|
ghInstalled,
|
|
@@ -24104,12 +24716,17 @@ function mmiDoctorDeps() {
|
|
|
24104
24716
|
isOrgRepo: () => isOrgRepoRoot(),
|
|
24105
24717
|
installedPluginVersion: installedClaudePluginVersion,
|
|
24106
24718
|
pluginGuardState: claudePluginGuardState,
|
|
24107
|
-
releasedVersion: fetchNpmReleasedVersion,
|
|
24719
|
+
releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
|
|
24720
|
+
releasedVersionNote: throttled ? throttled.note : void 0,
|
|
24108
24721
|
// #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
|
|
24109
|
-
|
|
24722
|
+
// #3489: serialised. Both heals mutate machine-global state (the npm global prefix; the Claude
|
|
24723
|
+
// marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
|
|
24724
|
+
// rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
|
|
24725
|
+
// carries a CLI shim, so a concurrent npm global install races the same PATH surface.
|
|
24726
|
+
updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
|
|
24110
24727
|
// #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
|
|
24111
24728
|
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
24112
|
-
healPlugin: () => healClaudePluginForDoctor(),
|
|
24729
|
+
healPlugin: () => withEnvHealLock("Claude plugin reinstall", () => healClaudePluginForDoctor()),
|
|
24113
24730
|
currentCliVersion: resolveClientVersion,
|
|
24114
24731
|
readGitignore,
|
|
24115
24732
|
writeGitignore,
|
|
@@ -24145,9 +24762,9 @@ function mmiDoctorDeps() {
|
|
|
24145
24762
|
},
|
|
24146
24763
|
pluginCache: () => {
|
|
24147
24764
|
const plan = buildPluginCachePlan(
|
|
24148
|
-
(0,
|
|
24765
|
+
(0, import_node_os9.homedir)(),
|
|
24149
24766
|
runningPluginVersion(process.env, resolveClientVersion()),
|
|
24150
|
-
pluginCacheFsDeps((0,
|
|
24767
|
+
pluginCacheFsDeps((0, import_node_os9.homedir)(), () => 0)
|
|
24151
24768
|
);
|
|
24152
24769
|
return {
|
|
24153
24770
|
stale: plan.prune,
|
|
@@ -24157,16 +24774,45 @@ function mmiDoctorDeps() {
|
|
|
24157
24774
|
stagingBytes: plan.stagingBytes
|
|
24158
24775
|
};
|
|
24159
24776
|
},
|
|
24777
|
+
// #3485 wave 2 item 11: the --apply reap, through the SAME plan + reaper `plugin prune --apply` drives.
|
|
24778
|
+
// The plan's own keep-policy refuses the running and newest versions, so this cannot delete what is
|
|
24779
|
+
// loaded. Under the #3489 env-heal lock: deleting cache dirs while another doctor is mid-reinstall is
|
|
24780
|
+
// the same machine-global hazard the lock exists for.
|
|
24781
|
+
prunePluginCache: () => withEnvHealLock("plugin cache prune", async () => {
|
|
24782
|
+
const plan = buildPluginCachePlan(
|
|
24783
|
+
(0, import_node_os9.homedir)(),
|
|
24784
|
+
runningPluginVersion(process.env, resolveClientVersion()),
|
|
24785
|
+
pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
|
|
24786
|
+
{ withBytes: true }
|
|
24787
|
+
);
|
|
24788
|
+
const result = applyPluginCachePlan(
|
|
24789
|
+
plan,
|
|
24790
|
+
(p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }),
|
|
24791
|
+
stagingApplyFsGuard((0, import_node_os9.homedir)())
|
|
24792
|
+
);
|
|
24793
|
+
const removed = [...result.removed, ...result.removedStaging ?? []];
|
|
24794
|
+
if (result.failed.length) {
|
|
24795
|
+
return {
|
|
24796
|
+
ok: false,
|
|
24797
|
+
removed,
|
|
24798
|
+
detail: `${removed.length} removed, ${result.failed.length} failed \u2014 ${result.failed.map((f) => `${f.version}: ${f.error}`).join("; ")}`
|
|
24799
|
+
};
|
|
24800
|
+
}
|
|
24801
|
+
return { ok: true, removed, detail: removed.length ? `removed ${removed.join(", ")}` : "nothing to prune" };
|
|
24802
|
+
}),
|
|
24160
24803
|
// #3008: the schedules-notebook drift probe, compacted for the check. Full doctor only — the gather in
|
|
24161
24804
|
// runDoctorClean skips this dep entirely on fast/banner/preflight runs.
|
|
24162
24805
|
schedulesNotebook: async () => {
|
|
24163
|
-
const { entries, incomplete, drift } = await
|
|
24806
|
+
const { entries, incomplete, drift, reconciliation } = await notebookOnce();
|
|
24807
|
+
const repoName = await repoSlug().catch(() => "");
|
|
24808
|
+
const clearableHere = repoName ? reconciliation.filter((d) => driftClearableFrom(d, repoName)).length : 0;
|
|
24164
24809
|
return {
|
|
24165
24810
|
armed: entries.length,
|
|
24166
24811
|
live: entries.filter((e) => e.resolved === "live").length,
|
|
24167
24812
|
declared: entries.filter((e) => e.resolved === "declared").length,
|
|
24168
24813
|
incomplete,
|
|
24169
|
-
drift
|
|
24814
|
+
drift,
|
|
24815
|
+
clearableHere
|
|
24170
24816
|
};
|
|
24171
24817
|
},
|
|
24172
24818
|
// #3075: the docs-audit dead-man verdict probe, compacted for the check. Full doctor only, exactly like
|
|
@@ -24175,7 +24821,8 @@ function mmiDoctorDeps() {
|
|
|
24175
24821
|
docsAudit: async () => {
|
|
24176
24822
|
const repo = await currentRepoFullName();
|
|
24177
24823
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
24178
|
-
const
|
|
24824
|
+
const armedAt = await docsJanitorArmedAt(repo).catch(() => void 0);
|
|
24825
|
+
const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today, armedAt });
|
|
24179
24826
|
return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
|
|
24180
24827
|
},
|
|
24181
24828
|
// #3471: the competing-worktrees-roots probe. Full doctor only (the gather skips it on
|
|
@@ -24184,7 +24831,21 @@ function mmiDoctorDeps() {
|
|
|
24184
24831
|
worktreeRoots: async () => worktreeRootsProbe(await repoRoot()),
|
|
24185
24832
|
// #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
|
|
24186
24833
|
// A local record read — cheap enough for every lane, including the banner.
|
|
24187
|
-
sessionPayload: () => readSessionPayload(process.cwd())
|
|
24834
|
+
sessionPayload: () => readSessionPayload(process.cwd()),
|
|
24835
|
+
// #3485 items 9 and 8: why the MMI plugin has never printed an "updated — please restart" notice, and
|
|
24836
|
+
// which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
|
|
24837
|
+
marketplaceRows: () => {
|
|
24838
|
+
try {
|
|
24839
|
+
const home = (0, import_node_os9.homedir)();
|
|
24840
|
+
return marketplaceRows(
|
|
24841
|
+
MMI_MARKETPLACE_NAME,
|
|
24842
|
+
readFileSyncSafe((0, import_node_path30.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
|
|
24843
|
+
readFileSyncSafe((0, import_node_path30.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
|
|
24844
|
+
);
|
|
24845
|
+
} catch {
|
|
24846
|
+
return [];
|
|
24847
|
+
}
|
|
24848
|
+
}
|
|
24188
24849
|
};
|
|
24189
24850
|
}
|
|
24190
24851
|
async function requireFreshTrainCli(commandName) {
|
|
@@ -24309,19 +24970,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
24309
24970
|
});
|
|
24310
24971
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
24311
24972
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
24312
|
-
const path2 = (0,
|
|
24313
|
-
const current = (0,
|
|
24973
|
+
const path2 = (0, import_node_path30.join)(process.cwd(), ".gitignore");
|
|
24974
|
+
const current = (0, import_node_fs32.existsSync)(path2) ? (0, import_node_fs32.readFileSync)(path2, "utf8") : null;
|
|
24314
24975
|
const plan = planManagedGitignore(current);
|
|
24315
24976
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
24316
24977
|
if (opts.json) {
|
|
24317
|
-
if (opts.write && plan.changed) (0,
|
|
24978
|
+
if (opts.write && plan.changed) (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
|
|
24318
24979
|
console.log(JSON.stringify(plan, null, 2));
|
|
24319
24980
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
24320
24981
|
return;
|
|
24321
24982
|
}
|
|
24322
24983
|
if (opts.write) {
|
|
24323
24984
|
if (plan.changed) {
|
|
24324
|
-
(0,
|
|
24985
|
+
(0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
|
|
24325
24986
|
console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
|
|
24326
24987
|
} else {
|
|
24327
24988
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
@@ -24447,8 +25108,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
24447
25108
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
24448
25109
|
let root;
|
|
24449
25110
|
if (o.root !== void 0) {
|
|
24450
|
-
root = (0,
|
|
24451
|
-
if (!(0,
|
|
25111
|
+
root = (0, import_node_path30.resolve)(o.root);
|
|
25112
|
+
if (!(0, import_node_fs32.existsSync)(root) || !(0, import_node_fs32.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
|
|
24452
25113
|
const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
24453
25114
|
if (isPathUnderDirectory(gcRepoRoot, root)) {
|
|
24454
25115
|
return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
|
|
@@ -24522,26 +25183,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
24522
25183
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
24523
25184
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
24524
25185
|
const take = () => {
|
|
24525
|
-
const fd = (0,
|
|
25186
|
+
const fd = (0, import_node_fs32.openSync)(lockPath, "wx");
|
|
24526
25187
|
try {
|
|
24527
|
-
(0,
|
|
25188
|
+
(0, import_node_fs32.writeSync)(fd, String(Date.now()));
|
|
24528
25189
|
} finally {
|
|
24529
|
-
(0,
|
|
25190
|
+
(0, import_node_fs32.closeSync)(fd);
|
|
24530
25191
|
}
|
|
24531
25192
|
return () => {
|
|
24532
25193
|
try {
|
|
24533
|
-
(0,
|
|
25194
|
+
(0, import_node_fs32.rmSync)(lockPath, { force: true });
|
|
24534
25195
|
} catch {
|
|
24535
25196
|
}
|
|
24536
25197
|
};
|
|
24537
25198
|
};
|
|
24538
25199
|
try {
|
|
24539
|
-
(0,
|
|
25200
|
+
(0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(lockPath), { recursive: true });
|
|
24540
25201
|
return take();
|
|
24541
25202
|
} catch {
|
|
24542
25203
|
try {
|
|
24543
|
-
if (Date.now() - (0,
|
|
24544
|
-
(0,
|
|
25204
|
+
if (Date.now() - (0, import_node_fs32.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
25205
|
+
(0, import_node_fs32.rmSync)(lockPath, { force: true });
|
|
24545
25206
|
return take();
|
|
24546
25207
|
}
|
|
24547
25208
|
} catch {
|
|
@@ -24983,7 +25644,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
24983
25644
|
if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
24984
25645
|
if (o.secretsFile) {
|
|
24985
25646
|
try {
|
|
24986
|
-
vars.push(`secrets=${(0,
|
|
25647
|
+
vars.push(`secrets=${(0, import_node_fs32.readFileSync)(o.secretsFile, "utf8")}`);
|
|
24987
25648
|
} catch (e) {
|
|
24988
25649
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
24989
25650
|
}
|
|
@@ -25099,8 +25760,8 @@ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY ro
|
|
|
25099
25760
|
const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
|
|
25100
25761
|
if (branch) {
|
|
25101
25762
|
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
|
|
25102
|
-
const
|
|
25103
|
-
if (
|
|
25763
|
+
const sameRepo2 = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
|
|
25764
|
+
if (sameRepo2) {
|
|
25104
25765
|
const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
|
|
25105
25766
|
if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
|
|
25106
25767
|
${filelessTransitionGuide(target, o.stage)}`);
|
|
@@ -25297,8 +25958,8 @@ withExamples(mutating(
|
|
|
25297
25958
|
let targetRepo2;
|
|
25298
25959
|
try {
|
|
25299
25960
|
issueType = resolveCreateType(o.type, "issue create", o.label);
|
|
25300
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
25301
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
25961
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25962
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25302
25963
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
25303
25964
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
25304
25965
|
extraLabels = [...o.label ?? []];
|
|
@@ -25441,7 +26102,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
|
|
|
25441
26102
|
}
|
|
25442
26103
|
let body;
|
|
25443
26104
|
try {
|
|
25444
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
26105
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25445
26106
|
} catch (e) {
|
|
25446
26107
|
return fail(`issue comment: ${e.message}`);
|
|
25447
26108
|
}
|
|
@@ -25501,8 +26162,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
25501
26162
|
let title;
|
|
25502
26163
|
const sourceRepo = o.repo ?? await resolveRepo(void 0);
|
|
25503
26164
|
try {
|
|
25504
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
25505
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
26165
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
26166
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25506
26167
|
priority = resolveCreatePriority(o.priority, "report");
|
|
25507
26168
|
if (!ISSUE_TYPES.includes(o.type)) {
|
|
25508
26169
|
throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
@@ -25551,8 +26212,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
25551
26212
|
let args;
|
|
25552
26213
|
try {
|
|
25553
26214
|
skill = assertSkillName(o.skill);
|
|
25554
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
25555
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
26215
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
26216
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25556
26217
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
25557
26218
|
priority = resolveCreatePriority(o.priority, "skill-lesson");
|
|
25558
26219
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -25603,8 +26264,8 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
25603
26264
|
let body;
|
|
25604
26265
|
let title;
|
|
25605
26266
|
try {
|
|
25606
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
25607
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
26267
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
26268
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25608
26269
|
} catch (e) {
|
|
25609
26270
|
return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
25610
26271
|
}
|
|
@@ -25639,11 +26300,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
25639
26300
|
}
|
|
25640
26301
|
});
|
|
25641
26302
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
25642
|
-
const wfDir = (0,
|
|
25643
|
-
if (!(0,
|
|
25644
|
-
return (0,
|
|
26303
|
+
const wfDir = (0, import_node_path30.join)(cwd, ".github", "workflows");
|
|
26304
|
+
if (!(0, import_node_fs32.existsSync)(wfDir)) return [];
|
|
26305
|
+
return (0, import_node_fs32.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
25645
26306
|
try {
|
|
25646
|
-
return workflowReportsPrChecks((0,
|
|
26307
|
+
return workflowReportsPrChecks((0, import_node_fs32.readFileSync)((0, import_node_path30.join)(wfDir, name), "utf8"));
|
|
25647
26308
|
} catch {
|
|
25648
26309
|
return true;
|
|
25649
26310
|
}
|
|
@@ -25670,16 +26331,16 @@ function ciAuditDeps() {
|
|
|
25670
26331
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
25671
26332
|
readSeedFile: (path2) => {
|
|
25672
26333
|
if (!root) return null;
|
|
25673
|
-
const fullPath = (0,
|
|
25674
|
-
return (0,
|
|
26334
|
+
const fullPath = (0, import_node_path30.join)(root, path2);
|
|
26335
|
+
return (0, import_node_fs32.existsSync)(fullPath) ? (0, import_node_fs32.readFileSync)(fullPath, "utf8") : null;
|
|
25675
26336
|
}
|
|
25676
26337
|
};
|
|
25677
26338
|
}
|
|
25678
26339
|
function hubRoot() {
|
|
25679
|
-
const fromPkg = (0,
|
|
26340
|
+
const fromPkg = (0, import_node_path30.join)(__dirname, "..", "..");
|
|
25680
26341
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
25681
|
-
if ((0,
|
|
25682
|
-
if ((0,
|
|
26342
|
+
if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(fromPkg, marker))) return fromPkg;
|
|
26343
|
+
if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(process.cwd(), marker))) return process.cwd();
|
|
25683
26344
|
return null;
|
|
25684
26345
|
}
|
|
25685
26346
|
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
@@ -25736,11 +26397,18 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
25736
26397
|
const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
|
|
25737
26398
|
const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim();
|
|
25738
26399
|
const [repoFromGh, base] = viewed.split(/\s+/);
|
|
25739
|
-
if (base && base !== "development") {
|
|
25740
|
-
throw new Error(`pr land: base branch must be development (got ${base}) \u2014 promotion merges stay human-only`);
|
|
25741
|
-
}
|
|
25742
26400
|
const repo = repoOpt ?? repoFromGh;
|
|
25743
26401
|
if (!repo) throw new Error("pr land: could not resolve PR repo");
|
|
26402
|
+
let track;
|
|
26403
|
+
try {
|
|
26404
|
+
const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
|
|
26405
|
+
if (meta) track = resolveReleaseTrack(meta, void 0, repo);
|
|
26406
|
+
} catch {
|
|
26407
|
+
}
|
|
26408
|
+
if (isPromotionBase(base, track)) {
|
|
26409
|
+
const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
|
|
26410
|
+
throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
|
|
26411
|
+
}
|
|
25744
26412
|
return repo;
|
|
25745
26413
|
},
|
|
25746
26414
|
fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
|
|
@@ -25929,7 +26597,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
25929
26597
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
25930
26598
|
beforeWorktrees,
|
|
25931
26599
|
startingPath,
|
|
25932
|
-
pathExists: (p) => (0,
|
|
26600
|
+
pathExists: (p) => (0, import_node_fs32.existsSync)(p),
|
|
25933
26601
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
25934
26602
|
teardownWorktreeStage,
|
|
25935
26603
|
deferredStore,
|
|
@@ -26039,8 +26707,8 @@ function trainApplyDeps() {
|
|
|
26039
26707
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
26040
26708
|
announce: (args) => announceRelease({
|
|
26041
26709
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
26042
|
-
readFile: (path2) => (0,
|
|
26043
|
-
removeFile: (path2) => (0,
|
|
26710
|
+
readFile: (path2) => (0, import_promises8.readFile)(path2, "utf8"),
|
|
26711
|
+
removeFile: (path2) => (0, import_promises8.unlink)(path2)
|
|
26044
26712
|
}, args),
|
|
26045
26713
|
fetchEdgeDomains: async (slug) => {
|
|
26046
26714
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -26270,10 +26938,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
26270
26938
|
targets = resolution.targets;
|
|
26271
26939
|
}
|
|
26272
26940
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
26273
|
-
const fileMatrix = (0,
|
|
26941
|
+
const fileMatrix = (0, import_node_fs32.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs32.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
26274
26942
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
26275
26943
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
26276
|
-
const fileContracts = (0,
|
|
26944
|
+
const fileContracts = (0, import_node_fs32.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs32.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
26277
26945
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
26278
26946
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
26279
26947
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -26281,7 +26949,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
26281
26949
|
});
|
|
26282
26950
|
access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
|
|
26283
26951
|
var isWin2 = process.platform === "win32";
|
|
26284
|
-
program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin-heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--
|
|
26952
|
+
program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin-heal (env repairs only \u2014 never the repo working tree) with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").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 train preflights)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nThree rows (worktree roots, schedules, docs-audit) are report-only: they still print \u2717 with their fix,\nbut never move the exit code, because their remedy is a deliberate human review or lives in the repo\nthat owns the fault. Every other red row gates, including ones only you can clear by hand (#3485).\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
|
|
26285
26953
|
if (opts.guide) {
|
|
26286
26954
|
consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
|
|
26287
26955
|
return;
|
|
@@ -26307,16 +26975,16 @@ function directoryBytes(path2) {
|
|
|
26307
26975
|
let total = 0;
|
|
26308
26976
|
let entries;
|
|
26309
26977
|
try {
|
|
26310
|
-
entries = (0,
|
|
26978
|
+
entries = (0, import_node_fs32.readdirSync)(path2, { withFileTypes: true });
|
|
26311
26979
|
} catch {
|
|
26312
26980
|
return 0;
|
|
26313
26981
|
}
|
|
26314
26982
|
for (const entry of entries) {
|
|
26315
|
-
const child2 = (0,
|
|
26983
|
+
const child2 = (0, import_node_path30.join)(path2, entry.name);
|
|
26316
26984
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
26317
26985
|
else {
|
|
26318
26986
|
try {
|
|
26319
|
-
total += (0,
|
|
26987
|
+
total += (0, import_node_fs32.statSync)(child2).size;
|
|
26320
26988
|
} catch {
|
|
26321
26989
|
}
|
|
26322
26990
|
}
|
|
@@ -26324,25 +26992,25 @@ function directoryBytes(path2) {
|
|
|
26324
26992
|
return total;
|
|
26325
26993
|
}
|
|
26326
26994
|
function listDirEntries(dir) {
|
|
26327
|
-
return (0,
|
|
26995
|
+
return (0, import_node_fs32.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
26328
26996
|
}
|
|
26329
26997
|
function readInstalledPluginRefs(home) {
|
|
26330
26998
|
const p = installedPluginsPath(home);
|
|
26331
|
-
if (!(0,
|
|
26999
|
+
if (!(0, import_node_fs32.existsSync)(p)) return [];
|
|
26332
27000
|
try {
|
|
26333
|
-
return installedPluginPaths((0,
|
|
27001
|
+
return installedPluginPaths((0, import_node_fs32.readFileSync)(p, "utf8"));
|
|
26334
27002
|
} catch {
|
|
26335
27003
|
return null;
|
|
26336
27004
|
}
|
|
26337
27005
|
}
|
|
26338
27006
|
function pluginCacheFsDeps(home, dirBytes) {
|
|
26339
27007
|
return {
|
|
26340
|
-
exists: (p) => (0,
|
|
26341
|
-
listVersionDirs: (root) => (0,
|
|
27008
|
+
exists: (p) => (0, import_node_fs32.existsSync)(p),
|
|
27009
|
+
listVersionDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
26342
27010
|
dirBytes,
|
|
26343
|
-
listStagingDirs: (root) => (0,
|
|
27011
|
+
listStagingDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
26344
27012
|
try {
|
|
26345
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
27013
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path30.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs32.statSync)(p).mtimeMs) };
|
|
26346
27014
|
} catch {
|
|
26347
27015
|
return { name: d.name, mtimeMs: Date.now() };
|
|
26348
27016
|
}
|
|
@@ -26356,10 +27024,10 @@ function stagingApplyFsGuard(home) {
|
|
|
26356
27024
|
return {
|
|
26357
27025
|
referencedPaths: () => readInstalledPluginRefs(home),
|
|
26358
27026
|
mtimeMs: (name) => {
|
|
26359
|
-
const p = (0,
|
|
26360
|
-
if (!(0,
|
|
27027
|
+
const p = (0, import_node_path30.join)(stagingRoot, name);
|
|
27028
|
+
if (!(0, import_node_fs32.existsSync)(p)) return null;
|
|
26361
27029
|
try {
|
|
26362
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
27030
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs32.statSync)(q).mtimeMs);
|
|
26363
27031
|
} catch {
|
|
26364
27032
|
return null;
|
|
26365
27033
|
}
|
|
@@ -26369,13 +27037,13 @@ function stagingApplyFsGuard(home) {
|
|
|
26369
27037
|
}
|
|
26370
27038
|
program2.command("plugin-prune").description(`prune stale cached MMI plugin versions (keeps running + newest, ${PLUGIN_CACHE_KEEP} total) and orphaned temp_git_* staging dirs; dry-run unless --apply (#2903, #2990)`).option("--apply", "actually delete the stale version dirs + orphaned staging dirs (default: report only)").option("--json", "machine-readable output").action((o) => {
|
|
26371
27039
|
const plan = buildPluginCachePlan(
|
|
26372
|
-
(0,
|
|
27040
|
+
(0, import_node_os9.homedir)(),
|
|
26373
27041
|
runningPluginVersion(process.env, resolveClientVersion()),
|
|
26374
|
-
pluginCacheFsDeps((0,
|
|
27042
|
+
pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
|
|
26375
27043
|
{ withBytes: true }
|
|
26376
27044
|
);
|
|
26377
27045
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
26378
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
27046
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os9.homedir)())) : void 0;
|
|
26379
27047
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
26380
27048
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
26381
27049
|
else console.log(renderPluginCachePlan(plan, result));
|
|
@@ -26404,6 +27072,8 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
26404
27072
|
});
|
|
26405
27073
|
const line = whoamiLine(report, report.login ? ghMultiAccountCaveat(report.login) : void 0);
|
|
26406
27074
|
if (line) io.log(line);
|
|
27075
|
+
const authority = authorityLine(report);
|
|
27076
|
+
if (authority) io.log(authority);
|
|
26407
27077
|
},
|
|
26408
27078
|
boardSlice: (io) => runBoardSlice(io, {
|
|
26409
27079
|
loadConfig: () => loadConfigForRepo(),
|
|
@@ -26433,8 +27103,10 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
26433
27103
|
const line = localTrainSyncBannerLine(result);
|
|
26434
27104
|
if (line) io.log(line);
|
|
26435
27105
|
},
|
|
27106
|
+
// #3485 item 7: the ONLY lane that throttles the npm read — it runs on every session start, on a
|
|
27107
|
+
// blocking hook. Every interactive lane still reads live.
|
|
26436
27108
|
doctor: async (io) => {
|
|
26437
|
-
await runDoctorClean({ banner: true }, io, mmiDoctorDeps());
|
|
27109
|
+
await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true }));
|
|
26438
27110
|
}
|
|
26439
27111
|
});
|
|
26440
27112
|
await runSessionStart(parallel, sequential, bannerIo);
|
|
@@ -26457,6 +27129,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
|
|
|
26457
27129
|
DEFAULT_PRIORITY,
|
|
26458
27130
|
awsCallerArn,
|
|
26459
27131
|
classifyParseError,
|
|
27132
|
+
envHealLockPath,
|
|
26460
27133
|
gcPlan,
|
|
26461
27134
|
isOrgRegisteredRepo,
|
|
26462
27135
|
registryClientDeps,
|