@mutmutco/cli 3.9.0 → 3.11.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/index.cjs +0 -1
- package/dist/main.cjs +1309 -466
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3408,7 +3408,7 @@ var program = new Command();
|
|
|
3408
3408
|
|
|
3409
3409
|
// src/index.ts
|
|
3410
3410
|
var import_promises3 = require("node:fs/promises");
|
|
3411
|
-
var
|
|
3411
|
+
var import_node_fs21 = require("node:fs");
|
|
3412
3412
|
|
|
3413
3413
|
// src/bootstrap-org-ruleset.ts
|
|
3414
3414
|
var ORG_NO_AGENT_FILES_RULESET_NAME = "mmi-no-agent-files-org";
|
|
@@ -3540,7 +3540,6 @@ function resolveClientVersionManifestCandidates(distDir = __dirname) {
|
|
|
3540
3540
|
return [
|
|
3541
3541
|
(0, import_node_path2.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
|
|
3542
3542
|
(0, import_node_path2.join)(distDir, "..", "..", ".cursor-plugin", "plugin.json"),
|
|
3543
|
-
(0, import_node_path2.join)(distDir, "..", "..", ".codex-plugin", "plugin.json"),
|
|
3544
3543
|
(0, import_node_path2.join)(distDir, "..", "package.json")
|
|
3545
3544
|
];
|
|
3546
3545
|
}
|
|
@@ -4521,10 +4520,18 @@ function buildSessionStartPlan(verbs) {
|
|
|
4521
4520
|
// Identity reads are memoized process-wide, so this adds no extra gh/Hub round-trip.
|
|
4522
4521
|
{ name: "whoami", run: verbs.whoami },
|
|
4523
4522
|
// board slice (#1230): assigned + claimable glance, 3s cap, fail-soft — flushed after whoami.
|
|
4524
|
-
{ name: "board slice", run: verbs.boardSlice }
|
|
4523
|
+
{ name: "board slice", run: verbs.boardSlice },
|
|
4524
|
+
// command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
|
|
4525
|
+
// not after a denied gh call. <1KB, fail-soft — never blocks the banner.
|
|
4526
|
+
{ name: "command ladder", run: verbs.commandLadderHint }
|
|
4525
4527
|
],
|
|
4526
4528
|
sequential: [
|
|
4527
|
-
//
|
|
4529
|
+
// local train sync (#2582): fast-forward local development/main/rc to origin BEFORE doctor + before
|
|
4530
|
+
// the agent works, so a checkout never lags behind a release someone pushed to origin (and the
|
|
4531
|
+
// deferred main -> development alignment merge lands locally). Sequential + fail-soft: it runs one
|
|
4532
|
+
// bounded `git fetch` + ff, silent unless a branch actually moved, and never blocks the banner.
|
|
4533
|
+
{ name: "train sync", run: verbs.syncTrain },
|
|
4534
|
+
// Doctor runs last so it sees the final (synced) tree.
|
|
4528
4535
|
{ name: "doctor", run: verbs.doctor }
|
|
4529
4536
|
]
|
|
4530
4537
|
};
|
|
@@ -4566,6 +4573,103 @@ function scratchGcLines(cwd, env = process.env, now = Date.now()) {
|
|
|
4566
4573
|
}
|
|
4567
4574
|
}
|
|
4568
4575
|
|
|
4576
|
+
// src/git-clean-tree.ts
|
|
4577
|
+
function isAgentScratchPath(path2) {
|
|
4578
|
+
const normalized = path2.replace(/\\/g, "/").trim();
|
|
4579
|
+
return /^tmp_[^/]+$/.test(normalized);
|
|
4580
|
+
}
|
|
4581
|
+
function porcelainHasBlockingChanges(porcelain) {
|
|
4582
|
+
return porcelain.split("\n").some((line) => {
|
|
4583
|
+
const trimmed = line.trim();
|
|
4584
|
+
if (!trimmed) return false;
|
|
4585
|
+
const path2 = trimmed.slice(3).split(" -> ")[0]?.trim() ?? "";
|
|
4586
|
+
return path2 !== "" && !isAgentScratchPath(path2);
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
// src/local-train-sync.ts
|
|
4591
|
+
var TRAIN_BRANCHES = ["development", "main", "rc"];
|
|
4592
|
+
function clean(out) {
|
|
4593
|
+
return out.trim();
|
|
4594
|
+
}
|
|
4595
|
+
async function revParse(run, ref) {
|
|
4596
|
+
try {
|
|
4597
|
+
return clean(await run("git", ["rev-parse", "--verify", "--quiet", ref]));
|
|
4598
|
+
} catch {
|
|
4599
|
+
return "";
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
async function isFastForward(run, ancestor, descendant) {
|
|
4603
|
+
try {
|
|
4604
|
+
await run("git", ["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
4605
|
+
return true;
|
|
4606
|
+
} catch {
|
|
4607
|
+
return false;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
async function syncOneBranch(run, branch, currentBranch2, warn) {
|
|
4611
|
+
const localSha = await revParse(run, `refs/heads/${branch}`);
|
|
4612
|
+
if (!localSha) {
|
|
4613
|
+
return { branch, status: "skipped", reason: "no-local-branch", note: `${branch}: no local branch \u2014 skipped` };
|
|
4614
|
+
}
|
|
4615
|
+
const remoteSha = await revParse(run, `refs/remotes/origin/${branch}`);
|
|
4616
|
+
if (!remoteSha) {
|
|
4617
|
+
return { branch, status: "skipped", reason: "no-remote-branch", note: `${branch}: no origin/${branch} \u2014 skipped` };
|
|
4618
|
+
}
|
|
4619
|
+
if (localSha === remoteSha) {
|
|
4620
|
+
return { branch, status: "already-current", note: `${branch}: already at origin/${branch}` };
|
|
4621
|
+
}
|
|
4622
|
+
if (!await isFastForward(run, localSha, remoteSha)) {
|
|
4623
|
+
const note = `${branch}: local diverged from origin/${branch} \u2014 left untouched (never force)`;
|
|
4624
|
+
warn?.(note);
|
|
4625
|
+
return { branch, status: "skipped", reason: "diverged", note };
|
|
4626
|
+
}
|
|
4627
|
+
try {
|
|
4628
|
+
if (branch === currentBranch2) {
|
|
4629
|
+
const porcelain = await run("git", ["status", "--porcelain"]);
|
|
4630
|
+
if (porcelainHasBlockingChanges(porcelain)) {
|
|
4631
|
+
return { branch, status: "skipped", reason: "current-dirty", note: `${branch}: checked out with local changes \u2014 skipped (commit/stash, then rerun)` };
|
|
4632
|
+
}
|
|
4633
|
+
await run("git", ["merge", "--ff-only", `origin/${branch}`]);
|
|
4634
|
+
} else {
|
|
4635
|
+
await run("git", ["fetch", "origin", `${branch}:${branch}`]);
|
|
4636
|
+
}
|
|
4637
|
+
return { branch, status: "synced", note: `${branch}: fast-forwarded to origin/${branch}` };
|
|
4638
|
+
} catch (e) {
|
|
4639
|
+
const note = `${branch}: fast-forward failed \u2014 ${e instanceof Error ? e.message : String(e)}`;
|
|
4640
|
+
warn?.(note);
|
|
4641
|
+
return { branch, status: "skipped", reason: "ff-failed", note };
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
async function syncLocalTrainBranches(run, options = {}) {
|
|
4645
|
+
const branches = options.branches ?? TRAIN_BRANCHES;
|
|
4646
|
+
const doFetch = options.fetch ?? true;
|
|
4647
|
+
const fetchRun = options.fetchRun ?? ((args) => run("git", args));
|
|
4648
|
+
if (doFetch) {
|
|
4649
|
+
try {
|
|
4650
|
+
await fetchRun(["fetch", "origin"]);
|
|
4651
|
+
} catch (e) {
|
|
4652
|
+
options.warn?.(`local-train-sync: git fetch origin failed \u2014 syncing against cached refs (${e instanceof Error ? e.message : String(e)})`);
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
let currentBranch2 = "";
|
|
4656
|
+
try {
|
|
4657
|
+
currentBranch2 = clean(await run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
4658
|
+
} catch {
|
|
4659
|
+
currentBranch2 = "";
|
|
4660
|
+
}
|
|
4661
|
+
const results = [];
|
|
4662
|
+
for (const branch of branches) {
|
|
4663
|
+
results.push(await syncOneBranch(run, branch, currentBranch2, options.warn));
|
|
4664
|
+
}
|
|
4665
|
+
return { branches: results, changed: results.some((r) => r.status === "synced") };
|
|
4666
|
+
}
|
|
4667
|
+
function localTrainSyncBannerLine(result) {
|
|
4668
|
+
const moved = result.branches.filter((b) => b.status === "synced").map((b) => b.branch);
|
|
4669
|
+
if (moved.length === 0) return "";
|
|
4670
|
+
return `synced local ${moved.join(", ")} \u2192 origin`;
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4569
4673
|
// src/board.ts
|
|
4570
4674
|
var import_node_child_process5 = require("node:child_process");
|
|
4571
4675
|
var import_node_util5 = require("node:util");
|
|
@@ -5736,9 +5840,31 @@ async function refreshBoardSliceCache(deps) {
|
|
|
5736
5840
|
}
|
|
5737
5841
|
}
|
|
5738
5842
|
|
|
5739
|
-
// src/
|
|
5843
|
+
// src/hook-activity.ts
|
|
5740
5844
|
var import_node_fs9 = require("node:fs");
|
|
5741
5845
|
var import_node_path9 = require("node:path");
|
|
5846
|
+
var DEFAULT_SURFACE = "claude";
|
|
5847
|
+
function activityLogPath(cwd) {
|
|
5848
|
+
return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
|
|
5849
|
+
}
|
|
5850
|
+
function appendHookActivity(cwd, entry) {
|
|
5851
|
+
try {
|
|
5852
|
+
const path2 = activityLogPath(cwd);
|
|
5853
|
+
const line = {
|
|
5854
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5855
|
+
surface: DEFAULT_SURFACE,
|
|
5856
|
+
...entry
|
|
5857
|
+
};
|
|
5858
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path9.dirname)(path2), { recursive: true });
|
|
5859
|
+
(0, import_node_fs9.appendFileSync)(path2, `${JSON.stringify(line)}
|
|
5860
|
+
`, "utf8");
|
|
5861
|
+
} catch {
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
// src/worktree.ts
|
|
5866
|
+
var import_node_fs10 = require("node:fs");
|
|
5867
|
+
var import_node_path10 = require("node:path");
|
|
5742
5868
|
var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
5743
5869
|
var PKG = "package.json";
|
|
5744
5870
|
var LOCKFILE = "package-lock.json";
|
|
@@ -5746,21 +5872,21 @@ var NODE_MODULES = "node_modules";
|
|
|
5746
5872
|
var realFsProbe = {
|
|
5747
5873
|
isDir: (p) => {
|
|
5748
5874
|
try {
|
|
5749
|
-
return (0,
|
|
5875
|
+
return (0, import_node_fs10.statSync)(p).isDirectory();
|
|
5750
5876
|
} catch {
|
|
5751
5877
|
return false;
|
|
5752
5878
|
}
|
|
5753
5879
|
},
|
|
5754
5880
|
isFile: (p) => {
|
|
5755
5881
|
try {
|
|
5756
|
-
return (0,
|
|
5882
|
+
return (0, import_node_fs10.statSync)(p).isFile();
|
|
5757
5883
|
} catch {
|
|
5758
5884
|
return false;
|
|
5759
5885
|
}
|
|
5760
5886
|
},
|
|
5761
5887
|
listDirs: (p) => {
|
|
5762
5888
|
try {
|
|
5763
|
-
return (0,
|
|
5889
|
+
return (0, import_node_fs10.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5764
5890
|
} catch {
|
|
5765
5891
|
return [];
|
|
5766
5892
|
}
|
|
@@ -5768,12 +5894,12 @@ var realFsProbe = {
|
|
|
5768
5894
|
};
|
|
5769
5895
|
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
5770
5896
|
const factsFor = (dir) => {
|
|
5771
|
-
const abs = dir ? (0,
|
|
5897
|
+
const abs = dir ? (0, import_node_path10.join)(root, dir) : root;
|
|
5772
5898
|
return {
|
|
5773
5899
|
dir,
|
|
5774
|
-
hasPackageJson: fs2.isFile((0,
|
|
5775
|
-
hasLockfile: fs2.isFile((0,
|
|
5776
|
-
hasNodeModules: fs2.isDir((0,
|
|
5900
|
+
hasPackageJson: fs2.isFile((0, import_node_path10.join)(abs, PKG)),
|
|
5901
|
+
hasLockfile: fs2.isFile((0, import_node_path10.join)(abs, LOCKFILE)),
|
|
5902
|
+
hasNodeModules: fs2.isDir((0, import_node_path10.join)(abs, NODE_MODULES))
|
|
5777
5903
|
};
|
|
5778
5904
|
};
|
|
5779
5905
|
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
@@ -5783,7 +5909,7 @@ function npmInstallTargets(dirs) {
|
|
|
5783
5909
|
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
5784
5910
|
}
|
|
5785
5911
|
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
5786
|
-
return fs2.isFile((0,
|
|
5912
|
+
return fs2.isFile((0, import_node_path10.join)(root, ".git"));
|
|
5787
5913
|
}
|
|
5788
5914
|
function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
5789
5915
|
if (!isLinkedWorktree(root, fs2)) return null;
|
|
@@ -5793,8 +5919,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
|
5793
5919
|
return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
|
|
5794
5920
|
}
|
|
5795
5921
|
function defaultCopyFile(from, to) {
|
|
5796
|
-
(0,
|
|
5797
|
-
(0,
|
|
5922
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path10.dirname)(to), { recursive: true });
|
|
5923
|
+
(0, import_node_fs10.copyFileSync)(from, to);
|
|
5798
5924
|
}
|
|
5799
5925
|
async function provisionWorktree(worktreeRoot, deps) {
|
|
5800
5926
|
const fs2 = deps.fs ?? realFsProbe;
|
|
@@ -5806,7 +5932,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
5806
5932
|
const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
|
|
5807
5933
|
const installed = [];
|
|
5808
5934
|
for (const target of targets) {
|
|
5809
|
-
const cwd = target.dir ? (0,
|
|
5935
|
+
const cwd = target.dir ? (0, import_node_path10.join)(worktreeRoot, target.dir) : worktreeRoot;
|
|
5810
5936
|
log(`installing deps: ${target.command} in ${target.dir || "."}`);
|
|
5811
5937
|
await deps.runInstall(target.command, cwd);
|
|
5812
5938
|
installed.push(target);
|
|
@@ -5815,7 +5941,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
5815
5941
|
const copySkipped = [];
|
|
5816
5942
|
const primary = await deps.primaryCheckout();
|
|
5817
5943
|
for (const rel of LOCAL_ONLY_FILES) {
|
|
5818
|
-
const dest = (0,
|
|
5944
|
+
const dest = (0, import_node_path10.join)(worktreeRoot, rel);
|
|
5819
5945
|
if (fs2.isFile(dest)) {
|
|
5820
5946
|
copySkipped.push({ file: rel, reason: "already-present" });
|
|
5821
5947
|
continue;
|
|
@@ -5824,11 +5950,11 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
5824
5950
|
copySkipped.push({ file: rel, reason: "no-primary" });
|
|
5825
5951
|
continue;
|
|
5826
5952
|
}
|
|
5827
|
-
if (!fs2.isFile((0,
|
|
5953
|
+
if (!fs2.isFile((0, import_node_path10.join)(primary, rel))) {
|
|
5828
5954
|
copySkipped.push({ file: rel, reason: "absent-in-primary" });
|
|
5829
5955
|
continue;
|
|
5830
5956
|
}
|
|
5831
|
-
copyFile((0,
|
|
5957
|
+
copyFile((0, import_node_path10.join)(primary, rel), dest);
|
|
5832
5958
|
copied.push(rel);
|
|
5833
5959
|
log(`copied local config: ${rel}`);
|
|
5834
5960
|
}
|
|
@@ -5836,13 +5962,50 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
5836
5962
|
}
|
|
5837
5963
|
function defaultWorktreePath(repoRoot, branch) {
|
|
5838
5964
|
const safe = branch.replace(/[/\\]+/g, "-");
|
|
5839
|
-
return (0,
|
|
5965
|
+
return (0, import_node_path10.join)((0, import_node_path10.dirname)(repoRoot), "mmi-worktrees", safe);
|
|
5840
5966
|
}
|
|
5841
5967
|
function resolveWorktreeBase(from, remote) {
|
|
5842
5968
|
const remotePrefix = `${remote}/`;
|
|
5843
5969
|
const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
|
|
5844
5970
|
return { base: from, fetchBranch };
|
|
5845
5971
|
}
|
|
5972
|
+
var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration/i;
|
|
5973
|
+
function isGitConfigLockError(error) {
|
|
5974
|
+
return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
|
|
5975
|
+
}
|
|
5976
|
+
var ADD_WORKTREE_BACKOFF_MS = [100, 250, 500, 750, 1e3];
|
|
5977
|
+
var ADD_WORKTREE_MAX_ATTEMPTS = 6;
|
|
5978
|
+
async function cleanupOrphanBranch(branch, base, preExistingOid, deps) {
|
|
5979
|
+
if (preExistingOid !== void 0) return;
|
|
5980
|
+
const currentOid = await deps.revParse(`refs/heads/${branch}`);
|
|
5981
|
+
if (currentOid === void 0) return;
|
|
5982
|
+
const baseOid = await deps.revParse(base);
|
|
5983
|
+
if (baseOid !== void 0 && currentOid !== baseOid) return;
|
|
5984
|
+
await deps.deleteBranch(branch);
|
|
5985
|
+
}
|
|
5986
|
+
async function addWorktreeRobust(wtPath, branch, base, deps) {
|
|
5987
|
+
const maxAttempts = deps.maxAttempts ?? ADD_WORKTREE_MAX_ATTEMPTS;
|
|
5988
|
+
const backoff = deps.backoffMs ?? ADD_WORKTREE_BACKOFF_MS;
|
|
5989
|
+
const log = deps.log ?? (() => {
|
|
5990
|
+
});
|
|
5991
|
+
const preExistingOid = await deps.revParse(`refs/heads/${branch}`);
|
|
5992
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
5993
|
+
try {
|
|
5994
|
+
await deps.git(["worktree", "add", wtPath, "-b", branch, base]);
|
|
5995
|
+
return;
|
|
5996
|
+
} catch (e) {
|
|
5997
|
+
await cleanupOrphanBranch(branch, base, preExistingOid, deps).catch(() => {
|
|
5998
|
+
});
|
|
5999
|
+
const retriesLeft = i < maxAttempts - 1;
|
|
6000
|
+
if (retriesLeft && isGitConfigLockError(e)) {
|
|
6001
|
+
log(`worktree add: .git/config lock busy, retrying (${i + 1}/${maxAttempts - 1})`);
|
|
6002
|
+
await deps.sleep(backoff[Math.min(i, backoff.length - 1)]);
|
|
6003
|
+
continue;
|
|
6004
|
+
}
|
|
6005
|
+
throw e;
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
5846
6009
|
|
|
5847
6010
|
// src/whoami.ts
|
|
5848
6011
|
async function resolveWhoami(deps) {
|
|
@@ -5867,8 +6030,39 @@ function whoamiLine(report) {
|
|
|
5867
6030
|
return `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
|
|
5868
6031
|
}
|
|
5869
6032
|
|
|
6033
|
+
// src/command-ladder-hint.ts
|
|
6034
|
+
var COMMAND_LADDER_WRITES = [
|
|
6035
|
+
{ gh: "gh issue create", replacement: "mmi-cli issue create" },
|
|
6036
|
+
{ gh: "gh issue comment", replacement: "mmi-cli issue comment" },
|
|
6037
|
+
{ gh: "gh pr create", replacement: "mmi-cli pr create" },
|
|
6038
|
+
{ gh: "gh pr merge", replacement: "mmi-cli pr merge (or mmi-cli pr land)" }
|
|
6039
|
+
];
|
|
6040
|
+
var COMMAND_LADDER_READS = [
|
|
6041
|
+
{ gh: "gh issue view <N>", replacement: "mmi-cli issue view <N>" },
|
|
6042
|
+
{ gh: "gh pr view <N>", replacement: "mmi-cli pr view <N>" },
|
|
6043
|
+
{ gh: "gh pr checks <N>", replacement: "mmi-cli pr checks-wait <N>" }
|
|
6044
|
+
];
|
|
6045
|
+
var COMMAND_LADDER_BOARD = [
|
|
6046
|
+
{ gh: "gh issue list / project", replacement: "mmi-cli board read" }
|
|
6047
|
+
];
|
|
6048
|
+
function commandLadderHint() {
|
|
6049
|
+
const pad = 28;
|
|
6050
|
+
const lines = [];
|
|
6051
|
+
lines.push("[mmi] Command ladder (#2609): reach for mmi-cli first");
|
|
6052
|
+
for (const e of COMMAND_LADDER_WRITES) {
|
|
6053
|
+
lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
|
|
6054
|
+
}
|
|
6055
|
+
for (const e of COMMAND_LADDER_READS) {
|
|
6056
|
+
lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
|
|
6057
|
+
}
|
|
6058
|
+
for (const e of COMMAND_LADDER_BOARD) {
|
|
6059
|
+
lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
|
|
6060
|
+
}
|
|
6061
|
+
return lines.join("\n");
|
|
6062
|
+
}
|
|
6063
|
+
|
|
5870
6064
|
// src/index.ts
|
|
5871
|
-
var
|
|
6065
|
+
var import_node_path21 = require("node:path");
|
|
5872
6066
|
|
|
5873
6067
|
// src/merge-ci-policy.ts
|
|
5874
6068
|
function resolveMergeCiPolicy(input) {
|
|
@@ -6148,6 +6342,9 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
6148
6342
|
// doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
|
|
6149
6343
|
// into every repo's .gitignore.
|
|
6150
6344
|
".aws-sam/",
|
|
6345
|
+
// jerv-cli lease markers — the ephemeral per-worktree lease ledger `jerv-cli lease open` writes into a
|
|
6346
|
+
// worktree root; never tracked (it lives only for the life of the worktree). Hub#2586.
|
|
6347
|
+
".jerv-lease.json",
|
|
6151
6348
|
"/*.png",
|
|
6152
6349
|
// Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
|
|
6153
6350
|
// .env.example reference file stays tracked.
|
|
@@ -6807,7 +7004,7 @@ async function auditRepoCi(repo, deps) {
|
|
|
6807
7004
|
ok: hasRequiredChecks,
|
|
6808
7005
|
label: "product required status checks active",
|
|
6809
7006
|
detail: hasRequiredChecks ? void 0 : `no required status checks active \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset`,
|
|
6810
|
-
remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap
|
|
7007
|
+
remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap apply --execute automation (#1440)`
|
|
6811
7008
|
});
|
|
6812
7009
|
if (hasGateWorkflow) {
|
|
6813
7010
|
const emitted = registryRequiredContexts(meta) ?? await getEmittedPrContexts();
|
|
@@ -7116,6 +7313,13 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
7116
7313
|
error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
|
|
7117
7314
|
};
|
|
7118
7315
|
}
|
|
7316
|
+
const prState = await deps.readPrState(prNumber, repo);
|
|
7317
|
+
if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
|
|
7318
|
+
return {
|
|
7319
|
+
...base,
|
|
7320
|
+
error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
|
|
7321
|
+
};
|
|
7322
|
+
}
|
|
7119
7323
|
const merge = await mergeAutoWithTransientRetry(prNumber, repo, deps);
|
|
7120
7324
|
base.mergeStatus = merge.mergeStatus;
|
|
7121
7325
|
if (merge.mergeStatus === "failed") {
|
|
@@ -7136,10 +7340,10 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
7136
7340
|
}
|
|
7137
7341
|
|
|
7138
7342
|
// src/wave-status.ts
|
|
7139
|
-
var
|
|
7343
|
+
var import_node_fs12 = require("node:fs");
|
|
7140
7344
|
|
|
7141
7345
|
// src/gc.ts
|
|
7142
|
-
var
|
|
7346
|
+
var import_node_path11 = require("node:path");
|
|
7143
7347
|
var DEFERRED_SWEEP_COMMAND = "mmi-cli gc sweep-deferred";
|
|
7144
7348
|
var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
|
|
7145
7349
|
var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
|
|
@@ -7410,8 +7614,8 @@ function resolveSafeSiblingWorktreeCleanupTarget(worktreePath, siblingRoot, deps
|
|
|
7410
7614
|
return { ok: true, path: worktreePath };
|
|
7411
7615
|
}
|
|
7412
7616
|
function siblingMmiWorktreesRoot(repoRoot) {
|
|
7413
|
-
const parent = (0,
|
|
7414
|
-
return (0,
|
|
7617
|
+
const parent = (0, import_node_path11.dirname)(repoRoot);
|
|
7618
|
+
return (0, import_node_path11.basename)(parent).toLowerCase() === "mmi-worktrees" ? parent : (0, import_node_path11.join)(parent, "mmi-worktrees");
|
|
7415
7619
|
}
|
|
7416
7620
|
function classifySiblingWorktreeDir(entry) {
|
|
7417
7621
|
if (!entry.ownedByCurrentRepo) {
|
|
@@ -8072,8 +8276,8 @@ function formatGcPlan(plan, apply) {
|
|
|
8072
8276
|
|
|
8073
8277
|
// src/stage-runner.ts
|
|
8074
8278
|
var import_node_child_process6 = require("node:child_process");
|
|
8075
|
-
var
|
|
8076
|
-
var
|
|
8279
|
+
var import_node_fs11 = require("node:fs");
|
|
8280
|
+
var import_node_path12 = require("node:path");
|
|
8077
8281
|
var import_node_net = require("node:net");
|
|
8078
8282
|
var import_node_util6 = require("node:util");
|
|
8079
8283
|
var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
|
|
@@ -8190,11 +8394,11 @@ function appendForceRecreate(up) {
|
|
|
8190
8394
|
return `${up.trimEnd()} --force-recreate`;
|
|
8191
8395
|
}
|
|
8192
8396
|
function stageStatePath(cwd = process.cwd()) {
|
|
8193
|
-
return (0,
|
|
8397
|
+
return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
|
|
8194
8398
|
}
|
|
8195
8399
|
function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
|
|
8196
|
-
const dir = (0,
|
|
8197
|
-
return (0,
|
|
8400
|
+
const dir = (0, import_node_path12.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path12.resolve)(cwd, gitCommonDir);
|
|
8401
|
+
return (0, import_node_path12.join)(dir, "mmi", "stage", "state.json");
|
|
8198
8402
|
}
|
|
8199
8403
|
function normPath2(path2) {
|
|
8200
8404
|
return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
@@ -8418,14 +8622,14 @@ function stageProcessEnv(stagePort, extraEnv) {
|
|
|
8418
8622
|
}
|
|
8419
8623
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
8420
8624
|
if (!config.ensureEnv) return;
|
|
8421
|
-
const target = (0,
|
|
8422
|
-
const example = (0,
|
|
8423
|
-
if (!(0,
|
|
8424
|
-
(0,
|
|
8425
|
-
} else if ((0,
|
|
8426
|
-
const stale = detectStaleEnvFile((0,
|
|
8427
|
-
exampleMtimeMs: (0,
|
|
8428
|
-
targetMtimeMs: (0,
|
|
8625
|
+
const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
|
|
8626
|
+
const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
|
|
8627
|
+
if (!(0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
|
|
8628
|
+
(0, import_node_fs11.copyFileSync)(example, target);
|
|
8629
|
+
} else if ((0, import_node_fs11.existsSync)(target) && (0, import_node_fs11.existsSync)(example)) {
|
|
8630
|
+
const stale = detectStaleEnvFile((0, import_node_fs11.readFileSync)(example, "utf8"), (0, import_node_fs11.readFileSync)(target, "utf8"), {
|
|
8631
|
+
exampleMtimeMs: (0, import_node_fs11.statSync)(example).mtimeMs,
|
|
8632
|
+
targetMtimeMs: (0, import_node_fs11.statSync)(target).mtimeMs
|
|
8429
8633
|
});
|
|
8430
8634
|
if (stale) {
|
|
8431
8635
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -8433,8 +8637,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
|
8433
8637
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
8434
8638
|
}
|
|
8435
8639
|
}
|
|
8436
|
-
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0,
|
|
8437
|
-
(0,
|
|
8640
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs11.existsSync)(target)) {
|
|
8641
|
+
(0, import_node_fs11.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs11.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
8438
8642
|
}
|
|
8439
8643
|
}
|
|
8440
8644
|
async function gitText(cwd, args) {
|
|
@@ -8464,20 +8668,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
|
|
|
8464
8668
|
return void 0;
|
|
8465
8669
|
}
|
|
8466
8670
|
function readState(path2) {
|
|
8467
|
-
if (!(0,
|
|
8671
|
+
if (!(0, import_node_fs11.existsSync)(path2)) return null;
|
|
8468
8672
|
try {
|
|
8469
|
-
return JSON.parse((0,
|
|
8673
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
|
|
8470
8674
|
} catch {
|
|
8471
8675
|
return null;
|
|
8472
8676
|
}
|
|
8473
8677
|
}
|
|
8474
8678
|
function mkdirFor(path2) {
|
|
8475
8679
|
const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
|
|
8476
|
-
(0,
|
|
8680
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
8477
8681
|
}
|
|
8478
8682
|
function writeState(path2, state) {
|
|
8479
8683
|
mkdirFor(path2);
|
|
8480
|
-
(0,
|
|
8684
|
+
(0, import_node_fs11.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
|
|
8481
8685
|
}
|
|
8482
8686
|
function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
8483
8687
|
const reservation = {
|
|
@@ -8497,7 +8701,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
|
|
|
8497
8701
|
await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, timeoutMs);
|
|
8498
8702
|
}
|
|
8499
8703
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
8500
|
-
(0,
|
|
8704
|
+
(0, import_node_fs11.rmSync)(path2, { force: true });
|
|
8501
8705
|
}
|
|
8502
8706
|
}
|
|
8503
8707
|
async function killTree(pid) {
|
|
@@ -8653,8 +8857,8 @@ async function runStage(config = {}, opts = {}) {
|
|
|
8653
8857
|
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
8654
8858
|
if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
|
|
8655
8859
|
} catch (e) {
|
|
8656
|
-
(0,
|
|
8657
|
-
if (globalStatePath && globalStatePath !== statePath) (0,
|
|
8860
|
+
(0, import_node_fs11.rmSync)(statePath, { force: true });
|
|
8861
|
+
if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs11.rmSync)(globalStatePath, { force: true });
|
|
8658
8862
|
throw e;
|
|
8659
8863
|
}
|
|
8660
8864
|
const started = await startStage(config, {
|
|
@@ -8679,9 +8883,9 @@ function parseNextFromHead(headText) {
|
|
|
8679
8883
|
}
|
|
8680
8884
|
function readStageSummary(worktreePath) {
|
|
8681
8885
|
const statePath = stageStatePath(worktreePath);
|
|
8682
|
-
if (!(0,
|
|
8886
|
+
if (!(0, import_node_fs12.existsSync)(statePath)) return void 0;
|
|
8683
8887
|
try {
|
|
8684
|
-
const state = JSON.parse((0,
|
|
8888
|
+
const state = JSON.parse((0, import_node_fs12.readFileSync)(statePath, "utf8"));
|
|
8685
8889
|
const port = typeof state.port === "number" ? state.port : void 0;
|
|
8686
8890
|
if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
|
|
8687
8891
|
return { port, url: typeof state.url === "string" ? state.url : void 0 };
|
|
@@ -8797,7 +9001,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
8797
9001
|
// src/gh-create.ts
|
|
8798
9002
|
var import_promises = require("node:fs/promises");
|
|
8799
9003
|
var import_node_os3 = require("node:os");
|
|
8800
|
-
var
|
|
9004
|
+
var import_node_path13 = require("node:path");
|
|
8801
9005
|
var import_node_crypto3 = require("node:crypto");
|
|
8802
9006
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
8803
9007
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
@@ -8840,8 +9044,8 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
8840
9044
|
const remove = deps.remove ?? import_promises.unlink;
|
|
8841
9045
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
8842
9046
|
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
8843
|
-
const file = (0,
|
|
8844
|
-
await ensureDir((0,
|
|
9047
|
+
const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
9048
|
+
await ensureDir((0, import_node_path13.dirname)(file), { recursive: true }).catch(() => {
|
|
8845
9049
|
});
|
|
8846
9050
|
await write(file, args[i + 1], "utf8");
|
|
8847
9051
|
return {
|
|
@@ -9689,20 +9893,6 @@ async function runStageLiveDown(deps, t) {
|
|
|
9689
9893
|
};
|
|
9690
9894
|
}
|
|
9691
9895
|
|
|
9692
|
-
// src/git-clean-tree.ts
|
|
9693
|
-
function isAgentScratchPath(path2) {
|
|
9694
|
-
const normalized = path2.replace(/\\/g, "/").trim();
|
|
9695
|
-
return /^tmp_[^/]+$/.test(normalized);
|
|
9696
|
-
}
|
|
9697
|
-
function porcelainHasBlockingChanges(porcelain) {
|
|
9698
|
-
return porcelain.split("\n").some((line) => {
|
|
9699
|
-
const trimmed = line.trim();
|
|
9700
|
-
if (!trimmed) return false;
|
|
9701
|
-
const path2 = trimmed.slice(3).split(" -> ")[0]?.trim() ?? "";
|
|
9702
|
-
return path2 !== "" && !isAgentScratchPath(path2);
|
|
9703
|
-
});
|
|
9704
|
-
}
|
|
9705
|
-
|
|
9706
9896
|
// src/tenant-control-parse.ts
|
|
9707
9897
|
var OUTPUT_BEGIN = "mmi-control-output-begin";
|
|
9708
9898
|
var OUTPUT_END = "mmi-control-output-end";
|
|
@@ -9743,7 +9933,7 @@ function resolveDeployModel2(meta, repo) {
|
|
|
9743
9933
|
if (isDeployModel(m)) return m;
|
|
9744
9934
|
return resolveDeployModel(meta, repo);
|
|
9745
9935
|
}
|
|
9746
|
-
function
|
|
9936
|
+
function clean2(out) {
|
|
9747
9937
|
return out.trim();
|
|
9748
9938
|
}
|
|
9749
9939
|
function requireValue(value, label) {
|
|
@@ -9826,11 +10016,11 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
|
|
|
9826
10016
|
if (release.targetCommitish !== expectedTarget) {
|
|
9827
10017
|
throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
|
|
9828
10018
|
}
|
|
9829
|
-
const tagSha = requireValue(
|
|
10019
|
+
const tagSha = requireValue(clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
|
|
9830
10020
|
if (tagSha !== expectedSha) {
|
|
9831
10021
|
throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
|
|
9832
10022
|
}
|
|
9833
|
-
const branchOut =
|
|
10023
|
+
const branchOut = clean2(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
|
|
9834
10024
|
const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
|
|
9835
10025
|
if (branchSha !== expectedSha) {
|
|
9836
10026
|
throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
|
|
@@ -9891,7 +10081,7 @@ function ensurePositiveCount(out, emptyMessage) {
|
|
|
9891
10081
|
if (count <= 0) throw new Error(emptyMessage);
|
|
9892
10082
|
}
|
|
9893
10083
|
async function remoteBranchExists(deps, branch) {
|
|
9894
|
-
const out =
|
|
10084
|
+
const out = clean2(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
9895
10085
|
return out.length > 0;
|
|
9896
10086
|
}
|
|
9897
10087
|
async function loadReleaseTrackBranchHints(deps) {
|
|
@@ -9903,10 +10093,10 @@ async function loadReleaseTrackBranchHints(deps) {
|
|
|
9903
10093
|
return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
|
|
9904
10094
|
}
|
|
9905
10095
|
async function buildTrainApplyContext(deps) {
|
|
9906
|
-
const repo = requireValue(
|
|
10096
|
+
const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
9907
10097
|
const [owner, name] = repo.split("/");
|
|
9908
10098
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
9909
|
-
const login = requireValue(
|
|
10099
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
9910
10100
|
const verdict = await deps.trainAuthority(repo);
|
|
9911
10101
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
9912
10102
|
if (!verdict.train) {
|
|
@@ -9927,11 +10117,11 @@ async function requireCleanTree(deps) {
|
|
|
9927
10117
|
if (porcelainHasBlockingChanges(status)) throw new Error("working tree must be clean before train apply");
|
|
9928
10118
|
}
|
|
9929
10119
|
async function requireBranch(deps, branch) {
|
|
9930
|
-
const current =
|
|
10120
|
+
const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
9931
10121
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
9932
10122
|
}
|
|
9933
10123
|
async function currentBranch(deps) {
|
|
9934
|
-
return
|
|
10124
|
+
return clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"])) || "(unknown)";
|
|
9935
10125
|
}
|
|
9936
10126
|
async function restoreCheckoutAfterRelease(deps, startBranch) {
|
|
9937
10127
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -9974,17 +10164,17 @@ async function restoreCheckoutAfterRelease(deps, startBranch) {
|
|
|
9974
10164
|
}
|
|
9975
10165
|
}
|
|
9976
10166
|
async function withCheckoutRestoredAfterRelease(deps, result, startBranch) {
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
};
|
|
10167
|
+
const checkout = await restoreCheckoutAfterRelease(deps, startBranch);
|
|
10168
|
+
const branches = TRAIN_BRANCHES.filter((b) => b !== startBranch);
|
|
10169
|
+
const localSync = await syncLocalTrainBranches(deps.run, { branches, fetch: false, warn: deps.warn }).catch(() => void 0);
|
|
10170
|
+
return { ...result, checkout, ...localSync ? { localSync } : {} };
|
|
9981
10171
|
}
|
|
9982
10172
|
function normGitPath(p) {
|
|
9983
10173
|
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
9984
10174
|
}
|
|
9985
10175
|
async function releaseRcBranchLock(deps) {
|
|
9986
10176
|
const porcelain = await deps.run("git", ["worktree", "list", "--porcelain"]);
|
|
9987
|
-
const cwd = normGitPath(
|
|
10177
|
+
const cwd = normGitPath(clean2(await deps.run("git", ["rev-parse", "--show-toplevel"])));
|
|
9988
10178
|
for (const wt of parseWorktreePorcelain(porcelain)) {
|
|
9989
10179
|
if (wt.branch !== "rc") continue;
|
|
9990
10180
|
if (normGitPath(wt.path) === cwd) continue;
|
|
@@ -10200,7 +10390,7 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
|
|
|
10200
10390
|
return [...contexts];
|
|
10201
10391
|
}
|
|
10202
10392
|
async function findOpenAlignmentPr(deps, ctx) {
|
|
10203
|
-
const out =
|
|
10393
|
+
const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
|
|
10204
10394
|
if (!out) return void 0;
|
|
10205
10395
|
const rows = JSON.parse(out);
|
|
10206
10396
|
const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
|
|
@@ -10228,14 +10418,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10228
10418
|
note: `alignment PR already open: ${existing.url} \u2014 land it with \`mmi-cli pr merge ${existing.number} --auto --merge\``
|
|
10229
10419
|
};
|
|
10230
10420
|
}
|
|
10231
|
-
const ahead =
|
|
10421
|
+
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
|
|
10232
10422
|
if (ahead === "0") {
|
|
10233
10423
|
return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
|
|
10234
10424
|
}
|
|
10235
10425
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
10236
10426
|
|
|
10237
10427
|
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** \u2014 \`mmi-cli pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
|
|
10238
|
-
const url =
|
|
10428
|
+
const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
|
|
10239
10429
|
const number = parsePrNumber(url);
|
|
10240
10430
|
return {
|
|
10241
10431
|
status: "pr-pending",
|
|
@@ -10353,13 +10543,13 @@ Do not delete or force-move the pushed tag; rerun the train only after confirmin
|
|
|
10353
10543
|
}
|
|
10354
10544
|
async function probeRemoteTag(deps, tag) {
|
|
10355
10545
|
const remoteOut = await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/tags/${tag}`]);
|
|
10356
|
-
return
|
|
10546
|
+
return clean2(remoteOut).split(/\s+/)[0] || "";
|
|
10357
10547
|
}
|
|
10358
10548
|
async function ensureTagPushed(deps, tag, sha, probed) {
|
|
10359
10549
|
const remoteSha = probed ? probed.remoteSha : await probeRemoteTag(deps, tag);
|
|
10360
10550
|
let localSha = "";
|
|
10361
10551
|
try {
|
|
10362
|
-
localSha =
|
|
10552
|
+
localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
|
|
10363
10553
|
} catch {
|
|
10364
10554
|
}
|
|
10365
10555
|
if (remoteSha) {
|
|
@@ -10385,7 +10575,7 @@ function probeRcResumeTags(deps, base) {
|
|
|
10385
10575
|
}
|
|
10386
10576
|
async function resolveRcResumeTag(deps, base, sha, probed) {
|
|
10387
10577
|
const out = probed ?? await runGitRemoteRead(deps, ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
|
|
10388
|
-
const onSha =
|
|
10578
|
+
const onSha = clean2(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
|
|
10389
10579
|
const unique = [...new Set(onSha)];
|
|
10390
10580
|
if (unique.length === 0) return { tag: null };
|
|
10391
10581
|
const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
|
|
@@ -10581,7 +10771,7 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
|
|
|
10581
10771
|
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted, preFold) {
|
|
10582
10772
|
await deps.run("git", ["checkout", "main"]);
|
|
10583
10773
|
await ffOnlyPull(deps, "main");
|
|
10584
|
-
preFold.mainSha =
|
|
10774
|
+
preFold.mainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
|
|
10585
10775
|
if (predicted.length === 0) {
|
|
10586
10776
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
10587
10777
|
} else {
|
|
@@ -10592,8 +10782,8 @@ async function probeFoldFailureState(deps) {
|
|
|
10592
10782
|
const branch = await currentBranch(deps);
|
|
10593
10783
|
const mergeInProgress = await deps.run("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).then(() => true).catch(() => false);
|
|
10594
10784
|
const dirty = porcelainHasBlockingChanges(await deps.run("git", ["status", "--porcelain"]).catch(() => ""));
|
|
10595
|
-
const mainSha = await deps.run("git", ["rev-parse", "main"]).then(
|
|
10596
|
-
const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(
|
|
10785
|
+
const mainSha = await deps.run("git", ["rev-parse", "main"]).then(clean2).catch(() => "");
|
|
10786
|
+
const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(clean2).catch(() => "");
|
|
10597
10787
|
const originIsAncestorOfMain = Boolean(mainSha) && Boolean(originMainSha) ? await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", "main"]).then(() => true).catch(() => false) : false;
|
|
10598
10788
|
return { branch, mergeInProgress, dirty, mainSha, originMainSha, originIsAncestorOfMain };
|
|
10599
10789
|
}
|
|
@@ -10654,7 +10844,7 @@ Recovery sequence:
|
|
|
10654
10844
|
);
|
|
10655
10845
|
}
|
|
10656
10846
|
}
|
|
10657
|
-
const rcSha = await deps.run("git", ["rev-parse", "rc"]).then(
|
|
10847
|
+
const rcSha = await deps.run("git", ["rev-parse", "rc"]).then(clean2).catch(() => "");
|
|
10658
10848
|
const rcDescends = Boolean(preRcSha) && Boolean(rcSha) ? await deps.run("git", ["merge-base", "--is-ancestor", preRcSha, "rc"]).then(() => true).catch(() => false) : false;
|
|
10659
10849
|
if (branch === "rc" && rcDescends) {
|
|
10660
10850
|
try {
|
|
@@ -10745,7 +10935,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
10745
10935
|
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
|
|
10746
10936
|
}
|
|
10747
10937
|
await runGitPush(deps, ["push", "origin", "main"]);
|
|
10748
|
-
const releaseUrl =
|
|
10938
|
+
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
10749
10939
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
10750
10940
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
10751
10941
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
@@ -10783,21 +10973,21 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10783
10973
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
10784
10974
|
);
|
|
10785
10975
|
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
10786
|
-
const releaseBase = requireValue(
|
|
10976
|
+
const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
10787
10977
|
await releaseRcBranchLock(deps);
|
|
10788
10978
|
const rcTagProbe = await probeRcResumeTags(deps, releaseBase);
|
|
10789
10979
|
await deps.run("git", ["checkout", "rc"]);
|
|
10790
10980
|
await ffOnlyPull(deps, "rc");
|
|
10791
|
-
const preRcSha = requireValue(
|
|
10981
|
+
const preRcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "pre-merge rc sha");
|
|
10792
10982
|
let rcSha;
|
|
10793
10983
|
let resume;
|
|
10794
10984
|
let tag2;
|
|
10795
10985
|
let tagPush;
|
|
10796
10986
|
try {
|
|
10797
10987
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
10798
|
-
rcSha = requireValue(
|
|
10988
|
+
rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
10799
10989
|
resume = await resolveRcResumeTag(deps, releaseBase, rcSha, rcTagProbe);
|
|
10800
|
-
tag2 = resume.tag ?? requireValue(
|
|
10990
|
+
tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
10801
10991
|
tagPush = await ensureTagPushed(deps, tag2, rcSha);
|
|
10802
10992
|
} catch (e) {
|
|
10803
10993
|
throw await recoverFailedRcand(deps, e, preRcSha);
|
|
@@ -10835,8 +11025,8 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10835
11025
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
10836
11026
|
);
|
|
10837
11027
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
10838
|
-
const tag2 = requireValue(
|
|
10839
|
-
const rcShaAtRelease = !directTrack && hasRcBranch ?
|
|
11028
|
+
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
11029
|
+
const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
10840
11030
|
const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
|
|
10841
11031
|
deps,
|
|
10842
11032
|
deployModel2,
|
|
@@ -10849,7 +11039,7 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10849
11039
|
const { versionFold: versionFold2, releaseSha: releaseSha2 } = await runFoldStage(deps, "development", preFold2, async () => {
|
|
10850
11040
|
await executeMergeToMain(deps, "development", "development -> main", tolerated2, predicted2, preFold2);
|
|
10851
11041
|
const versionFold3 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10852
|
-
const releaseSha3 = requireValue(
|
|
11042
|
+
const releaseSha3 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10853
11043
|
return { versionFold: versionFold3, releaseSha: releaseSha3 };
|
|
10854
11044
|
});
|
|
10855
11045
|
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2, "development", preFold2, tagProbe2);
|
|
@@ -10927,14 +11117,14 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10927
11117
|
`hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026].`
|
|
10928
11118
|
);
|
|
10929
11119
|
}
|
|
10930
|
-
const releasedRcSha =
|
|
10931
|
-
const tag = requireValue(
|
|
11120
|
+
const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
11121
|
+
const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
10932
11122
|
const tagProbe = { remoteSha: await probeRemoteTag(deps, tag) };
|
|
10933
11123
|
const preFold = {};
|
|
10934
11124
|
const { versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
|
|
10935
11125
|
await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted, preFold);
|
|
10936
11126
|
const versionFold2 = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
10937
|
-
const releaseSha2 = requireValue(
|
|
11127
|
+
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10938
11128
|
return { versionFold: versionFold2, releaseSha: releaseSha2 };
|
|
10939
11129
|
});
|
|
10940
11130
|
const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, "rc", preFold, tagProbe);
|
|
@@ -11053,7 +11243,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
11053
11243
|
}
|
|
11054
11244
|
try {
|
|
11055
11245
|
await deps.run("git", ["fetch", "origin", "rc"]);
|
|
11056
|
-
const rcNow =
|
|
11246
|
+
const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
11057
11247
|
if (rcNow !== releasedRcSha) {
|
|
11058
11248
|
return {
|
|
11059
11249
|
status: "skipped",
|
|
@@ -11084,7 +11274,7 @@ async function runTenantRedeploy(deps, options) {
|
|
|
11084
11274
|
const repo = options.repo;
|
|
11085
11275
|
const [owner, name] = repo.split("/");
|
|
11086
11276
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
11087
|
-
const login = requireValue(
|
|
11277
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
11088
11278
|
const verdict = await deps.trainAuthority(repo);
|
|
11089
11279
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
11090
11280
|
if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
|
|
@@ -11324,7 +11514,7 @@ var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
|
11324
11514
|
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
11325
11515
|
var HOTFIX_VERIFY_ATTEMPTS = 5;
|
|
11326
11516
|
var HOTFIX_VERIFY_RETRY_MS = 6e3;
|
|
11327
|
-
function
|
|
11517
|
+
function clean3(out) {
|
|
11328
11518
|
return out.trim();
|
|
11329
11519
|
}
|
|
11330
11520
|
function sleeper(deps) {
|
|
@@ -11382,7 +11572,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
11382
11572
|
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
11383
11573
|
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
11384
11574
|
}
|
|
11385
|
-
const sha =
|
|
11575
|
+
const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
11386
11576
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
11387
11577
|
return { sha, label: sha.slice(0, 7) };
|
|
11388
11578
|
}
|
|
@@ -11410,7 +11600,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11410
11600
|
};
|
|
11411
11601
|
}
|
|
11412
11602
|
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
11413
|
-
const remoteBranch =
|
|
11603
|
+
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
11414
11604
|
if (remoteBranch) {
|
|
11415
11605
|
await deps.run("git", ["checkout", branch]);
|
|
11416
11606
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
@@ -11441,7 +11631,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11441
11631
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
11442
11632
|
}
|
|
11443
11633
|
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
|
|
11444
|
-
const prUrl =
|
|
11634
|
+
const prUrl = clean3(await deps.run("gh", [
|
|
11445
11635
|
"pr",
|
|
11446
11636
|
"create",
|
|
11447
11637
|
"--repo",
|
|
@@ -11585,7 +11775,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11585
11775
|
}
|
|
11586
11776
|
let verifyNote;
|
|
11587
11777
|
if (deployModel === "hub-serverless") {
|
|
11588
|
-
const previousRef =
|
|
11778
|
+
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
11589
11779
|
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
11590
11780
|
try {
|
|
11591
11781
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
@@ -11672,9 +11862,9 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
11672
11862
|
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts), warnings };
|
|
11673
11863
|
}
|
|
11674
11864
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
11675
|
-
const branchExists = Boolean(
|
|
11865
|
+
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
11676
11866
|
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
11677
|
-
const remoteTag =
|
|
11867
|
+
const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
11678
11868
|
const tagPushed = Boolean(remoteTag);
|
|
11679
11869
|
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
11680
11870
|
let releaseExists = false;
|
|
@@ -11708,7 +11898,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
11708
11898
|
}
|
|
11709
11899
|
}
|
|
11710
11900
|
}
|
|
11711
|
-
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(
|
|
11901
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
11712
11902
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
11713
11903
|
}
|
|
11714
11904
|
function compareHotfixVersions(a, b) {
|
|
@@ -11743,7 +11933,7 @@ async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
|
|
|
11743
11933
|
const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
|
|
11744
11934
|
if (m) tags.add(m[1]);
|
|
11745
11935
|
}
|
|
11746
|
-
const branchOut =
|
|
11936
|
+
const branchOut = clean3(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
|
|
11747
11937
|
for (const line of branchOut.split("\n").filter(Boolean)) {
|
|
11748
11938
|
const ref = line.split(/\s+/)[1] ?? "";
|
|
11749
11939
|
const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
|
|
@@ -11909,8 +12099,8 @@ async function announceRelease(deps, args) {
|
|
|
11909
12099
|
}
|
|
11910
12100
|
|
|
11911
12101
|
// src/port-registry.ts
|
|
11912
|
-
var
|
|
11913
|
-
var
|
|
12102
|
+
var import_node_fs13 = require("node:fs");
|
|
12103
|
+
var import_node_path14 = require("node:path");
|
|
11914
12104
|
|
|
11915
12105
|
// ../infra/port-geometry.mjs
|
|
11916
12106
|
var PORT_BLOCK = 100;
|
|
@@ -11924,8 +12114,8 @@ function nextPortBlock(registry2) {
|
|
|
11924
12114
|
return [base, base + PORT_SPAN];
|
|
11925
12115
|
}
|
|
11926
12116
|
function loadPortRegistry(path2) {
|
|
11927
|
-
if (!(0,
|
|
11928
|
-
const raw = JSON.parse((0,
|
|
12117
|
+
if (!(0, import_node_fs13.existsSync)(path2)) return {};
|
|
12118
|
+
const raw = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
|
|
11929
12119
|
const out = {};
|
|
11930
12120
|
for (const [key, value] of Object.entries(raw)) {
|
|
11931
12121
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -11939,9 +12129,9 @@ function ensurePortRange(repo, path2) {
|
|
|
11939
12129
|
const existing = registry2[repo];
|
|
11940
12130
|
if (existing) return existing;
|
|
11941
12131
|
const range = nextPortBlock(registry2);
|
|
11942
|
-
const raw = (0,
|
|
12132
|
+
const raw = (0, import_node_fs13.existsSync)(path2) ? JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8")) : {};
|
|
11943
12133
|
raw[repo] = range;
|
|
11944
|
-
(0,
|
|
12134
|
+
(0, import_node_fs13.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
11945
12135
|
return range;
|
|
11946
12136
|
}
|
|
11947
12137
|
function portCursorSeed(registry2) {
|
|
@@ -11963,20 +12153,26 @@ function existingPortRange(repo, registry2) {
|
|
|
11963
12153
|
return registry2[repo] ?? null;
|
|
11964
12154
|
}
|
|
11965
12155
|
function portRangeInfraAt(root, source) {
|
|
11966
|
-
const registryPath = (0,
|
|
11967
|
-
const ddbScriptPath = (0,
|
|
11968
|
-
if (!(0,
|
|
12156
|
+
const registryPath = (0, import_node_path14.join)(root, "infra", "port-ranges.json");
|
|
12157
|
+
const ddbScriptPath = (0, import_node_path14.join)(root, "infra", "port-ddb.mjs");
|
|
12158
|
+
if (!(0, import_node_fs13.existsSync)(registryPath) || !(0, import_node_fs13.existsSync)(ddbScriptPath)) return null;
|
|
11969
12159
|
return { root, source, registryPath, ddbScriptPath };
|
|
11970
12160
|
}
|
|
11971
|
-
function resolvePortRangeInfra(cwd) {
|
|
12161
|
+
function resolvePortRangeInfra(cwd, packageDir) {
|
|
11972
12162
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
11973
12163
|
if (direct) return direct;
|
|
11974
|
-
for (let dir = cwd; ; dir = (0,
|
|
11975
|
-
const sibling = portRangeInfraAt((0,
|
|
12164
|
+
for (let dir = cwd; ; dir = (0, import_node_path14.dirname)(dir)) {
|
|
12165
|
+
const sibling = portRangeInfraAt((0, import_node_path14.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
11976
12166
|
if (sibling) return sibling;
|
|
11977
|
-
const parent = (0,
|
|
11978
|
-
if (parent === dir)
|
|
12167
|
+
const parent = (0, import_node_path14.dirname)(dir);
|
|
12168
|
+
if (parent === dir) break;
|
|
12169
|
+
}
|
|
12170
|
+
if (packageDir) {
|
|
12171
|
+
const pkgRoot = (0, import_node_path14.join)(packageDir, "..", "..");
|
|
12172
|
+
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
12173
|
+
if (pkgFrom) return pkgFrom;
|
|
11979
12174
|
}
|
|
12175
|
+
return null;
|
|
11980
12176
|
}
|
|
11981
12177
|
async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
11982
12178
|
const registry2 = loadPortRegistry(path2);
|
|
@@ -12235,7 +12431,7 @@ function renderAccessReport(report) {
|
|
|
12235
12431
|
}
|
|
12236
12432
|
|
|
12237
12433
|
// src/bootstrap-verify.ts
|
|
12238
|
-
var
|
|
12434
|
+
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
12239
12435
|
var requiredDocs = ["README.md", "architecture.md"];
|
|
12240
12436
|
var requiredIssueTemplates = [
|
|
12241
12437
|
".github/ISSUE_TEMPLATE/bug.yml",
|
|
@@ -12413,7 +12609,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12413
12609
|
detail: hasPushAllowlist(protection) ? void 0 : "restrictions.users is empty or unset"
|
|
12414
12610
|
});
|
|
12415
12611
|
}
|
|
12416
|
-
const strayTrainBranches =
|
|
12612
|
+
const strayTrainBranches = TRAIN_BRANCHES2.filter((b) => !branchesWanted.includes(b) && branchNames.has(b));
|
|
12417
12613
|
checks.push({
|
|
12418
12614
|
ok: strayTrainBranches.length === 0,
|
|
12419
12615
|
label: "no stray train branch for this track",
|
|
@@ -12863,38 +13059,51 @@ async function tenantDeploy(payload, deps) {
|
|
|
12863
13059
|
// src/wiki-publish-command.ts
|
|
12864
13060
|
async function wikiPublish(deps, opts) {
|
|
12865
13061
|
if (!opts.pagesDir) {
|
|
12866
|
-
|
|
13062
|
+
if (opts.json) {
|
|
13063
|
+
deps.log(JSON.stringify({ ok: false, error: "pages-dir is required" }, null, 2));
|
|
13064
|
+
} else {
|
|
13065
|
+
deps.err("wiki publish: <pages-dir> is required");
|
|
13066
|
+
}
|
|
12867
13067
|
return false;
|
|
12868
13068
|
}
|
|
12869
13069
|
const minted = await deps.mint(opts.repo);
|
|
12870
13070
|
if (!minted.ok) {
|
|
12871
|
-
|
|
13071
|
+
if (opts.json) {
|
|
13072
|
+
deps.log(JSON.stringify({ ok: false, error: minted.error }, null, 2));
|
|
13073
|
+
} else {
|
|
13074
|
+
deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
|
|
13075
|
+
}
|
|
12872
13076
|
return false;
|
|
12873
13077
|
}
|
|
12874
|
-
const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir });
|
|
13078
|
+
const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir, json: opts.json });
|
|
12875
13079
|
if (!ok) {
|
|
12876
13080
|
return false;
|
|
12877
13081
|
}
|
|
12878
|
-
|
|
13082
|
+
if (opts.json) {
|
|
13083
|
+
deps.log(JSON.stringify({ ok: true, repo: opts.repo }, null, 2));
|
|
13084
|
+
} else {
|
|
13085
|
+
deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
|
|
13086
|
+
}
|
|
12879
13087
|
return true;
|
|
12880
13088
|
}
|
|
12881
13089
|
|
|
12882
13090
|
// src/wiki-publish-git.ts
|
|
12883
13091
|
var import_node_child_process8 = require("node:child_process");
|
|
12884
|
-
var
|
|
13092
|
+
var import_node_fs14 = require("node:fs");
|
|
12885
13093
|
var import_node_os5 = require("node:os");
|
|
12886
|
-
var
|
|
13094
|
+
var import_node_path15 = require("node:path");
|
|
12887
13095
|
var WIKI_STAMP_FILE = ".wiki-state.json";
|
|
12888
13096
|
function redactWikiSecrets(msg) {
|
|
12889
13097
|
return msg.replace(/x-access-token:[^@\s]+@/gi, "x-access-token:***@").replace(/(AUTHORIZATION:\s*basic\s+)[A-Za-z0-9+/=]+/gi, "$1***");
|
|
12890
13098
|
}
|
|
13099
|
+
function wikiAuthArgs(token) {
|
|
13100
|
+
return ["-c", `http.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`];
|
|
13101
|
+
}
|
|
12891
13102
|
function publishWikiViaGit(deps, opts) {
|
|
12892
13103
|
const dir = deps.mkdtemp();
|
|
12893
13104
|
const url = `https://github.com/${opts.repo}.wiki.git`;
|
|
12894
|
-
const authArgs =
|
|
12895
|
-
|
|
12896
|
-
`http.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${opts.token}`).toString("base64")}`
|
|
12897
|
-
];
|
|
13105
|
+
const authArgs = wikiAuthArgs(opts.token);
|
|
13106
|
+
const json = opts.json ?? false;
|
|
12898
13107
|
try {
|
|
12899
13108
|
deps.gitRun(["init", "-q"], dir);
|
|
12900
13109
|
deps.gitRun(["remote", "add", "origin", url], dir);
|
|
@@ -12904,20 +13113,20 @@ function publishWikiViaGit(deps, opts) {
|
|
|
12904
13113
|
let copied = 0;
|
|
12905
13114
|
for (const f of deps.readdir(opts.pagesDir)) {
|
|
12906
13115
|
if (f.endsWith(".md")) {
|
|
12907
|
-
deps.copyFile((0,
|
|
13116
|
+
deps.copyFile((0, import_node_path15.join)(opts.pagesDir, f), (0, import_node_path15.join)(dir, f));
|
|
12908
13117
|
copied++;
|
|
12909
13118
|
}
|
|
12910
13119
|
}
|
|
12911
13120
|
if (!copied) {
|
|
12912
|
-
deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
|
|
13121
|
+
if (!json) deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
|
|
12913
13122
|
return false;
|
|
12914
13123
|
}
|
|
12915
|
-
if (deps.fileExists((0,
|
|
12916
|
-
deps.copyFile((0,
|
|
13124
|
+
if (deps.fileExists((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
|
|
13125
|
+
deps.copyFile((0, import_node_path15.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path15.join)(dir, WIKI_STAMP_FILE));
|
|
12917
13126
|
}
|
|
12918
13127
|
deps.gitRun(["add", "-A"], dir);
|
|
12919
13128
|
if (deps.gitProbe(["diff", "--cached", "--quiet"], dir)) {
|
|
12920
|
-
deps.log("wiki publish: already up to date");
|
|
13129
|
+
if (!json) deps.log("wiki publish: already up to date");
|
|
12921
13130
|
return true;
|
|
12922
13131
|
}
|
|
12923
13132
|
deps.gitRun(
|
|
@@ -12935,27 +13144,52 @@ function publishWikiViaGit(deps, opts) {
|
|
|
12935
13144
|
try {
|
|
12936
13145
|
deps.gitRun([...authArgs, "push", "origin", "HEAD:master"], dir);
|
|
12937
13146
|
} catch (e) {
|
|
12938
|
-
deps.err(
|
|
13147
|
+
if (!json) deps.err(
|
|
12939
13148
|
`wiki publish failed: push to ${opts.repo}.wiki.git rejected: ${redactWikiSecrets(e.message)} \u2014 if the wiki advanced before this push, re-run wiki publish to pick up the latest wiki state`
|
|
12940
13149
|
);
|
|
12941
13150
|
return false;
|
|
12942
13151
|
}
|
|
12943
|
-
deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
|
|
13152
|
+
if (!json) deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
|
|
12944
13153
|
return true;
|
|
12945
13154
|
} catch (e) {
|
|
12946
|
-
deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
|
|
13155
|
+
if (!json) deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
|
|
12947
13156
|
return false;
|
|
12948
13157
|
} finally {
|
|
12949
13158
|
deps.rm(dir);
|
|
12950
13159
|
}
|
|
12951
13160
|
}
|
|
13161
|
+
function readWikiStampViaGit(deps, opts) {
|
|
13162
|
+
const dir = deps.mkdtemp();
|
|
13163
|
+
const url = `https://github.com/${opts.repo}.wiki.git`;
|
|
13164
|
+
const authArgs = wikiAuthArgs(opts.token);
|
|
13165
|
+
try {
|
|
13166
|
+
deps.gitRun(["init", "-q"], dir);
|
|
13167
|
+
deps.gitRun(["remote", "add", "origin", url], dir);
|
|
13168
|
+
if (!deps.gitProbe([...authArgs, "ls-remote", "origin"], dir)) {
|
|
13169
|
+
return { ok: false, error: `could not reach ${opts.repo}.wiki.git to read the wiki stamp \u2014 auth or network failure` };
|
|
13170
|
+
}
|
|
13171
|
+
if (!deps.gitProbe([...authArgs, "fetch", "--depth", "1", "origin", "master"], dir)) {
|
|
13172
|
+
return { ok: true, stamp: {} };
|
|
13173
|
+
}
|
|
13174
|
+
deps.gitRun(["reset", "--hard", "FETCH_HEAD"], dir);
|
|
13175
|
+
const stampPath = (0, import_node_path15.join)(dir, WIKI_STAMP_FILE);
|
|
13176
|
+
if (!deps.fileExists(stampPath)) return { ok: true, stamp: {} };
|
|
13177
|
+
const parsed = JSON.parse(deps.readFile(stampPath));
|
|
13178
|
+
return { ok: true, stamp: parsed && typeof parsed === "object" ? parsed : {} };
|
|
13179
|
+
} catch (e) {
|
|
13180
|
+
return { ok: false, error: redactWikiSecrets(e.message) };
|
|
13181
|
+
} finally {
|
|
13182
|
+
deps.rm(dir);
|
|
13183
|
+
}
|
|
13184
|
+
}
|
|
12952
13185
|
function realWikiGitPublishDeps() {
|
|
12953
13186
|
return {
|
|
12954
|
-
mkdtemp: () => (0,
|
|
12955
|
-
readdir: (dir) => (0,
|
|
12956
|
-
copyFile: (src, dest) => (0,
|
|
12957
|
-
fileExists: (p) => (0,
|
|
12958
|
-
|
|
13187
|
+
mkdtemp: () => (0, import_node_fs14.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "wiki-")),
|
|
13188
|
+
readdir: (dir) => (0, import_node_fs14.readdirSync)(dir),
|
|
13189
|
+
copyFile: (src, dest) => (0, import_node_fs14.copyFileSync)(src, dest),
|
|
13190
|
+
fileExists: (p) => (0, import_node_fs14.existsSync)(p),
|
|
13191
|
+
readFile: (p) => (0, import_node_fs14.readFileSync)(p, "utf8"),
|
|
13192
|
+
rm: (p) => (0, import_node_fs14.rmSync)(p, { recursive: true, force: true }),
|
|
12959
13193
|
gitProbe: (args, cwd) => {
|
|
12960
13194
|
try {
|
|
12961
13195
|
(0, import_node_child_process8.execFileSync)("git", args, { cwd, stdio: "pipe" });
|
|
@@ -12972,6 +13206,124 @@ function realWikiGitPublishDeps() {
|
|
|
12972
13206
|
};
|
|
12973
13207
|
}
|
|
12974
13208
|
|
|
13209
|
+
// src/wiki-pages.ts
|
|
13210
|
+
var import_node_crypto4 = require("node:crypto");
|
|
13211
|
+
var import_node_fs15 = require("node:fs");
|
|
13212
|
+
var import_node_path16 = require("node:path");
|
|
13213
|
+
var MARKER_RE = /<!--\s*wiki:page\s+([^>]*?)-->/g;
|
|
13214
|
+
var attr = (raw, key) => {
|
|
13215
|
+
const m = new RegExp(`${key}\\s*=\\s*"([^"]*)"`).exec(raw);
|
|
13216
|
+
return m ? m[1].trim() : "";
|
|
13217
|
+
};
|
|
13218
|
+
var sanitizeSlug = (s) => String(s ?? "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
13219
|
+
function extractWikiMarkers(text, sourcePath = "") {
|
|
13220
|
+
const out = [];
|
|
13221
|
+
for (const m of String(text ?? "").matchAll(MARKER_RE)) {
|
|
13222
|
+
const raw = m[1];
|
|
13223
|
+
const slug = sanitizeSlug(attr(raw, "slug"));
|
|
13224
|
+
const title = attr(raw, "title");
|
|
13225
|
+
if (!slug || !title) continue;
|
|
13226
|
+
out.push({ slug, title, artifact: attr(raw, "artifact"), sourcePath });
|
|
13227
|
+
}
|
|
13228
|
+
return out;
|
|
13229
|
+
}
|
|
13230
|
+
var DURABLE_GLOBS = [
|
|
13231
|
+
"architecture.md",
|
|
13232
|
+
"docs/Architecture",
|
|
13233
|
+
"kb/architecture",
|
|
13234
|
+
"docs/org-architecture.md",
|
|
13235
|
+
"docs/org-readme.md"
|
|
13236
|
+
];
|
|
13237
|
+
var walkMarkdown = (root, rel) => {
|
|
13238
|
+
const abs = (0, import_node_path16.join)(root, rel);
|
|
13239
|
+
let st;
|
|
13240
|
+
try {
|
|
13241
|
+
st = (0, import_node_fs15.statSync)(abs);
|
|
13242
|
+
} catch {
|
|
13243
|
+
return [];
|
|
13244
|
+
}
|
|
13245
|
+
if (st.isFile()) return rel.endsWith(".md") ? [rel] : [];
|
|
13246
|
+
return (0, import_node_fs15.readdirSync)(abs).flatMap((name) => walkMarkdown(root, (0, import_node_path16.join)(rel, name)));
|
|
13247
|
+
};
|
|
13248
|
+
function collectFeaturePages({ listDocs, readDoc }) {
|
|
13249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13250
|
+
const pages = [];
|
|
13251
|
+
for (const path2 of listDocs()) {
|
|
13252
|
+
for (const mk of extractWikiMarkers(readDoc(path2), path2)) {
|
|
13253
|
+
if (seen.has(mk.slug)) continue;
|
|
13254
|
+
seen.add(mk.slug);
|
|
13255
|
+
pages.push(mk);
|
|
13256
|
+
}
|
|
13257
|
+
}
|
|
13258
|
+
return pages;
|
|
13259
|
+
}
|
|
13260
|
+
function fsReaders(rootDir) {
|
|
13261
|
+
return {
|
|
13262
|
+
listDocs: () => DURABLE_GLOBS.flatMap((g) => walkMarkdown(rootDir, g)),
|
|
13263
|
+
readDoc: (p) => {
|
|
13264
|
+
try {
|
|
13265
|
+
return (0, import_node_fs15.readFileSync)((0, import_node_path16.join)(rootDir, p), "utf8");
|
|
13266
|
+
} catch {
|
|
13267
|
+
return "";
|
|
13268
|
+
}
|
|
13269
|
+
}
|
|
13270
|
+
};
|
|
13271
|
+
}
|
|
13272
|
+
function hashPageInputs({ slug, title, source }) {
|
|
13273
|
+
return (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify([slug ?? "", title ?? "", source ?? ""])).digest("hex");
|
|
13274
|
+
}
|
|
13275
|
+
function computePageStamp(pages, readSource) {
|
|
13276
|
+
const stamp = {};
|
|
13277
|
+
for (const p of pages ?? []) {
|
|
13278
|
+
stamp[p.slug] = {
|
|
13279
|
+
hash: hashPageInputs({ slug: p.slug, title: p.title, source: readSource(p.sourcePath) }),
|
|
13280
|
+
sourcePath: p.sourcePath
|
|
13281
|
+
};
|
|
13282
|
+
}
|
|
13283
|
+
return stamp;
|
|
13284
|
+
}
|
|
13285
|
+
function selectChangedPages(pages, prevStamp, readSource) {
|
|
13286
|
+
const prev = prevStamp && typeof prevStamp === "object" ? prevStamp : {};
|
|
13287
|
+
const backfill = Object.keys(prev).length === 0;
|
|
13288
|
+
return (pages ?? []).filter((p) => {
|
|
13289
|
+
if (backfill) return true;
|
|
13290
|
+
const prior = prev[p.slug];
|
|
13291
|
+
if (!prior) return true;
|
|
13292
|
+
const cur = hashPageInputs({ slug: p.slug, title: p.title, source: readSource(p.sourcePath) });
|
|
13293
|
+
return prior.hash !== cur;
|
|
13294
|
+
});
|
|
13295
|
+
}
|
|
13296
|
+
|
|
13297
|
+
// src/wiki-plan-command.ts
|
|
13298
|
+
async function wikiPlan(deps, opts) {
|
|
13299
|
+
const wikiRepo = `${opts.repo}.wiki.git`;
|
|
13300
|
+
const pages = deps.collectPages();
|
|
13301
|
+
if (pages.length === 0) {
|
|
13302
|
+
emit(deps, { repo: opts.repo, wikiRepo, backfill: false, pages: [], stamp: {} });
|
|
13303
|
+
return true;
|
|
13304
|
+
}
|
|
13305
|
+
const read = await deps.mintAndReadStamp(opts.repo);
|
|
13306
|
+
if (!read.ok) {
|
|
13307
|
+
deps.err(`wiki plan failed: could not read the wiki stamp for ${opts.repo}: ${read.error}`);
|
|
13308
|
+
return false;
|
|
13309
|
+
}
|
|
13310
|
+
const prevStamp = read.stamp;
|
|
13311
|
+
const backfill = Object.keys(prevStamp).length === 0;
|
|
13312
|
+
const changed = new Set(selectChangedPages(pages, prevStamp, deps.readSource).map((p) => p.slug));
|
|
13313
|
+
const stamp = computePageStamp(pages, deps.readSource);
|
|
13314
|
+
emit(deps, {
|
|
13315
|
+
repo: opts.repo,
|
|
13316
|
+
wikiRepo,
|
|
13317
|
+
backfill,
|
|
13318
|
+
pages: pages.map((p) => ({ ...p, changed: changed.has(p.slug) })),
|
|
13319
|
+
stamp
|
|
13320
|
+
});
|
|
13321
|
+
return true;
|
|
13322
|
+
}
|
|
13323
|
+
function emit(deps, result) {
|
|
13324
|
+
deps.out(JSON.stringify(result, null, 2));
|
|
13325
|
+
}
|
|
13326
|
+
|
|
12975
13327
|
// src/project-readiness.ts
|
|
12976
13328
|
function stagesForTrack(meta) {
|
|
12977
13329
|
return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
|
|
@@ -13990,22 +14342,22 @@ var DEPLOY_STAGES = ["dev", "rc", "main"];
|
|
|
13990
14342
|
var DEPLOY_DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
|
13991
14343
|
var DEPLOY_SHELL_SAFE_RE = /^[A-Za-z0-9./:_?&=%-]*$/;
|
|
13992
14344
|
function buildSetDeployPatch(slug, input) {
|
|
13993
|
-
const
|
|
13994
|
-
const stage2 =
|
|
14345
|
+
const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
|
|
14346
|
+
const stage2 = clean4(input.stage);
|
|
13995
14347
|
if (!stage2 || !DEPLOY_STAGES.includes(stage2)) {
|
|
13996
14348
|
throw new Error(`project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
|
|
13997
14349
|
}
|
|
13998
|
-
const substrate =
|
|
14350
|
+
const substrate = clean4(input.substrate) ?? "hetzner-ssh";
|
|
13999
14351
|
if (!DEPLOY_SUBSTRATES.includes(substrate)) {
|
|
14000
14352
|
throw new Error(`project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
|
|
14001
14353
|
}
|
|
14002
|
-
const sshHost =
|
|
14354
|
+
const sshHost = clean4(input.sshHost);
|
|
14003
14355
|
if (substrate === "hetzner-ssh" && !sshHost) {
|
|
14004
14356
|
throw new Error("project set-deploy: hetzner-ssh requires --ssh-host");
|
|
14005
14357
|
}
|
|
14006
|
-
const sshUser =
|
|
14007
|
-
const deployPath =
|
|
14008
|
-
const serviceName =
|
|
14358
|
+
const sshUser = clean4(input.sshUser) ?? "root";
|
|
14359
|
+
const deployPath = clean4(input.deployPath) ?? `/opt/mmi/${slug}/${stage2}`;
|
|
14360
|
+
const serviceName = clean4(input.service) ?? slug;
|
|
14009
14361
|
for (const [label, v] of [["--ssh-host", sshHost], ["--ssh-user", sshUser], ["--deploy-path", deployPath], ["--service", serviceName]]) {
|
|
14010
14362
|
if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`project set-deploy: ${label} contains unsafe characters`);
|
|
14011
14363
|
}
|
|
@@ -14014,9 +14366,9 @@ function buildSetDeployPatch(slug, input) {
|
|
|
14014
14366
|
port = Number(input.port);
|
|
14015
14367
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("project set-deploy: --port must be an integer 1..65535");
|
|
14016
14368
|
}
|
|
14017
|
-
const domain =
|
|
14369
|
+
const domain = clean4(input.domain);
|
|
14018
14370
|
if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
|
|
14019
|
-
const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) =>
|
|
14371
|
+
const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) => clean4(a)).filter((a) => a !== void 0);
|
|
14020
14372
|
for (const a of aliases) {
|
|
14021
14373
|
if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
|
|
14022
14374
|
}
|
|
@@ -14239,7 +14591,12 @@ async function targetRepo(deps, opts) {
|
|
|
14239
14591
|
return opts.repo ?? repoOf(await vaultSlug(deps, opts));
|
|
14240
14592
|
}
|
|
14241
14593
|
async function secretsWhere(deps, opts) {
|
|
14242
|
-
|
|
14594
|
+
const p = vaultPointer(await vaultSlug(deps, opts));
|
|
14595
|
+
if (opts.json) {
|
|
14596
|
+
deps.log(JSON.stringify(p, null, 2));
|
|
14597
|
+
return;
|
|
14598
|
+
}
|
|
14599
|
+
deps.log(formatVaultPointer(p));
|
|
14243
14600
|
}
|
|
14244
14601
|
async function readErr(res) {
|
|
14245
14602
|
try {
|
|
@@ -14302,6 +14659,10 @@ async function secretsList(deps, opts) {
|
|
|
14302
14659
|
return;
|
|
14303
14660
|
}
|
|
14304
14661
|
const { secrets } = await res.json();
|
|
14662
|
+
if (opts.json) {
|
|
14663
|
+
deps.log(JSON.stringify({ secrets: secrets ?? [] }, null, 2));
|
|
14664
|
+
return;
|
|
14665
|
+
}
|
|
14305
14666
|
deps.log(formatSecretList(secrets ?? []));
|
|
14306
14667
|
}
|
|
14307
14668
|
var CAPABILITIES_TIMEOUT_MS = 2e4;
|
|
@@ -14517,6 +14878,13 @@ function requiredRuntimeSecretNames(stage2, contract, opts = {}) {
|
|
|
14517
14878
|
function stageKey2(stage2, key) {
|
|
14518
14879
|
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
14519
14880
|
}
|
|
14881
|
+
function keyDeclaredPerStage(declaredSecrets, key) {
|
|
14882
|
+
if (!declaredSecrets) return void 0;
|
|
14883
|
+
const entry = declaredSecrets[key];
|
|
14884
|
+
if (!entry) return void 0;
|
|
14885
|
+
if (!Array.isArray(entry.stages) || entry.stages.length === 0) return false;
|
|
14886
|
+
return true;
|
|
14887
|
+
}
|
|
14520
14888
|
async function secretsPreflight(deps, opts) {
|
|
14521
14889
|
const repo = await targetRepo(deps, opts);
|
|
14522
14890
|
const qs = new URLSearchParams({ repo }).toString();
|
|
@@ -14537,9 +14905,23 @@ async function secretsPreflight(deps, opts) {
|
|
|
14537
14905
|
}
|
|
14538
14906
|
const { secrets } = await res.json();
|
|
14539
14907
|
const present = new Set((secrets ?? []).map((s) => s.key));
|
|
14540
|
-
const missing = opts.required.filter(
|
|
14541
|
-
|
|
14542
|
-
|
|
14908
|
+
const missing = opts.required.filter((key) => {
|
|
14909
|
+
const perStage = keyDeclaredPerStage(opts.declaredSecrets, key);
|
|
14910
|
+
if (key.includes("/")) {
|
|
14911
|
+
return !present.has(key);
|
|
14912
|
+
}
|
|
14913
|
+
if (perStage === false) {
|
|
14914
|
+
return !present.has(key);
|
|
14915
|
+
}
|
|
14916
|
+
if (perStage === true) {
|
|
14917
|
+
return !present.has(stageKey2(opts.stage, key));
|
|
14918
|
+
}
|
|
14919
|
+
return !present.has(stageKey2(opts.stage, key)) && !present.has(key);
|
|
14920
|
+
});
|
|
14921
|
+
if (opts.json) {
|
|
14922
|
+
deps.log(JSON.stringify({ ok: missing.length === 0, stage: opts.stage, required: opts.required, missing, present: Array.from(present) }, null, 2));
|
|
14923
|
+
return missing.length === 0;
|
|
14924
|
+
}
|
|
14543
14925
|
if (missing.length) {
|
|
14544
14926
|
deps.log(`missing ${missing.map((key) => key.includes("/") ? key : `${stageKey2(opts.stage, key)} (and no stageless ${key})`).join(", ")}`);
|
|
14545
14927
|
return false;
|
|
@@ -14764,9 +15146,6 @@ async function secretsImportRailsCredentials(deps, opts) {
|
|
|
14764
15146
|
}
|
|
14765
15147
|
return true;
|
|
14766
15148
|
}
|
|
14767
|
-
async function secretsEdit(deps, key, opts) {
|
|
14768
|
-
return secretsSet(deps, key, opts);
|
|
14769
|
-
}
|
|
14770
15149
|
async function secretsRemove(deps, key, opts) {
|
|
14771
15150
|
if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
14772
15151
|
const repo = await targetRepo(deps, opts);
|
|
@@ -14862,8 +15241,12 @@ async function secretsCopy(deps, opts) {
|
|
|
14862
15241
|
}
|
|
14863
15242
|
return true;
|
|
14864
15243
|
}
|
|
15244
|
+
function resolveSpawnTarget(command, args, platform2 = process.platform) {
|
|
15245
|
+
return platform2 === "win32" ? { cmd: "cmd.exe", args: ["/d", "/s", "/c", command, ...args] } : { cmd: command, args };
|
|
15246
|
+
}
|
|
14865
15247
|
function defaultSpawn(command, args, env) {
|
|
14866
|
-
const
|
|
15248
|
+
const target = resolveSpawnTarget(command, args);
|
|
15249
|
+
const r = (0, import_node_child_process9.spawnSync)(target.cmd, target.args, { stdio: "inherit", env });
|
|
14867
15250
|
if (r.error) throw r.error;
|
|
14868
15251
|
return r.status ?? 1;
|
|
14869
15252
|
}
|
|
@@ -14923,8 +15306,8 @@ async function secretsUse(deps, key, opts) {
|
|
|
14923
15306
|
}
|
|
14924
15307
|
|
|
14925
15308
|
// src/secrets-commands.ts
|
|
14926
|
-
var
|
|
14927
|
-
var
|
|
15309
|
+
var import_node_fs16 = require("node:fs");
|
|
15310
|
+
var import_node_path17 = require("node:path");
|
|
14928
15311
|
var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
|
|
14929
15312
|
var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
|
|
14930
15313
|
var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
|
|
@@ -14932,18 +15315,18 @@ function collectMap(value, previous = []) {
|
|
|
14932
15315
|
return [...previous, value];
|
|
14933
15316
|
}
|
|
14934
15317
|
async function decryptRailsCredentials(input) {
|
|
14935
|
-
const appDir = (0,
|
|
15318
|
+
const appDir = (0, import_node_path17.resolve)(input.appDir ?? process.cwd());
|
|
14936
15319
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
14937
15320
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
14938
|
-
const credentialsPath = (0,
|
|
14939
|
-
const masterKeyPath = (0,
|
|
15321
|
+
const credentialsPath = (0, import_node_path17.resolve)(appDir, credentialsFile);
|
|
15322
|
+
const masterKeyPath = (0, import_node_path17.resolve)(appDir, masterKeyFile);
|
|
14940
15323
|
const env = {
|
|
14941
15324
|
...process.env,
|
|
14942
15325
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
14943
15326
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
14944
15327
|
};
|
|
14945
|
-
if ((0,
|
|
14946
|
-
env.RAILS_MASTER_KEY = (0,
|
|
15328
|
+
if ((0, import_node_fs16.existsSync)(masterKeyPath)) {
|
|
15329
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs16.readFileSync)(masterKeyPath, "utf8").trim();
|
|
14947
15330
|
}
|
|
14948
15331
|
const script = [
|
|
14949
15332
|
'require "json"',
|
|
@@ -14997,8 +15380,8 @@ async function withSecrets(run) {
|
|
|
14997
15380
|
}
|
|
14998
15381
|
function registerSecretsCommands(program3) {
|
|
14999
15382
|
const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
|
|
15000
|
-
secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsWhere(d, o)));
|
|
15001
|
-
secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
|
|
15383
|
+
secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
|
|
15384
|
+
secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
|
|
15002
15385
|
secrets.command("find <intent>").description("resolve a plain-language intent to the canonical secret(s) + the exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
|
|
15003
15386
|
if (!await secretsFind(d, intent, o)) process.exitCode = 1;
|
|
15004
15387
|
}));
|
|
@@ -15009,13 +15392,13 @@ function registerSecretsCommands(program3) {
|
|
|
15009
15392
|
secrets.command("org-catalog").description("MASTER-ONLY: set the _org/<provider>/* catalog declarations from a JSON file (#2244)").requiredOption("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } }').action((o) => withSecrets(async (d) => {
|
|
15010
15393
|
let body;
|
|
15011
15394
|
try {
|
|
15012
|
-
body = (0,
|
|
15395
|
+
body = (0, import_node_fs16.readFileSync)((0, import_node_path17.resolve)(o.file), "utf8");
|
|
15013
15396
|
} catch (e) {
|
|
15014
15397
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
15015
15398
|
}
|
|
15016
15399
|
if (!await secretsOrgCatalogSet(d, body)) process.exitCode = 1;
|
|
15017
15400
|
}));
|
|
15018
|
-
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
|
|
15401
|
+
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--json", "machine-readable output").action(async (o) => {
|
|
15019
15402
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
15020
15403
|
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
15021
15404
|
}
|
|
@@ -15036,7 +15419,7 @@ function registerSecretsCommands(program3) {
|
|
|
15036
15419
|
if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
|
|
15037
15420
|
d.err('secrets preflight: requiredRuntimeSecrets is unset: treating as an empty contract (no required keys). Declare an explicit {"dev":[],"rc":[],"main":[]} in registry META to silence this warning.');
|
|
15038
15421
|
}
|
|
15039
|
-
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
|
|
15422
|
+
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required, json: o.json, declaredSecrets: meta?.secrets });
|
|
15040
15423
|
if (!ok) process.exitCode = 1;
|
|
15041
15424
|
});
|
|
15042
15425
|
secrets.command("get <key>").description("print one secret value, RAW \u2014 MASTER-ONLY (WS6 #2184); non-master: `secrets use` to consume keyless or `secrets verify` to validate").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "vault slug override for org-infra namespaces (cloudflare, shared, \u2026) \u2014 master-gated").action((key, o) => withSecrets(async (d) => {
|
|
@@ -15051,14 +15434,12 @@ function registerSecretsCommands(program3) {
|
|
|
15051
15434
|
const ok = await secretsVerify(d, key, o);
|
|
15052
15435
|
if (!ok) process.exitCode = 1;
|
|
15053
15436
|
}));
|
|
15054
|
-
|
|
15437
|
+
const setHandler = (key, o) => withSecrets(async (d) => {
|
|
15055
15438
|
const ok = await secretsSet(d, key, o);
|
|
15056
15439
|
if (!ok) process.exitCode = 1;
|
|
15057
|
-
})
|
|
15058
|
-
secrets.command("
|
|
15059
|
-
|
|
15060
|
-
if (!ok) process.exitCode = 1;
|
|
15061
|
-
}));
|
|
15440
|
+
});
|
|
15441
|
+
secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
|
|
15442
|
+
secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
|
|
15062
15443
|
secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
|
|
15063
15444
|
const stages = ["dev", "rc", "main"];
|
|
15064
15445
|
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
@@ -15084,7 +15465,7 @@ function registerSecretsCommands(program3) {
|
|
|
15084
15465
|
{
|
|
15085
15466
|
...d,
|
|
15086
15467
|
decryptRailsCredentials,
|
|
15087
|
-
removeFile: (path2) => (0,
|
|
15468
|
+
removeFile: (path2) => (0, import_node_fs16.unlinkSync)((0, import_node_path17.resolve)(o.appDir ?? process.cwd(), path2))
|
|
15088
15469
|
},
|
|
15089
15470
|
{
|
|
15090
15471
|
repo: o.repo,
|
|
@@ -15173,10 +15554,10 @@ function registerEdgeCommands(program3) {
|
|
|
15173
15554
|
}
|
|
15174
15555
|
|
|
15175
15556
|
// src/doctor-run.ts
|
|
15176
|
-
var
|
|
15557
|
+
var import_node_fs20 = require("node:fs");
|
|
15177
15558
|
var import_node_child_process11 = require("node:child_process");
|
|
15178
15559
|
var import_promises2 = require("node:fs/promises");
|
|
15179
|
-
var
|
|
15560
|
+
var import_node_path20 = require("node:path");
|
|
15180
15561
|
var import_node_os7 = require("node:os");
|
|
15181
15562
|
|
|
15182
15563
|
// src/plugin-guard.ts
|
|
@@ -15199,13 +15580,14 @@ function buildGuardSessionStartLine(state, opts = {}) {
|
|
|
15199
15580
|
|
|
15200
15581
|
// src/cursor-plugin-seed.ts
|
|
15201
15582
|
var import_node_child_process10 = require("node:child_process");
|
|
15202
|
-
var
|
|
15583
|
+
var import_node_fs17 = require("node:fs");
|
|
15203
15584
|
var import_node_os6 = require("node:os");
|
|
15204
|
-
var
|
|
15585
|
+
var import_node_path18 = require("node:path");
|
|
15205
15586
|
var import_node_util7 = require("node:util");
|
|
15206
15587
|
|
|
15207
15588
|
// src/doctor.ts
|
|
15208
15589
|
var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
|
|
15590
|
+
var GITHUB_AUTH_LABEL = "GitHub auth identity (gh ops)";
|
|
15209
15591
|
var AWS_CROSS_ACCOUNT_LABEL = "AWS cross-account identity (master-agent audits)";
|
|
15210
15592
|
var AWS_CROSS_ACCOUNT_FIX = "use a non-root IAM user/session profile for master-agent AWS checks; set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), then verify `aws sts get-caller-identity` does not end in :root";
|
|
15211
15593
|
var MMI_AGENTIC_ONBOARDING_GUIDE = {
|
|
@@ -15219,7 +15601,7 @@ function buildGithubAuthCheck(input) {
|
|
|
15219
15601
|
const ok = Boolean(input.login?.trim());
|
|
15220
15602
|
return {
|
|
15221
15603
|
ok,
|
|
15222
|
-
label:
|
|
15604
|
+
label: GITHUB_AUTH_LABEL,
|
|
15223
15605
|
fix: input.ghInstalled ? GH_PROJECT_LOGIN_FIX : `install GitHub CLI (https://cli.github.com), then: ${GH_PROJECT_LOGIN_FIX.replace(/^run: /, "")}`
|
|
15224
15606
|
};
|
|
15225
15607
|
}
|
|
@@ -15397,6 +15779,136 @@ function buildRepoLocalWorktreeCheck(input) {
|
|
|
15397
15779
|
fix: REPO_LOCAL_WORKTREE_FIX
|
|
15398
15780
|
};
|
|
15399
15781
|
}
|
|
15782
|
+
var LOCKFILE_OPTIONALS_LABEL = "npm lockfile keeps optionalDependencies across platforms (Windows lockfile drop #2594)";
|
|
15783
|
+
var LOCKFILE_OPTIONALS_FIX = "regenerate the lockfile without platform filtering: `rm package-lock.json && npm install` on a machine that resolves every optionalDependency, then commit it (a Windows-only lockfile drops Linux-only optionals on CI)";
|
|
15784
|
+
function buildLockfileOptionalsCheck(input) {
|
|
15785
|
+
const base = {
|
|
15786
|
+
ok: true,
|
|
15787
|
+
label: LOCKFILE_OPTIONALS_LABEL,
|
|
15788
|
+
fix: LOCKFILE_OPTIONALS_FIX,
|
|
15789
|
+
missingOptionals: []
|
|
15790
|
+
};
|
|
15791
|
+
if (!input.isOrgRepo) return base;
|
|
15792
|
+
const optionalDeps = input.packageJsonOptionalDeps ?? {};
|
|
15793
|
+
const packages = input.lockfilePackages ?? {};
|
|
15794
|
+
const missingOptionals = Object.keys(optionalDeps).filter((name) => !(`node_modules/${name}` in packages));
|
|
15795
|
+
if (missingOptionals.length === 0) return base;
|
|
15796
|
+
return {
|
|
15797
|
+
...base,
|
|
15798
|
+
ok: false,
|
|
15799
|
+
missingOptionals,
|
|
15800
|
+
detail: `${missingOptionals.length} optionalDependency entr${missingOptionals.length === 1 ? "y" : "ies"} missing from the lockfile packages map: ${missingOptionals.join(", ")}`
|
|
15801
|
+
};
|
|
15802
|
+
}
|
|
15803
|
+
var FILE_ENCODING_LABEL = "managed file encoding (UTF-8, no BOM, no mojibake)";
|
|
15804
|
+
var FILE_ENCODING_FIX = 're-save the flagged file(s) as UTF-8 without BOM (VS Code: status-bar encoding button \u2192 "Save with Encoding" \u2192 UTF-8), and set `PYTHONUTF8=1` for Python tooling on Windows so it never falls back to cp1252';
|
|
15805
|
+
function inspectFileEncoding(bytes) {
|
|
15806
|
+
const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
|
|
15807
|
+
let invalidUtf8 = false;
|
|
15808
|
+
let replacementChars = false;
|
|
15809
|
+
try {
|
|
15810
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
15811
|
+
replacementChars = text.includes("\uFFFD");
|
|
15812
|
+
} catch {
|
|
15813
|
+
invalidUtf8 = true;
|
|
15814
|
+
replacementChars = new TextDecoder("utf-8", { fatal: false }).decode(bytes).includes("\uFFFD");
|
|
15815
|
+
}
|
|
15816
|
+
return { bom, invalidUtf8, replacementChars };
|
|
15817
|
+
}
|
|
15818
|
+
function buildManagedFileEncodingCheck(input) {
|
|
15819
|
+
const base = {
|
|
15820
|
+
ok: true,
|
|
15821
|
+
label: FILE_ENCODING_LABEL,
|
|
15822
|
+
fix: FILE_ENCODING_FIX,
|
|
15823
|
+
findings: []
|
|
15824
|
+
};
|
|
15825
|
+
if (!input.isOrgRepo) return base;
|
|
15826
|
+
const defective = input.findings.filter((f) => f.bom || f.invalidUtf8 || f.replacementChars);
|
|
15827
|
+
if (defective.length === 0) return base;
|
|
15828
|
+
return {
|
|
15829
|
+
...base,
|
|
15830
|
+
ok: false,
|
|
15831
|
+
severityOverride: "advisory",
|
|
15832
|
+
findings: defective,
|
|
15833
|
+
detail: `${defective.length} managed file(s) not clean UTF-8: ${defective.map((f) => f.path).join(", ")}`
|
|
15834
|
+
};
|
|
15835
|
+
}
|
|
15836
|
+
var LOCAL_BEHIND_ORIGIN_LABEL = "local release branches current with origin (development/main/rc)";
|
|
15837
|
+
var LOCAL_BEHIND_ORIGIN_FIX = "run `git fetch origin && git switch <branch> && git pull --ff-only` for each flagged branch (purely behind, so the fast-forward is safe)";
|
|
15838
|
+
var RELEASE_TRACKING_BRANCHES = /* @__PURE__ */ new Set(["development", "main", "master", "rc", "release"]);
|
|
15839
|
+
function isReleaseTrackingBranch(name) {
|
|
15840
|
+
if (!name) return false;
|
|
15841
|
+
const base = name.split("/").pop() ?? name;
|
|
15842
|
+
if (RELEASE_TRACKING_BRANCHES.has(base)) return true;
|
|
15843
|
+
return name.split("/")[0] === "release";
|
|
15844
|
+
}
|
|
15845
|
+
function buildStaleLocalBranchCheck(input) {
|
|
15846
|
+
const base = {
|
|
15847
|
+
ok: true,
|
|
15848
|
+
label: LOCAL_BEHIND_ORIGIN_LABEL,
|
|
15849
|
+
fix: LOCAL_BEHIND_ORIGIN_FIX,
|
|
15850
|
+
stale: []
|
|
15851
|
+
};
|
|
15852
|
+
if (!input.isOrgRepo) return base;
|
|
15853
|
+
const stale = input.branches.filter((b) => isReleaseTrackingBranch(b.name) && b.behind > 0 && b.ahead === 0).map((b) => ({ name: b.name, behind: b.behind }));
|
|
15854
|
+
if (stale.length === 0) return base;
|
|
15855
|
+
return {
|
|
15856
|
+
...base,
|
|
15857
|
+
ok: false,
|
|
15858
|
+
severityOverride: "advisory",
|
|
15859
|
+
stale,
|
|
15860
|
+
detail: `${stale.length} release branch(es) behind origin: ${stale.map((s) => `${s.name} (-${s.behind})`).join(", ")}`
|
|
15861
|
+
};
|
|
15862
|
+
}
|
|
15863
|
+
var REDACTOR_LIVENESS_LABEL = "secret-redact hook liveness (PostToolUse redactor crashes)";
|
|
15864
|
+
var REDACTOR_LIVENESS_FIX = 'grep the hook activity log for \'"script":"secret-redact","outcome":"failed"\' (path: `git rev-parse --git-path mmi-runtime/hooks/activity.jsonl`); reinstall the MMI plugin or run `mmi-cli doctor --apply` to repair, then reopen the session \u2014 the redactor stayed fail-open so tool calls proceeded, but secrets may have passed through unredacted while it was down';
|
|
15865
|
+
function summarizeRedactorFailures(lines) {
|
|
15866
|
+
const parsed = [];
|
|
15867
|
+
for (const raw of lines) {
|
|
15868
|
+
const trimmed = raw.trim();
|
|
15869
|
+
if (!trimmed) continue;
|
|
15870
|
+
try {
|
|
15871
|
+
parsed.push(JSON.parse(trimmed));
|
|
15872
|
+
} catch {
|
|
15873
|
+
}
|
|
15874
|
+
}
|
|
15875
|
+
let sessionStartIdx = -1;
|
|
15876
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
15877
|
+
if (parsed[i].event === "SessionStart") sessionStartIdx = i;
|
|
15878
|
+
}
|
|
15879
|
+
const start = sessionStartIdx >= 0 ? sessionStartIdx : 0;
|
|
15880
|
+
let failedCount = 0;
|
|
15881
|
+
let lastFailedTs;
|
|
15882
|
+
let lastFailedAction;
|
|
15883
|
+
for (let i = start; i < parsed.length; i += 1) {
|
|
15884
|
+
const e = parsed[i];
|
|
15885
|
+
if (e.script === "secret-redact" && e.outcome === "failed") {
|
|
15886
|
+
failedCount += 1;
|
|
15887
|
+
lastFailedTs = e.ts;
|
|
15888
|
+
lastFailedAction = e.action;
|
|
15889
|
+
}
|
|
15890
|
+
}
|
|
15891
|
+
return { failedCount, lastFailedTs, lastFailedAction };
|
|
15892
|
+
}
|
|
15893
|
+
function buildRedactorLivenessCheck(input) {
|
|
15894
|
+
const base = {
|
|
15895
|
+
ok: true,
|
|
15896
|
+
label: REDACTOR_LIVENESS_LABEL,
|
|
15897
|
+
fix: REDACTOR_LIVENESS_FIX,
|
|
15898
|
+
failedCount: 0
|
|
15899
|
+
};
|
|
15900
|
+
if (!input.isOrgRepo) return base;
|
|
15901
|
+
if (input.failedCount === 0) return base;
|
|
15902
|
+
const when = input.lastFailedTs ? ` (most recent ${input.lastFailedTs})` : "";
|
|
15903
|
+
const reason = input.lastFailedAction ? `: ${input.lastFailedAction}` : "";
|
|
15904
|
+
return {
|
|
15905
|
+
ok: false,
|
|
15906
|
+
label: REDACTOR_LIVENESS_LABEL,
|
|
15907
|
+
fix: `secret-redact crashed ${input.failedCount} time(s) this session${when}${reason}. ${REDACTOR_LIVENESS_FIX}`,
|
|
15908
|
+
failedCount: input.failedCount,
|
|
15909
|
+
lastFailedTs: input.lastFailedTs
|
|
15910
|
+
};
|
|
15911
|
+
}
|
|
15400
15912
|
var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
|
|
15401
15913
|
var SCRATCH_GC_FIX = "run `mmi-cli gc --scratch --dry-run` to inspect; run `mmi-cli gc --scratch --apply` to prune safe scratch; for kept plans run `mmi-cli northstar status` or `mmi-cli northstar sync --wait` first";
|
|
15402
15914
|
function buildScratchGcCheck(plan) {
|
|
@@ -15855,7 +16367,16 @@ function buildDoctorJsonPayload(input) {
|
|
|
15855
16367
|
return {
|
|
15856
16368
|
ok: doctorExitCode(input.checks) === 0,
|
|
15857
16369
|
hasAdvisories: input.checks.some((c) => !c.ok && isAdvisoryDoctorCheck(c)),
|
|
15858
|
-
checks: input.checks
|
|
16370
|
+
checks: input.checks.map((check) => {
|
|
16371
|
+
const metadata = doctorMetadataForCheck(check);
|
|
16372
|
+
return {
|
|
16373
|
+
...check,
|
|
16374
|
+
...metadata,
|
|
16375
|
+
label: check.label,
|
|
16376
|
+
metadata,
|
|
16377
|
+
durationMs: check.durationMs ?? 0
|
|
16378
|
+
};
|
|
16379
|
+
}),
|
|
15859
16380
|
updateReport: input.updateReport,
|
|
15860
16381
|
...input.resources.length ? { resources: input.resources } : {}
|
|
15861
16382
|
};
|
|
@@ -15914,6 +16435,25 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
15914
16435
|
staleSurfaces: stale
|
|
15915
16436
|
};
|
|
15916
16437
|
}
|
|
16438
|
+
var CLI_VERSION_DRIFT_LABEL = "mmi-cli version drift (installed vs latest published)";
|
|
16439
|
+
var CLI_VERSION_DRIFT_FIX = "run: mmi-cli doctor --apply --no-repo-writes (or npm install -g @mutmutco/cli@latest)";
|
|
16440
|
+
function buildCliVersionDriftCheck(input) {
|
|
16441
|
+
const base = {
|
|
16442
|
+
ok: true,
|
|
16443
|
+
label: CLI_VERSION_DRIFT_LABEL,
|
|
16444
|
+
fix: CLI_VERSION_DRIFT_FIX,
|
|
16445
|
+
currentVersion: input.currentVersion,
|
|
16446
|
+
releasedVersion: input.releasedVersion
|
|
16447
|
+
};
|
|
16448
|
+
if (!input.releasedVersion || !isSemverVersion(input.releasedVersion) || !isSemverVersion(input.currentVersion)) return base;
|
|
16449
|
+
if (compareVersions(input.currentVersion, input.releasedVersion) >= 0) return base;
|
|
16450
|
+
return {
|
|
16451
|
+
...base,
|
|
16452
|
+
ok: false,
|
|
16453
|
+
label: `${CLI_VERSION_DRIFT_LABEL}: ${input.currentVersion} < ${input.releasedVersion}`,
|
|
16454
|
+
fix: `${CLI_VERSION_DRIFT_FIX} (installed ${input.currentVersion} is behind published ${input.releasedVersion})`
|
|
16455
|
+
};
|
|
16456
|
+
}
|
|
15917
16457
|
var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
|
|
15918
16458
|
function buildOpencodeVersionCheck(input) {
|
|
15919
16459
|
const fix = pluginRecoveryFix("opencode");
|
|
@@ -16211,13 +16751,13 @@ function buildCursorHookCliCheck(input) {
|
|
|
16211
16751
|
return base;
|
|
16212
16752
|
}
|
|
16213
16753
|
var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
|
|
16754
|
+
var HUB_COMPAT_LABEL = "Hub compatibility (client version vs Hub minimum)";
|
|
16214
16755
|
function buildHubCompatCheck(input) {
|
|
16215
|
-
const label = "Hub compatibility (client version vs Hub minimum)";
|
|
16216
16756
|
const min = input.versionInfo?.minClientVersion;
|
|
16217
16757
|
if (!input.isOrgRepo || !min || !parseSemver(min) || !parseSemver(input.installedVersion)) {
|
|
16218
|
-
return { ok: true, label, fix: HUB_COMPAT_FIX };
|
|
16758
|
+
return { ok: true, label: HUB_COMPAT_LABEL, fix: HUB_COMPAT_FIX };
|
|
16219
16759
|
}
|
|
16220
|
-
return { ok: versionAtLeast(input.installedVersion, min), label: `${
|
|
16760
|
+
return { ok: versionAtLeast(input.installedVersion, min), label: `${HUB_COMPAT_LABEL}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
|
|
16221
16761
|
}
|
|
16222
16762
|
var HUB_DEPLOY_FRESHNESS_LABEL = "Hub deploy freshness (x-hub-version vs CLI / release)";
|
|
16223
16763
|
var HUB_DEPLOY_FRESHNESS_FIX = "prod Hub updates on /release (MMI-Hub is direct-track); lag vs development is expected between releases";
|
|
@@ -16252,8 +16792,6 @@ function buildHubDeployFreshnessCheck(input) {
|
|
|
16252
16792
|
staleAgainst
|
|
16253
16793
|
};
|
|
16254
16794
|
}
|
|
16255
|
-
var PLAYWRIGHT_MCP_VISION_CAP_LABEL = "Playwright MCP vision caps (--caps=vision prohibited)";
|
|
16256
|
-
var PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL = "Playwright MCP output dir (use tmp/playwright-mcp)";
|
|
16257
16795
|
function textHasPlaywrightMcp(content) {
|
|
16258
16796
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
16259
16797
|
return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
|
|
@@ -16311,7 +16849,7 @@ function buildSelfUpdateHaltPayload(input) {
|
|
|
16311
16849
|
checks: input.checks
|
|
16312
16850
|
};
|
|
16313
16851
|
}
|
|
16314
|
-
var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE =
|
|
16852
|
+
var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 2;
|
|
16315
16853
|
var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
|
|
16316
16854
|
function selfUpdateHaltSentinelLine(input) {
|
|
16317
16855
|
const to = input.report.releasedVersion ?? "latest";
|
|
@@ -16321,8 +16859,11 @@ var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
|
|
|
16321
16859
|
function buildSelfUpdateReexecArgs(opts) {
|
|
16322
16860
|
const args = ["doctor"];
|
|
16323
16861
|
if (opts.banner) args.push("--banner");
|
|
16862
|
+
if (opts.fast) args.push("--fast");
|
|
16324
16863
|
if (opts.preflight) args.push("--preflight");
|
|
16325
16864
|
if (opts.verbose) args.push("--verbose");
|
|
16865
|
+
if (opts.ci) args.push("--ci");
|
|
16866
|
+
if (opts.strictAdvisory) args.push("--strict-advisory");
|
|
16326
16867
|
if (opts.guide) args.push("--guide");
|
|
16327
16868
|
if (opts.json) args.push("--json");
|
|
16328
16869
|
if (opts.apply) args.push("--apply");
|
|
@@ -16343,28 +16884,12 @@ function preflightOutcome(input) {
|
|
|
16343
16884
|
function pluginAutonomousHaltLine(reloadHint) {
|
|
16344
16885
|
return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
|
|
16345
16886
|
}
|
|
16346
|
-
var DOCTOR_EXTENDED_CHECK_LABELS = /* @__PURE__ */ new Set([
|
|
16347
|
-
AWS_CROSS_ACCOUNT_LABEL,
|
|
16348
|
-
HUB_DEPLOY_FRESHNESS_LABEL,
|
|
16349
|
-
PLAYWRIGHT_MCP_VISION_CAP_LABEL,
|
|
16350
|
-
PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
|
|
16351
|
-
BROWSER_ARTIFACTS_LABEL,
|
|
16352
|
-
SCRATCH_GC_LABEL,
|
|
16353
|
-
GIT_GC_LABEL,
|
|
16354
|
-
OPENCODE_VERSION_LABEL,
|
|
16355
|
-
OPENCODE_DESKTOP_BOOTSTRAP_LABEL
|
|
16356
|
-
]);
|
|
16357
|
-
function isDoctorExtendedCheck(label) {
|
|
16358
|
-
if (DOCTOR_EXTENDED_CHECK_LABELS.has(label)) return true;
|
|
16359
|
-
if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return true;
|
|
16360
|
-
if (label.startsWith("@mutmutco design-system") || label.startsWith("@mutmutco registry components")) return true;
|
|
16361
|
-
return false;
|
|
16362
|
-
}
|
|
16363
16887
|
function isAdvisoryDoctorCheck(check) {
|
|
16364
16888
|
if (check.severityOverride) return check.severityOverride === "advisory";
|
|
16365
|
-
return
|
|
16889
|
+
return (check.metadata ?? doctorMetadataForLabel(check.label)).severity === "advisory";
|
|
16366
16890
|
}
|
|
16367
|
-
function doctorExitCode(checks) {
|
|
16891
|
+
function doctorExitCode(checks, opts = {}) {
|
|
16892
|
+
if (opts.doctorError) return 2;
|
|
16368
16893
|
return checks.some((c) => !c.ok && !isAdvisoryDoctorCheck(c)) ? 1 : 0;
|
|
16369
16894
|
}
|
|
16370
16895
|
function renderPluginUpdateReportStaleOnly(report, allChecksPass = false) {
|
|
@@ -16390,6 +16915,7 @@ function doctorVerdictLine(input) {
|
|
|
16390
16915
|
const gaps = input.checks.filter((c) => !c.ok);
|
|
16391
16916
|
const hardGaps = gaps.filter((c) => !isAdvisoryDoctorCheck(c));
|
|
16392
16917
|
const softGaps = gaps.filter((c) => isAdvisoryDoctorCheck(c));
|
|
16918
|
+
const skipSuffix = input.skippedSlowChecks ? ` \xB7 skipped ${input.skippedSlowChecks} slow checks` : "";
|
|
16393
16919
|
if (input.shouldApply) {
|
|
16394
16920
|
if (hardGaps.length > 0) {
|
|
16395
16921
|
return `\u2717 ${hardGaps.length} repair${hardGaps.length === 1 ? "" : "s"} failed \u2014 see the items above.`;
|
|
@@ -16403,19 +16929,30 @@ function doctorVerdictLine(input) {
|
|
|
16403
16929
|
}
|
|
16404
16930
|
const attention = gaps.length;
|
|
16405
16931
|
if (attention > 0) {
|
|
16406
|
-
return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal.`;
|
|
16932
|
+
return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal${skipSuffix}.`;
|
|
16407
16933
|
}
|
|
16408
|
-
return
|
|
16934
|
+
return `\u2713 healthy \xB7 ${input.checks.length} checks \xB7 0 gaps${skipSuffix}`;
|
|
16935
|
+
}
|
|
16936
|
+
function doctorCheckSummaryLine(check) {
|
|
16937
|
+
const meta = doctorMetadataForCheck(check);
|
|
16938
|
+
return ` ${doctorCheckGlyph(check)} ${meta.id} \u2014 ${meta.protects.join("; ")} \u2014 ${check.fix}`;
|
|
16409
16939
|
}
|
|
16410
16940
|
function doctorHumanLines(input) {
|
|
16411
16941
|
const gaps = input.checks.filter((c) => !c.ok);
|
|
16942
|
+
const healthyCount = input.checks.length - gaps.length;
|
|
16412
16943
|
const lines = [
|
|
16413
|
-
doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount })
|
|
16414
|
-
"",
|
|
16415
|
-
"Checks",
|
|
16416
|
-
...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${c.label}`)
|
|
16944
|
+
doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount, skippedSlowChecks: input.skippedSlowChecks })
|
|
16417
16945
|
];
|
|
16418
|
-
if (gaps.length > 0) {
|
|
16946
|
+
if (gaps.length > 0 || input.verbose) {
|
|
16947
|
+
lines.push("", "Checks");
|
|
16948
|
+
if (input.verbose) {
|
|
16949
|
+
lines.push(...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${doctorMetadataForCheck(c).id} \u2014 ${c.label}`));
|
|
16950
|
+
} else {
|
|
16951
|
+
lines.push(...gaps.map(doctorCheckSummaryLine));
|
|
16952
|
+
if (healthyCount > 0) lines.push(` \u2713 ${healthyCount} healthy check${healthyCount === 1 ? "" : "s"} collapsed`);
|
|
16953
|
+
}
|
|
16954
|
+
}
|
|
16955
|
+
if (gaps.length > 0 && input.verbose) {
|
|
16419
16956
|
lines.push("", input.shouldApply ? "Repairs" : "Will fix on --apply");
|
|
16420
16957
|
for (const c of gaps) {
|
|
16421
16958
|
const mark = input.shouldApply ? isAdvisoryDoctorCheck(c) ? "\u26A0" : "\u2717" : "\u2192";
|
|
@@ -16425,10 +16962,10 @@ function doctorHumanLines(input) {
|
|
|
16425
16962
|
if (input.shouldApply && input.pluginReloadRequired) {
|
|
16426
16963
|
lines.push("", `\u21BB ${input.reloadHint ?? "reload"} so the healed plugin/MCP installs load.`);
|
|
16427
16964
|
}
|
|
16428
|
-
const stale = renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0);
|
|
16965
|
+
const stale = input.verbose || gaps.length > 0 ? renderPluginUpdateReportStaleOnly(input.updateReport, gaps.length === 0) : [];
|
|
16429
16966
|
if (stale.length) {
|
|
16430
16967
|
lines.push("", ...stale);
|
|
16431
|
-
} else if (!input.verbose) {
|
|
16968
|
+
} else if (!input.verbose && gaps.length > 0) {
|
|
16432
16969
|
lines.push("", DOCTOR_VERBOSE_HINT);
|
|
16433
16970
|
}
|
|
16434
16971
|
if (!input.verbose) return lines;
|
|
@@ -16476,6 +17013,100 @@ function doctorAuditLines(input) {
|
|
|
16476
17013
|
return lines;
|
|
16477
17014
|
}
|
|
16478
17015
|
var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
|
|
17016
|
+
var MMI_CLI_ON_PATH_LABEL = "mmi-cli on PATH";
|
|
17017
|
+
var PLUGIN_CLONE_LABEL = "plugin git clone (SSH\u2192HTTPS rewrite)";
|
|
17018
|
+
var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
|
|
17019
|
+
var CURSOR_ENABLED_PIN_LABEL = "Cursor Team Marketplace enabled plugin pin";
|
|
17020
|
+
var DOCTOR_CHECK_METADATA = [
|
|
17021
|
+
{ id: "github.auth", category: "auth", severity: "hard", cost: "fast", heal: "none", label: GITHUB_AUTH_LABEL, protects: ["GitHub issue, PR, project, and registry operations use a resolved viewer identity"] },
|
|
17022
|
+
{ id: "cli.path", category: "repo", severity: "hard", cost: "fast", heal: "auto", label: MMI_CLI_ON_PATH_LABEL, protects: ["Session hooks and surface commands can invoke mmi-cli"] },
|
|
17023
|
+
{ id: "hub.compat", category: "hub", severity: "hard", cost: "fast", heal: "auto", label: HUB_COMPAT_LABEL, protects: ["Client stays above the Hub minimum version floor"] },
|
|
17024
|
+
{ id: "hub.deploy-freshness", category: "hub", severity: "advisory", cost: "slow", heal: "none", label: HUB_DEPLOY_FRESHNESS_LABEL, protects: ["Operators can see prod Hub lag against release and local CLI versions"] },
|
|
17025
|
+
{ id: "cli.version-drift", category: "repo", severity: "advisory", cost: "slow", heal: "none", label: CLI_VERSION_DRIFT_LABEL, protects: ["The installed mmi-cli is not behind the npm-published version (stale installs miss commands)"] },
|
|
17026
|
+
{ id: "aws.cross-account", category: "aws", severity: "advisory", cost: "slow", heal: "none", label: AWS_CROSS_ACCOUNT_LABEL, protects: ["Master-agent AWS audits do not start from root credentials"] },
|
|
17027
|
+
{ id: "plugin.clone-rewrite", category: "plugin", severity: "hard", cost: "fast", heal: "none", label: PLUGIN_CLONE_LABEL, protects: ["Plugin marketplace clones can use the HTTPS rewrite expected on this machine"] },
|
|
17028
|
+
{ id: "plugin.install-record", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: PLUGIN_LABEL, protects: ["Claude/Codex plugin loader sees mmi@mutmutco installed for this project"] },
|
|
17029
|
+
{ id: "plugin.legacy-id", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: LEGACY_PLUGIN_ID_LABEL, protects: ["Legacy mmi@mmi rows cannot mask the canonical mmi@mutmutco install"] },
|
|
17030
|
+
{ id: "plugin.config-drift", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: PLUGIN_DRIFT_LABEL, protects: ["Duplicate or stale plugin rows collapse to one current user-scope row"] },
|
|
17031
|
+
{ id: "plugin.resolvability", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: PLUGIN_RESOLVABILITY_LABEL, protects: ["Marketplace clone and plugin cache both exist for the active surface"] },
|
|
17032
|
+
{ id: "plugin.installed-version", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: INSTALLED_PLUGIN_VERSION_LABEL, protects: ["Claude/Codex installed plugin records reach the latest release"] },
|
|
17033
|
+
{ id: "plugin.cache-cleanup", category: "plugin", severity: "hard", cost: "fast", heal: "auto", label: MMI_PLUGIN_CACHE_CLEANUP_LABEL, protects: ["Stale Claude/Codex cache directories do not shadow current installs"] },
|
|
17034
|
+
{ id: "plugin.nested-cache", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: NESTED_PLUGIN_TREE_LABEL, protects: ["Self-nested plugin cache trees do not exceed host path limits"] },
|
|
17035
|
+
{ id: "codex.active-cache", category: "plugin", severity: "hard", cost: "fast", heal: "surface", label: CODEX_ACTIVE_CACHE_LABEL, protects: ["Codex actually loads the released plugin cache"] },
|
|
17036
|
+
{ id: "session.skill-root", category: "plugin", severity: "advisory", cost: "fast", heal: "none", label: SESSION_SKILL_ROOT_LABEL, protects: ["Running sessions know when a restart is needed to load current skills"] },
|
|
17037
|
+
{ id: "repo.gitignore-block", category: "repo", severity: "hard", cost: "fast", heal: "auto", label: GITIGNORE_BLOCK_LABEL, protects: ["Org-managed ignore entries keep generated and scratch files out of git"] },
|
|
17038
|
+
{ id: "repo.worktree-location", category: "repo", severity: "hard", cost: "fast", heal: "none", label: REPO_LOCAL_WORKTREE_LABEL, protects: ["Worktrees stay in the canonical sibling root"] },
|
|
17039
|
+
{ id: "hooks.redactor-liveness", category: "hooks", severity: "advisory", cost: "fast", heal: "none", label: REDACTOR_LIVENESS_LABEL, protects: ["A crashed secret-redactor cannot silently pass secrets into model context forever"] },
|
|
17040
|
+
{ id: "repo.scratch-gc", category: "repo", severity: "advisory", cost: "slow", heal: "auto", label: SCRATCH_GC_LABEL, protects: ["Scratch, plan, and browser artifact clutter stays bounded"] },
|
|
17041
|
+
{ id: "repo.git-gc", category: "git", severity: "advisory", cost: "slow", heal: "auto", label: GIT_GC_LABEL, protects: ["Merged branches, stale refs, and dead worktree metadata stay bounded"] },
|
|
17042
|
+
{ id: "browser.artifacts", category: "browser", severity: "advisory", cost: "slow", heal: "none", label: BROWSER_ARTIFACTS_LABEL, protects: ["Playwright/browser output stays under tmp/playwright-mcp"] },
|
|
17043
|
+
{ id: "mcp.playwright-registration", category: "browser", severity: "hard", cost: "fast", heal: "auto", label: MCP_RECONCILE_LABEL, protects: ["Org-managed Playwright MCP registrations exist and stay drift-free"] },
|
|
17044
|
+
{ id: "opencode.version", category: "opencode", severity: "hard", cost: "fast", heal: "surface", label: OPENCODE_VERSION_LABEL, protects: ["OpenCode loads the released MMI adapter"] },
|
|
17045
|
+
{ id: "opencode.hook-active", category: "opencode", severity: "hard", cost: "fast", heal: "surface", label: OPENCODE_HOOK_ACTIVE_LABEL, protects: ["OpenCode shell hooks stamp the active MMI adapter surface"] },
|
|
17046
|
+
{ id: "opencode.config-plugin", category: "opencode", severity: "hard", cost: "fast", heal: "auto", label: OPENCODE_CONFIG_PLUGIN_LABEL, protects: ["OpenCode config includes the MMI npm adapter"] },
|
|
17047
|
+
{ id: "opencode.surface-assets", category: "opencode", severity: "hard", cost: "fast", heal: "auto", label: OPENCODE_SURFACE_ASSETS_LABEL, protects: ["OpenCode commands and skills are materialized"] },
|
|
17048
|
+
{ id: "opencode.desktop-bootstrap", category: "opencode", severity: "advisory", cost: "slow", heal: "none", label: OPENCODE_DESKTOP_BOOTSTRAP_LABEL, protects: ["OpenCode Desktop is not pinned to a deleted project bootstrap"] },
|
|
17049
|
+
{ id: "opencode.legacy-config", category: "opencode", severity: "hard", cost: "fast", heal: "none", label: OPENCODE_LEGACY_CONFIG_LABEL, protects: ["Legacy OpenCode config cannot load stale bare plugin ids"] },
|
|
17050
|
+
{ id: "cursor.install", category: "cursor", severity: "hard", cost: "fast", heal: "auto", label: CURSOR_PLUGIN_INSTALL_LABEL, protects: ["Cursor Team Marketplace cache exists and is complete"] },
|
|
17051
|
+
{ id: "cursor.cache-cleanup", category: "cursor", severity: "advisory", cost: "fast", heal: "none", label: CURSOR_PLUGIN_CACHE_CLEANUP_LABEL, protects: ["Old Cursor cache pins are visible without unsafe automatic deletion"] },
|
|
17052
|
+
{ id: "cursor.enabled-pin", category: "cursor", severity: "hard", cost: "fast", heal: "none", label: CURSOR_ENABLED_PIN_LABEL, protects: ["Cursor Team Marketplace enabled pin matches the healthy cache"] },
|
|
17053
|
+
{ id: "cursor.third-party-extensibility", category: "cursor", severity: "hard", cost: "fast", heal: "none", label: CURSOR_THIRD_PARTY_EXTENSIBILITY_LABEL, protects: ["Cursor does not import third-party hooks that break the Team Marketplace plugin"] },
|
|
17054
|
+
{ id: "cursor.hook-cli", category: "cursor", severity: "hard", cost: "fast", heal: "surface", label: CURSOR_HOOK_CLI_LABEL, protects: ["Cursor hooks can reach a bundled or PATH mmi-cli"] },
|
|
17055
|
+
// Incident coverage (#2594) — three real incident classes that previously had zero doctor coverage.
|
|
17056
|
+
{ id: "repo-hygiene.lockfile-platform-optionals", category: "repo-hygiene", severity: "hard", cost: "fast", heal: "surface", label: LOCKFILE_OPTIONALS_LABEL, protects: ["A Windows-generated lockfile does not silently drop Linux-only optionalDependencies before CI"] },
|
|
17057
|
+
{ id: "repo-hygiene.file-encoding", category: "repo-hygiene", severity: "advisory", cost: "slow", heal: "surface", label: FILE_ENCODING_LABEL, protects: ["Managed source and config files stay clean UTF-8 without BOM or mojibake"] },
|
|
17058
|
+
{ id: "repo-hygiene.local-behind-origin", category: "repo-hygiene", severity: "advisory", cost: "slow", heal: "surface", label: LOCAL_BEHIND_ORIGIN_LABEL, protects: ["Local release branches (development/main/rc) do not fall behind origin after a release"] }
|
|
17059
|
+
];
|
|
17060
|
+
var DOCTOR_CHECK_METADATA_BY_LABEL = Object.fromEntries(
|
|
17061
|
+
DOCTOR_CHECK_METADATA.map((meta) => [meta.label, meta])
|
|
17062
|
+
);
|
|
17063
|
+
var DOCTOR_EXTENDED_CHECK_LABELS = new Set(
|
|
17064
|
+
DOCTOR_CHECK_METADATA.filter((meta) => meta.cost === "slow").map((meta) => meta.label)
|
|
17065
|
+
);
|
|
17066
|
+
function derivedDoctorCheckMetadata(label) {
|
|
17067
|
+
if (label.startsWith(HUB_COMPAT_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_COMPAT_LABEL], label };
|
|
17068
|
+
if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return { ...DOCTOR_CHECK_METADATA_BY_LABEL[HUB_DEPLOY_FRESHNESS_LABEL], label };
|
|
17069
|
+
if (label.startsWith("@mutmutco design-system")) {
|
|
17070
|
+
return {
|
|
17071
|
+
id: "registry.design-system-components",
|
|
17072
|
+
category: "hub",
|
|
17073
|
+
severity: "advisory",
|
|
17074
|
+
cost: "slow",
|
|
17075
|
+
heal: "none",
|
|
17076
|
+
label,
|
|
17077
|
+
protects: ["Design-system component cache can be compared against the live registry"]
|
|
17078
|
+
};
|
|
17079
|
+
}
|
|
17080
|
+
if (label.startsWith("@mutmutco registry components")) {
|
|
17081
|
+
return {
|
|
17082
|
+
id: "registry.components",
|
|
17083
|
+
category: "hub",
|
|
17084
|
+
severity: "advisory",
|
|
17085
|
+
cost: "slow",
|
|
17086
|
+
heal: "none",
|
|
17087
|
+
label,
|
|
17088
|
+
protects: ["Repo component cache can be compared against the live registry"]
|
|
17089
|
+
};
|
|
17090
|
+
}
|
|
17091
|
+
return void 0;
|
|
17092
|
+
}
|
|
17093
|
+
function fallbackDoctorCheckMetadata(label) {
|
|
17094
|
+
return {
|
|
17095
|
+
id: `legacy.${label.toLowerCase().replace(/[^a-z0-9]+/g, ".").replace(/(^\.|\.$)/g, "") || "unknown"}`,
|
|
17096
|
+
category: "repo",
|
|
17097
|
+
severity: "hard",
|
|
17098
|
+
cost: "fast",
|
|
17099
|
+
heal: "none",
|
|
17100
|
+
label,
|
|
17101
|
+
protects: ["Legacy check without catalog metadata"]
|
|
17102
|
+
};
|
|
17103
|
+
}
|
|
17104
|
+
function doctorMetadataForLabel(label) {
|
|
17105
|
+
return DOCTOR_CHECK_METADATA_BY_LABEL[label] ?? derivedDoctorCheckMetadata(label) ?? fallbackDoctorCheckMetadata(label);
|
|
17106
|
+
}
|
|
17107
|
+
function doctorMetadataForCheck(check) {
|
|
17108
|
+
return check.metadata ?? doctorMetadataForLabel(check.label);
|
|
17109
|
+
}
|
|
16479
17110
|
function buildPluginResolvabilityCheck(input) {
|
|
16480
17111
|
const { state } = buildPluginGuardDecision(input);
|
|
16481
17112
|
const surface = input.surface ?? "shell";
|
|
@@ -16521,17 +17152,17 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
16521
17152
|
}
|
|
16522
17153
|
function cursorUserGlobalStatePath() {
|
|
16523
17154
|
if (process.platform === "win32") {
|
|
16524
|
-
const base = process.env.APPDATA || (0,
|
|
16525
|
-
return (0,
|
|
17155
|
+
const base = process.env.APPDATA || (0, import_node_path18.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
|
|
17156
|
+
return (0, import_node_path18.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
16526
17157
|
}
|
|
16527
17158
|
if (process.platform === "darwin") {
|
|
16528
|
-
return (0,
|
|
17159
|
+
return (0, import_node_path18.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
16529
17160
|
}
|
|
16530
|
-
return (0,
|
|
17161
|
+
return (0, import_node_path18.join)((0, import_node_os6.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
16531
17162
|
}
|
|
16532
17163
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
16533
17164
|
const dbPath = cursorUserGlobalStatePath();
|
|
16534
|
-
if (!(0,
|
|
17165
|
+
if (!(0, import_node_fs17.existsSync)(dbPath)) return void 0;
|
|
16535
17166
|
try {
|
|
16536
17167
|
const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
|
|
16537
17168
|
timeout: 5e3
|
|
@@ -16545,57 +17176,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
|
16545
17176
|
}
|
|
16546
17177
|
}
|
|
16547
17178
|
function syncDirContents(src, dest) {
|
|
16548
|
-
(0,
|
|
16549
|
-
for (const name of (0,
|
|
16550
|
-
(0,
|
|
17179
|
+
(0, import_node_fs17.mkdirSync)(dest, { recursive: true });
|
|
17180
|
+
for (const name of (0, import_node_fs17.readdirSync)(dest)) {
|
|
17181
|
+
(0, import_node_fs17.rmSync)((0, import_node_path18.join)(dest, name), { recursive: true, force: true });
|
|
16551
17182
|
}
|
|
16552
|
-
(0,
|
|
17183
|
+
(0, import_node_fs17.cpSync)(src, dest, { recursive: true });
|
|
16553
17184
|
}
|
|
16554
17185
|
function releaseTag(releasedVersion) {
|
|
16555
17186
|
return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
|
|
16556
17187
|
}
|
|
16557
17188
|
async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
|
|
16558
|
-
const tarFile = (0,
|
|
17189
|
+
const tarFile = (0, import_node_path18.join)(tmpRoot, "archive.tar");
|
|
16559
17190
|
try {
|
|
16560
17191
|
await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
|
|
16561
17192
|
await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
|
|
16562
17193
|
timeout: 6e4
|
|
16563
17194
|
});
|
|
16564
17195
|
await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
|
|
16565
|
-
const pluginMmi = (0,
|
|
16566
|
-
return (0,
|
|
17196
|
+
const pluginMmi = (0, import_node_path18.join)(tmpRoot, "plugins", "mmi");
|
|
17197
|
+
return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
16567
17198
|
} catch {
|
|
16568
17199
|
return void 0;
|
|
16569
17200
|
}
|
|
16570
17201
|
}
|
|
16571
17202
|
async function downloadPluginMmiViaGh(tag, tmpRoot) {
|
|
16572
|
-
const tarPath = (0,
|
|
17203
|
+
const tarPath = (0, import_node_path18.join)(tmpRoot, "repo.tgz");
|
|
16573
17204
|
try {
|
|
16574
|
-
(0,
|
|
17205
|
+
(0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
|
|
16575
17206
|
const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
|
|
16576
17207
|
timeout: 12e4,
|
|
16577
17208
|
maxBuffer: 100 * 1024 * 1024,
|
|
16578
17209
|
encoding: "buffer",
|
|
16579
17210
|
windowsHide: true
|
|
16580
17211
|
});
|
|
16581
|
-
(0,
|
|
17212
|
+
(0, import_node_fs17.writeFileSync)(tarPath, stdout);
|
|
16582
17213
|
await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
|
|
16583
|
-
const top = (0,
|
|
17214
|
+
const top = (0, import_node_fs17.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
|
|
16584
17215
|
if (!top) return void 0;
|
|
16585
|
-
const pluginMmi = (0,
|
|
16586
|
-
return (0,
|
|
17216
|
+
const pluginMmi = (0, import_node_path18.join)(tmpRoot, top, "plugins", "mmi");
|
|
17217
|
+
return (0, import_node_fs17.existsSync)((0, import_node_path18.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
16587
17218
|
} catch {
|
|
16588
17219
|
return void 0;
|
|
16589
17220
|
}
|
|
16590
17221
|
}
|
|
16591
17222
|
async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
|
|
16592
|
-
(0,
|
|
17223
|
+
(0, import_node_fs17.mkdirSync)(tmpRoot, { recursive: true });
|
|
16593
17224
|
const tag = releaseTag(releasedVersion);
|
|
16594
17225
|
if (hubCheckout) {
|
|
16595
17226
|
const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
|
|
16596
17227
|
if (fromHub) return fromHub;
|
|
16597
17228
|
}
|
|
16598
|
-
return downloadPluginMmiViaGh(tag, (0,
|
|
17229
|
+
return downloadPluginMmiViaGh(tag, (0, import_node_path18.join)(tmpRoot, "gh"));
|
|
16599
17230
|
}
|
|
16600
17231
|
function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
16601
17232
|
if (!isSemverVersion2(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
|
|
@@ -16609,7 +17240,7 @@ function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRoo
|
|
|
16609
17240
|
const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
|
|
16610
17241
|
if (needing.length > 0) return needing.map((pin) => pin.path);
|
|
16611
17242
|
if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion2(releasedVersion)) {
|
|
16612
|
-
return [(0,
|
|
17243
|
+
return [(0, import_node_path18.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
|
|
16613
17244
|
}
|
|
16614
17245
|
return [];
|
|
16615
17246
|
}
|
|
@@ -16624,10 +17255,10 @@ async function applyCursorPluginCacheSeed(input) {
|
|
|
16624
17255
|
for (const dest of targets) {
|
|
16625
17256
|
syncDirContents(source, dest);
|
|
16626
17257
|
}
|
|
16627
|
-
(0,
|
|
17258
|
+
(0, import_node_fs17.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
16628
17259
|
return true;
|
|
16629
17260
|
}
|
|
16630
|
-
var
|
|
17261
|
+
var CURSOR_ENABLED_PIN_LABEL2 = "Cursor Team Marketplace enabled plugin pin";
|
|
16631
17262
|
function cursorSplitBrainCapabilities() {
|
|
16632
17263
|
const skills = OPENCODE_WORKFLOW_COMMANDS.map((c) => `/${c}`).join(", ");
|
|
16633
17264
|
return `org skills (${skills}) and SessionStart hooks`;
|
|
@@ -16647,7 +17278,7 @@ function parseCursorPluginLog(text) {
|
|
|
16647
17278
|
return { enabledPin, loadSucceeded, loadFailed };
|
|
16648
17279
|
}
|
|
16649
17280
|
function buildCursorEnabledPinCheck(input) {
|
|
16650
|
-
const label =
|
|
17281
|
+
const label = CURSOR_ENABLED_PIN_LABEL2;
|
|
16651
17282
|
const healthy = { ok: true, label, fix: "" };
|
|
16652
17283
|
if (!input.isOrgRepo) return healthy;
|
|
16653
17284
|
if (!input.enabledPin && !input.loadFailed) return healthy;
|
|
@@ -16682,7 +17313,7 @@ var PLAYWRIGHT_MCP_SPEC = {
|
|
|
16682
17313
|
args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
|
|
16683
17314
|
legacyNames: []
|
|
16684
17315
|
};
|
|
16685
|
-
var
|
|
17316
|
+
var MCP_RECONCILE_LABEL2 = "MCP server registrations (org-managed Playwright)";
|
|
16686
17317
|
var MCP_RECONCILE_FIX = "register/reconcile the org Playwright MCP server (npx -y @playwright/mcp@latest --output-dir tmp/playwright-mcp) \u2014 run `mmi-cli doctor --apply`; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
|
|
16687
17318
|
function extractServerBlock(content, format, spec) {
|
|
16688
17319
|
return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
|
|
@@ -16796,7 +17427,7 @@ function planClaudeCliMcpHeal(item, spec) {
|
|
|
16796
17427
|
function buildMcpReconcileCheck(plan) {
|
|
16797
17428
|
const base = {
|
|
16798
17429
|
ok: true,
|
|
16799
|
-
label:
|
|
17430
|
+
label: MCP_RECONCILE_LABEL2,
|
|
16800
17431
|
fix: MCP_RECONCILE_FIX,
|
|
16801
17432
|
severityOverride: "advisory"
|
|
16802
17433
|
};
|
|
@@ -16983,9 +17614,9 @@ ${block}
|
|
|
16983
17614
|
}
|
|
16984
17615
|
|
|
16985
17616
|
// src/cli-doctor-shared.ts
|
|
16986
|
-
var
|
|
16987
|
-
var
|
|
16988
|
-
var
|
|
17617
|
+
var import_node_fs18 = require("node:fs");
|
|
17618
|
+
var import_node_path19 = require("node:path");
|
|
17619
|
+
var import_node_fs19 = require("node:fs");
|
|
16989
17620
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
16990
17621
|
async function awsCallerArn() {
|
|
16991
17622
|
try {
|
|
@@ -17031,7 +17662,7 @@ async function localBranchHeads() {
|
|
|
17031
17662
|
}
|
|
17032
17663
|
async function currentRepoWorktreeGitRoot(repoRoot) {
|
|
17033
17664
|
const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
17034
|
-
return gitCommonDir ? (0,
|
|
17665
|
+
return gitCommonDir ? (0, import_node_path19.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
|
|
17035
17666
|
}
|
|
17036
17667
|
async function worktreeBranches() {
|
|
17037
17668
|
const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
@@ -17051,18 +17682,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
|
|
|
17051
17682
|
const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
|
|
17052
17683
|
if (!match?.[1]) return void 0;
|
|
17053
17684
|
const raw = match[1].trim();
|
|
17054
|
-
return (0,
|
|
17685
|
+
return (0, import_node_path19.isAbsolute)(raw) ? raw : (0, import_node_path19.resolve)(worktreePath, raw);
|
|
17055
17686
|
}
|
|
17056
17687
|
function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
17057
17688
|
if (!worktreeGitRoot) return false;
|
|
17058
17689
|
try {
|
|
17059
|
-
const entries = (0,
|
|
17690
|
+
const entries = (0, import_node_fs19.readdirSync)(worktreeGitRoot, { withFileTypes: true });
|
|
17060
17691
|
for (const ent of entries) {
|
|
17061
17692
|
if (!ent.isDirectory()) continue;
|
|
17062
17693
|
try {
|
|
17063
|
-
const gitdirPath = (0,
|
|
17064
|
-
const resolvedGitdir = (0,
|
|
17065
|
-
if (sameWorktreeMetadataPath((0,
|
|
17694
|
+
const gitdirPath = (0, import_node_fs18.readFileSync)((0, import_node_path19.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
|
|
17695
|
+
const resolvedGitdir = (0, import_node_path19.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path19.resolve)(worktreeGitRoot, ent.name, gitdirPath);
|
|
17696
|
+
if (sameWorktreeMetadataPath((0, import_node_path19.dirname)(resolvedGitdir), worktreePath)) return true;
|
|
17066
17697
|
} catch {
|
|
17067
17698
|
}
|
|
17068
17699
|
}
|
|
@@ -17072,7 +17703,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
|
17072
17703
|
}
|
|
17073
17704
|
function pathExistsKnown(path2) {
|
|
17074
17705
|
try {
|
|
17075
|
-
(0,
|
|
17706
|
+
(0, import_node_fs19.statSync)(path2);
|
|
17076
17707
|
return true;
|
|
17077
17708
|
} catch (e) {
|
|
17078
17709
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
@@ -17081,10 +17712,10 @@ function pathExistsKnown(path2) {
|
|
|
17081
17712
|
}
|
|
17082
17713
|
}
|
|
17083
17714
|
function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
17084
|
-
const gitPath = (0,
|
|
17715
|
+
const gitPath = (0, import_node_path19.join)(path2, ".git");
|
|
17085
17716
|
let st;
|
|
17086
17717
|
try {
|
|
17087
|
-
st = (0,
|
|
17718
|
+
st = (0, import_node_fs19.lstatSync)(gitPath);
|
|
17088
17719
|
} catch (e) {
|
|
17089
17720
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
17090
17721
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
@@ -17101,7 +17732,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
|
17101
17732
|
if (st.isDirectory()) return { path: path2, gitType: "dir" };
|
|
17102
17733
|
if (!st.isFile()) return { path: path2, gitType: "other" };
|
|
17103
17734
|
try {
|
|
17104
|
-
const gitFileContent = (0,
|
|
17735
|
+
const gitFileContent = (0, import_node_fs18.readFileSync)(gitPath, "utf8");
|
|
17105
17736
|
const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
|
|
17106
17737
|
const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
|
|
17107
17738
|
return {
|
|
@@ -17118,7 +17749,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
|
17118
17749
|
}
|
|
17119
17750
|
function inspectDeadWorktreeDirContent(path2) {
|
|
17120
17751
|
try {
|
|
17121
|
-
return { entries: (0,
|
|
17752
|
+
return { entries: (0, import_node_fs19.readdirSync)(path2) };
|
|
17122
17753
|
} catch (e) {
|
|
17123
17754
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
17124
17755
|
return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
|
|
@@ -17137,8 +17768,8 @@ async function siblingWorktreeDirs() {
|
|
|
17137
17768
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
|
|
17138
17769
|
const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
|
|
17139
17770
|
try {
|
|
17140
|
-
const entries = (0,
|
|
17141
|
-
return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0,
|
|
17771
|
+
const entries = (0, import_node_fs19.readdirSync)(siblingRoot, { withFileTypes: true });
|
|
17772
|
+
return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path19.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
|
|
17142
17773
|
} catch {
|
|
17143
17774
|
return [];
|
|
17144
17775
|
}
|
|
@@ -17188,7 +17819,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
17188
17819
|
}
|
|
17189
17820
|
function readRepoVersion() {
|
|
17190
17821
|
try {
|
|
17191
|
-
return JSON.parse((0,
|
|
17822
|
+
return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
17192
17823
|
} catch {
|
|
17193
17824
|
return void 0;
|
|
17194
17825
|
}
|
|
@@ -17340,11 +17971,11 @@ async function applyPluginHeal(token, surface, log, opts) {
|
|
|
17340
17971
|
}
|
|
17341
17972
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
17342
17973
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
17343
|
-
return (0,
|
|
17974
|
+
return (0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
17344
17975
|
};
|
|
17345
17976
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
17346
17977
|
try {
|
|
17347
|
-
return JSON.parse((0,
|
|
17978
|
+
return JSON.parse((0, import_node_fs20.readFileSync)(installedPluginsPath(surface), "utf8"));
|
|
17348
17979
|
} catch {
|
|
17349
17980
|
return null;
|
|
17350
17981
|
}
|
|
@@ -17352,13 +17983,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
|
17352
17983
|
function marketplaceCloneCandidates(surface, home) {
|
|
17353
17984
|
if (surface === "codex") {
|
|
17354
17985
|
return [
|
|
17355
|
-
(0,
|
|
17356
|
-
(0,
|
|
17986
|
+
(0, import_node_path20.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
|
|
17987
|
+
(0, import_node_path20.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
|
|
17357
17988
|
];
|
|
17358
17989
|
}
|
|
17359
|
-
return [(0,
|
|
17990
|
+
return [(0, import_node_path20.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
17360
17991
|
}
|
|
17361
|
-
function marketplaceClonePresent(surface, home, exists =
|
|
17992
|
+
function marketplaceClonePresent(surface, home, exists = import_node_fs20.existsSync) {
|
|
17362
17993
|
return marketplaceCloneCandidates(surface, home).some(exists);
|
|
17363
17994
|
}
|
|
17364
17995
|
function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
|
|
@@ -17368,14 +17999,14 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
17368
17999
|
isOrgRepo,
|
|
17369
18000
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
17370
18001
|
marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
|
|
17371
|
-
pluginCachePresent: (0,
|
|
18002
|
+
pluginCachePresent: (0, import_node_fs20.existsSync)((0, import_node_path20.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
17372
18003
|
};
|
|
17373
18004
|
}
|
|
17374
18005
|
function installedPluginSources() {
|
|
17375
18006
|
return ["claude", "codex"].map((surface) => {
|
|
17376
|
-
const recordPath = (0,
|
|
18007
|
+
const recordPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
17377
18008
|
try {
|
|
17378
|
-
return { surface, installed: JSON.parse((0,
|
|
18009
|
+
return { surface, installed: JSON.parse((0, import_node_fs20.readFileSync)(recordPath, "utf8")), recordPath };
|
|
17379
18010
|
} catch {
|
|
17380
18011
|
return { surface, installed: null, recordPath };
|
|
17381
18012
|
}
|
|
@@ -17383,7 +18014,7 @@ function installedPluginSources() {
|
|
|
17383
18014
|
}
|
|
17384
18015
|
function readClaudeSettings() {
|
|
17385
18016
|
try {
|
|
17386
|
-
return JSON.parse((0,
|
|
18017
|
+
return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
17387
18018
|
} catch {
|
|
17388
18019
|
return null;
|
|
17389
18020
|
}
|
|
@@ -17405,7 +18036,7 @@ function writeProjectInstallRecord(record) {
|
|
|
17405
18036
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
17406
18037
|
list.push(record);
|
|
17407
18038
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
17408
|
-
(0,
|
|
18039
|
+
(0, import_node_fs20.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
17409
18040
|
`, "utf8");
|
|
17410
18041
|
return true;
|
|
17411
18042
|
} catch {
|
|
@@ -17418,9 +18049,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
17418
18049
|
if (!file) return false;
|
|
17419
18050
|
if (!file.plugins) file.plugins = {};
|
|
17420
18051
|
const path2 = installedPluginsPath();
|
|
17421
|
-
(0,
|
|
18052
|
+
(0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
|
|
17422
18053
|
file.plugins[pluginId] = records;
|
|
17423
|
-
(0,
|
|
18054
|
+
(0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
17424
18055
|
`, "utf8");
|
|
17425
18056
|
return true;
|
|
17426
18057
|
} catch {
|
|
@@ -17428,22 +18059,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
17428
18059
|
}
|
|
17429
18060
|
}
|
|
17430
18061
|
function opencodeConfigDir() {
|
|
17431
|
-
return (0,
|
|
18062
|
+
return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "opencode");
|
|
17432
18063
|
}
|
|
17433
18064
|
function opencodeConfigPath() {
|
|
17434
|
-
return (0,
|
|
18065
|
+
return (0, import_node_path20.join)(opencodeConfigDir(), "opencode.jsonc");
|
|
17435
18066
|
}
|
|
17436
18067
|
function opencodeCommandsDir() {
|
|
17437
|
-
return (0,
|
|
18068
|
+
return (0, import_node_path20.join)(opencodeConfigDir(), "commands");
|
|
17438
18069
|
}
|
|
17439
18070
|
function opencodeSkillsPath() {
|
|
17440
|
-
return (0,
|
|
18071
|
+
return (0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
|
|
17441
18072
|
}
|
|
17442
18073
|
function opencodeConfigSnapshot() {
|
|
17443
18074
|
const path2 = opencodeConfigPath();
|
|
17444
|
-
if (!(0,
|
|
18075
|
+
if (!(0, import_node_fs20.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
|
|
17445
18076
|
try {
|
|
17446
|
-
const raw = (0,
|
|
18077
|
+
const raw = (0, import_node_fs20.readFileSync)(path2, "utf8");
|
|
17447
18078
|
const parsed = JSON.parse(stripJsonc(raw));
|
|
17448
18079
|
const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
|
|
17449
18080
|
const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
|
|
@@ -17466,9 +18097,9 @@ function writeOpencodeConfigPlugin(snapshot) {
|
|
|
17466
18097
|
const plan = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
|
|
17467
18098
|
if (plan.action === "already") return true;
|
|
17468
18099
|
if (!plan.text || plan.action === "unsafe") return false;
|
|
17469
|
-
(0,
|
|
17470
|
-
if (snapshot.hasConfig) (0,
|
|
17471
|
-
(0,
|
|
18100
|
+
(0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
|
|
18101
|
+
if (snapshot.hasConfig) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
|
|
18102
|
+
(0, import_node_fs20.writeFileSync)(path2, plan.text, "utf8");
|
|
17472
18103
|
return true;
|
|
17473
18104
|
} catch {
|
|
17474
18105
|
return false;
|
|
@@ -17483,9 +18114,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
|
|
|
17483
18114
|
const normalized = skillsPath.replace(/\\/g, "/");
|
|
17484
18115
|
if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
|
|
17485
18116
|
parsed.skills = { ...skills, paths };
|
|
17486
|
-
(0,
|
|
17487
|
-
if (snapshot.hasConfig && (0,
|
|
17488
|
-
(0,
|
|
18117
|
+
(0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(snapshot.path), { recursive: true });
|
|
18118
|
+
if (snapshot.hasConfig && (0, import_node_fs20.existsSync)(snapshot.path)) (0, import_node_fs20.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
|
|
18119
|
+
(0, import_node_fs20.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
|
|
17489
18120
|
`, "utf8");
|
|
17490
18121
|
return true;
|
|
17491
18122
|
} catch {
|
|
@@ -17494,7 +18125,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
|
|
|
17494
18125
|
}
|
|
17495
18126
|
function opencodeExistingCommands() {
|
|
17496
18127
|
try {
|
|
17497
|
-
return (0,
|
|
18128
|
+
return (0, import_node_fs20.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
|
|
17498
18129
|
} catch {
|
|
17499
18130
|
return [];
|
|
17500
18131
|
}
|
|
@@ -17502,9 +18133,9 @@ function opencodeExistingCommands() {
|
|
|
17502
18133
|
function writeOpencodeCommandFiles() {
|
|
17503
18134
|
try {
|
|
17504
18135
|
const dir = opencodeCommandsDir();
|
|
17505
|
-
(0,
|
|
18136
|
+
(0, import_node_fs20.mkdirSync)(dir, { recursive: true });
|
|
17506
18137
|
for (const command of OPENCODE_WORKFLOW_COMMANDS) {
|
|
17507
|
-
(0,
|
|
18138
|
+
(0, import_node_fs20.writeFileSync)((0, import_node_path20.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
|
|
17508
18139
|
}
|
|
17509
18140
|
return true;
|
|
17510
18141
|
} catch {
|
|
@@ -17513,12 +18144,12 @@ function writeOpencodeCommandFiles() {
|
|
|
17513
18144
|
}
|
|
17514
18145
|
function readOpencodeAdapterDiskVersion() {
|
|
17515
18146
|
const candidates = [
|
|
17516
|
-
(0,
|
|
17517
|
-
(0,
|
|
18147
|
+
(0, import_node_path20.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
|
|
18148
|
+
(0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
|
|
17518
18149
|
];
|
|
17519
18150
|
for (const path2 of candidates) {
|
|
17520
18151
|
try {
|
|
17521
|
-
const parsed = JSON.parse((0,
|
|
18152
|
+
const parsed = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
|
|
17522
18153
|
if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
|
|
17523
18154
|
} catch {
|
|
17524
18155
|
continue;
|
|
@@ -17534,7 +18165,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
|
|
|
17534
18165
|
try {
|
|
17535
18166
|
const specs = opencodeMmiPluginSpecs(snapshot);
|
|
17536
18167
|
log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
|
|
17537
|
-
(0,
|
|
18168
|
+
(0, import_node_fs20.mkdirSync)(opencodeConfigDir(), { recursive: true });
|
|
17538
18169
|
await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
|
|
17539
18170
|
return true;
|
|
17540
18171
|
} catch {
|
|
@@ -17542,12 +18173,12 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
|
|
|
17542
18173
|
}
|
|
17543
18174
|
}
|
|
17544
18175
|
function opencodePackagesRoot() {
|
|
17545
|
-
return (0,
|
|
18176
|
+
return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
|
|
17546
18177
|
}
|
|
17547
18178
|
function opencodePluginCacheDirs() {
|
|
17548
18179
|
const root = opencodePackagesRoot();
|
|
17549
18180
|
try {
|
|
17550
|
-
return (0,
|
|
18181
|
+
return (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path20.join)(root, e.name));
|
|
17551
18182
|
} catch {
|
|
17552
18183
|
return [];
|
|
17553
18184
|
}
|
|
@@ -17555,7 +18186,7 @@ function opencodePluginCacheDirs() {
|
|
|
17555
18186
|
function readOpencodeCacheDirVersion(cacheDir) {
|
|
17556
18187
|
try {
|
|
17557
18188
|
const parsed = JSON.parse(
|
|
17558
|
-
(0,
|
|
18189
|
+
(0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
|
|
17559
18190
|
);
|
|
17560
18191
|
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
|
|
17561
18192
|
} catch {
|
|
@@ -17578,9 +18209,9 @@ function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
|
|
|
17578
18209
|
});
|
|
17579
18210
|
if (!plan.quarantine) continue;
|
|
17580
18211
|
try {
|
|
17581
|
-
const quarantineRoot = (0,
|
|
17582
|
-
(0,
|
|
17583
|
-
(0,
|
|
18212
|
+
const quarantineRoot = (0, import_node_path20.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
|
|
18213
|
+
(0, import_node_fs20.mkdirSync)(quarantineRoot, { recursive: true });
|
|
18214
|
+
(0, import_node_fs20.renameSync)(cacheDir, (0, import_node_path20.join)(quarantineRoot, (0, import_node_path20.basename)(cacheDir)));
|
|
17584
18215
|
log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
|
|
17585
18216
|
moved = true;
|
|
17586
18217
|
} catch {
|
|
@@ -17606,30 +18237,30 @@ function opencodePluginVersionsForReport() {
|
|
|
17606
18237
|
}
|
|
17607
18238
|
function opencodeDesktopLogsRoot() {
|
|
17608
18239
|
if (process.platform === "win32") {
|
|
17609
|
-
const base = process.env.APPDATA || (0,
|
|
17610
|
-
return (0,
|
|
18240
|
+
const base = process.env.APPDATA || (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming");
|
|
18241
|
+
return (0, import_node_path20.join)(base, "ai.opencode.desktop", "logs");
|
|
17611
18242
|
}
|
|
17612
18243
|
if (process.platform === "darwin") {
|
|
17613
|
-
return (0,
|
|
18244
|
+
return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
|
|
17614
18245
|
}
|
|
17615
|
-
return (0,
|
|
18246
|
+
return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "ai.opencode.desktop", "logs");
|
|
17616
18247
|
}
|
|
17617
18248
|
function opencodeDesktopBootstrapSnapshot() {
|
|
17618
18249
|
const root = opencodeDesktopLogsRoot();
|
|
17619
18250
|
try {
|
|
17620
|
-
const sessionDirs = (0,
|
|
18251
|
+
const sessionDirs = (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path20.join)(root, entry.name)).sort((a, b) => (0, import_node_fs20.statSync)(b).mtimeMs - (0, import_node_fs20.statSync)(a).mtimeMs);
|
|
17621
18252
|
const newest = sessionDirs[0];
|
|
17622
18253
|
if (!newest) return [];
|
|
17623
|
-
const logPath = (0,
|
|
17624
|
-
const text = (0,
|
|
17625
|
-
return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0,
|
|
18254
|
+
const logPath = (0, import_node_path20.join)(newest, "renderer.log");
|
|
18255
|
+
const text = (0, import_node_fs20.readFileSync)(logPath, "utf8");
|
|
18256
|
+
return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs20.existsSync)(directory)).map((directory) => ({ directory, logPath }));
|
|
17626
18257
|
} catch {
|
|
17627
18258
|
return [];
|
|
17628
18259
|
}
|
|
17629
18260
|
}
|
|
17630
18261
|
function opencodeLegacyConfigSnapshot() {
|
|
17631
|
-
const legacyPath = (0,
|
|
17632
|
-
if (!(0,
|
|
18262
|
+
const legacyPath = (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".opencode", "opencode.json");
|
|
18263
|
+
if (!(0, import_node_fs20.existsSync)(legacyPath)) return {};
|
|
17633
18264
|
const content = readTextFile(legacyPath);
|
|
17634
18265
|
if (content == null) return {};
|
|
17635
18266
|
const plugins = parseOpencodeLegacyConfigPlugins(content);
|
|
@@ -17641,24 +18272,24 @@ function opencodeLegacyConfigSnapshot() {
|
|
|
17641
18272
|
function quarantineOpencodeLegacyConfig(legacyPath) {
|
|
17642
18273
|
try {
|
|
17643
18274
|
const backupPath = `${legacyPath}.bak`;
|
|
17644
|
-
if ((0,
|
|
17645
|
-
(0,
|
|
18275
|
+
if ((0, import_node_fs20.existsSync)(backupPath)) return false;
|
|
18276
|
+
(0, import_node_fs20.renameSync)(legacyPath, backupPath);
|
|
17646
18277
|
return true;
|
|
17647
18278
|
} catch {
|
|
17648
18279
|
return false;
|
|
17649
18280
|
}
|
|
17650
18281
|
}
|
|
17651
18282
|
function cursorPluginCacheRoot() {
|
|
17652
|
-
return (0,
|
|
18283
|
+
return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
|
|
17653
18284
|
}
|
|
17654
18285
|
function cursorLogsRoot() {
|
|
17655
|
-
if (process.platform === "win32") return (0,
|
|
17656
|
-
if (process.platform === "darwin") return (0,
|
|
17657
|
-
return (0,
|
|
18286
|
+
if (process.platform === "win32") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "AppData", "Roaming", "Cursor", "logs");
|
|
18287
|
+
if (process.platform === "darwin") return (0, import_node_path20.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Cursor", "logs");
|
|
18288
|
+
return (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".config", "Cursor", "logs");
|
|
17658
18289
|
}
|
|
17659
18290
|
function safeReaddirNames(dir) {
|
|
17660
18291
|
try {
|
|
17661
|
-
return (0,
|
|
18292
|
+
return (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true }).map((e) => e.name);
|
|
17662
18293
|
} catch {
|
|
17663
18294
|
return [];
|
|
17664
18295
|
}
|
|
@@ -17667,15 +18298,15 @@ function readCursorPluginLogText() {
|
|
|
17667
18298
|
const root = cursorLogsRoot();
|
|
17668
18299
|
const sessions = safeReaddirNames(root).sort().reverse();
|
|
17669
18300
|
for (const session of sessions.slice(0, 5)) {
|
|
17670
|
-
const sessionDir = (0,
|
|
18301
|
+
const sessionDir = (0, import_node_path20.join)(root, session);
|
|
17671
18302
|
const texts = [];
|
|
17672
18303
|
for (const win of safeReaddirNames(sessionDir).filter((n) => n.startsWith("window"))) {
|
|
17673
|
-
const dir = (0,
|
|
18304
|
+
const dir = (0, import_node_path20.join)(sessionDir, win, "exthost", "anysphere.cursor-agent-exec");
|
|
17674
18305
|
for (const file of safeReaddirNames(dir)) {
|
|
17675
18306
|
if (!/^Cursor Plugins.*\.log$/i.test(file)) continue;
|
|
17676
|
-
const path2 = (0,
|
|
18307
|
+
const path2 = (0, import_node_path20.join)(dir, file);
|
|
17677
18308
|
try {
|
|
17678
|
-
texts.push({ text: (0,
|
|
18309
|
+
texts.push({ text: (0, import_node_fs20.readFileSync)(path2, "utf8"), mtime: (0, import_node_fs20.statSync)(path2).mtimeMs });
|
|
17679
18310
|
} catch {
|
|
17680
18311
|
}
|
|
17681
18312
|
}
|
|
@@ -17689,32 +18320,32 @@ function readCursorPluginLogText() {
|
|
|
17689
18320
|
function cursorPluginCachePinSnapshots() {
|
|
17690
18321
|
const root = cursorPluginCacheRoot();
|
|
17691
18322
|
try {
|
|
17692
|
-
return (0,
|
|
17693
|
-
const path2 = (0,
|
|
17694
|
-
const pluginJson = (0,
|
|
17695
|
-
const hooksJson = (0,
|
|
17696
|
-
const cliBundle = (0,
|
|
17697
|
-
const cursorHook = (0,
|
|
18323
|
+
return (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
18324
|
+
const path2 = (0, import_node_path20.join)(root, entry.name);
|
|
18325
|
+
const pluginJson = (0, import_node_path20.join)(path2, ".cursor-plugin", "plugin.json");
|
|
18326
|
+
const hooksJson = (0, import_node_path20.join)(path2, "hooks", "hooks.json");
|
|
18327
|
+
const cliBundle = (0, import_node_path20.join)(path2, "cli", "dist", "index.cjs");
|
|
18328
|
+
const cursorHook = (0, import_node_path20.join)(path2, "scripts", "cursor-hook.mjs");
|
|
17698
18329
|
let version;
|
|
17699
18330
|
try {
|
|
17700
|
-
const raw = JSON.parse((0,
|
|
18331
|
+
const raw = JSON.parse((0, import_node_fs20.readFileSync)(pluginJson, "utf8"));
|
|
17701
18332
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
17702
18333
|
} catch {
|
|
17703
18334
|
version = void 0;
|
|
17704
18335
|
}
|
|
17705
18336
|
let isEmpty = true;
|
|
17706
18337
|
try {
|
|
17707
|
-
isEmpty = (0,
|
|
18338
|
+
isEmpty = (0, import_node_fs20.readdirSync)(path2).length === 0;
|
|
17708
18339
|
} catch {
|
|
17709
18340
|
isEmpty = true;
|
|
17710
18341
|
}
|
|
17711
18342
|
return {
|
|
17712
18343
|
name: entry.name,
|
|
17713
18344
|
path: path2,
|
|
17714
|
-
hasPluginJson: (0,
|
|
17715
|
-
hasHooksJson: (0,
|
|
17716
|
-
hasCliBundle: (0,
|
|
17717
|
-
hasCursorHookScript: (0,
|
|
18345
|
+
hasPluginJson: (0, import_node_fs20.existsSync)(pluginJson),
|
|
18346
|
+
hasHooksJson: (0, import_node_fs20.existsSync)(hooksJson),
|
|
18347
|
+
hasCliBundle: (0, import_node_fs20.existsSync)(cliBundle),
|
|
18348
|
+
hasCursorHookScript: (0, import_node_fs20.existsSync)(cursorHook),
|
|
17718
18349
|
isEmpty,
|
|
17719
18350
|
version
|
|
17720
18351
|
};
|
|
@@ -17724,19 +18355,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
17724
18355
|
}
|
|
17725
18356
|
}
|
|
17726
18357
|
function hubCheckoutForCursorSeed() {
|
|
17727
|
-
const manifest = (0,
|
|
17728
|
-
return (0,
|
|
18358
|
+
const manifest = (0, import_node_path20.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
18359
|
+
return (0, import_node_fs20.existsSync)(manifest) ? process.cwd() : void 0;
|
|
17729
18360
|
}
|
|
17730
18361
|
function mmiPluginCacheRootSnapshots() {
|
|
17731
18362
|
const roots = [
|
|
17732
|
-
{ surface: "claude", root: (0,
|
|
17733
|
-
{ surface: "codex", root: (0,
|
|
18363
|
+
{ surface: "claude", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
|
|
18364
|
+
{ surface: "codex", root: (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
|
|
17734
18365
|
];
|
|
17735
18366
|
return roots.flatMap(({ surface, root }) => {
|
|
17736
18367
|
try {
|
|
17737
|
-
const entries = (0,
|
|
18368
|
+
const entries = (0, import_node_fs20.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
17738
18369
|
name: entry.name,
|
|
17739
|
-
path: (0,
|
|
18370
|
+
path: (0, import_node_path20.join)(root, entry.name),
|
|
17740
18371
|
isDirectory: entry.isDirectory()
|
|
17741
18372
|
}));
|
|
17742
18373
|
return [{ surface, root, entries }];
|
|
@@ -17747,7 +18378,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
17747
18378
|
}
|
|
17748
18379
|
function hasNestedMmiChild(versionDir) {
|
|
17749
18380
|
try {
|
|
17750
|
-
return (0,
|
|
18381
|
+
return (0, import_node_fs20.statSync)((0, import_node_path20.join)(versionDir, "mmi")).isDirectory();
|
|
17751
18382
|
} catch {
|
|
17752
18383
|
return false;
|
|
17753
18384
|
}
|
|
@@ -17758,10 +18389,10 @@ function nestedPluginTreeSnapshot() {
|
|
|
17758
18389
|
);
|
|
17759
18390
|
}
|
|
17760
18391
|
function uniqueQuarantineTarget(path2) {
|
|
17761
|
-
if (!(0,
|
|
18392
|
+
if (!(0, import_node_fs20.existsSync)(path2)) return path2;
|
|
17762
18393
|
for (let i = 1; i < 100; i += 1) {
|
|
17763
18394
|
const candidate = `${path2}-${i}`;
|
|
17764
|
-
if (!(0,
|
|
18395
|
+
if (!(0, import_node_fs20.existsSync)(candidate)) return candidate;
|
|
17765
18396
|
}
|
|
17766
18397
|
return `${path2}-${Date.now()}`;
|
|
17767
18398
|
}
|
|
@@ -17770,10 +18401,10 @@ function quarantinePluginCacheDirs(plan) {
|
|
|
17770
18401
|
const failed = [];
|
|
17771
18402
|
for (const move of plan) {
|
|
17772
18403
|
try {
|
|
17773
|
-
if (!(0,
|
|
18404
|
+
if (!(0, import_node_fs20.existsSync)(move.from)) continue;
|
|
17774
18405
|
const target = uniqueQuarantineTarget(move.to);
|
|
17775
|
-
(0,
|
|
17776
|
-
(0,
|
|
18406
|
+
(0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(target), { recursive: true });
|
|
18407
|
+
(0, import_node_fs20.renameSync)(move.from, target);
|
|
17777
18408
|
moved += 1;
|
|
17778
18409
|
} catch {
|
|
17779
18410
|
failed.push(move);
|
|
@@ -17792,23 +18423,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
17792
18423
|
}
|
|
17793
18424
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
17794
18425
|
try {
|
|
17795
|
-
if (!(0,
|
|
18426
|
+
if (!(0, import_node_fs20.existsSync)(targetPath)) return true;
|
|
17796
18427
|
if (isWin) {
|
|
17797
|
-
const emptyDir = (0,
|
|
17798
|
-
(0,
|
|
18428
|
+
const emptyDir = (0, import_node_path20.join)((0, import_node_os7.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
18429
|
+
(0, import_node_fs20.mkdirSync)(emptyDir, { recursive: true });
|
|
17799
18430
|
try {
|
|
17800
18431
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
17801
|
-
(0,
|
|
18432
|
+
(0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
|
|
17802
18433
|
} finally {
|
|
17803
18434
|
try {
|
|
17804
|
-
(0,
|
|
18435
|
+
(0, import_node_fs20.rmSync)(emptyDir, { recursive: true, force: true });
|
|
17805
18436
|
} catch {
|
|
17806
18437
|
}
|
|
17807
18438
|
}
|
|
17808
|
-
return !(0,
|
|
18439
|
+
return !(0, import_node_fs20.existsSync)(targetPath);
|
|
17809
18440
|
}
|
|
17810
|
-
(0,
|
|
17811
|
-
return !(0,
|
|
18441
|
+
(0, import_node_fs20.rmSync)(targetPath, { recursive: true, force: true });
|
|
18442
|
+
return !(0, import_node_fs20.existsSync)(targetPath);
|
|
17812
18443
|
} catch {
|
|
17813
18444
|
return false;
|
|
17814
18445
|
}
|
|
@@ -17821,18 +18452,18 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
17821
18452
|
}
|
|
17822
18453
|
return true;
|
|
17823
18454
|
}
|
|
17824
|
-
var gitignorePath = () => (0,
|
|
18455
|
+
var gitignorePath = () => (0, import_node_path20.join)(process.cwd(), ".gitignore");
|
|
17825
18456
|
function readTextFile(path2) {
|
|
17826
18457
|
try {
|
|
17827
|
-
if (!(0,
|
|
17828
|
-
return (0,
|
|
18458
|
+
if (!(0, import_node_fs20.existsSync)(path2)) return null;
|
|
18459
|
+
return (0, import_node_fs20.readFileSync)(path2, "utf8");
|
|
17829
18460
|
} catch {
|
|
17830
18461
|
return null;
|
|
17831
18462
|
}
|
|
17832
18463
|
}
|
|
17833
18464
|
function mcpDirExists(path2) {
|
|
17834
18465
|
try {
|
|
17835
|
-
return (0,
|
|
18466
|
+
return (0, import_node_fs20.existsSync)(path2);
|
|
17836
18467
|
} catch {
|
|
17837
18468
|
return false;
|
|
17838
18469
|
}
|
|
@@ -17840,60 +18471,60 @@ function mcpDirExists(path2) {
|
|
|
17840
18471
|
function mcpConfigTargets() {
|
|
17841
18472
|
const cwd = process.cwd();
|
|
17842
18473
|
const home = (0, import_node_os7.homedir)();
|
|
17843
|
-
const cursorProjectDir = (0,
|
|
17844
|
-
const cursorUserDir = (0,
|
|
17845
|
-
const codexDir = (0,
|
|
18474
|
+
const cursorProjectDir = (0, import_node_path20.join)(cwd, ".cursor");
|
|
18475
|
+
const cursorUserDir = (0, import_node_path20.join)(home, ".cursor");
|
|
18476
|
+
const codexDir = (0, import_node_path20.join)(home, ".codex");
|
|
17846
18477
|
return [
|
|
17847
18478
|
// Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
|
|
17848
18479
|
{
|
|
17849
18480
|
host: "claude-code",
|
|
17850
18481
|
label: "Claude Code project MCP",
|
|
17851
|
-
path: (0,
|
|
18482
|
+
path: (0, import_node_path20.join)(cwd, ".mcp.json"),
|
|
17852
18483
|
format: "json",
|
|
17853
18484
|
present: true,
|
|
17854
18485
|
// parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
|
|
17855
18486
|
isRepoFile: true,
|
|
17856
18487
|
createWhenFileAbsent: false,
|
|
17857
|
-
content: readTextFile((0,
|
|
18488
|
+
content: readTextFile((0, import_node_path20.join)(cwd, ".mcp.json"))
|
|
17858
18489
|
},
|
|
17859
18490
|
// Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
|
|
17860
18491
|
{
|
|
17861
18492
|
host: "cursor-project",
|
|
17862
18493
|
label: "Cursor project MCP",
|
|
17863
|
-
path: (0,
|
|
18494
|
+
path: (0, import_node_path20.join)(cursorProjectDir, "mcp.json"),
|
|
17864
18495
|
format: "json",
|
|
17865
18496
|
present: mcpDirExists(cursorProjectDir),
|
|
17866
18497
|
isRepoFile: true,
|
|
17867
18498
|
createWhenFileAbsent: true,
|
|
17868
|
-
content: readTextFile((0,
|
|
18499
|
+
content: readTextFile((0, import_node_path20.join)(cursorProjectDir, "mcp.json"))
|
|
17869
18500
|
},
|
|
17870
18501
|
// Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
|
|
17871
18502
|
{
|
|
17872
18503
|
host: "cursor-user",
|
|
17873
18504
|
label: "Cursor user MCP",
|
|
17874
|
-
path: (0,
|
|
18505
|
+
path: (0, import_node_path20.join)(cursorUserDir, "mcp.json"),
|
|
17875
18506
|
format: "json",
|
|
17876
18507
|
present: mcpDirExists(cursorUserDir),
|
|
17877
18508
|
isRepoFile: false,
|
|
17878
18509
|
createWhenFileAbsent: true,
|
|
17879
|
-
content: readTextFile((0,
|
|
18510
|
+
content: readTextFile((0, import_node_path20.join)(cursorUserDir, "mcp.json"))
|
|
17880
18511
|
},
|
|
17881
18512
|
// Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
|
|
17882
18513
|
{
|
|
17883
18514
|
host: "codex",
|
|
17884
18515
|
label: "Codex user config",
|
|
17885
|
-
path: (0,
|
|
18516
|
+
path: (0, import_node_path20.join)(codexDir, "config.toml"),
|
|
17886
18517
|
format: "toml",
|
|
17887
18518
|
present: mcpDirExists(codexDir),
|
|
17888
18519
|
isRepoFile: false,
|
|
17889
18520
|
createWhenFileAbsent: true,
|
|
17890
|
-
content: readTextFile((0,
|
|
18521
|
+
content: readTextFile((0, import_node_path20.join)(codexDir, "config.toml"))
|
|
17891
18522
|
}
|
|
17892
18523
|
];
|
|
17893
18524
|
}
|
|
17894
18525
|
function writeMcpConfigFile(path2, content) {
|
|
17895
18526
|
try {
|
|
17896
|
-
(0,
|
|
18527
|
+
(0, import_node_fs20.writeFileSync)(path2, content, "utf8");
|
|
17897
18528
|
return true;
|
|
17898
18529
|
} catch {
|
|
17899
18530
|
return false;
|
|
@@ -17973,7 +18604,7 @@ function strayBrowserArtifactPaths() {
|
|
|
17973
18604
|
const cwd = process.cwd();
|
|
17974
18605
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
17975
18606
|
try {
|
|
17976
|
-
return (0,
|
|
18607
|
+
return (0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, rel));
|
|
17977
18608
|
} catch {
|
|
17978
18609
|
return false;
|
|
17979
18610
|
}
|
|
@@ -17981,37 +18612,119 @@ function strayBrowserArtifactPaths() {
|
|
|
17981
18612
|
}
|
|
17982
18613
|
function readGitignore() {
|
|
17983
18614
|
try {
|
|
17984
|
-
return (0,
|
|
18615
|
+
return (0, import_node_fs20.readFileSync)(gitignorePath(), "utf8");
|
|
17985
18616
|
} catch {
|
|
17986
18617
|
return null;
|
|
17987
18618
|
}
|
|
17988
18619
|
}
|
|
17989
18620
|
function writeGitignore(content) {
|
|
17990
18621
|
try {
|
|
17991
|
-
(0,
|
|
18622
|
+
(0, import_node_fs20.writeFileSync)(gitignorePath(), content, "utf8");
|
|
17992
18623
|
return true;
|
|
17993
18624
|
} catch {
|
|
17994
18625
|
return false;
|
|
17995
18626
|
}
|
|
17996
18627
|
}
|
|
18628
|
+
function readRepoJson(rel) {
|
|
18629
|
+
try {
|
|
18630
|
+
return JSON.parse((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(process.cwd(), rel), "utf8"));
|
|
18631
|
+
} catch {
|
|
18632
|
+
return null;
|
|
18633
|
+
}
|
|
18634
|
+
}
|
|
18635
|
+
var ENCODING_SCAN_ROOT_FILES = ["AGENTS.md", "README.md", "CLAUDE.md", "package.json", "opencode.json"];
|
|
18636
|
+
var ENCODING_SCAN_DIRS = [".claude", "hooks", "scripts"];
|
|
18637
|
+
var ENCODING_SCAN_EXTS = /* @__PURE__ */ new Set([".md", ".json", ".jsonc", ".ts", ".js", ".cjs", ".mjs", ".sh", ".ps1", ".py", ".txt"]);
|
|
18638
|
+
var ENCODING_SCAN_MAX_FILES = 400;
|
|
18639
|
+
function listTextFilesUnder(dirRel, out, budget) {
|
|
18640
|
+
let entries;
|
|
18641
|
+
try {
|
|
18642
|
+
entries = (0, import_node_fs20.readdirSync)((0, import_node_path20.join)(process.cwd(), dirRel), { withFileTypes: true });
|
|
18643
|
+
} catch {
|
|
18644
|
+
return;
|
|
18645
|
+
}
|
|
18646
|
+
for (const ent of entries) {
|
|
18647
|
+
if (budget.count >= ENCODING_SCAN_MAX_FILES) return;
|
|
18648
|
+
const rel = `${dirRel}/${ent.name}`;
|
|
18649
|
+
if (ent.isDirectory()) {
|
|
18650
|
+
if (ent.name === "node_modules" || ent.name === "dist" || ent.name === ".git" || ent.name.startsWith(".")) continue;
|
|
18651
|
+
listTextFilesUnder(rel, out, budget);
|
|
18652
|
+
} else if (ent.isFile()) {
|
|
18653
|
+
const ext = ent.name.slice(ent.name.lastIndexOf("."));
|
|
18654
|
+
if (ENCODING_SCAN_EXTS.has(ext)) {
|
|
18655
|
+
out.push(rel);
|
|
18656
|
+
budget.count++;
|
|
18657
|
+
}
|
|
18658
|
+
}
|
|
18659
|
+
}
|
|
18660
|
+
}
|
|
18661
|
+
function managedFileEncodingFindings() {
|
|
18662
|
+
const cwd = process.cwd();
|
|
18663
|
+
const paths = [];
|
|
18664
|
+
for (const f of ENCODING_SCAN_ROOT_FILES) {
|
|
18665
|
+
try {
|
|
18666
|
+
if ((0, import_node_fs20.existsSync)((0, import_node_path20.join)(cwd, f))) paths.push(f);
|
|
18667
|
+
} catch {
|
|
18668
|
+
}
|
|
18669
|
+
}
|
|
18670
|
+
const budget = { count: 0 };
|
|
18671
|
+
for (const d of ENCODING_SCAN_DIRS) listTextFilesUnder(d, paths, budget);
|
|
18672
|
+
const findings = [];
|
|
18673
|
+
for (const rel of paths) {
|
|
18674
|
+
try {
|
|
18675
|
+
const probe = inspectFileEncoding((0, import_node_fs20.readFileSync)((0, import_node_path20.join)(cwd, rel)));
|
|
18676
|
+
if (probe.bom || probe.invalidUtf8 || probe.replacementChars) findings.push({ path: rel, ...probe });
|
|
18677
|
+
} catch {
|
|
18678
|
+
}
|
|
18679
|
+
}
|
|
18680
|
+
return findings;
|
|
18681
|
+
}
|
|
18682
|
+
async function branchAheadBehind(branch) {
|
|
18683
|
+
const spec = `origin/${branch}...${branch}`;
|
|
18684
|
+
try {
|
|
18685
|
+
const { stdout } = await execFileP2("git", ["rev-list", "--left-right", "--count", spec], { timeout: GIT_TIMEOUT_MS });
|
|
18686
|
+
const [behind, ahead] = stdout.trim().split(/\s+/).map((n) => Number.parseInt(n, 10));
|
|
18687
|
+
if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
|
|
18688
|
+
return { behind, ahead };
|
|
18689
|
+
} catch {
|
|
18690
|
+
return null;
|
|
18691
|
+
}
|
|
18692
|
+
}
|
|
18693
|
+
async function localReleaseBranchSnapshots() {
|
|
18694
|
+
let local;
|
|
18695
|
+
try {
|
|
18696
|
+
const { stdout } = await execFileP2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS });
|
|
18697
|
+
local = stdout.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
|
|
18698
|
+
} catch {
|
|
18699
|
+
return [];
|
|
18700
|
+
}
|
|
18701
|
+
const releaseBranches = local.filter((b) => b === "development" || b === "main" || b === "master" || b === "rc" || b.startsWith("release/"));
|
|
18702
|
+
const rows = [];
|
|
18703
|
+
for (const branch of releaseBranches) {
|
|
18704
|
+
const ab = await branchAheadBehind(branch);
|
|
18705
|
+
if (ab) rows.push({ name: branch, ...ab });
|
|
18706
|
+
}
|
|
18707
|
+
return rows;
|
|
18708
|
+
}
|
|
17997
18709
|
async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
17998
18710
|
if (opts.guide) {
|
|
17999
18711
|
if (opts.json) io.log(JSON.stringify({ resources: [MMI_AGENTIC_ONBOARDING_GUIDE] }, null, 2));
|
|
18000
18712
|
else io.log(MMI_AGENTIC_ONBOARDING_GUIDE.url);
|
|
18001
18713
|
return;
|
|
18002
18714
|
}
|
|
18003
|
-
const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
|
|
18715
|
+
const repairLocal = !opts.fast && (!opts.json || Boolean(opts.apply) || Boolean(opts.preflight));
|
|
18004
18716
|
const repoWritesAllowed = !opts.noRepoWrites;
|
|
18005
|
-
const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
|
|
18717
|
+
const runExtended = !opts.fast && (Boolean(opts.verbose) || Boolean(opts.json));
|
|
18718
|
+
const slowSkippedCount = opts.fast ? DOCTOR_EXTENDED_CHECK_LABELS.size : 0;
|
|
18006
18719
|
const checks = [];
|
|
18007
18720
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
18008
18721
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
18009
18722
|
const [login, pathProbe, releasedVersionResolution, cfg, callerArn, cloneProbe, isOrgRepo] = await Promise.all([
|
|
18010
|
-
githubLogin(),
|
|
18723
|
+
opts.fast ? Promise.resolve(void 0) : githubLogin(),
|
|
18011
18724
|
execFileP2(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
|
|
18012
|
-
fetchReleasedVersion(),
|
|
18725
|
+
opts.fast ? Promise.resolve({ version: void 0, source: "unavailable" }) : fetchReleasedVersion(),
|
|
18013
18726
|
loadConfig(),
|
|
18014
|
-
awsCallerArn(),
|
|
18727
|
+
opts.fast ? Promise.resolve(void 0) : awsCallerArn(),
|
|
18015
18728
|
execFileP2("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false),
|
|
18016
18729
|
// unset → repair below
|
|
18017
18730
|
// hub-v3 step-back: org-membership gates the repo-local checks/repairs by the git `origin` remote,
|
|
@@ -18030,7 +18743,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18030
18743
|
const semverPrefix = /^\d+\.\d+\.\d+/;
|
|
18031
18744
|
const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
|
|
18032
18745
|
const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
|
|
18033
|
-
const cursorCacheStale = (0,
|
|
18746
|
+
const cursorCacheStale = (0, import_node_fs20.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
|
|
18034
18747
|
const healPlan = doctorHealPlan({
|
|
18035
18748
|
isOrgRepo,
|
|
18036
18749
|
surface,
|
|
@@ -18052,20 +18765,22 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18052
18765
|
if (opts.preflight && !healPlan.needsEagerHeal) return;
|
|
18053
18766
|
const eagerHeal = Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal;
|
|
18054
18767
|
if (eagerHeal && healPlan.needsEagerHeal) io.err(DOCTOR_PREFLIGHT_NOTICE);
|
|
18055
|
-
const repairFull = !opts.json && !opts.banner || Boolean(opts.apply) || Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal;
|
|
18056
|
-
|
|
18057
|
-
|
|
18058
|
-
|
|
18059
|
-
|
|
18060
|
-
|
|
18061
|
-
|
|
18768
|
+
const repairFull = !opts.fast && (!opts.json && !opts.banner || Boolean(opts.apply) || Boolean(opts.preflight) || Boolean(opts.banner) && healPlan.needsEagerHeal);
|
|
18769
|
+
if (!opts.fast) {
|
|
18770
|
+
let ghInstalled = true;
|
|
18771
|
+
if (!login) {
|
|
18772
|
+
try {
|
|
18773
|
+
await execFileP2("gh", ["--version"]);
|
|
18774
|
+
} catch {
|
|
18775
|
+
ghInstalled = false;
|
|
18776
|
+
}
|
|
18062
18777
|
}
|
|
18778
|
+
checks.push(buildGithubAuthCheck({ login, ghInstalled }));
|
|
18063
18779
|
}
|
|
18064
|
-
checks.push(buildGithubAuthCheck({ login, ghInstalled }));
|
|
18065
18780
|
let onPath = pathProbe;
|
|
18066
18781
|
if (!onPath) {
|
|
18067
18782
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
18068
|
-
if (root && (0,
|
|
18783
|
+
if (root && (0, import_node_fs20.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
18069
18784
|
}
|
|
18070
18785
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
18071
18786
|
const reloadHint = reloadAction(surface);
|
|
@@ -18108,7 +18823,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18108
18823
|
return;
|
|
18109
18824
|
}
|
|
18110
18825
|
checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
|
|
18111
|
-
const hubVersionInfo = await fetchHubVersionInfo(cfg.sagaApiUrl);
|
|
18826
|
+
const hubVersionInfo = opts.fast ? null : await fetchHubVersionInfo(cfg.sagaApiUrl);
|
|
18112
18827
|
const installedVersion = resolveClientVersion();
|
|
18113
18828
|
checks.push(
|
|
18114
18829
|
buildHubCompatCheck({
|
|
@@ -18126,6 +18841,12 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18126
18841
|
releasedVersion
|
|
18127
18842
|
})
|
|
18128
18843
|
);
|
|
18844
|
+
checks.push(
|
|
18845
|
+
buildCliVersionDriftCheck({
|
|
18846
|
+
currentVersion: installedVersion,
|
|
18847
|
+
releasedVersion
|
|
18848
|
+
})
|
|
18849
|
+
);
|
|
18129
18850
|
}
|
|
18130
18851
|
if (runExtended) {
|
|
18131
18852
|
checks.push(buildAwsCrossAccountCheck({ callerArn }));
|
|
@@ -18212,8 +18933,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18212
18933
|
checks.push(gitignoreCheck);
|
|
18213
18934
|
checks.push(buildRepoLocalWorktreeCheck({
|
|
18214
18935
|
isOrgRepo,
|
|
18215
|
-
hasRepoLocalWorktrees: (0,
|
|
18936
|
+
hasRepoLocalWorktrees: (0, import_node_fs20.existsSync)((0, import_node_path20.join)(process.cwd(), ".worktrees"))
|
|
18216
18937
|
}));
|
|
18938
|
+
if (isOrgRepo) {
|
|
18939
|
+
const pkg = readRepoJson("package.json");
|
|
18940
|
+
const lock = readRepoJson("package-lock.json");
|
|
18941
|
+
checks.push(buildLockfileOptionalsCheck({
|
|
18942
|
+
isOrgRepo,
|
|
18943
|
+
packageJsonOptionalDeps: pkg?.optionalDependencies ?? null,
|
|
18944
|
+
lockfilePackages: lock?.packages ?? null
|
|
18945
|
+
}));
|
|
18946
|
+
} else {
|
|
18947
|
+
checks.push(buildLockfileOptionalsCheck({ isOrgRepo, packageJsonOptionalDeps: null, lockfilePackages: null }));
|
|
18948
|
+
}
|
|
18217
18949
|
let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
|
|
18218
18950
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
18219
18951
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
@@ -18398,7 +19130,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18398
19130
|
return versions.sort((a, b) => compareVersions(b, a))[0];
|
|
18399
19131
|
};
|
|
18400
19132
|
const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
18401
|
-
const codexActiveVersion = () => isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
|
|
19133
|
+
const codexActiveVersion = () => !opts.fast && isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
|
|
18402
19134
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
18403
19135
|
isOrgRepo,
|
|
18404
19136
|
roots: mmiPluginCacheRootSnapshots(),
|
|
@@ -18501,7 +19233,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18501
19233
|
}
|
|
18502
19234
|
checks.push(nestedPluginTreeCheck);
|
|
18503
19235
|
const cursorCacheRoot = cursorPluginCacheRoot();
|
|
18504
|
-
const cursorCacheRootExists = (0,
|
|
19236
|
+
const cursorCacheRootExists = (0, import_node_fs20.existsSync)(cursorCacheRoot);
|
|
18505
19237
|
let cursorPins = cursorPluginCachePinSnapshots() ?? [];
|
|
18506
19238
|
checks.push(
|
|
18507
19239
|
buildCursorPluginCacheCleanupCheck({
|
|
@@ -18529,7 +19261,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18529
19261
|
cacheRootExists: cursorCacheRootExists,
|
|
18530
19262
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
18531
19263
|
execFileP: execFileP2,
|
|
18532
|
-
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0,
|
|
19264
|
+
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path20.join)((0, import_node_os7.tmpdir)(), prefix)),
|
|
18533
19265
|
log: (m) => io.err(m)
|
|
18534
19266
|
});
|
|
18535
19267
|
if (seeded) {
|
|
@@ -18606,6 +19338,23 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18606
19338
|
cacheVersions: sessionCacheVersions
|
|
18607
19339
|
})
|
|
18608
19340
|
);
|
|
19341
|
+
{
|
|
19342
|
+
let redactorFailureLines = [];
|
|
19343
|
+
try {
|
|
19344
|
+
redactorFailureLines = (0, import_node_fs20.readFileSync)(activityLogPath(process.cwd()), "utf8").split("\n");
|
|
19345
|
+
} catch {
|
|
19346
|
+
redactorFailureLines = [];
|
|
19347
|
+
}
|
|
19348
|
+
const redactorSummary = summarizeRedactorFailures(redactorFailureLines);
|
|
19349
|
+
checks.push(
|
|
19350
|
+
buildRedactorLivenessCheck({
|
|
19351
|
+
isOrgRepo,
|
|
19352
|
+
failedCount: redactorSummary.failedCount,
|
|
19353
|
+
lastFailedTs: redactorSummary.lastFailedTs,
|
|
19354
|
+
lastFailedAction: redactorSummary.lastFailedAction
|
|
19355
|
+
})
|
|
19356
|
+
);
|
|
19357
|
+
}
|
|
18609
19358
|
if (runExtended) {
|
|
18610
19359
|
checks.push(
|
|
18611
19360
|
buildBrowserArtifactsCheck({
|
|
@@ -18630,6 +19379,14 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18630
19379
|
checks.push(buildGitGcCheck(await gcPlan("origin", 200)));
|
|
18631
19380
|
} catch {
|
|
18632
19381
|
}
|
|
19382
|
+
try {
|
|
19383
|
+
checks.push(buildManagedFileEncodingCheck({ isOrgRepo, findings: managedFileEncodingFindings() }));
|
|
19384
|
+
} catch {
|
|
19385
|
+
}
|
|
19386
|
+
try {
|
|
19387
|
+
checks.push(buildStaleLocalBranchCheck({ isOrgRepo, branches: await localReleaseBranchSnapshots() }));
|
|
19388
|
+
} catch {
|
|
19389
|
+
}
|
|
18633
19390
|
} else if (opts.apply && repoWritesAllowed) {
|
|
18634
19391
|
const scratchRun = executeScratchGc(repoRoot, { apply: true });
|
|
18635
19392
|
if (scratchRun.applied?.pruned.length) {
|
|
@@ -18650,6 +19407,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
18650
19407
|
healedCount,
|
|
18651
19408
|
pluginReloadRequired,
|
|
18652
19409
|
reloadHint,
|
|
19410
|
+
slowSkippedCount,
|
|
18653
19411
|
// Surface-aware update report (#865): the per-surface version snapshot + copy-paste update recipes, so
|
|
18654
19412
|
// an agent told "make sure the CLI and plugin are up to date" can run the right command per surface and
|
|
18655
19413
|
// echo back an unambiguous version line (CLI / Claude plugin / Codex marketplace / Codex active cache).
|
|
@@ -18675,6 +19433,7 @@ function emitDoctorReport(opts, io, ctx) {
|
|
|
18675
19433
|
const { checks, surface, healedCount, pluginReloadRequired, reloadHint } = ctx;
|
|
18676
19434
|
const gaps = checks.filter((c) => !c.ok);
|
|
18677
19435
|
if (opts.preflight) {
|
|
19436
|
+
process.exitCode = doctorExitCode(checks);
|
|
18678
19437
|
const outcome = preflightOutcome({ gaps, needsEagerHeal: ctx.needsEagerHeal, surface });
|
|
18679
19438
|
if (outcome.line) io.err(outcome.line);
|
|
18680
19439
|
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
@@ -18701,7 +19460,8 @@ function emitDoctorReport(opts, io, ctx) {
|
|
|
18701
19460
|
healedCount,
|
|
18702
19461
|
pluginReloadRequired,
|
|
18703
19462
|
reloadHint,
|
|
18704
|
-
verbose: Boolean(opts.verbose)
|
|
19463
|
+
verbose: Boolean(opts.verbose),
|
|
19464
|
+
skippedSlowChecks: ctx.slowSkippedCount
|
|
18705
19465
|
})) io.log(line);
|
|
18706
19466
|
for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
|
|
18707
19467
|
}
|
|
@@ -18724,23 +19484,23 @@ function mergeGuardHook(settings) {
|
|
|
18724
19484
|
next.hooks = hooks;
|
|
18725
19485
|
return next;
|
|
18726
19486
|
}
|
|
18727
|
-
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0,
|
|
19487
|
+
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path20.join)((0, import_node_os7.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
|
|
18728
19488
|
function ensureUserScopeGuardHook(opts = {}) {
|
|
18729
19489
|
const path2 = opts.settingsPath ?? userScopeSettingsPath();
|
|
18730
19490
|
try {
|
|
18731
19491
|
let current = null;
|
|
18732
|
-
if ((0,
|
|
19492
|
+
if ((0, import_node_fs20.existsSync)(path2)) {
|
|
18733
19493
|
try {
|
|
18734
|
-
current = JSON.parse((0,
|
|
19494
|
+
current = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
|
|
18735
19495
|
} catch {
|
|
18736
19496
|
return "failed";
|
|
18737
19497
|
}
|
|
18738
19498
|
}
|
|
18739
19499
|
if (settingsHasGuardHook(current)) return "already";
|
|
18740
19500
|
const merged = mergeGuardHook(current);
|
|
18741
|
-
(0,
|
|
18742
|
-
if ((0,
|
|
18743
|
-
(0,
|
|
19501
|
+
(0, import_node_fs20.mkdirSync)((0, import_node_path20.dirname)(path2), { recursive: true });
|
|
19502
|
+
if ((0, import_node_fs20.existsSync)(path2)) (0, import_node_fs20.copyFileSync)(path2, `${path2}.bak`);
|
|
19503
|
+
(0, import_node_fs20.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
|
|
18744
19504
|
`, "utf8");
|
|
18745
19505
|
return "written";
|
|
18746
19506
|
} catch {
|
|
@@ -18877,7 +19637,7 @@ async function applyGcPlan(plan, remote) {
|
|
|
18877
19637
|
cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
|
|
18878
19638
|
beforeWorktrees,
|
|
18879
19639
|
startingPath: branch.worktreePath,
|
|
18880
|
-
pathExists: (p) => (0,
|
|
19640
|
+
pathExists: (p) => (0, import_node_fs21.existsSync)(p),
|
|
18881
19641
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
18882
19642
|
teardownWorktreeStage,
|
|
18883
19643
|
deferredStore,
|
|
@@ -18900,7 +19660,7 @@ async function applyGcPlan(plan, remote) {
|
|
|
18900
19660
|
for (const wt of plan.worktreeDirs) {
|
|
18901
19661
|
try {
|
|
18902
19662
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
18903
|
-
realpath: (path2) => (0,
|
|
19663
|
+
realpath: (path2) => (0, import_node_fs21.realpathSync)(path2)
|
|
18904
19664
|
});
|
|
18905
19665
|
if (!cleanupTarget.ok) {
|
|
18906
19666
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -18940,16 +19700,21 @@ async function requireFreshTrainCli(commandName) {
|
|
|
18940
19700
|
throw new Error(staleTrainCliMessage(report, commandName));
|
|
18941
19701
|
}
|
|
18942
19702
|
var program2 = new Command();
|
|
18943
|
-
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
|
|
19703
|
+
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)");
|
|
18944
19704
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
18945
|
-
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
18946
|
-
const path2 = (0,
|
|
18947
|
-
const current = (0,
|
|
19705
|
+
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) => {
|
|
19706
|
+
const path2 = (0, import_node_path21.join)(process.cwd(), ".gitignore");
|
|
19707
|
+
const current = (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null;
|
|
18948
19708
|
const plan = planManagedGitignore(current);
|
|
18949
19709
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
19710
|
+
if (opts.json) {
|
|
19711
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
19712
|
+
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
19713
|
+
return;
|
|
19714
|
+
}
|
|
18950
19715
|
if (opts.write) {
|
|
18951
19716
|
if (plan.changed) {
|
|
18952
|
-
(0,
|
|
19717
|
+
(0, import_node_fs21.writeFileSync)(path2, plan.content, "utf8");
|
|
18953
19718
|
console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
|
|
18954
19719
|
} else {
|
|
18955
19720
|
console.log("mmi-cli rules gitignore: up to date");
|
|
@@ -19114,7 +19879,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
19114
19879
|
async function primaryCheckoutRoot(worktreeRoot) {
|
|
19115
19880
|
try {
|
|
19116
19881
|
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
19117
|
-
return out ? (0,
|
|
19882
|
+
return out ? (0, import_node_path21.dirname)(out) : void 0;
|
|
19118
19883
|
} catch {
|
|
19119
19884
|
return void 0;
|
|
19120
19885
|
}
|
|
@@ -19129,26 +19894,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
19129
19894
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
19130
19895
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
19131
19896
|
const take = () => {
|
|
19132
|
-
const fd = (0,
|
|
19897
|
+
const fd = (0, import_node_fs21.openSync)(lockPath, "wx");
|
|
19133
19898
|
try {
|
|
19134
|
-
(0,
|
|
19899
|
+
(0, import_node_fs21.writeSync)(fd, String(Date.now()));
|
|
19135
19900
|
} finally {
|
|
19136
|
-
(0,
|
|
19901
|
+
(0, import_node_fs21.closeSync)(fd);
|
|
19137
19902
|
}
|
|
19138
19903
|
return () => {
|
|
19139
19904
|
try {
|
|
19140
|
-
(0,
|
|
19905
|
+
(0, import_node_fs21.rmSync)(lockPath, { force: true });
|
|
19141
19906
|
} catch {
|
|
19142
19907
|
}
|
|
19143
19908
|
};
|
|
19144
19909
|
};
|
|
19145
19910
|
try {
|
|
19146
|
-
(0,
|
|
19911
|
+
(0, import_node_fs21.mkdirSync)((0, import_node_path21.dirname)(lockPath), { recursive: true });
|
|
19147
19912
|
return take();
|
|
19148
19913
|
} catch {
|
|
19149
19914
|
try {
|
|
19150
|
-
if (Date.now() - (0,
|
|
19151
|
-
(0,
|
|
19915
|
+
if (Date.now() - (0, import_node_fs21.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
19916
|
+
(0, import_node_fs21.rmSync)(lockPath, { force: true });
|
|
19152
19917
|
return take();
|
|
19153
19918
|
}
|
|
19154
19919
|
} catch {
|
|
@@ -19166,7 +19931,21 @@ worktree.command("create <branch>").description("create a worktree from a base r
|
|
|
19166
19931
|
const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
|
|
19167
19932
|
if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
|
|
19168
19933
|
}
|
|
19169
|
-
await
|
|
19934
|
+
await addWorktreeRobust(wtPath, branch, base, {
|
|
19935
|
+
git: async (args) => (await execFileP2("git", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
|
|
19936
|
+
revParse: async (ref) => {
|
|
19937
|
+
try {
|
|
19938
|
+
return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
|
|
19939
|
+
} catch {
|
|
19940
|
+
return void 0;
|
|
19941
|
+
}
|
|
19942
|
+
},
|
|
19943
|
+
deleteBranch: (b) => execFileP2("git", ["branch", "-D", b], { timeout: GIT_TIMEOUT_MS }).then(() => void 0),
|
|
19944
|
+
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
19945
|
+
log: (m) => {
|
|
19946
|
+
if (!o.json) console.error(` ${m}`);
|
|
19947
|
+
}
|
|
19948
|
+
});
|
|
19170
19949
|
const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
|
|
19171
19950
|
if (!o.json) console.error(` ${m}`);
|
|
19172
19951
|
}));
|
|
@@ -19285,7 +20064,7 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
19285
20064
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
19286
20065
|
function scheduleRelatedDiscovery(o) {
|
|
19287
20066
|
try {
|
|
19288
|
-
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
20067
|
+
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
|
|
19289
20068
|
if (o.repo) args.push("--repo", o.repo);
|
|
19290
20069
|
spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
|
|
19291
20070
|
} catch {
|
|
@@ -19638,7 +20417,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
19638
20417
|
const vars = rawValues("--var");
|
|
19639
20418
|
if (o.secretsFile) {
|
|
19640
20419
|
try {
|
|
19641
|
-
vars.push(`secrets=${(0,
|
|
20420
|
+
vars.push(`secrets=${(0, import_node_fs21.readFileSync)(o.secretsFile, "utf8")}`);
|
|
19642
20421
|
} catch (e) {
|
|
19643
20422
|
return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
19644
20423
|
}
|
|
@@ -19942,7 +20721,7 @@ issue.command("view <number>").description("read an issue as structured JSON \u2
|
|
|
19942
20721
|
return fail(`issue view: ${raw}${argvNote}`);
|
|
19943
20722
|
}
|
|
19944
20723
|
});
|
|
19945
|
-
issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
|
|
20724
|
+
issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").option("--fail-soft", "suppress errors (used by the background auto-caller; direct runs fail loudly)").action(async (o) => {
|
|
19946
20725
|
const number = Number(o.number);
|
|
19947
20726
|
if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
|
|
19948
20727
|
const repo = await resolveRepo(o.repo);
|
|
@@ -19974,7 +20753,10 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
19974
20753
|
]);
|
|
19975
20754
|
if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
|
|
19976
20755
|
await execFileP2("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
19977
|
-
} catch {
|
|
20756
|
+
} catch (e) {
|
|
20757
|
+
if (o.failSoft) return;
|
|
20758
|
+
const err = e;
|
|
20759
|
+
return fail(`issue discover-related: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
19978
20760
|
}
|
|
19979
20761
|
});
|
|
19980
20762
|
issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)").action(async (parentRef, childRef, o) => {
|
|
@@ -20193,14 +20975,16 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
20193
20975
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
20194
20976
|
console.log(JSON.stringify(created));
|
|
20195
20977
|
});
|
|
20196
|
-
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json
|
|
20978
|
+
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').action(async (number, o) => {
|
|
20197
20979
|
const n = Number(number);
|
|
20198
20980
|
if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
|
|
20199
20981
|
const repo = await resolveRepo(o.repo);
|
|
20200
20982
|
if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
|
|
20201
|
-
const fields =
|
|
20983
|
+
const fields = normalizeIssueViewJsonFields(o.json);
|
|
20984
|
+
const defaultPrFields = "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
|
|
20985
|
+
const effective = fields === normalizeIssueViewJsonFields(void 0) ? defaultPrFields : fields;
|
|
20202
20986
|
try {
|
|
20203
|
-
const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json",
|
|
20987
|
+
const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", effective]);
|
|
20204
20988
|
console.log(JSON.stringify(data));
|
|
20205
20989
|
} catch (e) {
|
|
20206
20990
|
const err = e;
|
|
@@ -20208,9 +20992,9 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
20208
20992
|
}
|
|
20209
20993
|
});
|
|
20210
20994
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
20211
|
-
const wfDir = (0,
|
|
20212
|
-
if (!(0,
|
|
20213
|
-
return (0,
|
|
20995
|
+
const wfDir = (0, import_node_path21.join)(cwd, ".github", "workflows");
|
|
20996
|
+
if (!(0, import_node_fs21.existsSync)(wfDir)) return [];
|
|
20997
|
+
return (0, import_node_fs21.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
20214
20998
|
}
|
|
20215
20999
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
20216
21000
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -20222,16 +21006,29 @@ async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
|
20222
21006
|
}
|
|
20223
21007
|
function ciAuditDeps() {
|
|
20224
21008
|
const cfgPromise = loadConfig();
|
|
21009
|
+
const root = hubRoot();
|
|
20225
21010
|
return {
|
|
20226
21011
|
client: defaultGitHubClient(),
|
|
20227
21012
|
listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
|
|
20228
21013
|
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
|
|
20229
|
-
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
|
|
20230
|
-
//
|
|
20231
|
-
//
|
|
20232
|
-
|
|
21014
|
+
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
|
|
21015
|
+
// Resolve from the installed package root so the reconcile works from any org repo checkout; when the
|
|
21016
|
+
// Hub root is not locatable (e.g. a global npm install without a local checkout), return null so the
|
|
21017
|
+
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
21018
|
+
readSeedFile: (path2) => {
|
|
21019
|
+
if (!root) return null;
|
|
21020
|
+
const fullPath = (0, import_node_path21.join)(root, path2);
|
|
21021
|
+
return (0, import_node_fs21.existsSync)(fullPath) ? (0, import_node_fs21.readFileSync)(fullPath, "utf8") : null;
|
|
21022
|
+
}
|
|
20233
21023
|
};
|
|
20234
21024
|
}
|
|
21025
|
+
function hubRoot() {
|
|
21026
|
+
const fromPkg = (0, import_node_path21.join)(__dirname, "..", "..");
|
|
21027
|
+
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
21028
|
+
if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(fromPkg, marker))) return fromPkg;
|
|
21029
|
+
if ((0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), marker))) return process.cwd();
|
|
21030
|
+
return null;
|
|
21031
|
+
}
|
|
20235
21032
|
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) => {
|
|
20236
21033
|
const result = await resolveMergeCiPolicyForCheckout(o.repo);
|
|
20237
21034
|
if (o.json) return printLine(JSON.stringify(result));
|
|
@@ -20305,6 +21102,12 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
20305
21102
|
checksPassing: checks === "success" || checks === "no-checks-reported"
|
|
20306
21103
|
};
|
|
20307
21104
|
},
|
|
21105
|
+
readPrState: async (prNumber, repo) => {
|
|
21106
|
+
const args = repo ? ["--repo", repo] : [];
|
|
21107
|
+
const result2 = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
|
|
21108
|
+
if (!result2.ok) throw new Error(`could not read PR state: ${result2.error}`);
|
|
21109
|
+
return JSON.parse(result2.state);
|
|
21110
|
+
},
|
|
20308
21111
|
mergeAuto: async (prNumber, repo) => {
|
|
20309
21112
|
const args = repo ? ["--repo", repo] : [];
|
|
20310
21113
|
const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
|
|
@@ -20399,7 +21202,7 @@ async function createDeferredWorktreeStore() {
|
|
|
20399
21202
|
},
|
|
20400
21203
|
write: async (entries) => {
|
|
20401
21204
|
try {
|
|
20402
|
-
await (0, import_promises3.mkdir)((0,
|
|
21205
|
+
await (0, import_promises3.mkdir)((0, import_node_path21.dirname)(registryPath), { recursive: true });
|
|
20403
21206
|
await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
20404
21207
|
} catch {
|
|
20405
21208
|
}
|
|
@@ -20413,13 +21216,13 @@ var realWorktreeDirRemover = {
|
|
|
20413
21216
|
probe: (p) => {
|
|
20414
21217
|
let st;
|
|
20415
21218
|
try {
|
|
20416
|
-
st = (0,
|
|
21219
|
+
st = (0, import_node_fs21.lstatSync)(p);
|
|
20417
21220
|
} catch {
|
|
20418
21221
|
return null;
|
|
20419
21222
|
}
|
|
20420
21223
|
if (st.isSymbolicLink()) return "link";
|
|
20421
21224
|
try {
|
|
20422
|
-
(0,
|
|
21225
|
+
(0, import_node_fs21.readlinkSync)(p);
|
|
20423
21226
|
return "link";
|
|
20424
21227
|
} catch {
|
|
20425
21228
|
}
|
|
@@ -20427,7 +21230,7 @@ var realWorktreeDirRemover = {
|
|
|
20427
21230
|
},
|
|
20428
21231
|
readdir: (p) => {
|
|
20429
21232
|
try {
|
|
20430
|
-
return (0,
|
|
21233
|
+
return (0, import_node_fs21.readdirSync)(p);
|
|
20431
21234
|
} catch {
|
|
20432
21235
|
return [];
|
|
20433
21236
|
}
|
|
@@ -20436,9 +21239,9 @@ var realWorktreeDirRemover = {
|
|
|
20436
21239
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
20437
21240
|
detachLink: (p) => {
|
|
20438
21241
|
try {
|
|
20439
|
-
(0,
|
|
21242
|
+
(0, import_node_fs21.rmdirSync)(p);
|
|
20440
21243
|
} catch {
|
|
20441
|
-
(0,
|
|
21244
|
+
(0, import_node_fs21.unlinkSync)(p);
|
|
20442
21245
|
}
|
|
20443
21246
|
},
|
|
20444
21247
|
removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -20468,9 +21271,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
20468
21271
|
}
|
|
20469
21272
|
}
|
|
20470
21273
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
20471
|
-
if (!(0,
|
|
21274
|
+
if (!(0, import_node_fs21.existsSync)(statePath)) return false;
|
|
20472
21275
|
try {
|
|
20473
|
-
const state = JSON.parse((0,
|
|
21276
|
+
const state = JSON.parse((0, import_node_fs21.readFileSync)(statePath, "utf8"));
|
|
20474
21277
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
20475
21278
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
20476
21279
|
} catch {
|
|
@@ -20542,7 +21345,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
20542
21345
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
20543
21346
|
beforeWorktrees,
|
|
20544
21347
|
startingPath,
|
|
20545
|
-
pathExists: (p) => (0,
|
|
21348
|
+
pathExists: (p) => (0, import_node_fs21.existsSync)(p),
|
|
20546
21349
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
20547
21350
|
teardownWorktreeStage,
|
|
20548
21351
|
deferredStore,
|
|
@@ -20763,7 +21566,7 @@ function stageScopedRunOpts(o) {
|
|
|
20763
21566
|
};
|
|
20764
21567
|
}
|
|
20765
21568
|
function printLine(value) {
|
|
20766
|
-
(0,
|
|
21569
|
+
(0, import_node_fs21.writeSync)(1, `${value}
|
|
20767
21570
|
`);
|
|
20768
21571
|
}
|
|
20769
21572
|
function stageKeepAlive() {
|
|
@@ -20777,8 +21580,8 @@ async function resolveStage() {
|
|
|
20777
21580
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
20778
21581
|
return decideStage({
|
|
20779
21582
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
20780
|
-
hasCompose: (0,
|
|
20781
|
-
hasEnvExample: (0,
|
|
21583
|
+
hasCompose: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), "docker-compose.yml")),
|
|
21584
|
+
hasEnvExample: (0, import_node_fs21.existsSync)((0, import_node_path21.join)(process.cwd(), ".env.example"))
|
|
20782
21585
|
});
|
|
20783
21586
|
}
|
|
20784
21587
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -20819,10 +21622,10 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
20819
21622
|
printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
|
|
20820
21623
|
return;
|
|
20821
21624
|
}
|
|
20822
|
-
const infra = resolvePortRangeInfra(process.cwd());
|
|
21625
|
+
const infra = resolvePortRangeInfra(process.cwd(), __dirname);
|
|
20823
21626
|
if (!infra) {
|
|
20824
21627
|
return failGraceful(
|
|
20825
|
-
`port-range: no MMI-Hub allocator files found
|
|
21628
|
+
`port-range: no MMI-Hub allocator files found (checked cwd ${process.cwd()}, sibling MMI-Hub dirs, and the installed package location); ensure the Hub's infra/port-ranges.json and infra/port-ddb.mjs are reachable`
|
|
20826
21629
|
);
|
|
20827
21630
|
}
|
|
20828
21631
|
const path2 = infra.registryPath;
|
|
@@ -21216,7 +22019,7 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
|
|
|
21216
22019
|
if (!audit.ok) process.exitCode = 1;
|
|
21217
22020
|
});
|
|
21218
22021
|
var wiki = program2.command("wiki").description("release wiki lane \u2014 publish generated pages via a short-lived, repo-scoped minted token");
|
|
21219
|
-
wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git; the token stays in memory (never printed/committed). Runs from any org repo checkout \u2014 the publisher ships inside mmi-cli, no repo-local script required. Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (pagesDir, o) => {
|
|
22022
|
+
wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git; the token stays in memory (never printed/committed). Runs from any org repo checkout \u2014 the publisher ships inside mmi-cli, no repo-local script required. Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").option("--json", "machine-readable output").action(async (pagesDir, o) => {
|
|
21220
22023
|
const repo = await resolveRepo(o.repo);
|
|
21221
22024
|
if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
|
|
21222
22025
|
const cfg = await loadConfig();
|
|
@@ -21227,13 +22030,33 @@ wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:
|
|
|
21227
22030
|
log: (msg) => console.log(msg),
|
|
21228
22031
|
err: (msg) => console.error(msg)
|
|
21229
22032
|
},
|
|
21230
|
-
{ repo, pagesDir }
|
|
22033
|
+
{ repo, pagesDir, json: o.json }
|
|
22034
|
+
);
|
|
22035
|
+
if (!ok) process.exitCode = 1;
|
|
22036
|
+
});
|
|
22037
|
+
wiki.command("plan").description("compute the release wiki plan for the cwd repo \u2014 collect its opt-in `wiki:page` pages, read the previous incremental stamp from <repo>.wiki.git (scoped minted token), and print a JSON plan naming which pages changed plus the fresh full stamp. Runs from ANY org repo checkout (no repo-local script). Fails loud on the credential/read gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (o) => {
|
|
22038
|
+
const repo = await resolveRepo(o.repo);
|
|
22039
|
+
if (!repo) return fail("wiki plan: could not determine the target repo (pass --repo owner/name)");
|
|
22040
|
+
const cfg = await loadConfig();
|
|
22041
|
+
const readers = fsReaders(process.cwd());
|
|
22042
|
+
const ok = await wikiPlan(
|
|
22043
|
+
{
|
|
22044
|
+
collectPages: () => collectFeaturePages(readers),
|
|
22045
|
+
readSource: (p) => readers.readDoc(p),
|
|
22046
|
+
mintAndReadStamp: async (r) => {
|
|
22047
|
+
const minted = await mintWikiToken(r, registryClientDeps(cfg));
|
|
22048
|
+
if (!minted.ok) return { ok: false, error: minted.error };
|
|
22049
|
+
return readWikiStampViaGit(realWikiGitPublishDeps(), { repo: r, token: minted.mint.token });
|
|
22050
|
+
},
|
|
22051
|
+
out: (json) => console.log(json),
|
|
22052
|
+
err: (msg) => console.error(msg)
|
|
22053
|
+
},
|
|
22054
|
+
{ repo }
|
|
21231
22055
|
);
|
|
21232
22056
|
if (!ok) process.exitCode = 1;
|
|
21233
22057
|
});
|
|
21234
|
-
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").
|
|
22058
|
+
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").action((o) => {
|
|
21235
22059
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
21236
|
-
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|
|
21237
22060
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
|
|
21238
22061
|
const steps = bootstrapPlan(o.repo, o.class);
|
|
21239
22062
|
console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
|
|
@@ -21250,7 +22073,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
21250
22073
|
client: defaultGitHubClient(),
|
|
21251
22074
|
projectMeta: meta,
|
|
21252
22075
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
21253
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
22076
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
|
|
21254
22077
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
21255
22078
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
21256
22079
|
requiredGcpApis: (() => {
|
|
@@ -21283,7 +22106,7 @@ bootstrap.command("org-ruleset").description("drift-check the codified org no-ag
|
|
|
21283
22106
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
21284
22107
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
21285
22108
|
});
|
|
21286
|
-
bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
22109
|
+
bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
21287
22110
|
const o = {
|
|
21288
22111
|
class: rawValue("--class", "deployable"),
|
|
21289
22112
|
projectType: rawValue("--project-type", ""),
|
|
@@ -21301,12 +22124,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
21301
22124
|
return fail(`bootstrap apply: ${e.message}`);
|
|
21302
22125
|
}
|
|
21303
22126
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
21304
|
-
if (!(0,
|
|
21305
|
-
const manifest = loadBootstrapSeeds((0,
|
|
22127
|
+
if (!(0, import_node_fs21.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`);
|
|
22128
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
|
|
21306
22129
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
21307
22130
|
const slug = parsedRepo.slug;
|
|
21308
22131
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
21309
|
-
const readFile2 = (p) => (0,
|
|
22132
|
+
const readFile2 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
|
|
21310
22133
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
21311
22134
|
const rawVars = {};
|
|
21312
22135
|
for (const value of rawValues("--var")) {
|
|
@@ -21603,16 +22426,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
21603
22426
|
const repoClass = o.class;
|
|
21604
22427
|
targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
|
|
21605
22428
|
} else {
|
|
21606
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
22429
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs21.existsSync)("projects.json") ? (0, import_node_fs21.readFileSync)("projects.json", "utf8") : null;
|
|
21607
22430
|
if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
|
|
21608
|
-
const fanoutJson = (0,
|
|
22431
|
+
const fanoutJson = (0, import_node_fs21.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs21.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
21609
22432
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
21610
22433
|
}
|
|
21611
22434
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
21612
|
-
const fileMatrix = (0,
|
|
22435
|
+
const fileMatrix = (0, import_node_fs21.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs21.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
21613
22436
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
21614
22437
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
21615
|
-
const fileContracts = (0,
|
|
22438
|
+
const fileContracts = (0, import_node_fs21.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs21.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
21616
22439
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
21617
22440
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
21618
22441
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -21620,7 +22443,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
21620
22443
|
});
|
|
21621
22444
|
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)));
|
|
21622
22445
|
var isWin2 = process.platform === "win32";
|
|
21623
|
-
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("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "run extended audit checks and print the full checklist + version report (#1989)").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)").action((opts) => (
|
|
22446
|
+
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", "run extended audit checks and print the full checklist + version report (#1989)").option("--ci", "stable CI output contract; no color, exit 0 for clear/advisory-only, 1 for hard gaps, 2 for doctor/tool failure").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)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n 2 doctor/tool execution failed before a normal verdict\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action((opts) => (
|
|
21624
22447
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
21625
22448
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
21626
22449
|
));
|
|
@@ -21629,9 +22452,13 @@ program2.command("plugin-heal").description("reinstall + re-enable the MMI plugi
|
|
|
21629
22452
|
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
21630
22453
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
21631
22454
|
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
22455
|
+
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
|
|
22456
|
+
return;
|
|
22457
|
+
}
|
|
22458
|
+
if (!await isOrgRepoRoot()) {
|
|
22459
|
+
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (non-org repo)" });
|
|
21632
22460
|
return;
|
|
21633
22461
|
}
|
|
21634
|
-
if (!await isOrgRepoRoot()) return;
|
|
21635
22462
|
spawnDeferredGcSweep();
|
|
21636
22463
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
21637
22464
|
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
@@ -21651,6 +22478,21 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
21651
22478
|
// never costs banner time and next session's glance renders instantly within budget.
|
|
21652
22479
|
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
21653
22480
|
}),
|
|
22481
|
+
// command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
|
|
22482
|
+
// not after a denied gh call. <1KB, pure in-memory — no network, no fs, fail-soft.
|
|
22483
|
+
commandLadderHint: async (io) => {
|
|
22484
|
+
const hint = commandLadderHint();
|
|
22485
|
+
if (hint) io.log(hint);
|
|
22486
|
+
},
|
|
22487
|
+
// local train sync (#2582): ff local development/main/rc to origin so this checkout never lags a
|
|
22488
|
+
// release pushed to origin (incl. the deferred main -> development alignment merge). Bounded git,
|
|
22489
|
+
// fail-soft, silent unless a branch moved.
|
|
22490
|
+
syncTrain: async (io) => {
|
|
22491
|
+
const gitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
22492
|
+
const result = await syncLocalTrainBranches(gitRun, { warn: (m) => io.err(`[mmi-hook] train sync: ${m}`) });
|
|
22493
|
+
const line = localTrainSyncBannerLine(result);
|
|
22494
|
+
if (line) io.log(line);
|
|
22495
|
+
},
|
|
21654
22496
|
doctor: (io) => runDoctor({ banner: true }, io)
|
|
21655
22497
|
});
|
|
21656
22498
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
@@ -21660,6 +22502,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
21660
22502
|
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
21661
22503
|
consoleIo.log(worktreeBanner);
|
|
21662
22504
|
}
|
|
22505
|
+
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
|
|
21663
22506
|
});
|
|
21664
22507
|
installProcessBackstop();
|
|
21665
22508
|
program2.parseAsync().catch((e) => failGraceful(e.message));
|