@mutmutco/cli 2.68.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -17
- package/dist/main.cjs +455 -374
- 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) {
|
|
4271
|
+
if (!pathContained(real, repoAnchor) || !rootCandidateStillAllowed(c, repoAnchor)) {
|
|
4262
4272
|
result.skipped += 1;
|
|
4263
4273
|
continue;
|
|
4264
4274
|
}
|
|
4265
|
-
|
|
4266
|
-
if (!containment || !pathContained(real, containment) || rootScoped && !rootCandidateStillAllowed(c, repoAnchor)) {
|
|
4267
|
-
result.skipped += 1;
|
|
4268
|
-
continue;
|
|
4269
|
-
}
|
|
4270
|
-
if (rootScoped && trackedPathStatus(c, repoAnchor) !== false) {
|
|
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,12 +9668,17 @@ 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) {
|
|
@@ -9981,8 +9982,8 @@ async function correlateRun(deps, args) {
|
|
|
9981
9982
|
function correlateTenantRun(deps, since, titleIncludes) {
|
|
9982
9983
|
return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
|
|
9983
9984
|
}
|
|
9984
|
-
function correlatePublishRun(deps, since) {
|
|
9985
|
-
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
|
|
9985
|
+
function correlatePublishRun(deps, since, titleIncludes) {
|
|
9986
|
+
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch", titleIncludes });
|
|
9986
9987
|
}
|
|
9987
9988
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
9988
9989
|
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
@@ -10268,7 +10269,7 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
|
|
|
10268
10269
|
deployStatus: "failure"
|
|
10269
10270
|
};
|
|
10270
10271
|
}
|
|
10271
|
-
const { runId, runUrl } = await correlatePublishRun(deps, since);
|
|
10272
|
+
const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
|
|
10272
10273
|
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
10273
10274
|
return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
10274
10275
|
}
|
|
@@ -10408,7 +10409,7 @@ async function pushRcAlignment(deps) {
|
|
|
10408
10409
|
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
10409
10410
|
return "origin/rc aligned to the released main";
|
|
10410
10411
|
} catch (e) {
|
|
10411
|
-
return `rc alignment push failed \u2014
|
|
10412
|
+
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
10413
|
}
|
|
10413
10414
|
}
|
|
10414
10415
|
async function runTrainApplyPipeline(mode, input) {
|
|
@@ -11612,9 +11613,7 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
11612
11613
|
var OWNER = "mutmutco";
|
|
11613
11614
|
var LOCKED_APP = "mmi-github-app";
|
|
11614
11615
|
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
|
-
};
|
|
11616
|
+
var REQUIRED_DATA_ACCESS = {};
|
|
11618
11617
|
function lockedBranches(repoClass, releaseTrack) {
|
|
11619
11618
|
if (releaseTrack) return branchesForTrack(releaseTrack);
|
|
11620
11619
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
@@ -14326,7 +14325,7 @@ async function secretsUse(deps, key, opts) {
|
|
|
14326
14325
|
` \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
14326
|
` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role. Never bake it into an image or commit it.`,
|
|
14328
14327
|
` \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
|
|
14328
|
+
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
14329
|
].join("\n")
|
|
14331
14330
|
);
|
|
14332
14331
|
return;
|
|
@@ -14620,7 +14619,7 @@ function registerEdgeCommands(program3) {
|
|
|
14620
14619
|
// src/doctor-run.ts
|
|
14621
14620
|
var import_node_fs17 = require("node:fs");
|
|
14622
14621
|
var import_node_child_process10 = require("node:child_process");
|
|
14623
|
-
var
|
|
14622
|
+
var import_promises2 = require("node:fs/promises");
|
|
14624
14623
|
var import_node_path17 = require("node:path");
|
|
14625
14624
|
var import_node_os6 = require("node:os");
|
|
14626
14625
|
|
|
@@ -14945,6 +14944,15 @@ function buildGitignoreManagedBlockCheck(input) {
|
|
|
14945
14944
|
const { added, removed, seeded } = diffManagedGitignoreBlock(input.content);
|
|
14946
14945
|
return { ...base, ok: false, contentToWrite: content, added, removed, seeded };
|
|
14947
14946
|
}
|
|
14947
|
+
var REPO_LOCAL_WORKTREE_LABEL = "worktree location (canonical sibling path)";
|
|
14948
|
+
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/`.";
|
|
14949
|
+
function buildRepoLocalWorktreeCheck(input) {
|
|
14950
|
+
return {
|
|
14951
|
+
ok: !(input.isOrgRepo && input.hasRepoLocalWorktrees),
|
|
14952
|
+
label: REPO_LOCAL_WORKTREE_LABEL,
|
|
14953
|
+
fix: REPO_LOCAL_WORKTREE_FIX
|
|
14954
|
+
};
|
|
14955
|
+
}
|
|
14948
14956
|
var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
|
|
14949
14957
|
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
14958
|
function buildScratchGcCheck(plan) {
|
|
@@ -15118,10 +15126,6 @@ var OPENCODE_WORKFLOW_COMMANDS = [
|
|
|
15118
15126
|
"release",
|
|
15119
15127
|
"hotfix",
|
|
15120
15128
|
"bootstrap",
|
|
15121
|
-
"grind",
|
|
15122
|
-
"build",
|
|
15123
|
-
"handoff",
|
|
15124
|
-
"coop",
|
|
15125
15129
|
"browser-automation"
|
|
15126
15130
|
];
|
|
15127
15131
|
function opencodeCommandDescription(command) {
|
|
@@ -15377,6 +15381,21 @@ function buildOpencodeVersionCheck(input) {
|
|
|
15377
15381
|
}
|
|
15378
15382
|
return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15379
15383
|
}
|
|
15384
|
+
function pickOpencodeActiveVersion(input) {
|
|
15385
|
+
for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
|
|
15386
|
+
const trimmed = value?.trim();
|
|
15387
|
+
if (trimmed) return trimmed;
|
|
15388
|
+
}
|
|
15389
|
+
return void 0;
|
|
15390
|
+
}
|
|
15391
|
+
function planOpencodePluginCacheQuarantine(input) {
|
|
15392
|
+
if (!input.cacheExists) return { quarantine: false };
|
|
15393
|
+
if (!isSemverVersion2(input.cacheVersion) || !isSemverVersion2(input.releasedVersion)) return { quarantine: false };
|
|
15394
|
+
if (compareVersions(input.cacheVersion, input.releasedVersion) < 0) {
|
|
15395
|
+
return { quarantine: true, cacheVersion: input.cacheVersion, releasedVersion: input.releasedVersion };
|
|
15396
|
+
}
|
|
15397
|
+
return { quarantine: false };
|
|
15398
|
+
}
|
|
15380
15399
|
var OPENCODE_HOOK_ACTIVE_LABEL = "OpenCode MMI adapter hooks active (shell.env stamp)";
|
|
15381
15400
|
function buildOpencodeHookActiveCheck(input) {
|
|
15382
15401
|
const base = { ok: true, label: OPENCODE_HOOK_ACTIVE_LABEL, fix: OPENCODE_RECOVERY };
|
|
@@ -16381,8 +16400,59 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
|
|
|
16381
16400
|
return false;
|
|
16382
16401
|
}
|
|
16383
16402
|
}
|
|
16403
|
+
function opencodePackagesRoot() {
|
|
16404
|
+
return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
|
|
16405
|
+
}
|
|
16406
|
+
function opencodePluginCacheDirs() {
|
|
16407
|
+
const root = opencodePackagesRoot();
|
|
16408
|
+
try {
|
|
16409
|
+
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));
|
|
16410
|
+
} catch {
|
|
16411
|
+
return [];
|
|
16412
|
+
}
|
|
16413
|
+
}
|
|
16414
|
+
function readOpencodeCacheDirVersion(cacheDir) {
|
|
16415
|
+
try {
|
|
16416
|
+
const parsed = JSON.parse(
|
|
16417
|
+
(0, import_node_fs17.readFileSync)((0, import_node_path17.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
|
|
16418
|
+
);
|
|
16419
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
|
|
16420
|
+
} catch {
|
|
16421
|
+
return void 0;
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
function readOpencodeLoadedCacheVersion() {
|
|
16425
|
+
const versions = opencodePluginCacheDirs().map(readOpencodeCacheDirVersion).filter((v) => Boolean(v));
|
|
16426
|
+
if (!versions.length) return void 0;
|
|
16427
|
+
return versions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
|
|
16428
|
+
}
|
|
16429
|
+
function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
|
|
16430
|
+
let moved = false;
|
|
16431
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16432
|
+
for (const cacheDir of opencodePluginCacheDirs()) {
|
|
16433
|
+
const plan = planOpencodePluginCacheQuarantine({
|
|
16434
|
+
cacheExists: true,
|
|
16435
|
+
cacheVersion: readOpencodeCacheDirVersion(cacheDir),
|
|
16436
|
+
releasedVersion
|
|
16437
|
+
});
|
|
16438
|
+
if (!plan.quarantine) continue;
|
|
16439
|
+
try {
|
|
16440
|
+
const quarantineRoot = (0, import_node_path17.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
|
|
16441
|
+
(0, import_node_fs17.mkdirSync)(quarantineRoot, { recursive: true });
|
|
16442
|
+
(0, import_node_fs17.renameSync)(cacheDir, (0, import_node_path17.join)(quarantineRoot, (0, import_node_path17.basename)(cacheDir)));
|
|
16443
|
+
log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
|
|
16444
|
+
moved = true;
|
|
16445
|
+
} catch {
|
|
16446
|
+
}
|
|
16447
|
+
}
|
|
16448
|
+
return moved;
|
|
16449
|
+
}
|
|
16384
16450
|
function opencodeInstalledVersionForDoctor() {
|
|
16385
|
-
return
|
|
16451
|
+
return pickOpencodeActiveVersion({
|
|
16452
|
+
envStamp: process.env.MMI_OPENCODE_PLUGIN_VERSION,
|
|
16453
|
+
cacheVersion: readOpencodeLoadedCacheVersion(),
|
|
16454
|
+
diskVersion: readOpencodeAdapterDiskVersion()
|
|
16455
|
+
});
|
|
16386
16456
|
}
|
|
16387
16457
|
function opencodePluginVersionsForReport() {
|
|
16388
16458
|
return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
|
|
@@ -16820,6 +16890,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16820
16890
|
}
|
|
16821
16891
|
}
|
|
16822
16892
|
checks.push(gitignoreCheck);
|
|
16893
|
+
checks.push(buildRepoLocalWorktreeCheck({
|
|
16894
|
+
isOrgRepo,
|
|
16895
|
+
hasRepoLocalWorktrees: (0, import_node_fs17.existsSync)((0, import_node_path17.join)(process.cwd(), ".worktrees"))
|
|
16896
|
+
}));
|
|
16823
16897
|
let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
|
|
16824
16898
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
16825
16899
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
@@ -16906,8 +16980,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16906
16980
|
releasedVersion
|
|
16907
16981
|
});
|
|
16908
16982
|
if (!opencodeVersionCheck.ok && repairFull) {
|
|
16909
|
-
|
|
16910
|
-
|
|
16983
|
+
const refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
|
|
16984
|
+
const quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
|
|
16985
|
+
if (refreshed || quarantined) {
|
|
16986
|
+
opencodeInstalledVersion = opencodeInstalledVersionForDoctor() ?? opencodeInstalledVersion;
|
|
16911
16987
|
opencodeVersionCheck = buildOpencodeVersionCheck({
|
|
16912
16988
|
isOrgRepo,
|
|
16913
16989
|
installedVersion: opencodeInstalledVersion,
|
|
@@ -17080,7 +17156,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17080
17156
|
releasedVersion,
|
|
17081
17157
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17082
17158
|
execFileP: execFileP2,
|
|
17083
|
-
mkdtemp: (prefix) => (0,
|
|
17159
|
+
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
17084
17160
|
log: (m) => io.err(m)
|
|
17085
17161
|
});
|
|
17086
17162
|
if (seeded) {
|
|
@@ -17323,7 +17399,6 @@ function repoFromSelector(selector) {
|
|
|
17323
17399
|
async function loadConfigForBoardSelector(selector, repoOption) {
|
|
17324
17400
|
return loadConfigForRepo(repoFromSelector(selector) ?? repoOption);
|
|
17325
17401
|
}
|
|
17326
|
-
var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/development";
|
|
17327
17402
|
function renderGcApplyResult(result) {
|
|
17328
17403
|
const lines = ["gc apply result:"];
|
|
17329
17404
|
lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
|
|
@@ -17439,65 +17514,11 @@ async function requireFreshTrainCli(commandName) {
|
|
|
17439
17514
|
releasedVersion: await fetchNpmReleasedVersion()
|
|
17440
17515
|
});
|
|
17441
17516
|
if (report.ok) return;
|
|
17442
|
-
throw new Error(
|
|
17517
|
+
throw new Error(staleTrainCliMessage(report, commandName));
|
|
17443
17518
|
}
|
|
17444
17519
|
var program2 = new Command();
|
|
17445
17520
|
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
|
-
});
|
|
17521
|
+
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
17501
17522
|
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
17523
|
const path2 = (0, import_node_path18.join)(process.cwd(), ".gitignore");
|
|
17503
17524
|
const current = (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null;
|
|
@@ -17519,30 +17540,6 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
17519
17540
|
console.log("mmi-cli rules gitignore: up to date");
|
|
17520
17541
|
}
|
|
17521
17542
|
});
|
|
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
17543
|
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
17544
|
const manifest = buildCommandManifest(program2);
|
|
17548
17545
|
consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
|
|
@@ -18453,8 +18450,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
18453
18450
|
let northStarSlug;
|
|
18454
18451
|
let extraLabels = [];
|
|
18455
18452
|
try {
|
|
18456
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18457
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18453
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18454
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18458
18455
|
if (o.northStar !== void 0) {
|
|
18459
18456
|
northStarSlug = normalizeNorthStarSlug(o.northStar);
|
|
18460
18457
|
body = appendNorthStarLine(body, northStarSlug);
|
|
@@ -18511,6 +18508,20 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
18511
18508
|
...parentLinkFields(parent, parentLinkError)
|
|
18512
18509
|
}));
|
|
18513
18510
|
});
|
|
18511
|
+
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) => {
|
|
18512
|
+
const n = Number(number);
|
|
18513
|
+
if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
|
|
18514
|
+
const repo = await resolveRepo(o.repo);
|
|
18515
|
+
if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
|
|
18516
|
+
const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,labels,author,assignees,milestone,body";
|
|
18517
|
+
try {
|
|
18518
|
+
const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", fields]);
|
|
18519
|
+
console.log(JSON.stringify(data));
|
|
18520
|
+
} catch (e) {
|
|
18521
|
+
const err = e;
|
|
18522
|
+
return fail(`issue view: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
18523
|
+
}
|
|
18524
|
+
});
|
|
18514
18525
|
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
18526
|
const number = Number(o.number);
|
|
18516
18527
|
if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
|
|
@@ -18566,7 +18577,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
|
|
|
18566
18577
|
}
|
|
18567
18578
|
let body;
|
|
18568
18579
|
try {
|
|
18569
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18580
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18570
18581
|
} catch (e) {
|
|
18571
18582
|
return fail(`issue comment: ${e.message}`);
|
|
18572
18583
|
}
|
|
@@ -18628,8 +18639,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
18628
18639
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
18629
18640
|
const sourceRepo = await resolveRepo(void 0);
|
|
18630
18641
|
try {
|
|
18631
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18632
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18642
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18643
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18633
18644
|
priority = normalizePriority(o.priority);
|
|
18634
18645
|
args = buildIssueArgs({
|
|
18635
18646
|
type: o.type,
|
|
@@ -18702,8 +18713,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
18702
18713
|
let args;
|
|
18703
18714
|
try {
|
|
18704
18715
|
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:
|
|
18716
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18717
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18707
18718
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
18708
18719
|
priority = normalizePriority(o.priority);
|
|
18709
18720
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -18754,14 +18765,28 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
18754
18765
|
let body;
|
|
18755
18766
|
let title;
|
|
18756
18767
|
try {
|
|
18757
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18758
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18768
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18769
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18759
18770
|
} catch (e) {
|
|
18760
18771
|
return fail(`pr create: ${e.message}`);
|
|
18761
18772
|
}
|
|
18762
18773
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
18763
18774
|
console.log(JSON.stringify(created));
|
|
18764
18775
|
});
|
|
18776
|
+
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) => {
|
|
18777
|
+
const n = Number(number);
|
|
18778
|
+
if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
|
|
18779
|
+
const repo = await resolveRepo(o.repo);
|
|
18780
|
+
if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
|
|
18781
|
+
const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
|
|
18782
|
+
try {
|
|
18783
|
+
const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
|
|
18784
|
+
console.log(JSON.stringify(data));
|
|
18785
|
+
} catch (e) {
|
|
18786
|
+
const err = e;
|
|
18787
|
+
return fail(`pr view: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
18788
|
+
}
|
|
18789
|
+
});
|
|
18765
18790
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
18766
18791
|
const wfDir = (0, import_node_path18.join)(cwd, ".github", "workflows");
|
|
18767
18792
|
if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
|
|
@@ -18930,15 +18955,15 @@ async function createDeferredWorktreeStore() {
|
|
|
18930
18955
|
return {
|
|
18931
18956
|
read: async () => {
|
|
18932
18957
|
try {
|
|
18933
|
-
return parseDeferredWorktreesFile(await (0,
|
|
18958
|
+
return parseDeferredWorktreesFile(await (0, import_promises3.readFile)(registryPath, "utf8"));
|
|
18934
18959
|
} catch {
|
|
18935
18960
|
return [];
|
|
18936
18961
|
}
|
|
18937
18962
|
},
|
|
18938
18963
|
write: async (entries) => {
|
|
18939
18964
|
try {
|
|
18940
|
-
await (0,
|
|
18941
|
-
await (0,
|
|
18965
|
+
await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
|
|
18966
|
+
await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
18942
18967
|
} catch {
|
|
18943
18968
|
}
|
|
18944
18969
|
}
|
|
@@ -18979,7 +19004,7 @@ var realWorktreeDirRemover = {
|
|
|
18979
19004
|
(0, import_node_fs18.unlinkSync)(p);
|
|
18980
19005
|
}
|
|
18981
19006
|
},
|
|
18982
|
-
removeTree: (p) => (0,
|
|
19007
|
+
removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
18983
19008
|
};
|
|
18984
19009
|
async function resolvePrimaryCheckout(execGit) {
|
|
18985
19010
|
try {
|
|
@@ -19037,9 +19062,9 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
19037
19062
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
19038
19063
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
19039
19064
|
);
|
|
19040
|
-
const remoteBefore =
|
|
19065
|
+
const remoteBefore = await remoteBranchExists2(headRef);
|
|
19041
19066
|
let remoteDeleteAttempted = false;
|
|
19042
|
-
let remoteNotAttemptedReason
|
|
19067
|
+
let remoteNotAttemptedReason;
|
|
19043
19068
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
|
|
19044
19069
|
const message = String(e.message || "");
|
|
19045
19070
|
if (/already been merged/i.test(message)) {
|
|
@@ -19061,10 +19086,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
19061
19086
|
}
|
|
19062
19087
|
}
|
|
19063
19088
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
19064
|
-
const remoteBranch =
|
|
19065
|
-
attempted: false,
|
|
19066
|
-
reason: remoteNotAttemptedReason
|
|
19067
|
-
}) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
19089
|
+
const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
19068
19090
|
exists: remoteBranchExists2
|
|
19069
19091
|
}, {
|
|
19070
19092
|
attempted: remoteDeleteAttempted,
|
|
@@ -19074,11 +19096,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
19074
19096
|
const deferredStore = await createDeferredWorktreeStore();
|
|
19075
19097
|
let localCleanup;
|
|
19076
19098
|
try {
|
|
19077
|
-
localCleanup =
|
|
19078
|
-
branch: headRef,
|
|
19079
|
-
localBranch: { name: headRef, status: "not-attempted", reason: "repo-option" },
|
|
19080
|
-
worktree: void 0
|
|
19081
|
-
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
19099
|
+
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
19082
19100
|
beforeWorktrees,
|
|
19083
19101
|
startingPath,
|
|
19084
19102
|
pathExists: (p) => (0, import_node_fs18.existsSync)(p),
|
|
@@ -19205,6 +19223,30 @@ board.command("backfill-priority").description("set board Priority from priority
|
|
|
19205
19223
|
return failGraceful(`board backfill-priority failed: ${e.message}`);
|
|
19206
19224
|
}
|
|
19207
19225
|
});
|
|
19226
|
+
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) => {
|
|
19227
|
+
let priority;
|
|
19228
|
+
try {
|
|
19229
|
+
priority = normalizePriority(priorityArg);
|
|
19230
|
+
} catch (e) {
|
|
19231
|
+
return fail(`board set-priority failed: ${e.message}`);
|
|
19232
|
+
}
|
|
19233
|
+
try {
|
|
19234
|
+
const defaultRepo = await resolveRepo(o.repo) ?? "";
|
|
19235
|
+
const selector = parseIssueSelector(issueRef, defaultRepo);
|
|
19236
|
+
if (!selector.repo) {
|
|
19237
|
+
return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
|
|
19238
|
+
}
|
|
19239
|
+
const result = await setBoardPriorityForSelector({
|
|
19240
|
+
config: await loadConfigForBoardSelector(issueRef, o.repo),
|
|
19241
|
+
selector,
|
|
19242
|
+
priority
|
|
19243
|
+
});
|
|
19244
|
+
if (o.json) return console.log(JSON.stringify(result));
|
|
19245
|
+
console.log(`Set ${result.ref} Priority -> ${result.priority}`);
|
|
19246
|
+
} catch (e) {
|
|
19247
|
+
return failGraceful(`board set-priority failed: ${e.message}`);
|
|
19248
|
+
}
|
|
19249
|
+
});
|
|
19208
19250
|
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
19251
|
try {
|
|
19210
19252
|
const result = await prunePriorityLabels({
|
|
@@ -19286,14 +19328,11 @@ function stageKeepAlive() {
|
|
|
19286
19328
|
}
|
|
19287
19329
|
async function resolveStage() {
|
|
19288
19330
|
const cfg = await loadConfig();
|
|
19289
|
-
const local = cfg.stage;
|
|
19290
19331
|
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch((e) => ({ ok: false, error: e.message }));
|
|
19291
19332
|
const project2 = read.ok ? read.project : null;
|
|
19292
19333
|
const portRangeMeta = project2?.portRange ?? void 0;
|
|
19293
19334
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
19294
19335
|
return decideStage({
|
|
19295
|
-
local,
|
|
19296
|
-
shell: shellFor(),
|
|
19297
19336
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
19298
19337
|
hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), "docker-compose.yml")),
|
|
19299
19338
|
hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), ".env.example"))
|
|
@@ -19317,19 +19356,8 @@ async function fetchStageVaultEnvMerge() {
|
|
|
19317
19356
|
}
|
|
19318
19357
|
function stageStepsFor(res, stops = true) {
|
|
19319
19358
|
if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
|
|
19320
|
-
if (res.source === "local") return stagePlan(res.config ?? {}, stops);
|
|
19321
19359
|
return [{ label: `no local stage to run \u2014 ${res.gap ?? "stage config gap"}` }];
|
|
19322
19360
|
}
|
|
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
19361
|
function reportedStageUrl(res, result) {
|
|
19334
19362
|
if (!res.derived) return void 0;
|
|
19335
19363
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
@@ -19420,7 +19448,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
|
|
|
19420
19448
|
const res = await resolveStage();
|
|
19421
19449
|
if (o.apply) {
|
|
19422
19450
|
if (res.source === "none") return failGraceful(`stage: ${res.gap}`);
|
|
19423
|
-
const cfg = res.
|
|
19451
|
+
const cfg = res.derived.config;
|
|
19424
19452
|
const hold = stageKeepAlive();
|
|
19425
19453
|
try {
|
|
19426
19454
|
const result = await runStage(cfg, stageScopedRunOpts({ timeoutMs: o.timeoutMs }));
|
|
@@ -19434,9 +19462,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
|
|
|
19434
19462
|
}
|
|
19435
19463
|
}
|
|
19436
19464
|
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);
|
|
19465
|
+
if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19440
19466
|
console.log(renderSteps("mmi-cli stage: dry-run plan", steps));
|
|
19441
19467
|
});
|
|
19442
19468
|
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 +19483,12 @@ stage.command("start").description("start the configured local stage process and
|
|
|
19457
19483
|
const res = await resolveStage();
|
|
19458
19484
|
if (!o.apply) {
|
|
19459
19485
|
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);
|
|
19486
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19463
19487
|
return printLine(renderSteps("mmi-cli stage start: dry-run plan", steps));
|
|
19464
19488
|
}
|
|
19465
19489
|
if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
|
|
19466
|
-
const cfg = res.
|
|
19467
|
-
const vaultEnvMerge =
|
|
19490
|
+
const cfg = res.derived.config;
|
|
19491
|
+
const vaultEnvMerge = await fetchStageVaultEnvMerge();
|
|
19468
19492
|
try {
|
|
19469
19493
|
const hold = stageKeepAlive();
|
|
19470
19494
|
let printed = false;
|
|
@@ -19492,14 +19516,12 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
19492
19516
|
const res = await resolveStage();
|
|
19493
19517
|
if (!o.apply) {
|
|
19494
19518
|
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);
|
|
19519
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19498
19520
|
return printLine(renderSteps("mmi-cli stage run: dry-run plan", steps));
|
|
19499
19521
|
}
|
|
19500
19522
|
if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
|
|
19501
|
-
const cfg = res.
|
|
19502
|
-
const vaultEnvMerge =
|
|
19523
|
+
const cfg = res.derived.config;
|
|
19524
|
+
const vaultEnvMerge = await fetchStageVaultEnvMerge();
|
|
19503
19525
|
try {
|
|
19504
19526
|
const hold = stageKeepAlive();
|
|
19505
19527
|
let printed = false;
|
|
@@ -19569,8 +19591,8 @@ function trainApplyDeps() {
|
|
|
19569
19591
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
19570
19592
|
announce: (args) => announceRelease({
|
|
19571
19593
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
19572
|
-
readFile: (path2) => (0,
|
|
19573
|
-
removeFile: (path2) => (0,
|
|
19594
|
+
readFile: (path2) => (0, import_promises3.readFile)(path2, "utf8"),
|
|
19595
|
+
removeFile: (path2) => (0, import_promises3.unlink)(path2)
|
|
19574
19596
|
}, args),
|
|
19575
19597
|
fetchEdgeDomains: async (slug) => {
|
|
19576
19598
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -19635,7 +19657,9 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
19635
19657
|
return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
|
|
19636
19658
|
}
|
|
19637
19659
|
if (o.apply && o.repo) {
|
|
19638
|
-
|
|
19660
|
+
const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
|
|
19661
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
|
|
19662
|
+
if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
|
|
19639
19663
|
}
|
|
19640
19664
|
if (o.apply) {
|
|
19641
19665
|
try {
|
|
@@ -19788,6 +19812,14 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
19788
19812
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
19789
19813
|
if (!report.ok) process.exitCode = 1;
|
|
19790
19814
|
});
|
|
19815
|
+
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 () => {
|
|
19816
|
+
const json = rawFlag("--json");
|
|
19817
|
+
const live = await fetchOrgNoAgentFilesRuleset(defaultGitHubClient());
|
|
19818
|
+
const plan = planOrgNoAgentFilesRuleset(live);
|
|
19819
|
+
if (json) console.log(JSON.stringify({ action: plan.action, id: plan.id, drift: plan.drift, desired: plan.desired }, null, 2));
|
|
19820
|
+
else console.log(renderOrgRulesetDriftReport(plan));
|
|
19821
|
+
if (plan.action !== "noop") process.exitCode = 1;
|
|
19822
|
+
});
|
|
19791
19823
|
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
19824
|
const o = {
|
|
19793
19825
|
class: rawValue("--class", "deployable"),
|
|
@@ -19811,7 +19843,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19811
19843
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
19812
19844
|
const slug = parsedRepo.slug;
|
|
19813
19845
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
19814
|
-
const
|
|
19846
|
+
const readFile2 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
|
|
19815
19847
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
19816
19848
|
const rawVars = {};
|
|
19817
19849
|
for (const value of rawValues("--var")) {
|
|
@@ -19844,6 +19876,25 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19844
19876
|
applyDeployModel = payload.deployModel;
|
|
19845
19877
|
} catch {
|
|
19846
19878
|
}
|
|
19879
|
+
let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
|
|
19880
|
+
let seededToBranch = 0;
|
|
19881
|
+
if (o.execute) {
|
|
19882
|
+
let branchRules = [];
|
|
19883
|
+
try {
|
|
19884
|
+
branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
|
|
19885
|
+
} catch {
|
|
19886
|
+
branchRules = [];
|
|
19887
|
+
}
|
|
19888
|
+
seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
|
|
19889
|
+
if (seedPlan.branch) {
|
|
19890
|
+
const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
|
|
19891
|
+
try {
|
|
19892
|
+
await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${seedPlan.branch}`, "-f", `sha=${headSha}`]);
|
|
19893
|
+
} catch (e) {
|
|
19894
|
+
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
19895
|
+
}
|
|
19896
|
+
}
|
|
19897
|
+
}
|
|
19847
19898
|
for (const seed of manifest.seeds) {
|
|
19848
19899
|
if (!seed.classes.includes(o.class)) continue;
|
|
19849
19900
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
@@ -19853,7 +19904,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19853
19904
|
let remoteContent = null;
|
|
19854
19905
|
if (resolved.source !== "fanout") {
|
|
19855
19906
|
try {
|
|
19856
|
-
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${
|
|
19907
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
|
|
19857
19908
|
exists = true;
|
|
19858
19909
|
try {
|
|
19859
19910
|
const parsed = JSON.parse(r.stdout);
|
|
@@ -19869,13 +19920,44 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19869
19920
|
}
|
|
19870
19921
|
const planned = planSeedAction(resolved, exists);
|
|
19871
19922
|
const isBlock = resolved.source === "managed-block";
|
|
19872
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
19923
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile2) : null;
|
|
19873
19924
|
const action = reconcileSeedAction(planned, content, isBlock);
|
|
19874
19925
|
actions.push(action);
|
|
19875
19926
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
19876
|
-
await gh(contentPutArgs(repo, resolved.target, content,
|
|
19927
|
+
await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
|
|
19877
19928
|
applied.push(`${action.action} ${resolved.target}`);
|
|
19929
|
+
if (seedPlan.mode === "pr") seededToBranch++;
|
|
19930
|
+
}
|
|
19931
|
+
}
|
|
19932
|
+
let seedPrUrl;
|
|
19933
|
+
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
|
|
19934
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
|
|
19935
|
+
});
|
|
19936
|
+
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
19937
|
+
const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
19938
|
+
if (prDecision.action === "reuse") {
|
|
19939
|
+
seedPrUrl = prDecision.url;
|
|
19940
|
+
} else {
|
|
19941
|
+
const created = await ghCreate([
|
|
19942
|
+
"pr",
|
|
19943
|
+
"create",
|
|
19944
|
+
"--repo",
|
|
19945
|
+
repo,
|
|
19946
|
+
"--base",
|
|
19947
|
+
baseBranch,
|
|
19948
|
+
"--head",
|
|
19949
|
+
seedPlan.branch,
|
|
19950
|
+
"--title",
|
|
19951
|
+
`bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
19952
|
+
"--body",
|
|
19953
|
+
`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").`
|
|
19954
|
+
]);
|
|
19955
|
+
seedPrUrl = created.url;
|
|
19878
19956
|
}
|
|
19957
|
+
await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
|
|
19958
|
+
if (!/already/i.test(String(e.message ?? ""))) throw e;
|
|
19959
|
+
});
|
|
19960
|
+
applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
|
|
19879
19961
|
}
|
|
19880
19962
|
if (o.execute && o.class === "deployable") {
|
|
19881
19963
|
try {
|
|
@@ -19886,7 +19968,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19886
19968
|
}
|
|
19887
19969
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
19888
19970
|
if (rulesetSeed) {
|
|
19889
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
19971
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile2);
|
|
19890
19972
|
if (rulesetContent) {
|
|
19891
19973
|
try {
|
|
19892
19974
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -19999,9 +20081,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19999
20081
|
"--head",
|
|
20000
20082
|
branchName,
|
|
20001
20083
|
"--title",
|
|
20002
|
-
`bootstrap: register ${parsedRepo.name} for org-
|
|
20084
|
+
`bootstrap: register ${parsedRepo.name} for the org-managed .gitignore fanout`,
|
|
20003
20085
|
"--body",
|
|
20004
|
-
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the
|
|
20086
|
+
`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
20087
|
]);
|
|
20006
20088
|
fanoutPrUrl = created.url;
|
|
20007
20089
|
}
|
|
@@ -20014,7 +20096,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
20014
20096
|
return failGraceful(`bootstrap apply: fanout registration failed: ${e.message}`);
|
|
20015
20097
|
}
|
|
20016
20098
|
}
|
|
20017
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
|
|
20099
|
+
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
20100
|
else {
|
|
20019
20101
|
console.log(renderSeedPlan(actions));
|
|
20020
20102
|
if (o.execute) console.log(`
|
|
@@ -20081,13 +20163,12 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
20081
20163
|
));
|
|
20082
20164
|
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
20165
|
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
|
|
20166
|
+
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
20085
20167
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
20086
20168
|
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
20169
|
return;
|
|
20088
20170
|
}
|
|
20089
20171
|
if (!await isOrgRepoRoot()) return;
|
|
20090
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
20091
20172
|
spawnDeferredGcSweep();
|
|
20092
20173
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
20093
20174
|
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
@@ -20103,7 +20184,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
20103
20184
|
boardSlice: (io) => runBoardSlice(io, {
|
|
20104
20185
|
loadConfig: () => loadConfigForRepo(),
|
|
20105
20186
|
readBoard,
|
|
20106
|
-
// #1813: warm the slice cache out-of-band (detached
|
|
20187
|
+
// #1813: warm the slice cache out-of-band (detached) so the ~20s live read
|
|
20107
20188
|
// never costs banner time and next session's glance renders instantly within budget.
|
|
20108
20189
|
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
20109
20190
|
}),
|