@mutmutco/cli 2.30.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +20 -10
- package/dist/main.cjs +838 -118
- package/dist/saga.cjs +20 -10
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3473,18 +3473,28 @@ function versionAtLeast(v, min) {
|
|
|
3473
3473
|
}
|
|
3474
3474
|
|
|
3475
3475
|
// src/client-version.ts
|
|
3476
|
-
function
|
|
3476
|
+
function resolveClientVersionManifestCandidates(distDir = __dirname) {
|
|
3477
|
+
return [
|
|
3478
|
+
(0, import_node_path2.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
|
|
3479
|
+
(0, import_node_path2.join)(distDir, "..", "..", ".cursor-plugin", "plugin.json"),
|
|
3480
|
+
(0, import_node_path2.join)(distDir, "..", "..", ".codex-plugin", "plugin.json"),
|
|
3481
|
+
(0, import_node_path2.join)(distDir, "..", "package.json")
|
|
3482
|
+
];
|
|
3483
|
+
}
|
|
3484
|
+
function readVersionFromManifest(path2) {
|
|
3477
3485
|
try {
|
|
3478
|
-
const
|
|
3479
|
-
return
|
|
3486
|
+
const version = JSON.parse((0, import_node_fs2.readFileSync)(path2, "utf8")).version;
|
|
3487
|
+
return typeof version === "string" && version.trim() ? version.trim() : null;
|
|
3480
3488
|
} catch {
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
function resolveClientVersion() {
|
|
3493
|
+
for (const manifest of resolveClientVersionManifestCandidates()) {
|
|
3494
|
+
const version = readVersionFromManifest(manifest);
|
|
3495
|
+
if (version) return version;
|
|
3487
3496
|
}
|
|
3497
|
+
return "0.0.0";
|
|
3488
3498
|
}
|
|
3489
3499
|
function clientVersionHeaders() {
|
|
3490
3500
|
return { [CLIENT_VERSION_HEADER]: resolveClientVersion() };
|
|
@@ -4102,7 +4112,7 @@ async function hubAuthToken(deps) {
|
|
|
4102
4112
|
var injectedStdin;
|
|
4103
4113
|
async function readStdin() {
|
|
4104
4114
|
if (injectedStdin !== void 0) return injectedStdin;
|
|
4105
|
-
if (process.stdin.isTTY) return "";
|
|
4115
|
+
if (process.stdin.isTTY !== false) return "";
|
|
4106
4116
|
const chunks = [];
|
|
4107
4117
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
4108
4118
|
return Buffer.concat(chunks).toString("utf8");
|
|
@@ -5247,6 +5257,15 @@ function buildIngestPayload(args) {
|
|
|
5247
5257
|
|
|
5248
5258
|
// src/honcho-client.ts
|
|
5249
5259
|
var HONCHO_ASSISTANT_PEER = "assistant";
|
|
5260
|
+
function parseHonchoQueueStatus(json) {
|
|
5261
|
+
const o = json && typeof json === "object" ? json : {};
|
|
5262
|
+
const pendingRaw = o.pending_work_units ?? o.depth ?? o.queue_depth ?? o.pending;
|
|
5263
|
+
const inProgressRaw = o.in_progress_work_units;
|
|
5264
|
+
const pending = typeof pendingRaw === "number" ? pendingRaw : null;
|
|
5265
|
+
const inProgress = typeof inProgressRaw === "number" ? inProgressRaw : null;
|
|
5266
|
+
const stalled = (pending ?? 0) > 0 && (inProgress ?? 0) === 0;
|
|
5267
|
+
return { pending, inProgress, stalled };
|
|
5268
|
+
}
|
|
5250
5269
|
var enc = encodeURIComponent;
|
|
5251
5270
|
var base = (apiUrl) => apiUrl.replace(/\/+$/, "");
|
|
5252
5271
|
var honchoRoutes = {
|
|
@@ -5313,20 +5332,28 @@ async function fetchPeerCard(cfg, peer, opts = {}) {
|
|
|
5313
5332
|
try {
|
|
5314
5333
|
const path2 = honchoRoutes.peerCard(cfg.workspace, peer, opts.project);
|
|
5315
5334
|
const res = await request(cfg, fetchImpl, "GET", path2, void 0, timeoutMs);
|
|
5316
|
-
if (!res.ok) return
|
|
5335
|
+
if (!res.ok) return { status: "error", httpStatus: res.status };
|
|
5317
5336
|
const json = await res.json();
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5337
|
+
const profile = formatPeerCardLines(json?.peer_card, maxChars);
|
|
5338
|
+
return profile ? { status: "ok", data: profile } : { status: "empty" };
|
|
5339
|
+
} catch (e) {
|
|
5340
|
+
return { status: "error", message: e.message };
|
|
5321
5341
|
}
|
|
5322
5342
|
}
|
|
5323
5343
|
async function fetchPeerCardWithFallback(cfg, peer, project2, opts = {}) {
|
|
5324
5344
|
if (project2) {
|
|
5325
5345
|
const scoped = await fetchPeerCard(cfg, peer, { ...opts, project: project2 });
|
|
5326
|
-
if (scoped) return { profile: scoped, scope: "project" };
|
|
5346
|
+
if (scoped.status === "ok") return { profile: scoped.data, scope: "project", status: "ok" };
|
|
5347
|
+
if (scoped.status === "error") {
|
|
5348
|
+
return { profile: null, scope: null, status: "error", httpStatus: scoped.httpStatus, message: scoped.message };
|
|
5349
|
+
}
|
|
5327
5350
|
}
|
|
5328
5351
|
const wide = await fetchPeerCard(cfg, peer, opts);
|
|
5329
|
-
return { profile: wide, scope:
|
|
5352
|
+
if (wide.status === "ok") return { profile: wide.data, scope: "peer", status: "ok" };
|
|
5353
|
+
if (wide.status === "error") {
|
|
5354
|
+
return { profile: null, scope: null, status: "error", httpStatus: wide.httpStatus, message: wide.message };
|
|
5355
|
+
}
|
|
5356
|
+
return { profile: null, scope: null, status: "empty" };
|
|
5330
5357
|
}
|
|
5331
5358
|
async function dialecticChat(cfg, peer, query, opts = {}, fetchImpl = fetch) {
|
|
5332
5359
|
try {
|
|
@@ -5336,25 +5363,44 @@ async function dialecticChat(cfg, peer, query, opts = {}, fetchImpl = fetch) {
|
|
|
5336
5363
|
...opts.target ? { target: opts.target } : {},
|
|
5337
5364
|
...opts.sessionId ? { session_id: opts.sessionId } : {}
|
|
5338
5365
|
}, opts.timeoutMs ?? 15e3);
|
|
5339
|
-
if (!res.ok) return
|
|
5366
|
+
if (!res.ok) return { status: "error", httpStatus: res.status };
|
|
5340
5367
|
const json = await res.json();
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5368
|
+
const answer = (json?.content ?? "").trim();
|
|
5369
|
+
return answer ? { status: "ok", data: answer } : { status: "empty" };
|
|
5370
|
+
} catch (e) {
|
|
5371
|
+
return { status: "error", message: e.message };
|
|
5344
5372
|
}
|
|
5345
5373
|
}
|
|
5346
5374
|
async function probeHoncho(cfg, fetchImpl = fetch, timeoutMs = 3e3, opts = {}) {
|
|
5375
|
+
const emptyQueue = { pending: null, inProgress: null, stalled: false };
|
|
5347
5376
|
try {
|
|
5348
5377
|
const healthRes = await request(cfg, fetchImpl, "GET", honchoRoutes.health(), void 0, timeoutMs);
|
|
5349
5378
|
if (!healthRes.ok) {
|
|
5350
|
-
return { reachable: false, status: healthRes.status, authOk: false, authStatus: void 0 };
|
|
5379
|
+
return { reachable: false, status: healthRes.status, authOk: false, authStatus: void 0, queue: emptyQueue };
|
|
5351
5380
|
}
|
|
5352
5381
|
const authPath = opts.peer ? honchoRoutes.peerCard(cfg.workspace, opts.peer) : honchoRoutes.queueStatus(cfg.workspace);
|
|
5353
5382
|
const authRes = await request(cfg, fetchImpl, "GET", authPath, void 0, timeoutMs);
|
|
5354
5383
|
const authOk = authRes.status !== 401 && authRes.status !== 403;
|
|
5355
|
-
|
|
5384
|
+
let queue = emptyQueue;
|
|
5385
|
+
if (authPath.includes("/queue/status") && authRes.ok) {
|
|
5386
|
+
try {
|
|
5387
|
+
queue = parseHonchoQueueStatus(await authRes.json());
|
|
5388
|
+
} catch {
|
|
5389
|
+
queue = emptyQueue;
|
|
5390
|
+
}
|
|
5391
|
+
} else {
|
|
5392
|
+
const queueRes = await request(cfg, fetchImpl, "GET", honchoRoutes.queueStatus(cfg.workspace), void 0, timeoutMs);
|
|
5393
|
+
if (queueRes.ok) {
|
|
5394
|
+
try {
|
|
5395
|
+
queue = parseHonchoQueueStatus(await queueRes.json());
|
|
5396
|
+
} catch {
|
|
5397
|
+
queue = emptyQueue;
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
return { reachable: true, status: healthRes.status, authOk, authStatus: authRes.status, queue };
|
|
5356
5402
|
} catch {
|
|
5357
|
-
return { reachable: false, authOk: false };
|
|
5403
|
+
return { reachable: false, authOk: false, queue: emptyQueue };
|
|
5358
5404
|
}
|
|
5359
5405
|
}
|
|
5360
5406
|
|
|
@@ -5542,8 +5588,21 @@ async function runHonchoContext(opts, io = consoleIo) {
|
|
|
5542
5588
|
return;
|
|
5543
5589
|
}
|
|
5544
5590
|
const key = await sagaKey(cfg);
|
|
5545
|
-
const { profile, scope } = await fetchPeerCardWithFallback(hc, peer, key.project);
|
|
5546
|
-
if (opts.json)
|
|
5591
|
+
const { profile, scope, status, httpStatus, message } = await fetchPeerCardWithFallback(hc, peer, key.project);
|
|
5592
|
+
if (opts.json) {
|
|
5593
|
+
return io.log(JSON.stringify({
|
|
5594
|
+
peer,
|
|
5595
|
+
project: key.project,
|
|
5596
|
+
cardScope: scope,
|
|
5597
|
+
profile,
|
|
5598
|
+
status,
|
|
5599
|
+
...status === "error" ? { error: { httpStatus, message } } : {}
|
|
5600
|
+
}));
|
|
5601
|
+
}
|
|
5602
|
+
if (status === "error") {
|
|
5603
|
+
io.err(`honcho context: peer card request failed${httpStatus ? ` (${httpStatus})` : ""}${message ? `: ${message}` : ""}`);
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5547
5606
|
if (!profile) return;
|
|
5548
5607
|
io.log(opts.banner ? profileBanner(profile, peer, scope === "project" ? key.project : void 0) : profile);
|
|
5549
5608
|
}
|
|
@@ -5567,9 +5626,21 @@ async function runHonchoChat(query, opts, io = consoleIo) {
|
|
|
5567
5626
|
io.err("honcho chat: query is empty after redaction");
|
|
5568
5627
|
return;
|
|
5569
5628
|
}
|
|
5570
|
-
const
|
|
5571
|
-
if (opts.json)
|
|
5572
|
-
|
|
5629
|
+
const result = await dialecticChat(hc, peer, safeQuery, { target: opts.target });
|
|
5630
|
+
if (opts.json) {
|
|
5631
|
+
return io.log(JSON.stringify({
|
|
5632
|
+
peer,
|
|
5633
|
+
query,
|
|
5634
|
+
answer: result.status === "ok" ? result.data : null,
|
|
5635
|
+
status: result.status,
|
|
5636
|
+
...result.status === "error" ? { error: { httpStatus: result.httpStatus, message: result.message } } : {}
|
|
5637
|
+
}));
|
|
5638
|
+
}
|
|
5639
|
+
if (result.status === "error") {
|
|
5640
|
+
io.err(`honcho chat: request failed${result.httpStatus ? ` (${result.httpStatus})` : ""}${result.message ? `: ${result.message}` : ""}`);
|
|
5641
|
+
return;
|
|
5642
|
+
}
|
|
5643
|
+
io.log(result.status === "ok" ? result.data : "(no answer \u2014 the profile may be empty or not yet built)");
|
|
5573
5644
|
}
|
|
5574
5645
|
async function runHonchoHealth(o, io = consoleIo) {
|
|
5575
5646
|
const cfg = await loadConfig();
|
|
@@ -5577,12 +5648,19 @@ async function runHonchoHealth(o, io = consoleIo) {
|
|
|
5577
5648
|
const apiKey = await honchoApiKey();
|
|
5578
5649
|
const hc = apiKey ? { apiUrl, apiKey, workspace } : null;
|
|
5579
5650
|
const peer = honchoPeerId(await honchoLogin(cfg), cfg);
|
|
5580
|
-
const liveness = hc ? await probeHoncho(hc, fetch, 3e3, { peer: peer ?? void 0 }) : {
|
|
5651
|
+
const liveness = hc ? await probeHoncho(hc, fetch, 3e3, { peer: peer ?? void 0 }) : {
|
|
5652
|
+
reachable: false,
|
|
5653
|
+
status: void 0,
|
|
5654
|
+
authOk: false,
|
|
5655
|
+
authStatus: void 0,
|
|
5656
|
+
queue: { pending: null, inProgress: null, stalled: false }
|
|
5657
|
+
};
|
|
5581
5658
|
const pending = readHonchoPending().length;
|
|
5582
5659
|
const ingestSkip = readIngestSkip();
|
|
5583
5660
|
const ingestSkipMessage = formatIngestSkip(ingestSkip);
|
|
5661
|
+
const deriverStalled = liveness.queue.stalled;
|
|
5584
5662
|
const report = {
|
|
5585
|
-
ok: !!hc && liveness.reachable && liveness.authOk,
|
|
5663
|
+
ok: !!hc && liveness.reachable && liveness.authOk && !deriverStalled,
|
|
5586
5664
|
configured: !!hc,
|
|
5587
5665
|
apiUrl,
|
|
5588
5666
|
apiKeyConfigured: !!apiKey,
|
|
@@ -5593,6 +5671,11 @@ async function runHonchoHealth(o, io = consoleIo) {
|
|
|
5593
5671
|
peer: peer ?? null,
|
|
5594
5672
|
workspace,
|
|
5595
5673
|
pending,
|
|
5674
|
+
deriverQueue: {
|
|
5675
|
+
pending: liveness.queue.pending,
|
|
5676
|
+
inProgress: liveness.queue.inProgress,
|
|
5677
|
+
stalled: deriverStalled
|
|
5678
|
+
},
|
|
5596
5679
|
ingestSkip: ingestSkip ?? null,
|
|
5597
5680
|
ingestSkipMessage: ingestSkipMessage ?? null
|
|
5598
5681
|
};
|
|
@@ -5600,6 +5683,7 @@ async function runHonchoHealth(o, io = consoleIo) {
|
|
|
5600
5683
|
if (o.banner) {
|
|
5601
5684
|
if (report.configured && !report.reachable) io.log(`honcho: configured but unreachable (${pending} pending)`);
|
|
5602
5685
|
if (report.configured && report.reachable && !report.authOk) io.log("honcho: configured but API key rejected (401/403)");
|
|
5686
|
+
if (deriverStalled) io.log(`honcho: deriver stalled (${liveness.queue.pending} pending, 0 in progress)`);
|
|
5603
5687
|
if (ingestSkipMessage) io.log(`honcho: ${ingestSkipMessage}`);
|
|
5604
5688
|
return;
|
|
5605
5689
|
}
|
|
@@ -5608,8 +5692,12 @@ async function runHonchoHealth(o, io = consoleIo) {
|
|
|
5608
5692
|
if (report.configured && !report.reachable) io.log(" - service unreachable");
|
|
5609
5693
|
if (report.configured && report.reachable && !report.authOk) io.log(" - API key rejected (401/403) \u2014 rotate or fix vault path");
|
|
5610
5694
|
if (!report.peer) io.log(" - no peer identity resolved (gh login) \u2014 ingest will be skipped");
|
|
5695
|
+
if (deriverStalled) io.log(` - deriver stalled (${liveness.queue.pending} pending, 0 in progress)`);
|
|
5611
5696
|
if (ingestSkipMessage) io.log(` - ${ingestSkipMessage}`);
|
|
5612
5697
|
if (pending > 0) io.log(` - ${pending} ingest(s) queued locally`);
|
|
5698
|
+
if (liveness.queue.pending != null && liveness.queue.pending > 0) {
|
|
5699
|
+
io.log(` - deriver queue: ${liveness.queue.pending} pending, ${liveness.queue.inProgress ?? 0} in progress`);
|
|
5700
|
+
}
|
|
5613
5701
|
}
|
|
5614
5702
|
async function runHonchoKey(o, io = consoleIo) {
|
|
5615
5703
|
const cfg = await loadConfig();
|
|
@@ -7141,7 +7229,7 @@ ${buildReportBody(body, sourceRepo)}`;
|
|
|
7141
7229
|
|
|
7142
7230
|
// src/skill-lesson.ts
|
|
7143
7231
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
7144
|
-
var SKILL_NAMES = ["bootstrap", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
7232
|
+
var SKILL_NAMES = ["bootstrap", "browser-automation", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
7145
7233
|
function assertSkillName(name) {
|
|
7146
7234
|
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
7147
7235
|
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
@@ -7267,11 +7355,183 @@ function buildPanelPlan(input) {
|
|
|
7267
7355
|
};
|
|
7268
7356
|
}
|
|
7269
7357
|
|
|
7358
|
+
// src/grind-policy.ts
|
|
7359
|
+
var DEFAULT_SEARCH_DENY_DOMAINS = [
|
|
7360
|
+
"stackoverflow.com",
|
|
7361
|
+
"stackexchange.com",
|
|
7362
|
+
"github.com/issues",
|
|
7363
|
+
"reddit.com"
|
|
7364
|
+
];
|
|
7365
|
+
|
|
7366
|
+
// src/verify-fusion.ts
|
|
7367
|
+
var DEFAULT_MODELS = {
|
|
7368
|
+
builder: "builder-slot",
|
|
7369
|
+
verifier: "verifier-slot",
|
|
7370
|
+
third: "third-slot",
|
|
7371
|
+
synthesizer: "verifier-slot"
|
|
7372
|
+
};
|
|
7373
|
+
function resolveFusionProviderUrl(explicit) {
|
|
7374
|
+
if (explicit) return explicit;
|
|
7375
|
+
const env = process.env.MMI_FUSION_PROVIDER_URL?.trim();
|
|
7376
|
+
return env || null;
|
|
7377
|
+
}
|
|
7378
|
+
function buildFusionPlan(input) {
|
|
7379
|
+
const routing = input.routing;
|
|
7380
|
+
const lenses = input.lenses ?? [...GRIND_LENSES];
|
|
7381
|
+
const provider = resolveFusionProviderUrl(input.providerUrl ?? void 0);
|
|
7382
|
+
const models = {
|
|
7383
|
+
...DEFAULT_MODELS,
|
|
7384
|
+
...input.models,
|
|
7385
|
+
synthesizer: input.models?.synthesizer ?? input.models?.third ?? input.models?.verifier ?? DEFAULT_MODELS.synthesizer
|
|
7386
|
+
};
|
|
7387
|
+
if (models.verifier === models.builder) {
|
|
7388
|
+
throw new Error("fusion plan: verifier must not equal builder");
|
|
7389
|
+
}
|
|
7390
|
+
if (models.synthesizer === models.builder) {
|
|
7391
|
+
throw new Error("fusion plan: synthesizer must not equal builder");
|
|
7392
|
+
}
|
|
7393
|
+
const toolPolicy = {
|
|
7394
|
+
webSearch: Boolean(input.toolPolicy?.webSearch),
|
|
7395
|
+
maxQueriesPerLens: input.toolPolicy?.maxQueriesPerLens ?? 3,
|
|
7396
|
+
denyDomains: input.toolPolicy?.denyDomains ?? []
|
|
7397
|
+
};
|
|
7398
|
+
return {
|
|
7399
|
+
provider,
|
|
7400
|
+
routing,
|
|
7401
|
+
lenses,
|
|
7402
|
+
models,
|
|
7403
|
+
toolPolicy,
|
|
7404
|
+
criteria: input.criteria,
|
|
7405
|
+
diff: input.diff,
|
|
7406
|
+
fallback: "host-panel",
|
|
7407
|
+
instructions: "Hosted fusion when provider is configured; else spawn host lenses and pipe JSON to `mmi-cli verify synthesize`. Synthesizer slot must differ from builder."
|
|
7408
|
+
};
|
|
7409
|
+
}
|
|
7410
|
+
function adaptFusionResponse(raw) {
|
|
7411
|
+
if (raw.lenses) {
|
|
7412
|
+
const lenses = parseLensResults(raw.lenses);
|
|
7413
|
+
const base2 = synthesizePanelReport(lenses);
|
|
7414
|
+
return {
|
|
7415
|
+
...base2,
|
|
7416
|
+
consensus: raw.consensus?.length ? raw.consensus : base2.consensus,
|
|
7417
|
+
contradictions: raw.contradictions ?? base2.contradictions,
|
|
7418
|
+
partial_coverage: raw.partial_coverage ?? base2.partial_coverage,
|
|
7419
|
+
unique_insights: raw.unique_insights ?? base2.unique_insights,
|
|
7420
|
+
blind_spots: raw.blind_spots ?? base2.blind_spots,
|
|
7421
|
+
nits: raw.nits ?? base2.nits
|
|
7422
|
+
};
|
|
7423
|
+
}
|
|
7424
|
+
const blockers = (raw.blockers ?? []).map((b, i) => ({
|
|
7425
|
+
id: `fusion-${i}`,
|
|
7426
|
+
title: b.title,
|
|
7427
|
+
file: b.file,
|
|
7428
|
+
line: b.line,
|
|
7429
|
+
why: b.why,
|
|
7430
|
+
sources: b.sources ?? ["hosted-fusion"]
|
|
7431
|
+
}));
|
|
7432
|
+
return {
|
|
7433
|
+
consensus: raw.consensus ?? [],
|
|
7434
|
+
contradictions: raw.contradictions ?? [],
|
|
7435
|
+
partial_coverage: raw.partial_coverage ?? [],
|
|
7436
|
+
unique_insights: raw.unique_insights ?? [],
|
|
7437
|
+
blind_spots: raw.blind_spots ?? [],
|
|
7438
|
+
blockers,
|
|
7439
|
+
nits: raw.nits ?? []
|
|
7440
|
+
};
|
|
7441
|
+
}
|
|
7442
|
+
async function runFusionProvider(plan2, deps = {}) {
|
|
7443
|
+
const url = resolveFusionProviderUrl(deps.providerUrl ?? plan2.provider ?? void 0);
|
|
7444
|
+
if (!url) {
|
|
7445
|
+
return { ok: false, source: "fallback", error: "no fusion provider configured" };
|
|
7446
|
+
}
|
|
7447
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
7448
|
+
const apiKey = deps.apiKey ?? process.env.MMI_FUSION_API_KEY?.trim();
|
|
7449
|
+
const headers = { "content-type": "application/json" };
|
|
7450
|
+
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
|
|
7451
|
+
try {
|
|
7452
|
+
const res = await fetchImpl(url, {
|
|
7453
|
+
method: "POST",
|
|
7454
|
+
headers,
|
|
7455
|
+
body: JSON.stringify({
|
|
7456
|
+
routing: plan2.routing,
|
|
7457
|
+
lenses: plan2.lenses,
|
|
7458
|
+
models: plan2.models,
|
|
7459
|
+
toolPolicy: plan2.toolPolicy,
|
|
7460
|
+
criteria: plan2.criteria,
|
|
7461
|
+
diff: plan2.diff
|
|
7462
|
+
}),
|
|
7463
|
+
signal: AbortSignal.timeout(3e4)
|
|
7464
|
+
});
|
|
7465
|
+
if (!res.ok) {
|
|
7466
|
+
return { ok: false, source: "fallback", error: `provider HTTP ${res.status}` };
|
|
7467
|
+
}
|
|
7468
|
+
const body = await res.json();
|
|
7469
|
+
return { ok: true, source: "hosted-fusion", report: adaptFusionResponse(body) };
|
|
7470
|
+
} catch (e) {
|
|
7471
|
+
return { ok: false, source: "fallback", error: e.message };
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7474
|
+
function parseFusionLenses(raw) {
|
|
7475
|
+
return raw.split(",").map((s) => assertGrindLens(s.trim()));
|
|
7476
|
+
}
|
|
7477
|
+
function parseFusionRouting(raw) {
|
|
7478
|
+
return assertVerifyRouting(raw);
|
|
7479
|
+
}
|
|
7480
|
+
|
|
7270
7481
|
// src/gc.ts
|
|
7482
|
+
var DEFERRED_SWEEP_COMMAND = "mmi-cli gc --apply";
|
|
7483
|
+
var DEFERRED_NOTE = "Worktree cleanup deferred \u2014 close this folder in your editor (or run cleanup from a shell outside it), then rerun mmi-cli gc --apply.";
|
|
7271
7484
|
var WORKTREE_LOCK_RE = /EPERM|EBUSY|EACCES|ENOTEMPTY|permission denied|access is denied|used by another process|resource busy|directory not empty/i;
|
|
7272
7485
|
function isWorktreeLockError(error) {
|
|
7273
7486
|
return WORKTREE_LOCK_RE.test(error instanceof Error ? error.message : String(error));
|
|
7274
7487
|
}
|
|
7488
|
+
function deferredWorktreesRegistryPath(gitDir) {
|
|
7489
|
+
const base2 = gitDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
7490
|
+
return `${base2}/mmi-deferred-worktrees.json`;
|
|
7491
|
+
}
|
|
7492
|
+
function parseDeferredWorktreesFile(text) {
|
|
7493
|
+
const parsed = JSON.parse(text);
|
|
7494
|
+
if (!parsed || !Array.isArray(parsed.entries)) return [];
|
|
7495
|
+
return parsed.entries.filter((e) => Boolean(e) && typeof e === "object" && typeof e.path === "string" && typeof e.branch === "string" && e.reason === "lock-held").map((e) => ({ ...e, registeredAt: e.registeredAt || (/* @__PURE__ */ new Date(0)).toISOString() }));
|
|
7496
|
+
}
|
|
7497
|
+
function serializeDeferredWorktrees(entries) {
|
|
7498
|
+
return `${JSON.stringify({ entries }, null, 2)}
|
|
7499
|
+
`;
|
|
7500
|
+
}
|
|
7501
|
+
function deferredPathKey(path2) {
|
|
7502
|
+
return normPath(path2);
|
|
7503
|
+
}
|
|
7504
|
+
function isPersistentWorktreeLockFailure(outcome) {
|
|
7505
|
+
return outcome.status === "failed" && isWorktreeLockError(outcome.error);
|
|
7506
|
+
}
|
|
7507
|
+
async function registerDeferredWorktree(store, entry) {
|
|
7508
|
+
const existing = await store.read();
|
|
7509
|
+
const key = deferredPathKey(entry.path);
|
|
7510
|
+
const already = existing.some((e) => deferredPathKey(e.path) === key);
|
|
7511
|
+
if (already) return { entries: existing, newlyRegistered: false };
|
|
7512
|
+
const next = {
|
|
7513
|
+
...entry,
|
|
7514
|
+
registeredAt: entry.registeredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7515
|
+
reason: "lock-held"
|
|
7516
|
+
};
|
|
7517
|
+
const entries = [...existing, next];
|
|
7518
|
+
await store.write(entries);
|
|
7519
|
+
return { entries, newlyRegistered: true };
|
|
7520
|
+
}
|
|
7521
|
+
async function sweepDeferredWorktrees(store, deps) {
|
|
7522
|
+
if (!store) return { removed: [], stillDeferred: [] };
|
|
7523
|
+
const entries = await store.read();
|
|
7524
|
+
if (!entries.length) return { removed: [], stillDeferred: [] };
|
|
7525
|
+
const removed = [];
|
|
7526
|
+
const stillDeferred = [];
|
|
7527
|
+
for (const entry of entries) {
|
|
7528
|
+
const outcome = await removeWorktreeWithRecovery(entry.path, deps);
|
|
7529
|
+
if (outcome.status === "removed") removed.push(entry.path);
|
|
7530
|
+
else stillDeferred.push(entry);
|
|
7531
|
+
}
|
|
7532
|
+
await store.write(stillDeferred);
|
|
7533
|
+
return { removed, stillDeferred };
|
|
7534
|
+
}
|
|
7275
7535
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7276
7536
|
async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
7277
7537
|
const maxAttempts = deps.maxAttempts ?? 3;
|
|
@@ -7337,7 +7597,9 @@ function summarizePrMergeCleanupStatus(input) {
|
|
|
7337
7597
|
if (input.remoteBranch.status === "failed") return "warnings";
|
|
7338
7598
|
if (input.localBranch.status === "failed") return "warnings";
|
|
7339
7599
|
if (input.localBranch.reason === "worktree-removal-failed") return "warnings";
|
|
7600
|
+
if (input.localBranch.reason === "worktree-removal-deferred") return "warnings";
|
|
7340
7601
|
if (input.worktree?.status === "failed") return "warnings";
|
|
7602
|
+
if (input.worktree?.status === "deferred") return "warnings";
|
|
7341
7603
|
return "clean";
|
|
7342
7604
|
}
|
|
7343
7605
|
function buildPrMergeResultPayload(input) {
|
|
@@ -7565,9 +7827,10 @@ function selectPrMergeCleanupWorktree(branch, before, after, startingPath) {
|
|
|
7565
7827
|
if (startingPath && before.some((w) => w.branch === branch && samePath(w.path, startingPath))) return startingPath;
|
|
7566
7828
|
return void 0;
|
|
7567
7829
|
}
|
|
7568
|
-
function selectSafeWorktreeCwd(worktrees, targetPath) {
|
|
7830
|
+
function selectSafeWorktreeCwd(worktrees, targetPath, options) {
|
|
7569
7831
|
if (!targetPath) return void 0;
|
|
7570
|
-
|
|
7832
|
+
const exists = options?.pathExists ?? (() => true);
|
|
7833
|
+
return worktrees.find((w) => !samePath(w.path, targetPath) && exists(w.path))?.path;
|
|
7571
7834
|
}
|
|
7572
7835
|
function branchMissingFromList(branch, stdout) {
|
|
7573
7836
|
const names = stdout.split(/\r?\n/).map((line) => line.replace(/^\*\s*/, "").trim()).filter(Boolean);
|
|
@@ -7598,8 +7861,16 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
7598
7861
|
}
|
|
7599
7862
|
const beforeWorktrees = options.beforeWorktrees ?? [];
|
|
7600
7863
|
const wtPath = selectPrMergeCleanupWorktree(branch, beforeWorktrees, afterWorktrees, options.startingPath);
|
|
7601
|
-
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath
|
|
7864
|
+
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath, {
|
|
7865
|
+
pathExists: options.pathExists
|
|
7866
|
+
});
|
|
7602
7867
|
const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
|
|
7868
|
+
const removeDeps = {
|
|
7869
|
+
git,
|
|
7870
|
+
sleep: options.sleep ?? defaultSleep,
|
|
7871
|
+
removeWorktreeDir: options.removeWorktreeDir
|
|
7872
|
+
};
|
|
7873
|
+
await sweepDeferredWorktrees(options.deferredStore, removeDeps).catch(() => void 0);
|
|
7603
7874
|
const mainWorktreePath = beforeWorktrees[0]?.path ?? afterWorktrees[0]?.path;
|
|
7604
7875
|
const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
|
|
7605
7876
|
if (wtPath && mainWorktreeTarget) {
|
|
@@ -7620,6 +7891,37 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
7620
7891
|
});
|
|
7621
7892
|
if (outcome.status === "removed") {
|
|
7622
7893
|
report.worktree = { path: wtPath, status: "removed", stageTeardown, recovery: outcome.recovery };
|
|
7894
|
+
} else if (isPersistentWorktreeLockFailure(outcome) && options.deferredStore) {
|
|
7895
|
+
try {
|
|
7896
|
+
const { newlyRegistered } = await registerDeferredWorktree(options.deferredStore, {
|
|
7897
|
+
path: wtPath,
|
|
7898
|
+
branch,
|
|
7899
|
+
reason: "lock-held"
|
|
7900
|
+
});
|
|
7901
|
+
report.worktree = {
|
|
7902
|
+
path: wtPath,
|
|
7903
|
+
status: "deferred",
|
|
7904
|
+
reason: "lock-held",
|
|
7905
|
+
error: outcome.error,
|
|
7906
|
+
deferredNote: DEFERRED_NOTE,
|
|
7907
|
+
deferredSweepCommand: DEFERRED_SWEEP_COMMAND,
|
|
7908
|
+
...newlyRegistered ? { safeCleanupCommand: safeWorktreeRemoveCommand(safeCwd, wtPath) } : {},
|
|
7909
|
+
stageTeardown
|
|
7910
|
+
};
|
|
7911
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-deferred" };
|
|
7912
|
+
return report;
|
|
7913
|
+
} catch (e) {
|
|
7914
|
+
report.worktree = {
|
|
7915
|
+
path: wtPath,
|
|
7916
|
+
status: "failed",
|
|
7917
|
+
reason: "deferred-registry-unavailable",
|
|
7918
|
+
error: errorMessage(e),
|
|
7919
|
+
safeCleanupCommand: safeWorktreeRemoveCommand(safeCwd, wtPath),
|
|
7920
|
+
stageTeardown
|
|
7921
|
+
};
|
|
7922
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-failed" };
|
|
7923
|
+
return report;
|
|
7924
|
+
}
|
|
7623
7925
|
} else {
|
|
7624
7926
|
report.worktree = {
|
|
7625
7927
|
path: wtPath,
|
|
@@ -8419,6 +8721,7 @@ var CURSOR_PLUGIN_INSTALL_LABEL = "Cursor Team Marketplace plugin install";
|
|
|
8419
8721
|
var CURSOR_MARKETPLACE_INSTALL_GUIDE = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/cursor-marketplace-install.md";
|
|
8420
8722
|
var CURSOR_PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
|
|
8421
8723
|
var CURSOR_HOOKS_JSON_REL = "hooks/hooks.json";
|
|
8724
|
+
var CURSOR_HOOK_CLI_LABEL = "Cursor hook CLI bundle";
|
|
8422
8725
|
function joinCachePath(root, ...parts) {
|
|
8423
8726
|
const sep = root.includes("\\") ? "\\" : "/";
|
|
8424
8727
|
return [root.replace(/[\\/]+$/, ""), ...parts].join(sep);
|
|
@@ -8474,6 +8777,20 @@ function buildCursorPluginInstallCheck(input) {
|
|
|
8474
8777
|
}
|
|
8475
8778
|
return { ...base2, cacheRoot: input.cacheRoot, pins: input.pins };
|
|
8476
8779
|
}
|
|
8780
|
+
function buildCursorHookCliCheck(input) {
|
|
8781
|
+
const fix = "update the MMI Team Marketplace plugin (releases ship cli/dist under plugins/mmi/cli/dist) or install mmi-cli on PATH \u2014 Cursor hooks fall back to PATH when the bundled CLI is missing";
|
|
8782
|
+
const base2 = { ok: true, label: CURSOR_HOOK_CLI_LABEL, fix };
|
|
8783
|
+
if (!input.isOrgRepo) return base2;
|
|
8784
|
+
const shouldCheck = input.surface === "cursor" || input.pins.length > 0;
|
|
8785
|
+
if (!shouldCheck) return base2;
|
|
8786
|
+
if (input.pins.length === 0) {
|
|
8787
|
+
if (input.surface === "cursor" && !input.mmiCliOnPath) return { ...base2, ok: false };
|
|
8788
|
+
return base2;
|
|
8789
|
+
}
|
|
8790
|
+
const missingBundle = input.pins.some((p) => !p.hasCliBundle);
|
|
8791
|
+
if (missingBundle && !input.mmiCliOnPath) return { ...base2, ok: false };
|
|
8792
|
+
return base2;
|
|
8793
|
+
}
|
|
8477
8794
|
var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
|
|
8478
8795
|
function buildHubCompatCheck(input) {
|
|
8479
8796
|
const label = "Hub compatibility (client version vs Hub minimum)";
|
|
@@ -8483,6 +8800,48 @@ function buildHubCompatCheck(input) {
|
|
|
8483
8800
|
}
|
|
8484
8801
|
return { ok: versionAtLeast(input.installedVersion, min), label: `${label}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
|
|
8485
8802
|
}
|
|
8803
|
+
var PLAYWRIGHT_MCP_VISION_CAP_LABEL = "Playwright MCP vision caps (--caps=vision prohibited)";
|
|
8804
|
+
var PLAYWRIGHT_MCP_VISION_CAP_FIX = "remove --caps=vision (and vision-first defaults) from Playwright MCP args \u2014 use DOM-first tools; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
|
|
8805
|
+
function textHasPlaywrightVisionCap(content) {
|
|
8806
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
8807
|
+
if (/(?:^|[\s"'`=])--caps=(?:[^"\s\n]*,)?vision\b/m.test(normalized)) return true;
|
|
8808
|
+
if (/\b--caps\s*=\s*["']?vision\b/.test(normalized)) return true;
|
|
8809
|
+
if (/"--caps"\s*,\s*"vision"/.test(normalized)) return true;
|
|
8810
|
+
if (/"caps"\s*:\s*"vision"/.test(normalized)) return true;
|
|
8811
|
+
const playwrightMcp = /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
|
|
8812
|
+
if (!playwrightMcp) return false;
|
|
8813
|
+
if (/\bvision[-_]?(?:first|only|mode)\b/i.test(normalized)) return true;
|
|
8814
|
+
return false;
|
|
8815
|
+
}
|
|
8816
|
+
function buildPlaywrightMcpVisionCapCheck(input) {
|
|
8817
|
+
const base2 = {
|
|
8818
|
+
ok: true,
|
|
8819
|
+
label: PLAYWRIGHT_MCP_VISION_CAP_LABEL,
|
|
8820
|
+
fix: PLAYWRIGHT_MCP_VISION_CAP_FIX
|
|
8821
|
+
};
|
|
8822
|
+
if (!input.isOrgRepo) return base2;
|
|
8823
|
+
const offending = input.configs.filter((c) => textHasPlaywrightVisionCap(c.content)).map((c) => c.path);
|
|
8824
|
+
if (offending.length === 0) return base2;
|
|
8825
|
+
return {
|
|
8826
|
+
...base2,
|
|
8827
|
+
ok: false,
|
|
8828
|
+
offendingPaths: offending,
|
|
8829
|
+
fix: `${PLAYWRIGHT_MCP_VISION_CAP_FIX} \u2014 found in: ${offending.join(", ")}`
|
|
8830
|
+
};
|
|
8831
|
+
}
|
|
8832
|
+
var STRAY_BROWSER_ARTIFACT_DIRS = [".playwright-mcp", "playwright-report", "test-results"];
|
|
8833
|
+
var BROWSER_ARTIFACTS_LABEL = "browser MCP artifacts outside tmp/ (use tmp/playwright-mcp)";
|
|
8834
|
+
var BROWSER_ARTIFACTS_FIX = "move or delete stray Playwright output at repo root; re-run MCP with --output-dir tmp/playwright-mcp \u2014 see skills/browser-automation/SKILL.md";
|
|
8835
|
+
function buildBrowserArtifactsCheck(input) {
|
|
8836
|
+
const base2 = { ok: true, label: BROWSER_ARTIFACTS_LABEL, fix: BROWSER_ARTIFACTS_FIX };
|
|
8837
|
+
if (!input.isOrgRepo || input.strayPaths.length === 0) return base2;
|
|
8838
|
+
return {
|
|
8839
|
+
...base2,
|
|
8840
|
+
ok: false,
|
|
8841
|
+
strayPaths: [...input.strayPaths],
|
|
8842
|
+
fix: `${BROWSER_ARTIFACTS_FIX} \u2014 found: ${input.strayPaths.join(", ")}`
|
|
8843
|
+
};
|
|
8844
|
+
}
|
|
8486
8845
|
|
|
8487
8846
|
// src/stage-live.ts
|
|
8488
8847
|
var import_node_net = require("node:net");
|
|
@@ -8856,10 +9215,22 @@ function resolveDeployModel(meta, repo) {
|
|
|
8856
9215
|
function projectTypeClearsWebProfile(projectType, deployModel) {
|
|
8857
9216
|
return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
|
|
8858
9217
|
}
|
|
8859
|
-
function
|
|
9218
|
+
function inferReleaseTrackFromBranches(hints) {
|
|
9219
|
+
if (hints.hasRcBranch) return void 0;
|
|
9220
|
+
if (hints.hasDevelopmentBranch && hints.hasMainBranch) return "direct";
|
|
9221
|
+
return void 0;
|
|
9222
|
+
}
|
|
9223
|
+
function resolveBootstrapReleaseTrack(cls, explicit) {
|
|
9224
|
+
if (isReleaseTrack(explicit)) return explicit;
|
|
9225
|
+
if (cls === "content") return "trunk";
|
|
9226
|
+
return "full";
|
|
9227
|
+
}
|
|
9228
|
+
function resolveReleaseTrack(meta, hints) {
|
|
8860
9229
|
const raw = typeof meta?.releaseTrack === "string" ? meta.releaseTrack : void 0;
|
|
8861
9230
|
if (isReleaseTrack(raw)) return raw;
|
|
8862
9231
|
if (meta?.class === "content" || meta?.deployModel === "content") return "trunk";
|
|
9232
|
+
const inferred = hints ? inferReleaseTrackFromBranches(hints) : void 0;
|
|
9233
|
+
if (inferred) return inferred;
|
|
8863
9234
|
return "full";
|
|
8864
9235
|
}
|
|
8865
9236
|
function branchesForTrack(track) {
|
|
@@ -9010,6 +9381,18 @@ function ensurePositiveCount(out, emptyMessage) {
|
|
|
9010
9381
|
if (!Number.isFinite(count)) throw new Error(`could not parse ahead count: ${out.trim() || "(empty)"}`);
|
|
9011
9382
|
if (count <= 0) throw new Error(emptyMessage);
|
|
9012
9383
|
}
|
|
9384
|
+
async function remoteBranchExists(deps, branch) {
|
|
9385
|
+
const out = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
9386
|
+
return out.length > 0;
|
|
9387
|
+
}
|
|
9388
|
+
async function loadReleaseTrackBranchHints(deps) {
|
|
9389
|
+
const [hasDevelopmentBranch, hasMainBranch, hasRcBranch] = await Promise.all([
|
|
9390
|
+
remoteBranchExists(deps, "development"),
|
|
9391
|
+
remoteBranchExists(deps, "main"),
|
|
9392
|
+
remoteBranchExists(deps, "rc")
|
|
9393
|
+
]);
|
|
9394
|
+
return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
|
|
9395
|
+
}
|
|
9013
9396
|
async function buildTrainApplyContext(deps) {
|
|
9014
9397
|
const repo = requireValue(clean(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
9015
9398
|
const [owner, name] = repo.split("/");
|
|
@@ -9443,7 +9826,8 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
9443
9826
|
await requireCleanTree(deps);
|
|
9444
9827
|
await deps.run("git", ["fetch", "origin"]);
|
|
9445
9828
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
9446
|
-
const
|
|
9829
|
+
const branchHints = await loadReleaseTrackBranchHints(deps);
|
|
9830
|
+
const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
|
|
9447
9831
|
if (command === "rcand") {
|
|
9448
9832
|
await requireBranch(deps, "development");
|
|
9449
9833
|
if (directTrack) {
|
|
@@ -9502,7 +9886,8 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
9502
9886
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
9503
9887
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
9504
9888
|
await deps.run("git", ["push", "origin", "main"]);
|
|
9505
|
-
const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
9889
|
+
const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
9890
|
+
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
9506
9891
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
9507
9892
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
9508
9893
|
const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
|
|
@@ -9534,13 +9919,16 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
9534
9919
|
if (command === "release" && options.dev) {
|
|
9535
9920
|
await requireBranch(deps, "development");
|
|
9536
9921
|
await ffOnlyPull(deps, "development");
|
|
9537
|
-
const
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
throw new Error(
|
|
9542
|
-
|
|
9543
|
-
|
|
9922
|
+
const hasRcBranch = branchHints.hasRcBranch ?? false;
|
|
9923
|
+
if (hasRcBranch) {
|
|
9924
|
+
const rcOnlyOut = (await deps.run("git", ["rev-list", "--count", "--right-only", "--cherry-pick", "--no-merges", "origin/development...origin/rc"])).trim();
|
|
9925
|
+
const rcOnly = Number.parseInt(rcOnlyOut, 10);
|
|
9926
|
+
if (!Number.isFinite(rcOnly)) throw new Error(`release --dev: could not count rc-only commits for the guard: ${rcOnlyOut || "(empty)"}`);
|
|
9927
|
+
if (rcOnly > 0) {
|
|
9928
|
+
throw new Error(
|
|
9929
|
+
`release --dev refused: origin/rc carries ${rcOnly} commit(s) not in origin/development \u2014 a development -> main release would drop that rc-only content. Land it on development first, or release the candidate via the default rc -> main path, then rerun.`
|
|
9930
|
+
);
|
|
9931
|
+
}
|
|
9544
9932
|
}
|
|
9545
9933
|
ensurePositiveCount(
|
|
9546
9934
|
await deps.run("git", ["rev-list", "--count", "origin/main..origin/development"]),
|
|
@@ -9548,7 +9936,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
9548
9936
|
);
|
|
9549
9937
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
9550
9938
|
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
9551
|
-
const rcShaAtRelease = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
9939
|
+
const rcShaAtRelease = hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
9552
9940
|
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
9553
9941
|
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
9554
9942
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -9571,18 +9959,23 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
9571
9959
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
9572
9960
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
9573
9961
|
await deps.run("git", ["push", "origin", "main"]);
|
|
9574
|
-
const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
9962
|
+
const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
9963
|
+
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
9575
9964
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
9576
9965
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
9577
9966
|
const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
|
|
9578
|
-
const retirement2 = await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease);
|
|
9967
|
+
const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
|
|
9579
9968
|
const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
|
|
9580
9969
|
let rcAlignment2;
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9970
|
+
if (hasRcBranch) {
|
|
9971
|
+
try {
|
|
9972
|
+
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
9973
|
+
rcAlignment2 = "origin/rc aligned to the released main";
|
|
9974
|
+
} catch (e) {
|
|
9975
|
+
rcAlignment2 = `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
|
|
9976
|
+
}
|
|
9977
|
+
} else {
|
|
9978
|
+
rcAlignment2 = "no origin/rc branch \u2014 rc alignment skipped";
|
|
9586
9979
|
}
|
|
9587
9980
|
const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
|
|
9588
9981
|
return {
|
|
@@ -9987,6 +10380,11 @@ async function deriveHotfixVersion(deps) {
|
|
|
9987
10380
|
function hotfixBranch(tag) {
|
|
9988
10381
|
return `hotfix/${tag}`;
|
|
9989
10382
|
}
|
|
10383
|
+
async function resolveHotfixDeployModel(deps, ctx) {
|
|
10384
|
+
const load = await loadProjectMeta(deps, ctx);
|
|
10385
|
+
const meta = load.status === "ok" ? load.meta : null;
|
|
10386
|
+
return resolveDeployModel2(meta, ctx.repo);
|
|
10387
|
+
}
|
|
9990
10388
|
async function findHotfixPr(deps, ctx, tag) {
|
|
9991
10389
|
const out = await deps.run("gh", [
|
|
9992
10390
|
"pr",
|
|
@@ -10024,6 +10422,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
10024
10422
|
}
|
|
10025
10423
|
async function runHotfixStart(deps, options) {
|
|
10026
10424
|
const ctx = await buildTrainApplyContext(deps);
|
|
10425
|
+
const deployModel = await resolveHotfixDeployModel(deps, ctx);
|
|
10027
10426
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
10028
10427
|
if (status.trim()) throw new Error("working tree must be clean before hotfix start");
|
|
10029
10428
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
@@ -10061,18 +10460,23 @@ async function runHotfixStart(deps, options) {
|
|
|
10061
10460
|
throw new Error(`cherry-pick of ${label} onto ${branch} conflicted \u2014 aborted; resolve by hand on a manual hotfix branch, keeping the -x trailer (${e.message ?? e})`);
|
|
10062
10461
|
}
|
|
10063
10462
|
notes.push(`cherry-picked ${label} onto ${branch} (from origin/main, -x trailer recorded)`);
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10463
|
+
if (deployModel === "hub-serverless") {
|
|
10464
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
|
|
10465
|
+
const changedFiles = (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
10466
|
+
await deps.run("git", ["add", "--", ...changedFiles]);
|
|
10467
|
+
const staged = await deps.run("git", ["diff", "--cached", "--name-only"]);
|
|
10468
|
+
if (staged.trim()) {
|
|
10469
|
+
await deps.run("git", ["commit", "-m", `hotfix ${tag}: lock plugin set + @mutmutco/cli distribution to ${version}`]);
|
|
10470
|
+
notes.push(`distribution prepared + committed for ${version} (${changedFiles.length} locked paths)`);
|
|
10471
|
+
} else {
|
|
10472
|
+
notes.push("distribution prepare produced no changes \u2014 nothing extra committed");
|
|
10473
|
+
}
|
|
10071
10474
|
} else {
|
|
10072
|
-
notes.push(
|
|
10475
|
+
notes.push(`distribution bump skipped (deployModel=${deployModel}, Hub-only step)`);
|
|
10073
10476
|
}
|
|
10074
10477
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
10075
10478
|
}
|
|
10479
|
+
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
|
|
10076
10480
|
const prUrl = clean2(await deps.run("gh", [
|
|
10077
10481
|
"pr",
|
|
10078
10482
|
"create",
|
|
@@ -10085,7 +10489,7 @@ async function runHotfixStart(deps, options) {
|
|
|
10085
10489
|
"--title",
|
|
10086
10490
|
`[hotfix] ${tag}`,
|
|
10087
10491
|
"--body",
|
|
10088
|
-
`Hotfix ${tag}: cherry-pick of ${label} onto origin/main
|
|
10492
|
+
`Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
|
|
10089
10493
|
|
|
10090
10494
|
Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
|
|
10091
10495
|
]));
|
|
@@ -10130,6 +10534,7 @@ async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
|
10130
10534
|
}
|
|
10131
10535
|
async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
10132
10536
|
const ctx = await buildTrainApplyContext(deps);
|
|
10537
|
+
const deployModel = await resolveHotfixDeployModel(deps, ctx);
|
|
10133
10538
|
const { tag, version } = normalizeHotfixVersion(versionInput);
|
|
10134
10539
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
10135
10540
|
if (status.trim()) throw new Error("working tree must be clean before hotfix release");
|
|
@@ -10164,33 +10569,74 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
10164
10569
|
}
|
|
10165
10570
|
}
|
|
10166
10571
|
const runs = [];
|
|
10167
|
-
|
|
10168
|
-
|
|
10572
|
+
let deployNote;
|
|
10573
|
+
if (deployModel === "hub-serverless") {
|
|
10574
|
+
for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
|
|
10575
|
+
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
10576
|
+
}
|
|
10577
|
+
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
10578
|
+
} else if (deployModel === "tenant-container" || deployModel === "solo-container") {
|
|
10579
|
+
const dispatch = await dispatchDeploy(
|
|
10580
|
+
deps,
|
|
10581
|
+
ctx,
|
|
10582
|
+
"main",
|
|
10583
|
+
"main",
|
|
10584
|
+
deployModel,
|
|
10585
|
+
true,
|
|
10586
|
+
(deps.now ?? Date.now)(),
|
|
10587
|
+
mergedSha,
|
|
10588
|
+
"report"
|
|
10589
|
+
);
|
|
10590
|
+
deployNote = dispatch.note;
|
|
10591
|
+
runs.push({
|
|
10592
|
+
workflow: "tenant-deploy.yml",
|
|
10593
|
+
url: dispatch.runUrl,
|
|
10594
|
+
conclusion: dispatch.deployStatus === "success" ? "success" : dispatch.deployStatus === "failure" ? "failure" : dispatch.deployStatus ?? "pending"
|
|
10595
|
+
});
|
|
10596
|
+
} else {
|
|
10597
|
+
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
10169
10598
|
}
|
|
10170
|
-
const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
10171
10599
|
let verifyNote;
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
attempt
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10600
|
+
if (deployModel === "hub-serverless") {
|
|
10601
|
+
const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
10602
|
+
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
10603
|
+
try {
|
|
10604
|
+
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
10605
|
+
const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
|
|
10606
|
+
const sleep = sleeper(deps);
|
|
10607
|
+
let attempt = 0;
|
|
10608
|
+
for (; ; ) {
|
|
10609
|
+
attempt++;
|
|
10610
|
+
try {
|
|
10611
|
+
await deps.run("node", verifyArgs);
|
|
10612
|
+
break;
|
|
10613
|
+
} catch (err) {
|
|
10614
|
+
if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
|
|
10615
|
+
await sleep(HOTFIX_VERIFY_RETRY_MS);
|
|
10616
|
+
}
|
|
10186
10617
|
}
|
|
10618
|
+
const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
|
|
10619
|
+
verifyNote = `distribution verified at ${tag}${publishSucceeded ? ` (npm included${retried})` : " (npm view skipped \u2014 publish run not confirmed)"}`;
|
|
10620
|
+
} finally {
|
|
10621
|
+
if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]);
|
|
10187
10622
|
}
|
|
10188
|
-
|
|
10189
|
-
verifyNote = `distribution
|
|
10190
|
-
} finally {
|
|
10191
|
-
if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]);
|
|
10623
|
+
} else {
|
|
10624
|
+
verifyNote = `distribution verify skipped (deployModel=${deployModel}, Hub-only step)`;
|
|
10192
10625
|
}
|
|
10193
|
-
return {
|
|
10626
|
+
return {
|
|
10627
|
+
...ctx,
|
|
10628
|
+
command: "hotfix-release",
|
|
10629
|
+
tag,
|
|
10630
|
+
mergedSha,
|
|
10631
|
+
deployModel,
|
|
10632
|
+
checks,
|
|
10633
|
+
tagNote,
|
|
10634
|
+
releaseNote,
|
|
10635
|
+
runs,
|
|
10636
|
+
deployNote,
|
|
10637
|
+
verifyNote,
|
|
10638
|
+
announceNote
|
|
10639
|
+
};
|
|
10194
10640
|
}
|
|
10195
10641
|
function deriveHotfixState(f) {
|
|
10196
10642
|
if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
|
|
@@ -10226,6 +10672,11 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
10226
10672
|
return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived };
|
|
10227
10673
|
}
|
|
10228
10674
|
}
|
|
10675
|
+
const inFlight = await findInFlightHotfixVersion(deps, ctx);
|
|
10676
|
+
if (inFlight) {
|
|
10677
|
+
const facts2 = await gatherHotfixFacts(deps, ctx, inFlight.tag, inFlight.version);
|
|
10678
|
+
return { ...ctx, command: "hotfix-status", ...facts2, ...deriveHotfixState(facts2) };
|
|
10679
|
+
}
|
|
10229
10680
|
({ tag, version } = await deriveHotfixVersion(deps));
|
|
10230
10681
|
}
|
|
10231
10682
|
const facts = await gatherHotfixFacts(deps, ctx, tag, version);
|
|
@@ -10271,6 +10722,47 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
10271
10722
|
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
|
|
10272
10723
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
10273
10724
|
}
|
|
10725
|
+
async function findInFlightHotfixVersion(deps, ctx) {
|
|
10726
|
+
const tags = /* @__PURE__ */ new Set();
|
|
10727
|
+
const out = await deps.run("gh", [
|
|
10728
|
+
"pr",
|
|
10729
|
+
"list",
|
|
10730
|
+
"--repo",
|
|
10731
|
+
ctx.repo,
|
|
10732
|
+
"--base",
|
|
10733
|
+
"main",
|
|
10734
|
+
"--state",
|
|
10735
|
+
"all",
|
|
10736
|
+
"--limit",
|
|
10737
|
+
"50",
|
|
10738
|
+
"--json",
|
|
10739
|
+
"headRefName"
|
|
10740
|
+
]);
|
|
10741
|
+
for (const row of JSON.parse(out || "[]")) {
|
|
10742
|
+
const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
|
|
10743
|
+
if (m) tags.add(m[1]);
|
|
10744
|
+
}
|
|
10745
|
+
const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
|
|
10746
|
+
for (const line of branchOut.split("\n").filter(Boolean)) {
|
|
10747
|
+
const ref = line.split(/\s+/)[1] ?? "";
|
|
10748
|
+
const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
|
|
10749
|
+
if (m) tags.add(m[1]);
|
|
10750
|
+
}
|
|
10751
|
+
const sorted = [...tags].sort((a, b) => {
|
|
10752
|
+
const pa = a.slice(1).split(".").map(Number);
|
|
10753
|
+
const pb = b.slice(1).split(".").map(Number);
|
|
10754
|
+
for (let i = 0; i < 3; i++) {
|
|
10755
|
+
if (pa[i] !== pb[i]) return pb[i] - pa[i];
|
|
10756
|
+
}
|
|
10757
|
+
return 0;
|
|
10758
|
+
});
|
|
10759
|
+
for (const tag of sorted) {
|
|
10760
|
+
const version = tag.slice(1);
|
|
10761
|
+
const facts = await gatherHotfixFacts(deps, ctx, tag, version);
|
|
10762
|
+
if (deriveHotfixState(facts).state !== "complete") return { tag, version };
|
|
10763
|
+
}
|
|
10764
|
+
return null;
|
|
10765
|
+
}
|
|
10274
10766
|
|
|
10275
10767
|
// src/release-announce.ts
|
|
10276
10768
|
var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
|
|
@@ -10716,6 +11208,9 @@ var requiredIssueTemplates = [
|
|
|
10716
11208
|
".github/ISSUE_TEMPLATE/config.yml"
|
|
10717
11209
|
];
|
|
10718
11210
|
var requiredWorkflows = [];
|
|
11211
|
+
var requiredProductWorkflows = [".github/workflows/gate.yml"];
|
|
11212
|
+
var requiredProductRulesetRef = ".github/rulesets/mmi-product-required-checks.json";
|
|
11213
|
+
var HUB_REPO3 = "mutmutco/MMI-Hub";
|
|
10719
11214
|
var requiredLabels = ["bug", "feature", "task", "priority:urgent", "priority:high", "priority:medium", "priority:low"];
|
|
10720
11215
|
var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
|
|
10721
11216
|
var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
|
|
@@ -10728,6 +11223,7 @@ var requiredProjectWorkflows = [
|
|
|
10728
11223
|
];
|
|
10729
11224
|
var requiredOrgRulesetTypes = ["pull_request", "non_fast_forward", "deletion"];
|
|
10730
11225
|
var requiredHubStatusChecks = ["cli", "infra", "docs"];
|
|
11226
|
+
var requiredProductStatusChecks = ["gate"];
|
|
10731
11227
|
function expectedBranches(repoClass, releaseTrack) {
|
|
10732
11228
|
if (isReleaseTrack(releaseTrack)) return branchesForTrack(releaseTrack);
|
|
10733
11229
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
@@ -10880,6 +11376,16 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
10880
11376
|
for (const path2 of requiredWorkflows) {
|
|
10881
11377
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
|
|
10882
11378
|
}
|
|
11379
|
+
if (repo !== HUB_REPO3) {
|
|
11380
|
+
for (const path2 of requiredProductWorkflows) {
|
|
11381
|
+
checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
|
|
11382
|
+
}
|
|
11383
|
+
checks.push({
|
|
11384
|
+
ok: await contentExists(deps, repo, baseBranch, requiredProductRulesetRef),
|
|
11385
|
+
label: "product required-check ruleset reference exists",
|
|
11386
|
+
detail: `expected: ${requiredProductRulesetRef} (apply as an active repo ruleset after bootstrap)`
|
|
11387
|
+
});
|
|
11388
|
+
}
|
|
10883
11389
|
if (repoClass === "deployable") {
|
|
10884
11390
|
const trainScript = "scripts/next-version.mjs";
|
|
10885
11391
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
|
|
@@ -11015,7 +11521,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
11015
11521
|
label: "covered by an active org ruleset",
|
|
11016
11522
|
detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
|
|
11017
11523
|
});
|
|
11018
|
-
if (repo ===
|
|
11524
|
+
if (repo === HUB_REPO3) {
|
|
11019
11525
|
const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
11020
11526
|
const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
|
|
11021
11527
|
checks.push({
|
|
@@ -11023,6 +11529,14 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
11023
11529
|
label: "Hub required status checks configured",
|
|
11024
11530
|
detail: optionDetail(missing)
|
|
11025
11531
|
});
|
|
11532
|
+
} else {
|
|
11533
|
+
const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
11534
|
+
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
11535
|
+
checks.push({
|
|
11536
|
+
ok: missing.length === 0,
|
|
11537
|
+
label: "product required status checks configured",
|
|
11538
|
+
detail: missing.length ? `missing contexts: ${missing.join(", ")} \u2014 apply ${requiredProductRulesetRef} as an active repo ruleset` : void 0
|
|
11539
|
+
});
|
|
11026
11540
|
}
|
|
11027
11541
|
const declaredApis = (deps.requiredGcpApis ?? []).filter((a) => a && a.trim());
|
|
11028
11542
|
if (declaredApis.length > 0) {
|
|
@@ -11074,12 +11588,42 @@ function parseOwnerRepo(repo) {
|
|
|
11074
11588
|
return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
|
|
11075
11589
|
}
|
|
11076
11590
|
var DEFAULT_INSTALL_CMD = "npm ci";
|
|
11077
|
-
|
|
11591
|
+
var DEFAULT_GATE_CMD = "npm run check";
|
|
11592
|
+
function gateSeedVars(cls, releaseTrack) {
|
|
11593
|
+
const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
|
|
11594
|
+
if (track === "trunk") {
|
|
11595
|
+
return {
|
|
11596
|
+
GATE_CMD: DEFAULT_GATE_CMD,
|
|
11597
|
+
GATE_PUSH_BRANCHES_YAML: "[main]",
|
|
11598
|
+
GATE_FULL_RUN_BRANCH: "main",
|
|
11599
|
+
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/main"]'
|
|
11600
|
+
};
|
|
11601
|
+
}
|
|
11602
|
+
if (track === "direct") {
|
|
11603
|
+
return {
|
|
11604
|
+
GATE_CMD: DEFAULT_GATE_CMD,
|
|
11605
|
+
GATE_PUSH_BRANCHES_YAML: "[development, main]",
|
|
11606
|
+
GATE_FULL_RUN_BRANCH: "development",
|
|
11607
|
+
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/main"]'
|
|
11608
|
+
};
|
|
11609
|
+
}
|
|
11610
|
+
return {
|
|
11611
|
+
GATE_CMD: DEFAULT_GATE_CMD,
|
|
11612
|
+
GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
|
|
11613
|
+
GATE_FULL_RUN_BRANCH: "development",
|
|
11614
|
+
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/rc", "refs/heads/main"]'
|
|
11615
|
+
};
|
|
11616
|
+
}
|
|
11617
|
+
function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
|
|
11078
11618
|
const out = { ...vars };
|
|
11079
11619
|
out.REPO_NAME ??= parsed.name;
|
|
11080
11620
|
out.REPO_SLUG ??= parsed.slug;
|
|
11081
11621
|
out.CLASS ??= cls;
|
|
11082
11622
|
out.INSTALL_CMD ??= DEFAULT_INSTALL_CMD;
|
|
11623
|
+
const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
|
|
11624
|
+
for (const [key, value] of Object.entries(gateSeedVars(cls, track))) {
|
|
11625
|
+
out[key] ??= value;
|
|
11626
|
+
}
|
|
11083
11627
|
return out;
|
|
11084
11628
|
}
|
|
11085
11629
|
function planSeedAction(seed, exists) {
|
|
@@ -11189,8 +11733,8 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
11189
11733
|
class: cls,
|
|
11190
11734
|
projectType,
|
|
11191
11735
|
deployModel,
|
|
11192
|
-
// #
|
|
11193
|
-
releaseTrack:
|
|
11736
|
+
// #1359: always persist an explicit track so release tooling never guesses from absence alone.
|
|
11737
|
+
releaseTrack: resolveBootstrapReleaseTrack(cls, options.releaseTrack),
|
|
11194
11738
|
// Board coords (from GraphQL at bootstrap, passed as --var by the skill).
|
|
11195
11739
|
projectOwner: vars.PROJECT_OWNER || void 0,
|
|
11196
11740
|
projectNumber: num(vars.PROJECT_NUMBER),
|
|
@@ -11204,6 +11748,11 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
11204
11748
|
kbPointer: `kb/projects/${slug}.md`
|
|
11205
11749
|
};
|
|
11206
11750
|
for (const k of Object.keys(payload)) if (payload[k] === void 0) delete payload[k];
|
|
11751
|
+
if (payload.projectId && payload.projectNumber == null) {
|
|
11752
|
+
throw new Error(
|
|
11753
|
+
"bootstrap apply: PROJECT_ID is set but PROJECT_NUMBER is missing \u2014 pass --var PROJECT_NUMBER=<N> or ensure the live board GraphQL query succeeds"
|
|
11754
|
+
);
|
|
11755
|
+
}
|
|
11207
11756
|
return payload;
|
|
11208
11757
|
}
|
|
11209
11758
|
var BOARD_FIELD_VAR_MAP = {
|
|
@@ -11216,9 +11765,18 @@ var BOARD_FIELD_VAR_MAP = {
|
|
|
11216
11765
|
options: { Urgent: "PRIORITY_URGENT", High: "PRIORITY_HIGH", Medium: "PRIORITY_MEDIUM", Low: "PRIORITY_LOW" }
|
|
11217
11766
|
}
|
|
11218
11767
|
};
|
|
11768
|
+
function projectV2BoardNode(fieldsJson) {
|
|
11769
|
+
if (Array.isArray(fieldsJson)) return { fields: { nodes: fieldsJson } };
|
|
11770
|
+
const wrapped = fieldsJson;
|
|
11771
|
+
return wrapped?.data?.node ?? wrapped?.node;
|
|
11772
|
+
}
|
|
11219
11773
|
function extractBoardFieldVars(fieldsJson) {
|
|
11220
11774
|
const out = {};
|
|
11221
|
-
const
|
|
11775
|
+
const projectNode = projectV2BoardNode(fieldsJson);
|
|
11776
|
+
if (typeof projectNode?.number === "number" && Number.isFinite(projectNode.number)) {
|
|
11777
|
+
out.PROJECT_NUMBER = String(projectNode.number);
|
|
11778
|
+
}
|
|
11779
|
+
const nodes = projectNode?.fields?.nodes;
|
|
11222
11780
|
if (!Array.isArray(nodes)) return out;
|
|
11223
11781
|
for (const node of nodes) {
|
|
11224
11782
|
const field = node;
|
|
@@ -11236,7 +11794,7 @@ function extractBoardFieldVars(fieldsJson) {
|
|
|
11236
11794
|
return out;
|
|
11237
11795
|
}
|
|
11238
11796
|
function boardFieldsQueryArgs(projectId) {
|
|
11239
|
-
const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
|
|
11797
|
+
const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
|
|
11240
11798
|
return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
|
|
11241
11799
|
}
|
|
11242
11800
|
function serializeRegistry(obj) {
|
|
@@ -11481,6 +12039,14 @@ function dnsErrorToResolution(code) {
|
|
|
11481
12039
|
}
|
|
11482
12040
|
var STAGES = ["dev", "rc", "main"];
|
|
11483
12041
|
var DEFAULT_RUNTIME_SECRET_NAMES2 = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
12042
|
+
function boardRegistryGaps(meta) {
|
|
12043
|
+
if (!meta?.projectId) return [];
|
|
12044
|
+
if (meta.projectNumber != null) return [];
|
|
12045
|
+
return ["projectNumber"];
|
|
12046
|
+
}
|
|
12047
|
+
function boardRegistryGapMessage(repo) {
|
|
12048
|
+
return `Board META incomplete for ${repo}: registry has projectId but no projectNumber \u2014 board claim and auto-add will fail until projectNumber is backfilled (re-run \`node infra/migrate/seed-registry.mjs\` or \`mmi-cli bootstrap apply --execute\` with board vars)`;
|
|
12049
|
+
}
|
|
11484
12050
|
function slugOfRepo(repoOrSlug) {
|
|
11485
12051
|
return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
|
|
11486
12052
|
}
|
|
@@ -11679,6 +12245,7 @@ function buildV2HealPatch(repoOrSlug, meta) {
|
|
|
11679
12245
|
}
|
|
11680
12246
|
}
|
|
11681
12247
|
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
12248
|
+
if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
|
|
11682
12249
|
return { slug, patch, appOwnedGaps };
|
|
11683
12250
|
}
|
|
11684
12251
|
async function runV2Heal(repoOrSlug, opts, deps) {
|
|
@@ -11768,7 +12335,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
11768
12335
|
const missing = required.filter((key) => !presentSecrets.has(key));
|
|
11769
12336
|
return [stage2, { required, present, missing }];
|
|
11770
12337
|
}));
|
|
11771
|
-
const metaMissing = ["class", "projectType", "deployModel", "vaultPath", "kbPointer"].filter((key) => meta[key] === void 0);
|
|
12338
|
+
const metaMissing = ["class", "projectType", "deployModel", "vaultPath", "kbPointer"].filter((key) => meta[key] === void 0).concat(boardRegistryGaps(meta));
|
|
11772
12339
|
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets2).every((v) => v.missing.length === 0);
|
|
11773
12340
|
const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
|
|
11774
12341
|
return {
|
|
@@ -12489,9 +13056,13 @@ async function relevantPlans(deps, signals, opts = {}) {
|
|
|
12489
13056
|
deps.err(`northstar relevant: ${e.message}`);
|
|
12490
13057
|
return;
|
|
12491
13058
|
}
|
|
12492
|
-
if (!plans.length)
|
|
13059
|
+
if (!plans.length) {
|
|
13060
|
+
if (opts.json) return deps.log(JSON.stringify({ ranked: [] }));
|
|
13061
|
+
return deps.log("no North Stars for this repo yet");
|
|
13062
|
+
}
|
|
12493
13063
|
const ranked = rankPlansByRelevance(plans, signals, { includeAll: opts.includeAll });
|
|
12494
13064
|
const top = (opts.includeAll ? ranked : ranked.filter((r) => r.score > 0)).slice(0, opts.limit ?? 5);
|
|
13065
|
+
if (opts.json) return deps.log(JSON.stringify({ ranked: top }));
|
|
12495
13066
|
if (!top.length) {
|
|
12496
13067
|
return deps.log(`no task-relevant North Stars among ${plans.length} for this repo \u2014 \`mmi-cli northstar relevant --all\` lists recent ones`);
|
|
12497
13068
|
}
|
|
@@ -12573,9 +13144,14 @@ async function planSync(deps, opts = {}) {
|
|
|
12573
13144
|
if (!opts.quiet) deps.err(`northstar sync: list refresh failed: ${e.message}`);
|
|
12574
13145
|
}
|
|
12575
13146
|
}
|
|
12576
|
-
async function planStatus(deps) {
|
|
13147
|
+
async function planStatus(deps, opts = {}) {
|
|
12577
13148
|
const queue = parseQueue(deps.readQueueRaw());
|
|
12578
13149
|
const idx = parseIndex(deps.readIndexRaw());
|
|
13150
|
+
if (opts.json) {
|
|
13151
|
+
deps.log(JSON.stringify({ queue, index: idx }));
|
|
13152
|
+
if (queue.some((e) => e.conflict || e.deadLettered)) process.exitCode = 1;
|
|
13153
|
+
return;
|
|
13154
|
+
}
|
|
12579
13155
|
for (const e of queue) {
|
|
12580
13156
|
if (e.conflict) deps.err(`${e.slug} \xB7 CONFLICT \u2014 ${e.conflict}`);
|
|
12581
13157
|
else if (e.deadLettered) deps.err(`${e.slug} \xB7 DEAD-LETTER \u2014 ${e.deadLettered}`);
|
|
@@ -13034,7 +13610,7 @@ async function runWhoami(io = consoleIo) {
|
|
|
13034
13610
|
io.log(JSON.stringify(report));
|
|
13035
13611
|
return report;
|
|
13036
13612
|
}
|
|
13037
|
-
program2.command("whoami").description('resolve the logged-in human: {login, source, sessionExpiresAt} JSON; source "unknown" (exit 0) when neither the Hub session nor gh can name them').action(async () => {
|
|
13613
|
+
program2.command("whoami").description('resolve the logged-in human: {login, source, sessionExpiresAt} JSON; source "unknown" (exit 0) when neither the Hub session nor gh can name them').option("--json", "machine-readable output (default)").action(async () => {
|
|
13038
13614
|
await runWhoami();
|
|
13039
13615
|
});
|
|
13040
13616
|
program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
@@ -13045,7 +13621,14 @@ program2.command("gc").description("dry-run cleanup for merged/closed PR branche
|
|
|
13045
13621
|
const plan2 = await gcPlan(o.remote, limit);
|
|
13046
13622
|
if (o.apply && !o.json) console.log(formatGcPlan(plan2, false));
|
|
13047
13623
|
let applyResult;
|
|
13048
|
-
if (o.apply)
|
|
13624
|
+
if (o.apply) {
|
|
13625
|
+
const deferredStore = await createDeferredWorktreeStore();
|
|
13626
|
+
await sweepDeferredWorktrees(
|
|
13627
|
+
deferredStore,
|
|
13628
|
+
worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout)
|
|
13629
|
+
).catch(() => void 0);
|
|
13630
|
+
applyResult = await applyGcPlan(plan2, o.remote);
|
|
13631
|
+
}
|
|
13049
13632
|
if (o.json) {
|
|
13050
13633
|
console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan: plan2, applyResult }, null, 2));
|
|
13051
13634
|
} else if (!o.apply) {
|
|
@@ -13310,12 +13893,12 @@ function registerNorthStarCommands(cmd) {
|
|
|
13310
13893
|
if (!ok) process.exitCode = 1;
|
|
13311
13894
|
}));
|
|
13312
13895
|
cmd.command("list").description("list your North Star plans (cross-device)").option("--project <name>", "filter by project").option("--json", "machine-readable output").option("--quiet", "silent when unconfigured/empty/unreachable (SessionStart hook)").option("--fresh", "bypass the local cache and read from the server").action((o) => withPlan(o.quiet ?? false, (d) => planList(d, o)));
|
|
13313
|
-
cmd.command("relevant").description("list North Stars relevant to your current task (branch + claimed issue + changed files)").option("--project <name>", "override the project key").option("--all", "include superseded/graduated and recent non-matching plans").option("--limit <n>", "max results (default 5)").option("--fresh", "bypass the local cache and read from the server").action((o) => withPlan(false, async (d) => {
|
|
13896
|
+
cmd.command("relevant").description("list North Stars relevant to your current task (branch + claimed issue + changed files)").option("--project <name>", "override the project key").option("--all", "include superseded/graduated and recent non-matching plans").option("--limit <n>", "max results (default 5)").option("--fresh", "bypass the local cache and read from the server").option("--json", "machine-readable output").action((o) => withPlan(false, async (d) => {
|
|
13314
13897
|
const signals = await gatherRelevanceSignals();
|
|
13315
|
-
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh });
|
|
13898
|
+
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
|
|
13316
13899
|
}));
|
|
13317
13900
|
cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").action((o) => withPlan(o.quiet ?? false, (d) => planSync(d, o)));
|
|
13318
|
-
cmd.command("status").description("show pending/conflicted background pushes and the plan-cache age").action(() => withPlan(false, (d) => planStatus(d)));
|
|
13901
|
+
cmd.command("status").description("show pending/conflicted background pushes and the plan-cache age").option("--json", "machine-readable output").action((o) => withPlan(false, (d) => planStatus(d, o)));
|
|
13319
13902
|
cmd.command("reconcile").description("refresh stale local etags from the server without --force (recovers from an object-store re-stamp)").action(() => withPlan(false, (d) => planReconcile(d)));
|
|
13320
13903
|
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
13321
13904
|
(slug, o) => withPlan(false, async (d) => {
|
|
@@ -13954,6 +14537,41 @@ verify.command("synthesize").description("merge lens JSON array into a PanelRepo
|
|
|
13954
14537
|
return fail(`verify synthesize: ${e.message}`);
|
|
13955
14538
|
}
|
|
13956
14539
|
});
|
|
14540
|
+
var fusion = verify.command("fusion").description("optional hosted fusion provider for grind verify (#1377)");
|
|
14541
|
+
fusion.command("plan").description("plan a hosted fusion job \u2014 print FusionPlan JSON (falls back to host panel when provider unset)").requiredOption("--criteria-file <path>", "UTF-8 file with success criteria").requiredOption("--diff-file <path>", "UTF-8 file with git diff output").option("--routing <routing>", "Balanced | Budget | Paranoid", "Balanced").option("--lenses <list>", `comma-separated lens names (default: ${GRIND_LENSES.join(",")})`, GRIND_LENSES.join(",")).option("--provider-url <url>", "fusion provider base URL (else MMI_FUSION_PROVIDER_URL)").option("--web-search", "enable bounded web search in fusion tool policy").action(async (o) => {
|
|
14542
|
+
try {
|
|
14543
|
+
const routing = parseFusionRouting(o.routing);
|
|
14544
|
+
const lenses = parseFusionLenses(o.lenses);
|
|
14545
|
+
const criteria = await (0, import_promises5.readFile)(o.criteriaFile, "utf8");
|
|
14546
|
+
const diff = await (0, import_promises5.readFile)(o.diffFile, "utf8");
|
|
14547
|
+
const plan2 = buildFusionPlan({
|
|
14548
|
+
routing,
|
|
14549
|
+
lenses,
|
|
14550
|
+
criteria,
|
|
14551
|
+
diff,
|
|
14552
|
+
providerUrl: o.providerUrl ?? null,
|
|
14553
|
+
toolPolicy: {
|
|
14554
|
+
webSearch: Boolean(o.webSearch),
|
|
14555
|
+
maxQueriesPerLens: 3,
|
|
14556
|
+
denyDomains: [...DEFAULT_SEARCH_DENY_DOMAINS]
|
|
14557
|
+
}
|
|
14558
|
+
});
|
|
14559
|
+
console.log(JSON.stringify(plan2));
|
|
14560
|
+
} catch (e) {
|
|
14561
|
+
return fail(`verify fusion plan: ${e.message}`);
|
|
14562
|
+
}
|
|
14563
|
+
});
|
|
14564
|
+
fusion.command("run").description("execute hosted fusion from a FusionPlan JSON file; prints PanelReport or fallback envelope").requiredOption("--plan-file <path>", "UTF-8 FusionPlan JSON from verify fusion plan").option("--provider-url <url>", "override fusion provider URL (else plan.provider or MMI_FUSION_PROVIDER_URL)").action(async (o) => {
|
|
14565
|
+
try {
|
|
14566
|
+
const raw = await (0, import_promises5.readFile)(o.planFile, "utf8");
|
|
14567
|
+
const plan2 = JSON.parse(raw);
|
|
14568
|
+
const result = await runFusionProvider(plan2, { providerUrl: o.providerUrl ?? plan2.provider ?? null });
|
|
14569
|
+
console.log(JSON.stringify(result));
|
|
14570
|
+
if (!result.ok) process.exitCode = 1;
|
|
14571
|
+
} catch (e) {
|
|
14572
|
+
return fail(`verify fusion run: ${e.message}`);
|
|
14573
|
+
}
|
|
14574
|
+
});
|
|
13957
14575
|
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
|
|
13958
14576
|
const targetRepo2 = o.repo ?? HUB_REPO;
|
|
13959
14577
|
const sourceRepo = await resolveRepo(void 0);
|
|
@@ -14026,12 +14644,43 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
14026
14644
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
14027
14645
|
console.log(JSON.stringify(created));
|
|
14028
14646
|
});
|
|
14029
|
-
async function
|
|
14647
|
+
async function remoteBranchExists2(branch, options = {}) {
|
|
14030
14648
|
return checkRemoteBranchExists(branch, {
|
|
14031
14649
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
14032
14650
|
}, options);
|
|
14033
14651
|
}
|
|
14034
14652
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
14653
|
+
async function createDeferredWorktreeStore() {
|
|
14654
|
+
try {
|
|
14655
|
+
const { stdout } = await execFileP2("git", ["rev-parse", "--git-dir"], { timeout: GIT_TIMEOUT_MS });
|
|
14656
|
+
const registryPath = deferredWorktreesRegistryPath(stdout.trim());
|
|
14657
|
+
return {
|
|
14658
|
+
read: async () => {
|
|
14659
|
+
try {
|
|
14660
|
+
return parseDeferredWorktreesFile(await (0, import_promises5.readFile)(registryPath, "utf8"));
|
|
14661
|
+
} catch {
|
|
14662
|
+
return [];
|
|
14663
|
+
}
|
|
14664
|
+
},
|
|
14665
|
+
write: async (entries) => {
|
|
14666
|
+
try {
|
|
14667
|
+
await (0, import_promises5.mkdir)((0, import_node_path13.dirname)(registryPath), { recursive: true });
|
|
14668
|
+
await (0, import_promises5.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
14669
|
+
} catch {
|
|
14670
|
+
}
|
|
14671
|
+
}
|
|
14672
|
+
};
|
|
14673
|
+
} catch {
|
|
14674
|
+
return void 0;
|
|
14675
|
+
}
|
|
14676
|
+
}
|
|
14677
|
+
function worktreeRemoveDeps(execGit) {
|
|
14678
|
+
return {
|
|
14679
|
+
git: execGit,
|
|
14680
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
14681
|
+
removeWorktreeDir: async (worktreePath) => (0, import_promises5.rm)(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
14682
|
+
};
|
|
14683
|
+
}
|
|
14035
14684
|
function teardownWorktreeStage(worktreePath) {
|
|
14036
14685
|
return runWorktreeStageTeardown(worktreePath, {
|
|
14037
14686
|
hasStageState: (wt) => (0, import_node_fs14.existsSync)(stageStatePath(wt)),
|
|
@@ -14053,7 +14702,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
14053
14702
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
14054
14703
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
14055
14704
|
);
|
|
14056
|
-
const remoteBefore = repoArgs.length ? void 0 : await
|
|
14705
|
+
const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists2(headRef);
|
|
14057
14706
|
let remoteDeleteAttempted = false;
|
|
14058
14707
|
let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
|
|
14059
14708
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
|
|
@@ -14081,26 +14730,39 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
14081
14730
|
attempted: false,
|
|
14082
14731
|
reason: remoteNotAttemptedReason
|
|
14083
14732
|
}) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
14084
|
-
exists:
|
|
14733
|
+
exists: remoteBranchExists2
|
|
14085
14734
|
}, {
|
|
14086
14735
|
attempted: remoteDeleteAttempted,
|
|
14087
14736
|
existedBefore: remoteBefore,
|
|
14088
14737
|
reason: remoteNotAttemptedReason
|
|
14089
14738
|
});
|
|
14090
|
-
const
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14739
|
+
const deferredStore = await createDeferredWorktreeStore();
|
|
14740
|
+
let localCleanup;
|
|
14741
|
+
try {
|
|
14742
|
+
localCleanup = repoArgs.length ? {
|
|
14743
|
+
branch: headRef,
|
|
14744
|
+
localBranch: { name: headRef, status: "not-attempted", reason: "repo-option" },
|
|
14745
|
+
worktree: void 0
|
|
14746
|
+
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
14747
|
+
beforeWorktrees,
|
|
14748
|
+
startingPath,
|
|
14749
|
+
pathExists: (p) => (0, import_node_fs14.existsSync)(p),
|
|
14750
|
+
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
14751
|
+
teardownWorktreeStage,
|
|
14752
|
+
deferredStore,
|
|
14753
|
+
removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir
|
|
14754
|
+
});
|
|
14755
|
+
} catch (e) {
|
|
14756
|
+
localCleanup = {
|
|
14757
|
+
branch: headRef,
|
|
14758
|
+
localBranch: {
|
|
14759
|
+
name: headRef,
|
|
14760
|
+
status: "failed",
|
|
14761
|
+
reason: "cleanup-exception",
|
|
14762
|
+
error: e instanceof Error ? e.message : String(e)
|
|
14763
|
+
}
|
|
14764
|
+
};
|
|
14765
|
+
}
|
|
14104
14766
|
console.log(JSON.stringify(buildPrMergeResultPayload({
|
|
14105
14767
|
number,
|
|
14106
14768
|
branch: headRef,
|
|
@@ -14567,10 +15229,11 @@ function renderHotfixStart(r) {
|
|
|
14567
15229
|
}
|
|
14568
15230
|
function renderHotfixRelease(r) {
|
|
14569
15231
|
return [
|
|
14570
|
-
`mmi-cli hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo}`,
|
|
15232
|
+
`mmi-cli hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo} (deployModel=${r.deployModel})`,
|
|
14571
15233
|
` - checks: ${r.checks}`,
|
|
14572
15234
|
` - ${r.tagNote}`,
|
|
14573
15235
|
` - ${r.releaseNote}`,
|
|
15236
|
+
` - deploy: ${r.deployNote}`,
|
|
14574
15237
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
14575
15238
|
` - ${r.verifyNote}`,
|
|
14576
15239
|
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
@@ -14661,6 +15324,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
14661
15324
|
json: rawFlag("--json")
|
|
14662
15325
|
};
|
|
14663
15326
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap apply: --class must be deployable or content");
|
|
15327
|
+
const bootstrapReleaseTrack = resolveBootstrapReleaseTrack(o.class, o.releaseTrack || void 0);
|
|
14664
15328
|
let parsedRepo;
|
|
14665
15329
|
try {
|
|
14666
15330
|
parsedRepo = parseOwnerRepo(repo);
|
|
@@ -14680,7 +15344,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
14680
15344
|
const eq = value.indexOf("=");
|
|
14681
15345
|
if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
14682
15346
|
}
|
|
14683
|
-
const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class);
|
|
15347
|
+
const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack);
|
|
14684
15348
|
if (vars.PROJECT_ID) {
|
|
14685
15349
|
try {
|
|
14686
15350
|
const r = await gh(boardFieldsQueryArgs(vars.PROJECT_ID));
|
|
@@ -14748,7 +15412,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
14748
15412
|
registerPayload = buildRegisterPayload(repo, o.class, vars, {
|
|
14749
15413
|
projectType: o.projectType || void 0,
|
|
14750
15414
|
deployModel: o.deployModel || void 0,
|
|
14751
|
-
releaseTrack:
|
|
15415
|
+
releaseTrack: bootstrapReleaseTrack
|
|
14752
15416
|
});
|
|
14753
15417
|
} catch (e) {
|
|
14754
15418
|
return fail(`bootstrap apply: ${e.message}`);
|
|
@@ -14975,6 +15639,7 @@ function cursorPluginCachePinSnapshots() {
|
|
|
14975
15639
|
const path2 = (0, import_node_path13.join)(root, entry.name);
|
|
14976
15640
|
const pluginJson = (0, import_node_path13.join)(path2, ".cursor-plugin", "plugin.json");
|
|
14977
15641
|
const hooksJson = (0, import_node_path13.join)(path2, "hooks", "hooks.json");
|
|
15642
|
+
const cliBundle = (0, import_node_path13.join)(path2, "cli", "dist", "index.cjs");
|
|
14978
15643
|
let isEmpty = true;
|
|
14979
15644
|
try {
|
|
14980
15645
|
isEmpty = (0, import_node_fs14.readdirSync)(path2).length === 0;
|
|
@@ -14986,6 +15651,7 @@ function cursorPluginCachePinSnapshots() {
|
|
|
14986
15651
|
path: path2,
|
|
14987
15652
|
hasPluginJson: (0, import_node_fs14.existsSync)(pluginJson),
|
|
14988
15653
|
hasHooksJson: (0, import_node_fs14.existsSync)(hooksJson),
|
|
15654
|
+
hasCliBundle: (0, import_node_fs14.existsSync)(cliBundle),
|
|
14989
15655
|
isEmpty
|
|
14990
15656
|
};
|
|
14991
15657
|
});
|
|
@@ -15050,6 +15716,39 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
15050
15716
|
return moved;
|
|
15051
15717
|
}
|
|
15052
15718
|
var gitignorePath = () => (0, import_node_path13.join)(process.cwd(), ".gitignore");
|
|
15719
|
+
function readTextFile(path2) {
|
|
15720
|
+
try {
|
|
15721
|
+
if (!(0, import_node_fs14.existsSync)(path2)) return null;
|
|
15722
|
+
return (0, import_node_fs14.readFileSync)(path2, "utf8");
|
|
15723
|
+
} catch {
|
|
15724
|
+
return null;
|
|
15725
|
+
}
|
|
15726
|
+
}
|
|
15727
|
+
function playwrightMcpConfigSnapshots() {
|
|
15728
|
+
const cwd = process.cwd();
|
|
15729
|
+
const home = (0, import_node_os4.homedir)();
|
|
15730
|
+
const candidates = [
|
|
15731
|
+
(0, import_node_path13.join)(cwd, ".cursor", "mcp.json"),
|
|
15732
|
+
(0, import_node_path13.join)(home, ".cursor", "mcp.json"),
|
|
15733
|
+
(0, import_node_path13.join)(home, ".codex", "config.toml")
|
|
15734
|
+
];
|
|
15735
|
+
const out = [];
|
|
15736
|
+
for (const path2 of candidates) {
|
|
15737
|
+
const content = readTextFile(path2);
|
|
15738
|
+
if (content != null) out.push({ path: path2, content });
|
|
15739
|
+
}
|
|
15740
|
+
return out;
|
|
15741
|
+
}
|
|
15742
|
+
function strayBrowserArtifactPaths() {
|
|
15743
|
+
const cwd = process.cwd();
|
|
15744
|
+
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
15745
|
+
try {
|
|
15746
|
+
return (0, import_node_fs14.existsSync)((0, import_node_path13.join)(cwd, rel));
|
|
15747
|
+
} catch {
|
|
15748
|
+
return false;
|
|
15749
|
+
}
|
|
15750
|
+
});
|
|
15751
|
+
}
|
|
15053
15752
|
function readGitignore() {
|
|
15054
15753
|
try {
|
|
15055
15754
|
return (0, import_node_fs14.readFileSync)(gitignorePath(), "utf8");
|
|
@@ -15237,6 +15936,27 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
15237
15936
|
hubCheckout: hubCheckoutForCursorSeed()
|
|
15238
15937
|
})
|
|
15239
15938
|
);
|
|
15939
|
+
const cursorPins = cursorPluginCachePinSnapshots() ?? [];
|
|
15940
|
+
checks.push(
|
|
15941
|
+
buildCursorHookCliCheck({
|
|
15942
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
15943
|
+
surface,
|
|
15944
|
+
pins: cursorPins,
|
|
15945
|
+
mmiCliOnPath: onPath
|
|
15946
|
+
})
|
|
15947
|
+
);
|
|
15948
|
+
checks.push(
|
|
15949
|
+
buildPlaywrightMcpVisionCapCheck({
|
|
15950
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
15951
|
+
configs: playwrightMcpConfigSnapshots()
|
|
15952
|
+
})
|
|
15953
|
+
);
|
|
15954
|
+
checks.push(
|
|
15955
|
+
buildBrowserArtifactsCheck({
|
|
15956
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
15957
|
+
strayPaths: strayBrowserArtifactPaths()
|
|
15958
|
+
})
|
|
15959
|
+
);
|
|
15240
15960
|
const gaps = checks.filter((c) => !c.ok);
|
|
15241
15961
|
if (opts.banner) {
|
|
15242
15962
|
if (gaps.length) io.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
|
|
@@ -15265,7 +15985,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
15265
15985
|
io.log(gaps.length ? `
|
|
15266
15986
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
15267
15987
|
}
|
|
15268
|
-
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
|
|
15988
|
+
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install, Playwright MCP vision caps, browser artifact hygiene), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
|
|
15269
15989
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
15270
15990
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
15271
15991
|
));
|