@mutmutco/cli 3.29.0 → 3.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +1030 -706
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3408,9 +3408,9 @@ function useColor() {
|
|
|
3408
3408
|
var program = new Command();
|
|
3409
3409
|
|
|
3410
3410
|
// src/index.ts
|
|
3411
|
-
var
|
|
3412
|
-
var
|
|
3413
|
-
var
|
|
3411
|
+
var import_promises6 = require("node:fs/promises");
|
|
3412
|
+
var import_node_fs27 = require("node:fs");
|
|
3413
|
+
var import_node_child_process12 = require("node:child_process");
|
|
3414
3414
|
|
|
3415
3415
|
// src/cli-shared.ts
|
|
3416
3416
|
var import_node_child_process3 = require("node:child_process");
|
|
@@ -4949,19 +4949,6 @@ async function fetchTrainAuthority(repo, deps) {
|
|
|
4949
4949
|
return { ok: false, error: e.message };
|
|
4950
4950
|
}
|
|
4951
4951
|
}
|
|
4952
|
-
async function mintWikiToken(repo, deps) {
|
|
4953
|
-
const res = await postJson("/wiki-mint", { repo }, deps, "POST", { noRetry: true });
|
|
4954
|
-
if (res.error) return { ok: false, error: res.error };
|
|
4955
|
-
const body = res.body ?? {};
|
|
4956
|
-
if (res.status === 426) return { ok: false, error: upgradeRequiredError({ status: 426 }, res.body) };
|
|
4957
|
-
if (res.status === 403) return { ok: false, error: `master-admin only (HTTP 403)${body.error ? ` \u2014 ${body.error}` : ""}` };
|
|
4958
|
-
if (res.status === 404) {
|
|
4959
|
-
return { ok: false, error: "the Hub API did not recognize /wiki-mint (HTTP 404) \u2014 the deployed Hub predates this command; it answers once a Hub release carrying it is deployed" };
|
|
4960
|
-
}
|
|
4961
|
-
if (!res.ok) return { ok: false, error: `wiki-mint HTTP ${res.status}${body.error ? ` \u2014 ${body.error}` : ""}` };
|
|
4962
|
-
if (!body.token || !body.expiresAt) return { ok: false, error: "malformed wiki-mint response (no token)" };
|
|
4963
|
-
return { ok: true, mint: { token: body.token, expiresAt: body.expiresAt, repository: body.repository ?? repo, permissions: body.permissions ?? {} } };
|
|
4964
|
-
}
|
|
4965
4952
|
async function fetchProjectsList(deps) {
|
|
4966
4953
|
if (!deps.baseUrl) return null;
|
|
4967
4954
|
const token = await deps.token();
|
|
@@ -5039,6 +5026,39 @@ async function fetchDeployFactsBySlug(slug, deps) {
|
|
|
5039
5026
|
return null;
|
|
5040
5027
|
}
|
|
5041
5028
|
}
|
|
5029
|
+
async function fetchSchedulesList(deps) {
|
|
5030
|
+
if (!deps.baseUrl) return null;
|
|
5031
|
+
const token = await deps.token();
|
|
5032
|
+
if (!token) return null;
|
|
5033
|
+
try {
|
|
5034
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/schedules/list`, {
|
|
5035
|
+
method: "GET",
|
|
5036
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
5037
|
+
});
|
|
5038
|
+
if (!res.ok) return null;
|
|
5039
|
+
const body = await res.json();
|
|
5040
|
+
return Array.isArray(body?.schedules) ? body.schedules : null;
|
|
5041
|
+
} catch {
|
|
5042
|
+
return null;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
async function fetchDocsAuditList(deps) {
|
|
5046
|
+
if (!deps.baseUrl) return { notArmed: true };
|
|
5047
|
+
try {
|
|
5048
|
+
const token = await deps.token();
|
|
5049
|
+
if (!token) return { notArmed: true };
|
|
5050
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/docs-audit/list`, {
|
|
5051
|
+
method: "GET",
|
|
5052
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
5053
|
+
});
|
|
5054
|
+
if (res.status === 404) return { notArmed: true };
|
|
5055
|
+
if (!res.ok) return { ok: false, error: `docs-audit list HTTP ${res.status}` };
|
|
5056
|
+
const body = await res.json();
|
|
5057
|
+
return { ok: true, rows: Array.isArray(body?.docsAudits) ? body.docsAudits : [] };
|
|
5058
|
+
} catch (e) {
|
|
5059
|
+
return { ok: false, error: e.message };
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5042
5062
|
async function fetchOrgConfig(deps) {
|
|
5043
5063
|
if (!deps.baseUrl) return null;
|
|
5044
5064
|
const token = await deps.token();
|
|
@@ -5092,6 +5112,9 @@ async function attestAppGaps(slug, repo, deps) {
|
|
|
5092
5112
|
async function setDeployCoords(slug, payload, deps) {
|
|
5093
5113
|
return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
|
|
5094
5114
|
}
|
|
5115
|
+
async function recordDocsAudit(verdict, deps) {
|
|
5116
|
+
return postJson("/docs-audit/record", { ...verdict }, deps);
|
|
5117
|
+
}
|
|
5095
5118
|
async function tenantControl(payload, deps) {
|
|
5096
5119
|
return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
|
|
5097
5120
|
}
|
|
@@ -5257,7 +5280,16 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
5257
5280
|
const backoff = deps.backoffMs ?? [250, 1e3];
|
|
5258
5281
|
let attempts = 0;
|
|
5259
5282
|
let lastError;
|
|
5260
|
-
|
|
5283
|
+
let safeForGitPrimary = true;
|
|
5284
|
+
if (deps.detachReparsePoints) {
|
|
5285
|
+
try {
|
|
5286
|
+
deps.detachReparsePoints(wtPath);
|
|
5287
|
+
} catch (e) {
|
|
5288
|
+
safeForGitPrimary = false;
|
|
5289
|
+
lastError = e;
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
for (let i = 0; safeForGitPrimary && i < maxAttempts; i++) {
|
|
5261
5293
|
attempts++;
|
|
5262
5294
|
try {
|
|
5263
5295
|
await deps.git(["worktree", "remove", "--force", wtPath]);
|
|
@@ -5977,6 +6009,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
5977
6009
|
const removeDeps = {
|
|
5978
6010
|
git,
|
|
5979
6011
|
sleep: options.sleep ?? defaultSleep,
|
|
6012
|
+
detachReparsePoints: options.detachReparsePoints,
|
|
6013
|
+
// #3064: junction-safe deferred sweep too
|
|
5980
6014
|
removeWorktreeDir: options.removeWorktreeDir
|
|
5981
6015
|
};
|
|
5982
6016
|
await sweepDeferredWorktrees(options.deferredStore, removeDeps).catch(() => void 0);
|
|
@@ -5995,6 +6029,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
5995
6029
|
const outcome = await removeWorktreeWithRecovery(wtPath, {
|
|
5996
6030
|
git,
|
|
5997
6031
|
sleep: options.sleep ?? defaultSleep,
|
|
6032
|
+
detachReparsePoints: options.detachReparsePoints,
|
|
6033
|
+
// #3064: reach the actual pr-merge teardown path
|
|
5998
6034
|
removeWorktreeDir: options.removeWorktreeDir
|
|
5999
6035
|
});
|
|
6000
6036
|
if (outcome.status === "removed") {
|
|
@@ -8152,7 +8188,9 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
8152
8188
|
}
|
|
8153
8189
|
const toStatus = options.toStatus ?? "Todo";
|
|
8154
8190
|
try {
|
|
8155
|
-
await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees
|
|
8191
|
+
await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
|
|
8192
|
+
body: { assignees: item.assignees }
|
|
8193
|
+
});
|
|
8156
8194
|
} catch (e) {
|
|
8157
8195
|
const warning = `partial unclaim: ${item.ref} assignees were not cleared (${ghError(e)})`;
|
|
8158
8196
|
if (!options.allowPartial) throw new Error(warning);
|
|
@@ -8994,7 +9032,7 @@ function commandLadderHint() {
|
|
|
8994
9032
|
}
|
|
8995
9033
|
|
|
8996
9034
|
// src/index.ts
|
|
8997
|
-
var
|
|
9035
|
+
var import_node_path23 = require("node:path");
|
|
8998
9036
|
|
|
8999
9037
|
// src/merge-ci-policy.ts
|
|
9000
9038
|
function resolveMergeCiPolicy(input) {
|
|
@@ -9512,9 +9550,6 @@ function seedMatchesProjectType(seed, projectType) {
|
|
|
9512
9550
|
return projectType != null && seed.projectTypes.includes(projectType);
|
|
9513
9551
|
}
|
|
9514
9552
|
function planSeedAction(seed, exists) {
|
|
9515
|
-
if (seed.source === "fanout") {
|
|
9516
|
-
return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
|
|
9517
|
-
}
|
|
9518
9553
|
if (seed.source === "managed-block") {
|
|
9519
9554
|
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
|
|
9520
9555
|
}
|
|
@@ -9620,7 +9655,6 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
9620
9655
|
name: vars.NAME || parsedRepo.name,
|
|
9621
9656
|
division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
|
|
9622
9657
|
repos: [`${parsedRepo.owner}/${slug}`],
|
|
9623
|
-
wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
|
|
9624
9658
|
branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
|
|
9625
9659
|
class: cls,
|
|
9626
9660
|
projectType,
|
|
@@ -9688,43 +9722,7 @@ function boardFieldsQueryArgs(projectId) {
|
|
|
9688
9722
|
const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
|
|
9689
9723
|
return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
|
|
9690
9724
|
}
|
|
9691
|
-
function
|
|
9692
|
-
return `${JSON.stringify(obj, null, 2)}
|
|
9693
|
-
`;
|
|
9694
|
-
}
|
|
9695
|
-
function planFanoutRegistration(fanoutTargetsRaw, projectsRaw, entry) {
|
|
9696
|
-
const fanout = JSON.parse(fanoutTargetsRaw);
|
|
9697
|
-
const projects = JSON.parse(projectsRaw);
|
|
9698
|
-
const fanoutRepos = Array.isArray(fanout.repos) ? fanout.repos : [];
|
|
9699
|
-
const projectEntries = Array.isArray(projects.projects) ? projects.projects : [];
|
|
9700
|
-
const name = entry.name ?? entry.repo;
|
|
9701
|
-
const canonName = entry.repo.toLowerCase();
|
|
9702
|
-
const inFanout = fanoutRepos.some((r) => typeof r.repo === "string" && r.repo.toLowerCase() === canonName);
|
|
9703
|
-
const inProjects = projectEntries.some(
|
|
9704
|
-
(p) => p.slug === entry.slug || Array.isArray(p.repos) && p.repos.some((full) => String(full).split("/").pop()?.toLowerCase() === canonName)
|
|
9705
|
-
);
|
|
9706
|
-
if (inFanout || inProjects) {
|
|
9707
|
-
return { changed: false, fanoutTargets: fanoutTargetsRaw, projects: projectsRaw };
|
|
9708
|
-
}
|
|
9709
|
-
const projectEntry = {
|
|
9710
|
-
name,
|
|
9711
|
-
slug: entry.slug,
|
|
9712
|
-
projectId: entry.projectId,
|
|
9713
|
-
wikiRepo: entry.wikiRepo,
|
|
9714
|
-
repos: [`mutmutco/${entry.repo}`]
|
|
9715
|
-
};
|
|
9716
|
-
if (entry.cls === "content") projectEntry.branch = "main";
|
|
9717
|
-
for (const k of Object.keys(projectEntry)) if (projectEntry[k] === void 0) delete projectEntry[k];
|
|
9718
|
-
const nextProjects = { ...projects, projects: [...projectEntries, projectEntry] };
|
|
9719
|
-
const fanoutEntry = { repo: entry.repo, branch: entry.branch, class: entry.cls };
|
|
9720
|
-
const nextFanout = { ...fanout, repos: [...fanoutRepos, fanoutEntry] };
|
|
9721
|
-
return {
|
|
9722
|
-
changed: true,
|
|
9723
|
-
fanoutTargets: serializeRegistry(nextFanout),
|
|
9724
|
-
projects: serializeRegistry(nextProjects)
|
|
9725
|
-
};
|
|
9726
|
-
}
|
|
9727
|
-
function decideFanoutPrAction(openPrs) {
|
|
9725
|
+
function decideSeedPrAction(openPrs) {
|
|
9728
9726
|
const list = Array.isArray(openPrs) ? openPrs : [];
|
|
9729
9727
|
for (const pr2 of list) {
|
|
9730
9728
|
const url = pr2?.url;
|
|
@@ -11055,7 +11053,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
|
|
|
11055
11053
|
}
|
|
11056
11054
|
|
|
11057
11055
|
// src/index.ts
|
|
11058
|
-
var
|
|
11056
|
+
var import_node_os8 = require("node:os");
|
|
11059
11057
|
|
|
11060
11058
|
// src/attach-to-project.ts
|
|
11061
11059
|
async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
|
|
@@ -11170,6 +11168,21 @@ function parseAddedItemId(stdout) {
|
|
|
11170
11168
|
function isAlreadyOnBoardError(stderr) {
|
|
11171
11169
|
return /already exists in (?:this|the) project/i.test(stderr);
|
|
11172
11170
|
}
|
|
11171
|
+
var CLOSING_KEYWORD = "clos(?:e|es|ed)|fix(?:es|ed)?|resolve(?:s|d)?";
|
|
11172
|
+
var CLOSING_CHAIN = new RegExp(
|
|
11173
|
+
`\\b(${CLOSING_KEYWORD})(\\s+)#(\\d+)((?:\\s*(?:,|,?\\s+and)\\s*#\\d+)+)`,
|
|
11174
|
+
"gi"
|
|
11175
|
+
);
|
|
11176
|
+
function normalizeClosingDirectives(body) {
|
|
11177
|
+
if (!body.includes("#")) return body;
|
|
11178
|
+
const segments = body.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
|
|
11179
|
+
return segments.map(
|
|
11180
|
+
(seg, i) => i % 2 === 1 ? seg : seg.replace(
|
|
11181
|
+
CLOSING_CHAIN,
|
|
11182
|
+
(_m, kw, ws, first, tail) => `${kw}${ws}#${first}${tail.replace(/#(\d+)/g, "closes #$1")}`
|
|
11183
|
+
)
|
|
11184
|
+
).join("");
|
|
11185
|
+
}
|
|
11173
11186
|
function buildPrArgs({ title, body, base, head, repo, draft }) {
|
|
11174
11187
|
const args = ["pr", "create"];
|
|
11175
11188
|
if (repo) args.push("--repo", repo);
|
|
@@ -11654,9 +11667,106 @@ function runningPluginVersion(env, cliVersion) {
|
|
|
11654
11667
|
if (leaf && isVersionDirName(leaf)) return leaf;
|
|
11655
11668
|
return cliVersion && isVersionDirName(cliVersion) ? cliVersion : void 0;
|
|
11656
11669
|
}
|
|
11670
|
+
function pluginCacheStagingRoot(home) {
|
|
11671
|
+
return `${home}/.claude/plugins/cache`;
|
|
11672
|
+
}
|
|
11673
|
+
function installedPluginsPath(home) {
|
|
11674
|
+
return `${home}/.claude/plugins/installed_plugins.json`;
|
|
11675
|
+
}
|
|
11676
|
+
var STAGING_DIR = /^temp_git_\d{13}_[a-z0-9]+$/i;
|
|
11677
|
+
function isStagingDirName(name) {
|
|
11678
|
+
return STAGING_DIR.test(name);
|
|
11679
|
+
}
|
|
11680
|
+
var STAGING_AGE_FLOOR_MS = 5 * 60 * 1e3;
|
|
11681
|
+
function installedPluginPaths(json) {
|
|
11682
|
+
const parsed = JSON.parse(json);
|
|
11683
|
+
if (parsed.plugins === void 0 || parsed.plugins === null) return null;
|
|
11684
|
+
const out = [];
|
|
11685
|
+
for (const entries of Object.values(parsed.plugins)) {
|
|
11686
|
+
for (const e of entries ?? []) if (e?.installPath) out.push(e.installPath);
|
|
11687
|
+
}
|
|
11688
|
+
return out;
|
|
11689
|
+
}
|
|
11690
|
+
function newestMtimeMs(path2, listDir, mtimeOf) {
|
|
11691
|
+
let newest = 0;
|
|
11692
|
+
try {
|
|
11693
|
+
newest = mtimeOf(path2);
|
|
11694
|
+
} catch {
|
|
11695
|
+
}
|
|
11696
|
+
let entries;
|
|
11697
|
+
try {
|
|
11698
|
+
entries = listDir(path2);
|
|
11699
|
+
} catch {
|
|
11700
|
+
return newest;
|
|
11701
|
+
}
|
|
11702
|
+
for (const e of entries) {
|
|
11703
|
+
const child = `${path2}/${e.name}`;
|
|
11704
|
+
let m = 0;
|
|
11705
|
+
if (e.isDirectory) m = newestMtimeMs(child, listDir, mtimeOf);
|
|
11706
|
+
else {
|
|
11707
|
+
try {
|
|
11708
|
+
m = mtimeOf(child);
|
|
11709
|
+
} catch {
|
|
11710
|
+
m = 0;
|
|
11711
|
+
}
|
|
11712
|
+
}
|
|
11713
|
+
if (m > newest) newest = m;
|
|
11714
|
+
}
|
|
11715
|
+
return newest;
|
|
11716
|
+
}
|
|
11717
|
+
function normStagingPath(p) {
|
|
11718
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
11719
|
+
}
|
|
11720
|
+
function stagingIsReferenced(dirPath, referenced) {
|
|
11721
|
+
const d = normStagingPath(dirPath);
|
|
11722
|
+
for (const r of referenced) {
|
|
11723
|
+
if (r === d || r.startsWith(`${d}/`)) return true;
|
|
11724
|
+
}
|
|
11725
|
+
return false;
|
|
11726
|
+
}
|
|
11727
|
+
function stagingDirIsSweepable(stagingRoot, entry, referencedPaths, now) {
|
|
11728
|
+
if (referencedPaths === null) return false;
|
|
11729
|
+
if (!isStagingDirName(entry.name)) return false;
|
|
11730
|
+
if (now - entry.mtimeMs < STAGING_AGE_FLOOR_MS) return false;
|
|
11731
|
+
const referenced = new Set(referencedPaths.map(normStagingPath));
|
|
11732
|
+
return !stagingIsReferenced(`${stagingRoot}/${entry.name}`, referenced);
|
|
11733
|
+
}
|
|
11734
|
+
function selectOrphanStagingDirs(stagingRoot, entries, referencedPaths, now, bytesOf) {
|
|
11735
|
+
if (referencedPaths === null) return [];
|
|
11736
|
+
return entries.filter((e) => stagingDirIsSweepable(stagingRoot, e, referencedPaths, now)).map((e) => ({ name: e.name, ageMs: now - e.mtimeMs, bytes: bytesOf(e.name) }));
|
|
11737
|
+
}
|
|
11738
|
+
function humanStagingAge(ms) {
|
|
11739
|
+
const minutes = Math.floor(ms / 6e4);
|
|
11740
|
+
if (minutes < 60) return `${minutes}m old`;
|
|
11741
|
+
const hours = Math.floor(minutes / 60);
|
|
11742
|
+
if (hours < 24) return `${hours}h old`;
|
|
11743
|
+
return `${Math.floor(hours / 24)}d old`;
|
|
11744
|
+
}
|
|
11657
11745
|
function buildPluginCachePlan(home, running, deps, opts = {}) {
|
|
11658
11746
|
const cacheRoot = pluginCacheRoot(home);
|
|
11659
|
-
const
|
|
11747
|
+
const stagingRoot = pluginCacheStagingRoot(home);
|
|
11748
|
+
let stagingEntries;
|
|
11749
|
+
try {
|
|
11750
|
+
stagingEntries = deps.listStagingDirs(stagingRoot);
|
|
11751
|
+
} catch {
|
|
11752
|
+
stagingEntries = [];
|
|
11753
|
+
}
|
|
11754
|
+
let referenced;
|
|
11755
|
+
try {
|
|
11756
|
+
referenced = deps.readInstalledPluginPaths();
|
|
11757
|
+
} catch {
|
|
11758
|
+
referenced = null;
|
|
11759
|
+
}
|
|
11760
|
+
const staging = selectOrphanStagingDirs(
|
|
11761
|
+
stagingRoot,
|
|
11762
|
+
stagingEntries,
|
|
11763
|
+
referenced,
|
|
11764
|
+
deps.now(),
|
|
11765
|
+
(name) => opts.withBytes ? deps.dirBytes(`${stagingRoot}/${name}`) : 0
|
|
11766
|
+
);
|
|
11767
|
+
const stagingBytes = staging.reduce((sum, s) => sum + s.bytes, 0);
|
|
11768
|
+
const stagingFields = { stagingRoot, staging, stagingBytes };
|
|
11769
|
+
const absent = { cacheRoot, present: false, keep: [], prune: [], bytes: 0, running, ...stagingFields };
|
|
11660
11770
|
if (!deps.exists(cacheRoot)) return absent;
|
|
11661
11771
|
let names;
|
|
11662
11772
|
try {
|
|
@@ -11669,9 +11779,9 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
|
|
|
11669
11779
|
const pruneSet = new Set(prune);
|
|
11670
11780
|
const keep = [...versions].sort((a, b) => compareVersions(b, a)).filter((v) => !pruneSet.has(v));
|
|
11671
11781
|
const bytes = opts.withBytes ? prune.reduce((sum, v) => sum + deps.dirBytes(`${cacheRoot}/${v}`), 0) : 0;
|
|
11672
|
-
return { cacheRoot, present: true, keep, prune, bytes, running };
|
|
11782
|
+
return { cacheRoot, present: true, keep, prune, bytes, running, ...stagingFields };
|
|
11673
11783
|
}
|
|
11674
|
-
function applyPluginCachePlan(plan, remove) {
|
|
11784
|
+
function applyPluginCachePlan(plan, remove, stagingGuard) {
|
|
11675
11785
|
const removed = [];
|
|
11676
11786
|
const failed = [];
|
|
11677
11787
|
for (const version of plan.prune) {
|
|
@@ -11682,28 +11792,85 @@ function applyPluginCachePlan(plan, remove) {
|
|
|
11682
11792
|
failed.push({ version, error: e.message });
|
|
11683
11793
|
}
|
|
11684
11794
|
}
|
|
11685
|
-
|
|
11795
|
+
const removedStaging = [];
|
|
11796
|
+
const failedStaging = [];
|
|
11797
|
+
const skippedStaging = [];
|
|
11798
|
+
const guardRefs = stagingGuard ? stagingGuard.referencedPaths() : void 0;
|
|
11799
|
+
const guardNow = stagingGuard ? stagingGuard.now() : 0;
|
|
11800
|
+
for (const s of plan.staging) {
|
|
11801
|
+
if (stagingGuard) {
|
|
11802
|
+
if (guardRefs == null) {
|
|
11803
|
+
skippedStaging.push({ name: s.name, reason: "reference set became unknown before apply" });
|
|
11804
|
+
continue;
|
|
11805
|
+
}
|
|
11806
|
+
const m = stagingGuard.mtimeMs(s.name);
|
|
11807
|
+
if (m === null) {
|
|
11808
|
+
skippedStaging.push({ name: s.name, reason: "dir vanished before apply" });
|
|
11809
|
+
continue;
|
|
11810
|
+
}
|
|
11811
|
+
if (!stagingDirIsSweepable(plan.stagingRoot, { name: s.name, mtimeMs: m }, guardRefs, guardNow)) {
|
|
11812
|
+
skippedStaging.push({ name: s.name, reason: "became referenced or active (under age floor) before apply" });
|
|
11813
|
+
continue;
|
|
11814
|
+
}
|
|
11815
|
+
}
|
|
11816
|
+
try {
|
|
11817
|
+
remove(`${plan.stagingRoot}/${s.name}`);
|
|
11818
|
+
removedStaging.push(s.name);
|
|
11819
|
+
} catch (e) {
|
|
11820
|
+
failedStaging.push({ name: s.name, error: e.message });
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
return {
|
|
11824
|
+
removed,
|
|
11825
|
+
failed,
|
|
11826
|
+
bytes: plan.bytes,
|
|
11827
|
+
removedStaging,
|
|
11828
|
+
failedStaging,
|
|
11829
|
+
skippedStaging,
|
|
11830
|
+
stagingBytes: plan.stagingBytes
|
|
11831
|
+
};
|
|
11686
11832
|
}
|
|
11687
11833
|
var CONCURRENT_SESSION_WARNING = "only the version THIS session runs from is protected \u2014 a concurrent Claude session running from a pruned version will break mid-session; close other sessions before --apply";
|
|
11688
11834
|
function renderPluginCachePlan(plan, applied) {
|
|
11689
|
-
if (!plan.present) return `plugin-prune: no plugin cache at ${plan.cacheRoot} \u2014 nothing to prune`;
|
|
11690
11835
|
const mb = (b) => `${(b / 1e6).toFixed(1)} MB`;
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11836
|
+
if (!plan.present && plan.staging.length === 0) {
|
|
11837
|
+
return `plugin-prune: no plugin cache at ${plan.cacheRoot} \u2014 nothing to prune`;
|
|
11838
|
+
}
|
|
11839
|
+
const lines = [];
|
|
11840
|
+
if (plan.present) {
|
|
11841
|
+
lines.push(` cache: ${plan.cacheRoot}`);
|
|
11842
|
+
const keepLabel = plan.keep.map((v) => v === plan.running ? `${v} (running)` : v).join(", ") || "(none)";
|
|
11843
|
+
lines.push(` keep ${keepLabel}`);
|
|
11844
|
+
if (plan.prune.length === 0) {
|
|
11845
|
+
lines.push(" prune nothing stale");
|
|
11846
|
+
} else {
|
|
11847
|
+
const size = plan.bytes > 0 ? `, ${mb(plan.bytes)}` : "";
|
|
11848
|
+
lines.push(` prune ${plan.prune.join(" ")} (${plan.prune.length} dirs${size})`);
|
|
11849
|
+
lines.push(` ! ${CONCURRENT_SESSION_WARNING}`);
|
|
11850
|
+
}
|
|
11851
|
+
}
|
|
11852
|
+
if (plan.staging.length > 0) {
|
|
11853
|
+
lines.push(` staging: ${plan.stagingRoot}`);
|
|
11854
|
+
for (const s of plan.staging) {
|
|
11855
|
+
const size = s.bytes > 0 ? `, ${mb(s.bytes)}` : "";
|
|
11856
|
+
lines.push(` orphan ${s.name} (${humanStagingAge(s.ageMs)}${size})`);
|
|
11857
|
+
}
|
|
11858
|
+
const total = plan.stagingBytes > 0 ? `, ${mb(plan.stagingBytes)}` : "";
|
|
11859
|
+
lines.push(` ${plan.staging.length} orphaned staging dir(s)${total}`);
|
|
11697
11860
|
}
|
|
11698
|
-
const size = plan.bytes > 0 ? `, ${mb(plan.bytes)}` : "";
|
|
11699
|
-
lines.push(` prune ${plan.prune.join(" ")} (${plan.prune.length} dirs${size})`);
|
|
11700
|
-
lines.push(` ! ${CONCURRENT_SESSION_WARNING}`);
|
|
11701
11861
|
if (!applied) {
|
|
11702
|
-
lines.push(" \u2192 re-run with --apply to delete");
|
|
11862
|
+
if (plan.prune.length > 0 || plan.staging.length > 0) lines.push(" \u2192 re-run with --apply to delete");
|
|
11703
11863
|
return lines.join("\n");
|
|
11704
11864
|
}
|
|
11705
|
-
|
|
11706
|
-
|
|
11865
|
+
if (plan.prune.length > 0) {
|
|
11866
|
+
lines.push(` removed ${applied.removed.length} version dir(s) (${mb(applied.bytes)})`);
|
|
11867
|
+
for (const f of applied.failed) lines.push(` \u2717 ${f.version}: ${f.error}`);
|
|
11868
|
+
}
|
|
11869
|
+
if (plan.staging.length > 0) {
|
|
11870
|
+
lines.push(` removed ${applied.removedStaging.length} staging dir(s) (${mb(applied.stagingBytes)})`);
|
|
11871
|
+
for (const f of applied.failedStaging) lines.push(` \u2717 ${f.name}: ${f.error}`);
|
|
11872
|
+
for (const s of applied.skippedStaging) lines.push(` ~ skipped ${s.name}: ${s.reason}`);
|
|
11873
|
+
}
|
|
11707
11874
|
return lines.join("\n");
|
|
11708
11875
|
}
|
|
11709
11876
|
|
|
@@ -11880,8 +12047,7 @@ function bootstrapPlan(repo, repoClass) {
|
|
|
11880
12047
|
{ label: `provision branch model: ${branchModel}`, gated: true },
|
|
11881
12048
|
{ label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
|
|
11882
12049
|
{ label: "attach GitHub Project v2 and register Hub registry META", gated: true },
|
|
11883
|
-
{ label: "seed README.md and architecture.md", gated: true }
|
|
11884
|
-
{ label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
|
|
12050
|
+
{ label: "seed README.md and architecture.md", gated: true }
|
|
11885
12051
|
];
|
|
11886
12052
|
}
|
|
11887
12053
|
function renderSteps(title, steps) {
|
|
@@ -13534,6 +13700,38 @@ function checkHotfixCoverage(options = {}) {
|
|
|
13534
13700
|
const uncovered = commits.filter((c) => c.coverage === "uncovered");
|
|
13535
13701
|
return { ok: uncovered.length === 0, mainRef, rcRef, commits, uncovered };
|
|
13536
13702
|
}
|
|
13703
|
+
function checkHotfixCarries(options) {
|
|
13704
|
+
const { cwd = process.cwd(), branch, baseRef, targets } = options;
|
|
13705
|
+
const git = options.git ?? ((args, opts) => (0, import_node_child_process8.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
13706
|
+
const isAncestor = (sha, ref) => {
|
|
13707
|
+
try {
|
|
13708
|
+
git(["merge-base", "--is-ancestor", sha, ref]);
|
|
13709
|
+
return true;
|
|
13710
|
+
} catch {
|
|
13711
|
+
return false;
|
|
13712
|
+
}
|
|
13713
|
+
};
|
|
13714
|
+
const revList = (range) => {
|
|
13715
|
+
const out = git(["rev-list", "--no-merges", range]).trim();
|
|
13716
|
+
return out ? out.split("\n") : [];
|
|
13717
|
+
};
|
|
13718
|
+
const cherrySources = (sha) => {
|
|
13719
|
+
const message = git(["log", "-1", "--format=%B", sha]);
|
|
13720
|
+
return [...message.matchAll(CHERRY_TRAILER)].map((m) => m[1]);
|
|
13721
|
+
};
|
|
13722
|
+
const carried = /* @__PURE__ */ new Set();
|
|
13723
|
+
for (const sha of revList(`${baseRef}..${branch}`)) {
|
|
13724
|
+
for (const source of cherrySources(sha)) carried.add(source);
|
|
13725
|
+
}
|
|
13726
|
+
const shaMatches = (a, b) => a.startsWith(b) || b.startsWith(a);
|
|
13727
|
+
const evaluated = targets.map((t) => {
|
|
13728
|
+
if (isAncestor(t.sha, branch)) return { ...t, carried: true, via: "ancestor" };
|
|
13729
|
+
if ([...carried].some((s) => shaMatches(s, t.sha))) return { ...t, carried: true, via: "trailer" };
|
|
13730
|
+
return { ...t, carried: false };
|
|
13731
|
+
});
|
|
13732
|
+
const missing = evaluated.filter((t) => !t.carried);
|
|
13733
|
+
return { ok: missing.length === 0, branch, baseRef, targets: evaluated, missing };
|
|
13734
|
+
}
|
|
13537
13735
|
|
|
13538
13736
|
// src/tenant-sweep.ts
|
|
13539
13737
|
function isRcBearingTenant(p) {
|
|
@@ -13635,6 +13833,7 @@ async function resolveHotfixDeployModel(deps, ctx) {
|
|
|
13635
13833
|
const meta = requireProjectMetaForTrain(load, ctx.repo);
|
|
13636
13834
|
return resolveDeployModel2(meta, ctx.repo);
|
|
13637
13835
|
}
|
|
13836
|
+
var HOTFIX_CARRIES_MARKER = /<!--\s*mmi-hotfix-carries:\s*([^>]+?)\s*-->/i;
|
|
13638
13837
|
async function findHotfixPr(deps, ctx, tag) {
|
|
13639
13838
|
const out = await deps.run("gh", [
|
|
13640
13839
|
"pr",
|
|
@@ -13648,7 +13847,7 @@ async function findHotfixPr(deps, ctx, tag) {
|
|
|
13648
13847
|
"--limit",
|
|
13649
13848
|
"50",
|
|
13650
13849
|
"--json",
|
|
13651
|
-
"number,state,url,mergeCommit,headRefName"
|
|
13850
|
+
"number,state,url,body,mergeCommit,headRefName"
|
|
13652
13851
|
]);
|
|
13653
13852
|
const rows = JSON.parse(out || "[]");
|
|
13654
13853
|
const branch = hotfixBranch(tag);
|
|
@@ -13670,6 +13869,31 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
13670
13869
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
13671
13870
|
return { sha, label: sha.slice(0, 7) };
|
|
13672
13871
|
}
|
|
13872
|
+
function splitCarrySpecs(values = []) {
|
|
13873
|
+
return values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
13874
|
+
}
|
|
13875
|
+
function carrySpecsFromPrBody(body) {
|
|
13876
|
+
const marker = HOTFIX_CARRIES_MARKER.exec(body ?? "");
|
|
13877
|
+
if (marker) return splitCarrySpecs([marker[1]]);
|
|
13878
|
+
const specs = [];
|
|
13879
|
+
for (const m of (body ?? "").matchAll(/\bPR\s+#(\d+)\b/gi)) specs.push(`#${m[1]}`);
|
|
13880
|
+
for (const m of (body ?? "").matchAll(/\b[0-9a-f]{7,40}\b/gi)) specs.push(m[0]);
|
|
13881
|
+
return [...new Set(specs)];
|
|
13882
|
+
}
|
|
13883
|
+
async function resolveHotfixCarryTargets(deps, ctx, pr2, explicit = []) {
|
|
13884
|
+
const specs = splitCarrySpecs(explicit).length > 0 ? splitCarrySpecs(explicit) : carrySpecsFromPrBody(pr2.body);
|
|
13885
|
+
const targets = [];
|
|
13886
|
+
for (const spec of specs) {
|
|
13887
|
+
const { sha, label } = await resolveHotfixSource(deps, ctx, spec);
|
|
13888
|
+
if (!targets.some((t) => t.sha === sha)) targets.push({ sha, label });
|
|
13889
|
+
}
|
|
13890
|
+
return targets;
|
|
13891
|
+
}
|
|
13892
|
+
async function hotfixBaseTagForRelease(deps, tag) {
|
|
13893
|
+
const tags = (await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--merged", "origin/main", "--sort=-v:refname"])).split("\n").map((s) => s.trim()).filter((t) => /^v\d+\.\d+\.\d+$/.test(t));
|
|
13894
|
+
const current = normalizeHotfixVersion(tag).tag;
|
|
13895
|
+
return tags.find((t) => compareHotfixVersions(t, current) < 0) ?? tags.find((t) => t !== current) ?? "origin/main^";
|
|
13896
|
+
}
|
|
13673
13897
|
async function runHotfixStart(deps, options) {
|
|
13674
13898
|
const ctx = await buildTrainApplyContext(deps);
|
|
13675
13899
|
const deployModel = await resolveHotfixDeployModel(deps, ctx);
|
|
@@ -13739,6 +13963,8 @@ async function runHotfixStart(deps, options) {
|
|
|
13739
13963
|
"--body",
|
|
13740
13964
|
`Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
|
|
13741
13965
|
|
|
13966
|
+
<!-- mmi-hotfix-carries: ${sha} -->
|
|
13967
|
+
|
|
13742
13968
|
Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
|
|
13743
13969
|
]));
|
|
13744
13970
|
notes.push(`opened hotfix PR ${prUrl} \u2014 merge it (human-initiated), then: mmi-cli hotfix release ${tag}`);
|
|
@@ -13795,6 +14021,15 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13795
14021
|
await deps.run("git", ["merge-base", "--is-ancestor", mergedSha, "origin/main"]).catch(() => {
|
|
13796
14022
|
throw new Error(`merged hotfix SHA ${mergedSha.slice(0, 7)} is not on origin/main \u2014 refusing to tag`);
|
|
13797
14023
|
});
|
|
14024
|
+
const carryTargets = await resolveHotfixCarryTargets(deps, ctx, pr2, options.carries);
|
|
14025
|
+
if (carryTargets.length > 0) {
|
|
14026
|
+
const baseRef = await hotfixBaseTagForRelease(deps, tag);
|
|
14027
|
+
const carries = deps.hotfixCarries ? deps.hotfixCarries({ branch: mergedSha, baseRef, targets: carryTargets }) : { ok: true, branch: mergedSha, baseRef, targets: carryTargets.map((t) => ({ ...t, carried: true, via: "ancestor" })), missing: [] };
|
|
14028
|
+
if (!carries.ok) {
|
|
14029
|
+
const missing = carries.missing.map((t) => t.label ?? t.sha.slice(0, 12)).join(", ");
|
|
14030
|
+
throw new Error(`hotfix-carries: ${carries.missing.length} declared fix target(s) missing from ${tag} before tagging: ${missing}. Cherry-pick the missing fix(es) with -x onto ${hotfixBranch(tag)}, merge that PR, then rerun hotfix release.`);
|
|
14031
|
+
}
|
|
14032
|
+
}
|
|
13798
14033
|
const tagPush = await ensureTagPushed(deps, tag, mergedSha);
|
|
13799
14034
|
const tagPushSince = Date.now();
|
|
13800
14035
|
const tagNote = tagPush.note;
|
|
@@ -14301,7 +14536,7 @@ async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @
|
|
|
14301
14536
|
branch,
|
|
14302
14537
|
kind: "app-bypass-missing",
|
|
14303
14538
|
severity: "high",
|
|
14304
|
-
detail: `the ${LOCKED_APP} App is missing from the ${branch} allowlist \u2014
|
|
14539
|
+
detail: `the ${LOCKED_APP} App is missing from the ${branch} allowlist \u2014 release promotions will break`,
|
|
14305
14540
|
remediation: `gh api -X POST repos/${repo}/branches/${branch}/protection/restrictions/apps -f apps[]="${LOCKED_APP}"`
|
|
14306
14541
|
});
|
|
14307
14542
|
}
|
|
@@ -14369,6 +14604,20 @@ async function auditPluginReadAccess(owners, projectAdmins, deps) {
|
|
|
14369
14604
|
return findings;
|
|
14370
14605
|
}
|
|
14371
14606
|
async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
|
|
14607
|
+
if (targets.length === 0) {
|
|
14608
|
+
return {
|
|
14609
|
+
ok: false,
|
|
14610
|
+
owners: [],
|
|
14611
|
+
orgFindings: [{
|
|
14612
|
+
repo: OWNER2,
|
|
14613
|
+
kind: "empty-target-inventory",
|
|
14614
|
+
severity: "high",
|
|
14615
|
+
detail: "access audit received an empty repo inventory \u2014 zero repos to audit; a green audit of nothing is a false pass",
|
|
14616
|
+
remediation: `confirm the Hub registry is reachable and returns projects, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
|
|
14617
|
+
}],
|
|
14618
|
+
repos: []
|
|
14619
|
+
};
|
|
14620
|
+
}
|
|
14372
14621
|
let owners;
|
|
14373
14622
|
try {
|
|
14374
14623
|
owners = new Set(await resolveOwners(deps));
|
|
@@ -14396,10 +14645,8 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
|
|
|
14396
14645
|
const ok = orgFindings.every((f) => f.severity !== "high") && repos.every((r) => r.ok);
|
|
14397
14646
|
return { ok, owners: [...owners], orgFindings, repos };
|
|
14398
14647
|
}
|
|
14399
|
-
function loadAccessTargets(projectsJson
|
|
14648
|
+
function loadAccessTargets(projectsJson) {
|
|
14400
14649
|
const projects = safeJson(projectsJson, {}).projects ?? [];
|
|
14401
|
-
const fanout = fanoutJson ? safeJson(fanoutJson, {}).repos ?? [] : [];
|
|
14402
|
-
const legacyContentNames = new Set(fanout.filter((r) => r.class === "content" && r.repo).map((r) => r.repo.toLowerCase()));
|
|
14403
14650
|
const seen = /* @__PURE__ */ new Set();
|
|
14404
14651
|
const targets = [];
|
|
14405
14652
|
for (const project2 of projects) {
|
|
@@ -14410,13 +14657,28 @@ function loadAccessTargets(projectsJson, fanoutJson) {
|
|
|
14410
14657
|
if (seen.has(repo)) continue;
|
|
14411
14658
|
seen.add(repo);
|
|
14412
14659
|
const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
|
|
14413
|
-
const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName)
|
|
14660
|
+
const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) ? "content" : "deployable";
|
|
14414
14661
|
const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
|
|
14415
14662
|
targets.push({ repo, class: cls, releaseTrack });
|
|
14416
14663
|
}
|
|
14417
14664
|
}
|
|
14418
14665
|
return targets;
|
|
14419
14666
|
}
|
|
14667
|
+
function resolveOrgAuditTargets(registryProjects) {
|
|
14668
|
+
if (registryProjects === null) {
|
|
14669
|
+
return {
|
|
14670
|
+
ok: false,
|
|
14671
|
+
finding: {
|
|
14672
|
+
repo: OWNER2,
|
|
14673
|
+
kind: "registry-unreadable",
|
|
14674
|
+
severity: "high",
|
|
14675
|
+
detail: "the project registry SSOT (Hub API) was unreachable or unconfigured \u2014 the audit target set is unknown; auditing the stale committed projects.json instead could pass green while missing live repos",
|
|
14676
|
+
remediation: `confirm the Hub API base URL + token are configured and reachable, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
|
|
14677
|
+
}
|
|
14678
|
+
};
|
|
14679
|
+
}
|
|
14680
|
+
return { ok: true, targets: loadAccessTargets(JSON.stringify({ projects: registryProjects })) };
|
|
14681
|
+
}
|
|
14420
14682
|
function loadAccessMatrix(matrixJson) {
|
|
14421
14683
|
if (!matrixJson) return {};
|
|
14422
14684
|
return safeJson(matrixJson, {}).projectAdmins ?? {};
|
|
@@ -14471,279 +14733,202 @@ function renderAccessReport(report) {
|
|
|
14471
14733
|
return lines.join("\n");
|
|
14472
14734
|
}
|
|
14473
14735
|
|
|
14474
|
-
// src/
|
|
14475
|
-
async function wikiPublish(deps, opts) {
|
|
14476
|
-
if (!opts.pagesDir) {
|
|
14477
|
-
if (opts.json) {
|
|
14478
|
-
deps.log(JSON.stringify({ ok: false, error: "pages-dir is required" }, null, 2));
|
|
14479
|
-
} else {
|
|
14480
|
-
deps.err("wiki publish: <pages-dir> is required");
|
|
14481
|
-
}
|
|
14482
|
-
return false;
|
|
14483
|
-
}
|
|
14484
|
-
const minted = await deps.mint(opts.repo);
|
|
14485
|
-
if (!minted.ok) {
|
|
14486
|
-
if (opts.json) {
|
|
14487
|
-
deps.log(JSON.stringify({ ok: false, error: minted.error }, null, 2));
|
|
14488
|
-
} else {
|
|
14489
|
-
deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
|
|
14490
|
-
}
|
|
14491
|
-
return false;
|
|
14492
|
-
}
|
|
14493
|
-
const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir, json: opts.json });
|
|
14494
|
-
if (!ok) {
|
|
14495
|
-
return false;
|
|
14496
|
-
}
|
|
14497
|
-
if (opts.json) {
|
|
14498
|
-
deps.log(JSON.stringify({ ok: true, repo: opts.repo }, null, 2));
|
|
14499
|
-
} else {
|
|
14500
|
-
deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
|
|
14501
|
-
}
|
|
14502
|
-
return true;
|
|
14503
|
-
}
|
|
14504
|
-
|
|
14505
|
-
// src/wiki-publish-git.ts
|
|
14506
|
-
var import_node_child_process9 = require("node:child_process");
|
|
14736
|
+
// src/docs-index-command.ts
|
|
14507
14737
|
var import_node_fs16 = require("node:fs");
|
|
14508
|
-
var import_node_os5 = require("node:os");
|
|
14509
14738
|
var import_node_path14 = require("node:path");
|
|
14510
|
-
var
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
copied++;
|
|
14533
|
-
}
|
|
14534
|
-
}
|
|
14535
|
-
if (!copied) {
|
|
14536
|
-
if (!json) deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
|
|
14537
|
-
return false;
|
|
14538
|
-
}
|
|
14539
|
-
if (deps.fileExists((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
|
|
14540
|
-
deps.copyFile((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path14.join)(dir, WIKI_STAMP_FILE));
|
|
14739
|
+
var DOCS_INDEX_PATH = "docs/index.md";
|
|
14740
|
+
var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
|
|
14741
|
+
function escapeCell(text) {
|
|
14742
|
+
return text.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
14743
|
+
}
|
|
14744
|
+
function extractDocMeta(relPath, raw) {
|
|
14745
|
+
const lines = raw.split(/\r?\n/);
|
|
14746
|
+
let i = 0;
|
|
14747
|
+
if (lines[i]?.trim() === "---") {
|
|
14748
|
+
i++;
|
|
14749
|
+
while (i < lines.length && lines[i].trim() !== "---") i++;
|
|
14750
|
+
if (i < lines.length) i++;
|
|
14751
|
+
}
|
|
14752
|
+
let title = "";
|
|
14753
|
+
let scope = "";
|
|
14754
|
+
for (; i < lines.length; i++) {
|
|
14755
|
+
const line = lines[i];
|
|
14756
|
+
const trimmed = line.trim();
|
|
14757
|
+
if (trimmed === "") continue;
|
|
14758
|
+
if (trimmed.startsWith("<!--")) {
|
|
14759
|
+
while (i < lines.length && !lines[i].includes("-->")) i++;
|
|
14760
|
+
continue;
|
|
14541
14761
|
}
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
return true;
|
|
14762
|
+
if (!title && trimmed.startsWith("# ")) {
|
|
14763
|
+
title = trimmed.slice(2).trim();
|
|
14764
|
+
continue;
|
|
14546
14765
|
}
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
"-c",
|
|
14550
|
-
"user.email=mmi-github-app[bot]@users.noreply.github.com",
|
|
14551
|
-
"-c",
|
|
14552
|
-
"user.name=mmi-github-app[bot]",
|
|
14553
|
-
"commit",
|
|
14554
|
-
"-m",
|
|
14555
|
-
"docs(wiki): refresh MMI Future project wiki"
|
|
14556
|
-
],
|
|
14557
|
-
dir
|
|
14558
|
-
);
|
|
14559
|
-
try {
|
|
14560
|
-
deps.gitRun([...authArgs, "push", "origin", "HEAD:master"], dir);
|
|
14561
|
-
} catch (e) {
|
|
14562
|
-
if (!json) deps.err(
|
|
14563
|
-
`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`
|
|
14564
|
-
);
|
|
14565
|
-
return false;
|
|
14766
|
+
if (!title) {
|
|
14767
|
+
break;
|
|
14566
14768
|
}
|
|
14567
|
-
if (
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
if (!json) deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
|
|
14571
|
-
return false;
|
|
14572
|
-
} finally {
|
|
14573
|
-
deps.rm(dir);
|
|
14769
|
+
if (trimmed.startsWith("#")) continue;
|
|
14770
|
+
scope = escapeCell(trimmed);
|
|
14771
|
+
break;
|
|
14574
14772
|
}
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
const url = `https://github.com/${opts.repo}.wiki.git`;
|
|
14579
|
-
const authArgs = wikiAuthArgs(opts.token);
|
|
14580
|
-
try {
|
|
14581
|
-
deps.gitRun(["init", "-q"], dir);
|
|
14582
|
-
deps.gitRun(["remote", "add", "origin", url], dir);
|
|
14583
|
-
if (!deps.gitProbe([...authArgs, "ls-remote", "origin"], dir)) {
|
|
14584
|
-
return { ok: false, error: `could not reach ${opts.repo}.wiki.git to read the wiki stamp \u2014 auth or network failure` };
|
|
14585
|
-
}
|
|
14586
|
-
if (!deps.gitProbe([...authArgs, "fetch", "--depth", "1", "origin", "master"], dir)) {
|
|
14587
|
-
return { ok: true, stamp: {} };
|
|
14588
|
-
}
|
|
14589
|
-
deps.gitRun(["reset", "--hard", "FETCH_HEAD"], dir);
|
|
14590
|
-
const stampPath = (0, import_node_path14.join)(dir, WIKI_STAMP_FILE);
|
|
14591
|
-
if (!deps.fileExists(stampPath)) return { ok: true, stamp: {} };
|
|
14592
|
-
let parsed;
|
|
14593
|
-
try {
|
|
14594
|
-
parsed = JSON.parse(deps.readFile(stampPath));
|
|
14595
|
-
} catch (e) {
|
|
14596
|
-
deps.err(
|
|
14597
|
-
`wiki stamp: ${WIKI_STAMP_FILE} in ${opts.repo}.wiki.git is unreadable (${redactWikiSecrets(e.message)}) \u2014 treating as absent (full backfill); the next wiki publish rewrites it`
|
|
14598
|
-
);
|
|
14599
|
-
return { ok: true, stamp: {} };
|
|
14600
|
-
}
|
|
14601
|
-
return { ok: true, stamp: parsed && typeof parsed === "object" ? parsed : {} };
|
|
14602
|
-
} catch (e) {
|
|
14603
|
-
return { ok: false, error: redactWikiSecrets(e.message) };
|
|
14604
|
-
} finally {
|
|
14605
|
-
deps.rm(dir);
|
|
14773
|
+
if (!title) {
|
|
14774
|
+
const base = relPath.split("/").pop() ?? relPath;
|
|
14775
|
+
title = base.replace(/\.md$/, "");
|
|
14606
14776
|
}
|
|
14777
|
+
return { path: relPath, title, scope };
|
|
14607
14778
|
}
|
|
14608
|
-
function
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
return false;
|
|
14622
|
-
}
|
|
14623
|
-
},
|
|
14624
|
-
gitRun: (args, cwd) => {
|
|
14625
|
-
(0, import_node_child_process9.execFileSync)("git", args, { cwd, stdio: "inherit" });
|
|
14626
|
-
},
|
|
14627
|
-
log: (msg) => console.log(msg),
|
|
14628
|
-
err: (msg) => console.error(msg)
|
|
14629
|
-
};
|
|
14630
|
-
}
|
|
14631
|
-
|
|
14632
|
-
// src/wiki-pages.ts
|
|
14633
|
-
var import_node_crypto4 = require("node:crypto");
|
|
14634
|
-
var import_node_fs17 = require("node:fs");
|
|
14635
|
-
var import_node_path15 = require("node:path");
|
|
14636
|
-
var MARKER_RE = /<!--\s*wiki:page\s+([^>]*?)-->/g;
|
|
14637
|
-
var attr = (raw, key) => {
|
|
14638
|
-
const m = new RegExp(`${key}\\s*=\\s*"([^"]*)"`).exec(raw);
|
|
14639
|
-
return m ? m[1].trim() : "";
|
|
14640
|
-
};
|
|
14641
|
-
var sanitizeSlug = (s) => String(s ?? "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
14642
|
-
function extractWikiMarkers(text, sourcePath = "") {
|
|
14643
|
-
const out = [];
|
|
14644
|
-
for (const m of String(text ?? "").matchAll(MARKER_RE)) {
|
|
14645
|
-
const raw = m[1];
|
|
14646
|
-
const slug = sanitizeSlug(attr(raw, "slug"));
|
|
14647
|
-
const title = attr(raw, "title");
|
|
14648
|
-
if (!slug || !title) continue;
|
|
14649
|
-
out.push({ slug, title, artifact: attr(raw, "artifact"), sourcePath });
|
|
14779
|
+
function renderDocsIndex(entries) {
|
|
14780
|
+
const lines = [
|
|
14781
|
+
GENERATED_HEADER,
|
|
14782
|
+
"# Documentation index",
|
|
14783
|
+
"",
|
|
14784
|
+
"Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
|
|
14785
|
+
"can never diverge from the records it lists.",
|
|
14786
|
+
"",
|
|
14787
|
+
"| Document | Scope |",
|
|
14788
|
+
"| --- | --- |"
|
|
14789
|
+
];
|
|
14790
|
+
for (const entry of entries) {
|
|
14791
|
+
lines.push(`| [${escapeCell(entry.title)}](${entry.path}) | ${entry.scope} |`);
|
|
14650
14792
|
}
|
|
14651
|
-
return
|
|
14793
|
+
return lines.join("\n") + "\n";
|
|
14652
14794
|
}
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
try {
|
|
14663
|
-
st = (0, import_node_fs17.statSync)(abs);
|
|
14664
|
-
} catch {
|
|
14665
|
-
return [];
|
|
14795
|
+
function docsIndex(deps, opts = {}) {
|
|
14796
|
+
const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
|
|
14797
|
+
const content = renderDocsIndex(entries);
|
|
14798
|
+
const current = deps.readIndex();
|
|
14799
|
+
const drift = current !== content;
|
|
14800
|
+
let wrote = false;
|
|
14801
|
+
if (!opts.check && drift) {
|
|
14802
|
+
deps.writeIndex(content);
|
|
14803
|
+
wrote = true;
|
|
14666
14804
|
}
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
|
|
14671
|
-
const
|
|
14672
|
-
|
|
14673
|
-
|
|
14674
|
-
for (const
|
|
14675
|
-
|
|
14676
|
-
|
|
14677
|
-
|
|
14805
|
+
return { content, drift, wrote };
|
|
14806
|
+
}
|
|
14807
|
+
function walkMarkdown(dir) {
|
|
14808
|
+
const out = [];
|
|
14809
|
+
const stack = [dir];
|
|
14810
|
+
while (stack.length) {
|
|
14811
|
+
const current = stack.pop();
|
|
14812
|
+
for (const entry of (0, import_node_fs16.readdirSync)(current, { withFileTypes: true })) {
|
|
14813
|
+
const full = (0, import_node_path14.join)(current, entry.name);
|
|
14814
|
+
if (entry.isDirectory()) {
|
|
14815
|
+
stack.push(full);
|
|
14816
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
14817
|
+
out.push((0, import_node_path14.relative)(dir, full).split(import_node_path14.sep).join("/"));
|
|
14818
|
+
}
|
|
14678
14819
|
}
|
|
14679
14820
|
}
|
|
14680
|
-
return
|
|
14821
|
+
return out;
|
|
14681
14822
|
}
|
|
14682
|
-
function
|
|
14823
|
+
function createDocsIndexDeps(repoRoot2) {
|
|
14824
|
+
const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
|
|
14825
|
+
const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
14683
14826
|
return {
|
|
14684
|
-
listDocs: () =>
|
|
14685
|
-
readDoc: (
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
} catch {
|
|
14689
|
-
return "";
|
|
14690
|
-
}
|
|
14691
|
-
}
|
|
14827
|
+
listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter((p) => p !== "index.md").sort() : [],
|
|
14828
|
+
readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
|
|
14829
|
+
readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
|
|
14830
|
+
writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
|
|
14692
14831
|
};
|
|
14693
14832
|
}
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14833
|
+
|
|
14834
|
+
// src/docs-audit-command.ts
|
|
14835
|
+
function serializeOutcome(outcome) {
|
|
14836
|
+
switch (outcome.kind) {
|
|
14837
|
+
case "clean":
|
|
14838
|
+
return "clean";
|
|
14839
|
+
case "refreshed":
|
|
14840
|
+
return `refreshed-${outcome.count}`;
|
|
14841
|
+
case "failed":
|
|
14842
|
+
return "failed";
|
|
14843
|
+
}
|
|
14844
|
+
}
|
|
14845
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
14846
|
+
function isValidIsoDate(date) {
|
|
14847
|
+
if (typeof date !== "string" || !ISO_DATE_RE.test(date)) return false;
|
|
14848
|
+
const parsed = Date.parse(`${date}T00:00:00Z`);
|
|
14849
|
+
if (Number.isNaN(parsed)) return false;
|
|
14850
|
+
return new Date(parsed).toISOString().slice(0, 10) === date;
|
|
14851
|
+
}
|
|
14852
|
+
var OUTCOME_WIRE_RE = /^(clean|refreshed-[1-9]\d*|failed)$/;
|
|
14853
|
+
function isValidOutcomeWire(outcome) {
|
|
14854
|
+
return typeof outcome === "string" && OUTCOME_WIRE_RE.test(outcome);
|
|
14855
|
+
}
|
|
14856
|
+
function docsAuditRecord(input) {
|
|
14857
|
+
const repo = input.repo.trim();
|
|
14858
|
+
const date = input.date.trim();
|
|
14859
|
+
const shaRange = input.shaRange.trim();
|
|
14860
|
+
const checkerVendor = input.checkerVendor.trim();
|
|
14861
|
+
if (!repo) throw new Error("docs-audit record: repo is required");
|
|
14862
|
+
if (!date) throw new Error("docs-audit record: date is required");
|
|
14863
|
+
if (!isValidIsoDate(date)) throw new Error(`docs-audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
|
|
14864
|
+
if (!shaRange) throw new Error("docs-audit record: shaRange is required");
|
|
14865
|
+
if (!checkerVendor) throw new Error("docs-audit record: checkerVendor is required");
|
|
14866
|
+
if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
|
|
14867
|
+
throw new Error("docs-audit record: a `refreshed` outcome must carry a positive integer count");
|
|
14868
|
+
}
|
|
14869
|
+
if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
|
|
14870
|
+
throw new Error("docs-audit record: a `failed` outcome must carry a reason");
|
|
14871
|
+
}
|
|
14872
|
+
const outcome = serializeOutcome(input.outcome);
|
|
14873
|
+
if (!isValidOutcomeWire(outcome)) {
|
|
14874
|
+
throw new Error(`docs-audit record: outcome serialized to an invalid wire form "${outcome}"`);
|
|
14875
|
+
}
|
|
14876
|
+
return { repo, date, shaRange, outcome, checkerVendor };
|
|
14877
|
+
}
|
|
14878
|
+
function ageInDays(verdictDate, today) {
|
|
14879
|
+
const a = Date.parse(`${verdictDate}T00:00:00Z`);
|
|
14880
|
+
const b = Date.parse(`${today}T00:00:00Z`);
|
|
14881
|
+
return Math.max(0, Math.round((b - a) / 864e5));
|
|
14882
|
+
}
|
|
14883
|
+
function docsAuditStatus(fetch2, opts) {
|
|
14884
|
+
if ("notArmed" in fetch2) {
|
|
14885
|
+
return { ok: true, state: "not-armed", line: `docs-audit: ${opts.repo} janitor not armed (registry route not live yet)` };
|
|
14886
|
+
}
|
|
14887
|
+
if (!fetch2.ok) {
|
|
14888
|
+
return { ok: false, state: "error", line: `docs-audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
|
|
14889
|
+
}
|
|
14890
|
+
if (fetch2.verdict === null) {
|
|
14891
|
+
return { ok: false, state: "missing", line: `docs-audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
|
|
14892
|
+
}
|
|
14893
|
+
const verdict = fetch2.verdict;
|
|
14894
|
+
for (const field of ["repo", "shaRange", "checkerVendor"]) {
|
|
14895
|
+
const value = verdict[field];
|
|
14896
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
14897
|
+
const shown = typeof value === "string" ? `"${value}"` : `type ${Array.isArray(value) ? "array" : typeof value}`;
|
|
14898
|
+
return {
|
|
14899
|
+
ok: false,
|
|
14900
|
+
state: "malformed",
|
|
14901
|
+
line: `docs-audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
|
|
14902
|
+
};
|
|
14903
|
+
}
|
|
14904
|
+
}
|
|
14905
|
+
if (!isValidIsoDate(verdict.date)) {
|
|
14906
|
+
return {
|
|
14907
|
+
ok: false,
|
|
14908
|
+
state: "malformed",
|
|
14909
|
+
line: `docs-audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
|
|
14703
14910
|
};
|
|
14704
14911
|
}
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
if (backfill) return true;
|
|
14712
|
-
const prior = prev[p.slug];
|
|
14713
|
-
if (!prior) return true;
|
|
14714
|
-
const cur = hashPageInputs({ slug: p.slug, title: p.title, source: readSource(p.sourcePath) });
|
|
14715
|
-
return prior.hash !== cur;
|
|
14716
|
-
});
|
|
14717
|
-
}
|
|
14718
|
-
|
|
14719
|
-
// src/wiki-plan-command.ts
|
|
14720
|
-
async function wikiPlan(deps, opts) {
|
|
14721
|
-
const wikiRepo = `${opts.repo}.wiki.git`;
|
|
14722
|
-
const pages = deps.collectPages();
|
|
14723
|
-
if (pages.length === 0) {
|
|
14724
|
-
emit(deps, { repo: opts.repo, wikiRepo, backfill: false, pages: [], stamp: {} });
|
|
14725
|
-
return true;
|
|
14912
|
+
if (!isValidOutcomeWire(verdict.outcome)) {
|
|
14913
|
+
return {
|
|
14914
|
+
ok: false,
|
|
14915
|
+
state: "malformed",
|
|
14916
|
+
line: `docs-audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
|
|
14917
|
+
};
|
|
14726
14918
|
}
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
deps.err(`wiki plan failed: could not read the wiki stamp for ${opts.repo}: ${read.error}`);
|
|
14730
|
-
return false;
|
|
14919
|
+
if (!isValidIsoDate(opts.today)) {
|
|
14920
|
+
throw new Error(`docs-audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
|
|
14731
14921
|
}
|
|
14732
|
-
const
|
|
14733
|
-
const
|
|
14734
|
-
const
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
});
|
|
14743
|
-
return true;
|
|
14744
|
-
}
|
|
14745
|
-
function emit(deps, result) {
|
|
14746
|
-
deps.out(JSON.stringify(result, null, 2));
|
|
14922
|
+
const cadence = opts.cadenceDays ?? 7;
|
|
14923
|
+
const grace = opts.graceDays ?? 3;
|
|
14924
|
+
const age = ageInDays(verdict.date, opts.today);
|
|
14925
|
+
if (age > cadence + grace) {
|
|
14926
|
+
return { ok: false, state: "stale", line: `docs-audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
|
|
14927
|
+
}
|
|
14928
|
+
if (verdict.outcome === "failed") {
|
|
14929
|
+
return { ok: false, state: "failed", line: `docs-audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
|
|
14930
|
+
}
|
|
14931
|
+
return { ok: true, state: "clean", line: `docs-audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
14747
14932
|
}
|
|
14748
14933
|
|
|
14749
14934
|
// src/project-readiness.ts
|
|
@@ -15562,7 +15747,6 @@ var SETTABLE_VAR_KEYS = [
|
|
|
15562
15747
|
"projectOwner",
|
|
15563
15748
|
"projectNumber",
|
|
15564
15749
|
"branch",
|
|
15565
|
-
"wikiRepo",
|
|
15566
15750
|
"vaultPath",
|
|
15567
15751
|
"repos",
|
|
15568
15752
|
"oauth",
|
|
@@ -15943,9 +16127,9 @@ function writeError(res) {
|
|
|
15943
16127
|
}
|
|
15944
16128
|
|
|
15945
16129
|
// src/secrets-commands.ts
|
|
15946
|
-
var
|
|
15947
|
-
var
|
|
15948
|
-
var
|
|
16130
|
+
var import_node_fs17 = require("node:fs");
|
|
16131
|
+
var import_node_path15 = require("node:path");
|
|
16132
|
+
var import_node_os5 = require("node:os");
|
|
15949
16133
|
|
|
15950
16134
|
// src/secrets-diff.ts
|
|
15951
16135
|
var TIMEOUT_MS2 = 8e3;
|
|
@@ -16047,18 +16231,18 @@ function collectMap(value, previous = []) {
|
|
|
16047
16231
|
return [...previous, value];
|
|
16048
16232
|
}
|
|
16049
16233
|
async function decryptRailsCredentials(input) {
|
|
16050
|
-
const appDir = (0,
|
|
16234
|
+
const appDir = (0, import_node_path15.resolve)(input.appDir ?? process.cwd());
|
|
16051
16235
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
16052
16236
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
16053
|
-
const credentialsPath = (0,
|
|
16054
|
-
const masterKeyPath = (0,
|
|
16237
|
+
const credentialsPath = (0, import_node_path15.resolve)(appDir, credentialsFile);
|
|
16238
|
+
const masterKeyPath = (0, import_node_path15.resolve)(appDir, masterKeyFile);
|
|
16055
16239
|
const env = {
|
|
16056
16240
|
...process.env,
|
|
16057
16241
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
16058
16242
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
16059
16243
|
};
|
|
16060
|
-
if ((0,
|
|
16061
|
-
env.RAILS_MASTER_KEY = (0,
|
|
16244
|
+
if ((0, import_node_fs17.existsSync)(masterKeyPath)) {
|
|
16245
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs17.readFileSync)(masterKeyPath, "utf8").trim();
|
|
16062
16246
|
}
|
|
16063
16247
|
const script = [
|
|
16064
16248
|
'require "json"',
|
|
@@ -16068,9 +16252,9 @@ async function decryptRailsCredentials(input) {
|
|
|
16068
16252
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
16069
16253
|
"puts JSON.generate(config.config)"
|
|
16070
16254
|
].join("\n");
|
|
16071
|
-
const scriptDir = (0,
|
|
16072
|
-
const scriptPath = (0,
|
|
16073
|
-
(0,
|
|
16255
|
+
const scriptDir = (0, import_node_fs17.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
|
|
16256
|
+
const scriptPath = (0, import_node_path15.join)(scriptDir, "decrypt.rb");
|
|
16257
|
+
(0, import_node_fs17.writeFileSync)(scriptPath, script, "utf8");
|
|
16074
16258
|
try {
|
|
16075
16259
|
const args = ["exec", "ruby", scriptPath];
|
|
16076
16260
|
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
@@ -16082,7 +16266,7 @@ async function decryptRailsCredentials(input) {
|
|
|
16082
16266
|
});
|
|
16083
16267
|
return JSON.parse(stdout);
|
|
16084
16268
|
} finally {
|
|
16085
|
-
(0,
|
|
16269
|
+
(0, import_node_fs17.rmSync)(scriptDir, { recursive: true, force: true });
|
|
16086
16270
|
}
|
|
16087
16271
|
}
|
|
16088
16272
|
async function readSecretStdin() {
|
|
@@ -16157,7 +16341,7 @@ function registerSecretsCommands(program3) {
|
|
|
16157
16341
|
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) => {
|
|
16158
16342
|
let body;
|
|
16159
16343
|
try {
|
|
16160
|
-
body = (0,
|
|
16344
|
+
body = (0, import_node_fs17.readFileSync)((0, import_node_path15.resolve)(o.file), "utf8");
|
|
16161
16345
|
} catch (e) {
|
|
16162
16346
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
16163
16347
|
}
|
|
@@ -16261,7 +16445,7 @@ function registerSecretsCommands(program3) {
|
|
|
16261
16445
|
{
|
|
16262
16446
|
...d,
|
|
16263
16447
|
decryptRailsCredentials,
|
|
16264
|
-
removeFile: (path2) => (0,
|
|
16448
|
+
removeFile: (path2) => (0, import_node_fs17.unlinkSync)((0, import_node_path15.resolve)(o.appDir ?? process.cwd(), path2))
|
|
16265
16449
|
},
|
|
16266
16450
|
{
|
|
16267
16451
|
repo: o.repo,
|
|
@@ -16286,7 +16470,7 @@ function registerSecretsCommands(program3) {
|
|
|
16286
16470
|
}
|
|
16287
16471
|
|
|
16288
16472
|
// src/app-actor.ts
|
|
16289
|
-
var
|
|
16473
|
+
var import_node_crypto4 = require("node:crypto");
|
|
16290
16474
|
var APP_ACTOR_ENV = "MMI_ACTOR";
|
|
16291
16475
|
var APP_VAULT_REPO = "mutmutco/MMI-Hub";
|
|
16292
16476
|
var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
|
|
@@ -16330,7 +16514,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
|
|
|
16330
16514
|
exp: now + APP_JWT_TTL_S,
|
|
16331
16515
|
iss: appId
|
|
16332
16516
|
}));
|
|
16333
|
-
const signer = (0,
|
|
16517
|
+
const signer = (0, import_node_crypto4.createSign)("RSA-SHA256");
|
|
16334
16518
|
signer.update(`${header}.${payload}`);
|
|
16335
16519
|
return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
|
|
16336
16520
|
}
|
|
@@ -16636,7 +16820,7 @@ function registerBoxCommands(program3) {
|
|
|
16636
16820
|
|
|
16637
16821
|
// src/schedules-commands.ts
|
|
16638
16822
|
var import_promises2 = require("node:fs/promises");
|
|
16639
|
-
var
|
|
16823
|
+
var import_node_child_process9 = require("node:child_process");
|
|
16640
16824
|
var import_node_util7 = require("node:util");
|
|
16641
16825
|
|
|
16642
16826
|
// src/schedules.ts
|
|
@@ -16780,9 +16964,82 @@ function spliceDoc(docText, generatedSection) {
|
|
|
16780
16964
|
}
|
|
16781
16965
|
return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
|
|
16782
16966
|
}
|
|
16967
|
+
function splitCadence(cadence) {
|
|
16968
|
+
return cadence.split("+").map((c) => c.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
16969
|
+
}
|
|
16970
|
+
var CRON_FIELD = "[0-9*/,-]+";
|
|
16971
|
+
var CRON_TOKEN_RE = new RegExp(`${CRON_FIELD}(?:\\s+${CRON_FIELD}){4}`, "g");
|
|
16972
|
+
function extractCronTokens(cadence) {
|
|
16973
|
+
const out = /* @__PURE__ */ new Set();
|
|
16974
|
+
for (const m of cadence.match(CRON_TOKEN_RE) ?? []) out.add(m.replace(/\s+/g, " ").trim());
|
|
16975
|
+
return out;
|
|
16976
|
+
}
|
|
16977
|
+
function cadenceStale(registryCadence, liveCadence) {
|
|
16978
|
+
const registered = extractCronTokens(registryCadence);
|
|
16979
|
+
const liveCrons = splitCadence(liveCadence);
|
|
16980
|
+
if (!liveCrons.length) return false;
|
|
16981
|
+
return !liveCrons.every((cron) => registered.has(cron));
|
|
16982
|
+
}
|
|
16983
|
+
function reconcileGithubActions(liveGithub, registry2, readRepos) {
|
|
16984
|
+
const registryGithub = registry2.filter((r) => r.executor === "github-actions");
|
|
16985
|
+
const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
|
|
16986
|
+
const registryById = new Map(registryGithub.map((r) => [r.id, r]));
|
|
16987
|
+
const drifts = [];
|
|
16988
|
+
for (const e of liveGithub) {
|
|
16989
|
+
if (!registryById.has(e.name)) {
|
|
16990
|
+
drifts.push({
|
|
16991
|
+
class: "live-but-unregistered",
|
|
16992
|
+
name: e.name,
|
|
16993
|
+
executor: e.executor,
|
|
16994
|
+
detail: "armed with no SCHEDULE# row",
|
|
16995
|
+
remedy: 'add the eight-field header (docs/schedules.md "The entry template") and re-run the schedules lift so it registers a SCHEDULE# row'
|
|
16996
|
+
});
|
|
16997
|
+
}
|
|
16998
|
+
}
|
|
16999
|
+
for (const r of registryGithub) {
|
|
17000
|
+
const live = liveByName.get(r.id);
|
|
17001
|
+
if (live) {
|
|
17002
|
+
if (cadenceStale(r.cadence, live.cadence)) {
|
|
17003
|
+
drifts.push({
|
|
17004
|
+
class: "file-vs-registry-stale",
|
|
17005
|
+
name: r.id,
|
|
17006
|
+
executor: r.executor || live.executor,
|
|
17007
|
+
detail: `live cadence \`${live.cadence}\` disagrees with registered \`${r.cadence}\``,
|
|
17008
|
+
remedy: "re-run the schedules lift to refresh the SCHEDULE# row from the current header/cron"
|
|
17009
|
+
});
|
|
17010
|
+
}
|
|
17011
|
+
continue;
|
|
17012
|
+
}
|
|
17013
|
+
if (readRepos.has(r.repo)) {
|
|
17014
|
+
drifts.push({
|
|
17015
|
+
class: "registered-but-dead",
|
|
17016
|
+
name: r.id,
|
|
17017
|
+
executor: r.executor || "github-actions",
|
|
17018
|
+
detail: "registered with no armed workflow",
|
|
17019
|
+
remedy: `remove it from ${r.repo} (or re-arm the workflow), then re-run the schedules lift to prune the stale SCHEDULE# row`
|
|
17020
|
+
});
|
|
17021
|
+
}
|
|
17022
|
+
}
|
|
17023
|
+
return drifts.sort((a, b) => a.class.localeCompare(b.class) || a.name.localeCompare(b.name));
|
|
17024
|
+
}
|
|
17025
|
+
function renderDrift(d) {
|
|
17026
|
+
return `${d.class}: ${d.name} (${d.executor}) \u2014 ${d.detail}; remedy: ${d.remedy}`;
|
|
17027
|
+
}
|
|
17028
|
+
function assembleReconciliation(githubEntries2, readRepos, registry2) {
|
|
17029
|
+
if (registry2 === null) {
|
|
17030
|
+
return {
|
|
17031
|
+
reconciliation: [],
|
|
17032
|
+
driftLines: [],
|
|
17033
|
+
incomplete: ["registry: could not read GET /schedules/list \u2014 reconciliation skipped (Hub API unreachable or not authenticated)"]
|
|
17034
|
+
};
|
|
17035
|
+
}
|
|
17036
|
+
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
17037
|
+
const drifts = reconcileGithubActions(live, registry2, readRepos);
|
|
17038
|
+
return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
|
|
17039
|
+
}
|
|
16783
17040
|
|
|
16784
17041
|
// src/schedules-commands.ts
|
|
16785
|
-
var execFileP5 = (0, import_node_util7.promisify)(
|
|
17042
|
+
var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process9.execFile);
|
|
16786
17043
|
var AWS_REGION = "eu-central-1";
|
|
16787
17044
|
var AWS_TIMEOUT_MS = 3e4;
|
|
16788
17045
|
var AWS_RETRY_DELAY_MS = 1500;
|
|
@@ -16829,12 +17086,13 @@ async function githubEntries(client) {
|
|
|
16829
17086
|
const entries = [];
|
|
16830
17087
|
const incomplete = [];
|
|
16831
17088
|
const drift = [];
|
|
17089
|
+
const readRepos = [];
|
|
16832
17090
|
let repos;
|
|
16833
17091
|
try {
|
|
16834
17092
|
const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
|
|
16835
17093
|
repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
|
|
16836
17094
|
} catch (e) {
|
|
16837
|
-
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift };
|
|
17095
|
+
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, readRepos };
|
|
16838
17096
|
}
|
|
16839
17097
|
const results = await Promise.all(
|
|
16840
17098
|
repos.map(async (repo) => {
|
|
@@ -16848,13 +17106,14 @@ async function githubEntries(client) {
|
|
|
16848
17106
|
for (const r of results) {
|
|
16849
17107
|
if ("error" in r) incomplete.push(`github: ${r.repo}: ${r.error}`);
|
|
16850
17108
|
else {
|
|
17109
|
+
readRepos.push(r.repo);
|
|
16851
17110
|
entries.push(...r.entries);
|
|
16852
17111
|
if (r.failures.length) {
|
|
16853
17112
|
drift.push(`${r.repo}: ${r.failures.length} workflow record(s) listed active with no file on the default branch (deleted one-offs?) \u2014 deregister them: ${r.failures.map((f) => f.split(": ")[1]).join(", ")}`);
|
|
16854
17113
|
}
|
|
16855
17114
|
}
|
|
16856
17115
|
}
|
|
16857
|
-
return { entries, incomplete, drift };
|
|
17116
|
+
return { entries, incomplete, drift, readRepos };
|
|
16858
17117
|
}
|
|
16859
17118
|
async function awsJson(args) {
|
|
16860
17119
|
const run = async () => {
|
|
@@ -16891,12 +17150,21 @@ async function awsEntries() {
|
|
|
16891
17150
|
}
|
|
16892
17151
|
return { entries, incomplete, drift: [] };
|
|
16893
17152
|
}
|
|
16894
|
-
async function
|
|
16895
|
-
|
|
17153
|
+
async function defaultRegistryRead() {
|
|
17154
|
+
return fetchSchedulesList(registryClientDeps(await loadConfig()));
|
|
17155
|
+
}
|
|
17156
|
+
async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead) {
|
|
17157
|
+
const [gh, aws, registry2] = await Promise.all([
|
|
17158
|
+
githubEntries(client),
|
|
17159
|
+
awsEntries(),
|
|
17160
|
+
readRegistry().catch(() => null)
|
|
17161
|
+
]);
|
|
17162
|
+
const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2);
|
|
16896
17163
|
return {
|
|
16897
17164
|
entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
|
|
16898
|
-
incomplete: [...gh.incomplete, ...aws.incomplete],
|
|
16899
|
-
drift: [...gh.drift, ...aws.drift]
|
|
17165
|
+
incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
|
|
17166
|
+
drift: [...gh.drift, ...aws.drift, ...recon.driftLines],
|
|
17167
|
+
reconciliation: recon.reconciliation
|
|
16900
17168
|
};
|
|
16901
17169
|
}
|
|
16902
17170
|
function warnIncomplete2(incomplete) {
|
|
@@ -16909,7 +17177,7 @@ function reportDrift(drift) {
|
|
|
16909
17177
|
function registerSchedulesCommands(program3) {
|
|
16910
17178
|
program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "splice the generated inventory into the given doc between the schedules:inventory markers (docs/schedules.md)").action(async (o) => {
|
|
16911
17179
|
try {
|
|
16912
|
-
const { entries, incomplete, drift } = await fetchNotebook();
|
|
17180
|
+
const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
|
|
16913
17181
|
if (o.doc) {
|
|
16914
17182
|
if (incomplete.length) {
|
|
16915
17183
|
warnIncomplete2(incomplete);
|
|
@@ -16924,7 +17192,7 @@ function registerSchedulesCommands(program3) {
|
|
|
16924
17192
|
return;
|
|
16925
17193
|
}
|
|
16926
17194
|
if (o.json) {
|
|
16927
|
-
console.log(JSON.stringify({ org: entries, incomplete, drift }, null, 2));
|
|
17195
|
+
console.log(JSON.stringify({ org: entries, incomplete, drift, reconciliation }, null, 2));
|
|
16928
17196
|
} else {
|
|
16929
17197
|
console.log(formatSchedulesTable(entries));
|
|
16930
17198
|
reportDrift(drift);
|
|
@@ -17396,11 +17664,11 @@ function registerQueryCommands(program3) {
|
|
|
17396
17664
|
}
|
|
17397
17665
|
|
|
17398
17666
|
// src/bootstrap-commands.ts
|
|
17399
|
-
var
|
|
17667
|
+
var import_node_fs18 = require("node:fs");
|
|
17400
17668
|
|
|
17401
17669
|
// src/bootstrap-verify.ts
|
|
17402
17670
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
17403
|
-
var requiredDocs = ["README.md", "architecture.md"];
|
|
17671
|
+
var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md"];
|
|
17404
17672
|
var requiredIssueTemplates = [
|
|
17405
17673
|
".github/ISSUE_TEMPLATE/bug.yml",
|
|
17406
17674
|
".github/ISSUE_TEMPLATE/feature.yml",
|
|
@@ -17561,7 +17829,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
17561
17829
|
const repoInfo = await restJson3(deps, `repos/${repo}`, {});
|
|
17562
17830
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
17563
17831
|
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
17564
|
-
checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
|
|
17565
17832
|
const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
|
|
17566
17833
|
const branchNames = new Set(branchList.map((b) => b.name));
|
|
17567
17834
|
for (const branch of branchesWanted) {
|
|
@@ -17763,8 +18030,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
17763
18030
|
});
|
|
17764
18031
|
}
|
|
17765
18032
|
}
|
|
17766
|
-
const fanout = repo === "mutmutco/MMI-Hub" ? true : localRegistryCheck(deps, ".github/fanout-targets.json", (json) => Array.isArray(json?.repos) && json.repos.some((r) => repoRefsMatch(r.repo ?? "", repo.split("/")[1]) && r.branch === baseBranch));
|
|
17767
|
-
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
17768
18033
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
|
|
17769
18034
|
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "project registry includes repo" });
|
|
17770
18035
|
const rulesetList = await restJson3(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
@@ -17944,7 +18209,7 @@ function registerBootstrapCommands(program3) {
|
|
|
17944
18209
|
client: defaultGitHubClient(),
|
|
17945
18210
|
projectMeta: meta,
|
|
17946
18211
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
17947
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
18212
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
|
|
17948
18213
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
17949
18214
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
17950
18215
|
requiredGcpApis: (() => {
|
|
@@ -17995,12 +18260,12 @@ function registerBootstrapCommands(program3) {
|
|
|
17995
18260
|
return fail(`bootstrap apply: ${e.message}`);
|
|
17996
18261
|
}
|
|
17997
18262
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
17998
|
-
if (!(0,
|
|
17999
|
-
const manifest = loadBootstrapSeeds((0,
|
|
18263
|
+
if (!(0, import_node_fs18.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`);
|
|
18264
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs18.readFileSync)(manifestPath, "utf8"));
|
|
18000
18265
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
18001
18266
|
const slug = parsedRepo.slug;
|
|
18002
18267
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
18003
|
-
const readFile5 = (p) => (0,
|
|
18268
|
+
const readFile5 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
|
|
18004
18269
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
18005
18270
|
const rawVars = {};
|
|
18006
18271
|
for (const value of cmdOpts.var ?? []) {
|
|
@@ -18062,21 +18327,19 @@ function registerBootstrapCommands(program3) {
|
|
|
18062
18327
|
let exists = false;
|
|
18063
18328
|
let sha;
|
|
18064
18329
|
let remoteContent = null;
|
|
18065
|
-
|
|
18330
|
+
try {
|
|
18331
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
|
|
18332
|
+
exists = true;
|
|
18066
18333
|
try {
|
|
18067
|
-
const
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
sha = parsed.sha;
|
|
18072
|
-
if (parsed.encoding === "base64" && typeof parsed.content === "string") {
|
|
18073
|
-
remoteContent = Buffer.from(parsed.content, "base64").toString("utf8");
|
|
18074
|
-
}
|
|
18075
|
-
} catch {
|
|
18334
|
+
const parsed = JSON.parse(r.stdout);
|
|
18335
|
+
sha = parsed.sha;
|
|
18336
|
+
if (parsed.encoding === "base64" && typeof parsed.content === "string") {
|
|
18337
|
+
remoteContent = Buffer.from(parsed.content, "base64").toString("utf8");
|
|
18076
18338
|
}
|
|
18077
18339
|
} catch {
|
|
18078
|
-
exists = false;
|
|
18079
18340
|
}
|
|
18341
|
+
} catch {
|
|
18342
|
+
exists = false;
|
|
18080
18343
|
}
|
|
18081
18344
|
const planned = planSeedAction(resolved, exists);
|
|
18082
18345
|
const isBlock = resolved.source === "managed-block";
|
|
@@ -18094,7 +18357,7 @@ function registerBootstrapCommands(program3) {
|
|
|
18094
18357
|
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
|
|
18095
18358
|
});
|
|
18096
18359
|
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
18097
|
-
const prDecision =
|
|
18360
|
+
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
18098
18361
|
if (prDecision.action === "reuse") {
|
|
18099
18362
|
seedPrUrl = prDecision.url;
|
|
18100
18363
|
} else {
|
|
@@ -18187,83 +18450,7 @@ function registerBootstrapCommands(program3) {
|
|
|
18187
18450
|
applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
|
|
18188
18451
|
}
|
|
18189
18452
|
}
|
|
18190
|
-
|
|
18191
|
-
if (o.execute) {
|
|
18192
|
-
const fanoutEntry = {
|
|
18193
|
-
repo: parsedRepo.name,
|
|
18194
|
-
slug,
|
|
18195
|
-
projectId: vars.PROJECT_ID || void 0,
|
|
18196
|
-
wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
|
|
18197
|
-
branch: baseBranch,
|
|
18198
|
-
cls: o.class,
|
|
18199
|
-
name: vars.NAME || parsedRepo.name
|
|
18200
|
-
};
|
|
18201
|
-
const readHubFile = async (path2) => {
|
|
18202
|
-
const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc(path2)}?ref=development`]);
|
|
18203
|
-
const parsed = JSON.parse(r.stdout);
|
|
18204
|
-
if (parsed.encoding !== "base64" || typeof parsed.content !== "string" || !parsed.sha) {
|
|
18205
|
-
throw new Error(`could not read ${HUB_REPO}/${path2}`);
|
|
18206
|
-
}
|
|
18207
|
-
return { content: Buffer.from(parsed.content, "base64").toString("utf8"), sha: parsed.sha };
|
|
18208
|
-
};
|
|
18209
|
-
try {
|
|
18210
|
-
const fanoutFile = await readHubFile(".github/fanout-targets.json");
|
|
18211
|
-
const projectsFile = await readHubFile("projects.json");
|
|
18212
|
-
const plan = planFanoutRegistration(fanoutFile.content, projectsFile.content, fanoutEntry);
|
|
18213
|
-
if (!plan.changed) {
|
|
18214
|
-
applied.push(`fanout: already registered (${parsedRepo.name})`);
|
|
18215
|
-
} else {
|
|
18216
|
-
const branchName = `bootstrap-register-fanout-${slug}`;
|
|
18217
|
-
const headSha = (await gh(["api", `repos/${HUB_REPO}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
|
|
18218
|
-
try {
|
|
18219
|
-
await gh(["api", "-X", "POST", `repos/${HUB_REPO}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
|
|
18220
|
-
} catch (e) {
|
|
18221
|
-
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
18222
|
-
}
|
|
18223
|
-
const branchFileSha = async (path2) => {
|
|
18224
|
-
try {
|
|
18225
|
-
const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc(path2)}?ref=${branchName}`, "--jq", ".sha"]);
|
|
18226
|
-
return r.stdout.trim() || void 0;
|
|
18227
|
-
} catch (e) {
|
|
18228
|
-
if (/404|Not Found/i.test(String(e.message ?? ""))) return void 0;
|
|
18229
|
-
throw e;
|
|
18230
|
-
}
|
|
18231
|
-
};
|
|
18232
|
-
const fanoutBranchSha = await branchFileSha(".github/fanout-targets.json");
|
|
18233
|
-
const projectsBranchSha = await branchFileSha("projects.json");
|
|
18234
|
-
await gh(contentPutArgs(HUB_REPO, ".github/fanout-targets.json", plan.fanoutTargets, branchName, fanoutBranchSha));
|
|
18235
|
-
await gh(contentPutArgs(HUB_REPO, "projects.json", plan.projects, branchName, projectsBranchSha));
|
|
18236
|
-
const openPrs = await gh(["pr", "list", "--repo", HUB_REPO, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
|
|
18237
|
-
const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
18238
|
-
if (prDecision.action === "reuse") {
|
|
18239
|
-
fanoutPrUrl = prDecision.url;
|
|
18240
|
-
} else {
|
|
18241
|
-
const created = await ghCreate([
|
|
18242
|
-
"pr",
|
|
18243
|
-
"create",
|
|
18244
|
-
"--repo",
|
|
18245
|
-
HUB_REPO,
|
|
18246
|
-
"--base",
|
|
18247
|
-
"development",
|
|
18248
|
-
"--head",
|
|
18249
|
-
branchName,
|
|
18250
|
-
"--title",
|
|
18251
|
-
`bootstrap: register ${parsedRepo.name} for the org-managed .gitignore fanout`,
|
|
18252
|
-
"--body",
|
|
18253
|
-
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the org-managed .gitignore block reaches it (hub-v3 retired the whole-spine fanout).`
|
|
18254
|
-
]);
|
|
18255
|
-
fanoutPrUrl = created.url;
|
|
18256
|
-
}
|
|
18257
|
-
await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO, "--auto", "--squash"]).catch((e) => {
|
|
18258
|
-
if (!/already/i.test(String(e.message ?? ""))) throw e;
|
|
18259
|
-
});
|
|
18260
|
-
applied.push(`fanout: PR ${fanoutPrUrl} (auto-merge enabled)`);
|
|
18261
|
-
}
|
|
18262
|
-
} catch (e) {
|
|
18263
|
-
return failGraceful(`bootstrap apply: fanout registration failed: ${e.message}`);
|
|
18264
|
-
}
|
|
18265
|
-
}
|
|
18266
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
|
|
18453
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
|
|
18267
18454
|
else {
|
|
18268
18455
|
console.log(renderSeedPlan(actions));
|
|
18269
18456
|
if (o.execute) console.log(`
|
|
@@ -18274,13 +18461,13 @@ LIVE apply to ${repo}:
|
|
|
18274
18461
|
}
|
|
18275
18462
|
|
|
18276
18463
|
// src/stage-commands.ts
|
|
18277
|
-
var import_node_fs21 = require("node:fs");
|
|
18278
|
-
var import_node_path18 = require("node:path");
|
|
18279
|
-
|
|
18280
|
-
// src/port-registry.ts
|
|
18281
18464
|
var import_node_fs20 = require("node:fs");
|
|
18282
18465
|
var import_node_path17 = require("node:path");
|
|
18283
18466
|
|
|
18467
|
+
// src/port-registry.ts
|
|
18468
|
+
var import_node_fs19 = require("node:fs");
|
|
18469
|
+
var import_node_path16 = require("node:path");
|
|
18470
|
+
|
|
18284
18471
|
// ../infra/port-geometry.mjs
|
|
18285
18472
|
var PORT_BLOCK = 100;
|
|
18286
18473
|
var PORT_SPAN = 10;
|
|
@@ -18293,8 +18480,8 @@ function nextPortBlock(registry2) {
|
|
|
18293
18480
|
return [base, base + PORT_SPAN];
|
|
18294
18481
|
}
|
|
18295
18482
|
function loadPortRegistry(path2) {
|
|
18296
|
-
if (!(0,
|
|
18297
|
-
const raw = JSON.parse((0,
|
|
18483
|
+
if (!(0, import_node_fs19.existsSync)(path2)) return {};
|
|
18484
|
+
const raw = JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
|
|
18298
18485
|
const out = {};
|
|
18299
18486
|
for (const [key, value] of Object.entries(raw)) {
|
|
18300
18487
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -18308,9 +18495,9 @@ function ensurePortRange(repo, path2) {
|
|
|
18308
18495
|
const existing = registry2[repo];
|
|
18309
18496
|
if (existing) return existing;
|
|
18310
18497
|
const range = nextPortBlock(registry2);
|
|
18311
|
-
const raw = (0,
|
|
18498
|
+
const raw = (0, import_node_fs19.existsSync)(path2) ? JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8")) : {};
|
|
18312
18499
|
raw[repo] = range;
|
|
18313
|
-
(0,
|
|
18500
|
+
(0, import_node_fs19.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
18314
18501
|
return range;
|
|
18315
18502
|
}
|
|
18316
18503
|
function portCursorSeed(registry2) {
|
|
@@ -18332,22 +18519,22 @@ function existingPortRange(repo, registry2) {
|
|
|
18332
18519
|
return registry2[repo] ?? null;
|
|
18333
18520
|
}
|
|
18334
18521
|
function portRangeInfraAt(root, source) {
|
|
18335
|
-
const registryPath = (0,
|
|
18336
|
-
const ddbScriptPath = (0,
|
|
18337
|
-
if (!(0,
|
|
18522
|
+
const registryPath = (0, import_node_path16.join)(root, "infra", "port-ranges.json");
|
|
18523
|
+
const ddbScriptPath = (0, import_node_path16.join)(root, "infra", "port-ddb.mjs");
|
|
18524
|
+
if (!(0, import_node_fs19.existsSync)(registryPath) || !(0, import_node_fs19.existsSync)(ddbScriptPath)) return null;
|
|
18338
18525
|
return { root, source, registryPath, ddbScriptPath };
|
|
18339
18526
|
}
|
|
18340
18527
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
18341
18528
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
18342
18529
|
if (direct) return direct;
|
|
18343
|
-
for (let dir = cwd; ; dir = (0,
|
|
18344
|
-
const sibling = portRangeInfraAt((0,
|
|
18530
|
+
for (let dir = cwd; ; dir = (0, import_node_path16.dirname)(dir)) {
|
|
18531
|
+
const sibling = portRangeInfraAt((0, import_node_path16.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
18345
18532
|
if (sibling) return sibling;
|
|
18346
|
-
const parent = (0,
|
|
18533
|
+
const parent = (0, import_node_path16.dirname)(dir);
|
|
18347
18534
|
if (parent === dir) break;
|
|
18348
18535
|
}
|
|
18349
18536
|
if (packageDir) {
|
|
18350
|
-
const pkgRoot = (0,
|
|
18537
|
+
const pkgRoot = (0, import_node_path16.join)(packageDir, "..", "..");
|
|
18351
18538
|
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
18352
18539
|
if (pkgFrom) return pkgFrom;
|
|
18353
18540
|
}
|
|
@@ -18523,8 +18710,8 @@ function registerStageCommands(program3) {
|
|
|
18523
18710
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
18524
18711
|
return decideStage({
|
|
18525
18712
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
18526
|
-
hasCompose: (0,
|
|
18527
|
-
hasEnvExample: (0,
|
|
18713
|
+
hasCompose: (0, import_node_fs20.existsSync)((0, import_node_path17.join)(process.cwd(), "docker-compose.yml")),
|
|
18714
|
+
hasEnvExample: (0, import_node_fs20.existsSync)((0, import_node_path17.join)(process.cwd(), ".env.example"))
|
|
18528
18715
|
});
|
|
18529
18716
|
}
|
|
18530
18717
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -18848,6 +19035,27 @@ function registerBoardCommands(program3) {
|
|
|
18848
19035
|
expected: [...BOARD_STATUSES]
|
|
18849
19036
|
});
|
|
18850
19037
|
}
|
|
19038
|
+
async function runBulkMove(issueRefs, o, status, failLabel) {
|
|
19039
|
+
try {
|
|
19040
|
+
const bulk = await moveBoardIssues({
|
|
19041
|
+
config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
|
|
19042
|
+
selectors: issueRefs,
|
|
19043
|
+
status,
|
|
19044
|
+
repo: o.repo,
|
|
19045
|
+
allowPartial: o.allowPartial
|
|
19046
|
+
});
|
|
19047
|
+
if (o.json) {
|
|
19048
|
+
console.log(JSON.stringify(bulk.results));
|
|
19049
|
+
} else {
|
|
19050
|
+
for (const result of bulk.results) {
|
|
19051
|
+
console.log(result.moved ? result.partial ? `Partially moved ${result.ref}: ${result.warning}` : `Moved ${result.ref} -> ${result.status}` : `Skipped ${result.ref}: ${result.reason}`);
|
|
19052
|
+
}
|
|
19053
|
+
}
|
|
19054
|
+
if (bulk.failed > 0) process.exitCode = 1;
|
|
19055
|
+
} catch (e) {
|
|
19056
|
+
return failGraceful(`${failLabel} failed: ${e.message}`);
|
|
19057
|
+
}
|
|
19058
|
+
}
|
|
18851
19059
|
withExamples(mutating(
|
|
18852
19060
|
board.command("move <status> <issues...>").description(`move one or more board items to a Status: ${BOARD_STATUSES.join(", ")} (quote multi-word statuses)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails"),
|
|
18853
19061
|
(_opts, args) => {
|
|
@@ -18867,25 +19075,7 @@ function registerBoardCommands(program3) {
|
|
|
18867
19075
|
}
|
|
18868
19076
|
return;
|
|
18869
19077
|
}
|
|
18870
|
-
|
|
18871
|
-
const bulk = await moveBoardIssues({
|
|
18872
|
-
config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
|
|
18873
|
-
selectors: issueRefs,
|
|
18874
|
-
status: canonicalStatus,
|
|
18875
|
-
repo: o.repo,
|
|
18876
|
-
allowPartial: o.allowPartial
|
|
18877
|
-
});
|
|
18878
|
-
if (o.json) {
|
|
18879
|
-
console.log(JSON.stringify(bulk.results));
|
|
18880
|
-
} else {
|
|
18881
|
-
for (const result of bulk.results) {
|
|
18882
|
-
console.log(result.moved ? result.partial ? `Partially moved ${result.ref}: ${result.warning}` : `Moved ${result.ref} -> ${result.status}` : `Skipped ${result.ref}: ${result.reason}`);
|
|
18883
|
-
}
|
|
18884
|
-
}
|
|
18885
|
-
if (bulk.failed > 0) process.exitCode = 1;
|
|
18886
|
-
} catch (e) {
|
|
18887
|
-
return failGraceful(`board move failed: ${e.message}`);
|
|
18888
|
-
}
|
|
19078
|
+
await runBulkMove(issueRefs, o, canonicalStatus, "board move");
|
|
18889
19079
|
}), [
|
|
18890
19080
|
'mmi-cli board move "In Progress" 2680',
|
|
18891
19081
|
"mmi-cli board move Done 2680 2681 2682"
|
|
@@ -18929,25 +19119,7 @@ function registerBoardCommands(program3) {
|
|
|
18929
19119
|
}
|
|
18930
19120
|
return;
|
|
18931
19121
|
}
|
|
18932
|
-
|
|
18933
|
-
const bulk = await moveBoardIssues({
|
|
18934
|
-
config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
|
|
18935
|
-
selectors: issueRefs,
|
|
18936
|
-
status: "Done",
|
|
18937
|
-
repo: o.repo,
|
|
18938
|
-
allowPartial: o.allowPartial
|
|
18939
|
-
});
|
|
18940
|
-
if (o.json) {
|
|
18941
|
-
console.log(JSON.stringify(bulk.results));
|
|
18942
|
-
} else {
|
|
18943
|
-
for (const result of bulk.results) {
|
|
18944
|
-
console.log(result.moved ? result.partial ? `Partially moved ${result.ref}: ${result.warning}` : `Moved ${result.ref} -> ${result.status}` : `Skipped ${result.ref}: ${result.reason}`);
|
|
18945
|
-
}
|
|
18946
|
-
}
|
|
18947
|
-
if (bulk.failed > 0) process.exitCode = 1;
|
|
18948
|
-
} catch (e) {
|
|
18949
|
-
return failGraceful(`board done failed: ${e.message}`);
|
|
18950
|
-
}
|
|
19122
|
+
await runBulkMove(issueRefs, o, "Done", "board done");
|
|
18951
19123
|
});
|
|
18952
19124
|
board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
|
|
18953
19125
|
const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
|
|
@@ -18990,9 +19162,9 @@ function registerBoardCommands(program3) {
|
|
|
18990
19162
|
}
|
|
18991
19163
|
|
|
18992
19164
|
// src/merge-cleanup.ts
|
|
18993
|
-
var
|
|
19165
|
+
var import_node_fs21 = require("node:fs");
|
|
18994
19166
|
var import_promises4 = require("node:fs/promises");
|
|
18995
|
-
var
|
|
19167
|
+
var import_node_child_process10 = require("node:child_process");
|
|
18996
19168
|
|
|
18997
19169
|
// src/board-advance.ts
|
|
18998
19170
|
function repoOf2(ref) {
|
|
@@ -19078,7 +19250,7 @@ function boardAdvanceFailureMessage(result) {
|
|
|
19078
19250
|
|
|
19079
19251
|
// src/deferred-registry-store.ts
|
|
19080
19252
|
var import_promises3 = require("node:fs/promises");
|
|
19081
|
-
var
|
|
19253
|
+
var import_node_path18 = require("node:path");
|
|
19082
19254
|
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
19083
19255
|
async function atomicWrite(target, contents) {
|
|
19084
19256
|
const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
@@ -19182,12 +19354,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
|
19182
19354
|
},
|
|
19183
19355
|
// Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
|
|
19184
19356
|
write: async (entries) => {
|
|
19185
|
-
await (0, import_promises3.mkdir)((0,
|
|
19357
|
+
await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
|
|
19186
19358
|
await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
|
|
19187
19359
|
},
|
|
19188
19360
|
// Serialized read-modify-write under the repo-wide lock (#2846).
|
|
19189
19361
|
update: async (mutate) => {
|
|
19190
|
-
await (0, import_promises3.mkdir)((0,
|
|
19362
|
+
await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
|
|
19191
19363
|
const deadline = Date.now() + opts.maxWaitMs;
|
|
19192
19364
|
for (; ; ) {
|
|
19193
19365
|
const guard = await acquireLock(lockPath, opts, deadline);
|
|
@@ -19281,12 +19453,24 @@ async function deleteReviewedRemoteBranch(remote, branch, expectedHeadOid) {
|
|
|
19281
19453
|
}
|
|
19282
19454
|
return "deleted";
|
|
19283
19455
|
}
|
|
19284
|
-
function
|
|
19285
|
-
const
|
|
19286
|
-
const
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
}
|
|
19456
|
+
function evaluatePrMergeHousekeeping(report, context, force = false) {
|
|
19457
|
+
const scratch = report.scratch;
|
|
19458
|
+
const failed = scratch?.status === "failed";
|
|
19459
|
+
const prunable = scratch?.safeAuto ?? 0;
|
|
19460
|
+
const unprunable = scratch?.skipped ?? 0;
|
|
19461
|
+
if (!(failed || prunable > 0 || unprunable > 0)) return { blocked: false };
|
|
19462
|
+
if (force) return { blocked: false };
|
|
19463
|
+
const detail = failed ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
|
|
19464
|
+
return {
|
|
19465
|
+
blocked: true,
|
|
19466
|
+
reason: `${context} housekeeping blocked merge: ${detail}. Run \`mmi-cli gc --scratch --apply\` to clear it, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
|
|
19467
|
+
};
|
|
19468
|
+
}
|
|
19469
|
+
function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
|
|
19470
|
+
const build = options.buildHousekeeping ?? ((repoRoot2) => buildPrMergeScratchHousekeeping(repoRoot2));
|
|
19471
|
+
const housekeeping = build(startingPath || process.cwd());
|
|
19472
|
+
const verdict = evaluatePrMergeHousekeeping(housekeeping, context, options.force);
|
|
19473
|
+
if (verdict.blocked) throw new Error(verdict.reason);
|
|
19290
19474
|
return housekeeping;
|
|
19291
19475
|
}
|
|
19292
19476
|
async function applyGcPlan(plan, remote) {
|
|
@@ -19297,16 +19481,21 @@ async function applyGcPlan(plan, remote) {
|
|
|
19297
19481
|
const deferredStore = await createDeferredWorktreeStore();
|
|
19298
19482
|
const branchTracking = await applyBranchAndTrackingCleanup(plan, {
|
|
19299
19483
|
localBranchHeads,
|
|
19300
|
-
cleanupBranch: (branch, expectedHeadOid) =>
|
|
19301
|
-
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
|
|
19484
|
+
cleanupBranch: (branch, expectedHeadOid) => {
|
|
19485
|
+
const wtDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
19486
|
+
return cleanupPrMergeLocalBranch(branch.branch, {
|
|
19487
|
+
beforeWorktrees,
|
|
19488
|
+
startingPath: branch.worktreePath,
|
|
19489
|
+
pathExists: (p) => (0, import_node_fs21.existsSync)(p),
|
|
19490
|
+
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
19491
|
+
teardownWorktreeStage,
|
|
19492
|
+
deferredStore,
|
|
19493
|
+
expectedHeadOid,
|
|
19494
|
+
detachReparsePoints: wtDeps.detachReparsePoints,
|
|
19495
|
+
// #3064: junction-safe teardown
|
|
19496
|
+
removeWorktreeDir: wtDeps.removeWorktreeDir
|
|
19497
|
+
});
|
|
19498
|
+
},
|
|
19310
19499
|
cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
|
|
19311
19500
|
cleanupTrackingRefs: (trackingRefs, unsafeBranches) => applyTrackingRefCleanup(trackingRefs, remote, unsafeBranches, {
|
|
19312
19501
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
@@ -19323,7 +19512,7 @@ async function applyGcPlan(plan, remote) {
|
|
|
19323
19512
|
for (const wt of plan.worktreeDirs) {
|
|
19324
19513
|
try {
|
|
19325
19514
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
19326
|
-
realpath: (path2) => (0,
|
|
19515
|
+
realpath: (path2) => (0, import_node_fs21.realpathSync)(path2)
|
|
19327
19516
|
});
|
|
19328
19517
|
if (!cleanupTarget.ok) {
|
|
19329
19518
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -19436,7 +19625,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
19436
19625
|
}
|
|
19437
19626
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
19438
19627
|
function spawnDeferredGcSweep() {
|
|
19439
|
-
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn:
|
|
19628
|
+
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19440
19629
|
}
|
|
19441
19630
|
async function createDeferredWorktreeStore() {
|
|
19442
19631
|
try {
|
|
@@ -19450,13 +19639,13 @@ var realWorktreeDirRemover = {
|
|
|
19450
19639
|
probe: (p) => {
|
|
19451
19640
|
let st;
|
|
19452
19641
|
try {
|
|
19453
|
-
st = (0,
|
|
19642
|
+
st = (0, import_node_fs21.lstatSync)(p);
|
|
19454
19643
|
} catch {
|
|
19455
19644
|
return null;
|
|
19456
19645
|
}
|
|
19457
19646
|
if (st.isSymbolicLink()) return "link";
|
|
19458
19647
|
try {
|
|
19459
|
-
(0,
|
|
19648
|
+
(0, import_node_fs21.readlinkSync)(p);
|
|
19460
19649
|
return "link";
|
|
19461
19650
|
} catch {
|
|
19462
19651
|
}
|
|
@@ -19464,7 +19653,7 @@ var realWorktreeDirRemover = {
|
|
|
19464
19653
|
},
|
|
19465
19654
|
readdir: (p) => {
|
|
19466
19655
|
try {
|
|
19467
|
-
return (0,
|
|
19656
|
+
return (0, import_node_fs21.readdirSync)(p);
|
|
19468
19657
|
} catch {
|
|
19469
19658
|
return [];
|
|
19470
19659
|
}
|
|
@@ -19473,9 +19662,9 @@ var realWorktreeDirRemover = {
|
|
|
19473
19662
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
19474
19663
|
detachLink: (p) => {
|
|
19475
19664
|
try {
|
|
19476
|
-
(0,
|
|
19665
|
+
(0, import_node_fs21.rmdirSync)(p);
|
|
19477
19666
|
} catch {
|
|
19478
|
-
(0,
|
|
19667
|
+
(0, import_node_fs21.unlinkSync)(p);
|
|
19479
19668
|
}
|
|
19480
19669
|
},
|
|
19481
19670
|
removeTree: (p) => (0, import_promises4.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -19491,6 +19680,9 @@ function worktreeRemoveDeps(execGit) {
|
|
|
19491
19680
|
return {
|
|
19492
19681
|
git: execGit,
|
|
19493
19682
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
19683
|
+
// #3064: unlink any reparse point (esp. a `node_modules` junction to base) before the git-native
|
|
19684
|
+
// `worktree remove --force`, which would otherwise recurse through it and empty the base checkout.
|
|
19685
|
+
detachReparsePoints: (worktreePath) => detachReparsePoints(worktreePath, realWorktreeDirRemover),
|
|
19494
19686
|
removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
|
|
19495
19687
|
};
|
|
19496
19688
|
}
|
|
@@ -19505,9 +19697,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
19505
19697
|
}
|
|
19506
19698
|
}
|
|
19507
19699
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
19508
|
-
if (!(0,
|
|
19700
|
+
if (!(0, import_node_fs21.existsSync)(statePath)) return false;
|
|
19509
19701
|
try {
|
|
19510
|
-
const state = JSON.parse((0,
|
|
19702
|
+
const state = JSON.parse((0, import_node_fs21.readFileSync)(statePath, "utf8"));
|
|
19511
19703
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
19512
19704
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
19513
19705
|
} catch {
|
|
@@ -19635,9 +19827,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
|
|
|
19635
19827
|
}
|
|
19636
19828
|
|
|
19637
19829
|
// src/worktree-lifecycle-commands.ts
|
|
19638
|
-
var
|
|
19639
|
-
var
|
|
19640
|
-
var import_node_path20 = require("node:path");
|
|
19830
|
+
var import_node_fs22 = require("node:fs");
|
|
19831
|
+
var import_node_path19 = require("node:path");
|
|
19641
19832
|
var GH_TIMEOUT_MS = 2e4;
|
|
19642
19833
|
var DEFAULT_BASE = "origin/development";
|
|
19643
19834
|
var DEFAULT_REMOTE = "origin";
|
|
@@ -19775,13 +19966,13 @@ function registerWorktreeCommands(program3) {
|
|
|
19775
19966
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
19776
19967
|
return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
19777
19968
|
}
|
|
19778
|
-
const gitFile = (0,
|
|
19779
|
-
const isLinked = (0,
|
|
19969
|
+
const gitFile = (0, import_node_path19.join)(wtPath, ".git");
|
|
19970
|
+
const isLinked = (0, import_node_fs22.existsSync)(gitFile) && (0, import_node_fs22.statSync)(gitFile).isFile();
|
|
19780
19971
|
if (apply && !isLinked) {
|
|
19781
19972
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
19782
19973
|
}
|
|
19783
19974
|
const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
19784
|
-
const primaryCheckout = commonDir ? (0,
|
|
19975
|
+
const primaryCheckout = commonDir ? (0, import_node_path19.dirname)(commonDir) : wtPath;
|
|
19785
19976
|
const stageSummary = readStageSummary(wtPath);
|
|
19786
19977
|
const hasStage = stageSummary != null;
|
|
19787
19978
|
const steps = landSteps(branch, true, hasStage);
|
|
@@ -19817,11 +20008,10 @@ function registerWorktreeCommands(program3) {
|
|
|
19817
20008
|
}
|
|
19818
20009
|
}
|
|
19819
20010
|
releaseCwdIfUnderWorktree(wtPath, primaryCheckout);
|
|
19820
|
-
const removeOutcome = await removeWorktreeWithRecovery(
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
});
|
|
20011
|
+
const removeOutcome = await removeWorktreeWithRecovery(
|
|
20012
|
+
wtPath,
|
|
20013
|
+
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-C", primaryCheckout, ...args], { timeout: GIT_TIMEOUT_MS })).stdout)
|
|
20014
|
+
);
|
|
19825
20015
|
report.push({
|
|
19826
20016
|
step: "remove worktree",
|
|
19827
20017
|
status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
|
|
@@ -19894,10 +20084,10 @@ async function gatherWorktreeContext() {
|
|
|
19894
20084
|
}
|
|
19895
20085
|
const orphanDirs = [];
|
|
19896
20086
|
const wtRoot = siblingMmiWorktreesRoot(repoRoot2);
|
|
19897
|
-
if ((0,
|
|
20087
|
+
if ((0, import_node_fs22.existsSync)(wtRoot)) {
|
|
19898
20088
|
let entries = [];
|
|
19899
20089
|
try {
|
|
19900
|
-
entries = (0,
|
|
20090
|
+
entries = (0, import_node_fs22.readdirSync)(wtRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path19.join)(wtRoot, e.name));
|
|
19901
20091
|
} catch {
|
|
19902
20092
|
}
|
|
19903
20093
|
const known = new Set(worktrees.map((w) => w.path));
|
|
@@ -19922,8 +20112,8 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
19922
20112
|
}
|
|
19923
20113
|
|
|
19924
20114
|
// src/issue-commands.ts
|
|
19925
|
-
var
|
|
19926
|
-
var
|
|
20115
|
+
var import_node_fs23 = require("node:fs");
|
|
20116
|
+
var import_node_crypto5 = require("node:crypto");
|
|
19927
20117
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
19928
20118
|
async function editIssue(client, options, deps = {}) {
|
|
19929
20119
|
const parsed = parseIssueRef(options.ref);
|
|
@@ -19937,7 +20127,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
19937
20127
|
if (options.body !== void 0 || options.bodyFile !== void 0) {
|
|
19938
20128
|
let body;
|
|
19939
20129
|
if (options.bodyFile) {
|
|
19940
|
-
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
20130
|
+
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs23.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
19941
20131
|
body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
|
|
19942
20132
|
} else {
|
|
19943
20133
|
body = options.body ?? "";
|
|
@@ -20172,7 +20362,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
20172
20362
|
const identity = `${spec.type}
|
|
20173
20363
|
${spec.title.trim()}
|
|
20174
20364
|
${spec.body ?? ""}`;
|
|
20175
|
-
const hash = (0,
|
|
20365
|
+
const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
20176
20366
|
return `${batchKey}:${hash}`;
|
|
20177
20367
|
}
|
|
20178
20368
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "northStar", "parent", "repo"]);
|
|
@@ -20464,7 +20654,7 @@ function extendCreateCommand(issue2) {
|
|
|
20464
20654
|
if (opts.batch) {
|
|
20465
20655
|
let specs;
|
|
20466
20656
|
try {
|
|
20467
|
-
const raw = (0,
|
|
20657
|
+
const raw = (0, import_node_fs23.readFileSync)(opts.batch, "utf8");
|
|
20468
20658
|
specs = JSON.parse(raw);
|
|
20469
20659
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
20470
20660
|
} catch (e) {
|
|
@@ -20508,13 +20698,13 @@ ${lines}`);
|
|
|
20508
20698
|
}
|
|
20509
20699
|
|
|
20510
20700
|
// src/train-commands.ts
|
|
20511
|
-
var import_node_fs26 = require("node:fs");
|
|
20512
|
-
var import_node_path22 = require("node:path");
|
|
20513
|
-
|
|
20514
|
-
// src/plugin-guard-io.ts
|
|
20515
20701
|
var import_node_fs25 = require("node:fs");
|
|
20516
20702
|
var import_node_path21 = require("node:path");
|
|
20517
|
-
|
|
20703
|
+
|
|
20704
|
+
// src/plugin-guard-io.ts
|
|
20705
|
+
var import_node_fs24 = require("node:fs");
|
|
20706
|
+
var import_node_path20 = require("node:path");
|
|
20707
|
+
var import_node_os6 = require("node:os");
|
|
20518
20708
|
|
|
20519
20709
|
// src/plugin-guard.ts
|
|
20520
20710
|
function buildPluginGuardDecision(i) {
|
|
@@ -20641,13 +20831,13 @@ var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
|
20641
20831
|
function runHostBin(bin, args, opts) {
|
|
20642
20832
|
return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
|
|
20643
20833
|
}
|
|
20644
|
-
var
|
|
20834
|
+
var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
|
|
20645
20835
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
20646
|
-
return (0,
|
|
20836
|
+
return (0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
20647
20837
|
};
|
|
20648
20838
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
20649
20839
|
try {
|
|
20650
|
-
return JSON.parse((0,
|
|
20840
|
+
return JSON.parse((0, import_node_fs24.readFileSync)(installedPluginsPath2(surface), "utf8"));
|
|
20651
20841
|
} catch {
|
|
20652
20842
|
return null;
|
|
20653
20843
|
}
|
|
@@ -20655,13 +20845,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
|
20655
20845
|
function marketplaceCloneCandidates(surface, home) {
|
|
20656
20846
|
if (surface === "codex") {
|
|
20657
20847
|
return [
|
|
20658
|
-
(0,
|
|
20659
|
-
(0,
|
|
20848
|
+
(0, import_node_path20.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
|
|
20849
|
+
(0, import_node_path20.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
|
|
20660
20850
|
];
|
|
20661
20851
|
}
|
|
20662
|
-
return [(0,
|
|
20852
|
+
return [(0, import_node_path20.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
20663
20853
|
}
|
|
20664
|
-
function marketplaceClonePresent(surface, home, exists =
|
|
20854
|
+
function marketplaceClonePresent(surface, home, exists = import_node_fs24.existsSync) {
|
|
20665
20855
|
return marketplaceCloneCandidates(surface, home).some(exists);
|
|
20666
20856
|
}
|
|
20667
20857
|
async function fetchNpmReleasedVersion() {
|
|
@@ -20678,8 +20868,8 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
20678
20868
|
return {
|
|
20679
20869
|
isOrgRepo,
|
|
20680
20870
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
20681
|
-
marketplaceClonePresent: marketplaceClonePresent(surface, (0,
|
|
20682
|
-
pluginCachePresent: (0,
|
|
20871
|
+
marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
|
|
20872
|
+
pluginCachePresent: (0, import_node_fs24.existsSync)((0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
20683
20873
|
};
|
|
20684
20874
|
}
|
|
20685
20875
|
function claudePluginGuardState(isOrgRepo) {
|
|
@@ -20804,7 +20994,7 @@ function formatTrainStatus(r) {
|
|
|
20804
20994
|
// src/train-commands.ts
|
|
20805
20995
|
function readRepoVersion() {
|
|
20806
20996
|
try {
|
|
20807
|
-
return JSON.parse((0,
|
|
20997
|
+
return JSON.parse((0, import_node_fs25.readFileSync)((0, import_node_path21.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
20808
20998
|
} catch {
|
|
20809
20999
|
return void 0;
|
|
20810
21000
|
}
|
|
@@ -21280,7 +21470,7 @@ function registerExplainCommand(program3) {
|
|
|
21280
21470
|
}
|
|
21281
21471
|
|
|
21282
21472
|
// src/pr-commands.ts
|
|
21283
|
-
var
|
|
21473
|
+
var import_promises5 = require("node:fs/promises");
|
|
21284
21474
|
var GC_GH_TIMEOUT_MS4 = 2e4;
|
|
21285
21475
|
var CHECKS_WATCH_POLL_MS = 15e3;
|
|
21286
21476
|
var CHECKS_WATCH_TIMEOUT_MS = 10 * 6e4;
|
|
@@ -21424,10 +21614,10 @@ function registerPrLifecycleCommands(program3) {
|
|
|
21424
21614
|
let body;
|
|
21425
21615
|
try {
|
|
21426
21616
|
if (o.title || o.titleFile) {
|
|
21427
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
21617
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
|
|
21428
21618
|
}
|
|
21429
21619
|
if (o.body || o.bodyFile) {
|
|
21430
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
21620
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
|
|
21431
21621
|
}
|
|
21432
21622
|
} catch (e) {
|
|
21433
21623
|
return fail(`pr edit: ${e.message}`);
|
|
@@ -21457,7 +21647,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
21457
21647
|
}
|
|
21458
21648
|
let body;
|
|
21459
21649
|
try {
|
|
21460
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
21650
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
|
|
21461
21651
|
} catch (e) {
|
|
21462
21652
|
return fail(`pr comment: ${e.message}`);
|
|
21463
21653
|
}
|
|
@@ -21945,9 +22135,10 @@ function checkCliVersion(input) {
|
|
|
21945
22135
|
function checkPluginCache(input) {
|
|
21946
22136
|
const evidence = [
|
|
21947
22137
|
`cached versions: ${input.cached}`,
|
|
21948
|
-
`stale: ${input.stale.length ? input.stale.join(", ") : "(none)"}
|
|
22138
|
+
`stale: ${input.stale.length ? input.stale.join(", ") : "(none)"}`,
|
|
22139
|
+
`orphaned staging dirs: ${input.staging.length ? input.staging.join(", ") : "(none)"}`
|
|
21949
22140
|
];
|
|
21950
|
-
if (input.stale.length === 0) {
|
|
22141
|
+
if (input.stale.length === 0 && input.staging.length === 0) {
|
|
21951
22142
|
return {
|
|
21952
22143
|
ok: true,
|
|
21953
22144
|
label: "plugin cache",
|
|
@@ -21955,11 +22146,21 @@ function checkPluginCache(input) {
|
|
|
21955
22146
|
verbose: evidence
|
|
21956
22147
|
};
|
|
21957
22148
|
}
|
|
21958
|
-
const
|
|
22149
|
+
const parts = [];
|
|
22150
|
+
if (input.stale.length) {
|
|
22151
|
+
const size = input.bytes > 0 ? `, ${(input.bytes / 1e6).toFixed(1)} MB` : "";
|
|
22152
|
+
parts.push(`${input.stale.length} stale${size}`);
|
|
22153
|
+
}
|
|
22154
|
+
if (input.staging.length) {
|
|
22155
|
+
const size = input.stagingBytes > 0 ? `, ${(input.stagingBytes / 1e6).toFixed(1)} MB` : "";
|
|
22156
|
+
parts.push(`${input.staging.length} orphaned staging dir(s)${size}`);
|
|
22157
|
+
}
|
|
21959
22158
|
return {
|
|
21960
22159
|
ok: false,
|
|
21961
22160
|
label: "plugin cache",
|
|
21962
|
-
|
|
22161
|
+
// Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
|
|
22162
|
+
// on its own so a cache with zero versioned dirs still gets an honest ✗.
|
|
22163
|
+
detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
|
|
21963
22164
|
fix: "run `mmi-cli plugin-prune` to review, then `mmi-cli plugin-prune --apply` to delete",
|
|
21964
22165
|
verbose: evidence
|
|
21965
22166
|
};
|
|
@@ -21999,12 +22200,27 @@ function checkSchedules(probe) {
|
|
|
21999
22200
|
ok: false,
|
|
22000
22201
|
label: "schedules",
|
|
22001
22202
|
detail: `${probe.drift.length} drift finding(s)`,
|
|
22002
|
-
fix: "run `mmi-cli schedules` \u2014
|
|
22203
|
+
fix: "run `mmi-cli schedules` \u2014 file/registry/live disagree; each drift line names its per-class remedy, applied in the owning repo",
|
|
22003
22204
|
verbose: evidence
|
|
22004
22205
|
};
|
|
22005
22206
|
}
|
|
22006
22207
|
return { ok: true, label: "schedules", detail: `${probe.armed} armed`, verbose: evidence };
|
|
22007
22208
|
}
|
|
22209
|
+
function checkDocsAudit(probe) {
|
|
22210
|
+
if (!probe) return null;
|
|
22211
|
+
if (!probe.armed) {
|
|
22212
|
+
return { ok: true, label: "docs-audit", detail: "janitor not armed", verbose: [probe.detail] };
|
|
22213
|
+
}
|
|
22214
|
+
if (!probe.ok) {
|
|
22215
|
+
return {
|
|
22216
|
+
ok: false,
|
|
22217
|
+
label: "docs-audit",
|
|
22218
|
+
fix: `${probe.detail} \u2014 re-run the janitor in the owning repo or re-arm its schedule (the remedy never lives in this doctor)`,
|
|
22219
|
+
verbose: [probe.detail]
|
|
22220
|
+
};
|
|
22221
|
+
}
|
|
22222
|
+
return { ok: true, label: "docs-audit", detail: probe.detail, verbose: [probe.detail] };
|
|
22223
|
+
}
|
|
22008
22224
|
function planGitignore(current) {
|
|
22009
22225
|
const { content, changed } = upsertManagedGitignoreBlock(current);
|
|
22010
22226
|
return changed ? { ok: false, content } : { ok: true };
|
|
@@ -22063,6 +22279,15 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
22063
22279
|
const sched = checkSchedules(probe);
|
|
22064
22280
|
if (sched) checks.push(sched);
|
|
22065
22281
|
}
|
|
22282
|
+
if (!opts.fast && !opts.banner && !opts.preflight && deps.docsAudit) {
|
|
22283
|
+
const probe = await deps.docsAudit().catch((e) => ({
|
|
22284
|
+
armed: true,
|
|
22285
|
+
ok: false,
|
|
22286
|
+
detail: `docs-audit probe failed \u2014 ${e.message}`
|
|
22287
|
+
}));
|
|
22288
|
+
const docsAudit2 = checkDocsAudit(probe);
|
|
22289
|
+
if (docsAudit2) checks.push(docsAudit2);
|
|
22290
|
+
}
|
|
22066
22291
|
if (isOrgRepo && !opts.fast && !opts.banner) {
|
|
22067
22292
|
try {
|
|
22068
22293
|
checks.push(checkTrainSync(await deps.syncTrain()));
|
|
@@ -22147,17 +22372,17 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
22147
22372
|
}
|
|
22148
22373
|
|
|
22149
22374
|
// src/doctor-io.ts
|
|
22150
|
-
var
|
|
22151
|
-
var
|
|
22152
|
-
var
|
|
22153
|
-
var
|
|
22375
|
+
var import_node_fs26 = require("node:fs");
|
|
22376
|
+
var import_node_os7 = require("node:os");
|
|
22377
|
+
var import_node_path22 = require("node:path");
|
|
22378
|
+
var import_node_child_process11 = require("node:child_process");
|
|
22154
22379
|
var import_node_util8 = require("node:util");
|
|
22155
|
-
var execFileP6 = (0, import_node_util8.promisify)(
|
|
22380
|
+
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process11.execFile);
|
|
22156
22381
|
var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
22157
22382
|
function installedClaudePluginVersion() {
|
|
22158
22383
|
try {
|
|
22159
22384
|
const file = JSON.parse(
|
|
22160
|
-
(0,
|
|
22385
|
+
(0, import_node_fs26.readFileSync)((0, import_node_path22.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
22161
22386
|
);
|
|
22162
22387
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
22163
22388
|
if (versions.length === 0) return void 0;
|
|
@@ -22168,7 +22393,7 @@ function installedClaudePluginVersion() {
|
|
|
22168
22393
|
}
|
|
22169
22394
|
function worktreeRootSync() {
|
|
22170
22395
|
try {
|
|
22171
|
-
const out = (0,
|
|
22396
|
+
const out = (0, import_node_child_process11.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
22172
22397
|
let root = out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
22173
22398
|
if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
|
|
22174
22399
|
return root || null;
|
|
@@ -22178,13 +22403,13 @@ function worktreeRootSync() {
|
|
|
22178
22403
|
}
|
|
22179
22404
|
var gitignorePath = () => {
|
|
22180
22405
|
const root = worktreeRootSync();
|
|
22181
|
-
return root === null ? null : (0,
|
|
22406
|
+
return root === null ? null : (0, import_node_path22.join)(root, ".gitignore");
|
|
22182
22407
|
};
|
|
22183
22408
|
function readGitignore() {
|
|
22184
22409
|
const path2 = gitignorePath();
|
|
22185
22410
|
if (path2 === null) return null;
|
|
22186
22411
|
try {
|
|
22187
|
-
return (0,
|
|
22412
|
+
return (0, import_node_fs26.readFileSync)(path2, "utf8");
|
|
22188
22413
|
} catch {
|
|
22189
22414
|
return null;
|
|
22190
22415
|
}
|
|
@@ -22193,7 +22418,7 @@ function writeGitignore(content) {
|
|
|
22193
22418
|
const path2 = gitignorePath();
|
|
22194
22419
|
if (path2 === null) return false;
|
|
22195
22420
|
try {
|
|
22196
|
-
(0,
|
|
22421
|
+
(0, import_node_fs26.writeFileSync)(path2, content, "utf8");
|
|
22197
22422
|
return true;
|
|
22198
22423
|
} catch {
|
|
22199
22424
|
return false;
|
|
@@ -22217,11 +22442,27 @@ async function repoRoot() {
|
|
|
22217
22442
|
}
|
|
22218
22443
|
function hasRepoLocalWorktrees() {
|
|
22219
22444
|
const root = worktreeRootSync();
|
|
22220
|
-
return root !== null && (0,
|
|
22445
|
+
return root !== null && (0, import_node_fs26.existsSync)((0, import_node_path22.join)(root, ".worktrees"));
|
|
22221
22446
|
}
|
|
22222
22447
|
|
|
22223
22448
|
// src/index.ts
|
|
22224
22449
|
var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
22450
|
+
async function currentRepoFullName() {
|
|
22451
|
+
const remote = (await gitOut(["remote", "get-url", "origin"])).replace(/\.git$/, "");
|
|
22452
|
+
const parts = remote.split(/[:/]/).filter(Boolean);
|
|
22453
|
+
if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
22454
|
+
return `mutmutco/${await repoSlug()}`;
|
|
22455
|
+
}
|
|
22456
|
+
async function readDocsAuditFetch(repo) {
|
|
22457
|
+
const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
|
|
22458
|
+
if ("notArmed" in list) return { notArmed: true };
|
|
22459
|
+
if (!list.ok) return { ok: false, error: list.error };
|
|
22460
|
+
const row = list.rows.find((r) => r.repo === repo);
|
|
22461
|
+
return {
|
|
22462
|
+
ok: true,
|
|
22463
|
+
verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
|
|
22464
|
+
};
|
|
22465
|
+
}
|
|
22225
22466
|
function mmiDoctorDeps() {
|
|
22226
22467
|
return {
|
|
22227
22468
|
githubLogin,
|
|
@@ -22265,12 +22506,18 @@ function mmiDoctorDeps() {
|
|
|
22265
22506
|
return { personal, app };
|
|
22266
22507
|
},
|
|
22267
22508
|
pluginCache: () => {
|
|
22268
|
-
const plan = buildPluginCachePlan(
|
|
22269
|
-
|
|
22270
|
-
|
|
22271
|
-
|
|
22272
|
-
|
|
22273
|
-
return {
|
|
22509
|
+
const plan = buildPluginCachePlan(
|
|
22510
|
+
(0, import_node_os8.homedir)(),
|
|
22511
|
+
runningPluginVersion(process.env, resolveClientVersion()),
|
|
22512
|
+
pluginCacheFsDeps((0, import_node_os8.homedir)(), () => 0)
|
|
22513
|
+
);
|
|
22514
|
+
return {
|
|
22515
|
+
stale: plan.prune,
|
|
22516
|
+
bytes: plan.bytes,
|
|
22517
|
+
cached: plan.keep.length + plan.prune.length,
|
|
22518
|
+
staging: plan.staging.map((s) => s.name),
|
|
22519
|
+
stagingBytes: plan.stagingBytes
|
|
22520
|
+
};
|
|
22274
22521
|
},
|
|
22275
22522
|
// #3008: the schedules-notebook drift probe, compacted for the check. Full doctor only — the gather in
|
|
22276
22523
|
// runDoctorClean skips this dep entirely on fast/banner/preflight runs.
|
|
@@ -22283,6 +22530,15 @@ function mmiDoctorDeps() {
|
|
|
22283
22530
|
incomplete,
|
|
22284
22531
|
drift
|
|
22285
22532
|
};
|
|
22533
|
+
},
|
|
22534
|
+
// #3075: the docs-audit dead-man verdict probe, compacted for the check. Full doctor only, exactly like
|
|
22535
|
+
// schedulesNotebook — a registry read the SessionStart banner / --preflight / --fast lanes never pay for.
|
|
22536
|
+
// The 404/absent route maps to `armed:false` (an informational "not armed" line), never a false RED.
|
|
22537
|
+
docsAudit: async () => {
|
|
22538
|
+
const repo = await currentRepoFullName();
|
|
22539
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
22540
|
+
const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
|
|
22541
|
+
return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
|
|
22286
22542
|
}
|
|
22287
22543
|
};
|
|
22288
22544
|
}
|
|
@@ -22357,19 +22613,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
22357
22613
|
});
|
|
22358
22614
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
22359
22615
|
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) => {
|
|
22360
|
-
const path2 = (0,
|
|
22361
|
-
const current = (0,
|
|
22616
|
+
const path2 = (0, import_node_path23.join)(process.cwd(), ".gitignore");
|
|
22617
|
+
const current = (0, import_node_fs27.existsSync)(path2) ? (0, import_node_fs27.readFileSync)(path2, "utf8") : null;
|
|
22362
22618
|
const plan = planManagedGitignore(current);
|
|
22363
22619
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
22364
22620
|
if (opts.json) {
|
|
22365
|
-
if (opts.write && plan.changed) (0,
|
|
22621
|
+
if (opts.write && plan.changed) (0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
|
|
22366
22622
|
console.log(JSON.stringify(plan, null, 2));
|
|
22367
22623
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
22368
22624
|
return;
|
|
22369
22625
|
}
|
|
22370
22626
|
if (opts.write) {
|
|
22371
22627
|
if (plan.changed) {
|
|
22372
|
-
(0,
|
|
22628
|
+
(0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
|
|
22373
22629
|
console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
|
|
22374
22630
|
} else {
|
|
22375
22631
|
console.log("mmi-cli rules gitignore: up to date");
|
|
@@ -22523,7 +22779,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
22523
22779
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
22524
22780
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
22525
22781
|
return new Promise((resolve5, reject) => {
|
|
22526
|
-
const child = (0,
|
|
22782
|
+
const child = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
22527
22783
|
const timer = setTimeout(() => {
|
|
22528
22784
|
try {
|
|
22529
22785
|
child.kill();
|
|
@@ -22545,7 +22801,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
22545
22801
|
async function primaryCheckoutRoot(worktreeRoot) {
|
|
22546
22802
|
try {
|
|
22547
22803
|
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
22548
|
-
return out ? (0,
|
|
22804
|
+
return out ? (0, import_node_path23.dirname)(out) : void 0;
|
|
22549
22805
|
} catch {
|
|
22550
22806
|
return void 0;
|
|
22551
22807
|
}
|
|
@@ -22560,26 +22816,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
22560
22816
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
22561
22817
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
22562
22818
|
const take = () => {
|
|
22563
|
-
const fd = (0,
|
|
22819
|
+
const fd = (0, import_node_fs27.openSync)(lockPath, "wx");
|
|
22564
22820
|
try {
|
|
22565
|
-
(0,
|
|
22821
|
+
(0, import_node_fs27.writeSync)(fd, String(Date.now()));
|
|
22566
22822
|
} finally {
|
|
22567
|
-
(0,
|
|
22823
|
+
(0, import_node_fs27.closeSync)(fd);
|
|
22568
22824
|
}
|
|
22569
22825
|
return () => {
|
|
22570
22826
|
try {
|
|
22571
|
-
(0,
|
|
22827
|
+
(0, import_node_fs27.rmSync)(lockPath, { force: true });
|
|
22572
22828
|
} catch {
|
|
22573
22829
|
}
|
|
22574
22830
|
};
|
|
22575
22831
|
};
|
|
22576
22832
|
try {
|
|
22577
|
-
(0,
|
|
22833
|
+
(0, import_node_fs27.mkdirSync)((0, import_node_path23.dirname)(lockPath), { recursive: true });
|
|
22578
22834
|
return take();
|
|
22579
22835
|
} catch {
|
|
22580
22836
|
try {
|
|
22581
|
-
if (Date.now() - (0,
|
|
22582
|
-
(0,
|
|
22837
|
+
if (Date.now() - (0, import_node_fs27.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
22838
|
+
(0, import_node_fs27.rmSync)(lockPath, { force: true });
|
|
22583
22839
|
return take();
|
|
22584
22840
|
}
|
|
22585
22841
|
} catch {
|
|
@@ -22759,7 +23015,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
22759
23015
|
try {
|
|
22760
23016
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
|
|
22761
23017
|
if (o.repo) args.push("--repo", o.repo);
|
|
22762
|
-
spawnDetachedSelf(args, { spawn:
|
|
23018
|
+
spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
|
|
22763
23019
|
} catch {
|
|
22764
23020
|
}
|
|
22765
23021
|
}
|
|
@@ -22767,6 +23023,50 @@ registerSecretsCommands(program2);
|
|
|
22767
23023
|
registerEdgeCommands(program2);
|
|
22768
23024
|
registerBoxCommands(program2);
|
|
22769
23025
|
registerSchedulesCommands(program2);
|
|
23026
|
+
var docs = program2.command("docs").description("generated docs surfaces \u2014 the routing index (org knowledge layer)");
|
|
23027
|
+
docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").action(async (o) => {
|
|
23028
|
+
try {
|
|
23029
|
+
const root = await repoRoot();
|
|
23030
|
+
const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
|
|
23031
|
+
if (o.check) {
|
|
23032
|
+
if (result.drift) {
|
|
23033
|
+
return failGraceful(`docs index: ${DOCS_INDEX_PATH} is stale \u2014 run \`mmi-cli docs index --write\` and commit the result`);
|
|
23034
|
+
}
|
|
23035
|
+
console.log(`docs index: ${DOCS_INDEX_PATH} is current`);
|
|
23036
|
+
return;
|
|
23037
|
+
}
|
|
23038
|
+
console.log(result.wrote ? `docs index: wrote ${DOCS_INDEX_PATH}` : `docs index: ${DOCS_INDEX_PATH} already current`);
|
|
23039
|
+
} catch (e) {
|
|
23040
|
+
await failGraceful(e.message);
|
|
23041
|
+
}
|
|
23042
|
+
});
|
|
23043
|
+
var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger \u2014 record a dated run verdict, or read the dead-man status back");
|
|
23044
|
+
docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").requiredOption("--outcome <kind>", "clean | refreshed | failed").option("--count <n>", "docs refreshed (required when --outcome refreshed)").option("--reason <text>", "why the run failed (required when --outcome failed)").requiredOption("--checker-vendor <vendor>", "which vendor's model actually ran the check").action(async (o) => {
|
|
23045
|
+
try {
|
|
23046
|
+
const repo = o.repo ?? await currentRepoFullName();
|
|
23047
|
+
const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
23048
|
+
let outcome;
|
|
23049
|
+
if (o.outcome === "clean") outcome = { kind: "clean" };
|
|
23050
|
+
else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
|
|
23051
|
+
else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
|
|
23052
|
+
else return failGraceful(`docs-audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
|
|
23053
|
+
const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
|
|
23054
|
+
await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
|
|
23055
|
+
} catch (e) {
|
|
23056
|
+
await failGraceful(e.message);
|
|
23057
|
+
}
|
|
23058
|
+
});
|
|
23059
|
+
docsAudit.command("status").description("read the janitor dead-man verdict back for one repo \u2014 missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").action(async (o) => {
|
|
23060
|
+
try {
|
|
23061
|
+
const repo = o.repo ?? await currentRepoFullName();
|
|
23062
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
23063
|
+
const result = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
|
|
23064
|
+
console.log(result.line);
|
|
23065
|
+
if (!result.ok) process.exitCode = 1;
|
|
23066
|
+
} catch (e) {
|
|
23067
|
+
await failGraceful(e.message);
|
|
23068
|
+
}
|
|
23069
|
+
});
|
|
22770
23070
|
async function reportWrite(label, res) {
|
|
22771
23071
|
if (res.ok) {
|
|
22772
23072
|
console.log(JSON.stringify(res.body));
|
|
@@ -23104,7 +23404,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
23104
23404
|
if (dupe) return fail(`project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
23105
23405
|
if (o.secretsFile) {
|
|
23106
23406
|
try {
|
|
23107
|
-
vars.push(`secrets=${(0,
|
|
23407
|
+
vars.push(`secrets=${(0, import_node_fs27.readFileSync)(o.secretsFile, "utf8")}`);
|
|
23108
23408
|
} catch (e) {
|
|
23109
23409
|
return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
23110
23410
|
}
|
|
@@ -23400,8 +23700,8 @@ withExamples(mutating(
|
|
|
23400
23700
|
let extraLabels = [];
|
|
23401
23701
|
try {
|
|
23402
23702
|
issueType = resolveCreateType(o.type, "issue create");
|
|
23403
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
23404
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23703
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23704
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23405
23705
|
if (o.northStar !== void 0) {
|
|
23406
23706
|
northStarSlug = normalizeNorthStarSlug(o.northStar);
|
|
23407
23707
|
body = appendNorthStarLine(body, northStarSlug);
|
|
@@ -23546,7 +23846,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
|
|
|
23546
23846
|
}
|
|
23547
23847
|
let body;
|
|
23548
23848
|
try {
|
|
23549
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23849
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23550
23850
|
} catch (e) {
|
|
23551
23851
|
return fail(`issue comment: ${e.message}`);
|
|
23552
23852
|
}
|
|
@@ -23606,8 +23906,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
23606
23906
|
let title;
|
|
23607
23907
|
const sourceRepo = o.repo ?? await resolveRepo(void 0);
|
|
23608
23908
|
try {
|
|
23609
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
23610
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23909
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23910
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23611
23911
|
priority = resolveCreatePriority(o.priority, "report");
|
|
23612
23912
|
if (!ISSUE_TYPES.includes(o.type)) {
|
|
23613
23913
|
throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
@@ -23656,8 +23956,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
23656
23956
|
let args;
|
|
23657
23957
|
try {
|
|
23658
23958
|
skill = assertSkillName(o.skill);
|
|
23659
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
23660
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
23959
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23960
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23661
23961
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
23662
23962
|
priority = resolveCreatePriority(o.priority, "skill-lesson");
|
|
23663
23963
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -23708,11 +24008,12 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
23708
24008
|
let body;
|
|
23709
24009
|
let title;
|
|
23710
24010
|
try {
|
|
23711
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
23712
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
24011
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
24012
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
23713
24013
|
} catch (e) {
|
|
23714
24014
|
return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
23715
24015
|
}
|
|
24016
|
+
body = normalizeClosingDirectives(body);
|
|
23716
24017
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
23717
24018
|
console.log(JSON.stringify(created));
|
|
23718
24019
|
}), [
|
|
@@ -23743,11 +24044,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
23743
24044
|
}
|
|
23744
24045
|
});
|
|
23745
24046
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
23746
|
-
const wfDir = (0,
|
|
23747
|
-
if (!(0,
|
|
23748
|
-
return (0,
|
|
24047
|
+
const wfDir = (0, import_node_path23.join)(cwd, ".github", "workflows");
|
|
24048
|
+
if (!(0, import_node_fs27.existsSync)(wfDir)) return [];
|
|
24049
|
+
return (0, import_node_fs27.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
23749
24050
|
try {
|
|
23750
|
-
return workflowReportsPrChecks((0,
|
|
24051
|
+
return workflowReportsPrChecks((0, import_node_fs27.readFileSync)((0, import_node_path23.join)(wfDir, name), "utf8"));
|
|
23751
24052
|
} catch {
|
|
23752
24053
|
return true;
|
|
23753
24054
|
}
|
|
@@ -23774,16 +24075,16 @@ function ciAuditDeps() {
|
|
|
23774
24075
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
23775
24076
|
readSeedFile: (path2) => {
|
|
23776
24077
|
if (!root) return null;
|
|
23777
|
-
const fullPath = (0,
|
|
23778
|
-
return (0,
|
|
24078
|
+
const fullPath = (0, import_node_path23.join)(root, path2);
|
|
24079
|
+
return (0, import_node_fs27.existsSync)(fullPath) ? (0, import_node_fs27.readFileSync)(fullPath, "utf8") : null;
|
|
23779
24080
|
}
|
|
23780
24081
|
};
|
|
23781
24082
|
}
|
|
23782
24083
|
function hubRoot() {
|
|
23783
|
-
const fromPkg = (0,
|
|
24084
|
+
const fromPkg = (0, import_node_path23.join)(__dirname, "..", "..");
|
|
23784
24085
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
23785
|
-
if ((0,
|
|
23786
|
-
if ((0,
|
|
24086
|
+
if ((0, import_node_fs27.existsSync)((0, import_node_path23.join)(fromPkg, marker))) return fromPkg;
|
|
24087
|
+
if ((0, import_node_fs27.existsSync)((0, import_node_path23.join)(process.cwd(), marker))) return process.cwd();
|
|
23787
24088
|
return null;
|
|
23788
24089
|
}
|
|
23789
24090
|
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) => {
|
|
@@ -23824,10 +24125,10 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
23824
24125
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
23825
24126
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
23826
24127
|
});
|
|
23827
|
-
pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").action(async (number, o) => {
|
|
24128
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
|
|
23828
24129
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
23829
24130
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
23830
|
-
assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land");
|
|
24131
|
+
assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
|
|
23831
24132
|
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
23832
24133
|
resolveRepo: async (prNumber, repoOpt) => {
|
|
23833
24134
|
const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
|
|
@@ -23917,13 +24218,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
23917
24218
|
}
|
|
23918
24219
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
23919
24220
|
});
|
|
23920
|
-
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").action(async (number, o) => {
|
|
24221
|
+
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
|
|
23921
24222
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
23922
24223
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
23923
24224
|
const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
|
|
23924
24225
|
const headIsProtected = isProtectedBranch(headRef);
|
|
23925
24226
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
23926
|
-
const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge");
|
|
24227
|
+
const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
|
|
23927
24228
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
23928
24229
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
23929
24230
|
);
|
|
@@ -23997,15 +24298,18 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
23997
24298
|
const deferredStore = await createDeferredWorktreeStore();
|
|
23998
24299
|
let localCleanup;
|
|
23999
24300
|
try {
|
|
24301
|
+
const wtDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
24000
24302
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
24001
24303
|
beforeWorktrees,
|
|
24002
24304
|
startingPath,
|
|
24003
|
-
pathExists: (p) => (0,
|
|
24305
|
+
pathExists: (p) => (0, import_node_fs27.existsSync)(p),
|
|
24004
24306
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
24005
24307
|
teardownWorktreeStage,
|
|
24006
24308
|
deferredStore,
|
|
24007
24309
|
expectedHeadOid: headRefOid || void 0,
|
|
24008
|
-
|
|
24310
|
+
detachReparsePoints: wtDeps.detachReparsePoints,
|
|
24311
|
+
// #3064: junction-safe teardown
|
|
24312
|
+
removeWorktreeDir: wtDeps.removeWorktreeDir,
|
|
24009
24313
|
// After merge, return the local checkout to the (fast-forwarded) branch the PR merged into so
|
|
24010
24314
|
// grind/build never leave the primary parked on a dead feature branch (#1606).
|
|
24011
24315
|
returnToBranch: baseRef,
|
|
@@ -24094,6 +24398,7 @@ function trainApplyDeps() {
|
|
|
24094
24398
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
24095
24399
|
// set never reaches this guard: the Hub is direct-track.)
|
|
24096
24400
|
hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: ["package.json", "package-lock.json"] }),
|
|
24401
|
+
hotfixCarries: (input) => checkHotfixCarries(input),
|
|
24097
24402
|
// #2737: enqueue GitHub auto-merge on the `main -> development` alignment PR the release opens, so the
|
|
24098
24403
|
// train lands the back-merge itself instead of leaving a manual step. Same enqueue path `pr land` uses,
|
|
24099
24404
|
// but method `--merge` (NOT squash): the misalignment guard needs the merge parentage to survive.
|
|
@@ -24101,8 +24406,8 @@ function trainApplyDeps() {
|
|
|
24101
24406
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
24102
24407
|
announce: (args) => announceRelease({
|
|
24103
24408
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
24104
|
-
readFile: (path2) => (0,
|
|
24105
|
-
removeFile: (path2) => (0,
|
|
24409
|
+
readFile: (path2) => (0, import_promises6.readFile)(path2, "utf8"),
|
|
24410
|
+
removeFile: (path2) => (0, import_promises6.unlink)(path2)
|
|
24106
24411
|
}, args),
|
|
24107
24412
|
fetchEdgeDomains: async (slug) => {
|
|
24108
24413
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -24249,7 +24554,7 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
|
|
|
24249
24554
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli hotfix: dry-run plan", steps));
|
|
24250
24555
|
});
|
|
24251
24556
|
hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
|
|
24252
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
|
|
24557
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] }), o.json, renderHotfixRelease));
|
|
24253
24558
|
hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
|
|
24254
24559
|
var ci = program2.command("ci").description("org CI + merge-readiness audit and reconcile");
|
|
24255
24560
|
ci.command("audit").description("read-only fleet scan: gate workflow, ruleset contexts, auto-merge, registry META (#1440)").option("--json", "machine-readable output").option("--markdown", "fleet summary table for issue comments").option("--repo <owner/repo>", "audit one repo instead of the full registry").action(async (o) => {
|
|
@@ -24284,43 +24589,6 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
|
|
|
24284
24589
|
}
|
|
24285
24590
|
if (!audit.ok) process.exitCode = 1;
|
|
24286
24591
|
});
|
|
24287
|
-
var wiki = program2.command("wiki").description("release wiki lane \u2014 publish generated pages via a short-lived, repo-scoped minted token");
|
|
24288
|
-
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) => {
|
|
24289
|
-
const repo = await resolveRepo(o.repo);
|
|
24290
|
-
if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
|
|
24291
|
-
const cfg = await loadConfig();
|
|
24292
|
-
const ok = await wikiPublish(
|
|
24293
|
-
{
|
|
24294
|
-
mint: (r) => mintWikiToken(r, registryClientDeps(cfg)),
|
|
24295
|
-
publish: (o2) => publishWikiViaGit(realWikiGitPublishDeps(), o2),
|
|
24296
|
-
log: (msg) => console.log(msg),
|
|
24297
|
-
err: (msg) => console.error(msg)
|
|
24298
|
-
},
|
|
24299
|
-
{ repo, pagesDir, json: o.json }
|
|
24300
|
-
);
|
|
24301
|
-
if (!ok) process.exitCode = 1;
|
|
24302
|
-
});
|
|
24303
|
-
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) => {
|
|
24304
|
-
const repo = await resolveRepo(o.repo);
|
|
24305
|
-
if (!repo) return fail("wiki plan: could not determine the target repo (pass --repo owner/name)");
|
|
24306
|
-
const cfg = await loadConfig();
|
|
24307
|
-
const readers = fsReaders(process.cwd());
|
|
24308
|
-
const ok = await wikiPlan(
|
|
24309
|
-
{
|
|
24310
|
-
collectPages: () => collectFeaturePages(readers),
|
|
24311
|
-
readSource: (p) => readers.readDoc(p),
|
|
24312
|
-
mintAndReadStamp: async (r) => {
|
|
24313
|
-
const minted = await mintWikiToken(r, registryClientDeps(cfg));
|
|
24314
|
-
if (!minted.ok) return { ok: false, error: minted.error };
|
|
24315
|
-
return readWikiStampViaGit(realWikiGitPublishDeps(), { repo: r, token: minted.mint.token });
|
|
24316
|
-
},
|
|
24317
|
-
out: (json) => console.log(json),
|
|
24318
|
-
err: (msg) => console.error(msg)
|
|
24319
|
-
},
|
|
24320
|
-
{ repo }
|
|
24321
|
-
);
|
|
24322
|
-
if (!ok) process.exitCode = 1;
|
|
24323
|
-
});
|
|
24324
24592
|
registerBootstrapCommands(program2);
|
|
24325
24593
|
var access = program2.command("access").description("org access audit (read-only)");
|
|
24326
24594
|
access.command("role [repo]").description("D14 train authority for a repo (server-side Hub check): master | project-admin | developer").option("--json", "machine-readable output").action(async (repoArg) => {
|
|
@@ -24358,16 +24626,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
24358
24626
|
const repoClass = o.class;
|
|
24359
24627
|
targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
|
|
24360
24628
|
} else {
|
|
24361
|
-
const
|
|
24362
|
-
if (!
|
|
24363
|
-
|
|
24364
|
-
|
|
24629
|
+
const resolution = resolveOrgAuditTargets(registryProjects);
|
|
24630
|
+
if (!resolution.ok) {
|
|
24631
|
+
const report2 = { ok: false, owners: [], orgFindings: [resolution.finding], repos: [] };
|
|
24632
|
+
console.log(o.json ? JSON.stringify(report2, null, 2) : renderAccessReport(report2));
|
|
24633
|
+
process.exitCode = 1;
|
|
24634
|
+
return;
|
|
24635
|
+
}
|
|
24636
|
+
targets = resolution.targets;
|
|
24365
24637
|
}
|
|
24366
24638
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
24367
|
-
const fileMatrix = (0,
|
|
24639
|
+
const fileMatrix = (0, import_node_fs27.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs27.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
24368
24640
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
24369
24641
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
24370
|
-
const fileContracts = (0,
|
|
24642
|
+
const fileContracts = (0, import_node_fs27.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs27.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
24371
24643
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
24372
24644
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
24373
24645
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -24377,7 +24649,7 @@ access.command("capabilities").description("enumerate your effective vault reach
|
|
|
24377
24649
|
var isWin2 = process.platform === "win32";
|
|
24378
24650
|
program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--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)").option("--self", "verify CLI/plugin version parity, PATH, gh auth scopes, and hook wiring (offline-safe; suggests plugin-heal on a hard gap) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n 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(async (opts) => {
|
|
24379
24651
|
if (opts.guide) {
|
|
24380
|
-
consoleIo.log("MMI Agentic Onboarding:
|
|
24652
|
+
consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
|
|
24381
24653
|
return;
|
|
24382
24654
|
}
|
|
24383
24655
|
const noRepoWrites = opts.repoWrites === false;
|
|
@@ -24400,33 +24672,79 @@ function directoryBytes(path2) {
|
|
|
24400
24672
|
let total = 0;
|
|
24401
24673
|
let entries;
|
|
24402
24674
|
try {
|
|
24403
|
-
entries = (0,
|
|
24675
|
+
entries = (0, import_node_fs27.readdirSync)(path2, { withFileTypes: true });
|
|
24404
24676
|
} catch {
|
|
24405
24677
|
return 0;
|
|
24406
24678
|
}
|
|
24407
24679
|
for (const entry of entries) {
|
|
24408
|
-
const child = (0,
|
|
24680
|
+
const child = (0, import_node_path23.join)(path2, entry.name);
|
|
24409
24681
|
if (entry.isDirectory()) total += directoryBytes(child);
|
|
24410
24682
|
else {
|
|
24411
24683
|
try {
|
|
24412
|
-
total += (0,
|
|
24684
|
+
total += (0, import_node_fs27.statSync)(child).size;
|
|
24413
24685
|
} catch {
|
|
24414
24686
|
}
|
|
24415
24687
|
}
|
|
24416
24688
|
}
|
|
24417
24689
|
return total;
|
|
24418
24690
|
}
|
|
24419
|
-
|
|
24420
|
-
|
|
24421
|
-
|
|
24422
|
-
|
|
24423
|
-
|
|
24424
|
-
|
|
24425
|
-
|
|
24691
|
+
function listDirEntries(dir) {
|
|
24692
|
+
return (0, import_node_fs27.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
24693
|
+
}
|
|
24694
|
+
function readInstalledPluginRefs(home) {
|
|
24695
|
+
const p = installedPluginsPath(home);
|
|
24696
|
+
if (!(0, import_node_fs27.existsSync)(p)) return [];
|
|
24697
|
+
try {
|
|
24698
|
+
return installedPluginPaths((0, import_node_fs27.readFileSync)(p, "utf8"));
|
|
24699
|
+
} catch {
|
|
24700
|
+
return null;
|
|
24701
|
+
}
|
|
24702
|
+
}
|
|
24703
|
+
function pluginCacheFsDeps(home, dirBytes) {
|
|
24704
|
+
return {
|
|
24705
|
+
exists: (p) => (0, import_node_fs27.existsSync)(p),
|
|
24706
|
+
listVersionDirs: (root) => (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
24707
|
+
dirBytes,
|
|
24708
|
+
listStagingDirs: (root) => (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
24709
|
+
try {
|
|
24710
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path23.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs27.statSync)(p).mtimeMs) };
|
|
24711
|
+
} catch {
|
|
24712
|
+
return { name: d.name, mtimeMs: Date.now() };
|
|
24713
|
+
}
|
|
24714
|
+
}),
|
|
24715
|
+
readInstalledPluginPaths: () => readInstalledPluginRefs(home),
|
|
24716
|
+
now: () => Date.now()
|
|
24717
|
+
};
|
|
24718
|
+
}
|
|
24719
|
+
function stagingApplyFsGuard(home) {
|
|
24720
|
+
const stagingRoot = pluginCacheStagingRoot(home);
|
|
24721
|
+
return {
|
|
24722
|
+
referencedPaths: () => readInstalledPluginRefs(home),
|
|
24723
|
+
mtimeMs: (name) => {
|
|
24724
|
+
const p = (0, import_node_path23.join)(stagingRoot, name);
|
|
24725
|
+
if (!(0, import_node_fs27.existsSync)(p)) return null;
|
|
24726
|
+
try {
|
|
24727
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs27.statSync)(q).mtimeMs);
|
|
24728
|
+
} catch {
|
|
24729
|
+
return null;
|
|
24730
|
+
}
|
|
24731
|
+
},
|
|
24732
|
+
now: () => Date.now()
|
|
24733
|
+
};
|
|
24734
|
+
}
|
|
24735
|
+
program2.command("plugin-prune").description(`prune stale cached MMI plugin versions (keeps running + newest, ${PLUGIN_CACHE_KEEP} total) and orphaned temp_git_* staging dirs; dry-run unless --apply (#2903, #2990)`).option("--apply", "actually delete the stale version dirs + orphaned staging dirs (default: report only)").option("--json", "machine-readable output").action((o) => {
|
|
24736
|
+
const plan = buildPluginCachePlan(
|
|
24737
|
+
(0, import_node_os8.homedir)(),
|
|
24738
|
+
runningPluginVersion(process.env, resolveClientVersion()),
|
|
24739
|
+
pluginCacheFsDeps((0, import_node_os8.homedir)(), directoryBytes),
|
|
24740
|
+
{ withBytes: true }
|
|
24741
|
+
);
|
|
24742
|
+
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
24743
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs27.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
|
|
24426
24744
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
24427
24745
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
24428
24746
|
else console.log(renderPluginCachePlan(plan, result));
|
|
24429
|
-
if (result?.failed.length) process.exitCode = 1;
|
|
24747
|
+
if (result?.failed.length || result?.failedStaging.length) process.exitCode = 1;
|
|
24430
24748
|
});
|
|
24431
24749
|
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
24432
24750
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
@@ -24455,7 +24773,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
24455
24773
|
readBoard,
|
|
24456
24774
|
// #1813: warm the slice cache out-of-band (detached) so the ~20s live read
|
|
24457
24775
|
// never costs banner time and next session's glance renders instantly within budget.
|
|
24458
|
-
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn:
|
|
24776
|
+
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
24459
24777
|
}),
|
|
24460
24778
|
// command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
|
|
24461
24779
|
// not after a denied gh call. <1KB, pure in-memory — no network, no fs, fail-soft.
|
|
@@ -24486,12 +24804,18 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
24486
24804
|
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
24487
24805
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
24488
24806
|
if (worktreeBanner) {
|
|
24489
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
24807
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
24490
24808
|
consoleIo.log(worktreeBanner);
|
|
24491
24809
|
}
|
|
24492
24810
|
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
|
|
24493
24811
|
});
|
|
24494
24812
|
installProcessBackstop();
|
|
24813
|
+
program2.command("lane", { hidden: true }).action(() => {
|
|
24814
|
+
fail("lane orchestration lives in jerv-cli \u2014 try: jerv-cli lane --help");
|
|
24815
|
+
});
|
|
24816
|
+
program2.command("fusion", { hidden: true }).action(() => {
|
|
24817
|
+
fail("fusion orchestration lives in jerv-cli \u2014 try: jerv-cli fusion --help");
|
|
24818
|
+
});
|
|
24495
24819
|
program2.parseAsync().then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
|
24496
24820
|
// Annotate the CommonJS export names for ESM import in node:
|
|
24497
24821
|
0 && (module.exports = {
|