@mutmutco/cli 2.68.0 → 3.0.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/README.md +5 -17
- package/dist/main.cjs +465 -378
- package/package.json +3 -3
package/dist/main.cjs
CHANGED
|
@@ -3407,42 +3407,104 @@ function useColor() {
|
|
|
3407
3407
|
var program = new Command();
|
|
3408
3408
|
|
|
3409
3409
|
// src/index.ts
|
|
3410
|
-
var
|
|
3410
|
+
var import_promises3 = require("node:fs/promises");
|
|
3411
3411
|
var import_node_fs18 = require("node:fs");
|
|
3412
3412
|
|
|
3413
|
-
// src/
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3413
|
+
// src/bootstrap-org-ruleset.ts
|
|
3414
|
+
var ORG_NO_AGENT_FILES_RULESET_NAME = "mmi-no-agent-files-org";
|
|
3415
|
+
var ORG_LOGIN = "mutmutco";
|
|
3416
|
+
var ORG_NO_AGENT_FILES_EXCLUDED_REPOS = ["Jerv-PowerTools"];
|
|
3417
|
+
var NO_AGENT_FILES_GLOBS = [
|
|
3418
|
+
"AGENTS.md",
|
|
3419
|
+
"**/AGENTS.md",
|
|
3420
|
+
"CLAUDE.md",
|
|
3421
|
+
"**/CLAUDE.md",
|
|
3422
|
+
"GEMINI.md",
|
|
3423
|
+
"**/GEMINI.md",
|
|
3424
|
+
".claude/**",
|
|
3425
|
+
"**/.claude/**",
|
|
3426
|
+
".cursor/rules/**",
|
|
3427
|
+
"**/.cursor/rules/**",
|
|
3428
|
+
".codex/**",
|
|
3429
|
+
"**/.codex/**",
|
|
3430
|
+
".agents/**",
|
|
3431
|
+
"**/.agents/**"
|
|
3432
|
+
];
|
|
3433
|
+
function orgNoAgentFilesRulesetDesired() {
|
|
3434
|
+
return {
|
|
3435
|
+
name: ORG_NO_AGENT_FILES_RULESET_NAME,
|
|
3436
|
+
target: "push",
|
|
3437
|
+
enforcement: "active",
|
|
3438
|
+
bypass_actors: [],
|
|
3439
|
+
conditions: { repository_name: { include: ["~ALL"], exclude: [...ORG_NO_AGENT_FILES_EXCLUDED_REPOS] } },
|
|
3440
|
+
rules: [{ type: "file_path_restriction", parameters: { restricted_file_paths: [...NO_AGENT_FILES_GLOBS] } }]
|
|
3441
|
+
};
|
|
3420
3442
|
}
|
|
3421
|
-
function
|
|
3422
|
-
|
|
3443
|
+
function sameSet(a, b) {
|
|
3444
|
+
const A = new Set(a ?? []);
|
|
3445
|
+
const B = new Set(b ?? []);
|
|
3446
|
+
if (A.size !== B.size) return false;
|
|
3447
|
+
for (const v of A) if (!B.has(v)) return false;
|
|
3448
|
+
return true;
|
|
3423
3449
|
}
|
|
3424
|
-
function
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3450
|
+
function diffOrgRuleset(live, desired = orgNoAgentFilesRulesetDesired()) {
|
|
3451
|
+
const drift = [];
|
|
3452
|
+
if (!live) return [`ruleset '${desired.name}' is absent`];
|
|
3453
|
+
if (live.name !== desired.name) drift.push(`name: '${live.name}' != '${desired.name}'`);
|
|
3454
|
+
if (live.target !== desired.target) drift.push(`target: '${live.target}' != '${desired.target}'`);
|
|
3455
|
+
if (live.enforcement !== desired.enforcement) drift.push(`enforcement: '${live.enforcement}' != '${desired.enforcement}'`);
|
|
3456
|
+
const bypass = Array.isArray(live.bypass_actors) ? live.bypass_actors : [];
|
|
3457
|
+
if (bypass.length !== 0) drift.push(`bypass_actors: ${bypass.length} actor(s) present \u2014 must be empty (no bypass)`);
|
|
3458
|
+
const include = live.conditions?.repository_name?.include;
|
|
3459
|
+
const exclude = live.conditions?.repository_name?.exclude;
|
|
3460
|
+
if (!sameSet(include, desired.conditions.repository_name.include)) {
|
|
3461
|
+
drift.push(`conditions.include: [${(include ?? []).join(", ")}] != [${desired.conditions.repository_name.include.join(", ")}]`);
|
|
3462
|
+
}
|
|
3463
|
+
if (!sameSet(exclude, desired.conditions.repository_name.exclude)) {
|
|
3464
|
+
drift.push(`conditions.exclude: [${(exclude ?? []).join(", ")}] != [${desired.conditions.repository_name.exclude.join(", ")}]`);
|
|
3465
|
+
}
|
|
3466
|
+
const rules2 = Array.isArray(live.rules) ? live.rules : [];
|
|
3467
|
+
const fpr = rules2.filter((r) => r?.type === "file_path_restriction");
|
|
3468
|
+
if (fpr.length !== 1) {
|
|
3469
|
+
drift.push(`rules: expected exactly 1 file_path_restriction, found ${fpr.length}`);
|
|
3470
|
+
} else if (!sameSet(fpr[0]?.parameters?.restricted_file_paths, desired.rules[0].parameters.restricted_file_paths)) {
|
|
3471
|
+
const got = fpr[0]?.parameters?.restricted_file_paths ?? [];
|
|
3472
|
+
drift.push(`restricted_file_paths: ${got.length} glob(s) differ from the codified ${desired.rules[0].parameters.restricted_file_paths.length}`);
|
|
3473
|
+
}
|
|
3474
|
+
return drift;
|
|
3475
|
+
}
|
|
3476
|
+
function planOrgNoAgentFilesRuleset(live) {
|
|
3477
|
+
const desired = orgNoAgentFilesRulesetDesired();
|
|
3478
|
+
if (!live) return { action: "create", drift: [`ruleset '${desired.name}' is absent`], desired };
|
|
3479
|
+
const drift = diffOrgRuleset(live, desired);
|
|
3480
|
+
return { action: drift.length === 0 ? "noop" : "update", drift, desired, id: live.id };
|
|
3481
|
+
}
|
|
3482
|
+
function renderOrgRulesetDriftReport(plan) {
|
|
3483
|
+
const head = `mmi-cli bootstrap org-ruleset: ${ORG_NO_AGENT_FILES_RULESET_NAME} (org ${ORG_LOGIN})`;
|
|
3484
|
+
if (plan.action === "noop") {
|
|
3485
|
+
return `${head}
|
|
3486
|
+
OK \u2014 live ruleset matches the codified IaC (${NO_AGENT_FILES_GLOBS.length} globs, include ~ALL, empty bypass, exclude ${ORG_NO_AGENT_FILES_EXCLUDED_REPOS.join(", ")}).`;
|
|
3487
|
+
}
|
|
3488
|
+
const verb = plan.action === "create" ? "MISSING \u2014 codified IaC would CREATE it" : "DRIFT \u2014 live ruleset diverges from the codified IaC";
|
|
3489
|
+
return [
|
|
3490
|
+
`${head}`,
|
|
3491
|
+
verb,
|
|
3492
|
+
...plan.drift.map((d) => ` - ${d}`),
|
|
3493
|
+
"This command is read-only; it never mutates the live ruleset. Recreate/repair via the GitHub UI or a",
|
|
3494
|
+
"deliberate POST/PUT of orgNoAgentFilesRulesetDesired() \u2014 see cli/src/bootstrap-org-ruleset.ts."
|
|
3495
|
+
].join("\n");
|
|
3435
3496
|
}
|
|
3436
|
-
function
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3497
|
+
async function fetchOrgNoAgentFilesRuleset(client, org = ORG_LOGIN) {
|
|
3498
|
+
const list = await client.rest("GET", `orgs/${org}/rulesets`, { timeoutMs: 2e4 });
|
|
3499
|
+
const summary = (list ?? []).find((r) => r?.name === ORG_NO_AGENT_FILES_RULESET_NAME);
|
|
3500
|
+
if (summary?.id == null) return null;
|
|
3501
|
+
return await client.rest("GET", `orgs/${org}/rulesets/${summary.id}`, { timeoutMs: 2e4 }) ?? null;
|
|
3439
3502
|
}
|
|
3440
3503
|
|
|
3441
3504
|
// src/index.ts
|
|
3442
3505
|
var import_node_child_process11 = require("node:child_process");
|
|
3443
3506
|
|
|
3444
3507
|
// src/cli-shared.ts
|
|
3445
|
-
var import_promises = require("node:fs/promises");
|
|
3446
3508
|
var import_node_child_process3 = require("node:child_process");
|
|
3447
3509
|
var import_node_util4 = require("node:util");
|
|
3448
3510
|
|
|
@@ -3887,16 +3949,8 @@ async function hubHeaders(extra = {}) {
|
|
|
3887
3949
|
const base = { ...clientVersionHeaders(), ...extra };
|
|
3888
3950
|
return t ? { ...base, Authorization: `Bearer ${t}` } : base;
|
|
3889
3951
|
}
|
|
3890
|
-
var CONFIG_FILE = ".mmi/config.json";
|
|
3891
3952
|
async function loadConfig() {
|
|
3892
|
-
|
|
3893
|
-
try {
|
|
3894
|
-
file = JSON.parse(await (0, import_promises.readFile)(CONFIG_FILE, "utf8"));
|
|
3895
|
-
} catch {
|
|
3896
|
-
file = {};
|
|
3897
|
-
}
|
|
3898
|
-
if (!file.sagaApiUrl) file.sagaApiUrl = defaultHubUrl();
|
|
3899
|
-
return file;
|
|
3953
|
+
return { sagaApiUrl: defaultHubUrl() };
|
|
3900
3954
|
}
|
|
3901
3955
|
async function originRemoteUrl() {
|
|
3902
3956
|
return gitOut(["config", "--get", "remote.origin.url"]);
|
|
@@ -3922,29 +3976,8 @@ function fail(msg) {
|
|
|
3922
3976
|
hardExit(1);
|
|
3923
3977
|
}
|
|
3924
3978
|
function installProcessBackstop() {
|
|
3925
|
-
process.on("unhandledRejection", (reason) =>
|
|
3926
|
-
process.on("uncaughtException", (err) =>
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
// src/docs-sync.ts
|
|
3930
|
-
var SYNCED_DOCS = ["README.md", "architecture.md"];
|
|
3931
|
-
async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
3932
|
-
const updated = [];
|
|
3933
|
-
const skippedDirty = [];
|
|
3934
|
-
for (const file of docs2) {
|
|
3935
|
-
if (await deps.isDirty(file)) {
|
|
3936
|
-
skippedDirty.push(file);
|
|
3937
|
-
continue;
|
|
3938
|
-
}
|
|
3939
|
-
const origin = await deps.originContent(file);
|
|
3940
|
-
if (origin === null) continue;
|
|
3941
|
-
const local = await deps.localContent(file);
|
|
3942
|
-
if (needsUpdate(origin, local)) {
|
|
3943
|
-
await deps.writeDoc(file, normalizeEol(origin));
|
|
3944
|
-
updated.push(file);
|
|
3945
|
-
}
|
|
3946
|
-
}
|
|
3947
|
-
return { updated, skippedDirty };
|
|
3979
|
+
process.on("unhandledRejection", (reason) => void failGraceful(reason instanceof Error ? reason.message : String(reason)));
|
|
3980
|
+
process.on("uncaughtException", (err) => void failGraceful(err instanceof Error ? err.message : String(err)));
|
|
3948
3981
|
}
|
|
3949
3982
|
|
|
3950
3983
|
// src/session-start.ts
|
|
@@ -3979,8 +4012,12 @@ function hashContent(s) {
|
|
|
3979
4012
|
return (h >>> 0).toString(16);
|
|
3980
4013
|
}
|
|
3981
4014
|
|
|
4015
|
+
// src/rules-sync.ts
|
|
4016
|
+
function normalizeEol(s) {
|
|
4017
|
+
return s.replace(/\r\n/g, "\n");
|
|
4018
|
+
}
|
|
4019
|
+
|
|
3982
4020
|
// src/scratch-gc.ts
|
|
3983
|
-
var HEAD_TS_STALE_MS = 48 * 36e5;
|
|
3984
4021
|
var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
|
|
3985
4022
|
var ROOT_SCRATCH_STALE_MS = 24 * 36e5;
|
|
3986
4023
|
var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
|
|
@@ -4002,16 +4039,16 @@ function markScratchGcRun(stampPath, now = Date.now()) {
|
|
|
4002
4039
|
}
|
|
4003
4040
|
}
|
|
4004
4041
|
function executeScratchGc(repoRoot, opts, now = Date.now()) {
|
|
4005
|
-
const snap = collectScratchSnapshot(repoRoot);
|
|
4042
|
+
const snap = collectScratchSnapshot(repoRoot, { project: opts.project });
|
|
4006
4043
|
const plan = planScratchGc(snap, now);
|
|
4007
4044
|
if (!opts.apply) return { plan };
|
|
4008
|
-
return { plan, applied: applyScratchGc(plan,
|
|
4045
|
+
return { plan, applied: applyScratchGc(plan, repoRoot, now, opts.project) };
|
|
4009
4046
|
}
|
|
4010
|
-
function buildPrMergeScratchHousekeeping(repoRoot, deps = {}) {
|
|
4047
|
+
function buildPrMergeScratchHousekeeping(repoRoot, deps = {}, project2) {
|
|
4011
4048
|
const runScratchGc = deps.runScratchGc ?? executeScratchGc;
|
|
4012
4049
|
try {
|
|
4013
|
-
const scratchApply = runScratchGc(repoRoot, { apply: true });
|
|
4014
|
-
const scratchAfter = runScratchGc(repoRoot, { apply: false });
|
|
4050
|
+
const scratchApply = runScratchGc(repoRoot, { apply: true, project: project2 });
|
|
4051
|
+
const scratchAfter = runScratchGc(repoRoot, { apply: false, project: project2 });
|
|
4015
4052
|
const applied = scratchApply.applied;
|
|
4016
4053
|
const pruned = applied?.pruned.length ?? 0;
|
|
4017
4054
|
const skipped = applied?.skipped ?? 0;
|
|
@@ -4043,13 +4080,10 @@ function buildPrMergeScratchHousekeeping(repoRoot, deps = {}) {
|
|
|
4043
4080
|
}
|
|
4044
4081
|
var ROOT_SCRATCH_DIRS = /* @__PURE__ */ new Set(["tmp", ".playwright-mcp", "test-results", "playwright-report"]);
|
|
4045
4082
|
var ROOT_SCRATCH_FILE_PREFIXES = ["tmp_"];
|
|
4046
|
-
var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json"]);
|
|
4047
4083
|
function planScratchGc(snap, now = Date.now()) {
|
|
4048
4084
|
const candidates = [];
|
|
4049
4085
|
const normalizePath = (p) => p.replace(/\\/g, "/");
|
|
4050
|
-
const repoRoot = normalizePath(
|
|
4051
|
-
const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
|
|
4052
|
-
const days = (ms) => `${Math.floor(ms / 864e5)}d`;
|
|
4086
|
+
const repoRoot = normalizePath(snap.repoRoot.replace(/[\\/]+$/, ""));
|
|
4053
4087
|
for (const f of snap.rootScratchFiles ?? []) {
|
|
4054
4088
|
const dir = normalizePath(f.dir).replace(/\/+$/, "");
|
|
4055
4089
|
const age = now - f.mtimeMs;
|
|
@@ -4078,15 +4112,6 @@ function planScratchGc(snap, now = Date.now()) {
|
|
|
4078
4112
|
});
|
|
4079
4113
|
}
|
|
4080
4114
|
}
|
|
4081
|
-
for (const f of snap.mmiFiles) {
|
|
4082
|
-
const age = now - f.mtimeMs;
|
|
4083
|
-
const add = (family, reason) => candidates.push({ path: f.path, family, kind: "file", tier: "safe-auto", reason, bytes: f.bytes });
|
|
4084
|
-
if (NEVER_BASENAMES.has(f.name)) continue;
|
|
4085
|
-
if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
|
|
4086
|
-
if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
|
|
4087
|
-
continue;
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
4115
|
if (snap.syncQueueSlugs === null) {
|
|
4091
4116
|
for (const f of snap.planMdFiles) {
|
|
4092
4117
|
if (now - f.mtimeMs <= PLAN_ADVISORY_AGE_MS) continue;
|
|
@@ -4151,14 +4176,6 @@ function syncedPlanMetaEntry(meta, project2, slug, hash) {
|
|
|
4151
4176
|
const entry = meta[metaKey(project2, slug)];
|
|
4152
4177
|
return Boolean(entry?.hash === hash && entry.syncedAt);
|
|
4153
4178
|
}
|
|
4154
|
-
function readProject(repoRoot) {
|
|
4155
|
-
try {
|
|
4156
|
-
const cfg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(repoRoot, ".mmi", "config.json"), "utf8"));
|
|
4157
|
-
if (typeof cfg.project === "string" && cfg.project.trim()) return cfg.project.trim();
|
|
4158
|
-
} catch {
|
|
4159
|
-
}
|
|
4160
|
-
return void 0;
|
|
4161
|
-
}
|
|
4162
4179
|
function readPlanMeta(plansRoot) {
|
|
4163
4180
|
try {
|
|
4164
4181
|
return parseMeta((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(plansRoot, ".plan-meta.json"), "utf8"));
|
|
@@ -4196,7 +4213,7 @@ function physicalPlanCandidateStillAllowed(candidatePath, repoAnchor, lstat = im
|
|
|
4196
4213
|
}
|
|
4197
4214
|
}
|
|
4198
4215
|
function formatScratchGcPlan(plan, applied, applyResult) {
|
|
4199
|
-
if (plan.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local
|
|
4216
|
+
if (plan.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local scratch is clean.";
|
|
4200
4217
|
const lines = [];
|
|
4201
4218
|
const bytes = applyResult?.bytes ?? plan.safeAuto.reduce((n, c) => n + c.bytes, 0);
|
|
4202
4219
|
const safeCount = applyResult ? applyResult.pruned.length : plan.safeAuto.length;
|
|
@@ -4240,34 +4257,22 @@ function treeOlderThan(root, now, floor) {
|
|
|
4240
4257
|
}
|
|
4241
4258
|
return true;
|
|
4242
4259
|
}
|
|
4243
|
-
function applyScratchGc(plan,
|
|
4260
|
+
function applyScratchGc(plan, repoRoot, now = Date.now(), project2) {
|
|
4244
4261
|
const result = { pruned: [], skipped: 0, bytes: 0 };
|
|
4245
4262
|
let repoAnchor;
|
|
4246
|
-
let anchor;
|
|
4247
4263
|
try {
|
|
4248
|
-
repoAnchor = (0, import_node_fs5.realpathSync)(
|
|
4264
|
+
repoAnchor = (0, import_node_fs5.realpathSync)(repoRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4249
4265
|
} catch {
|
|
4250
4266
|
return result;
|
|
4251
4267
|
}
|
|
4252
|
-
try {
|
|
4253
|
-
anchor = (0, import_node_fs5.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4254
|
-
} catch {
|
|
4255
|
-
anchor = void 0;
|
|
4256
|
-
}
|
|
4257
4268
|
for (const c of plan.safeAuto) {
|
|
4258
4269
|
try {
|
|
4259
4270
|
const real = (0, import_node_fs5.realpathSync)(c.path).replace(/\\/g, "/");
|
|
4260
|
-
|
|
4261
|
-
if (!rootScoped && !anchor) {
|
|
4262
|
-
result.skipped += 1;
|
|
4263
|
-
continue;
|
|
4264
|
-
}
|
|
4265
|
-
const containment = rootScoped ? repoAnchor : anchor;
|
|
4266
|
-
if (!containment || !pathContained(real, containment) || rootScoped && !rootCandidateStillAllowed(c, repoAnchor)) {
|
|
4271
|
+
if (!pathContained(real, repoAnchor) || !rootCandidateStillAllowed(c, repoAnchor)) {
|
|
4267
4272
|
result.skipped += 1;
|
|
4268
4273
|
continue;
|
|
4269
4274
|
}
|
|
4270
|
-
if (
|
|
4275
|
+
if (trackedPathStatus(c, repoAnchor) !== false) {
|
|
4271
4276
|
result.skipped += 1;
|
|
4272
4277
|
continue;
|
|
4273
4278
|
}
|
|
@@ -4298,14 +4303,13 @@ function applyScratchGc(plan, mmiRoot, now = Date.now()) {
|
|
|
4298
4303
|
result.skipped += 1;
|
|
4299
4304
|
continue;
|
|
4300
4305
|
}
|
|
4301
|
-
const project2 = readProject(repoAnchor);
|
|
4302
4306
|
const meta = readPlanMeta(plansRoot);
|
|
4303
4307
|
if (!meta || !syncedPlanMetaEntry(meta, project2, slug, currentHash)) {
|
|
4304
4308
|
result.skipped += 1;
|
|
4305
4309
|
continue;
|
|
4306
4310
|
}
|
|
4307
4311
|
}
|
|
4308
|
-
const floor = c.family === "
|
|
4312
|
+
const floor = c.family === "plan" ? PLAN_ADVISORY_AGE_MS : c.family === "scratch-dir" || c.family === "scratch-file" ? ROOT_SCRATCH_STALE_MS : 0;
|
|
4309
4313
|
if (floor > 0 && now - st.mtimeMs <= floor) {
|
|
4310
4314
|
result.skipped += 1;
|
|
4311
4315
|
continue;
|
|
@@ -4389,8 +4393,7 @@ function rootScratchDirSnapshot(root, readdir, stat) {
|
|
|
4389
4393
|
function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
4390
4394
|
const readdir = deps.readdir ?? import_node_fs5.readdirSync;
|
|
4391
4395
|
const stat = deps.stat ?? import_node_fs5.statSync;
|
|
4392
|
-
const
|
|
4393
|
-
const mmiRoot = (0, import_node_path5.join)(repoRoot, ".mmi");
|
|
4396
|
+
const readFile2 = deps.readFile ?? import_node_fs5.readFileSync;
|
|
4394
4397
|
const plansRoot = (0, import_node_path5.join)(repoRoot, "plans");
|
|
4395
4398
|
const rootScratchFiles = [];
|
|
4396
4399
|
try {
|
|
@@ -4414,20 +4417,6 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4414
4417
|
}
|
|
4415
4418
|
} catch {
|
|
4416
4419
|
}
|
|
4417
|
-
const mmiFiles = [];
|
|
4418
|
-
try {
|
|
4419
|
-
for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
|
|
4420
|
-
if (!ent.isFile()) continue;
|
|
4421
|
-
const dir = ent.parentPath ?? ent.path ?? mmiRoot;
|
|
4422
|
-
const full = (0, import_node_path5.join)(dir, ent.name);
|
|
4423
|
-
try {
|
|
4424
|
-
const st = stat(full);
|
|
4425
|
-
mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
4426
|
-
} catch {
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
} catch {
|
|
4430
|
-
}
|
|
4431
4420
|
const planMdFiles = [];
|
|
4432
4421
|
try {
|
|
4433
4422
|
for (const ent of readdir(plansRoot, { withFileTypes: true })) {
|
|
@@ -4435,7 +4424,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4435
4424
|
const full = (0, import_node_path5.join)(plansRoot, ent.name);
|
|
4436
4425
|
try {
|
|
4437
4426
|
const st = stat(full);
|
|
4438
|
-
const raw =
|
|
4427
|
+
const raw = readFile2(full, "utf8");
|
|
4439
4428
|
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size, kind: "file", hash: hashContent(normalizeEol(raw)) });
|
|
4440
4429
|
} catch {
|
|
4441
4430
|
}
|
|
@@ -4444,20 +4433,15 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4444
4433
|
}
|
|
4445
4434
|
let planMeta = {};
|
|
4446
4435
|
try {
|
|
4447
|
-
planMeta = parseMeta(
|
|
4436
|
+
planMeta = parseMeta(readFile2((0, import_node_path5.join)(plansRoot, ".plan-meta.json"), "utf8"));
|
|
4448
4437
|
} catch {
|
|
4449
4438
|
planMeta = {};
|
|
4450
4439
|
}
|
|
4451
|
-
|
|
4452
|
-
try {
|
|
4453
|
-
const cfg = JSON.parse(readFile3((0, import_node_path5.join)(repoRoot, ".mmi", "config.json"), "utf8"));
|
|
4454
|
-
if (typeof cfg.project === "string" && cfg.project.trim()) project2 = cfg.project.trim();
|
|
4455
|
-
} catch {
|
|
4456
|
-
}
|
|
4440
|
+
const project2 = deps.project?.trim() || void 0;
|
|
4457
4441
|
const syncQueueSlugs = (() => {
|
|
4458
4442
|
let queueRaw;
|
|
4459
4443
|
try {
|
|
4460
|
-
queueRaw =
|
|
4444
|
+
queueRaw = readFile2((0, import_node_path5.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
4461
4445
|
} catch (e) {
|
|
4462
4446
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
4463
4447
|
return code === "ENOENT" || code === "ENOTDIR" ? /* @__PURE__ */ new Set() : null;
|
|
@@ -4470,7 +4454,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4470
4454
|
return null;
|
|
4471
4455
|
}
|
|
4472
4456
|
})();
|
|
4473
|
-
return { repoRoot,
|
|
4457
|
+
return { repoRoot, rootScratchFiles, planMdFiles, planMeta, project: project2, syncQueueSlugs };
|
|
4474
4458
|
}
|
|
4475
4459
|
|
|
4476
4460
|
// src/repo-runtime-state.ts
|
|
@@ -5062,7 +5046,6 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
5062
5046
|
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
5063
5047
|
const item = lookup.item;
|
|
5064
5048
|
if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
5065
|
-
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
5066
5049
|
const optionId = cfg.statusOptions[options.status];
|
|
5067
5050
|
try {
|
|
5068
5051
|
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
|
|
@@ -5252,6 +5235,36 @@ async function setBoardItemPriority(client, cfg, itemId, priority) {
|
|
|
5252
5235
|
await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
|
|
5253
5236
|
return cliPriorityToFieldName(priority);
|
|
5254
5237
|
}
|
|
5238
|
+
var defaultRetrySleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
5239
|
+
async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
|
|
5240
|
+
const attempts = Math.max(1, opts.attempts ?? 5);
|
|
5241
|
+
const delayMs = opts.delayMs ?? 300;
|
|
5242
|
+
const sleep = opts.sleep ?? defaultRetrySleep;
|
|
5243
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
5244
|
+
const itemId = (await fetchIssueProjectItem(client, cfg, selector)).item?.itemId;
|
|
5245
|
+
if (itemId) return itemId;
|
|
5246
|
+
if (attempt < attempts - 1) await sleep(delayMs * (attempt + 1));
|
|
5247
|
+
}
|
|
5248
|
+
return void 0;
|
|
5249
|
+
}
|
|
5250
|
+
async function setBoardPriorityForSelector(options, deps = {}) {
|
|
5251
|
+
const cfg = resolveBoardConfig(options.config);
|
|
5252
|
+
if (!isPriorityFieldConfigured(cfg)) {
|
|
5253
|
+
throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
|
|
5254
|
+
}
|
|
5255
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
5256
|
+
const itemId = await resolveProjectItemIdWithRetry(client, cfg, options.selector, options.retry);
|
|
5257
|
+
if (!itemId) {
|
|
5258
|
+
throw new Error(
|
|
5259
|
+
`issue #${options.selector.number} is not on the ${options.selector.repo} board yet (no Project v2 item) \u2014 add it to the board first, then retry`
|
|
5260
|
+
);
|
|
5261
|
+
}
|
|
5262
|
+
const priority = await setBoardItemPriority(client, cfg, itemId, options.priority);
|
|
5263
|
+
if (!priority) {
|
|
5264
|
+
throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
|
|
5265
|
+
}
|
|
5266
|
+
return { ref: `${options.selector.repo}#${options.selector.number}`, itemId, priority };
|
|
5267
|
+
}
|
|
5255
5268
|
async function backfillBoardPriorities(options, deps = {}) {
|
|
5256
5269
|
const cfg = resolveBoardConfig(options.config);
|
|
5257
5270
|
if (!isPriorityFieldConfigured(cfg)) {
|
|
@@ -5872,6 +5885,8 @@ function parseGhPrChecksResult(stdout, stderr) {
|
|
|
5872
5885
|
}
|
|
5873
5886
|
var PR_CHECKS_POLL_MS = 3e4;
|
|
5874
5887
|
var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
|
|
5888
|
+
var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
|
|
5889
|
+
var PR_CHECKS_CONFIRM_MS = 5e3;
|
|
5875
5890
|
async function waitForPrChecks(deps) {
|
|
5876
5891
|
const { policy, reason } = await deps.resolvePolicy();
|
|
5877
5892
|
if (policy === "no-ci") {
|
|
@@ -5880,10 +5895,20 @@ async function waitForPrChecks(deps) {
|
|
|
5880
5895
|
const now = deps.now ?? (() => Date.now());
|
|
5881
5896
|
const deadline = now() + PR_CHECKS_TIMEOUT_MS;
|
|
5882
5897
|
let lastDetail = "pending";
|
|
5898
|
+
let successStreak = 0;
|
|
5883
5899
|
while (now() < deadline) {
|
|
5884
5900
|
const state = await deps.pollChecks();
|
|
5885
|
-
if (state === "success") return { policy, status: "success", detail: "checks-success" };
|
|
5886
5901
|
if (state === "failure") return { policy, status: "failure", detail: lastDetail };
|
|
5902
|
+
if (state === "success") {
|
|
5903
|
+
successStreak += 1;
|
|
5904
|
+
if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
|
|
5905
|
+
return { policy, status: "success", detail: "checks-success" };
|
|
5906
|
+
}
|
|
5907
|
+
lastDetail = "confirming-success";
|
|
5908
|
+
await deps.sleep(PR_CHECKS_CONFIRM_MS);
|
|
5909
|
+
continue;
|
|
5910
|
+
}
|
|
5911
|
+
successStreak = 0;
|
|
5887
5912
|
if (state === "no-checks-reported") {
|
|
5888
5913
|
lastDetail = "no-checks-reported (waiting for workflow to queue)";
|
|
5889
5914
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
@@ -6065,6 +6090,10 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
6065
6090
|
"docs/superpowers/",
|
|
6066
6091
|
".playwright-mcp/",
|
|
6067
6092
|
".claude/worktrees/",
|
|
6093
|
+
// #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<branch>` (outside the tree,
|
|
6094
|
+
// via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
|
|
6095
|
+
// doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
|
|
6096
|
+
// into every repo's .gitignore.
|
|
6068
6097
|
".aws-sam/",
|
|
6069
6098
|
"/*.png",
|
|
6070
6099
|
// Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
|
|
@@ -6329,10 +6358,10 @@ function labelsToPrune(orgLabelNames) {
|
|
|
6329
6358
|
const org = new Set(orgLabelNames);
|
|
6330
6359
|
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
6331
6360
|
}
|
|
6332
|
-
function resolveSeedContent(seed, vars,
|
|
6333
|
-
if (seed.source === "self") return
|
|
6361
|
+
function resolveSeedContent(seed, vars, readFile2) {
|
|
6362
|
+
if (seed.source === "self") return readFile2(seed.target);
|
|
6334
6363
|
if (seed.source.startsWith("seed:")) {
|
|
6335
|
-
const tmpl =
|
|
6364
|
+
const tmpl = readFile2(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
6336
6365
|
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
6337
6366
|
}
|
|
6338
6367
|
return null;
|
|
@@ -6496,6 +6525,23 @@ function decideFanoutPrAction(openPrs) {
|
|
|
6496
6525
|
}
|
|
6497
6526
|
return { action: "create" };
|
|
6498
6527
|
}
|
|
6528
|
+
function decideSeedDelivery(branchRules) {
|
|
6529
|
+
const rules2 = Array.isArray(branchRules) ? branchRules : [];
|
|
6530
|
+
const types = new Set(
|
|
6531
|
+
rules2.map((r) => r?.type).filter((t) => typeof t === "string")
|
|
6532
|
+
);
|
|
6533
|
+
const gating = ["pull_request", "required_status_checks"].filter((t) => types.has(t));
|
|
6534
|
+
if (gating.length) return { mode: "pr", reason: `base branch is protected (${gating.join(", ")}) \u2014 seed via a branch + PR` };
|
|
6535
|
+
return { mode: "direct", reason: "base branch is unprotected \u2014 direct PUT is safe" };
|
|
6536
|
+
}
|
|
6537
|
+
function planSeedDelivery(branchRules, slug, baseBranch) {
|
|
6538
|
+
const decision = decideSeedDelivery(branchRules);
|
|
6539
|
+
if (decision.mode === "pr") {
|
|
6540
|
+
const branch = `bootstrap-seed-${slug}`;
|
|
6541
|
+
return { mode: "pr", ref: branch, branch, reason: decision.reason };
|
|
6542
|
+
}
|
|
6543
|
+
return { mode: "direct", ref: baseBranch, reason: decision.reason };
|
|
6544
|
+
}
|
|
6499
6545
|
function contentPutArgs(repo, path2, content, branch, sha) {
|
|
6500
6546
|
const args = [
|
|
6501
6547
|
"api",
|
|
@@ -8599,7 +8645,7 @@ async function collectWaveStatus(deps) {
|
|
|
8599
8645
|
} catch {
|
|
8600
8646
|
dirty = true;
|
|
8601
8647
|
}
|
|
8602
|
-
const headText = await deps.
|
|
8648
|
+
const headText = await deps.fetchHeadText?.(wt.branch).catch(() => void 0);
|
|
8603
8649
|
rows.push({
|
|
8604
8650
|
path: wt.path,
|
|
8605
8651
|
branch: wt.branch,
|
|
@@ -8641,12 +8687,11 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
|
|
|
8641
8687
|
}
|
|
8642
8688
|
|
|
8643
8689
|
// src/attach-to-project.ts
|
|
8644
|
-
async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m)) {
|
|
8690
|
+
async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
|
|
8645
8691
|
let projectItemId;
|
|
8646
8692
|
try {
|
|
8647
8693
|
const boardCfg = resolveBoardConfig(cfg);
|
|
8648
|
-
|
|
8649
|
-
projectItemId = lookup.item?.itemId;
|
|
8694
|
+
projectItemId = await resolveProjectItemIdWithRetry(client, boardCfg, selector, retry);
|
|
8650
8695
|
} catch (e) {
|
|
8651
8696
|
warn(`warning: issue #${selector.number} board item lookup failed after auto-add: ${e.message}
|
|
8652
8697
|
`);
|
|
@@ -8671,7 +8716,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
8671
8716
|
}
|
|
8672
8717
|
|
|
8673
8718
|
// src/gh-create.ts
|
|
8674
|
-
var
|
|
8719
|
+
var import_promises = require("node:fs/promises");
|
|
8675
8720
|
var import_node_os3 = require("node:os");
|
|
8676
8721
|
var import_node_path12 = require("node:path");
|
|
8677
8722
|
var import_node_crypto3 = require("node:crypto");
|
|
@@ -8712,9 +8757,13 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
8712
8757
|
const i = args.indexOf("--body");
|
|
8713
8758
|
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
8714
8759
|
} };
|
|
8715
|
-
const write = deps.write ??
|
|
8716
|
-
const remove = deps.remove ??
|
|
8717
|
-
const
|
|
8760
|
+
const write = deps.write ?? import_promises.writeFile;
|
|
8761
|
+
const remove = deps.remove ?? import_promises.unlink;
|
|
8762
|
+
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
8763
|
+
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
8764
|
+
const file = (0, import_node_path12.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
8765
|
+
await ensureDir((0, import_node_path12.dirname)(file), { recursive: true }).catch(() => {
|
|
8766
|
+
});
|
|
8718
8767
|
await write(file, args[i + 1], "utf8");
|
|
8719
8768
|
return {
|
|
8720
8769
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -9133,6 +9182,9 @@ function parseNpmVersion(stdout) {
|
|
|
9133
9182
|
const v = stdout.trim();
|
|
9134
9183
|
return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v) ? v : void 0;
|
|
9135
9184
|
}
|
|
9185
|
+
function staleTrainCliMessage(report, commandName) {
|
|
9186
|
+
return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path`;
|
|
9187
|
+
}
|
|
9136
9188
|
function versionAutoUpdateAction(report, hasPluginRoot) {
|
|
9137
9189
|
if (report.ok || report.staleAgainst !== "released") return "none";
|
|
9138
9190
|
return hasPluginRoot ? "plugin-pull" : "npm";
|
|
@@ -9296,14 +9348,6 @@ function trainPlanTargetsFromTags(tags, explicitRaw) {
|
|
|
9296
9348
|
}
|
|
9297
9349
|
return targets;
|
|
9298
9350
|
}
|
|
9299
|
-
function stagePlan(stage2 = {}, stops = true) {
|
|
9300
|
-
return [
|
|
9301
|
-
...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
|
|
9302
|
-
{ label: "run local build", command: stage2.build || "(no stage.build configured)" },
|
|
9303
|
-
{ label: "start local stage", command: stage2.up || "(no stage.up configured)" },
|
|
9304
|
-
{ label: "check health", command: stage2.healthUrl ? `curl --fail ${stage2.healthUrl}` : "(no stage.healthUrl configured)" }
|
|
9305
|
-
];
|
|
9306
|
-
}
|
|
9307
9351
|
function derivedStagePlan(derived, shell2, stops = true) {
|
|
9308
9352
|
const { port } = derived;
|
|
9309
9353
|
const envOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
|
|
@@ -9463,55 +9507,8 @@ function deriveStage(inputs) {
|
|
|
9463
9507
|
function stageUrlForPort(port) {
|
|
9464
9508
|
return `http://127.0.0.1:${port}/`;
|
|
9465
9509
|
}
|
|
9466
|
-
var POSIX_ONLY = [
|
|
9467
|
-
/(^|[^&|])&&([^&]|$)/,
|
|
9468
|
-
// && chaining (cmd.exe uses it too, but PowerShell <7 / the typical agent shell does not)
|
|
9469
|
-
/\|\|/,
|
|
9470
|
-
// || chaining
|
|
9471
|
-
// `VAR=value cmd` LEADING env prefix only — anchored to a command boundary (start-of-string or after a
|
|
9472
|
-
// ; & | separator) so it never fires on a flag value mid-command (`-e DEBUG=true app`, `--env X=y svc`),
|
|
9473
|
-
// which are valid cross-shell. The value excludes separators so it can't span past the command boundary.
|
|
9474
|
-
/(?:^|[;&|])\s*[A-Za-z_][A-Za-z0-9_]*=[^\s;&|]+\s+\S/,
|
|
9475
|
-
/(^|[\s;&|])(cp|rm|mv|ln|cat|touch|export)\s/
|
|
9476
|
-
// bare POSIX file/env builtins
|
|
9477
|
-
];
|
|
9478
|
-
function looksPosixOnly(command) {
|
|
9479
|
-
if (!command?.trim()) return false;
|
|
9480
|
-
return POSIX_ONLY.some((re) => re.test(command));
|
|
9481
|
-
}
|
|
9482
|
-
function stalePosixFields(config, shell2) {
|
|
9483
|
-
if (shell2 !== "powershell") return [];
|
|
9484
|
-
const fields = [];
|
|
9485
|
-
if (looksPosixOnly(config?.build)) fields.push("build");
|
|
9486
|
-
if (looksPosixOnly(config?.up)) fields.push("up");
|
|
9487
|
-
return fields;
|
|
9488
|
-
}
|
|
9489
|
-
function sanitizeLocalStage(local, stale) {
|
|
9490
|
-
if (!stale.length) return local;
|
|
9491
|
-
const clean3 = { ...local };
|
|
9492
|
-
for (const field of stale) delete clean3[field];
|
|
9493
|
-
return clean3;
|
|
9494
|
-
}
|
|
9495
|
-
function staleNote(staleFields, outcome) {
|
|
9496
|
-
const list = staleFields.join(", ");
|
|
9497
|
-
const plural = staleFields.length > 1 ? "fields" : "field";
|
|
9498
|
-
return `local .mmi stage ${plural} ${list} ${staleFields.length > 1 ? "are" : "is"} POSIX-only and unusable on PowerShell \u2014 ${outcome}`;
|
|
9499
|
-
}
|
|
9500
9510
|
function decideStage(inputs) {
|
|
9501
|
-
const {
|
|
9502
|
-
const staleFields = stalePosixFields(local, shell2);
|
|
9503
|
-
const stale = staleFields.length > 0;
|
|
9504
|
-
const upStale = staleFields.includes("up");
|
|
9505
|
-
if (local?.up?.trim() && !upStale) {
|
|
9506
|
-
if (!stale) return { source: "local", config: local };
|
|
9507
|
-
return {
|
|
9508
|
-
source: "local",
|
|
9509
|
-
config: sanitizeLocalStage(local, staleFields),
|
|
9510
|
-
staleIgnored: true,
|
|
9511
|
-
staleFields,
|
|
9512
|
-
gap: staleNote(staleFields, `kept the cross-shell parts of the local recipe and ignored ${staleFields.join(", ")}`)
|
|
9513
|
-
};
|
|
9514
|
-
}
|
|
9511
|
+
const { registry: registry2, hasCompose, hasEnvExample } = inputs;
|
|
9515
9512
|
const deriveInputs = {
|
|
9516
9513
|
portRange: registry2.portRange,
|
|
9517
9514
|
deployModel: registry2.deployModel,
|
|
@@ -9519,19 +9516,10 @@ function decideStage(inputs) {
|
|
|
9519
9516
|
hasEnvExample
|
|
9520
9517
|
};
|
|
9521
9518
|
const derived = deriveStage(deriveInputs);
|
|
9522
|
-
if (derived) {
|
|
9523
|
-
return {
|
|
9524
|
-
source: "derived",
|
|
9525
|
-
derived,
|
|
9526
|
-
registryError: registry2.error,
|
|
9527
|
-
staleIgnored: stale || void 0,
|
|
9528
|
-
staleFields: stale ? staleFields : void 0
|
|
9529
|
-
};
|
|
9530
|
-
}
|
|
9519
|
+
if (derived) return { source: "derived", derived, registryError: registry2.error };
|
|
9531
9520
|
const registryGap = registry2.error ? `Hub registry read failed (${registry2.error}) \u2014 cannot derive a default local stage` : null;
|
|
9532
|
-
const
|
|
9533
|
-
|
|
9534
|
-
return { source: "none", gap, staleIgnored: stale || void 0, staleFields: stale ? staleFields : void 0, registryError: registry2.error };
|
|
9521
|
+
const gap = registryGap ?? deriveStageGap(deriveInputs) ?? "no registry-derived default available";
|
|
9522
|
+
return { source: "none", gap, registryError: registry2.error };
|
|
9535
9523
|
}
|
|
9536
9524
|
|
|
9537
9525
|
// src/stage-live.ts
|
|
@@ -9661,6 +9649,14 @@ function requireValue(value, label) {
|
|
|
9661
9649
|
if (!value) throw new Error(`${label} could not be resolved`);
|
|
9662
9650
|
return value;
|
|
9663
9651
|
}
|
|
9652
|
+
function planTrainApplyRepoGuard(applyRepo, cwdRepo, rerun) {
|
|
9653
|
+
if (cwdRepo && cwdRepo.toLowerCase() === applyRepo.toLowerCase()) return { ok: true };
|
|
9654
|
+
const where = cwdRepo ? `this checkout is ${cwdRepo}` : "this checkout could not be identified";
|
|
9655
|
+
return {
|
|
9656
|
+
ok: false,
|
|
9657
|
+
message: `--apply runs from the current checkout, not --repo (${where}). cd into the ${applyRepo} checkout and rerun without --repo: ${rerun}`
|
|
9658
|
+
};
|
|
9659
|
+
}
|
|
9664
9660
|
async function resolveFoldPaths(deps, model) {
|
|
9665
9661
|
if (model === "hub-serverless") {
|
|
9666
9662
|
return (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
@@ -9672,17 +9668,28 @@ async function resolveFoldPaths(deps, model) {
|
|
|
9672
9668
|
}
|
|
9673
9669
|
return ["package.json", "package-lock.json"];
|
|
9674
9670
|
}
|
|
9671
|
+
async function installAppFoldDeps(deps) {
|
|
9672
|
+
const hasLock = await deps.run("git", ["cat-file", "-e", "HEAD:package-lock.json"]).then(() => true).catch(() => false);
|
|
9673
|
+
await deps.run("npm", hasLock ? ["ci"] : ["install"]);
|
|
9674
|
+
}
|
|
9675
9675
|
async function foldReleaseVersion(deps, model, tag, foldPaths) {
|
|
9676
9676
|
if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
|
|
9677
9677
|
const version = tag.replace(/^v/, "");
|
|
9678
9678
|
if (model === "hub-serverless") {
|
|
9679
9679
|
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
|
|
9680
9680
|
} else {
|
|
9681
|
+
await installAppFoldDeps(deps);
|
|
9681
9682
|
await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
|
|
9682
9683
|
}
|
|
9683
9684
|
for (const path2 of foldPaths) {
|
|
9684
9685
|
await deps.run("git", ["add", "--", path2]).catch(() => void 0);
|
|
9685
9686
|
}
|
|
9687
|
+
const strayVersionEdits = (await deps.run("git", ["diff", "--name-only"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
9688
|
+
if (strayVersionEdits.length > 0) {
|
|
9689
|
+
throw new Error(
|
|
9690
|
+
`version fold left tracked changes outside the fold set uncommitted: ${strayVersionEdits.join(", ")}. The repo's \`version\` script modified paths the train does not stage (fold set: ${foldPaths.join(", ") || "(none)"}). Add them to that script's git-add list (or the fold set) so the release commit is complete \u2014 shipping now would tag a main whose version-lockstep check is red.`
|
|
9691
|
+
);
|
|
9692
|
+
}
|
|
9686
9693
|
const staged = (await deps.run("git", ["diff", "--cached", "--name-only"])).trim();
|
|
9687
9694
|
if (!staged) return `version fold: manifests already at ${version} \u2014 nothing committed`;
|
|
9688
9695
|
await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
|
|
@@ -9981,8 +9988,8 @@ async function correlateRun(deps, args) {
|
|
|
9981
9988
|
function correlateTenantRun(deps, since, titleIncludes) {
|
|
9982
9989
|
return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
|
|
9983
9990
|
}
|
|
9984
|
-
function correlatePublishRun(deps, since) {
|
|
9985
|
-
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
|
|
9991
|
+
function correlatePublishRun(deps, since, titleIncludes) {
|
|
9992
|
+
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch", titleIncludes });
|
|
9986
9993
|
}
|
|
9987
9994
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
9988
9995
|
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
@@ -10067,7 +10074,7 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10067
10074
|
status: "pr-pending",
|
|
10068
10075
|
prNumber: existing.number,
|
|
10069
10076
|
prUrl: existing.url,
|
|
10070
|
-
note: `alignment PR already open: ${existing.url} \u2014 land it with \`
|
|
10077
|
+
note: `alignment PR already open: ${existing.url} \u2014 land it with \`mmi-cli pr merge ${existing.number} --auto --merge\``
|
|
10071
10078
|
};
|
|
10072
10079
|
}
|
|
10073
10080
|
const ahead = clean(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
|
|
@@ -10076,14 +10083,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10076
10083
|
}
|
|
10077
10084
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
10078
10085
|
|
|
10079
|
-
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge**
|
|
10086
|
+
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** \u2014 \`mmi-cli pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
|
|
10080
10087
|
const url = clean(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
|
|
10081
10088
|
const number = parsePrNumber(url);
|
|
10082
10089
|
return {
|
|
10083
10090
|
status: "pr-pending",
|
|
10084
10091
|
prNumber: number,
|
|
10085
10092
|
prUrl: url || void 0,
|
|
10086
|
-
note: `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"} \u2014 land it with \`
|
|
10093
|
+
note: `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"} \u2014 land it with \`mmi-cli pr merge ${number ?? "<number>"} --auto --merge\``
|
|
10087
10094
|
};
|
|
10088
10095
|
}
|
|
10089
10096
|
function resolveContextState(context, checkRuns, statuses) {
|
|
@@ -10268,7 +10275,7 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
|
|
|
10268
10275
|
deployStatus: "failure"
|
|
10269
10276
|
};
|
|
10270
10277
|
}
|
|
10271
|
-
const { runId, runUrl } = await correlatePublishRun(deps, since);
|
|
10278
|
+
const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
|
|
10272
10279
|
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
10273
10280
|
return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
10274
10281
|
}
|
|
@@ -10408,7 +10415,7 @@ async function pushRcAlignment(deps) {
|
|
|
10408
10415
|
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
10409
10416
|
return "origin/rc aligned to the released main";
|
|
10410
10417
|
} catch (e) {
|
|
10411
|
-
return `rc alignment push failed \u2014
|
|
10418
|
+
return `rc alignment push failed \u2014 do NOT bare-push rc (Step 5 forbids it; branch protection rejects it). rc realigns on the next \`mmi-cli rcand\`, or rerun via the train: ${e instanceof Error ? e.message : String(e)}`;
|
|
10412
10419
|
}
|
|
10413
10420
|
}
|
|
10414
10421
|
async function runTrainApplyPipeline(mode, input) {
|
|
@@ -11612,9 +11619,7 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
11612
11619
|
var OWNER = "mutmutco";
|
|
11613
11620
|
var LOCKED_APP = "mmi-github-app";
|
|
11614
11621
|
var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
|
|
11615
|
-
var REQUIRED_DATA_ACCESS = {
|
|
11616
|
-
"mutmutco/MM-Chat": [{ name: "kb-projection-reader", dbRole: "kb_reader", vaultParamNeedle: "KB_READ_DB_URL" }]
|
|
11617
|
-
};
|
|
11622
|
+
var REQUIRED_DATA_ACCESS = {};
|
|
11618
11623
|
function lockedBranches(repoClass, releaseTrack) {
|
|
11619
11624
|
if (releaseTrack) return branchesForTrack(releaseTrack);
|
|
11620
11625
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
@@ -14326,7 +14331,7 @@ async function secretsUse(deps, key, opts) {
|
|
|
14326
14331
|
` \u2022 Keyless run: \`mmi-cli secrets use ${key}${opts.slug ? ` --slug ${opts.slug}` : ""} -- <command>\` injects it into the command's env (never printed). A granted non-master can USE an org secret this way.`,
|
|
14327
14332
|
` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role. Never bake it into an image or commit it.`,
|
|
14328
14333
|
` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
|
|
14329
|
-
tier === "project" ? " \u2022
|
|
14334
|
+
tier === "project" ? " \u2022 One stageless value per repo (/mmi-future/<slug>/<KEY>) \u2014 no dev/rc/main copies. Project-admins self-serve their repo keys." : " \u2022 Org-infra/cross-slug keys remain master-gated unless granted; project-admins self-serve their own repo keys."
|
|
14330
14335
|
].join("\n")
|
|
14331
14336
|
);
|
|
14332
14337
|
return;
|
|
@@ -14620,7 +14625,7 @@ function registerEdgeCommands(program3) {
|
|
|
14620
14625
|
// src/doctor-run.ts
|
|
14621
14626
|
var import_node_fs17 = require("node:fs");
|
|
14622
14627
|
var import_node_child_process10 = require("node:child_process");
|
|
14623
|
-
var
|
|
14628
|
+
var import_promises2 = require("node:fs/promises");
|
|
14624
14629
|
var import_node_path17 = require("node:path");
|
|
14625
14630
|
var import_node_os6 = require("node:os");
|
|
14626
14631
|
|
|
@@ -14945,6 +14950,15 @@ function buildGitignoreManagedBlockCheck(input) {
|
|
|
14945
14950
|
const { added, removed, seeded } = diffManagedGitignoreBlock(input.content);
|
|
14946
14951
|
return { ...base, ok: false, contentToWrite: content, added, removed, seeded };
|
|
14947
14952
|
}
|
|
14953
|
+
var REPO_LOCAL_WORKTREE_LABEL = "worktree location (canonical sibling path)";
|
|
14954
|
+
var REPO_LOCAL_WORKTREE_FIX = "repo-local `.worktrees/` is not the canonical worktree path (#2321) \u2014 use `mmi-cli worktree create <branch>`, which provisions the sibling `../mmi-worktrees/<branch>` outside the tree. Move your work there, then remove the in-repo `.worktrees/`.";
|
|
14955
|
+
function buildRepoLocalWorktreeCheck(input) {
|
|
14956
|
+
return {
|
|
14957
|
+
ok: !(input.isOrgRepo && input.hasRepoLocalWorktrees),
|
|
14958
|
+
label: REPO_LOCAL_WORKTREE_LABEL,
|
|
14959
|
+
fix: REPO_LOCAL_WORKTREE_FIX
|
|
14960
|
+
};
|
|
14961
|
+
}
|
|
14948
14962
|
var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
|
|
14949
14963
|
var SCRATCH_GC_FIX = "run `mmi-cli gc --scratch --dry-run` to inspect; run `mmi-cli gc --scratch --apply` to prune safe scratch; for kept plans run `mmi-cli northstar status` or `mmi-cli northstar sync --wait` first";
|
|
14950
14964
|
function buildScratchGcCheck(plan) {
|
|
@@ -15118,10 +15132,6 @@ var OPENCODE_WORKFLOW_COMMANDS = [
|
|
|
15118
15132
|
"release",
|
|
15119
15133
|
"hotfix",
|
|
15120
15134
|
"bootstrap",
|
|
15121
|
-
"grind",
|
|
15122
|
-
"build",
|
|
15123
|
-
"handoff",
|
|
15124
|
-
"coop",
|
|
15125
15135
|
"browser-automation"
|
|
15126
15136
|
];
|
|
15127
15137
|
function opencodeCommandDescription(command) {
|
|
@@ -15377,6 +15387,21 @@ function buildOpencodeVersionCheck(input) {
|
|
|
15377
15387
|
}
|
|
15378
15388
|
return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15379
15389
|
}
|
|
15390
|
+
function pickOpencodeActiveVersion(input) {
|
|
15391
|
+
for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
|
|
15392
|
+
const trimmed = value?.trim();
|
|
15393
|
+
if (trimmed) return trimmed;
|
|
15394
|
+
}
|
|
15395
|
+
return void 0;
|
|
15396
|
+
}
|
|
15397
|
+
function planOpencodePluginCacheQuarantine(input) {
|
|
15398
|
+
if (!input.cacheExists) return { quarantine: false };
|
|
15399
|
+
if (!isSemverVersion2(input.cacheVersion) || !isSemverVersion2(input.releasedVersion)) return { quarantine: false };
|
|
15400
|
+
if (compareVersions(input.cacheVersion, input.releasedVersion) < 0) {
|
|
15401
|
+
return { quarantine: true, cacheVersion: input.cacheVersion, releasedVersion: input.releasedVersion };
|
|
15402
|
+
}
|
|
15403
|
+
return { quarantine: false };
|
|
15404
|
+
}
|
|
15380
15405
|
var OPENCODE_HOOK_ACTIVE_LABEL = "OpenCode MMI adapter hooks active (shell.env stamp)";
|
|
15381
15406
|
function buildOpencodeHookActiveCheck(input) {
|
|
15382
15407
|
const base = { ok: true, label: OPENCODE_HOOK_ACTIVE_LABEL, fix: OPENCODE_RECOVERY };
|
|
@@ -16381,8 +16406,59 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
|
|
|
16381
16406
|
return false;
|
|
16382
16407
|
}
|
|
16383
16408
|
}
|
|
16409
|
+
function opencodePackagesRoot() {
|
|
16410
|
+
return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
|
|
16411
|
+
}
|
|
16412
|
+
function opencodePluginCacheDirs() {
|
|
16413
|
+
const root = opencodePackagesRoot();
|
|
16414
|
+
try {
|
|
16415
|
+
return (0, import_node_fs17.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path17.join)(root, e.name));
|
|
16416
|
+
} catch {
|
|
16417
|
+
return [];
|
|
16418
|
+
}
|
|
16419
|
+
}
|
|
16420
|
+
function readOpencodeCacheDirVersion(cacheDir) {
|
|
16421
|
+
try {
|
|
16422
|
+
const parsed = JSON.parse(
|
|
16423
|
+
(0, import_node_fs17.readFileSync)((0, import_node_path17.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
|
|
16424
|
+
);
|
|
16425
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
|
|
16426
|
+
} catch {
|
|
16427
|
+
return void 0;
|
|
16428
|
+
}
|
|
16429
|
+
}
|
|
16430
|
+
function readOpencodeLoadedCacheVersion() {
|
|
16431
|
+
const versions = opencodePluginCacheDirs().map(readOpencodeCacheDirVersion).filter((v) => Boolean(v));
|
|
16432
|
+
if (!versions.length) return void 0;
|
|
16433
|
+
return versions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
|
|
16434
|
+
}
|
|
16435
|
+
function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
|
|
16436
|
+
let moved = false;
|
|
16437
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16438
|
+
for (const cacheDir of opencodePluginCacheDirs()) {
|
|
16439
|
+
const plan = planOpencodePluginCacheQuarantine({
|
|
16440
|
+
cacheExists: true,
|
|
16441
|
+
cacheVersion: readOpencodeCacheDirVersion(cacheDir),
|
|
16442
|
+
releasedVersion
|
|
16443
|
+
});
|
|
16444
|
+
if (!plan.quarantine) continue;
|
|
16445
|
+
try {
|
|
16446
|
+
const quarantineRoot = (0, import_node_path17.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
|
|
16447
|
+
(0, import_node_fs17.mkdirSync)(quarantineRoot, { recursive: true });
|
|
16448
|
+
(0, import_node_fs17.renameSync)(cacheDir, (0, import_node_path17.join)(quarantineRoot, (0, import_node_path17.basename)(cacheDir)));
|
|
16449
|
+
log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
|
|
16450
|
+
moved = true;
|
|
16451
|
+
} catch {
|
|
16452
|
+
}
|
|
16453
|
+
}
|
|
16454
|
+
return moved;
|
|
16455
|
+
}
|
|
16384
16456
|
function opencodeInstalledVersionForDoctor() {
|
|
16385
|
-
return
|
|
16457
|
+
return pickOpencodeActiveVersion({
|
|
16458
|
+
envStamp: process.env.MMI_OPENCODE_PLUGIN_VERSION,
|
|
16459
|
+
cacheVersion: readOpencodeLoadedCacheVersion(),
|
|
16460
|
+
diskVersion: readOpencodeAdapterDiskVersion()
|
|
16461
|
+
});
|
|
16386
16462
|
}
|
|
16387
16463
|
function opencodePluginVersionsForReport() {
|
|
16388
16464
|
return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
|
|
@@ -16820,6 +16896,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16820
16896
|
}
|
|
16821
16897
|
}
|
|
16822
16898
|
checks.push(gitignoreCheck);
|
|
16899
|
+
checks.push(buildRepoLocalWorktreeCheck({
|
|
16900
|
+
isOrgRepo,
|
|
16901
|
+
hasRepoLocalWorktrees: (0, import_node_fs17.existsSync)((0, import_node_path17.join)(process.cwd(), ".worktrees"))
|
|
16902
|
+
}));
|
|
16823
16903
|
let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
|
|
16824
16904
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
16825
16905
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
@@ -16906,8 +16986,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16906
16986
|
releasedVersion
|
|
16907
16987
|
});
|
|
16908
16988
|
if (!opencodeVersionCheck.ok && repairFull) {
|
|
16909
|
-
|
|
16910
|
-
|
|
16989
|
+
const refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
|
|
16990
|
+
const quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
|
|
16991
|
+
if (refreshed || quarantined) {
|
|
16992
|
+
opencodeInstalledVersion = opencodeInstalledVersionForDoctor() ?? opencodeInstalledVersion;
|
|
16911
16993
|
opencodeVersionCheck = buildOpencodeVersionCheck({
|
|
16912
16994
|
isOrgRepo,
|
|
16913
16995
|
installedVersion: opencodeInstalledVersion,
|
|
@@ -17080,7 +17162,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17080
17162
|
releasedVersion,
|
|
17081
17163
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17082
17164
|
execFileP: execFileP2,
|
|
17083
|
-
mkdtemp: (prefix) => (0,
|
|
17165
|
+
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
17084
17166
|
log: (m) => io.err(m)
|
|
17085
17167
|
});
|
|
17086
17168
|
if (seeded) {
|
|
@@ -17323,7 +17405,6 @@ function repoFromSelector(selector) {
|
|
|
17323
17405
|
async function loadConfigForBoardSelector(selector, repoOption) {
|
|
17324
17406
|
return loadConfigForRepo(repoFromSelector(selector) ?? repoOption);
|
|
17325
17407
|
}
|
|
17326
|
-
var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/development";
|
|
17327
17408
|
function renderGcApplyResult(result) {
|
|
17328
17409
|
const lines = ["gc apply result:"];
|
|
17329
17410
|
lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
|
|
@@ -17439,65 +17520,11 @@ async function requireFreshTrainCli(commandName) {
|
|
|
17439
17520
|
releasedVersion: await fetchNpmReleasedVersion()
|
|
17440
17521
|
});
|
|
17441
17522
|
if (report.ok) return;
|
|
17442
|
-
throw new Error(
|
|
17523
|
+
throw new Error(staleTrainCliMessage(report, commandName));
|
|
17443
17524
|
}
|
|
17444
17525
|
var program2 = new Command();
|
|
17445
17526
|
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
|
|
17446
|
-
|
|
17447
|
-
const cfg = await loadConfig();
|
|
17448
|
-
if (isRulesSource(cfg.orgRulesSource)) {
|
|
17449
|
-
if (!opts.quiet) io.log('mmi-cli rules: source repo (orgRulesSource: "self") \u2014 skipping self-sync');
|
|
17450
|
-
return true;
|
|
17451
|
-
}
|
|
17452
|
-
if (!await isOrgRegisteredRepo(cfg)) {
|
|
17453
|
-
if (!opts.quiet) io.log("mmi-cli rules: not an org repo \u2014 skipping spine delivery");
|
|
17454
|
-
return true;
|
|
17455
|
-
}
|
|
17456
|
-
const base = resolveRulesBase(cfg.orgRulesSource, DEFAULT_RULES_SOURCE);
|
|
17457
|
-
const token = await githubToken();
|
|
17458
|
-
let changed = 0;
|
|
17459
|
-
const files = [
|
|
17460
|
-
"AGENTS.md",
|
|
17461
|
-
"CLAUDE.md",
|
|
17462
|
-
".claude/settings.json",
|
|
17463
|
-
".claude/output-styles/mmi-plain.md",
|
|
17464
|
-
".cursor/rules/mmi-plain-language.mdc",
|
|
17465
|
-
".cursor/rules/mmi-tool-economy.mdc",
|
|
17466
|
-
".cursor/rules/mmi-code-economy.mdc"
|
|
17467
|
-
];
|
|
17468
|
-
const fetched = await Promise.all(files.map(async (file) => {
|
|
17469
|
-
try {
|
|
17470
|
-
const url = `${base}/${file}`;
|
|
17471
|
-
const res = await fetch(url, { headers: rulesSourceAuthHeaders(url, token), signal: AbortSignal.timeout(1e4) });
|
|
17472
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
17473
|
-
return { file, source: await res.text() };
|
|
17474
|
-
} catch (e) {
|
|
17475
|
-
return { file, error: e.message };
|
|
17476
|
-
}
|
|
17477
|
-
}));
|
|
17478
|
-
const failures = fetched.filter((entry) => "error" in entry);
|
|
17479
|
-
for (const failure of failures) {
|
|
17480
|
-
io.err(`mmi-cli rules: could not fetch ${failure.file} (${failure.error}); left it untouched`);
|
|
17481
|
-
}
|
|
17482
|
-
for (const entry of fetched) {
|
|
17483
|
-
if ("error" in entry) continue;
|
|
17484
|
-
const { file, source } = entry;
|
|
17485
|
-
const current = (0, import_node_fs18.existsSync)(file) ? await (0, import_promises4.readFile)(file, "utf8") : null;
|
|
17486
|
-
if (needsUpdate(source, current)) {
|
|
17487
|
-
const slash = file.lastIndexOf("/");
|
|
17488
|
-
if (slash > 0) (0, import_node_fs18.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
17489
|
-
await (0, import_promises4.writeFile)(file, normalizeEol(source), "utf8");
|
|
17490
|
-
changed++;
|
|
17491
|
-
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
17492
|
-
}
|
|
17493
|
-
}
|
|
17494
|
-
if (!opts.quiet && changed === 0) io.log("mmi-cli rules: up to date");
|
|
17495
|
-
return failures.length === 0;
|
|
17496
|
-
}
|
|
17497
|
-
var rules = program2.command("rules").description("org rules delivery");
|
|
17498
|
-
rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch the org-delivered files (AGENTS.md / CLAUDE.md / .claude/settings.json / output style / Cursor rule) from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
|
|
17499
|
-
if (!await runRulesSync(opts)) process.exitCode = 1;
|
|
17500
|
-
});
|
|
17527
|
+
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
17501
17528
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
17502
17529
|
const path2 = (0, import_node_path18.join)(process.cwd(), ".gitignore");
|
|
17503
17530
|
const current = (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null;
|
|
@@ -17519,30 +17546,6 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
17519
17546
|
console.log("mmi-cli rules gitignore: up to date");
|
|
17520
17547
|
}
|
|
17521
17548
|
});
|
|
17522
|
-
async function runDocsSync(opts, io = consoleIo) {
|
|
17523
|
-
const ref = await gitOut(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
|
|
17524
|
-
const def = (ref.startsWith("origin/") ? ref.slice("origin/".length) : ref) || "development";
|
|
17525
|
-
await gitOut(["fetch", "origin", def, "--quiet"]);
|
|
17526
|
-
const result = await syncDocs({
|
|
17527
|
-
isDirty: async (f) => await gitOut(["status", "--porcelain", "--", f]) !== "",
|
|
17528
|
-
originContent: async (f) => {
|
|
17529
|
-
try {
|
|
17530
|
-
return (await execFileP2("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
|
|
17531
|
-
} catch {
|
|
17532
|
-
return null;
|
|
17533
|
-
}
|
|
17534
|
-
},
|
|
17535
|
-
localContent: async (f) => (0, import_node_fs18.existsSync)(f) ? await (0, import_promises4.readFile)(f, "utf8") : null,
|
|
17536
|
-
writeDoc: async (f, c) => {
|
|
17537
|
-
await (0, import_promises4.writeFile)(f, c, "utf8");
|
|
17538
|
-
}
|
|
17539
|
-
});
|
|
17540
|
-
for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
|
|
17541
|
-
if (!opts.quiet && result.skippedDirty.length) io.log(`mmi-cli docs: kept local edits in ${result.skippedDirty.join(", ")}`);
|
|
17542
|
-
if (!opts.quiet && result.updated.length === 0 && result.skippedDirty.length === 0) io.log("mmi-cli docs: up to date");
|
|
17543
|
-
}
|
|
17544
|
-
var docs = program2.command("docs").description("repo-owned authoritative docs");
|
|
17545
|
-
docs.command("sync").option("--quiet", "stay silent unless something changed or errored").description("refresh README.md / architecture.md from the repo default branch; never clobbers uncommitted edits").action((opts) => runDocsSync(opts));
|
|
17546
17549
|
program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
|
|
17547
17550
|
const manifest = buildCommandManifest(program2);
|
|
17548
17551
|
consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
|
|
@@ -18453,8 +18456,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
18453
18456
|
let northStarSlug;
|
|
18454
18457
|
let extraLabels = [];
|
|
18455
18458
|
try {
|
|
18456
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18457
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18459
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18460
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18458
18461
|
if (o.northStar !== void 0) {
|
|
18459
18462
|
northStarSlug = normalizeNorthStarSlug(o.northStar);
|
|
18460
18463
|
body = appendNorthStarLine(body, northStarSlug);
|
|
@@ -18511,6 +18514,20 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
18511
18514
|
...parentLinkFields(parent, parentLinkError)
|
|
18512
18515
|
}));
|
|
18513
18516
|
});
|
|
18517
|
+
issue.command("view <number>").description("read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
|
|
18518
|
+
const n = Number(number);
|
|
18519
|
+
if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
|
|
18520
|
+
const repo = await resolveRepo(o.repo);
|
|
18521
|
+
if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
|
|
18522
|
+
const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,labels,author,assignees,milestone,body";
|
|
18523
|
+
try {
|
|
18524
|
+
const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", fields]);
|
|
18525
|
+
console.log(JSON.stringify(data));
|
|
18526
|
+
} catch (e) {
|
|
18527
|
+
const err = e;
|
|
18528
|
+
return fail(`issue view: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
18529
|
+
}
|
|
18530
|
+
});
|
|
18514
18531
|
issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
|
|
18515
18532
|
const number = Number(o.number);
|
|
18516
18533
|
if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
|
|
@@ -18566,7 +18583,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
|
|
|
18566
18583
|
}
|
|
18567
18584
|
let body;
|
|
18568
18585
|
try {
|
|
18569
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18586
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18570
18587
|
} catch (e) {
|
|
18571
18588
|
return fail(`issue comment: ${e.message}`);
|
|
18572
18589
|
}
|
|
@@ -18628,8 +18645,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
18628
18645
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
18629
18646
|
const sourceRepo = await resolveRepo(void 0);
|
|
18630
18647
|
try {
|
|
18631
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18632
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18648
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18649
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18633
18650
|
priority = normalizePriority(o.priority);
|
|
18634
18651
|
args = buildIssueArgs({
|
|
18635
18652
|
type: o.type,
|
|
@@ -18702,8 +18719,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
18702
18719
|
let args;
|
|
18703
18720
|
try {
|
|
18704
18721
|
skill = assertSkillName(o.skill);
|
|
18705
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18706
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18722
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18723
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18707
18724
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
18708
18725
|
priority = normalizePriority(o.priority);
|
|
18709
18726
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -18754,14 +18771,28 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
18754
18771
|
let body;
|
|
18755
18772
|
let title;
|
|
18756
18773
|
try {
|
|
18757
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18758
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18774
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18775
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18759
18776
|
} catch (e) {
|
|
18760
18777
|
return fail(`pr create: ${e.message}`);
|
|
18761
18778
|
}
|
|
18762
18779
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
18763
18780
|
console.log(JSON.stringify(created));
|
|
18764
18781
|
});
|
|
18782
|
+
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
|
|
18783
|
+
const n = Number(number);
|
|
18784
|
+
if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
|
|
18785
|
+
const repo = await resolveRepo(o.repo);
|
|
18786
|
+
if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
|
|
18787
|
+
const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
|
|
18788
|
+
try {
|
|
18789
|
+
const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
|
|
18790
|
+
console.log(JSON.stringify(data));
|
|
18791
|
+
} catch (e) {
|
|
18792
|
+
const err = e;
|
|
18793
|
+
return fail(`pr view: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
18794
|
+
}
|
|
18795
|
+
});
|
|
18765
18796
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
18766
18797
|
const wfDir = (0, import_node_path18.join)(cwd, ".github", "workflows");
|
|
18767
18798
|
if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
|
|
@@ -18930,15 +18961,15 @@ async function createDeferredWorktreeStore() {
|
|
|
18930
18961
|
return {
|
|
18931
18962
|
read: async () => {
|
|
18932
18963
|
try {
|
|
18933
|
-
return parseDeferredWorktreesFile(await (0,
|
|
18964
|
+
return parseDeferredWorktreesFile(await (0, import_promises3.readFile)(registryPath, "utf8"));
|
|
18934
18965
|
} catch {
|
|
18935
18966
|
return [];
|
|
18936
18967
|
}
|
|
18937
18968
|
},
|
|
18938
18969
|
write: async (entries) => {
|
|
18939
18970
|
try {
|
|
18940
|
-
await (0,
|
|
18941
|
-
await (0,
|
|
18971
|
+
await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
|
|
18972
|
+
await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
18942
18973
|
} catch {
|
|
18943
18974
|
}
|
|
18944
18975
|
}
|
|
@@ -18979,7 +19010,7 @@ var realWorktreeDirRemover = {
|
|
|
18979
19010
|
(0, import_node_fs18.unlinkSync)(p);
|
|
18980
19011
|
}
|
|
18981
19012
|
},
|
|
18982
|
-
removeTree: (p) => (0,
|
|
19013
|
+
removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
18983
19014
|
};
|
|
18984
19015
|
async function resolvePrimaryCheckout(execGit) {
|
|
18985
19016
|
try {
|
|
@@ -19037,9 +19068,9 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
19037
19068
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
19038
19069
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
19039
19070
|
);
|
|
19040
|
-
const remoteBefore =
|
|
19071
|
+
const remoteBefore = await remoteBranchExists2(headRef);
|
|
19041
19072
|
let remoteDeleteAttempted = false;
|
|
19042
|
-
let remoteNotAttemptedReason
|
|
19073
|
+
let remoteNotAttemptedReason;
|
|
19043
19074
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
|
|
19044
19075
|
const message = String(e.message || "");
|
|
19045
19076
|
if (/already been merged/i.test(message)) {
|
|
@@ -19061,10 +19092,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
19061
19092
|
}
|
|
19062
19093
|
}
|
|
19063
19094
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
19064
|
-
const remoteBranch =
|
|
19065
|
-
attempted: false,
|
|
19066
|
-
reason: remoteNotAttemptedReason
|
|
19067
|
-
}) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
19095
|
+
const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
19068
19096
|
exists: remoteBranchExists2
|
|
19069
19097
|
}, {
|
|
19070
19098
|
attempted: remoteDeleteAttempted,
|
|
@@ -19074,11 +19102,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
19074
19102
|
const deferredStore = await createDeferredWorktreeStore();
|
|
19075
19103
|
let localCleanup;
|
|
19076
19104
|
try {
|
|
19077
|
-
localCleanup =
|
|
19078
|
-
branch: headRef,
|
|
19079
|
-
localBranch: { name: headRef, status: "not-attempted", reason: "repo-option" },
|
|
19080
|
-
worktree: void 0
|
|
19081
|
-
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
19105
|
+
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
19082
19106
|
beforeWorktrees,
|
|
19083
19107
|
startingPath,
|
|
19084
19108
|
pathExists: (p) => (0, import_node_fs18.existsSync)(p),
|
|
@@ -19205,6 +19229,30 @@ board.command("backfill-priority").description("set board Priority from priority
|
|
|
19205
19229
|
return failGraceful(`board backfill-priority failed: ${e.message}`);
|
|
19206
19230
|
}
|
|
19207
19231
|
});
|
|
19232
|
+
board.command("set-priority <issue> <priority>").description(`set a board item's Priority field (${CLI_PRIORITIES.join("|")}) \u2014 recovers a Priority lost on the create-time auto-add race (#2349)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").action(async (issueRef, priorityArg, o) => {
|
|
19233
|
+
let priority;
|
|
19234
|
+
try {
|
|
19235
|
+
priority = normalizePriority(priorityArg);
|
|
19236
|
+
} catch (e) {
|
|
19237
|
+
return fail(`board set-priority failed: ${e.message}`);
|
|
19238
|
+
}
|
|
19239
|
+
try {
|
|
19240
|
+
const defaultRepo = await resolveRepo(o.repo) ?? "";
|
|
19241
|
+
const selector = parseIssueSelector(issueRef, defaultRepo);
|
|
19242
|
+
if (!selector.repo) {
|
|
19243
|
+
return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
|
|
19244
|
+
}
|
|
19245
|
+
const result = await setBoardPriorityForSelector({
|
|
19246
|
+
config: await loadConfigForBoardSelector(issueRef, o.repo),
|
|
19247
|
+
selector,
|
|
19248
|
+
priority
|
|
19249
|
+
});
|
|
19250
|
+
if (o.json) return console.log(JSON.stringify(result));
|
|
19251
|
+
console.log(`Set ${result.ref} Priority -> ${result.priority}`);
|
|
19252
|
+
} catch (e) {
|
|
19253
|
+
return failGraceful(`board set-priority failed: ${e.message}`);
|
|
19254
|
+
}
|
|
19255
|
+
});
|
|
19208
19256
|
board.command("prune-priority-labels").description("remove retired priority:* labels (#416) from issues whose board Priority field is already set").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--dry-run", "report what would be removed without writing").option("--concurrency <n>", "parallel issue edits (default 8)", "8").action(async (o) => {
|
|
19209
19257
|
try {
|
|
19210
19258
|
const result = await prunePriorityLabels({
|
|
@@ -19286,14 +19334,11 @@ function stageKeepAlive() {
|
|
|
19286
19334
|
}
|
|
19287
19335
|
async function resolveStage() {
|
|
19288
19336
|
const cfg = await loadConfig();
|
|
19289
|
-
const local = cfg.stage;
|
|
19290
19337
|
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch((e) => ({ ok: false, error: e.message }));
|
|
19291
19338
|
const project2 = read.ok ? read.project : null;
|
|
19292
19339
|
const portRangeMeta = project2?.portRange ?? void 0;
|
|
19293
19340
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
19294
19341
|
return decideStage({
|
|
19295
|
-
local,
|
|
19296
|
-
shell: shellFor(),
|
|
19297
19342
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
19298
19343
|
hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), "docker-compose.yml")),
|
|
19299
19344
|
hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), ".env.example"))
|
|
@@ -19317,19 +19362,8 @@ async function fetchStageVaultEnvMerge() {
|
|
|
19317
19362
|
}
|
|
19318
19363
|
function stageStepsFor(res, stops = true) {
|
|
19319
19364
|
if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
|
|
19320
|
-
if (res.source === "local") return stagePlan(res.config ?? {}, stops);
|
|
19321
19365
|
return [{ label: `no local stage to run \u2014 ${res.gap ?? "stage config gap"}` }];
|
|
19322
19366
|
}
|
|
19323
|
-
function staleStageNote(res) {
|
|
19324
|
-
if (!res.staleIgnored) return null;
|
|
19325
|
-
const fields = res.staleFields ?? [];
|
|
19326
|
-
const list = fields.join(", ");
|
|
19327
|
-
const label = fields.length > 1 ? `fields ${list}` : `field ${list || "build/up"}`;
|
|
19328
|
-
if (res.source === "local") {
|
|
19329
|
-
return `note: POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 kept the rest of the local recipe`;
|
|
19330
|
-
}
|
|
19331
|
-
return `note: stale POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 using the registry-derived default`;
|
|
19332
|
-
}
|
|
19333
19367
|
function reportedStageUrl(res, result) {
|
|
19334
19368
|
if (!res.derived) return void 0;
|
|
19335
19369
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
@@ -19420,7 +19454,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
|
|
|
19420
19454
|
const res = await resolveStage();
|
|
19421
19455
|
if (o.apply) {
|
|
19422
19456
|
if (res.source === "none") return failGraceful(`stage: ${res.gap}`);
|
|
19423
|
-
const cfg = res.
|
|
19457
|
+
const cfg = res.derived.config;
|
|
19424
19458
|
const hold = stageKeepAlive();
|
|
19425
19459
|
try {
|
|
19426
19460
|
const result = await runStage(cfg, stageScopedRunOpts({ timeoutMs: o.timeoutMs }));
|
|
@@ -19434,9 +19468,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
|
|
|
19434
19468
|
}
|
|
19435
19469
|
}
|
|
19436
19470
|
const steps = stageStepsFor(res);
|
|
19437
|
-
if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url,
|
|
19438
|
-
const note = staleStageNote(res);
|
|
19439
|
-
if (note) printLine(note);
|
|
19471
|
+
if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19440
19472
|
console.log(renderSteps("mmi-cli stage: dry-run plan", steps));
|
|
19441
19473
|
});
|
|
19442
19474
|
stage.command("stop").description("stop the previous local stage process recorded in tmp/stage/state.json").option("--json", "machine-readable output").option("--apply", "kill the recorded process tree and remove the state file").action(async () => {
|
|
@@ -19457,14 +19489,12 @@ stage.command("start").description("start the configured local stage process and
|
|
|
19457
19489
|
const res = await resolveStage();
|
|
19458
19490
|
if (!o.apply) {
|
|
19459
19491
|
const steps = stageStepsFor(res, false);
|
|
19460
|
-
if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url,
|
|
19461
|
-
const note = staleStageNote(res);
|
|
19462
|
-
if (note) printLine(note);
|
|
19492
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19463
19493
|
return printLine(renderSteps("mmi-cli stage start: dry-run plan", steps));
|
|
19464
19494
|
}
|
|
19465
19495
|
if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
|
|
19466
|
-
const cfg = res.
|
|
19467
|
-
const vaultEnvMerge =
|
|
19496
|
+
const cfg = res.derived.config;
|
|
19497
|
+
const vaultEnvMerge = await fetchStageVaultEnvMerge();
|
|
19468
19498
|
try {
|
|
19469
19499
|
const hold = stageKeepAlive();
|
|
19470
19500
|
let printed = false;
|
|
@@ -19492,14 +19522,12 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
19492
19522
|
const res = await resolveStage();
|
|
19493
19523
|
if (!o.apply) {
|
|
19494
19524
|
const steps = stageStepsFor(res);
|
|
19495
|
-
if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url,
|
|
19496
|
-
const note = staleStageNote(res);
|
|
19497
|
-
if (note) printLine(note);
|
|
19525
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19498
19526
|
return printLine(renderSteps("mmi-cli stage run: dry-run plan", steps));
|
|
19499
19527
|
}
|
|
19500
19528
|
if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
|
|
19501
|
-
const cfg = res.
|
|
19502
|
-
const vaultEnvMerge =
|
|
19529
|
+
const cfg = res.derived.config;
|
|
19530
|
+
const vaultEnvMerge = await fetchStageVaultEnvMerge();
|
|
19503
19531
|
try {
|
|
19504
19532
|
const hold = stageKeepAlive();
|
|
19505
19533
|
let printed = false;
|
|
@@ -19569,8 +19597,8 @@ function trainApplyDeps() {
|
|
|
19569
19597
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
19570
19598
|
announce: (args) => announceRelease({
|
|
19571
19599
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
19572
|
-
readFile: (path2) => (0,
|
|
19573
|
-
removeFile: (path2) => (0,
|
|
19600
|
+
readFile: (path2) => (0, import_promises3.readFile)(path2, "utf8"),
|
|
19601
|
+
removeFile: (path2) => (0, import_promises3.unlink)(path2)
|
|
19574
19602
|
}, args),
|
|
19575
19603
|
fetchEdgeDomains: async (slug) => {
|
|
19576
19604
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -19603,7 +19631,7 @@ function renderTrainApply(commandName, r) {
|
|
|
19603
19631
|
if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
|
|
19604
19632
|
if (r.devRollForward) {
|
|
19605
19633
|
const f = r.devRollForward;
|
|
19606
|
-
base = f.status === "pr-pending" ? `${base}; dev roll-forward: ALIGNMENT PR PENDING \u2014 land it with \`
|
|
19634
|
+
base = f.status === "pr-pending" ? `${base}; dev roll-forward: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli pr merge ${f.prNumber ?? "<number>"} --auto --merge\`${f.prUrl ? ` (${f.prUrl})` : ""}` : `${base}; dev roll-forward: ${f.note}`;
|
|
19607
19635
|
}
|
|
19608
19636
|
if (r.checkout) {
|
|
19609
19637
|
base = `${base}; checkout: ${r.checkout.note}`;
|
|
@@ -19635,7 +19663,9 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
19635
19663
|
return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
|
|
19636
19664
|
}
|
|
19637
19665
|
if (o.apply && o.repo) {
|
|
19638
|
-
|
|
19666
|
+
const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
|
|
19667
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
|
|
19668
|
+
if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
|
|
19639
19669
|
}
|
|
19640
19670
|
if (o.apply) {
|
|
19641
19671
|
try {
|
|
@@ -19788,6 +19818,14 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
19788
19818
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
19789
19819
|
if (!report.ok) process.exitCode = 1;
|
|
19790
19820
|
});
|
|
19821
|
+
bootstrap.command("org-ruleset").description("drift-check the codified org no-agent-files push ruleset (mmi-no-agent-files-org) against live; read-only, never mutates (#2196)").option("--json", "machine-readable output").action(async () => {
|
|
19822
|
+
const json = rawFlag("--json");
|
|
19823
|
+
const live = await fetchOrgNoAgentFilesRuleset(defaultGitHubClient());
|
|
19824
|
+
const plan = planOrgNoAgentFilesRuleset(live);
|
|
19825
|
+
if (json) console.log(JSON.stringify({ action: plan.action, id: plan.id, drift: plan.drift, desired: plan.desired }, null, 2));
|
|
19826
|
+
else console.log(renderOrgRulesetDriftReport(plan));
|
|
19827
|
+
if (plan.action !== "noop") process.exitCode = 1;
|
|
19828
|
+
});
|
|
19791
19829
|
bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
19792
19830
|
const o = {
|
|
19793
19831
|
class: rawValue("--class", "deployable"),
|
|
@@ -19811,7 +19849,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19811
19849
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
19812
19850
|
const slug = parsedRepo.slug;
|
|
19813
19851
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
19814
|
-
const
|
|
19852
|
+
const readFile2 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
|
|
19815
19853
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
19816
19854
|
const rawVars = {};
|
|
19817
19855
|
for (const value of rawValues("--var")) {
|
|
@@ -19844,6 +19882,25 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19844
19882
|
applyDeployModel = payload.deployModel;
|
|
19845
19883
|
} catch {
|
|
19846
19884
|
}
|
|
19885
|
+
let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
|
|
19886
|
+
let seededToBranch = 0;
|
|
19887
|
+
if (o.execute) {
|
|
19888
|
+
let branchRules = [];
|
|
19889
|
+
try {
|
|
19890
|
+
branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
|
|
19891
|
+
} catch {
|
|
19892
|
+
branchRules = [];
|
|
19893
|
+
}
|
|
19894
|
+
seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
|
|
19895
|
+
if (seedPlan.branch) {
|
|
19896
|
+
const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
|
|
19897
|
+
try {
|
|
19898
|
+
await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${seedPlan.branch}`, "-f", `sha=${headSha}`]);
|
|
19899
|
+
} catch (e) {
|
|
19900
|
+
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
19901
|
+
}
|
|
19902
|
+
}
|
|
19903
|
+
}
|
|
19847
19904
|
for (const seed of manifest.seeds) {
|
|
19848
19905
|
if (!seed.classes.includes(o.class)) continue;
|
|
19849
19906
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
@@ -19853,7 +19910,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19853
19910
|
let remoteContent = null;
|
|
19854
19911
|
if (resolved.source !== "fanout") {
|
|
19855
19912
|
try {
|
|
19856
|
-
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${
|
|
19913
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
|
|
19857
19914
|
exists = true;
|
|
19858
19915
|
try {
|
|
19859
19916
|
const parsed = JSON.parse(r.stdout);
|
|
@@ -19869,13 +19926,44 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19869
19926
|
}
|
|
19870
19927
|
const planned = planSeedAction(resolved, exists);
|
|
19871
19928
|
const isBlock = resolved.source === "managed-block";
|
|
19872
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
19929
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile2) : null;
|
|
19873
19930
|
const action = reconcileSeedAction(planned, content, isBlock);
|
|
19874
19931
|
actions.push(action);
|
|
19875
19932
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
19876
|
-
await gh(contentPutArgs(repo, resolved.target, content,
|
|
19933
|
+
await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
|
|
19877
19934
|
applied.push(`${action.action} ${resolved.target}`);
|
|
19935
|
+
if (seedPlan.mode === "pr") seededToBranch++;
|
|
19936
|
+
}
|
|
19937
|
+
}
|
|
19938
|
+
let seedPrUrl;
|
|
19939
|
+
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
|
|
19940
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
|
|
19941
|
+
});
|
|
19942
|
+
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
19943
|
+
const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
19944
|
+
if (prDecision.action === "reuse") {
|
|
19945
|
+
seedPrUrl = prDecision.url;
|
|
19946
|
+
} else {
|
|
19947
|
+
const created = await ghCreate([
|
|
19948
|
+
"pr",
|
|
19949
|
+
"create",
|
|
19950
|
+
"--repo",
|
|
19951
|
+
repo,
|
|
19952
|
+
"--base",
|
|
19953
|
+
baseBranch,
|
|
19954
|
+
"--head",
|
|
19955
|
+
seedPlan.branch,
|
|
19956
|
+
"--title",
|
|
19957
|
+
`bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
19958
|
+
"--body",
|
|
19959
|
+
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected").`
|
|
19960
|
+
]);
|
|
19961
|
+
seedPrUrl = created.url;
|
|
19878
19962
|
}
|
|
19963
|
+
await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
|
|
19964
|
+
if (!/already/i.test(String(e.message ?? ""))) throw e;
|
|
19965
|
+
});
|
|
19966
|
+
applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
|
|
19879
19967
|
}
|
|
19880
19968
|
if (o.execute && o.class === "deployable") {
|
|
19881
19969
|
try {
|
|
@@ -19886,7 +19974,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19886
19974
|
}
|
|
19887
19975
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
19888
19976
|
if (rulesetSeed) {
|
|
19889
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
19977
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile2);
|
|
19890
19978
|
if (rulesetContent) {
|
|
19891
19979
|
try {
|
|
19892
19980
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -19999,9 +20087,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19999
20087
|
"--head",
|
|
20000
20088
|
branchName,
|
|
20001
20089
|
"--title",
|
|
20002
|
-
`bootstrap: register ${parsedRepo.name} for org-
|
|
20090
|
+
`bootstrap: register ${parsedRepo.name} for the org-managed .gitignore fanout`,
|
|
20003
20091
|
"--body",
|
|
20004
|
-
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the
|
|
20092
|
+
`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).`
|
|
20005
20093
|
]);
|
|
20006
20094
|
fanoutPrUrl = created.url;
|
|
20007
20095
|
}
|
|
@@ -20014,7 +20102,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
20014
20102
|
return failGraceful(`bootstrap apply: fanout registration failed: ${e.message}`);
|
|
20015
20103
|
}
|
|
20016
20104
|
}
|
|
20017
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
|
|
20105
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
|
|
20018
20106
|
else {
|
|
20019
20107
|
console.log(renderSeedPlan(actions));
|
|
20020
20108
|
if (o.execute) console.log(`
|
|
@@ -20081,13 +20169,12 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
20081
20169
|
));
|
|
20082
20170
|
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
|
|
20083
20171
|
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
20084
|
-
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process
|
|
20172
|
+
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
20085
20173
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
20086
20174
|
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
20087
20175
|
return;
|
|
20088
20176
|
}
|
|
20089
20177
|
if (!await isOrgRepoRoot()) return;
|
|
20090
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
20091
20178
|
spawnDeferredGcSweep();
|
|
20092
20179
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
20093
20180
|
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
@@ -20103,7 +20190,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
20103
20190
|
boardSlice: (io) => runBoardSlice(io, {
|
|
20104
20191
|
loadConfig: () => loadConfigForRepo(),
|
|
20105
20192
|
readBoard,
|
|
20106
|
-
// #1813: warm the slice cache out-of-band (detached
|
|
20193
|
+
// #1813: warm the slice cache out-of-band (detached) so the ~20s live read
|
|
20107
20194
|
// never costs banner time and next session's glance renders instantly within budget.
|
|
20108
20195
|
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
20109
20196
|
}),
|