@mutmutco/cli 3.50.0 → 3.52.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 +1034 -441
- 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
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
function pathsAreCaseInsensitive(platform2 = process.platform) {
|
|
5744
|
+
return platform2 === "win32" || platform2 === "darwin";
|
|
5706
5745
|
}
|
|
5707
|
-
function normPath(p) {
|
|
5708
|
-
|
|
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 = {
|
|
@@ -7443,7 +7489,7 @@ async function worktreeBranches() {
|
|
|
7443
7489
|
let dirty = true;
|
|
7444
7490
|
try {
|
|
7445
7491
|
const { stdout: status } = await execFileP2("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7446
|
-
dirty = status
|
|
7492
|
+
dirty = !isRemovableDirt(classifyWorktreeDirt(status));
|
|
7447
7493
|
} catch {
|
|
7448
7494
|
dirty = true;
|
|
7449
7495
|
}
|
|
@@ -7859,6 +7905,12 @@ function parseIssueSelector(selector, defaultRepo) {
|
|
|
7859
7905
|
if (local) return { repo: defaultRepo, number: Number(local[1]) };
|
|
7860
7906
|
throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
|
|
7861
7907
|
}
|
|
7908
|
+
function sameRepo(itemRepo, selectorKey) {
|
|
7909
|
+
const repo = itemRepo.toLowerCase();
|
|
7910
|
+
if (repo === selectorKey) return true;
|
|
7911
|
+
if (selectorKey.includes("/")) return false;
|
|
7912
|
+
return repo.slice(repo.indexOf("/") + 1) === selectorKey;
|
|
7913
|
+
}
|
|
7862
7914
|
function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
|
|
7863
7915
|
const empty = () => ({ userOwned: [], claimable: [], taken: [], unownedInFlight: [] });
|
|
7864
7916
|
const groups = { primary: empty(), secondary: empty() };
|
|
@@ -7866,7 +7918,7 @@ function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
|
|
|
7866
7918
|
const repoKey = currentRepo.toLowerCase();
|
|
7867
7919
|
for (const item of items) {
|
|
7868
7920
|
if (!ACTIVE_STATUSES.has(item.status)) continue;
|
|
7869
|
-
const scope = item.repository
|
|
7921
|
+
const scope = sameRepo(item.repository, repoKey) ? "primary" : "secondary";
|
|
7870
7922
|
const assignees = item.assignees.map((a) => a.toLowerCase());
|
|
7871
7923
|
const assignedToViewer = assignees.includes(viewerKey);
|
|
7872
7924
|
if (assignedToViewer) {
|
|
@@ -8413,11 +8465,11 @@ var defaultRetrySleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, m
|
|
|
8413
8465
|
async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
|
|
8414
8466
|
const attempts = Math.max(1, opts.attempts ?? 5);
|
|
8415
8467
|
const delayMs = opts.delayMs ?? 300;
|
|
8416
|
-
const
|
|
8468
|
+
const sleep3 = opts.sleep ?? defaultRetrySleep;
|
|
8417
8469
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
8418
8470
|
const itemId = (await fetchIssueProjectItem(client, cfg, selector)).item?.itemId;
|
|
8419
8471
|
if (itemId) return itemId;
|
|
8420
|
-
if (attempt < attempts - 1) await
|
|
8472
|
+
if (attempt < attempts - 1) await sleep3(delayMs * (attempt + 1));
|
|
8421
8473
|
}
|
|
8422
8474
|
return void 0;
|
|
8423
8475
|
}
|
|
@@ -9000,9 +9052,196 @@ async function gatherStaleWorktreeWarning(gitRun = defaultGitRun) {
|
|
|
9000
9052
|
}
|
|
9001
9053
|
}
|
|
9002
9054
|
|
|
9003
|
-
// src/
|
|
9055
|
+
// src/released-version-cache.ts
|
|
9004
9056
|
var import_node_fs12 = require("node:fs");
|
|
9005
9057
|
var import_node_path10 = require("node:path");
|
|
9058
|
+
var RELEASED_VERSION_CACHE_MS = 24 * 36e5;
|
|
9059
|
+
function releasedVersionCachePath(runtimeRoot) {
|
|
9060
|
+
return (0, import_node_path10.join)(runtimeRoot, "head-ts", ".released-version");
|
|
9061
|
+
}
|
|
9062
|
+
function readReleasedVersionCache(cachePath, now = Date.now(), read = import_node_fs12.readFileSync) {
|
|
9063
|
+
let parsed;
|
|
9064
|
+
try {
|
|
9065
|
+
parsed = JSON.parse(read(cachePath, "utf8"));
|
|
9066
|
+
} catch {
|
|
9067
|
+
return void 0;
|
|
9068
|
+
}
|
|
9069
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
9070
|
+
const { version, at } = parsed;
|
|
9071
|
+
if (typeof version !== "string" || !version.trim()) return void 0;
|
|
9072
|
+
if (typeof at !== "number" || !Number.isFinite(at)) return void 0;
|
|
9073
|
+
const age = now - at;
|
|
9074
|
+
if (age < 0 || age >= RELEASED_VERSION_CACHE_MS) return void 0;
|
|
9075
|
+
return { version, at };
|
|
9076
|
+
}
|
|
9077
|
+
function writeReleasedVersionCache(cachePath, version, now = Date.now()) {
|
|
9078
|
+
try {
|
|
9079
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(cachePath), { recursive: true });
|
|
9080
|
+
(0, import_node_fs12.writeFileSync)(cachePath, JSON.stringify({ version, at: now }), "utf8");
|
|
9081
|
+
} catch {
|
|
9082
|
+
}
|
|
9083
|
+
}
|
|
9084
|
+
function cacheAgeHours(at, now = Date.now()) {
|
|
9085
|
+
return Math.max(0, Math.floor((now - at) / 36e5));
|
|
9086
|
+
}
|
|
9087
|
+
function cachedReadNote(cachedAt, now = Date.now()) {
|
|
9088
|
+
const hours = cacheAgeHours(cachedAt, now);
|
|
9089
|
+
return `(cached ${hours < 1 ? "<1" : hours}h ago \u2014 the banner reads npm at most once a day)`;
|
|
9090
|
+
}
|
|
9091
|
+
|
|
9092
|
+
// src/marketplace-autoupdate.ts
|
|
9093
|
+
var KNOWN_MARKETPLACES_RELATIVE = [".claude", "plugins", "known_marketplaces.json"];
|
|
9094
|
+
var MMI_MARKETPLACE_NAME = "mutmutco";
|
|
9095
|
+
function readFileSyncSafe(path2, read) {
|
|
9096
|
+
try {
|
|
9097
|
+
return read(path2, "utf8");
|
|
9098
|
+
} catch {
|
|
9099
|
+
return void 0;
|
|
9100
|
+
}
|
|
9101
|
+
}
|
|
9102
|
+
var AUTO_UPDATE_DEFAULT_ON = /* @__PURE__ */ new Set([
|
|
9103
|
+
"claude-plugins-official",
|
|
9104
|
+
"claude-code-marketplace",
|
|
9105
|
+
"claude-code-plugins",
|
|
9106
|
+
"anthropic-marketplace",
|
|
9107
|
+
"anthropic-plugins",
|
|
9108
|
+
"agent-skills",
|
|
9109
|
+
"anthropic-agent-skills"
|
|
9110
|
+
]);
|
|
9111
|
+
function resolveAutoUpdate(probe) {
|
|
9112
|
+
const { name, registered, declared, settingsDeclared } = probe;
|
|
9113
|
+
if (!registered) {
|
|
9114
|
+
return { name, effective: false, reason: "not registered \u2014 nothing to update" };
|
|
9115
|
+
}
|
|
9116
|
+
if (typeof declared === "boolean") {
|
|
9117
|
+
return { name, effective: declared, reason: `declared ${declared} in known_marketplaces.json` };
|
|
9118
|
+
}
|
|
9119
|
+
if (typeof settingsDeclared === "boolean") {
|
|
9120
|
+
return { name, effective: settingsDeclared, reason: `declared ${settingsDeclared} under extraKnownMarketplaces.${name}` };
|
|
9121
|
+
}
|
|
9122
|
+
if (AUTO_UPDATE_DEFAULT_ON.has(name.toLowerCase())) {
|
|
9123
|
+
return { name, effective: true, reason: "undeclared \u2014 an official Anthropic marketplace, on by default" };
|
|
9124
|
+
}
|
|
9125
|
+
return {
|
|
9126
|
+
name,
|
|
9127
|
+
effective: false,
|
|
9128
|
+
reason: "undeclared \u2014 third-party marketplaces are OFF by default, so nothing auto-updates and no update notice ever prints"
|
|
9129
|
+
};
|
|
9130
|
+
}
|
|
9131
|
+
function readKnownMarketplace(raw, name) {
|
|
9132
|
+
if (!raw) return { registered: false };
|
|
9133
|
+
let parsed;
|
|
9134
|
+
try {
|
|
9135
|
+
parsed = JSON.parse(raw);
|
|
9136
|
+
} catch {
|
|
9137
|
+
return { registered: false };
|
|
9138
|
+
}
|
|
9139
|
+
if (!parsed || typeof parsed !== "object") return { registered: false };
|
|
9140
|
+
const entry = parsed[name];
|
|
9141
|
+
if (!entry || typeof entry !== "object") return { registered: false };
|
|
9142
|
+
const { autoUpdate, source } = entry;
|
|
9143
|
+
const ref = source && typeof source === "object" ? source.ref : void 0;
|
|
9144
|
+
return {
|
|
9145
|
+
registered: true,
|
|
9146
|
+
...typeof autoUpdate === "boolean" ? { declared: autoUpdate } : {},
|
|
9147
|
+
...typeof ref === "string" && ref ? { ref } : {}
|
|
9148
|
+
};
|
|
9149
|
+
}
|
|
9150
|
+
function readSettingsAutoUpdate(raw, name) {
|
|
9151
|
+
if (!raw) return void 0;
|
|
9152
|
+
let parsed;
|
|
9153
|
+
try {
|
|
9154
|
+
parsed = JSON.parse(raw);
|
|
9155
|
+
} catch {
|
|
9156
|
+
return void 0;
|
|
9157
|
+
}
|
|
9158
|
+
const extra = parsed?.extraKnownMarketplaces;
|
|
9159
|
+
if (!extra || typeof extra !== "object") return void 0;
|
|
9160
|
+
const entry = extra[name];
|
|
9161
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
9162
|
+
const { autoUpdate } = entry;
|
|
9163
|
+
return typeof autoUpdate === "boolean" ? autoUpdate : void 0;
|
|
9164
|
+
}
|
|
9165
|
+
var AUTO_UPDATE_TOGGLE_STEPS = "`/plugin` \u2192 Marketplaces tab \u2192 select the marketplace \u2192 Enable auto-update";
|
|
9166
|
+
var CATALOG_CONTENT_REF = "main";
|
|
9167
|
+
var CATALOG_REF_PIN_STEPS = `add \`"ref": "${CATALOG_CONTENT_REF}"\` to this marketplace's \`source\` in ~/.claude/plugins/known_marketplaces.json, then restart Claude`;
|
|
9168
|
+
function resolveCatalogRef(probe) {
|
|
9169
|
+
const { name, registered, ref, autoUpdate } = probe;
|
|
9170
|
+
if (!registered) return void 0;
|
|
9171
|
+
if (ref) {
|
|
9172
|
+
const agrees = ref === CATALOG_CONTENT_REF;
|
|
9173
|
+
return {
|
|
9174
|
+
name,
|
|
9175
|
+
ref,
|
|
9176
|
+
autoUpdate,
|
|
9177
|
+
unpinned: false,
|
|
9178
|
+
agrees,
|
|
9179
|
+
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`
|
|
9180
|
+
};
|
|
9181
|
+
}
|
|
9182
|
+
return {
|
|
9183
|
+
name,
|
|
9184
|
+
autoUpdate,
|
|
9185
|
+
unpinned: true,
|
|
9186
|
+
agrees: false,
|
|
9187
|
+
reason: `no ref pinned \u2014 the catalog is served from the repo's default branch while plugin content is pinned to ${CATALOG_CONTENT_REF}`
|
|
9188
|
+
};
|
|
9189
|
+
}
|
|
9190
|
+
function checkMarketplaceAutoUpdate(probe) {
|
|
9191
|
+
if (!probe) return null;
|
|
9192
|
+
const evidence = [`${probe.name}: auto-update ${probe.effective ? "on" : "off"} \u2014 ${probe.reason}`];
|
|
9193
|
+
if (probe.effective) {
|
|
9194
|
+
return { ok: true, label: `marketplace auto-update (${probe.name})`, detail: "on", verbose: evidence };
|
|
9195
|
+
}
|
|
9196
|
+
return {
|
|
9197
|
+
ok: true,
|
|
9198
|
+
warn: true,
|
|
9199
|
+
label: `marketplace auto-update (${probe.name})`,
|
|
9200
|
+
detail: `off \u2014 this machine will not pick up a new plugin release on its own; turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`,
|
|
9201
|
+
verbose: evidence
|
|
9202
|
+
};
|
|
9203
|
+
}
|
|
9204
|
+
function checkMarketplaceCatalogRef(probe) {
|
|
9205
|
+
if (!probe) return null;
|
|
9206
|
+
const label = `marketplace catalog ref (${probe.name})`;
|
|
9207
|
+
const evidence = [`${probe.name}: ${probe.reason}`, `auto-update: ${probe.autoUpdate ? "on" : "off"}`];
|
|
9208
|
+
if (probe.agrees) {
|
|
9209
|
+
return { ok: true, label, detail: `${probe.ref}`, verbose: evidence };
|
|
9210
|
+
}
|
|
9211
|
+
const drift = probe.unpinned ? "unpinned" : `pinned to ${probe.ref}, not ${CATALOG_CONTENT_REF}`;
|
|
9212
|
+
if (probe.autoUpdate) {
|
|
9213
|
+
return {
|
|
9214
|
+
ok: false,
|
|
9215
|
+
label,
|
|
9216
|
+
fix: `${drift} while auto-update is ON \u2014 this machine installs plugin releases advertised by a branch nobody released; ${CATALOG_REF_PIN_STEPS}`,
|
|
9217
|
+
verbose: evidence
|
|
9218
|
+
};
|
|
9219
|
+
}
|
|
9220
|
+
return {
|
|
9221
|
+
ok: true,
|
|
9222
|
+
warn: true,
|
|
9223
|
+
label,
|
|
9224
|
+
detail: `${drift} \u2014 harmless while auto-update is off, and the thing to fix before turning it on; ${CATALOG_REF_PIN_STEPS}`,
|
|
9225
|
+
verbose: evidence
|
|
9226
|
+
};
|
|
9227
|
+
}
|
|
9228
|
+
function marketplaceRows(name, known, settings) {
|
|
9229
|
+
const { registered, declared, ref } = readKnownMarketplace(known, name);
|
|
9230
|
+
const autoUpdate = resolveAutoUpdate({
|
|
9231
|
+
name,
|
|
9232
|
+
registered,
|
|
9233
|
+
declared,
|
|
9234
|
+
settingsDeclared: readSettingsAutoUpdate(settings, name)
|
|
9235
|
+
});
|
|
9236
|
+
const rows = [checkMarketplaceAutoUpdate(autoUpdate), checkMarketplaceCatalogRef(
|
|
9237
|
+
resolveCatalogRef({ name, registered, ref, autoUpdate: autoUpdate.effective })
|
|
9238
|
+
)];
|
|
9239
|
+
return rows.filter((r) => r !== null);
|
|
9240
|
+
}
|
|
9241
|
+
|
|
9242
|
+
// src/hook-activity.ts
|
|
9243
|
+
var import_node_fs13 = require("node:fs");
|
|
9244
|
+
var import_node_path11 = require("node:path");
|
|
9006
9245
|
var DEFAULT_SURFACE = "claude";
|
|
9007
9246
|
function activityLogPath(cwd) {
|
|
9008
9247
|
return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
|
|
@@ -9015,16 +9254,16 @@ function appendHookActivity(cwd, entry) {
|
|
|
9015
9254
|
surface: DEFAULT_SURFACE,
|
|
9016
9255
|
...entry
|
|
9017
9256
|
};
|
|
9018
|
-
(0,
|
|
9019
|
-
(0,
|
|
9257
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(path2), { recursive: true });
|
|
9258
|
+
(0, import_node_fs13.appendFileSync)(path2, `${JSON.stringify(line)}
|
|
9020
9259
|
`, "utf8");
|
|
9021
9260
|
} catch {
|
|
9022
9261
|
}
|
|
9023
9262
|
}
|
|
9024
9263
|
|
|
9025
9264
|
// src/worktree.ts
|
|
9026
|
-
var
|
|
9027
|
-
var
|
|
9265
|
+
var import_node_fs14 = require("node:fs");
|
|
9266
|
+
var import_node_path12 = require("node:path");
|
|
9028
9267
|
var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
9029
9268
|
var PKG = "package.json";
|
|
9030
9269
|
var LOCKFILE = "package-lock.json";
|
|
@@ -9032,21 +9271,21 @@ var NODE_MODULES = "node_modules";
|
|
|
9032
9271
|
var realFsProbe = {
|
|
9033
9272
|
isDir: (p) => {
|
|
9034
9273
|
try {
|
|
9035
|
-
return (0,
|
|
9274
|
+
return (0, import_node_fs14.statSync)(p).isDirectory();
|
|
9036
9275
|
} catch {
|
|
9037
9276
|
return false;
|
|
9038
9277
|
}
|
|
9039
9278
|
},
|
|
9040
9279
|
isFile: (p) => {
|
|
9041
9280
|
try {
|
|
9042
|
-
return (0,
|
|
9281
|
+
return (0, import_node_fs14.statSync)(p).isFile();
|
|
9043
9282
|
} catch {
|
|
9044
9283
|
return false;
|
|
9045
9284
|
}
|
|
9046
9285
|
},
|
|
9047
9286
|
listDirs: (p) => {
|
|
9048
9287
|
try {
|
|
9049
|
-
return (0,
|
|
9288
|
+
return (0, import_node_fs14.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9050
9289
|
} catch {
|
|
9051
9290
|
return [];
|
|
9052
9291
|
}
|
|
@@ -9054,12 +9293,12 @@ var realFsProbe = {
|
|
|
9054
9293
|
};
|
|
9055
9294
|
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
9056
9295
|
const factsFor = (dir) => {
|
|
9057
|
-
const abs = dir ? (0,
|
|
9296
|
+
const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
|
|
9058
9297
|
return {
|
|
9059
9298
|
dir,
|
|
9060
|
-
hasPackageJson: fs2.isFile((0,
|
|
9061
|
-
hasLockfile: fs2.isFile((0,
|
|
9062
|
-
hasNodeModules: fs2.isDir((0,
|
|
9299
|
+
hasPackageJson: fs2.isFile((0, import_node_path12.join)(abs, PKG)),
|
|
9300
|
+
hasLockfile: fs2.isFile((0, import_node_path12.join)(abs, LOCKFILE)),
|
|
9301
|
+
hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES))
|
|
9063
9302
|
};
|
|
9064
9303
|
};
|
|
9065
9304
|
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
@@ -9069,7 +9308,7 @@ function npmInstallTargets(dirs) {
|
|
|
9069
9308
|
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
9070
9309
|
}
|
|
9071
9310
|
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
9072
|
-
return fs2.isFile((0,
|
|
9311
|
+
return fs2.isFile((0, import_node_path12.join)(root, ".git"));
|
|
9073
9312
|
}
|
|
9074
9313
|
function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
9075
9314
|
if (!isLinkedWorktree(root, fs2)) return null;
|
|
@@ -9079,8 +9318,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
|
9079
9318
|
return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
|
|
9080
9319
|
}
|
|
9081
9320
|
function defaultCopyFile(from, to) {
|
|
9082
|
-
(0,
|
|
9083
|
-
(0,
|
|
9321
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
|
|
9322
|
+
(0, import_node_fs14.copyFileSync)(from, to);
|
|
9084
9323
|
}
|
|
9085
9324
|
async function provisionWorktree(worktreeRoot, deps) {
|
|
9086
9325
|
const fs2 = deps.fs ?? realFsProbe;
|
|
@@ -9092,7 +9331,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
9092
9331
|
const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
|
|
9093
9332
|
const installed = [];
|
|
9094
9333
|
for (const target of targets) {
|
|
9095
|
-
const cwd = target.dir ? (0,
|
|
9334
|
+
const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
|
|
9096
9335
|
log(`installing deps: ${target.command} in ${target.dir || "."}`);
|
|
9097
9336
|
await deps.runInstall(target.command, cwd);
|
|
9098
9337
|
installed.push(target);
|
|
@@ -9101,7 +9340,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
9101
9340
|
const copySkipped = [];
|
|
9102
9341
|
const primary = await deps.primaryCheckout();
|
|
9103
9342
|
for (const rel of LOCAL_ONLY_FILES) {
|
|
9104
|
-
const dest = (0,
|
|
9343
|
+
const dest = (0, import_node_path12.join)(worktreeRoot, rel);
|
|
9105
9344
|
if (fs2.isFile(dest)) {
|
|
9106
9345
|
copySkipped.push({ file: rel, reason: "already-present" });
|
|
9107
9346
|
continue;
|
|
@@ -9110,24 +9349,30 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
9110
9349
|
copySkipped.push({ file: rel, reason: "no-primary" });
|
|
9111
9350
|
continue;
|
|
9112
9351
|
}
|
|
9113
|
-
if (!fs2.isFile((0,
|
|
9352
|
+
if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
|
|
9114
9353
|
copySkipped.push({ file: rel, reason: "absent-in-primary" });
|
|
9115
9354
|
continue;
|
|
9116
9355
|
}
|
|
9117
|
-
copyFile((0,
|
|
9356
|
+
copyFile((0, import_node_path12.join)(primary, rel), dest);
|
|
9118
9357
|
copied.push(rel);
|
|
9119
9358
|
log(`copied local config: ${rel}`);
|
|
9120
9359
|
}
|
|
9121
9360
|
return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
|
|
9122
9361
|
}
|
|
9362
|
+
function capWorktreeDirName(name, max = 40) {
|
|
9363
|
+
if (name.length <= max) return name;
|
|
9364
|
+
const hard = name.slice(0, max);
|
|
9365
|
+
const lastDash = hard.lastIndexOf("-");
|
|
9366
|
+
return (lastDash >= max - 12 ? hard.slice(0, lastDash) : hard).replace(/-+$/, "");
|
|
9367
|
+
}
|
|
9123
9368
|
function defaultWorktreePath(repoRoot2, branch) {
|
|
9124
|
-
const safe = branch.replace(/[/\\]+/g, "-");
|
|
9125
|
-
return (0,
|
|
9369
|
+
const safe = capWorktreeDirName(branch.replace(/[/\\]+/g, "-"));
|
|
9370
|
+
return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path12.basename)(repoRoot2), safe);
|
|
9126
9371
|
}
|
|
9127
9372
|
async function primaryCheckoutRootOf(git) {
|
|
9128
9373
|
try {
|
|
9129
9374
|
const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
|
|
9130
|
-
return out ? (0,
|
|
9375
|
+
return out ? (0, import_node_path12.dirname)(out) : void 0;
|
|
9131
9376
|
} catch {
|
|
9132
9377
|
return void 0;
|
|
9133
9378
|
}
|
|
@@ -9183,7 +9428,15 @@ async function resolveWhoami(deps) {
|
|
|
9183
9428
|
} catch {
|
|
9184
9429
|
session = void 0;
|
|
9185
9430
|
}
|
|
9186
|
-
if (session?.login)
|
|
9431
|
+
if (session?.login) {
|
|
9432
|
+
return {
|
|
9433
|
+
login: session.login,
|
|
9434
|
+
source: "hub-session",
|
|
9435
|
+
sessionExpiresAt: session.expiresAt,
|
|
9436
|
+
// Exact-match, and only here. The `github` branch below deliberately never carries a role.
|
|
9437
|
+
...session.role === "owner" ? { role: "owner" } : {}
|
|
9438
|
+
};
|
|
9439
|
+
}
|
|
9187
9440
|
let ghLogin;
|
|
9188
9441
|
try {
|
|
9189
9442
|
ghLogin = await deps.ghLogin();
|
|
@@ -9198,6 +9451,10 @@ function whoamiLine(report, caveat) {
|
|
|
9198
9451
|
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
9452
|
return caveat?.trim() ? `${line} ${caveat.trim()}` : line;
|
|
9200
9453
|
}
|
|
9454
|
+
function authorityLine(report) {
|
|
9455
|
+
if (report.source !== "hub-session" || report.role !== "owner" || !report.login) return null;
|
|
9456
|
+
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.`;
|
|
9457
|
+
}
|
|
9201
9458
|
|
|
9202
9459
|
// src/command-ladder-hint.ts
|
|
9203
9460
|
var COMMAND_LADDER_WRITES = [
|
|
@@ -9233,7 +9490,7 @@ function commandLadderHint() {
|
|
|
9233
9490
|
}
|
|
9234
9491
|
|
|
9235
9492
|
// src/index.ts
|
|
9236
|
-
var
|
|
9493
|
+
var import_node_path30 = require("node:path");
|
|
9237
9494
|
|
|
9238
9495
|
// src/merge-ci-policy.ts
|
|
9239
9496
|
function resolveMergeCiPolicy(input) {
|
|
@@ -9321,16 +9578,16 @@ function pollStateFromParsed(parsed) {
|
|
|
9321
9578
|
}
|
|
9322
9579
|
var PR_MERGEABLE_UNKNOWN_POLL_RETRIES = 4;
|
|
9323
9580
|
var PR_MERGEABLE_UNKNOWN_POLL_MS = 3e3;
|
|
9324
|
-
async function resolveSettledMergeableState(pollMergeable,
|
|
9581
|
+
async function resolveSettledMergeableState(pollMergeable, sleep3) {
|
|
9325
9582
|
for (let attempt = 1; attempt <= PR_MERGEABLE_UNKNOWN_POLL_RETRIES; attempt++) {
|
|
9326
9583
|
const state = await pollMergeable();
|
|
9327
9584
|
if (state !== "UNKNOWN") return state;
|
|
9328
|
-
if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await
|
|
9585
|
+
if (attempt < PR_MERGEABLE_UNKNOWN_POLL_RETRIES) await sleep3(PR_MERGEABLE_UNKNOWN_POLL_MS);
|
|
9329
9586
|
}
|
|
9330
9587
|
return "UNKNOWN";
|
|
9331
9588
|
}
|
|
9332
9589
|
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
|
|
9590
|
+
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
9591
|
}
|
|
9335
9592
|
function conflictingResult(policy, baseBranch, waitedMs) {
|
|
9336
9593
|
return { policy, status: "conflicting", reason: conflictingPrMessage(baseBranch), detail: "conflicting", waitedMs };
|
|
@@ -9686,6 +9943,12 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9686
9943
|
// and `.cursor/rules/` because a repo may want its rules tracked despite the wall.
|
|
9687
9944
|
".codex/",
|
|
9688
9945
|
".agents/",
|
|
9946
|
+
// #3525: `jerv-cli fusion attest record` writes `.jerv/attest/<head>.attest.json` INTO the worktree,
|
|
9947
|
+
// so every worktree that has ever passed the fusion gate reports `?? .jerv/` forever. `worktree land`
|
|
9948
|
+
// refuses to clean a dirty worktree, which made the attestation artifact the gate requires the exact
|
|
9949
|
+
// thing blocking the cleanup that gate's own worktree needs. Org-universal: every repo runs the same
|
|
9950
|
+
// gate, so this belongs in the managed block rather than in one repo's own ignores.
|
|
9951
|
+
".jerv/",
|
|
9689
9952
|
// #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<RepoName>/<branch>` (outside the tree,
|
|
9690
9953
|
// via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
|
|
9691
9954
|
// doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
|
|
@@ -10079,10 +10342,10 @@ function labelsToPrune(orgLabelNames) {
|
|
|
10079
10342
|
const org = new Set(orgLabelNames);
|
|
10080
10343
|
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
10081
10344
|
}
|
|
10082
|
-
function resolveSeedContent(seed, vars,
|
|
10083
|
-
if (seed.source === "self") return
|
|
10345
|
+
function resolveSeedContent(seed, vars, readFile7) {
|
|
10346
|
+
if (seed.source === "self") return readFile7(seed.target);
|
|
10084
10347
|
if (seed.source.startsWith("seed:")) {
|
|
10085
|
-
const tmpl =
|
|
10348
|
+
const tmpl = readFile7(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
10086
10349
|
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
10087
10350
|
}
|
|
10088
10351
|
return null;
|
|
@@ -10748,8 +11011,8 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
|
10748
11011
|
if (first.mergeStatus !== "failed") return first;
|
|
10749
11012
|
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
|
|
10750
11013
|
if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
|
|
10751
|
-
const
|
|
10752
|
-
await
|
|
11014
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
11015
|
+
await sleep3(PR_LAND_MERGE_RETRY_DELAY_MS);
|
|
10753
11016
|
const retried = await deps.mergeAuto(prNumber, repo);
|
|
10754
11017
|
if (retried.mergeStatus !== "failed") return retried;
|
|
10755
11018
|
return { mergeStatus: "failed", error: `merge retry after transient failure also failed: ${retried.error ?? first.error ?? "unknown error"}` };
|
|
@@ -10759,13 +11022,13 @@ var AUTO_MERGE_CONFIRM_DELAY_MS = 3e3;
|
|
|
10759
11022
|
async function confirmAutoMergeEnqueued(deps, options) {
|
|
10760
11023
|
const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
|
|
10761
11024
|
const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
|
|
10762
|
-
const
|
|
11025
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
10763
11026
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
10764
11027
|
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
10765
11028
|
const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
|
|
10766
11029
|
if (stuck) return "enqueued";
|
|
10767
11030
|
if (attempt < retries - 1) {
|
|
10768
|
-
await
|
|
11031
|
+
await sleep3(delayMs);
|
|
10769
11032
|
await deps.reEnqueue().catch(() => {
|
|
10770
11033
|
});
|
|
10771
11034
|
}
|
|
@@ -10776,7 +11039,7 @@ async function confirmAutoMergeEnqueued(deps, options) {
|
|
|
10776
11039
|
async function readGhPrStateWithRetry(fetchState, options) {
|
|
10777
11040
|
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
10778
11041
|
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
10779
|
-
const
|
|
11042
|
+
const sleep3 = options?.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
10780
11043
|
let lastError = "empty state";
|
|
10781
11044
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
10782
11045
|
try {
|
|
@@ -10786,7 +11049,7 @@ async function readGhPrStateWithRetry(fetchState, options) {
|
|
|
10786
11049
|
} catch (e) {
|
|
10787
11050
|
lastError = String(e.message || "gh pr view failed");
|
|
10788
11051
|
}
|
|
10789
|
-
if (attempt < retries - 1) await
|
|
11052
|
+
if (attempt < retries - 1) await sleep3(delayMs);
|
|
10790
11053
|
}
|
|
10791
11054
|
return { ok: false, error: lastError };
|
|
10792
11055
|
}
|
|
@@ -10849,12 +11112,12 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
10849
11112
|
}
|
|
10850
11113
|
|
|
10851
11114
|
// src/wave-status.ts
|
|
10852
|
-
var
|
|
11115
|
+
var import_node_fs16 = require("node:fs");
|
|
10853
11116
|
|
|
10854
11117
|
// src/stage-runner.ts
|
|
10855
11118
|
var import_node_child_process7 = require("node:child_process");
|
|
10856
|
-
var
|
|
10857
|
-
var
|
|
11119
|
+
var import_node_fs15 = require("node:fs");
|
|
11120
|
+
var import_node_path13 = require("node:path");
|
|
10858
11121
|
var import_node_net = require("node:net");
|
|
10859
11122
|
var import_node_util6 = require("node:util");
|
|
10860
11123
|
|
|
@@ -10993,11 +11256,11 @@ function appendForceRecreate(up) {
|
|
|
10993
11256
|
return `${up.trimEnd()} --force-recreate`;
|
|
10994
11257
|
}
|
|
10995
11258
|
function stageStatePath(cwd = process.cwd()) {
|
|
10996
|
-
return (0,
|
|
11259
|
+
return (0, import_node_path13.join)(cwd, "tmp", "stage", "state.json");
|
|
10997
11260
|
}
|
|
10998
11261
|
function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
|
|
10999
|
-
const dir = (0,
|
|
11000
|
-
return (0,
|
|
11262
|
+
const dir = (0, import_node_path13.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path13.resolve)(cwd, gitCommonDir);
|
|
11263
|
+
return (0, import_node_path13.join)(dir, "mmi", "stage", "state.json");
|
|
11001
11264
|
}
|
|
11002
11265
|
function normPath2(path2) {
|
|
11003
11266
|
return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
@@ -11221,14 +11484,14 @@ function stageProcessEnv(stagePort, extraEnv) {
|
|
|
11221
11484
|
}
|
|
11222
11485
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
11223
11486
|
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,
|
|
11487
|
+
const target = (0, import_node_path13.join)(cwd, config.ensureEnv.target);
|
|
11488
|
+
const example = (0, import_node_path13.join)(cwd, config.ensureEnv.example);
|
|
11489
|
+
if (!(0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
|
|
11490
|
+
(0, import_node_fs15.copyFileSync)(example, target);
|
|
11491
|
+
} else if ((0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
|
|
11492
|
+
const stale = detectStaleEnvFile((0, import_node_fs15.readFileSync)(example, "utf8"), (0, import_node_fs15.readFileSync)(target, "utf8"), {
|
|
11493
|
+
exampleMtimeMs: (0, import_node_fs15.statSync)(example).mtimeMs,
|
|
11494
|
+
targetMtimeMs: (0, import_node_fs15.statSync)(target).mtimeMs
|
|
11232
11495
|
});
|
|
11233
11496
|
if (stale) {
|
|
11234
11497
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -11236,8 +11499,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
|
11236
11499
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
11237
11500
|
}
|
|
11238
11501
|
}
|
|
11239
|
-
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0,
|
|
11240
|
-
(0,
|
|
11502
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs15.existsSync)(target)) {
|
|
11503
|
+
(0, import_node_fs15.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs15.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
11241
11504
|
}
|
|
11242
11505
|
}
|
|
11243
11506
|
async function gitText(cwd, args) {
|
|
@@ -11267,20 +11530,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
|
|
|
11267
11530
|
return void 0;
|
|
11268
11531
|
}
|
|
11269
11532
|
function readState(path2) {
|
|
11270
|
-
if (!(0,
|
|
11533
|
+
if (!(0, import_node_fs15.existsSync)(path2)) return null;
|
|
11271
11534
|
try {
|
|
11272
|
-
return JSON.parse((0,
|
|
11535
|
+
return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
|
|
11273
11536
|
} catch {
|
|
11274
11537
|
return null;
|
|
11275
11538
|
}
|
|
11276
11539
|
}
|
|
11277
11540
|
function mkdirFor(path2) {
|
|
11278
11541
|
const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
|
|
11279
|
-
(0,
|
|
11542
|
+
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
11280
11543
|
}
|
|
11281
11544
|
function writeState(path2, state) {
|
|
11282
11545
|
mkdirFor(path2);
|
|
11283
|
-
(0,
|
|
11546
|
+
(0, import_node_fs15.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
|
|
11284
11547
|
}
|
|
11285
11548
|
function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
11286
11549
|
const reservation = {
|
|
@@ -11300,7 +11563,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
|
|
|
11300
11563
|
await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
|
|
11301
11564
|
}
|
|
11302
11565
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
11303
|
-
(0,
|
|
11566
|
+
(0, import_node_fs15.rmSync)(path2, { force: true });
|
|
11304
11567
|
}
|
|
11305
11568
|
}
|
|
11306
11569
|
async function killTree(pid) {
|
|
@@ -11458,8 +11721,8 @@ async function runStage(config = {}, opts = {}) {
|
|
|
11458
11721
|
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
11459
11722
|
if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
|
|
11460
11723
|
} catch (e) {
|
|
11461
|
-
(0,
|
|
11462
|
-
if (globalStatePath && globalStatePath !== statePath) (0,
|
|
11724
|
+
(0, import_node_fs15.rmSync)(statePath, { force: true });
|
|
11725
|
+
if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs15.rmSync)(globalStatePath, { force: true });
|
|
11463
11726
|
throw e;
|
|
11464
11727
|
}
|
|
11465
11728
|
const started = await startStage(config, {
|
|
@@ -11480,9 +11743,9 @@ function parseNextFromHead(headText) {
|
|
|
11480
11743
|
}
|
|
11481
11744
|
function readStageSummary(worktreePath) {
|
|
11482
11745
|
const statePath = stageStatePath(worktreePath);
|
|
11483
|
-
if (!(0,
|
|
11746
|
+
if (!(0, import_node_fs16.existsSync)(statePath)) return void 0;
|
|
11484
11747
|
try {
|
|
11485
|
-
const state = JSON.parse((0,
|
|
11748
|
+
const state = JSON.parse((0, import_node_fs16.readFileSync)(statePath, "utf8"));
|
|
11486
11749
|
const port = typeof state.port === "number" ? state.port : void 0;
|
|
11487
11750
|
if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
|
|
11488
11751
|
return { port, url: typeof state.url === "string" ? state.url : void 0 };
|
|
@@ -11567,7 +11830,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
|
|
|
11567
11830
|
}
|
|
11568
11831
|
|
|
11569
11832
|
// src/index.ts
|
|
11570
|
-
var
|
|
11833
|
+
var import_node_os9 = require("node:os");
|
|
11571
11834
|
|
|
11572
11835
|
// src/attach-to-project.ts
|
|
11573
11836
|
async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
|
|
@@ -11601,7 +11864,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
11601
11864
|
// src/gh-create.ts
|
|
11602
11865
|
var import_promises = require("node:fs/promises");
|
|
11603
11866
|
var import_node_os3 = require("node:os");
|
|
11604
|
-
var
|
|
11867
|
+
var import_node_path14 = require("node:path");
|
|
11605
11868
|
var import_node_crypto3 = require("node:crypto");
|
|
11606
11869
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
11607
11870
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
@@ -11655,8 +11918,8 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
11655
11918
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
11656
11919
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
11657
11920
|
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
11658
|
-
const file = (0,
|
|
11659
|
-
await ensureDir((0,
|
|
11921
|
+
const file = (0, import_node_path14.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
11922
|
+
await ensureDir((0, import_node_path14.dirname)(file), { recursive: true }).catch(() => {
|
|
11660
11923
|
});
|
|
11661
11924
|
await write(file, args[i + 1], "utf8");
|
|
11662
11925
|
return {
|
|
@@ -11723,23 +11986,61 @@ function parseExistingPr(stderr) {
|
|
|
11723
11986
|
if (!match) return void 0;
|
|
11724
11987
|
return { number: Number(match[1]), url: match[0] };
|
|
11725
11988
|
}
|
|
11726
|
-
|
|
11989
|
+
var GH_CREATE_UPSTREAM_RETRIES = 3;
|
|
11990
|
+
var GH_CREATE_RETRY_BACKOFF_MS = [2e3, 5e3];
|
|
11991
|
+
function httpStatusCodes(stderr) {
|
|
11992
|
+
const out = [];
|
|
11993
|
+
for (const m of stderr.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) out.push(Number(m[1]));
|
|
11994
|
+
for (const m of stderr.matchAll(/\b(\d{3})\s+Internal Server Error\b/g)) out.push(Number(m[1]));
|
|
11995
|
+
return out;
|
|
11996
|
+
}
|
|
11997
|
+
function isUpstreamGitHubFault(stderr) {
|
|
11998
|
+
const codes = httpStatusCodes(stderr);
|
|
11999
|
+
if (codes.some((c) => c >= 400 && c < 500)) return false;
|
|
12000
|
+
if (codes.some((c) => c >= 500 && c < 600)) return true;
|
|
12001
|
+
return /Something went wrong while executing your query/.test(stderr) || /^\s*unexpected end of JSON input\s*$/m.test(stderr);
|
|
12002
|
+
}
|
|
12003
|
+
function mayRetryCreate(verb) {
|
|
12004
|
+
return verb === "pr";
|
|
12005
|
+
}
|
|
12006
|
+
function upstreamFaultMessage(verb, stderr) {
|
|
12007
|
+
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];
|
|
12008
|
+
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";
|
|
12009
|
+
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.`;
|
|
12010
|
+
}
|
|
12011
|
+
async function ghCreate(args, deps = {}) {
|
|
12012
|
+
const exec = deps.exec ?? execFileP2;
|
|
12013
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
11727
12014
|
const swapped = await bodyArgsViaFile(args);
|
|
11728
12015
|
try {
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
12016
|
+
for (let attempt = 1; attempt <= GH_CREATE_UPSTREAM_RETRIES; attempt++) {
|
|
12017
|
+
try {
|
|
12018
|
+
const { stdout } = await exec("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
12019
|
+
return parseCreatedUrl(stdout);
|
|
12020
|
+
} catch (e) {
|
|
12021
|
+
const err = e;
|
|
12022
|
+
const errText = `${err.stderr ?? ""}
|
|
12023
|
+
${err.message ?? ""}`;
|
|
12024
|
+
const faultText = (err.stderr ?? "").trim() ? err.stderr : err.message ?? "";
|
|
12025
|
+
const existing = args[0] === "pr" ? parseExistingPr(errText) : void 0;
|
|
12026
|
+
if (existing) {
|
|
12027
|
+
await swapped.cleanup();
|
|
12028
|
+
console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
|
|
12029
|
+
return { ...existing, existing: true };
|
|
12030
|
+
}
|
|
12031
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
12032
|
+
if (isUpstreamGitHubFault(faultText) && mayRetryCreate(args[0]) && attempt < GH_CREATE_UPSTREAM_RETRIES) {
|
|
12033
|
+
const waitMs = GH_CREATE_RETRY_BACKOFF_MS[attempt - 1];
|
|
12034
|
+
console.warn(`gh ${args[0]} create: GitHub API server fault (attempt ${attempt}/${GH_CREATE_UPSTREAM_RETRIES}) \u2014 retrying in ${waitMs}ms`);
|
|
12035
|
+
await sleep3(waitMs);
|
|
12036
|
+
continue;
|
|
12037
|
+
}
|
|
12038
|
+
await swapped.cleanup();
|
|
12039
|
+
if (isUpstreamGitHubFault(faultText)) return fail(upstreamFaultMessage(args[0], faultText));
|
|
12040
|
+
return fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
12041
|
+
}
|
|
11739
12042
|
}
|
|
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})` : ""}`);
|
|
12043
|
+
throw new Error("ghCreate: retry loop exhausted without a verdict");
|
|
11743
12044
|
} finally {
|
|
11744
12045
|
await swapped.cleanup();
|
|
11745
12046
|
}
|
|
@@ -12893,8 +13194,8 @@ function parseVerifyBroker(stdout) {
|
|
|
12893
13194
|
}
|
|
12894
13195
|
|
|
12895
13196
|
// src/train-apply.ts
|
|
12896
|
-
var
|
|
12897
|
-
var
|
|
13197
|
+
var import_node_fs17 = require("node:fs");
|
|
13198
|
+
var import_node_path15 = require("node:path");
|
|
12898
13199
|
function resolveDeployModel2(meta, repo) {
|
|
12899
13200
|
const m = meta?.deployModel;
|
|
12900
13201
|
if (isDeployModel(m)) return m;
|
|
@@ -13003,7 +13304,7 @@ function mergeTreeConflictFiles(stdout) {
|
|
|
13003
13304
|
return stdout.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
|
|
13004
13305
|
}
|
|
13005
13306
|
async function runMergeTreePreflight(deps, ours, theirs) {
|
|
13006
|
-
const
|
|
13307
|
+
const sleep3 = resolveSleep(deps);
|
|
13007
13308
|
let lastError;
|
|
13008
13309
|
for (let attempt = 0; attempt < MERGE_TREE_PREFLIGHT_ATTEMPTS; attempt++) {
|
|
13009
13310
|
try {
|
|
@@ -13013,7 +13314,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
|
|
|
13013
13314
|
lastError = e;
|
|
13014
13315
|
const files = mergeTreeConflictFiles(String(e.stdout ?? ""));
|
|
13015
13316
|
if (files.length > 0) return files;
|
|
13016
|
-
if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await
|
|
13317
|
+
if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await sleep3(MERGE_TREE_PREFLIGHT_DELAY_MS);
|
|
13017
13318
|
}
|
|
13018
13319
|
}
|
|
13019
13320
|
const msg = lastError?.message ?? String(lastError);
|
|
@@ -13217,7 +13518,7 @@ function isPersistentGitPushFailure(e) {
|
|
|
13217
13518
|
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
13519
|
}
|
|
13219
13520
|
async function runGitWithRetry(deps, args, shouldRetry) {
|
|
13220
|
-
const
|
|
13521
|
+
const sleep3 = resolveSleep(deps);
|
|
13221
13522
|
let lastError;
|
|
13222
13523
|
for (let attempt = 1; attempt <= GIT_NETWORK_ATTEMPTS; attempt++) {
|
|
13223
13524
|
try {
|
|
@@ -13225,7 +13526,7 @@ async function runGitWithRetry(deps, args, shouldRetry) {
|
|
|
13225
13526
|
} catch (e) {
|
|
13226
13527
|
lastError = e;
|
|
13227
13528
|
if (attempt >= GIT_NETWORK_ATTEMPTS || !shouldRetry(e)) throw e;
|
|
13228
|
-
await
|
|
13529
|
+
await sleep3(GIT_NETWORK_RETRY_DELAY_MS * attempt);
|
|
13229
13530
|
}
|
|
13230
13531
|
}
|
|
13231
13532
|
throw lastError;
|
|
@@ -13246,13 +13547,13 @@ var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project
|
|
|
13246
13547
|
var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
|
|
13247
13548
|
var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
|
|
13248
13549
|
async function correlateRun(deps, args) {
|
|
13249
|
-
const
|
|
13550
|
+
const sleep3 = resolveSleep(deps);
|
|
13250
13551
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
13251
13552
|
const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO3 : HUB_REPO3;
|
|
13252
13553
|
let lastError;
|
|
13253
13554
|
let parsedAnyResponse = false;
|
|
13254
13555
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
13255
|
-
if (attempt > 0) await
|
|
13556
|
+
if (attempt > 0) await sleep3(CORRELATE_DELAY_MS);
|
|
13256
13557
|
const listArgs = [
|
|
13257
13558
|
"run",
|
|
13258
13559
|
"list",
|
|
@@ -13445,13 +13746,13 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
13445
13746
|
const t = Date.parse(String(r.startedAt ?? ""));
|
|
13446
13747
|
return !Number.isFinite(t) || t >= freshThreshold;
|
|
13447
13748
|
};
|
|
13448
|
-
const
|
|
13749
|
+
const sleep3 = resolveSleep(deps);
|
|
13449
13750
|
let lastStatus = "not checked";
|
|
13450
13751
|
let lastError;
|
|
13451
13752
|
const everObserved = /* @__PURE__ */ new Set();
|
|
13452
13753
|
const autoSatisfied = /* @__PURE__ */ new Set();
|
|
13453
13754
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
13454
|
-
if (attempt > 0) await
|
|
13755
|
+
if (attempt > 0) await sleep3(TRAIN_CHECK_DELAY_MS);
|
|
13455
13756
|
let checkRuns;
|
|
13456
13757
|
let statuses;
|
|
13457
13758
|
try {
|
|
@@ -13585,14 +13886,14 @@ function isTransientDispatchFailure(e) {
|
|
|
13585
13886
|
return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
|
|
13586
13887
|
}
|
|
13587
13888
|
async function dispatchTenantDeployWithRetry(deps, input) {
|
|
13588
|
-
const
|
|
13889
|
+
const sleep3 = resolveSleep(deps);
|
|
13589
13890
|
for (let attempt = 1; ; attempt++) {
|
|
13590
13891
|
try {
|
|
13591
13892
|
await deps.dispatchTenantDeploy(input);
|
|
13592
13893
|
return;
|
|
13593
13894
|
} catch (e) {
|
|
13594
13895
|
if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
|
|
13595
|
-
await
|
|
13896
|
+
await sleep3(DISPATCH_RETRY_DELAY_MS * attempt);
|
|
13596
13897
|
}
|
|
13597
13898
|
}
|
|
13598
13899
|
}
|
|
@@ -13612,9 +13913,9 @@ var PUBLISH_LOG_RETRY_ATTEMPTS = 4;
|
|
|
13612
13913
|
var PUBLISH_LOG_RETRY_DELAY_MS = 6e3;
|
|
13613
13914
|
async function reconcilePublishFailure(deps, runId) {
|
|
13614
13915
|
if (runId == null) return "failure";
|
|
13615
|
-
const
|
|
13916
|
+
const sleep3 = resolveSleep(deps);
|
|
13616
13917
|
for (let attempt = 0; attempt < PUBLISH_LOG_RETRY_ATTEMPTS; attempt++) {
|
|
13617
|
-
if (attempt > 0) await
|
|
13918
|
+
if (attempt > 0) await sleep3(PUBLISH_LOG_RETRY_DELAY_MS);
|
|
13618
13919
|
const log = await fetchControlRunLog(deps, runId);
|
|
13619
13920
|
if (PUBLISH_IDEMPOTENT_MARKER.test(log)) return "success";
|
|
13620
13921
|
}
|
|
@@ -13802,17 +14103,17 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
13802
14103
|
return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
|
|
13803
14104
|
}
|
|
13804
14105
|
function readLocalGateWorkflows() {
|
|
13805
|
-
const dir = (0,
|
|
14106
|
+
const dir = (0, import_node_path15.join)(".github", "workflows");
|
|
13806
14107
|
let names;
|
|
13807
14108
|
try {
|
|
13808
|
-
names = (0,
|
|
14109
|
+
names = (0, import_node_fs17.readdirSync)(dir);
|
|
13809
14110
|
} catch {
|
|
13810
14111
|
return null;
|
|
13811
14112
|
}
|
|
13812
14113
|
const files = [];
|
|
13813
14114
|
for (const name of names.filter(isGateWorkflowPath)) {
|
|
13814
14115
|
try {
|
|
13815
|
-
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0,
|
|
14116
|
+
files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(dir, name), "utf8") });
|
|
13816
14117
|
} catch {
|
|
13817
14118
|
}
|
|
13818
14119
|
}
|
|
@@ -14408,13 +14709,13 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
14408
14709
|
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
14710
|
};
|
|
14410
14711
|
}
|
|
14411
|
-
const
|
|
14712
|
+
const sleep3 = resolveSleep(deps);
|
|
14412
14713
|
let last;
|
|
14413
14714
|
for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
|
|
14414
14715
|
last = await attemptRetire(deps, ctx);
|
|
14415
14716
|
if (last.status === "retired") return last;
|
|
14416
14717
|
if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
|
|
14417
|
-
await
|
|
14718
|
+
await sleep3(RETIRE_BACKOFF_MS * attempt);
|
|
14418
14719
|
}
|
|
14419
14720
|
const f = last;
|
|
14420
14721
|
if (f.category === "wait-timeout") return f;
|
|
@@ -14945,9 +15246,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
|
|
|
14945
15246
|
return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
|
|
14946
15247
|
}
|
|
14947
15248
|
async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
14948
|
-
const
|
|
15249
|
+
const sleep3 = sleeper(deps);
|
|
14949
15250
|
for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
|
|
14950
|
-
if (attempt > 0) await
|
|
15251
|
+
if (attempt > 0) await sleep3(HOTFIX_RUN_FIND_DELAY_MS);
|
|
14951
15252
|
let rows;
|
|
14952
15253
|
try {
|
|
14953
15254
|
const out = await deps.run("gh", [
|
|
@@ -15085,7 +15386,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
15085
15386
|
try {
|
|
15086
15387
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
15087
15388
|
const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
|
|
15088
|
-
const
|
|
15389
|
+
const sleep3 = sleeper(deps);
|
|
15089
15390
|
let attempt = 0;
|
|
15090
15391
|
for (; ; ) {
|
|
15091
15392
|
attempt++;
|
|
@@ -15094,7 +15395,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
15094
15395
|
break;
|
|
15095
15396
|
} catch (err) {
|
|
15096
15397
|
if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
|
|
15097
|
-
await
|
|
15398
|
+
await sleep3(HOTFIX_VERIFY_RETRY_MS);
|
|
15098
15399
|
}
|
|
15099
15400
|
}
|
|
15100
15401
|
const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
|
|
@@ -15709,8 +16010,8 @@ function renderAccessReport(report) {
|
|
|
15709
16010
|
}
|
|
15710
16011
|
|
|
15711
16012
|
// src/docs-index-command.ts
|
|
15712
|
-
var
|
|
15713
|
-
var
|
|
16013
|
+
var import_node_fs18 = require("node:fs");
|
|
16014
|
+
var import_node_path16 = require("node:path");
|
|
15714
16015
|
var DOCS_INDEX_PATH = "docs/index.md";
|
|
15715
16016
|
function isRoutableDocsPath(relPath) {
|
|
15716
16017
|
const normalized = relPath.replace(/\\/g, "/");
|
|
@@ -15792,32 +16093,32 @@ function walkMarkdown(dir) {
|
|
|
15792
16093
|
const stack = [dir];
|
|
15793
16094
|
while (stack.length) {
|
|
15794
16095
|
const current = stack.pop();
|
|
15795
|
-
for (const entry of (0,
|
|
15796
|
-
const full = (0,
|
|
16096
|
+
for (const entry of (0, import_node_fs18.readdirSync)(current, { withFileTypes: true })) {
|
|
16097
|
+
const full = (0, import_node_path16.join)(current, entry.name);
|
|
15797
16098
|
if (entry.isDirectory()) {
|
|
15798
16099
|
stack.push(full);
|
|
15799
16100
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
15800
|
-
out.push((0,
|
|
16101
|
+
out.push((0, import_node_path16.relative)(dir, full).split(import_node_path16.sep).join("/"));
|
|
15801
16102
|
}
|
|
15802
16103
|
}
|
|
15803
16104
|
}
|
|
15804
16105
|
return out;
|
|
15805
16106
|
}
|
|
15806
16107
|
function createDocsIndexDeps(repoRoot2) {
|
|
15807
|
-
const docsDir = (0,
|
|
15808
|
-
const indexPath = (0,
|
|
16108
|
+
const docsDir = (0, import_node_path16.join)(repoRoot2, "docs");
|
|
16109
|
+
const indexPath = (0, import_node_path16.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
15809
16110
|
return {
|
|
15810
|
-
listDocs: () => (0,
|
|
15811
|
-
readDoc: (relPath) => (0,
|
|
15812
|
-
readIndex: () => (0,
|
|
15813
|
-
writeIndex: (content) => (0,
|
|
16111
|
+
listDocs: () => (0, import_node_fs18.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
|
|
16112
|
+
readDoc: (relPath) => (0, import_node_fs18.readFileSync)((0, import_node_path16.join)(docsDir, relPath), "utf8"),
|
|
16113
|
+
readIndex: () => (0, import_node_fs18.existsSync)(indexPath) ? (0, import_node_fs18.readFileSync)(indexPath, "utf8") : null,
|
|
16114
|
+
writeIndex: (content) => (0, import_node_fs18.writeFileSync)(indexPath, content, "utf8")
|
|
15814
16115
|
};
|
|
15815
16116
|
}
|
|
15816
16117
|
|
|
15817
16118
|
// src/doc-refs-core.ts
|
|
15818
16119
|
var import_node_child_process9 = require("node:child_process");
|
|
15819
|
-
var
|
|
15820
|
-
var
|
|
16120
|
+
var import_node_fs19 = require("node:fs");
|
|
16121
|
+
var import_node_path17 = require("node:path");
|
|
15821
16122
|
var PIN_RE = /<!--\s*pinned by\s+([^:]+?)\s*:\s*"([^"]+)"[^"]*-->/;
|
|
15822
16123
|
var PIN_MENTION_RE = /<!--\s*pinned by\b/;
|
|
15823
16124
|
var REF_RE = /`([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)(?::\d+)?`/g;
|
|
@@ -15851,7 +16152,7 @@ function extractPins(markdown) {
|
|
|
15851
16152
|
});
|
|
15852
16153
|
return pins;
|
|
15853
16154
|
}
|
|
15854
|
-
function checkPins(root,
|
|
16155
|
+
function checkPins(root, readFile7, docs2) {
|
|
15855
16156
|
const findings = [];
|
|
15856
16157
|
for (const [doc, markdown] of Object.entries(docs2)) {
|
|
15857
16158
|
for (const pin of extractPins(markdown)) {
|
|
@@ -15859,7 +16160,7 @@ function checkPins(root, readFile6, docs2) {
|
|
|
15859
16160
|
findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
|
|
15860
16161
|
continue;
|
|
15861
16162
|
}
|
|
15862
|
-
const source =
|
|
16163
|
+
const source = readFile7((0, import_node_path17.join)(root, pin.file));
|
|
15863
16164
|
if (source == null) {
|
|
15864
16165
|
findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
|
|
15865
16166
|
continue;
|
|
@@ -15920,17 +16221,17 @@ function checkRefs(root, deps, docs2) {
|
|
|
15920
16221
|
for (const [doc, markdown] of Object.entries(docs2)) {
|
|
15921
16222
|
for (const { ref, line } of extractRefs(markdown)) {
|
|
15922
16223
|
const first = ref.split("/")[0];
|
|
15923
|
-
if (!exists((0,
|
|
15924
|
-
if (!exists((0,
|
|
16224
|
+
if (!exists((0, import_node_path17.join)(root, first))) continue;
|
|
16225
|
+
if (!exists((0, import_node_path17.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
|
|
15925
16226
|
}
|
|
15926
|
-
const docDir =
|
|
16227
|
+
const docDir = import_node_path17.posix.dirname(doc);
|
|
15927
16228
|
for (const { target, line } of extractLinks(markdown)) {
|
|
15928
|
-
const resolved =
|
|
16229
|
+
const resolved = import_node_path17.posix.normalize(import_node_path17.posix.join(docDir === "." ? "" : docDir, target));
|
|
15929
16230
|
if (resolved.startsWith("..")) {
|
|
15930
16231
|
candidates.push({ kind: "missing-link", doc, line, detail: target, escapesRoot: true });
|
|
15931
16232
|
continue;
|
|
15932
16233
|
}
|
|
15933
|
-
if (!exists((0,
|
|
16234
|
+
if (!exists((0, import_node_path17.join)(root, resolved))) {
|
|
15934
16235
|
candidates.push({ kind: "missing-link", doc, line, detail: target, resolved });
|
|
15935
16236
|
}
|
|
15936
16237
|
}
|
|
@@ -15964,22 +16265,22 @@ function checkCommands(docs2, commandPaths) {
|
|
|
15964
16265
|
return { ok: findings.length === 0, findings };
|
|
15965
16266
|
}
|
|
15966
16267
|
function readFileOrNull(path2) {
|
|
15967
|
-
return (0,
|
|
16268
|
+
return (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null;
|
|
15968
16269
|
}
|
|
15969
16270
|
function walk(dir, root, out) {
|
|
15970
|
-
for (const entry of (0,
|
|
15971
|
-
const full = (0,
|
|
15972
|
-
if ((0,
|
|
16271
|
+
for (const entry of (0, import_node_fs19.readdirSync)(dir)) {
|
|
16272
|
+
const full = (0, import_node_path17.join)(dir, entry);
|
|
16273
|
+
if ((0, import_node_fs19.statSync)(full).isDirectory()) walk(full, root, out);
|
|
15973
16274
|
else if (entry.endsWith(".md")) out.push(full.slice(root.length + 1).replaceAll("\\", "/"));
|
|
15974
16275
|
}
|
|
15975
16276
|
return out;
|
|
15976
16277
|
}
|
|
15977
16278
|
function defaultListDocs(root) {
|
|
15978
|
-
const docsDir = (0,
|
|
15979
|
-
const docs2 = ((0,
|
|
16279
|
+
const docsDir = (0, import_node_path17.join)(root, "docs");
|
|
16280
|
+
const docs2 = ((0, import_node_fs19.existsSync)(docsDir) ? walk(docsDir, root, []) : []).filter(
|
|
15980
16281
|
(rel) => !SKIP_WALK.some((skip) => rel.startsWith(skip))
|
|
15981
16282
|
);
|
|
15982
|
-
return [...ROOT_DOCS.filter((rel) => (0,
|
|
16283
|
+
return [...ROOT_DOCS.filter((rel) => (0, import_node_fs19.existsSync)((0, import_node_path17.join)(root, rel))), ...docs2];
|
|
15983
16284
|
}
|
|
15984
16285
|
function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.execFileSync) {
|
|
15985
16286
|
const inRepo = relPaths.filter((p) => !p.startsWith(".."));
|
|
@@ -15997,16 +16298,16 @@ function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.exec
|
|
|
15997
16298
|
}
|
|
15998
16299
|
}
|
|
15999
16300
|
function runDocRefs(root, deps = {}) {
|
|
16000
|
-
const
|
|
16001
|
-
const exists = deps.exists ??
|
|
16301
|
+
const readFile7 = deps.readFile ?? readFileOrNull;
|
|
16302
|
+
const exists = deps.exists ?? import_node_fs19.existsSync;
|
|
16002
16303
|
const listDocs = deps.listDocs ?? defaultListDocs;
|
|
16003
16304
|
const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
|
|
16004
16305
|
const commandPaths = deps.commandPaths ?? null;
|
|
16005
16306
|
const docs2 = Object.fromEntries(
|
|
16006
|
-
listDocs(root).map((rel) => [rel,
|
|
16307
|
+
listDocs(root).map((rel) => [rel, readFile7((0, import_node_path17.join)(root, rel))]).filter(([, body]) => body != null)
|
|
16007
16308
|
);
|
|
16008
16309
|
const findings = [
|
|
16009
|
-
...checkPins(root,
|
|
16310
|
+
...checkPins(root, readFile7, docs2).findings,
|
|
16010
16311
|
...checkRefs(root, { exists, isIgnored }, docs2).findings
|
|
16011
16312
|
];
|
|
16012
16313
|
if (commandPaths == null) {
|
|
@@ -16078,7 +16379,28 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
16078
16379
|
if (!fetch2.ok) {
|
|
16079
16380
|
return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
|
|
16080
16381
|
}
|
|
16382
|
+
if (!isValidIsoDate(opts.today)) {
|
|
16383
|
+
throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
|
|
16384
|
+
}
|
|
16081
16385
|
if (fetch2.verdict === null) {
|
|
16386
|
+
const armedAt = opts.armedAt;
|
|
16387
|
+
if (armedAt && isValidIsoDate(armedAt)) {
|
|
16388
|
+
const cadenceDays = opts.cadenceDays ?? 7;
|
|
16389
|
+
const graceDays = opts.graceDays ?? 3;
|
|
16390
|
+
const sinceArmed = ageInDays(armedAt, opts.today);
|
|
16391
|
+
if (sinceArmed <= cadenceDays + graceDays) {
|
|
16392
|
+
return {
|
|
16393
|
+
ok: true,
|
|
16394
|
+
state: "awaiting-first-tick",
|
|
16395
|
+
line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
|
|
16396
|
+
};
|
|
16397
|
+
}
|
|
16398
|
+
return {
|
|
16399
|
+
ok: false,
|
|
16400
|
+
state: "missing",
|
|
16401
|
+
line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
|
|
16402
|
+
};
|
|
16403
|
+
}
|
|
16082
16404
|
return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
|
|
16083
16405
|
}
|
|
16084
16406
|
const verdict = fetch2.verdict;
|
|
@@ -16107,9 +16429,6 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
16107
16429
|
line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
|
|
16108
16430
|
};
|
|
16109
16431
|
}
|
|
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
16432
|
const cadence = opts.cadenceDays ?? 7;
|
|
16114
16433
|
const grace = opts.graceDays ?? 3;
|
|
16115
16434
|
const age = ageInDays(verdict.date, opts.today);
|
|
@@ -16945,8 +17264,8 @@ function writeError(res) {
|
|
|
16945
17264
|
}
|
|
16946
17265
|
|
|
16947
17266
|
// src/secrets-commands.ts
|
|
16948
|
-
var
|
|
16949
|
-
var
|
|
17267
|
+
var import_node_fs20 = require("node:fs");
|
|
17268
|
+
var import_node_path18 = require("node:path");
|
|
16950
17269
|
var import_node_os5 = require("node:os");
|
|
16951
17270
|
|
|
16952
17271
|
// src/project-runtime.ts
|
|
@@ -17063,25 +17382,25 @@ var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
|
|
|
17063
17382
|
var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
|
|
17064
17383
|
function resolvePreflightRepoScope(optRepo, cwdRepo) {
|
|
17065
17384
|
const targetRepo2 = optRepo ? optRepo.includes("/") ? optRepo : `mutmutco/${optRepo}` : cwdRepo;
|
|
17066
|
-
const
|
|
17067
|
-
return { targetRepo: targetRepo2, sameRepo };
|
|
17385
|
+
const sameRepo2 = !optRepo || Boolean(cwdRepo) && Boolean(targetRepo2) && cwdRepo.toLowerCase() === targetRepo2.toLowerCase();
|
|
17386
|
+
return { targetRepo: targetRepo2, sameRepo: sameRepo2 };
|
|
17068
17387
|
}
|
|
17069
17388
|
function collectMap(value, previous = []) {
|
|
17070
17389
|
return [...previous, value];
|
|
17071
17390
|
}
|
|
17072
17391
|
async function decryptRailsCredentials(input) {
|
|
17073
|
-
const appDir = (0,
|
|
17392
|
+
const appDir = (0, import_node_path18.resolve)(input.appDir ?? process.cwd());
|
|
17074
17393
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
17075
17394
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
17076
|
-
const credentialsPath = (0,
|
|
17077
|
-
const masterKeyPath = (0,
|
|
17395
|
+
const credentialsPath = (0, import_node_path18.resolve)(appDir, credentialsFile);
|
|
17396
|
+
const masterKeyPath = (0, import_node_path18.resolve)(appDir, masterKeyFile);
|
|
17078
17397
|
const env = {
|
|
17079
17398
|
...process.env,
|
|
17080
17399
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
17081
17400
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
17082
17401
|
};
|
|
17083
|
-
if ((0,
|
|
17084
|
-
env.RAILS_MASTER_KEY = (0,
|
|
17402
|
+
if ((0, import_node_fs20.existsSync)(masterKeyPath)) {
|
|
17403
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs20.readFileSync)(masterKeyPath, "utf8").trim();
|
|
17085
17404
|
}
|
|
17086
17405
|
const script = [
|
|
17087
17406
|
'require "json"',
|
|
@@ -17091,9 +17410,9 @@ async function decryptRailsCredentials(input) {
|
|
|
17091
17410
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
17092
17411
|
"puts JSON.generate(config.config)"
|
|
17093
17412
|
].join("\n");
|
|
17094
|
-
const scriptDir = (0,
|
|
17095
|
-
const scriptPath = (0,
|
|
17096
|
-
(0,
|
|
17413
|
+
const scriptDir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
|
|
17414
|
+
const scriptPath = (0, import_node_path18.join)(scriptDir, "decrypt.rb");
|
|
17415
|
+
(0, import_node_fs20.writeFileSync)(scriptPath, script, "utf8");
|
|
17097
17416
|
try {
|
|
17098
17417
|
const args = ["exec", "ruby", scriptPath];
|
|
17099
17418
|
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
@@ -17105,7 +17424,7 @@ async function decryptRailsCredentials(input) {
|
|
|
17105
17424
|
});
|
|
17106
17425
|
return JSON.parse(stdout);
|
|
17107
17426
|
} finally {
|
|
17108
|
-
(0,
|
|
17427
|
+
(0, import_node_fs20.rmSync)(scriptDir, { recursive: true, force: true });
|
|
17109
17428
|
}
|
|
17110
17429
|
}
|
|
17111
17430
|
async function readSecretStdin() {
|
|
@@ -17195,7 +17514,7 @@ function registerSecretsCommands(program3) {
|
|
|
17195
17514
|
let body;
|
|
17196
17515
|
if (o.file) {
|
|
17197
17516
|
try {
|
|
17198
|
-
body = (0,
|
|
17517
|
+
body = (0, import_node_fs20.readFileSync)((0, import_node_path18.resolve)(o.file), "utf8");
|
|
17199
17518
|
} catch (e) {
|
|
17200
17519
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
17201
17520
|
}
|
|
@@ -17229,11 +17548,11 @@ function registerSecretsCommands(program3) {
|
|
|
17229
17548
|
const stage = o.stage;
|
|
17230
17549
|
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
|
|
17231
17550
|
const cwdRepo = repoFromRemoteUrl((await execFileP2("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
|
|
17232
|
-
const { sameRepo } = resolvePreflightRepoScope(o.repo, cwdRepo);
|
|
17551
|
+
const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
|
|
17233
17552
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
17234
17553
|
const fact = facts?.stages?.[stage] ?? null;
|
|
17235
|
-
const composeText =
|
|
17236
|
-
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(
|
|
17554
|
+
const composeText = sameRepo2 ? await readStageBranchCompose(branch) : null;
|
|
17555
|
+
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo2), force: false, repo });
|
|
17237
17556
|
if (!verdict.ok) {
|
|
17238
17557
|
d.err(`secrets preflight: ${verdict.reason}`);
|
|
17239
17558
|
filelessOk = false;
|
|
@@ -17300,7 +17619,7 @@ function registerSecretsCommands(program3) {
|
|
|
17300
17619
|
{
|
|
17301
17620
|
...d,
|
|
17302
17621
|
decryptRailsCredentials,
|
|
17303
|
-
removeFile: (path2) => (0,
|
|
17622
|
+
removeFile: (path2) => (0, import_node_fs20.unlinkSync)((0, import_node_path18.resolve)(o.appDir ?? process.cwd(), path2))
|
|
17304
17623
|
},
|
|
17305
17624
|
{
|
|
17306
17625
|
repo: o.repo,
|
|
@@ -17474,7 +17793,7 @@ function checkGithubPools(probe) {
|
|
|
17474
17793
|
}
|
|
17475
17794
|
|
|
17476
17795
|
// src/box-commands.ts
|
|
17477
|
-
var
|
|
17796
|
+
var import_node_fs21 = require("node:fs");
|
|
17478
17797
|
|
|
17479
17798
|
// src/box.ts
|
|
17480
17799
|
var BOX_KEYS = {
|
|
@@ -17677,7 +17996,7 @@ function registerBoxCommands(program3) {
|
|
|
17677
17996
|
}
|
|
17678
17997
|
if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
|
|
17679
17998
|
else if (o.ssh && o.script) {
|
|
17680
|
-
(0,
|
|
17999
|
+
(0, import_node_fs21.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
|
|
17681
18000
|
console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
|
|
17682
18001
|
} else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
17683
18002
|
${SSH_RECIPE_AGENT_NOTE}`);
|
|
@@ -17775,7 +18094,7 @@ function lambdaTargetFromArn(arn) {
|
|
|
17775
18094
|
}
|
|
17776
18095
|
function awsScheduleEntry(payload) {
|
|
17777
18096
|
if (!payload || typeof payload !== "object") return null;
|
|
17778
|
-
const { Name, ScheduleExpression, State, Target, Description } = payload;
|
|
18097
|
+
const { Name, ScheduleExpression, State, Target, Description, CreationDate } = payload;
|
|
17779
18098
|
if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
|
|
17780
18099
|
if (State !== "ENABLED" || isJervResource(Name)) return null;
|
|
17781
18100
|
let target = "";
|
|
@@ -17798,6 +18117,7 @@ function awsScheduleEntry(payload) {
|
|
|
17798
18117
|
}
|
|
17799
18118
|
const sourceBase = `aws scheduler schedule ${Name} (eu-central-1)`;
|
|
17800
18119
|
const source = qualifier && target ? `${sourceBase} \u2192 ${target}:${qualifier}` : sourceBase;
|
|
18120
|
+
const armedAt = armedAtFromCreationDate(CreationDate);
|
|
17801
18121
|
return {
|
|
17802
18122
|
name: Name,
|
|
17803
18123
|
cadence: ScheduleExpression,
|
|
@@ -17805,9 +18125,25 @@ function awsScheduleEntry(payload) {
|
|
|
17805
18125
|
llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
|
|
17806
18126
|
resolved: "live",
|
|
17807
18127
|
source,
|
|
17808
|
-
...scheduleId ? { scheduleId } : {}
|
|
18128
|
+
...scheduleId ? { scheduleId } : {},
|
|
18129
|
+
...armedAt ? { armedAt } : {}
|
|
17809
18130
|
};
|
|
17810
18131
|
}
|
|
18132
|
+
function armedAtFromCreationDate(creationDate) {
|
|
18133
|
+
if (creationDate instanceof Date) {
|
|
18134
|
+
return Number.isNaN(creationDate.getTime()) ? void 0 : creationDate.toISOString().slice(0, 10);
|
|
18135
|
+
}
|
|
18136
|
+
if (typeof creationDate === "string" && creationDate.trim()) {
|
|
18137
|
+
const parsed = Date.parse(creationDate);
|
|
18138
|
+
return Number.isNaN(parsed) ? void 0 : new Date(parsed).toISOString().slice(0, 10);
|
|
18139
|
+
}
|
|
18140
|
+
if (typeof creationDate === "number" && Number.isFinite(creationDate)) {
|
|
18141
|
+
const ms = creationDate > 1e12 ? creationDate : creationDate * 1e3;
|
|
18142
|
+
const date = new Date(ms);
|
|
18143
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString().slice(0, 10);
|
|
18144
|
+
}
|
|
18145
|
+
return void 0;
|
|
18146
|
+
}
|
|
17811
18147
|
function awsScheduleRefs(payload) {
|
|
17812
18148
|
if (!payload || typeof payload !== "object") return [];
|
|
17813
18149
|
const { Schedules: schedules } = payload;
|
|
@@ -17873,6 +18209,12 @@ function spliceDoc(docText, generatedSection) {
|
|
|
17873
18209
|
}
|
|
17874
18210
|
return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
|
|
17875
18211
|
}
|
|
18212
|
+
function driftClearableFrom(drift, repoName) {
|
|
18213
|
+
if (drift.class !== "file-vs-registry-stale") return false;
|
|
18214
|
+
const slash = drift.name.indexOf("/");
|
|
18215
|
+
if (slash <= 0) return false;
|
|
18216
|
+
return drift.name.slice(0, slash).toLowerCase() === repoName.toLowerCase();
|
|
18217
|
+
}
|
|
17876
18218
|
var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["cursor-agent"]);
|
|
17877
18219
|
function isHarbourLlmLauncher(executor) {
|
|
17878
18220
|
return HARBOUR_LLM_LAUNCHERS.has(executor);
|
|
@@ -18253,9 +18595,105 @@ function registerSchedulesCommands(program3) {
|
|
|
18253
18595
|
});
|
|
18254
18596
|
}
|
|
18255
18597
|
|
|
18256
|
-
// src/
|
|
18598
|
+
// src/file-lock.ts
|
|
18257
18599
|
var import_promises3 = require("node:fs/promises");
|
|
18258
|
-
var
|
|
18600
|
+
var import_node_path19 = require("node:path");
|
|
18601
|
+
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
18602
|
+
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
18603
|
+
var FileLockBusyError = class extends Error {
|
|
18604
|
+
lockPath;
|
|
18605
|
+
constructor(label, lockPath, maxWaitMs) {
|
|
18606
|
+
super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
|
|
18607
|
+
this.name = "FileLockBusyError";
|
|
18608
|
+
this.lockPath = lockPath;
|
|
18609
|
+
}
|
|
18610
|
+
};
|
|
18611
|
+
function resolveFileLockOpts(opts = {}) {
|
|
18612
|
+
return {
|
|
18613
|
+
staleMs: opts.staleMs ?? 3e4,
|
|
18614
|
+
retryMs: opts.retryMs ?? 50,
|
|
18615
|
+
maxWaitMs: opts.maxWaitMs ?? 5e3,
|
|
18616
|
+
label: opts.label ?? "file lock"
|
|
18617
|
+
};
|
|
18618
|
+
}
|
|
18619
|
+
async function acquireFileLock(lockPath, opts, deadline) {
|
|
18620
|
+
let immediateRetries = 0;
|
|
18621
|
+
for (; ; ) {
|
|
18622
|
+
let handle;
|
|
18623
|
+
try {
|
|
18624
|
+
handle = await (0, import_promises3.open)(lockPath, "wx");
|
|
18625
|
+
} catch (e) {
|
|
18626
|
+
const code = e.code;
|
|
18627
|
+
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
|
|
18628
|
+
const retryNow = async () => {
|
|
18629
|
+
if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
18630
|
+
if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
|
|
18631
|
+
};
|
|
18632
|
+
try {
|
|
18633
|
+
const age = Date.now() - (await (0, import_promises3.stat)(lockPath)).mtimeMs;
|
|
18634
|
+
if (age > opts.staleMs) {
|
|
18635
|
+
try {
|
|
18636
|
+
await (0, import_promises3.unlink)(lockPath);
|
|
18637
|
+
await retryNow();
|
|
18638
|
+
continue;
|
|
18639
|
+
} catch (unlinkError) {
|
|
18640
|
+
if (unlinkError instanceof FileLockBusyError) throw unlinkError;
|
|
18641
|
+
const unlinkCode = unlinkError.code;
|
|
18642
|
+
if (unlinkCode === "ENOENT") {
|
|
18643
|
+
await retryNow();
|
|
18644
|
+
continue;
|
|
18645
|
+
}
|
|
18646
|
+
}
|
|
18647
|
+
}
|
|
18648
|
+
} catch (statError) {
|
|
18649
|
+
if (statError instanceof FileLockBusyError) throw statError;
|
|
18650
|
+
if (statError.code !== "ENOENT") throw statError;
|
|
18651
|
+
await retryNow();
|
|
18652
|
+
continue;
|
|
18653
|
+
}
|
|
18654
|
+
if (Date.now() >= deadline) {
|
|
18655
|
+
throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
|
|
18656
|
+
}
|
|
18657
|
+
await sleep(opts.retryMs);
|
|
18658
|
+
continue;
|
|
18659
|
+
}
|
|
18660
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
18661
|
+
try {
|
|
18662
|
+
await handle.writeFile(token);
|
|
18663
|
+
} catch (e) {
|
|
18664
|
+
await handle.close().catch(() => void 0);
|
|
18665
|
+
throw e;
|
|
18666
|
+
}
|
|
18667
|
+
return { token, handle };
|
|
18668
|
+
}
|
|
18669
|
+
}
|
|
18670
|
+
async function fileLockHeldBy(lockPath, token) {
|
|
18671
|
+
try {
|
|
18672
|
+
return await (0, import_promises3.readFile)(lockPath, "utf8") === token;
|
|
18673
|
+
} catch {
|
|
18674
|
+
return false;
|
|
18675
|
+
}
|
|
18676
|
+
}
|
|
18677
|
+
async function releaseFileLock(lockPath, guard) {
|
|
18678
|
+
await guard.handle.close().catch(() => void 0);
|
|
18679
|
+
if (await fileLockHeldBy(lockPath, guard.token)) {
|
|
18680
|
+
await (0, import_promises3.unlink)(lockPath).catch(() => void 0);
|
|
18681
|
+
}
|
|
18682
|
+
}
|
|
18683
|
+
async function withFileLock(lockPath, opts, fn) {
|
|
18684
|
+
const resolved = resolveFileLockOpts(opts);
|
|
18685
|
+
await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
18686
|
+
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
18687
|
+
try {
|
|
18688
|
+
return await fn();
|
|
18689
|
+
} finally {
|
|
18690
|
+
await releaseFileLock(lockPath, guard);
|
|
18691
|
+
}
|
|
18692
|
+
}
|
|
18693
|
+
|
|
18694
|
+
// src/schedules-lift-command.ts
|
|
18695
|
+
var import_promises4 = require("node:fs/promises");
|
|
18696
|
+
var import_node_path20 = require("node:path");
|
|
18259
18697
|
|
|
18260
18698
|
// src/schedules-lift.ts
|
|
18261
18699
|
var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
|
|
@@ -18354,14 +18792,14 @@ var RegistryUnreachableError = class extends Error {
|
|
|
18354
18792
|
async function readWorkflowFiles(dir) {
|
|
18355
18793
|
let names;
|
|
18356
18794
|
try {
|
|
18357
|
-
names = await (0,
|
|
18795
|
+
names = await (0, import_promises4.readdir)(dir);
|
|
18358
18796
|
} catch {
|
|
18359
18797
|
return [];
|
|
18360
18798
|
}
|
|
18361
18799
|
const files = [];
|
|
18362
18800
|
for (const name of names.sort()) {
|
|
18363
18801
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
18364
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0,
|
|
18802
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises4.readFile)((0, import_node_path20.join)(dir, name), "utf8") });
|
|
18365
18803
|
}
|
|
18366
18804
|
return files;
|
|
18367
18805
|
}
|
|
@@ -18903,7 +19341,7 @@ function registerQueryCommands(program3) {
|
|
|
18903
19341
|
}
|
|
18904
19342
|
|
|
18905
19343
|
// src/bootstrap-commands.ts
|
|
18906
|
-
var
|
|
19344
|
+
var import_node_fs22 = require("node:fs");
|
|
18907
19345
|
|
|
18908
19346
|
// src/bootstrap-verify.ts
|
|
18909
19347
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
@@ -19451,7 +19889,7 @@ function registerBootstrapCommands(program3) {
|
|
|
19451
19889
|
client: defaultGitHubClient(),
|
|
19452
19890
|
projectMeta: meta,
|
|
19453
19891
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
19454
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
19892
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
|
|
19455
19893
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
19456
19894
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
19457
19895
|
requiredGcpApis: (() => {
|
|
@@ -19502,12 +19940,12 @@ function registerBootstrapCommands(program3) {
|
|
|
19502
19940
|
return fail(`bootstrap apply: ${e.message}`);
|
|
19503
19941
|
}
|
|
19504
19942
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
19505
|
-
if (!(0,
|
|
19506
|
-
const manifest = loadBootstrapSeeds((0,
|
|
19943
|
+
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`);
|
|
19944
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
|
|
19507
19945
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
19508
19946
|
const slug = parsedRepo.slug;
|
|
19509
19947
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
19510
|
-
const
|
|
19948
|
+
const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
|
|
19511
19949
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
19512
19950
|
const rawVars = {};
|
|
19513
19951
|
for (const value of cmdOpts.var ?? []) {
|
|
@@ -19585,7 +20023,7 @@ function registerBootstrapCommands(program3) {
|
|
|
19585
20023
|
}
|
|
19586
20024
|
const planned = planSeedAction(resolved, exists);
|
|
19587
20025
|
const isBlock = resolved.source === "managed-block";
|
|
19588
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
20026
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
|
|
19589
20027
|
const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
|
|
19590
20028
|
actions.push(action);
|
|
19591
20029
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
@@ -19640,7 +20078,7 @@ function registerBootstrapCommands(program3) {
|
|
|
19640
20078
|
}
|
|
19641
20079
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
19642
20080
|
if (rulesetSeed) {
|
|
19643
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
20081
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
|
|
19644
20082
|
if (rulesetContent) {
|
|
19645
20083
|
try {
|
|
19646
20084
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -19703,12 +20141,12 @@ LIVE apply to ${repo}:
|
|
|
19703
20141
|
}
|
|
19704
20142
|
|
|
19705
20143
|
// src/stage-commands.ts
|
|
19706
|
-
var
|
|
19707
|
-
var
|
|
20144
|
+
var import_node_fs24 = require("node:fs");
|
|
20145
|
+
var import_node_path22 = require("node:path");
|
|
19708
20146
|
|
|
19709
20147
|
// src/port-registry.ts
|
|
19710
|
-
var
|
|
19711
|
-
var
|
|
20148
|
+
var import_node_fs23 = require("node:fs");
|
|
20149
|
+
var import_node_path21 = require("node:path");
|
|
19712
20150
|
|
|
19713
20151
|
// ../infra/port-geometry.mjs
|
|
19714
20152
|
var PORT_BLOCK = 100;
|
|
@@ -19722,8 +20160,8 @@ function nextPortBlock(registry2) {
|
|
|
19722
20160
|
return [base, base + PORT_SPAN];
|
|
19723
20161
|
}
|
|
19724
20162
|
function loadPortRegistry(path2) {
|
|
19725
|
-
if (!(0,
|
|
19726
|
-
const raw = JSON.parse((0,
|
|
20163
|
+
if (!(0, import_node_fs23.existsSync)(path2)) return {};
|
|
20164
|
+
const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8"));
|
|
19727
20165
|
const out = {};
|
|
19728
20166
|
for (const [key, value] of Object.entries(raw)) {
|
|
19729
20167
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -19737,9 +20175,9 @@ function ensurePortRange(repo, path2) {
|
|
|
19737
20175
|
const existing = registry2[repo];
|
|
19738
20176
|
if (existing) return existing;
|
|
19739
20177
|
const range = nextPortBlock(registry2);
|
|
19740
|
-
const raw = (0,
|
|
20178
|
+
const raw = (0, import_node_fs23.existsSync)(path2) ? JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")) : {};
|
|
19741
20179
|
raw[repo] = range;
|
|
19742
|
-
(0,
|
|
20180
|
+
(0, import_node_fs23.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
19743
20181
|
return range;
|
|
19744
20182
|
}
|
|
19745
20183
|
function portCursorSeed(registry2) {
|
|
@@ -19761,22 +20199,22 @@ function existingPortRange(repo, registry2) {
|
|
|
19761
20199
|
return registry2[repo] ?? null;
|
|
19762
20200
|
}
|
|
19763
20201
|
function portRangeInfraAt(root, source) {
|
|
19764
|
-
const registryPath = (0,
|
|
19765
|
-
const ddbScriptPath = (0,
|
|
19766
|
-
if (!(0,
|
|
20202
|
+
const registryPath = (0, import_node_path21.join)(root, "infra", "port-ranges.json");
|
|
20203
|
+
const ddbScriptPath = (0, import_node_path21.join)(root, "infra", "port-ddb.mjs");
|
|
20204
|
+
if (!(0, import_node_fs23.existsSync)(registryPath) || !(0, import_node_fs23.existsSync)(ddbScriptPath)) return null;
|
|
19767
20205
|
return { root, source, registryPath, ddbScriptPath };
|
|
19768
20206
|
}
|
|
19769
20207
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
19770
20208
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
19771
20209
|
if (direct) return direct;
|
|
19772
|
-
for (let dir = cwd; ; dir = (0,
|
|
19773
|
-
const sibling = portRangeInfraAt((0,
|
|
20210
|
+
for (let dir = cwd; ; dir = (0, import_node_path21.dirname)(dir)) {
|
|
20211
|
+
const sibling = portRangeInfraAt((0, import_node_path21.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
19774
20212
|
if (sibling) return sibling;
|
|
19775
|
-
const parent = (0,
|
|
20213
|
+
const parent = (0, import_node_path21.dirname)(dir);
|
|
19776
20214
|
if (parent === dir) break;
|
|
19777
20215
|
}
|
|
19778
20216
|
if (packageDir) {
|
|
19779
|
-
const pkgRoot = (0,
|
|
20217
|
+
const pkgRoot = (0, import_node_path21.join)(packageDir, "..", "..");
|
|
19780
20218
|
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
19781
20219
|
if (pkgFrom) return pkgFrom;
|
|
19782
20220
|
}
|
|
@@ -19952,8 +20390,8 @@ function registerStageCommands(program3) {
|
|
|
19952
20390
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
19953
20391
|
return decideStage({
|
|
19954
20392
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
19955
|
-
hasCompose: (0,
|
|
19956
|
-
hasEnvExample: (0,
|
|
20393
|
+
hasCompose: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), "docker-compose.yml")),
|
|
20394
|
+
hasEnvExample: (0, import_node_fs24.existsSync)((0, import_node_path22.join)(process.cwd(), ".env.example"))
|
|
19957
20395
|
});
|
|
19958
20396
|
}
|
|
19959
20397
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -20388,9 +20826,9 @@ function registerBoardCommands(program3) {
|
|
|
20388
20826
|
}
|
|
20389
20827
|
|
|
20390
20828
|
// src/merge-cleanup.ts
|
|
20391
|
-
var
|
|
20392
|
-
var
|
|
20393
|
-
var
|
|
20829
|
+
var import_node_fs25 = require("node:fs");
|
|
20830
|
+
var import_promises6 = require("node:fs/promises");
|
|
20831
|
+
var import_node_path24 = require("node:path");
|
|
20394
20832
|
var import_node_child_process11 = require("node:child_process");
|
|
20395
20833
|
|
|
20396
20834
|
// src/board-advance.ts
|
|
@@ -20476,117 +20914,64 @@ function boardAdvanceFailureMessage(result) {
|
|
|
20476
20914
|
}
|
|
20477
20915
|
|
|
20478
20916
|
// src/deferred-registry-store.ts
|
|
20479
|
-
var
|
|
20480
|
-
var
|
|
20481
|
-
var
|
|
20917
|
+
var import_promises5 = require("node:fs/promises");
|
|
20918
|
+
var import_node_path23 = require("node:path");
|
|
20919
|
+
var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
20482
20920
|
async function atomicWrite(target, contents) {
|
|
20483
20921
|
const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
20484
|
-
await (0,
|
|
20922
|
+
await (0, import_promises5.writeFile)(tmp, contents, "utf8");
|
|
20485
20923
|
try {
|
|
20486
20924
|
await renameWithRetry(tmp, target);
|
|
20487
20925
|
} catch (e) {
|
|
20488
|
-
await (0,
|
|
20926
|
+
await (0, import_promises5.unlink)(tmp).catch(() => void 0);
|
|
20489
20927
|
throw e;
|
|
20490
20928
|
}
|
|
20491
20929
|
}
|
|
20492
20930
|
async function renameWithRetry(from, to, attempts = 5, backoffMs = 20) {
|
|
20493
20931
|
for (let i = 0; ; i++) {
|
|
20494
20932
|
try {
|
|
20495
|
-
await (0,
|
|
20933
|
+
await (0, import_promises5.rename)(from, to);
|
|
20496
20934
|
return;
|
|
20497
20935
|
} catch (e) {
|
|
20498
20936
|
const code = e.code;
|
|
20499
20937
|
if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") throw e;
|
|
20500
|
-
await
|
|
20938
|
+
await sleep2(backoffMs);
|
|
20501
20939
|
}
|
|
20502
20940
|
}
|
|
20503
20941
|
}
|
|
20504
20942
|
async function readStrict(registryPath) {
|
|
20505
20943
|
let text;
|
|
20506
20944
|
try {
|
|
20507
|
-
text = await (0,
|
|
20945
|
+
text = await (0, import_promises5.readFile)(registryPath, "utf8");
|
|
20508
20946
|
} catch (e) {
|
|
20509
20947
|
if (e.code === "ENOENT") return [];
|
|
20510
20948
|
throw e;
|
|
20511
20949
|
}
|
|
20512
20950
|
return parseDeferredWorktreesFile(text);
|
|
20513
20951
|
}
|
|
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
|
-
}
|
|
20952
|
+
var acquireLock = acquireFileLock;
|
|
20953
|
+
var lockHeldBy = fileLockHeldBy;
|
|
20954
|
+
var releaseLock = releaseFileLock;
|
|
20566
20955
|
function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
20567
|
-
const opts = {
|
|
20568
|
-
staleMs: lockOpts.staleMs ?? 3e4,
|
|
20569
|
-
retryMs: lockOpts.retryMs ?? 50,
|
|
20570
|
-
maxWaitMs: lockOpts.maxWaitMs ?? 5e3
|
|
20571
|
-
};
|
|
20956
|
+
const opts = resolveFileLockOpts({ ...lockOpts, label: "deferred registry lock" });
|
|
20572
20957
|
const lockPath = `${registryPath}.lock`;
|
|
20573
20958
|
return {
|
|
20574
20959
|
// Lenient read for the sweep's initial fetch: any error (missing/corrupt) yields an empty queue.
|
|
20575
20960
|
read: async () => {
|
|
20576
20961
|
try {
|
|
20577
|
-
return parseDeferredWorktreesFile(await (0,
|
|
20962
|
+
return parseDeferredWorktreesFile(await (0, import_promises5.readFile)(registryPath, "utf8"));
|
|
20578
20963
|
} catch {
|
|
20579
20964
|
return [];
|
|
20580
20965
|
}
|
|
20581
20966
|
},
|
|
20582
20967
|
// Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
|
|
20583
20968
|
write: async (entries) => {
|
|
20584
|
-
await (0,
|
|
20969
|
+
await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
|
|
20585
20970
|
await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
|
|
20586
20971
|
},
|
|
20587
20972
|
// Serialized read-modify-write under the repo-wide lock (#2846).
|
|
20588
20973
|
update: async (mutate) => {
|
|
20589
|
-
await (0,
|
|
20974
|
+
await (0, import_promises5.mkdir)((0, import_node_path23.dirname)(registryPath), { recursive: true });
|
|
20590
20975
|
const deadline = Date.now() + opts.maxWaitMs;
|
|
20591
20976
|
for (; ; ) {
|
|
20592
20977
|
const guard = await acquireLock(lockPath, opts, deadline);
|
|
@@ -20603,7 +20988,7 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
|
20603
20988
|
if (Date.now() >= deadline) {
|
|
20604
20989
|
throw new Error(`deferred registry lock contention: ${lockPath} lost to a concurrent holder past ${opts.maxWaitMs}ms`);
|
|
20605
20990
|
}
|
|
20606
|
-
await
|
|
20991
|
+
await sleep2(opts.retryMs);
|
|
20607
20992
|
}
|
|
20608
20993
|
}
|
|
20609
20994
|
};
|
|
@@ -20650,11 +21035,17 @@ function renderGcApplyResult(result) {
|
|
|
20650
21035
|
lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
|
|
20651
21036
|
lines.push(` tracking refs removed: ${result.removedTrackingRefs.length ? result.removedTrackingRefs.join(", ") : "none"}`);
|
|
20652
21037
|
lines.push(` dead sibling dirs removed: ${result.removedWorktreeDirs.length ? result.removedWorktreeDirs.join(", ") : "none"}`);
|
|
20653
|
-
lines.push(`
|
|
21038
|
+
lines.push(` stale registrations pruned: ${result.pruned ? "yes" : "no"} (clears registrations only \u2014 never deletes a directory)`);
|
|
20654
21039
|
if (result.failed.length) {
|
|
20655
21040
|
lines.push(` failed (${result.failed.length}):`);
|
|
20656
21041
|
for (const f of result.failed) lines.push(` - ${f}`);
|
|
20657
21042
|
}
|
|
21043
|
+
const removedNothing = result.removedBranches.length === 0 && result.removedRemoteBranches.length === 0 && result.removedTrackingRefs.length === 0 && result.removedWorktreeDirs.length === 0;
|
|
21044
|
+
if (removedNothing) {
|
|
21045
|
+
lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
|
|
21046
|
+
lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
|
|
21047
|
+
lines.push(" directory and re-run to clear its registration.");
|
|
21048
|
+
}
|
|
20658
21049
|
return lines.join("\n");
|
|
20659
21050
|
}
|
|
20660
21051
|
async function deleteReviewedRemoteBranch(remote, branch, expectedHeadOid) {
|
|
@@ -20713,7 +21104,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
20713
21104
|
return cleanupPrMergeLocalBranch(branch.branch, {
|
|
20714
21105
|
beforeWorktrees,
|
|
20715
21106
|
startingPath: branch.worktreePath,
|
|
20716
|
-
pathExists: (p) => (0,
|
|
21107
|
+
pathExists: (p) => (0, import_node_fs25.existsSync)(p),
|
|
20717
21108
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
20718
21109
|
teardownWorktreeStage,
|
|
20719
21110
|
deferredStore,
|
|
@@ -20735,12 +21126,12 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
20735
21126
|
const removeDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
20736
21127
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20737
21128
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
20738
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
21129
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path24.dirname)((0, import_node_path24.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
20739
21130
|
const siblingRoot = opts.root ? resolveExplicitScanRoot(opts.root, primaryRepoRoot) : siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
20740
21131
|
for (const wt of plan.worktreeDirs) {
|
|
20741
21132
|
try {
|
|
20742
21133
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
20743
|
-
realpath: (path2) => (0,
|
|
21134
|
+
realpath: (path2) => (0, import_node_fs25.realpathSync)(path2)
|
|
20744
21135
|
});
|
|
20745
21136
|
if (!cleanupTarget.ok) {
|
|
20746
21137
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -20901,13 +21292,13 @@ var realWorktreeDirRemover = {
|
|
|
20901
21292
|
probe: (p) => {
|
|
20902
21293
|
let st;
|
|
20903
21294
|
try {
|
|
20904
|
-
st = (0,
|
|
21295
|
+
st = (0, import_node_fs25.lstatSync)(p);
|
|
20905
21296
|
} catch {
|
|
20906
21297
|
return null;
|
|
20907
21298
|
}
|
|
20908
21299
|
if (st.isSymbolicLink()) return "link";
|
|
20909
21300
|
try {
|
|
20910
|
-
(0,
|
|
21301
|
+
(0, import_node_fs25.readlinkSync)(p);
|
|
20911
21302
|
return "link";
|
|
20912
21303
|
} catch {
|
|
20913
21304
|
}
|
|
@@ -20915,7 +21306,7 @@ var realWorktreeDirRemover = {
|
|
|
20915
21306
|
},
|
|
20916
21307
|
readdir: (p) => {
|
|
20917
21308
|
try {
|
|
20918
|
-
return (0,
|
|
21309
|
+
return (0, import_node_fs25.readdirSync)(p);
|
|
20919
21310
|
} catch {
|
|
20920
21311
|
return [];
|
|
20921
21312
|
}
|
|
@@ -20924,12 +21315,12 @@ var realWorktreeDirRemover = {
|
|
|
20924
21315
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
20925
21316
|
detachLink: (p) => {
|
|
20926
21317
|
try {
|
|
20927
|
-
(0,
|
|
21318
|
+
(0, import_node_fs25.rmdirSync)(p);
|
|
20928
21319
|
} catch {
|
|
20929
|
-
(0,
|
|
21320
|
+
(0, import_node_fs25.unlinkSync)(p);
|
|
20930
21321
|
}
|
|
20931
21322
|
},
|
|
20932
|
-
removeTree: (p) => (0,
|
|
21323
|
+
removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
20933
21324
|
};
|
|
20934
21325
|
async function resolvePrimaryCheckout(execGit) {
|
|
20935
21326
|
try {
|
|
@@ -20959,9 +21350,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
20959
21350
|
}
|
|
20960
21351
|
}
|
|
20961
21352
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
20962
|
-
if (!(0,
|
|
21353
|
+
if (!(0, import_node_fs25.existsSync)(statePath)) return false;
|
|
20963
21354
|
try {
|
|
20964
|
-
const state = JSON.parse((0,
|
|
21355
|
+
const state = JSON.parse((0, import_node_fs25.readFileSync)(statePath, "utf8"));
|
|
20965
21356
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
20966
21357
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
20967
21358
|
} catch {
|
|
@@ -21168,8 +21559,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
|
|
|
21168
21559
|
}
|
|
21169
21560
|
|
|
21170
21561
|
// src/worktree-lifecycle-commands.ts
|
|
21171
|
-
var
|
|
21172
|
-
var
|
|
21562
|
+
var import_node_fs26 = require("node:fs");
|
|
21563
|
+
var import_node_path25 = require("node:path");
|
|
21173
21564
|
var GH_TIMEOUT_MS = 2e4;
|
|
21174
21565
|
var DEFAULT_BASE = "origin/development";
|
|
21175
21566
|
var DEFAULT_REMOTE = "origin";
|
|
@@ -21272,7 +21663,7 @@ function classifyStaleLeaks(input) {
|
|
|
21272
21663
|
var defaultOrphanDirScanDeps = {
|
|
21273
21664
|
listDirs: (root) => {
|
|
21274
21665
|
try {
|
|
21275
|
-
return (0,
|
|
21666
|
+
return (0, import_node_fs26.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path25.join)(root, e.name));
|
|
21276
21667
|
} catch {
|
|
21277
21668
|
return [];
|
|
21278
21669
|
}
|
|
@@ -21328,13 +21719,13 @@ function registerWorktreeCommands(program3) {
|
|
|
21328
21719
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
21329
21720
|
return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
21330
21721
|
}
|
|
21331
|
-
const gitFile = (0,
|
|
21332
|
-
const isLinked = (0,
|
|
21722
|
+
const gitFile = (0, import_node_path25.join)(wtPath, ".git");
|
|
21723
|
+
const isLinked = (0, import_node_fs26.existsSync)(gitFile) && (0, import_node_fs26.statSync)(gitFile).isFile();
|
|
21333
21724
|
if (apply && !isLinked) {
|
|
21334
21725
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
21335
21726
|
}
|
|
21336
21727
|
const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
21337
|
-
const primaryCheckout = commonDir ? (0,
|
|
21728
|
+
const primaryCheckout = commonDir ? (0, import_node_path25.dirname)(commonDir) : wtPath;
|
|
21338
21729
|
const stageSummary = readStageSummary(wtPath);
|
|
21339
21730
|
const hasStage = stageSummary != null;
|
|
21340
21731
|
const steps = landSteps(branch, true, hasStage);
|
|
@@ -21445,10 +21836,10 @@ async function gatherWorktreeContext() {
|
|
|
21445
21836
|
if (s) stages.push({ path: wt.path, port: s.port });
|
|
21446
21837
|
}
|
|
21447
21838
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
21448
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
21839
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path25.dirname)((0, import_node_path25.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
21449
21840
|
const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
21450
21841
|
let orphanDirs = [];
|
|
21451
|
-
if ((0,
|
|
21842
|
+
if ((0, import_node_fs26.existsSync)(wtRoot)) {
|
|
21452
21843
|
orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
|
|
21453
21844
|
...defaultOrphanDirScanDeps,
|
|
21454
21845
|
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
@@ -21471,7 +21862,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
21471
21862
|
}
|
|
21472
21863
|
|
|
21473
21864
|
// src/issue-commands.ts
|
|
21474
|
-
var
|
|
21865
|
+
var import_node_fs27 = require("node:fs");
|
|
21475
21866
|
var import_node_crypto5 = require("node:crypto");
|
|
21476
21867
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
21477
21868
|
async function editIssue(client, options, deps = {}) {
|
|
@@ -21485,7 +21876,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
21485
21876
|
if (options.body !== void 0 || options.bodyFile !== void 0) {
|
|
21486
21877
|
let body;
|
|
21487
21878
|
if (options.bodyFile) {
|
|
21488
|
-
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
21879
|
+
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs27.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
21489
21880
|
body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
|
|
21490
21881
|
} else {
|
|
21491
21882
|
body = options.body ?? "";
|
|
@@ -21982,7 +22373,7 @@ function extendCreateCommand(issue2, batchAttach) {
|
|
|
21982
22373
|
if (opts.batch) {
|
|
21983
22374
|
let specs;
|
|
21984
22375
|
try {
|
|
21985
|
-
const raw = (0,
|
|
22376
|
+
const raw = (0, import_node_fs27.readFileSync)(opts.batch, "utf8");
|
|
21986
22377
|
specs = JSON.parse(raw);
|
|
21987
22378
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
21988
22379
|
} catch (e) {
|
|
@@ -22040,12 +22431,12 @@ ${lines}`);
|
|
|
22040
22431
|
}
|
|
22041
22432
|
|
|
22042
22433
|
// src/train-commands.ts
|
|
22043
|
-
var
|
|
22044
|
-
var
|
|
22434
|
+
var import_node_fs29 = require("node:fs");
|
|
22435
|
+
var import_node_path27 = require("node:path");
|
|
22045
22436
|
|
|
22046
22437
|
// src/plugin-guard-io.ts
|
|
22047
|
-
var
|
|
22048
|
-
var
|
|
22438
|
+
var import_node_fs28 = require("node:fs");
|
|
22439
|
+
var import_node_path26 = require("node:path");
|
|
22049
22440
|
var import_node_os6 = require("node:os");
|
|
22050
22441
|
|
|
22051
22442
|
// src/plugin-guard.ts
|
|
@@ -22175,11 +22566,11 @@ function runHostBin(bin, args, opts) {
|
|
|
22175
22566
|
}
|
|
22176
22567
|
var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
|
|
22177
22568
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
22178
|
-
return (0,
|
|
22569
|
+
return (0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
22179
22570
|
};
|
|
22180
22571
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
22181
22572
|
try {
|
|
22182
|
-
return JSON.parse((0,
|
|
22573
|
+
return JSON.parse((0, import_node_fs28.readFileSync)(installedPluginsPath2(surface), "utf8"));
|
|
22183
22574
|
} catch {
|
|
22184
22575
|
return null;
|
|
22185
22576
|
}
|
|
@@ -22187,13 +22578,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
|
22187
22578
|
function marketplaceCloneCandidates(surface, home) {
|
|
22188
22579
|
if (surface === "codex") {
|
|
22189
22580
|
return [
|
|
22190
|
-
(0,
|
|
22191
|
-
(0,
|
|
22581
|
+
(0, import_node_path26.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
|
|
22582
|
+
(0, import_node_path26.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
|
|
22192
22583
|
];
|
|
22193
22584
|
}
|
|
22194
|
-
return [(0,
|
|
22585
|
+
return [(0, import_node_path26.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
22195
22586
|
}
|
|
22196
|
-
function marketplaceClonePresent(surface, home, exists =
|
|
22587
|
+
function marketplaceClonePresent(surface, home, exists = import_node_fs28.existsSync) {
|
|
22197
22588
|
return marketplaceCloneCandidates(surface, home).some(exists);
|
|
22198
22589
|
}
|
|
22199
22590
|
async function fetchNpmReleasedVersion() {
|
|
@@ -22221,7 +22612,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
22221
22612
|
isOrgRepo,
|
|
22222
22613
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
22223
22614
|
marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
|
|
22224
|
-
pluginCachePresent: (0,
|
|
22615
|
+
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path26.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
22225
22616
|
};
|
|
22226
22617
|
}
|
|
22227
22618
|
function claudePluginGuardState(isOrgRepo) {
|
|
@@ -22356,7 +22747,7 @@ function formatTrainStatus(r) {
|
|
|
22356
22747
|
// src/train-commands.ts
|
|
22357
22748
|
function readRepoVersion() {
|
|
22358
22749
|
try {
|
|
22359
|
-
return JSON.parse((0,
|
|
22750
|
+
return JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path27.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
22360
22751
|
} catch {
|
|
22361
22752
|
return void 0;
|
|
22362
22753
|
}
|
|
@@ -22502,6 +22893,9 @@ function registerDeployCommands(program3) {
|
|
|
22502
22893
|
}
|
|
22503
22894
|
|
|
22504
22895
|
// src/discovery-commands.ts
|
|
22896
|
+
var import_node_fs30 = require("node:fs");
|
|
22897
|
+
var import_node_os7 = require("node:os");
|
|
22898
|
+
var import_node_path28 = require("node:path");
|
|
22505
22899
|
var GC_GH_TIMEOUT_MS3 = 2e4;
|
|
22506
22900
|
async function collectStatus() {
|
|
22507
22901
|
let branch = "";
|
|
@@ -22586,6 +22980,23 @@ async function recommendNext(deps) {
|
|
|
22586
22980
|
const top = sorted[0];
|
|
22587
22981
|
return { number: top.number, title: top.title, url: top.url, priority: top.priority, repo: top.repository };
|
|
22588
22982
|
}
|
|
22983
|
+
function onboardPluginGate(deps) {
|
|
22984
|
+
const { registered, declared, ref } = readKnownMarketplace(deps.readKnown(), MMI_MARKETPLACE_NAME);
|
|
22985
|
+
if (!registered) {
|
|
22986
|
+
return { ok: false, detail: `${MMI_MARKETPLACE_NAME} marketplace not registered on this machine` };
|
|
22987
|
+
}
|
|
22988
|
+
const autoUpdate = resolveAutoUpdate({
|
|
22989
|
+
name: MMI_MARKETPLACE_NAME,
|
|
22990
|
+
registered,
|
|
22991
|
+
declared,
|
|
22992
|
+
settingsDeclared: readSettingsAutoUpdate(deps.readSettings(), MMI_MARKETPLACE_NAME)
|
|
22993
|
+
}).effective;
|
|
22994
|
+
const catalog = resolveCatalogRef({ name: MMI_MARKETPLACE_NAME, registered, ref, autoUpdate });
|
|
22995
|
+
const gaps = [];
|
|
22996
|
+
if (!autoUpdate) gaps.push(`auto-update is off \u2014 turn it on with ${AUTO_UPDATE_TOGGLE_STEPS}`);
|
|
22997
|
+
if (catalog && !catalog.agrees) gaps.push(`the catalog is ${catalog.unpinned ? "unpinned" : `pinned to ${catalog.ref}`} \u2014 ${CATALOG_REF_PIN_STEPS}`);
|
|
22998
|
+
return gaps.length === 0 ? { ok: true, detail: `auto-update on, catalog pinned to ${catalog?.ref}` } : { ok: false, detail: gaps.join("; ") };
|
|
22999
|
+
}
|
|
22589
23000
|
async function collectOnboardStatus() {
|
|
22590
23001
|
const cfg = await loadConfig();
|
|
22591
23002
|
let track = "unknown";
|
|
@@ -22659,7 +23070,12 @@ async function collectOnboardStatus() {
|
|
|
22659
23070
|
else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
|
|
22660
23071
|
else nextCommand = "mmi-cli board read \u2014 no claimable items found";
|
|
22661
23072
|
}
|
|
22662
|
-
|
|
23073
|
+
const home = (0, import_node_os7.homedir)();
|
|
23074
|
+
const plugin = onboardPluginGate({
|
|
23075
|
+
readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
|
|
23076
|
+
readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
|
|
23077
|
+
});
|
|
23078
|
+
return { track, board, registry: registry2, secrets, plugin, nextCommand };
|
|
22663
23079
|
}
|
|
22664
23080
|
function registerDiscoveryCommands(program3) {
|
|
22665
23081
|
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 +23119,7 @@ function registerDiscoveryCommands(program3) {
|
|
|
22703
23119
|
console.log(`Board: ${report.board.ok ? "\u2713" : "\u2717"} ${report.board.detail}`);
|
|
22704
23120
|
console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
|
|
22705
23121
|
console.log(`Secrets: ${report.secrets.ok ? "\u2713" : "\u2717"} ${report.secrets.detail}`);
|
|
23122
|
+
console.log(`Plugin: ${report.plugin.ok ? "\u2713" : "\u2717"} ${report.plugin.detail}`);
|
|
22706
23123
|
console.log(`
|
|
22707
23124
|
Next command: ${report.nextCommand}`);
|
|
22708
23125
|
}
|
|
@@ -22862,7 +23279,7 @@ function registerExplainCommand(program3) {
|
|
|
22862
23279
|
}
|
|
22863
23280
|
|
|
22864
23281
|
// src/pr-commands.ts
|
|
22865
|
-
var
|
|
23282
|
+
var import_promises7 = require("node:fs/promises");
|
|
22866
23283
|
var GC_GH_TIMEOUT_MS4 = 2e4;
|
|
22867
23284
|
var CHECKS_WATCH_POLL_MS = 15e3;
|
|
22868
23285
|
var CHECKS_WATCH_TIMEOUT_MS = 10 * 6e4;
|
|
@@ -23006,10 +23423,10 @@ function registerPrLifecycleCommands(program3) {
|
|
|
23006
23423
|
let body;
|
|
23007
23424
|
try {
|
|
23008
23425
|
if (o.title || o.titleFile) {
|
|
23009
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
23426
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
23010
23427
|
}
|
|
23011
23428
|
if (o.body || o.bodyFile) {
|
|
23012
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23429
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
23013
23430
|
}
|
|
23014
23431
|
} catch (e) {
|
|
23015
23432
|
return fail(`pr edit: ${e.message}`);
|
|
@@ -23039,7 +23456,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
23039
23456
|
}
|
|
23040
23457
|
let body;
|
|
23041
23458
|
try {
|
|
23042
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23459
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
23043
23460
|
} catch (e) {
|
|
23044
23461
|
return fail(`pr comment: ${e.message}`);
|
|
23045
23462
|
}
|
|
@@ -23443,7 +23860,7 @@ function renderDoctorText(checks, opts) {
|
|
|
23443
23860
|
return lines.join("\n");
|
|
23444
23861
|
}
|
|
23445
23862
|
function doctorReportExitCode(checks) {
|
|
23446
|
-
return checks.some((c) => !c.ok) ? 1 : 0;
|
|
23863
|
+
return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
|
|
23447
23864
|
}
|
|
23448
23865
|
|
|
23449
23866
|
// src/doctor-clean.ts
|
|
@@ -23477,6 +23894,7 @@ function checkGithubAuth(probe) {
|
|
|
23477
23894
|
};
|
|
23478
23895
|
}
|
|
23479
23896
|
function checkAwsIdentity(probe) {
|
|
23897
|
+
if (probe.probed === false) return null;
|
|
23480
23898
|
if (!probe.isOrgRepo) return null;
|
|
23481
23899
|
const arn = probe.callerArn?.trim();
|
|
23482
23900
|
return {
|
|
@@ -23485,7 +23903,11 @@ function checkAwsIdentity(probe) {
|
|
|
23485
23903
|
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
23904
|
// Name the caller. A green "aws identity" says only "not root" — it does not say WHO, and "no AWS
|
|
23487
23905
|
// configured at all" is also green here. Under --verbose those two must not look the same.
|
|
23488
|
-
|
|
23906
|
+
//
|
|
23907
|
+
// #3485: the empty case says "not resolved", never "not configured". `awsCallerArn` returns undefined
|
|
23908
|
+
// for ANY failure — expired SSO, no `aws` binary, a timeout — so asserting "no AWS identity configured"
|
|
23909
|
+
// is the same false-fact green this issue is about, one layer down. Evidence states what was observed.
|
|
23910
|
+
verbose: [`caller: ${arn || "(none resolved \u2014 no AWS configured, or the probe failed; either is allowed)"}`]
|
|
23489
23911
|
};
|
|
23490
23912
|
}
|
|
23491
23913
|
function checkRepoWorktrees(probe) {
|
|
@@ -23515,6 +23937,11 @@ function checkWorktreeRoots(probe) {
|
|
|
23515
23937
|
return {
|
|
23516
23938
|
ok: false,
|
|
23517
23939
|
label: "worktree roots",
|
|
23940
|
+
// #3485: report-only in EVERY mode means report-only in the exit code too. `worktree gc --root` does
|
|
23941
|
+
// exist and is the eventual fix, but this row is deliberately not a gate: the remedy is a human
|
|
23942
|
+
// review of directories that may hold unlanded work, and a size-based auto-sweep once nearly deleted
|
|
23943
|
+
// an unlanded correctness fix. Counting it made `mmi-cli doctor` exit 1 until someone swept by hand.
|
|
23944
|
+
reportOnly: true,
|
|
23518
23945
|
// The fix is a review, never a delete: `gc --root` still refuses anything it cannot classify as dead,
|
|
23519
23946
|
// and doctor itself — with or without --apply — never touches these directories.
|
|
23520
23947
|
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 +23957,8 @@ function checkClaudePlugin(probe) {
|
|
|
23530
23957
|
const { installed, released, guardState } = probe;
|
|
23531
23958
|
const evidence = [
|
|
23532
23959
|
`installed: ${installed ?? "(none)"}`,
|
|
23533
|
-
|
|
23960
|
+
// #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
|
|
23961
|
+
`released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
|
|
23534
23962
|
`resolvable: ${guardState}`
|
|
23535
23963
|
];
|
|
23536
23964
|
const trigger = pluginHealTrigger(probe);
|
|
@@ -23552,15 +23980,16 @@ function checkClaudePlugin(probe) {
|
|
|
23552
23980
|
verbose: evidence
|
|
23553
23981
|
};
|
|
23554
23982
|
}
|
|
23983
|
+
if (!released) return null;
|
|
23555
23984
|
return { ok: true, label: installed ? `Claude plugin ${installed}` : "Claude plugin", verbose: evidence };
|
|
23556
23985
|
}
|
|
23557
|
-
function checkCliVersion(input) {
|
|
23986
|
+
function checkCliVersion(input, releasedNote) {
|
|
23558
23987
|
const report = buildVersionLagReport(input);
|
|
23559
23988
|
const evidence = [
|
|
23560
23989
|
`running: ${report.currentVersion}`,
|
|
23561
|
-
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`
|
|
23990
|
+
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
|
|
23562
23991
|
];
|
|
23563
|
-
if (!report.releasedVersion) return
|
|
23992
|
+
if (!report.releasedVersion) return null;
|
|
23564
23993
|
if (report.ok) return { ok: true, label: `mmi-cli ${report.currentVersion}`, verbose: evidence };
|
|
23565
23994
|
return {
|
|
23566
23995
|
ok: false,
|
|
@@ -23595,6 +24024,12 @@ function checkPluginCache(input) {
|
|
|
23595
24024
|
return {
|
|
23596
24025
|
ok: false,
|
|
23597
24026
|
label: "plugin cache",
|
|
24027
|
+
// #3485: deliberately NOT `reportOnly`, and not in the closed set docs/doctor-contract.md enumerates.
|
|
24028
|
+
// A checker argued it qualifies because no `doctor --apply` clears it; I tagged it, then reverted.
|
|
24029
|
+
// `mmi-cli plugin prune --apply` clears it in one command and a re-run goes green, so this is an
|
|
24030
|
+
// ordinary red the operator is expected to act on. "The doctor cannot heal it automatically" was never
|
|
24031
|
+
// the bar — see the contract for why report-only is a per-row ruling rather than a rule you can apply
|
|
24032
|
+
// from here.
|
|
23598
24033
|
// Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
|
|
23599
24034
|
// on its own so a cache with zero versioned dirs still gets an honest ✗.
|
|
23600
24035
|
detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
|
|
@@ -23623,10 +24058,14 @@ function checkSchedules(probe) {
|
|
|
23623
24058
|
...probe.incomplete.map((i) => `incomplete: ${i}`),
|
|
23624
24059
|
...probe.drift.map((d) => `drift: ${d}`)
|
|
23625
24060
|
];
|
|
24061
|
+
const clearableHere = probe.clearableHere ?? 0;
|
|
23626
24062
|
if (probe.incomplete.length) {
|
|
23627
24063
|
return {
|
|
23628
24064
|
ok: false,
|
|
23629
24065
|
label: "schedules",
|
|
24066
|
+
// #3485: ruled report-only — an unreadable source is a read failure somewhere in the org, and
|
|
24067
|
+
// nothing in this checkout clears it.
|
|
24068
|
+
reportOnly: true,
|
|
23630
24069
|
detail: `${probe.incomplete.length} source(s) unreadable`,
|
|
23631
24070
|
fix: "run `mmi-cli org schedules` for the full report \u2014 the notebook may be missing armed entries",
|
|
23632
24071
|
verbose: evidence
|
|
@@ -23636,8 +24075,14 @@ function checkSchedules(probe) {
|
|
|
23636
24075
|
return {
|
|
23637
24076
|
ok: false,
|
|
23638
24077
|
label: "schedules",
|
|
24078
|
+
// #3485 ruled this row report-only because the drift classes are owned by other repos. #3492 makes
|
|
24079
|
+
// that conditional rather than blanket: when a finding IS clearable from this checkout — a
|
|
24080
|
+
// `file-vs-registry-stale` row for this repo, one `org schedules register` away — the row gates
|
|
24081
|
+
// like any other actionable red. Exempting a fault the operator can fix in one command is the
|
|
24082
|
+
// tolerated-red failure the tier exists to prevent, not an instance of it.
|
|
24083
|
+
...clearableHere > 0 ? {} : { reportOnly: true },
|
|
23639
24084
|
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",
|
|
24085
|
+
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
24086
|
verbose: evidence
|
|
23642
24087
|
};
|
|
23643
24088
|
}
|
|
@@ -23652,6 +24097,10 @@ function checkDocsAudit(probe) {
|
|
|
23652
24097
|
return {
|
|
23653
24098
|
ok: false,
|
|
23654
24099
|
label: "docs-audit",
|
|
24100
|
+
// #3485: ruled report-only — the remedy is re-running or re-arming the janitor in the repo that
|
|
24101
|
+
// owns it. `mmi-cli docs audit record` can write a verdict from here, but hand-writing one to clear
|
|
24102
|
+
// a dead-man check defeats the check (#3067 pillar 4: silence is an alarm, never a success).
|
|
24103
|
+
reportOnly: true,
|
|
23655
24104
|
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
24105
|
verbose: [probe.detail]
|
|
23657
24106
|
};
|
|
@@ -23705,12 +24154,15 @@ function gcReapable(plan) {
|
|
|
23705
24154
|
return plan.branches.length + plan.trackingRefs.length + plan.worktreeDirs.length;
|
|
23706
24155
|
}
|
|
23707
24156
|
async function runDoctorClean(opts, io, deps) {
|
|
23708
|
-
const
|
|
24157
|
+
const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
|
|
24158
|
+
const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
|
|
24159
|
+
const probeReleased = !opts.fast || Boolean(opts.self);
|
|
24160
|
+
const probeAws = !opts.fast && !opts.banner;
|
|
23709
24161
|
const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
|
|
23710
24162
|
deps.githubLogin(),
|
|
23711
24163
|
deps.isOrgRepo(),
|
|
23712
|
-
|
|
23713
|
-
|
|
24164
|
+
probeReleased ? deps.releasedVersion() : Promise.resolve(void 0),
|
|
24165
|
+
probeAws ? deps.awsCallerArn() : Promise.resolve(void 0),
|
|
23714
24166
|
// #3365. Skipped on the banner/preflight lanes, which run on every SessionStart and must not pay a
|
|
23715
24167
|
// network round-trip — and on a bare `--fast`, whose contract is offline-safe. It DOES run on
|
|
23716
24168
|
// `--self`, which rides the fast lane but is the interactive "is my setup actually fine?" diagnostic,
|
|
@@ -23725,7 +24177,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23725
24177
|
if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
|
|
23726
24178
|
checks.push(...checkGithubPools(await deps.githubPools()));
|
|
23727
24179
|
}
|
|
23728
|
-
const aws = checkAwsIdentity({ isOrgRepo, callerArn });
|
|
24180
|
+
const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
|
|
23729
24181
|
if (aws) checks.push(aws);
|
|
23730
24182
|
checks.push(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
|
|
23731
24183
|
if (isOrgRepo) {
|
|
@@ -23737,16 +24189,17 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23737
24189
|
];
|
|
23738
24190
|
if (gi.ok) {
|
|
23739
24191
|
checks.push({ ok: true, label: "gitignore block", verbose: giEvidence });
|
|
23740
|
-
} else if (
|
|
24192
|
+
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
23741
24193
|
checks.push({ ok: true, label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
23742
24194
|
restartPending = true;
|
|
23743
24195
|
} else {
|
|
23744
24196
|
checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
23745
24197
|
}
|
|
23746
24198
|
}
|
|
23747
|
-
const
|
|
24199
|
+
const releasedNote = deps.releasedVersionNote?.();
|
|
24200
|
+
const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo), releasedNote };
|
|
23748
24201
|
const healTrigger = pluginHealTrigger(pluginProbe);
|
|
23749
|
-
if (
|
|
24202
|
+
if (applyEnv && deps.healPlugin && healTrigger) {
|
|
23750
24203
|
const heal = await deps.healPlugin();
|
|
23751
24204
|
const label = healTrigger === "behind" ? `Claude plugin ${installed} \u2192 ${released}` : "Claude plugin \u2014 reinstalled";
|
|
23752
24205
|
const healEvidence = [
|
|
@@ -23763,18 +24216,25 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23763
24216
|
} : {
|
|
23764
24217
|
ok: false,
|
|
23765
24218
|
label,
|
|
23766
|
-
|
|
24219
|
+
// #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
|
|
24220
|
+
// not fix what it found — but it is not this invocation's to clear. The other process is doing it;
|
|
24221
|
+
// waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
|
|
24222
|
+
// genuine gap and still gates.
|
|
24223
|
+
...heal.skipped ? { reportOnly: true } : {},
|
|
24224
|
+
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`,
|
|
23767
24225
|
verbose: healEvidence
|
|
23768
24226
|
});
|
|
23769
|
-
restartPending = true;
|
|
24227
|
+
if (!heal.skipped) restartPending = true;
|
|
23770
24228
|
} else {
|
|
23771
24229
|
const plugin = checkClaudePlugin(pluginProbe);
|
|
23772
|
-
|
|
23773
|
-
|
|
24230
|
+
if (plugin) {
|
|
24231
|
+
checks.push(plugin);
|
|
24232
|
+
if (!plugin.ok) restartPending = true;
|
|
24233
|
+
}
|
|
23774
24234
|
}
|
|
23775
24235
|
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
23776
24236
|
const cliReport = buildVersionLagReport(cliInput);
|
|
23777
|
-
if (
|
|
24237
|
+
if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
|
|
23778
24238
|
const heal = await deps.updateCli(cliReport.releasedVersion);
|
|
23779
24239
|
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
23780
24240
|
checks.push(heal.ok ? {
|
|
@@ -23785,17 +24245,48 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23785
24245
|
} : {
|
|
23786
24246
|
ok: false,
|
|
23787
24247
|
label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
23788
|
-
|
|
24248
|
+
// #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
|
|
24249
|
+
...heal.skipped ? { reportOnly: true } : {},
|
|
24250
|
+
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)}\``,
|
|
23789
24251
|
verbose: healEvidence
|
|
23790
24252
|
});
|
|
23791
24253
|
} else {
|
|
23792
|
-
|
|
24254
|
+
const cli = checkCliVersion(cliInput, releasedNote);
|
|
24255
|
+
if (cli) checks.push(cli);
|
|
24256
|
+
}
|
|
24257
|
+
const cacheProbe = deps.pluginCache();
|
|
24258
|
+
const cacheStale = cacheProbe.stale.length > 0 || cacheProbe.staging.length > 0;
|
|
24259
|
+
if (applyEnv && deps.prunePluginCache && cacheStale) {
|
|
24260
|
+
const pruned = await deps.prunePluginCache();
|
|
24261
|
+
const removed = pruned.removed ?? [];
|
|
24262
|
+
const pruneEvidence = [
|
|
24263
|
+
`cached versions: ${cacheProbe.cached}`,
|
|
24264
|
+
`stale: ${cacheProbe.stale.length ? cacheProbe.stale.join(", ") : "(none)"}`,
|
|
24265
|
+
`orphaned staging dirs: ${cacheProbe.staging.length ? cacheProbe.staging.join(", ") : "(none)"}`,
|
|
24266
|
+
`prune: ${pruned.detail}`,
|
|
24267
|
+
...removed.map((r) => `removed: ${r}`)
|
|
24268
|
+
];
|
|
24269
|
+
checks.push(pruned.ok ? {
|
|
24270
|
+
ok: true,
|
|
24271
|
+
label: "plugin cache",
|
|
24272
|
+
detail: removed.length ? `pruned ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}` : "nothing to prune",
|
|
24273
|
+
verbose: pruneEvidence
|
|
24274
|
+
} : {
|
|
24275
|
+
ok: false,
|
|
24276
|
+
label: "plugin cache",
|
|
24277
|
+
// Same split as the other env heals (#3489): a contention skip is not this run's gap.
|
|
24278
|
+
...pruned.skipped ? { reportOnly: true } : {},
|
|
24279
|
+
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\``,
|
|
24280
|
+
verbose: pruneEvidence
|
|
24281
|
+
});
|
|
24282
|
+
} else {
|
|
24283
|
+
checks.push(checkPluginCache(cacheProbe));
|
|
23793
24284
|
}
|
|
23794
|
-
checks.push(checkPluginCache(deps.pluginCache()));
|
|
23795
24285
|
if (deps.sessionPayload) {
|
|
23796
24286
|
const payload = checkSessionPayload(deps.sessionPayload());
|
|
23797
24287
|
if (payload) checks.push(payload);
|
|
23798
24288
|
}
|
|
24289
|
+
if (deps.marketplaceRows) checks.push(...deps.marketplaceRows());
|
|
23799
24290
|
if (!opts.fast && !opts.banner && !opts.preflight) {
|
|
23800
24291
|
const probe = await deps.schedulesNotebook().catch((e) => ({
|
|
23801
24292
|
armed: 0,
|
|
@@ -23821,7 +24312,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23821
24312
|
const roots = checkWorktreeRoots(probe);
|
|
23822
24313
|
if (roots) checks.push(roots);
|
|
23823
24314
|
}
|
|
23824
|
-
if (isOrgRepo && !opts.fast && !opts.banner) {
|
|
24315
|
+
if (isOrgRepo && !opts.fast && !opts.banner && !opts.preflight) {
|
|
23825
24316
|
try {
|
|
23826
24317
|
checks.push(checkTrainSync(await deps.syncTrain()));
|
|
23827
24318
|
} catch (e) {
|
|
@@ -23836,7 +24327,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23836
24327
|
const repoRoot2 = await deps.repoRoot();
|
|
23837
24328
|
try {
|
|
23838
24329
|
const plan = await deps.gcPlan("origin", 200);
|
|
23839
|
-
if (
|
|
24330
|
+
if (applyRepo) {
|
|
23840
24331
|
const r = await deps.applyGcPlan(plan, "origin");
|
|
23841
24332
|
const reaped = r.removedBranches.length + r.removedRemoteBranches.length + r.removedWorktreeDirs.length + r.removedTrackingRefs.length;
|
|
23842
24333
|
const detailParts = [];
|
|
@@ -23876,7 +24367,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23876
24367
|
}
|
|
23877
24368
|
const scratch = deps.executeScratchGc(repoRoot2, { apply: false });
|
|
23878
24369
|
const scratchEvidence = scratch.plan.safeAuto.length ? scratch.plan.safeAuto.map((c) => `aged: ${c.path} (${c.reason})`) : ["nothing aged"];
|
|
23879
|
-
if (
|
|
24370
|
+
if (applyRepo && scratch.plan.safeAuto.length > 0) {
|
|
23880
24371
|
const applied = deps.executeScratchGc(repoRoot2, { apply: true });
|
|
23881
24372
|
const pruned = applied.applied?.pruned.length ?? 0;
|
|
23882
24373
|
checks.push({ ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
|
|
@@ -23923,17 +24414,17 @@ function parseOriginRepo(remoteUrl) {
|
|
|
23923
24414
|
}
|
|
23924
24415
|
function ghHostsConfigPath(env, platform2) {
|
|
23925
24416
|
const sep2 = platform2 === "win32" ? "\\" : "/";
|
|
23926
|
-
const
|
|
24417
|
+
const join26 = (...parts) => parts.join(sep2);
|
|
23927
24418
|
const explicit = env.GH_CONFIG_DIR?.trim();
|
|
23928
|
-
if (explicit) return
|
|
24419
|
+
if (explicit) return join26(explicit, "hosts.yml");
|
|
23929
24420
|
if (platform2 === "win32") {
|
|
23930
24421
|
const appData = (env.AppData ?? env.APPDATA)?.trim();
|
|
23931
|
-
return appData ?
|
|
24422
|
+
return appData ? join26(appData, "GitHub CLI", "hosts.yml") : void 0;
|
|
23932
24423
|
}
|
|
23933
24424
|
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
23934
|
-
if (xdg) return
|
|
24425
|
+
if (xdg) return join26(xdg, "gh", "hosts.yml");
|
|
23935
24426
|
const home = env.HOME?.trim();
|
|
23936
|
-
return home ?
|
|
24427
|
+
return home ? join26(home, ".config", "gh", "hosts.yml") : void 0;
|
|
23937
24428
|
}
|
|
23938
24429
|
function parseGhHostsAccounts(yaml, host = "github.com") {
|
|
23939
24430
|
let hostIndent = null;
|
|
@@ -23983,9 +24474,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
23983
24474
|
}
|
|
23984
24475
|
|
|
23985
24476
|
// src/doctor-io.ts
|
|
23986
|
-
var
|
|
23987
|
-
var
|
|
23988
|
-
var
|
|
24477
|
+
var import_node_fs31 = require("node:fs");
|
|
24478
|
+
var import_node_os8 = require("node:os");
|
|
24479
|
+
var import_node_path29 = require("node:path");
|
|
23989
24480
|
var import_node_child_process12 = require("node:child_process");
|
|
23990
24481
|
var import_node_util8 = require("node:util");
|
|
23991
24482
|
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
|
|
@@ -23993,7 +24484,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
|
23993
24484
|
function installedClaudePluginVersion() {
|
|
23994
24485
|
try {
|
|
23995
24486
|
const file = JSON.parse(
|
|
23996
|
-
(0,
|
|
24487
|
+
(0, import_node_fs31.readFileSync)((0, import_node_path29.join)((0, import_node_os8.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
23997
24488
|
);
|
|
23998
24489
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
23999
24490
|
if (versions.length === 0) return void 0;
|
|
@@ -24014,13 +24505,13 @@ function worktreeRootSync() {
|
|
|
24014
24505
|
}
|
|
24015
24506
|
var gitignorePath = () => {
|
|
24016
24507
|
const root = worktreeRootSync();
|
|
24017
|
-
return root === null ? null : (0,
|
|
24508
|
+
return root === null ? null : (0, import_node_path29.join)(root, ".gitignore");
|
|
24018
24509
|
};
|
|
24019
24510
|
function readGitignore() {
|
|
24020
24511
|
const path2 = gitignorePath();
|
|
24021
24512
|
if (path2 === null) return null;
|
|
24022
24513
|
try {
|
|
24023
|
-
return (0,
|
|
24514
|
+
return (0, import_node_fs31.readFileSync)(path2, "utf8");
|
|
24024
24515
|
} catch {
|
|
24025
24516
|
return null;
|
|
24026
24517
|
}
|
|
@@ -24029,7 +24520,7 @@ function writeGitignore(content) {
|
|
|
24029
24520
|
const path2 = gitignorePath();
|
|
24030
24521
|
if (path2 === null) return false;
|
|
24031
24522
|
try {
|
|
24032
|
-
(0,
|
|
24523
|
+
(0, import_node_fs31.writeFileSync)(path2, content, "utf8");
|
|
24033
24524
|
return true;
|
|
24034
24525
|
} catch {
|
|
24035
24526
|
return false;
|
|
@@ -24053,7 +24544,7 @@ async function repoRoot() {
|
|
|
24053
24544
|
}
|
|
24054
24545
|
function hasRepoLocalWorktrees() {
|
|
24055
24546
|
const root = worktreeRootSync();
|
|
24056
|
-
return root !== null && (0,
|
|
24547
|
+
return root !== null && (0, import_node_fs31.existsSync)((0, import_node_path29.join)(root, ".worktrees"));
|
|
24057
24548
|
}
|
|
24058
24549
|
|
|
24059
24550
|
// src/index.ts
|
|
@@ -24088,13 +24579,61 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
24088
24579
|
function ghMultiAccountCaveat(announcedLogin) {
|
|
24089
24580
|
try {
|
|
24090
24581
|
const hostsPath = ghHostsConfigPath(process.env, process.platform);
|
|
24091
|
-
if (!hostsPath || !(0,
|
|
24092
|
-
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0,
|
|
24582
|
+
if (!hostsPath || !(0, import_node_fs32.existsSync)(hostsPath)) return void 0;
|
|
24583
|
+
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs32.readFileSync)(hostsPath, "utf8")));
|
|
24093
24584
|
} catch {
|
|
24094
24585
|
return void 0;
|
|
24095
24586
|
}
|
|
24096
24587
|
}
|
|
24097
|
-
|
|
24588
|
+
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
24589
|
+
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
24590
|
+
function envHealLockPath(home) {
|
|
24591
|
+
return (0, import_node_path30.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
24592
|
+
}
|
|
24593
|
+
async function withEnvHealLock(what, run) {
|
|
24594
|
+
try {
|
|
24595
|
+
return await withFileLock(
|
|
24596
|
+
envHealLockPath((0, import_node_os9.homedir)()),
|
|
24597
|
+
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
24598
|
+
run
|
|
24599
|
+
);
|
|
24600
|
+
} catch (e) {
|
|
24601
|
+
if (e instanceof FileLockBusyError) {
|
|
24602
|
+
return { ok: false, skipped: true, detail: `${what} skipped \u2014 ${e.message}` };
|
|
24603
|
+
}
|
|
24604
|
+
return { ok: false, detail: `${what} could not take the env-heal lock \u2014 ${e.message}` };
|
|
24605
|
+
}
|
|
24606
|
+
}
|
|
24607
|
+
function docsJanitorScheduleId(repo) {
|
|
24608
|
+
return `${repo.split("/").pop()}/docs-janitor`;
|
|
24609
|
+
}
|
|
24610
|
+
function throttledReleasedVersion() {
|
|
24611
|
+
let note;
|
|
24612
|
+
return {
|
|
24613
|
+
note: () => note,
|
|
24614
|
+
read: async () => {
|
|
24615
|
+
const cachePath = releasedVersionCachePath(repoRuntimeStatePath(process.cwd()));
|
|
24616
|
+
const cached = readReleasedVersionCache(cachePath);
|
|
24617
|
+
if (cached) {
|
|
24618
|
+
note = cachedReadNote(cached.at);
|
|
24619
|
+
return cached.version;
|
|
24620
|
+
}
|
|
24621
|
+
const live = await fetchNpmReleasedVersion();
|
|
24622
|
+
if (live) writeReleasedVersionCache(cachePath, live);
|
|
24623
|
+
note = void 0;
|
|
24624
|
+
return live;
|
|
24625
|
+
}
|
|
24626
|
+
};
|
|
24627
|
+
}
|
|
24628
|
+
function mmiDoctorDeps(opts = {}) {
|
|
24629
|
+
const throttled = opts.throttleReleasedRead ? throttledReleasedVersion() : void 0;
|
|
24630
|
+
let notebook;
|
|
24631
|
+
const notebookOnce = () => notebook ??= fetchNotebook();
|
|
24632
|
+
const docsJanitorArmedAt = async (repo) => {
|
|
24633
|
+
const wanted = docsJanitorScheduleId(repo);
|
|
24634
|
+
const { entries } = await notebookOnce();
|
|
24635
|
+
return entries.find((e) => e.scheduleId === wanted)?.armedAt;
|
|
24636
|
+
};
|
|
24098
24637
|
return {
|
|
24099
24638
|
githubLogin,
|
|
24100
24639
|
ghInstalled,
|
|
@@ -24103,12 +24642,17 @@ function mmiDoctorDeps() {
|
|
|
24103
24642
|
isOrgRepo: () => isOrgRepoRoot(),
|
|
24104
24643
|
installedPluginVersion: installedClaudePluginVersion,
|
|
24105
24644
|
pluginGuardState: claudePluginGuardState,
|
|
24106
|
-
releasedVersion: fetchNpmReleasedVersion,
|
|
24645
|
+
releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
|
|
24646
|
+
releasedVersionNote: throttled ? throttled.note : void 0,
|
|
24107
24647
|
// #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
|
|
24108
|
-
|
|
24648
|
+
// #3489: serialised. Both heals mutate machine-global state (the npm global prefix; the Claude
|
|
24649
|
+
// marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
|
|
24650
|
+
// rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
|
|
24651
|
+
// carries a CLI shim, so a concurrent npm global install races the same PATH surface.
|
|
24652
|
+
updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
|
|
24109
24653
|
// #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
|
|
24110
24654
|
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
24111
|
-
healPlugin: () => healClaudePluginForDoctor(),
|
|
24655
|
+
healPlugin: () => withEnvHealLock("Claude plugin reinstall", () => healClaudePluginForDoctor()),
|
|
24112
24656
|
currentCliVersion: resolveClientVersion,
|
|
24113
24657
|
readGitignore,
|
|
24114
24658
|
writeGitignore,
|
|
@@ -24144,9 +24688,9 @@ function mmiDoctorDeps() {
|
|
|
24144
24688
|
},
|
|
24145
24689
|
pluginCache: () => {
|
|
24146
24690
|
const plan = buildPluginCachePlan(
|
|
24147
|
-
(0,
|
|
24691
|
+
(0, import_node_os9.homedir)(),
|
|
24148
24692
|
runningPluginVersion(process.env, resolveClientVersion()),
|
|
24149
|
-
pluginCacheFsDeps((0,
|
|
24693
|
+
pluginCacheFsDeps((0, import_node_os9.homedir)(), () => 0)
|
|
24150
24694
|
);
|
|
24151
24695
|
return {
|
|
24152
24696
|
stale: plan.prune,
|
|
@@ -24156,16 +24700,45 @@ function mmiDoctorDeps() {
|
|
|
24156
24700
|
stagingBytes: plan.stagingBytes
|
|
24157
24701
|
};
|
|
24158
24702
|
},
|
|
24703
|
+
// #3485 wave 2 item 11: the --apply reap, through the SAME plan + reaper `plugin prune --apply` drives.
|
|
24704
|
+
// The plan's own keep-policy refuses the running and newest versions, so this cannot delete what is
|
|
24705
|
+
// loaded. Under the #3489 env-heal lock: deleting cache dirs while another doctor is mid-reinstall is
|
|
24706
|
+
// the same machine-global hazard the lock exists for.
|
|
24707
|
+
prunePluginCache: () => withEnvHealLock("plugin cache prune", async () => {
|
|
24708
|
+
const plan = buildPluginCachePlan(
|
|
24709
|
+
(0, import_node_os9.homedir)(),
|
|
24710
|
+
runningPluginVersion(process.env, resolveClientVersion()),
|
|
24711
|
+
pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
|
|
24712
|
+
{ withBytes: true }
|
|
24713
|
+
);
|
|
24714
|
+
const result = applyPluginCachePlan(
|
|
24715
|
+
plan,
|
|
24716
|
+
(p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }),
|
|
24717
|
+
stagingApplyFsGuard((0, import_node_os9.homedir)())
|
|
24718
|
+
);
|
|
24719
|
+
const removed = [...result.removed, ...result.removedStaging ?? []];
|
|
24720
|
+
if (result.failed.length) {
|
|
24721
|
+
return {
|
|
24722
|
+
ok: false,
|
|
24723
|
+
removed,
|
|
24724
|
+
detail: `${removed.length} removed, ${result.failed.length} failed \u2014 ${result.failed.map((f) => `${f.version}: ${f.error}`).join("; ")}`
|
|
24725
|
+
};
|
|
24726
|
+
}
|
|
24727
|
+
return { ok: true, removed, detail: removed.length ? `removed ${removed.join(", ")}` : "nothing to prune" };
|
|
24728
|
+
}),
|
|
24159
24729
|
// #3008: the schedules-notebook drift probe, compacted for the check. Full doctor only — the gather in
|
|
24160
24730
|
// runDoctorClean skips this dep entirely on fast/banner/preflight runs.
|
|
24161
24731
|
schedulesNotebook: async () => {
|
|
24162
|
-
const { entries, incomplete, drift } = await
|
|
24732
|
+
const { entries, incomplete, drift, reconciliation } = await notebookOnce();
|
|
24733
|
+
const repoName = await repoSlug().catch(() => "");
|
|
24734
|
+
const clearableHere = repoName ? reconciliation.filter((d) => driftClearableFrom(d, repoName)).length : 0;
|
|
24163
24735
|
return {
|
|
24164
24736
|
armed: entries.length,
|
|
24165
24737
|
live: entries.filter((e) => e.resolved === "live").length,
|
|
24166
24738
|
declared: entries.filter((e) => e.resolved === "declared").length,
|
|
24167
24739
|
incomplete,
|
|
24168
|
-
drift
|
|
24740
|
+
drift,
|
|
24741
|
+
clearableHere
|
|
24169
24742
|
};
|
|
24170
24743
|
},
|
|
24171
24744
|
// #3075: the docs-audit dead-man verdict probe, compacted for the check. Full doctor only, exactly like
|
|
@@ -24174,7 +24747,8 @@ function mmiDoctorDeps() {
|
|
|
24174
24747
|
docsAudit: async () => {
|
|
24175
24748
|
const repo = await currentRepoFullName();
|
|
24176
24749
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
24177
|
-
const
|
|
24750
|
+
const armedAt = await docsJanitorArmedAt(repo).catch(() => void 0);
|
|
24751
|
+
const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today, armedAt });
|
|
24178
24752
|
return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
|
|
24179
24753
|
},
|
|
24180
24754
|
// #3471: the competing-worktrees-roots probe. Full doctor only (the gather skips it on
|
|
@@ -24183,7 +24757,21 @@ function mmiDoctorDeps() {
|
|
|
24183
24757
|
worktreeRoots: async () => worktreeRootsProbe(await repoRoot()),
|
|
24184
24758
|
// #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
|
|
24185
24759
|
// A local record read — cheap enough for every lane, including the banner.
|
|
24186
|
-
sessionPayload: () => readSessionPayload(process.cwd())
|
|
24760
|
+
sessionPayload: () => readSessionPayload(process.cwd()),
|
|
24761
|
+
// #3485 items 9 and 8: why the MMI plugin has never printed an "updated — please restart" notice, and
|
|
24762
|
+
// which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
|
|
24763
|
+
marketplaceRows: () => {
|
|
24764
|
+
try {
|
|
24765
|
+
const home = (0, import_node_os9.homedir)();
|
|
24766
|
+
return marketplaceRows(
|
|
24767
|
+
MMI_MARKETPLACE_NAME,
|
|
24768
|
+
readFileSyncSafe((0, import_node_path30.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
|
|
24769
|
+
readFileSyncSafe((0, import_node_path30.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
|
|
24770
|
+
);
|
|
24771
|
+
} catch {
|
|
24772
|
+
return [];
|
|
24773
|
+
}
|
|
24774
|
+
}
|
|
24187
24775
|
};
|
|
24188
24776
|
}
|
|
24189
24777
|
async function requireFreshTrainCli(commandName) {
|
|
@@ -24308,19 +24896,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
24308
24896
|
});
|
|
24309
24897
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
24310
24898
|
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) => {
|
|
24311
|
-
const path2 = (0,
|
|
24312
|
-
const current = (0,
|
|
24899
|
+
const path2 = (0, import_node_path30.join)(process.cwd(), ".gitignore");
|
|
24900
|
+
const current = (0, import_node_fs32.existsSync)(path2) ? (0, import_node_fs32.readFileSync)(path2, "utf8") : null;
|
|
24313
24901
|
const plan = planManagedGitignore(current);
|
|
24314
24902
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
24315
24903
|
if (opts.json) {
|
|
24316
|
-
if (opts.write && plan.changed) (0,
|
|
24904
|
+
if (opts.write && plan.changed) (0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
|
|
24317
24905
|
console.log(JSON.stringify(plan, null, 2));
|
|
24318
24906
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
24319
24907
|
return;
|
|
24320
24908
|
}
|
|
24321
24909
|
if (opts.write) {
|
|
24322
24910
|
if (plan.changed) {
|
|
24323
|
-
(0,
|
|
24911
|
+
(0, import_node_fs32.writeFileSync)(path2, plan.content, "utf8");
|
|
24324
24912
|
console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
|
|
24325
24913
|
} else {
|
|
24326
24914
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
@@ -24446,8 +25034,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
24446
25034
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
24447
25035
|
let root;
|
|
24448
25036
|
if (o.root !== void 0) {
|
|
24449
|
-
root = (0,
|
|
24450
|
-
if (!(0,
|
|
25037
|
+
root = (0, import_node_path30.resolve)(o.root);
|
|
25038
|
+
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`);
|
|
24451
25039
|
const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
24452
25040
|
if (isPathUnderDirectory(gcRepoRoot, root)) {
|
|
24453
25041
|
return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
|
|
@@ -24521,26 +25109,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
24521
25109
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
24522
25110
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
24523
25111
|
const take = () => {
|
|
24524
|
-
const fd = (0,
|
|
25112
|
+
const fd = (0, import_node_fs32.openSync)(lockPath, "wx");
|
|
24525
25113
|
try {
|
|
24526
|
-
(0,
|
|
25114
|
+
(0, import_node_fs32.writeSync)(fd, String(Date.now()));
|
|
24527
25115
|
} finally {
|
|
24528
|
-
(0,
|
|
25116
|
+
(0, import_node_fs32.closeSync)(fd);
|
|
24529
25117
|
}
|
|
24530
25118
|
return () => {
|
|
24531
25119
|
try {
|
|
24532
|
-
(0,
|
|
25120
|
+
(0, import_node_fs32.rmSync)(lockPath, { force: true });
|
|
24533
25121
|
} catch {
|
|
24534
25122
|
}
|
|
24535
25123
|
};
|
|
24536
25124
|
};
|
|
24537
25125
|
try {
|
|
24538
|
-
(0,
|
|
25126
|
+
(0, import_node_fs32.mkdirSync)((0, import_node_path30.dirname)(lockPath), { recursive: true });
|
|
24539
25127
|
return take();
|
|
24540
25128
|
} catch {
|
|
24541
25129
|
try {
|
|
24542
|
-
if (Date.now() - (0,
|
|
24543
|
-
(0,
|
|
25130
|
+
if (Date.now() - (0, import_node_fs32.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
25131
|
+
(0, import_node_fs32.rmSync)(lockPath, { force: true });
|
|
24544
25132
|
return take();
|
|
24545
25133
|
}
|
|
24546
25134
|
} catch {
|
|
@@ -24982,7 +25570,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
24982
25570
|
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`);
|
|
24983
25571
|
if (o.secretsFile) {
|
|
24984
25572
|
try {
|
|
24985
|
-
vars.push(`secrets=${(0,
|
|
25573
|
+
vars.push(`secrets=${(0, import_node_fs32.readFileSync)(o.secretsFile, "utf8")}`);
|
|
24986
25574
|
} catch (e) {
|
|
24987
25575
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
24988
25576
|
}
|
|
@@ -25098,8 +25686,8 @@ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY ro
|
|
|
25098
25686
|
const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
|
|
25099
25687
|
if (branch) {
|
|
25100
25688
|
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
|
|
25101
|
-
const
|
|
25102
|
-
if (
|
|
25689
|
+
const sameRepo2 = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
|
|
25690
|
+
if (sameRepo2) {
|
|
25103
25691
|
const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
|
|
25104
25692
|
if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
|
|
25105
25693
|
${filelessTransitionGuide(target, o.stage)}`);
|
|
@@ -25296,8 +25884,8 @@ withExamples(mutating(
|
|
|
25296
25884
|
let targetRepo2;
|
|
25297
25885
|
try {
|
|
25298
25886
|
issueType = resolveCreateType(o.type, "issue create", o.label);
|
|
25299
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
25300
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
25887
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25888
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25301
25889
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
25302
25890
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
25303
25891
|
extraLabels = [...o.label ?? []];
|
|
@@ -25440,7 +26028,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
|
|
|
25440
26028
|
}
|
|
25441
26029
|
let body;
|
|
25442
26030
|
try {
|
|
25443
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
26031
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25444
26032
|
} catch (e) {
|
|
25445
26033
|
return fail(`issue comment: ${e.message}`);
|
|
25446
26034
|
}
|
|
@@ -25500,8 +26088,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
25500
26088
|
let title;
|
|
25501
26089
|
const sourceRepo = o.repo ?? await resolveRepo(void 0);
|
|
25502
26090
|
try {
|
|
25503
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
25504
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
26091
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
26092
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25505
26093
|
priority = resolveCreatePriority(o.priority, "report");
|
|
25506
26094
|
if (!ISSUE_TYPES.includes(o.type)) {
|
|
25507
26095
|
throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
@@ -25550,8 +26138,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
25550
26138
|
let args;
|
|
25551
26139
|
try {
|
|
25552
26140
|
skill = assertSkillName(o.skill);
|
|
25553
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
25554
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
26141
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
26142
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25555
26143
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
25556
26144
|
priority = resolveCreatePriority(o.priority, "skill-lesson");
|
|
25557
26145
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -25602,8 +26190,8 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
25602
26190
|
let body;
|
|
25603
26191
|
let title;
|
|
25604
26192
|
try {
|
|
25605
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
25606
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
26193
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
26194
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
25607
26195
|
} catch (e) {
|
|
25608
26196
|
return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
25609
26197
|
}
|
|
@@ -25638,11 +26226,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
25638
26226
|
}
|
|
25639
26227
|
});
|
|
25640
26228
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
25641
|
-
const wfDir = (0,
|
|
25642
|
-
if (!(0,
|
|
25643
|
-
return (0,
|
|
26229
|
+
const wfDir = (0, import_node_path30.join)(cwd, ".github", "workflows");
|
|
26230
|
+
if (!(0, import_node_fs32.existsSync)(wfDir)) return [];
|
|
26231
|
+
return (0, import_node_fs32.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
25644
26232
|
try {
|
|
25645
|
-
return workflowReportsPrChecks((0,
|
|
26233
|
+
return workflowReportsPrChecks((0, import_node_fs32.readFileSync)((0, import_node_path30.join)(wfDir, name), "utf8"));
|
|
25646
26234
|
} catch {
|
|
25647
26235
|
return true;
|
|
25648
26236
|
}
|
|
@@ -25669,16 +26257,16 @@ function ciAuditDeps() {
|
|
|
25669
26257
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
25670
26258
|
readSeedFile: (path2) => {
|
|
25671
26259
|
if (!root) return null;
|
|
25672
|
-
const fullPath = (0,
|
|
25673
|
-
return (0,
|
|
26260
|
+
const fullPath = (0, import_node_path30.join)(root, path2);
|
|
26261
|
+
return (0, import_node_fs32.existsSync)(fullPath) ? (0, import_node_fs32.readFileSync)(fullPath, "utf8") : null;
|
|
25674
26262
|
}
|
|
25675
26263
|
};
|
|
25676
26264
|
}
|
|
25677
26265
|
function hubRoot() {
|
|
25678
|
-
const fromPkg = (0,
|
|
26266
|
+
const fromPkg = (0, import_node_path30.join)(__dirname, "..", "..");
|
|
25679
26267
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
25680
|
-
if ((0,
|
|
25681
|
-
if ((0,
|
|
26268
|
+
if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(fromPkg, marker))) return fromPkg;
|
|
26269
|
+
if ((0, import_node_fs32.existsSync)((0, import_node_path30.join)(process.cwd(), marker))) return process.cwd();
|
|
25682
26270
|
return null;
|
|
25683
26271
|
}
|
|
25684
26272
|
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) => {
|
|
@@ -25928,7 +26516,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
25928
26516
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
25929
26517
|
beforeWorktrees,
|
|
25930
26518
|
startingPath,
|
|
25931
|
-
pathExists: (p) => (0,
|
|
26519
|
+
pathExists: (p) => (0, import_node_fs32.existsSync)(p),
|
|
25932
26520
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
25933
26521
|
teardownWorktreeStage,
|
|
25934
26522
|
deferredStore,
|
|
@@ -26038,8 +26626,8 @@ function trainApplyDeps() {
|
|
|
26038
26626
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
26039
26627
|
announce: (args) => announceRelease({
|
|
26040
26628
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
26041
|
-
readFile: (path2) => (0,
|
|
26042
|
-
removeFile: (path2) => (0,
|
|
26629
|
+
readFile: (path2) => (0, import_promises8.readFile)(path2, "utf8"),
|
|
26630
|
+
removeFile: (path2) => (0, import_promises8.unlink)(path2)
|
|
26043
26631
|
}, args),
|
|
26044
26632
|
fetchEdgeDomains: async (slug) => {
|
|
26045
26633
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -26269,10 +26857,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
26269
26857
|
targets = resolution.targets;
|
|
26270
26858
|
}
|
|
26271
26859
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
26272
|
-
const fileMatrix = (0,
|
|
26860
|
+
const fileMatrix = (0, import_node_fs32.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs32.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
26273
26861
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
26274
26862
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
26275
|
-
const fileContracts = (0,
|
|
26863
|
+
const fileContracts = (0, import_node_fs32.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs32.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
26276
26864
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
26277
26865
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
26278
26866
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -26280,15 +26868,15 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
26280
26868
|
});
|
|
26281
26869
|
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)));
|
|
26282
26870
|
var isWin2 = process.platform === "win32";
|
|
26283
|
-
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("--
|
|
26871
|
+
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) => {
|
|
26284
26872
|
if (opts.guide) {
|
|
26285
26873
|
consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
|
|
26286
26874
|
return;
|
|
26287
26875
|
}
|
|
26288
|
-
const noRepoWrites = opts.repoWrites === false;
|
|
26289
26876
|
process.exitCode = await runDoctorClean(
|
|
26290
26877
|
{
|
|
26291
|
-
apply: Boolean(opts.apply)
|
|
26878
|
+
apply: Boolean(opts.apply),
|
|
26879
|
+
repoWrites: opts.repoWrites,
|
|
26292
26880
|
banner: opts.banner,
|
|
26293
26881
|
preflight: opts.preflight,
|
|
26294
26882
|
fast: opts.fast || opts.self,
|
|
@@ -26306,16 +26894,16 @@ function directoryBytes(path2) {
|
|
|
26306
26894
|
let total = 0;
|
|
26307
26895
|
let entries;
|
|
26308
26896
|
try {
|
|
26309
|
-
entries = (0,
|
|
26897
|
+
entries = (0, import_node_fs32.readdirSync)(path2, { withFileTypes: true });
|
|
26310
26898
|
} catch {
|
|
26311
26899
|
return 0;
|
|
26312
26900
|
}
|
|
26313
26901
|
for (const entry of entries) {
|
|
26314
|
-
const child2 = (0,
|
|
26902
|
+
const child2 = (0, import_node_path30.join)(path2, entry.name);
|
|
26315
26903
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
26316
26904
|
else {
|
|
26317
26905
|
try {
|
|
26318
|
-
total += (0,
|
|
26906
|
+
total += (0, import_node_fs32.statSync)(child2).size;
|
|
26319
26907
|
} catch {
|
|
26320
26908
|
}
|
|
26321
26909
|
}
|
|
@@ -26323,25 +26911,25 @@ function directoryBytes(path2) {
|
|
|
26323
26911
|
return total;
|
|
26324
26912
|
}
|
|
26325
26913
|
function listDirEntries(dir) {
|
|
26326
|
-
return (0,
|
|
26914
|
+
return (0, import_node_fs32.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
26327
26915
|
}
|
|
26328
26916
|
function readInstalledPluginRefs(home) {
|
|
26329
26917
|
const p = installedPluginsPath(home);
|
|
26330
|
-
if (!(0,
|
|
26918
|
+
if (!(0, import_node_fs32.existsSync)(p)) return [];
|
|
26331
26919
|
try {
|
|
26332
|
-
return installedPluginPaths((0,
|
|
26920
|
+
return installedPluginPaths((0, import_node_fs32.readFileSync)(p, "utf8"));
|
|
26333
26921
|
} catch {
|
|
26334
26922
|
return null;
|
|
26335
26923
|
}
|
|
26336
26924
|
}
|
|
26337
26925
|
function pluginCacheFsDeps(home, dirBytes) {
|
|
26338
26926
|
return {
|
|
26339
|
-
exists: (p) => (0,
|
|
26340
|
-
listVersionDirs: (root) => (0,
|
|
26927
|
+
exists: (p) => (0, import_node_fs32.existsSync)(p),
|
|
26928
|
+
listVersionDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
26341
26929
|
dirBytes,
|
|
26342
|
-
listStagingDirs: (root) => (0,
|
|
26930
|
+
listStagingDirs: (root) => (0, import_node_fs32.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
26343
26931
|
try {
|
|
26344
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
26932
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path30.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs32.statSync)(p).mtimeMs) };
|
|
26345
26933
|
} catch {
|
|
26346
26934
|
return { name: d.name, mtimeMs: Date.now() };
|
|
26347
26935
|
}
|
|
@@ -26355,10 +26943,10 @@ function stagingApplyFsGuard(home) {
|
|
|
26355
26943
|
return {
|
|
26356
26944
|
referencedPaths: () => readInstalledPluginRefs(home),
|
|
26357
26945
|
mtimeMs: (name) => {
|
|
26358
|
-
const p = (0,
|
|
26359
|
-
if (!(0,
|
|
26946
|
+
const p = (0, import_node_path30.join)(stagingRoot, name);
|
|
26947
|
+
if (!(0, import_node_fs32.existsSync)(p)) return null;
|
|
26360
26948
|
try {
|
|
26361
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
26949
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs32.statSync)(q).mtimeMs);
|
|
26362
26950
|
} catch {
|
|
26363
26951
|
return null;
|
|
26364
26952
|
}
|
|
@@ -26368,13 +26956,13 @@ function stagingApplyFsGuard(home) {
|
|
|
26368
26956
|
}
|
|
26369
26957
|
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) => {
|
|
26370
26958
|
const plan = buildPluginCachePlan(
|
|
26371
|
-
(0,
|
|
26959
|
+
(0, import_node_os9.homedir)(),
|
|
26372
26960
|
runningPluginVersion(process.env, resolveClientVersion()),
|
|
26373
|
-
pluginCacheFsDeps((0,
|
|
26961
|
+
pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
|
|
26374
26962
|
{ withBytes: true }
|
|
26375
26963
|
);
|
|
26376
26964
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
26377
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
26965
|
+
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;
|
|
26378
26966
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
26379
26967
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
26380
26968
|
else console.log(renderPluginCachePlan(plan, result));
|
|
@@ -26403,6 +26991,8 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
26403
26991
|
});
|
|
26404
26992
|
const line = whoamiLine(report, report.login ? ghMultiAccountCaveat(report.login) : void 0);
|
|
26405
26993
|
if (line) io.log(line);
|
|
26994
|
+
const authority = authorityLine(report);
|
|
26995
|
+
if (authority) io.log(authority);
|
|
26406
26996
|
},
|
|
26407
26997
|
boardSlice: (io) => runBoardSlice(io, {
|
|
26408
26998
|
loadConfig: () => loadConfigForRepo(),
|
|
@@ -26432,8 +27022,10 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
26432
27022
|
const line = localTrainSyncBannerLine(result);
|
|
26433
27023
|
if (line) io.log(line);
|
|
26434
27024
|
},
|
|
27025
|
+
// #3485 item 7: the ONLY lane that throttles the npm read — it runs on every session start, on a
|
|
27026
|
+
// blocking hook. Every interactive lane still reads live.
|
|
26435
27027
|
doctor: async (io) => {
|
|
26436
|
-
await runDoctorClean({ banner: true }, io, mmiDoctorDeps());
|
|
27028
|
+
await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true }));
|
|
26437
27029
|
}
|
|
26438
27030
|
});
|
|
26439
27031
|
await runSessionStart(parallel, sequential, bannerIo);
|
|
@@ -26456,6 +27048,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
|
|
|
26456
27048
|
DEFAULT_PRIORITY,
|
|
26457
27049
|
awsCallerArn,
|
|
26458
27050
|
classifyParseError,
|
|
27051
|
+
envHealLockPath,
|
|
26459
27052
|
gcPlan,
|
|
26460
27053
|
isOrgRegisteredRepo,
|
|
26461
27054
|
registryClientDeps,
|