@mutmutco/cli 2.39.0 → 2.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +2119 -1063
- package/dist/saga.cjs +4 -5
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3391,8 +3391,8 @@ function useColor() {
|
|
|
3391
3391
|
var program = new Command();
|
|
3392
3392
|
|
|
3393
3393
|
// src/index.ts
|
|
3394
|
-
var
|
|
3395
|
-
var
|
|
3394
|
+
var import_promises7 = require("node:fs/promises");
|
|
3395
|
+
var import_node_fs22 = require("node:fs");
|
|
3396
3396
|
|
|
3397
3397
|
// src/rules-sync.ts
|
|
3398
3398
|
function normalizeEol(s) {
|
|
@@ -3423,7 +3423,7 @@ function resolveRulesBase(orgRulesSource, defaultBase) {
|
|
|
3423
3423
|
}
|
|
3424
3424
|
|
|
3425
3425
|
// src/index.ts
|
|
3426
|
-
var
|
|
3426
|
+
var import_node_child_process12 = require("node:child_process");
|
|
3427
3427
|
|
|
3428
3428
|
// src/cli-shared.ts
|
|
3429
3429
|
var import_promises = require("node:fs/promises");
|
|
@@ -3509,7 +3509,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3509
3509
|
const attempts = opts.attempts ?? 3;
|
|
3510
3510
|
const baseDelayMs = opts.baseDelayMs ?? 250;
|
|
3511
3511
|
const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
|
|
3512
|
-
const
|
|
3512
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
3513
3513
|
let lastErr;
|
|
3514
3514
|
for (let i = 0; i < attempts; i++) {
|
|
3515
3515
|
const isLast = i === attempts - 1;
|
|
@@ -3517,26 +3517,25 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3517
3517
|
try {
|
|
3518
3518
|
const res = await fetchImpl(url, attemptInit);
|
|
3519
3519
|
if (!isLast && retryOn(res)) {
|
|
3520
|
-
await
|
|
3520
|
+
await sleep2(baseDelayMs * 2 ** i);
|
|
3521
3521
|
continue;
|
|
3522
3522
|
}
|
|
3523
3523
|
return res;
|
|
3524
3524
|
} catch (e) {
|
|
3525
3525
|
lastErr = e;
|
|
3526
3526
|
if (isLast) throw e;
|
|
3527
|
-
await
|
|
3527
|
+
await sleep2(baseDelayMs * 2 ** i);
|
|
3528
3528
|
}
|
|
3529
3529
|
}
|
|
3530
3530
|
throw lastErr;
|
|
3531
3531
|
}
|
|
3532
3532
|
|
|
3533
3533
|
// src/clean-exit.ts
|
|
3534
|
+
var UNDICI_GLOBAL_DISPATCHER_SYMBOL = Object.getOwnPropertySymbols(globalThis).find(
|
|
3535
|
+
(s) => s.description === "undici.globalDispatcher.1" || s.description?.startsWith("undici.globalDispatcher.")
|
|
3536
|
+
) ?? /* @__PURE__ */ Symbol.for("undici.globalDispatcher.1");
|
|
3534
3537
|
function globalDispatcher() {
|
|
3535
|
-
|
|
3536
|
-
const sym = Object.getOwnPropertySymbols(g).find(
|
|
3537
|
-
(s) => s.description === "undici.globalDispatcher.1" || s.description?.startsWith("undici.globalDispatcher.")
|
|
3538
|
-
);
|
|
3539
|
-
return sym ? g[sym] : void 0;
|
|
3538
|
+
return globalThis[UNDICI_GLOBAL_DISPATCHER_SYMBOL];
|
|
3540
3539
|
}
|
|
3541
3540
|
function destroyHttpPool() {
|
|
3542
3541
|
try {
|
|
@@ -4361,15 +4360,18 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
4361
4360
|
function cursorProjectsRoot(env) {
|
|
4362
4361
|
return (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".cursor", "projects");
|
|
4363
4362
|
}
|
|
4364
|
-
function
|
|
4363
|
+
function claudeTranscriptCandidate(hook, env = process.env) {
|
|
4365
4364
|
const sessionId = hook.session_id?.trim();
|
|
4366
4365
|
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return void 0;
|
|
4367
4366
|
const cwd = hook.cwd?.trim() || env.CLAUDE_PROJECT_DIR?.trim();
|
|
4368
4367
|
if (!cwd) return void 0;
|
|
4369
4368
|
const encoded = cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
4370
4369
|
const root = (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".claude", "projects");
|
|
4371
|
-
|
|
4372
|
-
|
|
4370
|
+
return (0, import_node_path6.join)(root, encoded, `${sessionId}.jsonl`);
|
|
4371
|
+
}
|
|
4372
|
+
function resolveClaudeTranscriptPath(hook, env = process.env) {
|
|
4373
|
+
const candidate = claudeTranscriptCandidate(hook, env);
|
|
4374
|
+
return candidate && (0, import_node_fs8.existsSync)(candidate) ? candidate : void 0;
|
|
4373
4375
|
}
|
|
4374
4376
|
function resolveCursorTranscriptPath(hook, env = process.env) {
|
|
4375
4377
|
const conversationId = (hook.conversation_id ?? hook.conversationId ?? hook.session_id)?.trim();
|
|
@@ -5451,9 +5453,16 @@ function clearIngestSkip() {
|
|
|
5451
5453
|
} catch {
|
|
5452
5454
|
}
|
|
5453
5455
|
}
|
|
5456
|
+
var INGEST_SKIP_REASON_MESSAGES = {
|
|
5457
|
+
"no-transcript": "no transcript path \u2014 hook payload lacks transcript_path and adapter found no file",
|
|
5458
|
+
"transcript-empty": "transcript had no user/assistant text",
|
|
5459
|
+
"transcript-unreadable": "transcript unreadable",
|
|
5460
|
+
"note-empty": "note source was empty"
|
|
5461
|
+
};
|
|
5454
5462
|
function formatIngestSkip(record) {
|
|
5455
5463
|
if (!record) return void 0;
|
|
5456
|
-
const
|
|
5464
|
+
const suffix = INGEST_SKIP_REASON_MESSAGES[record.reason] ?? record.reason;
|
|
5465
|
+
const base2 = `last ingest skipped (${record.surface}): ${suffix}`;
|
|
5457
5466
|
return record.detail ? `${base2} \u2014 ${record.detail}` : base2;
|
|
5458
5467
|
}
|
|
5459
5468
|
|
|
@@ -5630,8 +5639,54 @@ async function secretsList(deps, opts) {
|
|
|
5630
5639
|
deps.err(await upgradeMessage(res) ?? `secrets list failed: HTTP ${res.status}${await readErr(res)}`);
|
|
5631
5640
|
return;
|
|
5632
5641
|
}
|
|
5633
|
-
const { secrets
|
|
5634
|
-
deps.log(formatSecretList(
|
|
5642
|
+
const { secrets } = await res.json();
|
|
5643
|
+
deps.log(formatSecretList(secrets ?? []));
|
|
5644
|
+
}
|
|
5645
|
+
function formatCapabilities(r) {
|
|
5646
|
+
const head = `@${r.login} on ${r.repo} \u2014 ${r.role}`;
|
|
5647
|
+
const items = [...r.capabilities ?? []].sort((a, b) => a.scope.localeCompare(b.scope));
|
|
5648
|
+
if (!items.length) return `${head}
|
|
5649
|
+
|
|
5650
|
+
no vault credentials visible`;
|
|
5651
|
+
const width = Math.max(...items.map((i) => i.scope.length));
|
|
5652
|
+
const lines = items.map(
|
|
5653
|
+
(i) => `${i.accessible ? "+" : " "} ${i.scope.padEnd(width)} ${i.tier.padEnd(7)} ${i.reason}`
|
|
5654
|
+
);
|
|
5655
|
+
const reachable = items.filter((i) => i.accessible).length;
|
|
5656
|
+
return [
|
|
5657
|
+
head,
|
|
5658
|
+
"",
|
|
5659
|
+
`${reachable}/${items.length} vault credential(s) reachable (names + tier + scope \u2014 values are never shown):`,
|
|
5660
|
+
"",
|
|
5661
|
+
...lines,
|
|
5662
|
+
"",
|
|
5663
|
+
"+ = readable/usable now. Read one value: `mmi-cli secrets get <stage>/<KEY>` (only `get` prints a value)."
|
|
5664
|
+
].join("\n");
|
|
5665
|
+
}
|
|
5666
|
+
async function secretsCapabilities(deps, opts) {
|
|
5667
|
+
const repo = await targetRepo(deps, opts);
|
|
5668
|
+
const qs = new URLSearchParams({ repo }).toString();
|
|
5669
|
+
let res;
|
|
5670
|
+
try {
|
|
5671
|
+
res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
|
|
5672
|
+
method: "GET",
|
|
5673
|
+
headers: await deps.headers(),
|
|
5674
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
5675
|
+
});
|
|
5676
|
+
} catch (e) {
|
|
5677
|
+
deps.err(`access capabilities: ${e.message}`);
|
|
5678
|
+
return;
|
|
5679
|
+
}
|
|
5680
|
+
if (!res.ok) {
|
|
5681
|
+
deps.err(await upgradeMessage(res) ?? `access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
|
|
5682
|
+
return;
|
|
5683
|
+
}
|
|
5684
|
+
const report = await res.json();
|
|
5685
|
+
if (opts.json) {
|
|
5686
|
+
deps.log(JSON.stringify(report, null, 2));
|
|
5687
|
+
return;
|
|
5688
|
+
}
|
|
5689
|
+
deps.log(formatCapabilities(report));
|
|
5635
5690
|
}
|
|
5636
5691
|
var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
5637
5692
|
function stringList(v) {
|
|
@@ -5663,8 +5718,8 @@ async function secretsPreflight(deps, opts) {
|
|
|
5663
5718
|
deps.err(await upgradeMessage(res) ?? `secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
|
|
5664
5719
|
return false;
|
|
5665
5720
|
}
|
|
5666
|
-
const { secrets
|
|
5667
|
-
const present = new Set((
|
|
5721
|
+
const { secrets } = await res.json();
|
|
5722
|
+
const present = new Set((secrets ?? []).map((s) => s.key));
|
|
5668
5723
|
const required = opts.required.map((key) => stageKey(opts.stage, key));
|
|
5669
5724
|
const missing = required.filter((key) => !present.has(key));
|
|
5670
5725
|
if (missing.length) {
|
|
@@ -6104,9 +6159,16 @@ function parseHonchoQueueStatus(json) {
|
|
|
6104
6159
|
const inProgressRaw = o.in_progress_work_units;
|
|
6105
6160
|
const pending = typeof pendingRaw === "number" ? pendingRaw : null;
|
|
6106
6161
|
const inProgress = typeof inProgressRaw === "number" ? inProgressRaw : null;
|
|
6107
|
-
const stalled = (pending
|
|
6162
|
+
const stalled = isDeriverStalledSnapshot(pending, inProgress);
|
|
6108
6163
|
return { pending, inProgress, stalled };
|
|
6109
6164
|
}
|
|
6165
|
+
function isDeriverStalledSnapshot(pending, inProgress) {
|
|
6166
|
+
return (pending ?? 0) > 0 && (inProgress ?? 0) === 0;
|
|
6167
|
+
}
|
|
6168
|
+
var DERIVER_STALL_CONFIRM_MS = 750;
|
|
6169
|
+
function sleep(ms) {
|
|
6170
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6171
|
+
}
|
|
6110
6172
|
var enc = encodeURIComponent;
|
|
6111
6173
|
var base = (apiUrl) => apiUrl.replace(/\/+$/, "");
|
|
6112
6174
|
var honchoRoutes = {
|
|
@@ -6131,8 +6193,9 @@ async function request(cfg, fetchImpl, method, path2, body, timeoutMs) {
|
|
|
6131
6193
|
signal: AbortSignal.timeout(timeoutMs)
|
|
6132
6194
|
});
|
|
6133
6195
|
}
|
|
6196
|
+
var HONCHO_CORRELATION_MARKER = /\bHONCHO_\d/;
|
|
6134
6197
|
function formatPeerCardLines(lines, maxChars = DEFAULT_HONCHO_CARD_MAX_CHARS) {
|
|
6135
|
-
const card = Array.isArray(lines) ? lines.filter((s) => typeof s === "string" && s.trim().length > 0).join("\n").trim() : "";
|
|
6198
|
+
const card = Array.isArray(lines) ? lines.filter((s) => typeof s === "string" && s.trim().length > 0 && !HONCHO_CORRELATION_MARKER.test(s)).join("\n").trim() : "";
|
|
6136
6199
|
if (!card) return null;
|
|
6137
6200
|
return capContent(card, maxChars);
|
|
6138
6201
|
}
|
|
@@ -6239,6 +6302,23 @@ async function probeHoncho(cfg, fetchImpl = fetch, timeoutMs = 3e3, opts = {}) {
|
|
|
6239
6302
|
}
|
|
6240
6303
|
}
|
|
6241
6304
|
}
|
|
6305
|
+
if (queue.stalled) {
|
|
6306
|
+
await sleep(DERIVER_STALL_CONFIRM_MS);
|
|
6307
|
+
const confirmRes = await request(
|
|
6308
|
+
cfg,
|
|
6309
|
+
fetchImpl,
|
|
6310
|
+
"GET",
|
|
6311
|
+
honchoRoutes.queueStatus(cfg.workspace),
|
|
6312
|
+
void 0,
|
|
6313
|
+
timeoutMs
|
|
6314
|
+
);
|
|
6315
|
+
if (confirmRes.ok) {
|
|
6316
|
+
try {
|
|
6317
|
+
queue = parseHonchoQueueStatus(await confirmRes.json());
|
|
6318
|
+
} catch {
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6242
6322
|
return { reachable: true, status: healthRes.status, authOk, authStatus: authRes.status, queue };
|
|
6243
6323
|
} catch {
|
|
6244
6324
|
return { reachable: false, authOk: false, queue: emptyQueue };
|
|
@@ -6351,11 +6431,14 @@ async function runHonchoIngest(opts) {
|
|
|
6351
6431
|
}
|
|
6352
6432
|
}
|
|
6353
6433
|
if (!messages.length) {
|
|
6434
|
+
const hint = ingestTranscriptFallbackHint(surface);
|
|
6435
|
+
const tried = !transcriptPath && surface !== "cursor" ? claudeTranscriptCandidate(hook) : void 0;
|
|
6436
|
+
const detail = [hint, tried ? `tried: ${tried}` : void 0].filter(Boolean).join(" \u2014 ") || void 0;
|
|
6354
6437
|
recordIngestSkip({
|
|
6355
6438
|
reason: transcriptPath ? "transcript-empty" : "no-transcript",
|
|
6356
6439
|
surface,
|
|
6357
6440
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6358
|
-
detail
|
|
6441
|
+
detail
|
|
6359
6442
|
});
|
|
6360
6443
|
return;
|
|
6361
6444
|
}
|
|
@@ -6571,9 +6654,24 @@ function registerHonchoCommands(program3) {
|
|
|
6571
6654
|
}
|
|
6572
6655
|
|
|
6573
6656
|
// src/throttle-commands.ts
|
|
6657
|
+
var import_node_child_process6 = require("node:child_process");
|
|
6574
6658
|
var import_node_fs11 = require("node:fs");
|
|
6575
6659
|
var import_node_path9 = require("node:path");
|
|
6576
|
-
var
|
|
6660
|
+
var THROTTLE_TRACE_REL = (0, import_node_path9.join)(".mmi", "throttle", "trace.jsonl");
|
|
6661
|
+
function resolveRepoGitRoot(cwd = process.cwd()) {
|
|
6662
|
+
try {
|
|
6663
|
+
const root = (0, import_node_child_process6.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
6664
|
+
encoding: "utf8",
|
|
6665
|
+
timeout: 5e3
|
|
6666
|
+
}).trim();
|
|
6667
|
+
return root || cwd;
|
|
6668
|
+
} catch {
|
|
6669
|
+
return cwd;
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
function resolveThrottleTracePath(cwd = process.cwd()) {
|
|
6673
|
+
return (0, import_node_path9.join)(resolveRepoGitRoot(cwd), THROTTLE_TRACE_REL);
|
|
6674
|
+
}
|
|
6577
6675
|
function resolveModeFromEnv() {
|
|
6578
6676
|
const v = String(process.env.MMI_THROTTLE_MODE ?? "block").trim().toLowerCase();
|
|
6579
6677
|
if (v === "observe") return "observe";
|
|
@@ -6615,7 +6713,7 @@ function summarizeTrace(entries) {
|
|
|
6615
6713
|
}
|
|
6616
6714
|
return { denials, readBytesWouldBlock, byTool, byReason, bySurface, byMode };
|
|
6617
6715
|
}
|
|
6618
|
-
function runThrottleReport(io, tracePath =
|
|
6716
|
+
function runThrottleReport(io, tracePath = resolveThrottleTracePath()) {
|
|
6619
6717
|
const mode = resolveModeFromEnv();
|
|
6620
6718
|
if (!(0, import_node_fs11.existsSync)(tracePath)) {
|
|
6621
6719
|
io.log(`Throttle: no trace at ${tracePath} (gates have not denied anything yet).`);
|
|
@@ -6840,7 +6938,7 @@ function applyScratchGc(plan2, mmiRoot, now = Date.now()) {
|
|
|
6840
6938
|
function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
6841
6939
|
const readdir = deps.readdir ?? import_node_fs12.readdirSync;
|
|
6842
6940
|
const stat = deps.stat ?? import_node_fs12.statSync;
|
|
6843
|
-
const
|
|
6941
|
+
const readFile7 = deps.readFile ?? import_node_fs12.readFileSync;
|
|
6844
6942
|
const mmiRoot = (0, import_node_path10.join)(repoRoot, ".mmi");
|
|
6845
6943
|
const plansRoot = (0, import_node_path10.join)(repoRoot, "plans");
|
|
6846
6944
|
const mmiFiles = [];
|
|
@@ -6873,7 +6971,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
6873
6971
|
let syncQueueSlugs = null;
|
|
6874
6972
|
let queueRaw;
|
|
6875
6973
|
try {
|
|
6876
|
-
queueRaw =
|
|
6974
|
+
queueRaw = readFile7((0, import_node_path10.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
6877
6975
|
} catch {
|
|
6878
6976
|
syncQueueSlugs = /* @__PURE__ */ new Set();
|
|
6879
6977
|
}
|
|
@@ -6995,7 +7093,7 @@ function northstarPointer(injected = false) {
|
|
|
6995
7093
|
}
|
|
6996
7094
|
|
|
6997
7095
|
// src/board.ts
|
|
6998
|
-
var
|
|
7096
|
+
var import_node_child_process7 = require("node:child_process");
|
|
6999
7097
|
var import_node_util5 = require("node:util");
|
|
7000
7098
|
|
|
7001
7099
|
// src/board-priority.ts
|
|
@@ -7103,7 +7201,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
|
7103
7201
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
7104
7202
|
|
|
7105
7203
|
// src/board.ts
|
|
7106
|
-
var rawExecFileP3 = (0, import_node_util5.promisify)(
|
|
7204
|
+
var rawExecFileP3 = (0, import_node_util5.promisify)(import_node_child_process7.execFile);
|
|
7107
7205
|
var BOARD_GIT_TIMEOUT_MS = 1e4;
|
|
7108
7206
|
var WRITE_PROBE_CONCURRENCY = 8;
|
|
7109
7207
|
var CLAIM_CONCURRENCY = 5;
|
|
@@ -8292,7 +8390,7 @@ async function runNorthstarContext(io, deps) {
|
|
|
8292
8390
|
}
|
|
8293
8391
|
|
|
8294
8392
|
// src/index.ts
|
|
8295
|
-
var
|
|
8393
|
+
var import_node_path19 = require("node:path");
|
|
8296
8394
|
|
|
8297
8395
|
// src/merge-ci-policy.ts
|
|
8298
8396
|
function resolveMergeCiPolicy(input) {
|
|
@@ -8363,31 +8461,49 @@ async function waitForPrChecks(deps) {
|
|
|
8363
8461
|
|
|
8364
8462
|
// src/bootstrap-ruleset.ts
|
|
8365
8463
|
var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
|
|
8366
|
-
var PRODUCT_GATE_CONTEXT = "gate";
|
|
8367
8464
|
function stripRulesetComment(raw) {
|
|
8368
8465
|
const parsed = JSON.parse(raw);
|
|
8369
8466
|
delete parsed._comment;
|
|
8370
8467
|
return parsed;
|
|
8371
8468
|
}
|
|
8372
|
-
function
|
|
8469
|
+
function rulesetRequiredContexts(ruleset) {
|
|
8470
|
+
const contexts = [];
|
|
8373
8471
|
for (const rule of ruleset.rules ?? []) {
|
|
8374
8472
|
if (rule.type !== "required_status_checks") continue;
|
|
8375
8473
|
for (const check of rule.parameters?.required_status_checks ?? []) {
|
|
8376
|
-
if (check.context
|
|
8474
|
+
if (check.context) contexts.push(check.context);
|
|
8377
8475
|
}
|
|
8378
8476
|
}
|
|
8379
|
-
return
|
|
8477
|
+
return contexts;
|
|
8478
|
+
}
|
|
8479
|
+
function patchRulesetRequiredContexts(body, contexts) {
|
|
8480
|
+
const sorted = [...new Set(contexts)].sort((a, b) => a.localeCompare(b));
|
|
8481
|
+
const rules2 = (body.rules ?? []).map((rule) => {
|
|
8482
|
+
const r = rule;
|
|
8483
|
+
if (r.type !== "required_status_checks") return rule;
|
|
8484
|
+
return {
|
|
8485
|
+
...r,
|
|
8486
|
+
parameters: {
|
|
8487
|
+
...r.parameters,
|
|
8488
|
+
strict_required_status_checks_policy: r.parameters?.strict_required_status_checks_policy ?? false,
|
|
8489
|
+
required_status_checks: sorted.map((context) => ({ context }))
|
|
8490
|
+
}
|
|
8491
|
+
};
|
|
8492
|
+
});
|
|
8493
|
+
return { ...body, rules: rules2 };
|
|
8380
8494
|
}
|
|
8381
8495
|
function findProductRuleset(rulesets) {
|
|
8382
8496
|
return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
|
|
8383
8497
|
}
|
|
8384
8498
|
async function activateProductRuleset(repo, rulesetBody, client) {
|
|
8499
|
+
const want = new Set(rulesetRequiredContexts({ rules: rulesetBody.rules }));
|
|
8385
8500
|
const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
|
|
8386
8501
|
const existing = findProductRuleset(list ?? []);
|
|
8387
8502
|
if (existing?.id != null) {
|
|
8388
8503
|
const detail = await client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
|
|
8389
|
-
|
|
8390
|
-
|
|
8504
|
+
const have = new Set(rulesetRequiredContexts(detail));
|
|
8505
|
+
if (detail.enforcement === "active" && have.size === want.size && [...want].every((c) => have.has(c))) {
|
|
8506
|
+
return { action: "skipped", detail: "active ruleset already matches required contexts" };
|
|
8391
8507
|
}
|
|
8392
8508
|
await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: rulesetBody, timeoutMs: 2e4 });
|
|
8393
8509
|
return { action: "updated", detail: `ruleset ${existing.id}` };
|
|
@@ -8396,6 +8512,75 @@ async function activateProductRuleset(repo, rulesetBody, client) {
|
|
|
8396
8512
|
return { action: "created" };
|
|
8397
8513
|
}
|
|
8398
8514
|
|
|
8515
|
+
// src/workflow-context.ts
|
|
8516
|
+
function parseWorkflowJobIds(yaml) {
|
|
8517
|
+
const lines = yaml.split(/\r?\n/);
|
|
8518
|
+
let inJobs = false;
|
|
8519
|
+
let jobIndent = -1;
|
|
8520
|
+
const ids = [];
|
|
8521
|
+
for (const line of lines) {
|
|
8522
|
+
if (!inJobs) {
|
|
8523
|
+
if (/^jobs:\s*$/.test(line)) inJobs = true;
|
|
8524
|
+
continue;
|
|
8525
|
+
}
|
|
8526
|
+
if (/^\S/.test(line) && !line.startsWith("#")) break;
|
|
8527
|
+
const trimmed = line.trim();
|
|
8528
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
8529
|
+
const match = line.match(/^(\s+)([A-Za-z0-9_-]+):\s*$/);
|
|
8530
|
+
if (!match) continue;
|
|
8531
|
+
const indent = match[1].length;
|
|
8532
|
+
if (jobIndent < 0) {
|
|
8533
|
+
jobIndent = indent;
|
|
8534
|
+
ids.push(match[2]);
|
|
8535
|
+
continue;
|
|
8536
|
+
}
|
|
8537
|
+
if (indent === jobIndent) ids.push(match[2]);
|
|
8538
|
+
else if (indent < jobIndent) break;
|
|
8539
|
+
}
|
|
8540
|
+
return ids;
|
|
8541
|
+
}
|
|
8542
|
+
function workflowTriggersPullRequest(yaml) {
|
|
8543
|
+
const onBlock = extractOnBlock(yaml);
|
|
8544
|
+
if (!onBlock) return false;
|
|
8545
|
+
return /\bpull_request\b/.test(onBlock);
|
|
8546
|
+
}
|
|
8547
|
+
function extractOnBlock(yaml) {
|
|
8548
|
+
const lines = yaml.split(/\r?\n/);
|
|
8549
|
+
let inOn = false;
|
|
8550
|
+
let onIndent = -1;
|
|
8551
|
+
const block = [];
|
|
8552
|
+
for (const line of lines) {
|
|
8553
|
+
if (!inOn) {
|
|
8554
|
+
if (/^on:\s*$/.test(line)) {
|
|
8555
|
+
inOn = true;
|
|
8556
|
+
onIndent = 0;
|
|
8557
|
+
} else if (/^on:\s+\S/.test(line)) {
|
|
8558
|
+
return line;
|
|
8559
|
+
}
|
|
8560
|
+
continue;
|
|
8561
|
+
}
|
|
8562
|
+
if (/^\S/.test(line) && !line.startsWith("#")) break;
|
|
8563
|
+
const match = line.match(/^(\s+)/);
|
|
8564
|
+
const indent = match ? match[1].length : 0;
|
|
8565
|
+
if (onIndent >= 0 && indent === 0 && line.trim()) break;
|
|
8566
|
+
block.push(line);
|
|
8567
|
+
}
|
|
8568
|
+
return block.length ? block.join("\n") : null;
|
|
8569
|
+
}
|
|
8570
|
+
function collectPullRequestWorkflowContexts(workflows) {
|
|
8571
|
+
const contexts = /* @__PURE__ */ new Set();
|
|
8572
|
+
for (const wf of workflows) {
|
|
8573
|
+
if (!workflowTriggersPullRequest(wf.body)) continue;
|
|
8574
|
+
for (const id of parseWorkflowJobIds(wf.body)) contexts.add(id);
|
|
8575
|
+
}
|
|
8576
|
+
return [...contexts].sort((a, b) => a.localeCompare(b));
|
|
8577
|
+
}
|
|
8578
|
+
function contextsMatchRuleset(required, emitted) {
|
|
8579
|
+
if (required.size !== emitted.size) return false;
|
|
8580
|
+
for (const c of required) if (!emitted.has(c)) return false;
|
|
8581
|
+
return true;
|
|
8582
|
+
}
|
|
8583
|
+
|
|
8399
8584
|
// src/bootstrap-seeds.ts
|
|
8400
8585
|
var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
8401
8586
|
function loadBootstrapSeeds(manifestJson) {
|
|
@@ -8435,6 +8620,8 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
8435
8620
|
'# Org-wide cleanliness (AGENTS.md "Repo cleanliness") \u2014 enforced by `mmi-cli doctor`.',
|
|
8436
8621
|
"# Do not edit inside these markers; this block is regenerated on the next doctor run.",
|
|
8437
8622
|
"/tmp/",
|
|
8623
|
+
// Ad-hoc agent scratch at repo root (e.g. tmp_diff.txt) — same class as /tmp/ (#1676).
|
|
8624
|
+
"/tmp_*",
|
|
8438
8625
|
// Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
|
|
8439
8626
|
// via `mmi-cli plan push`, never git-tracked (AGENTS.md "Repo cleanliness", #1550).
|
|
8440
8627
|
"**/plans/",
|
|
@@ -8660,6 +8847,10 @@ function seedMatchesDeployModel(seed, deployModel) {
|
|
|
8660
8847
|
if (!seed.deployModels?.length) return true;
|
|
8661
8848
|
return deployModel != null && seed.deployModels.includes(deployModel);
|
|
8662
8849
|
}
|
|
8850
|
+
function seedMatchesDashboard(seed, isDashboard) {
|
|
8851
|
+
if (!seed.dashboard) return true;
|
|
8852
|
+
return isDashboard;
|
|
8853
|
+
}
|
|
8663
8854
|
function planSeedAction(seed, exists) {
|
|
8664
8855
|
if (seed.source === "fanout") {
|
|
8665
8856
|
return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
|
|
@@ -8707,10 +8898,10 @@ function labelsToPrune(orgLabelNames) {
|
|
|
8707
8898
|
const org = new Set(orgLabelNames);
|
|
8708
8899
|
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
8709
8900
|
}
|
|
8710
|
-
function resolveSeedContent(seed, vars,
|
|
8711
|
-
if (seed.source === "self") return
|
|
8901
|
+
function resolveSeedContent(seed, vars, readFile7) {
|
|
8902
|
+
if (seed.source === "self") return readFile7(seed.target);
|
|
8712
8903
|
if (seed.source.startsWith("seed:")) {
|
|
8713
|
-
const tmpl =
|
|
8904
|
+
const tmpl = readFile7(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
8714
8905
|
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
8715
8906
|
}
|
|
8716
8907
|
return null;
|
|
@@ -8767,6 +8958,9 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
8767
8958
|
class: cls,
|
|
8768
8959
|
projectType,
|
|
8769
8960
|
deployModel,
|
|
8961
|
+
// #1452: opt-in dashboard flag — only emitted when set (undefined keys are dropped below), so a
|
|
8962
|
+
// non-dashboard repo's META is byte-identical to before. Gates the @mutmutco components.json seed.
|
|
8963
|
+
dashboard: options.dashboard ? true : void 0,
|
|
8770
8964
|
// #1359: always persist an explicit track so release tooling never guesses from absence alone.
|
|
8771
8965
|
releaseTrack: resolveBootstrapReleaseTrack(cls, options.releaseTrack),
|
|
8772
8966
|
// Board coords (from GraphQL at bootstrap, passed as --var by the skill).
|
|
@@ -8894,7 +9088,6 @@ function contentPutArgs(repo, path2, content, branch, sha) {
|
|
|
8894
9088
|
|
|
8895
9089
|
// src/ci-audit.ts
|
|
8896
9090
|
var HUB_REPO = "mutmutco/MMI-Hub";
|
|
8897
|
-
var PRODUCT_GATE_CONTEXT2 = "gate";
|
|
8898
9091
|
var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
|
|
8899
9092
|
var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
|
|
8900
9093
|
var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
|
|
@@ -8931,6 +9124,45 @@ async function rulesetDetails(deps, repo, list) {
|
|
|
8931
9124
|
}
|
|
8932
9125
|
return details;
|
|
8933
9126
|
}
|
|
9127
|
+
async function fetchFileContent(deps, repo, branch, path2) {
|
|
9128
|
+
try {
|
|
9129
|
+
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
9130
|
+
const file = await deps.client.rest(
|
|
9131
|
+
"GET",
|
|
9132
|
+
`repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`
|
|
9133
|
+
);
|
|
9134
|
+
if (file.encoding !== "base64" || typeof file.content !== "string") return null;
|
|
9135
|
+
return Buffer.from(file.content, "base64").toString("utf8");
|
|
9136
|
+
} catch {
|
|
9137
|
+
return null;
|
|
9138
|
+
}
|
|
9139
|
+
}
|
|
9140
|
+
async function listWorkflowPaths(deps, repo, branch) {
|
|
9141
|
+
try {
|
|
9142
|
+
const encodedPath = ".github/workflows".split("/").map(encodeURIComponent).join("/");
|
|
9143
|
+
const entries = await deps.client.rest(
|
|
9144
|
+
"GET",
|
|
9145
|
+
`repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`
|
|
9146
|
+
);
|
|
9147
|
+
return (entries ?? []).filter((e) => e.type === "file" && /\.ya?ml$/i.test(e.name ?? "")).map((e) => `.github/workflows/${e.name}`);
|
|
9148
|
+
} catch {
|
|
9149
|
+
return [];
|
|
9150
|
+
}
|
|
9151
|
+
}
|
|
9152
|
+
async function resolveEmittedPrContexts(deps, repo, branch) {
|
|
9153
|
+
const paths = await listWorkflowPaths(deps, repo, branch);
|
|
9154
|
+
const workflows = [];
|
|
9155
|
+
for (const path2 of paths) {
|
|
9156
|
+
const body = await fetchFileContent(deps, repo, branch, path2);
|
|
9157
|
+
if (body) workflows.push({ path: path2, body });
|
|
9158
|
+
}
|
|
9159
|
+
return collectPullRequestWorkflowContexts(workflows);
|
|
9160
|
+
}
|
|
9161
|
+
function registryRequiredContexts(meta) {
|
|
9162
|
+
const raw = meta?.requiredChecks;
|
|
9163
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
9164
|
+
return raw.filter((c) => typeof c === "string" && c.length > 0);
|
|
9165
|
+
}
|
|
8934
9166
|
async function contentExists(deps, repo, branch, path2) {
|
|
8935
9167
|
try {
|
|
8936
9168
|
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
@@ -9030,13 +9262,25 @@ async function auditRepoCi(repo, deps) {
|
|
|
9030
9262
|
detail: missing.length ? `missing: ${missing.join(", ")}` : void 0
|
|
9031
9263
|
});
|
|
9032
9264
|
} else if (deployableGated) {
|
|
9033
|
-
const
|
|
9265
|
+
const hasRequiredChecks = statusChecks.size > 0;
|
|
9034
9266
|
checks.push({
|
|
9035
|
-
ok:
|
|
9036
|
-
label: "product
|
|
9037
|
-
detail:
|
|
9038
|
-
remediation:
|
|
9267
|
+
ok: hasRequiredChecks,
|
|
9268
|
+
label: "product required status checks active",
|
|
9269
|
+
detail: hasRequiredChecks ? void 0 : `no required status checks active \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset`,
|
|
9270
|
+
remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)`
|
|
9039
9271
|
});
|
|
9272
|
+
if (hasGateWorkflow) {
|
|
9273
|
+
const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
|
|
9274
|
+
if (emitted.length > 0) {
|
|
9275
|
+
const aligned = contextsMatchRuleset(statusChecks, new Set(emitted));
|
|
9276
|
+
checks.push({
|
|
9277
|
+
ok: aligned,
|
|
9278
|
+
label: "required check contexts match PR workflows",
|
|
9279
|
+
detail: aligned ? void 0 : `ruleset requires [${[...statusChecks].sort().join(", ")}] but workflows emit [${emitted.join(", ")}]`,
|
|
9280
|
+
remediation: aligned ? void 0 : `mmi-cli ci reconcile --repo ${repo} --apply`
|
|
9281
|
+
});
|
|
9282
|
+
}
|
|
9283
|
+
}
|
|
9040
9284
|
}
|
|
9041
9285
|
const workflowPaths = hasGateWorkflow && repoClass === "deployable" ? [PRODUCT_GATE_PATH] : [];
|
|
9042
9286
|
const { policy, reason } = resolveMergeCiPolicy({
|
|
@@ -9230,9 +9474,10 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
9230
9474
|
const report = await auditRepoCi(repo, deps);
|
|
9231
9475
|
if (report.class !== "deployable") return merge;
|
|
9232
9476
|
await seedGateYml(repo, deps, meta, merge);
|
|
9233
|
-
const
|
|
9234
|
-
|
|
9235
|
-
|
|
9477
|
+
const driftCheck = report.checks.find((c) => c.label === "required check contexts match PR workflows");
|
|
9478
|
+
const requiredCheck = report.checks.find((c) => c.label === "product required status checks active");
|
|
9479
|
+
if (requiredCheck?.ok && (driftCheck?.ok ?? true)) {
|
|
9480
|
+
merge.skipped.push("product ruleset already active and aligned");
|
|
9236
9481
|
return merge;
|
|
9237
9482
|
}
|
|
9238
9483
|
const raw = await fetchRulesetSeedBody(deps, repo);
|
|
@@ -9241,7 +9486,27 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
9241
9486
|
return merge;
|
|
9242
9487
|
}
|
|
9243
9488
|
try {
|
|
9244
|
-
|
|
9489
|
+
let body = stripRulesetComment(raw);
|
|
9490
|
+
if (!driftCheck?.ok) {
|
|
9491
|
+
const baseBranch = "development";
|
|
9492
|
+
const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
|
|
9493
|
+
if (!emitted.length) {
|
|
9494
|
+
merge.errors.push("cannot reconcile ruleset contexts \u2014 no PR workflow job ids found");
|
|
9495
|
+
return merge;
|
|
9496
|
+
}
|
|
9497
|
+
body = patchRulesetRequiredContexts(body, emitted);
|
|
9498
|
+
const activation2 = await activateProductRuleset(repo, body, deps.client);
|
|
9499
|
+
if (activation2.action === "skipped") merge.skipped.push(activation2.detail ?? "product ruleset");
|
|
9500
|
+
else merge.applied.push(`product ruleset ${activation2.action}${activation2.detail ? `: ${activation2.detail}` : ""}`);
|
|
9501
|
+
try {
|
|
9502
|
+
await putSeedFile(deps, repo, PRODUCT_RULESET_REF, `${JSON.stringify(body, null, 2)}
|
|
9503
|
+
`, baseBranch);
|
|
9504
|
+
merge.applied.push(`reconciled ${PRODUCT_RULESET_REF} contexts \u2192 [${emitted.join(", ")}]`);
|
|
9505
|
+
} catch (e) {
|
|
9506
|
+
merge.errors.push(`ruleset reference commit failed (live ruleset updated): ${e.message}`);
|
|
9507
|
+
}
|
|
9508
|
+
return merge;
|
|
9509
|
+
}
|
|
9245
9510
|
const activation = await activateProductRuleset(repo, body, deps.client);
|
|
9246
9511
|
if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
|
|
9247
9512
|
else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
|
|
@@ -9254,6 +9519,25 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
9254
9519
|
// src/pr-land.ts
|
|
9255
9520
|
var PR_LAND_POLL_MS = 3e4;
|
|
9256
9521
|
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
9522
|
+
var PR_LAND_STATE_READ_RETRIES = 3;
|
|
9523
|
+
var PR_LAND_STATE_READ_DELAY_MS = 2e3;
|
|
9524
|
+
async function readGhPrStateWithRetry(fetchState2, options) {
|
|
9525
|
+
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
9526
|
+
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
9527
|
+
const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
9528
|
+
let lastError = "empty state";
|
|
9529
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
9530
|
+
try {
|
|
9531
|
+
const state = (await fetchState2()).trim();
|
|
9532
|
+
if (state) return { ok: true, state };
|
|
9533
|
+
lastError = "empty state";
|
|
9534
|
+
} catch (e) {
|
|
9535
|
+
lastError = String(e.message || "gh pr view failed");
|
|
9536
|
+
}
|
|
9537
|
+
if (attempt < retries - 1) await sleep2(delayMs);
|
|
9538
|
+
}
|
|
9539
|
+
return { ok: false, error: lastError };
|
|
9540
|
+
}
|
|
9257
9541
|
async function runPrLand(prNumber, options, deps) {
|
|
9258
9542
|
const repo = await deps.resolveRepo(prNumber, options.repo);
|
|
9259
9543
|
const base2 = { status: "failed", repo, pr: prNumber };
|
|
@@ -9461,22 +9745,15 @@ function formatManifestHuman(manifest) {
|
|
|
9461
9745
|
// src/config-discovery.ts
|
|
9462
9746
|
function stripMutableBoardConfig(cfg) {
|
|
9463
9747
|
const {
|
|
9464
|
-
projectOwner,
|
|
9465
|
-
projectNumber,
|
|
9466
|
-
projectId,
|
|
9467
|
-
statusFieldId,
|
|
9468
|
-
statusOptions,
|
|
9469
|
-
priorityFieldId,
|
|
9470
|
-
priorityOptions,
|
|
9748
|
+
projectOwner: _projectOwner,
|
|
9749
|
+
projectNumber: _projectNumber,
|
|
9750
|
+
projectId: _projectId,
|
|
9751
|
+
statusFieldId: _statusFieldId,
|
|
9752
|
+
statusOptions: _statusOptions,
|
|
9753
|
+
priorityFieldId: _priorityFieldId,
|
|
9754
|
+
priorityOptions: _priorityOptions,
|
|
9471
9755
|
...rest
|
|
9472
9756
|
} = cfg;
|
|
9473
|
-
void projectOwner;
|
|
9474
|
-
void projectNumber;
|
|
9475
|
-
void projectId;
|
|
9476
|
-
void statusFieldId;
|
|
9477
|
-
void statusOptions;
|
|
9478
|
-
void priorityFieldId;
|
|
9479
|
-
void priorityOptions;
|
|
9480
9757
|
return rest;
|
|
9481
9758
|
}
|
|
9482
9759
|
function boardConfigFromProject(meta, floor = {}) {
|
|
@@ -10160,7 +10437,7 @@ function buildRemoteBranchCleanupReport(branch, input) {
|
|
|
10160
10437
|
return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
|
|
10161
10438
|
}
|
|
10162
10439
|
async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, options = {}) {
|
|
10163
|
-
const
|
|
10440
|
+
const sleep2 = options.sleep ?? defaultSleep;
|
|
10164
10441
|
const maxAttempts = options.maxAttempts ?? 5;
|
|
10165
10442
|
const backoff = [500, 1e3, 1500, 2e3];
|
|
10166
10443
|
if (!input.attempted) {
|
|
@@ -10171,7 +10448,7 @@ async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, option
|
|
|
10171
10448
|
}
|
|
10172
10449
|
let existsAfter = await deps.exists(branch, { prune: true });
|
|
10173
10450
|
for (let i = 1; i < maxAttempts && existsAfter === true; i++) {
|
|
10174
|
-
await
|
|
10451
|
+
await sleep2(backoff[Math.min(i - 1, backoff.length - 1)]);
|
|
10175
10452
|
existsAfter = await deps.exists(branch, { prune: true });
|
|
10176
10453
|
}
|
|
10177
10454
|
return buildRemoteBranchCleanupReport(branch, {
|
|
@@ -10456,6 +10733,39 @@ function safeWorktreeRemoveCommand(safeCwd, targetPath) {
|
|
|
10456
10733
|
function errorMessage(error) {
|
|
10457
10734
|
return error instanceof Error ? error.message : String(error);
|
|
10458
10735
|
}
|
|
10736
|
+
async function fastForwardCurrentBranch(git) {
|
|
10737
|
+
try {
|
|
10738
|
+
const before = (await git(["rev-parse", "HEAD"])).trim();
|
|
10739
|
+
await git(["pull", "--ff-only"]);
|
|
10740
|
+
const after = (await git(["rev-parse", "HEAD"])).trim();
|
|
10741
|
+
return { status: before === after ? "up-to-date" : "fast-forwarded" };
|
|
10742
|
+
} catch (e) {
|
|
10743
|
+
return { status: "failed", error: errorMessage(e) };
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
async function returnCheckoutToBase(git, base2, mergedBranch, currentBranch) {
|
|
10747
|
+
if (currentBranch === mergedBranch) {
|
|
10748
|
+
const dirty = (await git(["status", "--porcelain"]).catch(() => "dirty") || "").trim().length > 0;
|
|
10749
|
+
if (dirty) {
|
|
10750
|
+
return { report: { branch: base2, switched: false, sync: "skipped", reason: "dirty-worktree" }, canDeleteBranch: false };
|
|
10751
|
+
}
|
|
10752
|
+
try {
|
|
10753
|
+
await git(["checkout", base2]);
|
|
10754
|
+
} catch (e) {
|
|
10755
|
+
return {
|
|
10756
|
+
report: { branch: base2, switched: false, sync: "skipped", reason: "checkout-failed", error: errorMessage(e) },
|
|
10757
|
+
canDeleteBranch: false
|
|
10758
|
+
};
|
|
10759
|
+
}
|
|
10760
|
+
const ff2 = await fastForwardCurrentBranch(git);
|
|
10761
|
+
return { report: { branch: base2, switched: true, sync: ff2.status, ...ff2.error ? { error: ff2.error } : {} }, canDeleteBranch: true };
|
|
10762
|
+
}
|
|
10763
|
+
if (currentBranch !== base2) {
|
|
10764
|
+
return { report: { branch: base2, switched: false, sync: "skipped", reason: "not-on-base" }, canDeleteBranch: true };
|
|
10765
|
+
}
|
|
10766
|
+
const ff = await fastForwardCurrentBranch(git);
|
|
10767
|
+
return { report: { branch: base2, switched: false, sync: ff.status, ...ff.error ? { error: ff.error } : {} }, canDeleteBranch: true };
|
|
10768
|
+
}
|
|
10459
10769
|
async function cleanupPrMergeLocalBranch(branch, options) {
|
|
10460
10770
|
const report = {
|
|
10461
10771
|
branch,
|
|
@@ -10549,7 +10859,19 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
10549
10859
|
}
|
|
10550
10860
|
}
|
|
10551
10861
|
const current = (await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || "").trim();
|
|
10552
|
-
if (
|
|
10862
|
+
if (options.returnToBranch && options.returnToBranch !== branch) {
|
|
10863
|
+
const { report: checkout, canDeleteBranch } = await returnCheckoutToBase(
|
|
10864
|
+
git,
|
|
10865
|
+
options.returnToBranch,
|
|
10866
|
+
branch,
|
|
10867
|
+
current
|
|
10868
|
+
);
|
|
10869
|
+
report.checkout = checkout;
|
|
10870
|
+
if (!canDeleteBranch) {
|
|
10871
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: checkout.reason ?? "current-branch" };
|
|
10872
|
+
return report;
|
|
10873
|
+
}
|
|
10874
|
+
} else if (branch === current) {
|
|
10553
10875
|
report.localBranch = { name: branch, status: "not-attempted", reason: "current-branch" };
|
|
10554
10876
|
return report;
|
|
10555
10877
|
}
|
|
@@ -10736,10 +11058,10 @@ function trainPlan(command, options = {}) {
|
|
|
10736
11058
|
{ label: "no residue: development already has the fix; the next /release fold + back-merge re-aligns version manifests" }
|
|
10737
11059
|
];
|
|
10738
11060
|
}
|
|
10739
|
-
function bootstrapPlan(repo, repoClass) {
|
|
11061
|
+
function bootstrapPlan(repo, repoClass, opts = {}) {
|
|
10740
11062
|
const branchModel = repoClass === "content" ? "content repo: main only" : "deployable repo: development, rc, main";
|
|
10741
11063
|
const protectedBranches = repoClass === "content" ? "main" : "development, rc, main";
|
|
10742
|
-
|
|
11064
|
+
const steps = [
|
|
10743
11065
|
{ label: `create or inspect ${repo}` },
|
|
10744
11066
|
{ label: `provision branch model: ${branchModel}`, gated: true },
|
|
10745
11067
|
{ label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
|
|
@@ -10748,6 +11070,13 @@ function bootstrapPlan(repo, repoClass) {
|
|
|
10748
11070
|
{ label: "commit .claude/settings.json and .cursor/rules/<repo>.mdc", gated: true },
|
|
10749
11071
|
{ label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
|
|
10750
11072
|
];
|
|
11073
|
+
if (opts.dashboard) {
|
|
11074
|
+
steps.push({
|
|
11075
|
+
label: "seed components.json wired to the @mutmutco registry; scaffold the app from the MMD-UI apps/starter",
|
|
11076
|
+
gated: true
|
|
11077
|
+
});
|
|
11078
|
+
}
|
|
11079
|
+
return steps;
|
|
10751
11080
|
}
|
|
10752
11081
|
|
|
10753
11082
|
// src/stage-default.ts
|
|
@@ -10863,7 +11192,7 @@ function decideStage(inputs) {
|
|
|
10863
11192
|
}
|
|
10864
11193
|
|
|
10865
11194
|
// src/cursor-plugin-seed.ts
|
|
10866
|
-
var
|
|
11195
|
+
var import_node_child_process8 = require("node:child_process");
|
|
10867
11196
|
var import_node_fs15 = require("node:fs");
|
|
10868
11197
|
var import_node_os5 = require("node:os");
|
|
10869
11198
|
var import_node_path14 = require("node:path");
|
|
@@ -10874,7 +11203,7 @@ function isSemverVersion(v) {
|
|
|
10874
11203
|
var MMI_HUB_REPO = "mutmutco/MMI-Hub";
|
|
10875
11204
|
var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
|
|
10876
11205
|
var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
|
|
10877
|
-
var execFileBuffer = (0, import_node_util6.promisify)(
|
|
11206
|
+
var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process8.execFile);
|
|
10878
11207
|
function gitFetchReleaseTagArgs(hubCheckout, tag) {
|
|
10879
11208
|
return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
|
|
10880
11209
|
}
|
|
@@ -11008,8 +11337,10 @@ function buildAwsCrossAccountCheck(input) {
|
|
|
11008
11337
|
fix: AWS_CROSS_ACCOUNT_FIX
|
|
11009
11338
|
};
|
|
11010
11339
|
}
|
|
11011
|
-
var MMI_PLUGIN_ID = "mmi@
|
|
11012
|
-
var
|
|
11340
|
+
var MMI_PLUGIN_ID = "mmi@mutmutco";
|
|
11341
|
+
var LEGACY_MMI_PLUGIN_ID = "mmi@mmi";
|
|
11342
|
+
var LEGACY_MMI_MARKETPLACE = "mmi";
|
|
11343
|
+
var PLUGIN_LABEL = "plugin install record (mmi@mutmutco for this project)";
|
|
11013
11344
|
function pluginInstallManualFix(projectPath, surface = "claude-cli") {
|
|
11014
11345
|
const register = surface === "codex" ? `\`codex plugin add ${MMI_PLUGIN_ID}\`` : surface === "cursor" ? `import the MMI Team Marketplace in Cursor Dashboard \u2192 Settings \u2192 Plugins (or enable the MMI plugin from the marketplace panel)` : surface === "shell" ? `enable the MMI plugin in your client` : surface === "claude-vscode" ? `\`claude plugin enable ${MMI_PLUGIN_ID}\`` : `\`/plugin install ${MMI_PLUGIN_ID}\``;
|
|
11015
11346
|
return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
|
|
@@ -11027,6 +11358,10 @@ function hasUserInstallRecord(file, pluginId) {
|
|
|
11027
11358
|
if (!Array.isArray(records)) return false;
|
|
11028
11359
|
return records.some((r) => r.scope === "user");
|
|
11029
11360
|
}
|
|
11361
|
+
function hasAnyPluginRecords(file, pluginId) {
|
|
11362
|
+
const records = file?.plugins?.[pluginId];
|
|
11363
|
+
return Array.isArray(records) && records.length > 0;
|
|
11364
|
+
}
|
|
11030
11365
|
function buildPluginInstallRecordCheck(input) {
|
|
11031
11366
|
const pluginId = input.pluginId ?? MMI_PLUGIN_ID;
|
|
11032
11367
|
const base2 = {
|
|
@@ -11036,6 +11371,13 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
11036
11371
|
pluginId
|
|
11037
11372
|
};
|
|
11038
11373
|
if (!input.isOrgRepo || !isMmiPluginEnabled(input.settings)) return base2;
|
|
11374
|
+
if (hasAnyPluginRecords(input.installed, LEGACY_MMI_PLUGIN_ID) && !hasAnyPluginRecords(input.installed, pluginId)) {
|
|
11375
|
+
return {
|
|
11376
|
+
...base2,
|
|
11377
|
+
ok: false,
|
|
11378
|
+
fix: `${CLAUDE_PLUGIN_RECOVERY} # then ${reloadAction("claude-cli")}`
|
|
11379
|
+
};
|
|
11380
|
+
}
|
|
11039
11381
|
if (hasProjectInstallRecord(input.installed, pluginId, input.projectPath) || hasUserInstallRecord(input.installed, pluginId)) return base2;
|
|
11040
11382
|
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11041
11383
|
const recordToInsert = input.mirrorFrom ? {
|
|
@@ -11053,7 +11395,35 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
11053
11395
|
recordToInsert
|
|
11054
11396
|
};
|
|
11055
11397
|
}
|
|
11056
|
-
var
|
|
11398
|
+
var LEGACY_PLUGIN_ID_LABEL = "legacy MMI plugin ID (mmi@mmi \u2192 mmi@mutmutco)";
|
|
11399
|
+
function buildLegacyPluginInstallCheck(input) {
|
|
11400
|
+
const base2 = {
|
|
11401
|
+
ok: true,
|
|
11402
|
+
label: LEGACY_PLUGIN_ID_LABEL,
|
|
11403
|
+
fix: pluginRecoveryFix(input.surface ?? "claude-cli"),
|
|
11404
|
+
legacyPluginId: LEGACY_MMI_PLUGIN_ID
|
|
11405
|
+
};
|
|
11406
|
+
if (!input.isOrgRepo) return base2;
|
|
11407
|
+
const staleSurfaces = [];
|
|
11408
|
+
for (const source of input.sources) {
|
|
11409
|
+
if (hasAnyPluginRecords(source.installed, LEGACY_MMI_PLUGIN_ID)) staleSurfaces.push(source.surface);
|
|
11410
|
+
}
|
|
11411
|
+
if (staleSurfaces.length === 0) return base2;
|
|
11412
|
+
const fixParts = [];
|
|
11413
|
+
if (staleSurfaces.includes("claude")) {
|
|
11414
|
+
fixParts.push(`${CLAUDE_PLUGIN_RECOVERY} # then ${reloadAction("claude-cli")}`);
|
|
11415
|
+
}
|
|
11416
|
+
if (staleSurfaces.includes("codex")) {
|
|
11417
|
+
fixParts.push(`${CODEX_PLUGIN_RECOVERY} # then ${reloadAction("codex")}`);
|
|
11418
|
+
}
|
|
11419
|
+
return {
|
|
11420
|
+
...base2,
|
|
11421
|
+
ok: false,
|
|
11422
|
+
staleSurfaces,
|
|
11423
|
+
fix: fixParts.join(" ; ") || base2.fix
|
|
11424
|
+
};
|
|
11425
|
+
}
|
|
11426
|
+
var PLUGIN_DRIFT_LABEL = "plugin config drift (mmi@mutmutco duplicate rows / stale gitCommitSha)";
|
|
11057
11427
|
function recordFreshness(r) {
|
|
11058
11428
|
return r.lastUpdated ?? r.installedAt ?? "";
|
|
11059
11429
|
}
|
|
@@ -11226,18 +11596,20 @@ function reloadAction(surface) {
|
|
|
11226
11596
|
return "restart Claude Code (or run /reload-plugins)";
|
|
11227
11597
|
}
|
|
11228
11598
|
}
|
|
11229
|
-
var CLAUDE_PLUGIN_RECOVERY =
|
|
11599
|
+
var CLAUDE_PLUGIN_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub && claude plugin install mmi@mutmutco`;
|
|
11230
11600
|
var CLAUDE_PLUGIN_HEAL_STEPS = [
|
|
11231
|
-
{ args: ["plugin", "marketplace", "remove",
|
|
11601
|
+
{ args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
|
|
11602
|
+
{ args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
|
|
11232
11603
|
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub"], gated: true },
|
|
11233
|
-
{ args: ["plugin", "install", "mmi@
|
|
11234
|
-
{ args: ["plugin", "enable", "mmi@
|
|
11604
|
+
{ args: ["plugin", "install", "mmi@mutmutco"], gated: true },
|
|
11605
|
+
{ args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
|
|
11235
11606
|
];
|
|
11236
|
-
var CODEX_PLUGIN_RECOVERY =
|
|
11607
|
+
var CODEX_PLUGIN_RECOVERY = `codex plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco`;
|
|
11237
11608
|
var CODEX_PLUGIN_HEAL_STEPS = [
|
|
11238
|
-
{ args: ["plugin", "marketplace", "remove",
|
|
11609
|
+
{ args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
|
|
11610
|
+
{ args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
|
|
11239
11611
|
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
|
|
11240
|
-
{ args: ["plugin", "add", "mmi@
|
|
11612
|
+
{ args: ["plugin", "add", "mmi@mutmutco"], gated: true }
|
|
11241
11613
|
];
|
|
11242
11614
|
function healStepAborts(step, ok) {
|
|
11243
11615
|
return !ok && step.gated;
|
|
@@ -11259,7 +11631,7 @@ function pluginRecoveryFix(surface) {
|
|
|
11259
11631
|
}
|
|
11260
11632
|
var PLUGIN_UPDATE_RECIPES = {
|
|
11261
11633
|
claude: [CLAUDE_PLUGIN_RECOVERY],
|
|
11262
|
-
codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@
|
|
11634
|
+
codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@mutmutco shows the released version"],
|
|
11263
11635
|
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
11264
11636
|
};
|
|
11265
11637
|
function highestSemver(versions) {
|
|
@@ -11589,82 +11961,401 @@ async function runStageLiveDown(deps, t) {
|
|
|
11589
11961
|
};
|
|
11590
11962
|
}
|
|
11591
11963
|
|
|
11592
|
-
// src/
|
|
11593
|
-
var import_node_child_process8 = require("node:child_process");
|
|
11964
|
+
// src/design-system.ts
|
|
11594
11965
|
var import_node_fs16 = require("node:fs");
|
|
11595
11966
|
var import_node_path15 = require("node:path");
|
|
11596
|
-
var
|
|
11597
|
-
var
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11967
|
+
var UI_PACKAGE_CANDIDATES = ["@mutmutco/ui-dashboard", "@mutmutco/ui", "@mutmutco/theme"];
|
|
11968
|
+
var DESIGN_SYSTEM_VERSION_LABEL = "@mutmutco design-system npm package (vs @latest)";
|
|
11969
|
+
function dashboardConsumerRegistryFix(error) {
|
|
11970
|
+
return `Hub registry read failed (${error}) \u2014 could not verify dashboard UI state; likely transient (cold start, network, or auth blip) \u2014 retry shortly`;
|
|
11971
|
+
}
|
|
11972
|
+
var DESIGN_SYSTEM_FIX = (pkg) => `run \`npm update ${pkg}\` (or \`mmi-cli doctor --apply\` to update automatically)`;
|
|
11973
|
+
function buildDesignSystemVersionCheck(input) {
|
|
11974
|
+
const base2 = {
|
|
11975
|
+
ok: true,
|
|
11976
|
+
label: DESIGN_SYSTEM_VERSION_LABEL,
|
|
11977
|
+
fix: `install a current @mutmutco UI package \u2014 see MM-KB kb/guides/dashboard-ui.md`
|
|
11978
|
+
};
|
|
11979
|
+
if (!input.isConsumerRepo || !input.packageName) return base2;
|
|
11980
|
+
if (!input.installedVersion) {
|
|
11981
|
+
return {
|
|
11982
|
+
ok: false,
|
|
11983
|
+
label: DESIGN_SYSTEM_VERSION_LABEL,
|
|
11984
|
+
fix: `add ${input.packageName} to package.json, then npm install \u2014 see kb/guides/dashboard-ui.md`,
|
|
11985
|
+
packageName: input.packageName,
|
|
11986
|
+
latestVersion: input.latestVersion
|
|
11610
11987
|
};
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11988
|
+
}
|
|
11989
|
+
if (!input.latestVersion) {
|
|
11990
|
+
return {
|
|
11991
|
+
...base2,
|
|
11992
|
+
packageName: input.packageName,
|
|
11993
|
+
installedVersion: input.installedVersion
|
|
11994
|
+
};
|
|
11995
|
+
}
|
|
11996
|
+
if (compareVersions(input.installedVersion, input.latestVersion) < 0) {
|
|
11997
|
+
return {
|
|
11998
|
+
ok: false,
|
|
11999
|
+
label: DESIGN_SYSTEM_VERSION_LABEL,
|
|
12000
|
+
fix: DESIGN_SYSTEM_FIX(input.packageName),
|
|
12001
|
+
packageName: input.packageName,
|
|
12002
|
+
installedVersion: input.installedVersion,
|
|
12003
|
+
latestVersion: input.latestVersion
|
|
12004
|
+
};
|
|
12005
|
+
}
|
|
12006
|
+
return {
|
|
12007
|
+
...base2,
|
|
12008
|
+
packageName: input.packageName,
|
|
12009
|
+
installedVersion: input.installedVersion,
|
|
12010
|
+
latestVersion: input.latestVersion
|
|
12011
|
+
};
|
|
11618
12012
|
}
|
|
11619
|
-
function
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
const eq = trimmed.indexOf("=");
|
|
11625
|
-
if (eq > 0) keys.add(trimmed.slice(0, eq).trim());
|
|
12013
|
+
function readJsonFile(path2) {
|
|
12014
|
+
try {
|
|
12015
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
|
|
12016
|
+
} catch {
|
|
12017
|
+
return void 0;
|
|
11626
12018
|
}
|
|
11627
|
-
return keys;
|
|
11628
12019
|
}
|
|
11629
|
-
function
|
|
11630
|
-
const
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
12020
|
+
function isUiFactoryCheckout(root) {
|
|
12021
|
+
const pkg = readJsonFile((0, import_node_path15.join)(root, "package.json"));
|
|
12022
|
+
return pkg?.name === "mmd-ui" && pkg?.private === true;
|
|
12023
|
+
}
|
|
12024
|
+
function resolveDeclaredUiPackage(root) {
|
|
12025
|
+
const pkg = readJsonFile((0, import_node_path15.join)(root, "package.json"));
|
|
12026
|
+
if (!pkg) return void 0;
|
|
12027
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
12028
|
+
for (const name of UI_PACKAGE_CANDIDATES) {
|
|
12029
|
+
const spec = deps[name];
|
|
12030
|
+
if (spec && spec !== "*" && !spec.startsWith("workspace:")) return name;
|
|
11636
12031
|
}
|
|
11637
12032
|
return void 0;
|
|
11638
12033
|
}
|
|
11639
|
-
function
|
|
11640
|
-
|
|
12034
|
+
function readLockfileInstalledVersion(root, packageName) {
|
|
12035
|
+
const lockPath = (0, import_node_path15.join)(root, "package-lock.json");
|
|
12036
|
+
if (!(0, import_node_fs16.existsSync)(lockPath)) return void 0;
|
|
12037
|
+
const lock = readJsonFile(lockPath);
|
|
12038
|
+
const node = lock?.packages?.[`node_modules/${packageName}`];
|
|
12039
|
+
const version = node?.version?.trim();
|
|
12040
|
+
return version && /^\d+\.\d+\.\d+/.test(version) ? version : void 0;
|
|
11641
12041
|
}
|
|
11642
|
-
function
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
const idx = indexByKey.get(key);
|
|
11656
|
-
if (idx != null) lines[idx] = line;
|
|
11657
|
-
else lines.push(line);
|
|
12042
|
+
function npmUiPackageLatestArgs(packageName) {
|
|
12043
|
+
return ["view", packageName, "version"];
|
|
12044
|
+
}
|
|
12045
|
+
function parseNpmViewVersion(stdout) {
|
|
12046
|
+
const v = stdout.trim();
|
|
12047
|
+
return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v) ? v : void 0;
|
|
12048
|
+
}
|
|
12049
|
+
function isDashboardMetaConsumer(meta) {
|
|
12050
|
+
return meta?.dashboard === true;
|
|
12051
|
+
}
|
|
12052
|
+
function designSystemSnapshot(root) {
|
|
12053
|
+
if (isUiFactoryCheckout(root)) {
|
|
12054
|
+
return {};
|
|
11658
12055
|
}
|
|
11659
|
-
const
|
|
11660
|
-
return
|
|
11661
|
-
|
|
12056
|
+
const packageName = resolveDeclaredUiPackage(root);
|
|
12057
|
+
return {
|
|
12058
|
+
packageName,
|
|
12059
|
+
installedVersion: packageName ? readLockfileInstalledVersion(root, packageName) : void 0
|
|
12060
|
+
};
|
|
11662
12061
|
}
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
12062
|
+
|
|
12063
|
+
// src/design-system-registry.ts
|
|
12064
|
+
var import_node_crypto7 = require("node:crypto");
|
|
12065
|
+
var import_node_fs18 = require("node:fs");
|
|
12066
|
+
var import_node_path16 = require("node:path");
|
|
12067
|
+
|
|
12068
|
+
// src/atomic-write.ts
|
|
12069
|
+
var import_node_fs17 = require("node:fs");
|
|
12070
|
+
function atomicWriteFileSync(path2, content) {
|
|
12071
|
+
const tmp = `${path2}.${process.pid}.tmp`;
|
|
12072
|
+
(0, import_node_fs17.writeFileSync)(tmp, content, "utf8");
|
|
12073
|
+
(0, import_node_fs17.renameSync)(tmp, path2);
|
|
12074
|
+
}
|
|
12075
|
+
|
|
12076
|
+
// src/design-system-registry.ts
|
|
12077
|
+
var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
|
|
12078
|
+
var DESIGN_SYSTEM_MANIFEST_PATH = ".mmi/design-system/manifest.json";
|
|
12079
|
+
var REGISTRY_COMPONENTS_LABEL = "@mutmutco registry components (.mmi cache vs live registry)";
|
|
12080
|
+
var REGISTRY_FIX = "run `mmi-cli doctor --apply` to pull registry components into `.mmi/design-system/components` \u2014 wire imports via `design-system.paths.json` (see kb/guides/dashboard-ui.md)";
|
|
12081
|
+
var REGISTRY_UNREACHABLE_FIX = "live @mutmutco registry unreachable \u2014 verify `components.json` `@mutmutco` registry URL and network, then retry `mmi-cli doctor`";
|
|
12082
|
+
function readJsonFile2(path2) {
|
|
12083
|
+
try {
|
|
12084
|
+
return JSON.parse((0, import_node_fs18.readFileSync)(path2, "utf8"));
|
|
12085
|
+
} catch {
|
|
12086
|
+
return void 0;
|
|
12087
|
+
}
|
|
12088
|
+
}
|
|
12089
|
+
function readComponentsJson(root) {
|
|
12090
|
+
return readJsonFile2((0, import_node_path16.join)(root, "components.json"));
|
|
12091
|
+
}
|
|
12092
|
+
function hasMutmutcoRegistry(root) {
|
|
12093
|
+
const url = readComponentsJson(root)?.registries?.["@mutmutco"];
|
|
12094
|
+
return typeof url === "string" && url.includes("{name}");
|
|
12095
|
+
}
|
|
12096
|
+
function resolveCacheDir(root) {
|
|
12097
|
+
const custom = readComponentsJson(root)?.mmi?.cacheDir;
|
|
12098
|
+
return (0, import_node_path16.join)(root, custom ?? DESIGN_SYSTEM_CACHE_DIR);
|
|
12099
|
+
}
|
|
12100
|
+
function resolveRegistryUrlTemplate(root) {
|
|
12101
|
+
return readComponentsJson(root)?.registries?.["@mutmutco"];
|
|
12102
|
+
}
|
|
12103
|
+
function registryItemUrl(template, name) {
|
|
12104
|
+
return template.replace("{name}", name);
|
|
12105
|
+
}
|
|
12106
|
+
function readDesignSystemManifest(root) {
|
|
12107
|
+
const raw = readJsonFile2((0, import_node_path16.join)(root, DESIGN_SYSTEM_MANIFEST_PATH));
|
|
12108
|
+
if (!raw || !Array.isArray(raw.components)) return void 0;
|
|
12109
|
+
return raw;
|
|
12110
|
+
}
|
|
12111
|
+
function listInstalledRegistryComponents(root) {
|
|
12112
|
+
const fromCfg = readComponentsJson(root)?.mmi?.installed?.filter((n) => typeof n === "string" && n.trim());
|
|
12113
|
+
if (fromCfg?.length) return [...new Set(fromCfg.map((n) => n.trim()))];
|
|
12114
|
+
const manifest = readDesignSystemManifest(root);
|
|
12115
|
+
if (manifest?.components?.length) return [...manifest.components];
|
|
12116
|
+
return scanCachedComponentNames(resolveCacheDir(root));
|
|
12117
|
+
}
|
|
12118
|
+
function scanCachedComponentNames(cacheDir) {
|
|
12119
|
+
if (!(0, import_node_fs18.existsSync)(cacheDir)) return [];
|
|
12120
|
+
const names = /* @__PURE__ */ new Set();
|
|
12121
|
+
const walk = (dir) => {
|
|
12122
|
+
for (const ent of (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true })) {
|
|
12123
|
+
const p = (0, import_node_path16.join)(dir, ent.name);
|
|
12124
|
+
if (ent.isDirectory()) walk(p);
|
|
12125
|
+
else if (ent.isFile() && /\.(tsx?|jsx?)$/.test(ent.name)) {
|
|
12126
|
+
names.add(ent.name.replace(/\.(tsx|ts|jsx|js)$/, ""));
|
|
12127
|
+
}
|
|
12128
|
+
}
|
|
12129
|
+
};
|
|
12130
|
+
try {
|
|
12131
|
+
walk(cacheDir);
|
|
12132
|
+
} catch {
|
|
12133
|
+
}
|
|
12134
|
+
return [...names].sort();
|
|
12135
|
+
}
|
|
12136
|
+
function contentHash(content) {
|
|
12137
|
+
return (0, import_node_crypto7.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
12138
|
+
}
|
|
12139
|
+
function buildRegistryComponentsCheck(input) {
|
|
12140
|
+
const base2 = {
|
|
12141
|
+
ok: true,
|
|
12142
|
+
label: REGISTRY_COMPONENTS_LABEL,
|
|
12143
|
+
fix: "registry components are current in `.mmi/design-system/components`"
|
|
12144
|
+
};
|
|
12145
|
+
if (!input.isConsumerRepo || input.components.length === 0) return base2;
|
|
12146
|
+
if (input.registryUnreachable) {
|
|
12147
|
+
return {
|
|
12148
|
+
ok: false,
|
|
12149
|
+
label: REGISTRY_COMPONENTS_LABEL,
|
|
12150
|
+
fix: REGISTRY_UNREACHABLE_FIX,
|
|
12151
|
+
components: input.components,
|
|
12152
|
+
cacheVersion: input.cacheVersion,
|
|
12153
|
+
targetVersion: input.targetVersion
|
|
12154
|
+
};
|
|
12155
|
+
}
|
|
12156
|
+
const missing = input.missingComponents ?? [];
|
|
12157
|
+
const stale = input.staleComponents ?? [];
|
|
12158
|
+
const versionBehind = Boolean(input.targetVersion) && (!input.cacheVersion || compareVersions(input.cacheVersion, input.targetVersion) < 0);
|
|
12159
|
+
if (missing.length || stale.length || versionBehind) {
|
|
12160
|
+
return {
|
|
12161
|
+
ok: false,
|
|
12162
|
+
label: REGISTRY_COMPONENTS_LABEL,
|
|
12163
|
+
fix: REGISTRY_FIX,
|
|
12164
|
+
components: input.components,
|
|
12165
|
+
cacheVersion: input.cacheVersion,
|
|
12166
|
+
targetVersion: input.targetVersion,
|
|
12167
|
+
staleComponents: [.../* @__PURE__ */ new Set([...missing, ...stale])]
|
|
12168
|
+
};
|
|
12169
|
+
}
|
|
12170
|
+
return {
|
|
12171
|
+
...base2,
|
|
12172
|
+
components: input.components,
|
|
12173
|
+
cacheVersion: input.cacheVersion,
|
|
12174
|
+
targetVersion: input.targetVersion
|
|
12175
|
+
};
|
|
12176
|
+
}
|
|
12177
|
+
function cacheRelativePath(target) {
|
|
12178
|
+
const prefix = "components/";
|
|
12179
|
+
return target.startsWith(prefix) ? target.slice(prefix.length) : target;
|
|
12180
|
+
}
|
|
12181
|
+
async function fetchRegistryItem(url, deps) {
|
|
12182
|
+
try {
|
|
12183
|
+
const res = await deps.fetch(url, { signal: AbortSignal.timeout(8e3) });
|
|
12184
|
+
if (!res.ok) return void 0;
|
|
12185
|
+
const json = await res.json();
|
|
12186
|
+
if (!json?.name || !Array.isArray(json.files)) return void 0;
|
|
12187
|
+
return json;
|
|
12188
|
+
} catch {
|
|
12189
|
+
return void 0;
|
|
12190
|
+
}
|
|
12191
|
+
}
|
|
12192
|
+
async function gatherRegistryComponentsState(root, targetVersion, deps) {
|
|
12193
|
+
const template = resolveRegistryUrlTemplate(root);
|
|
12194
|
+
const components = listInstalledRegistryComponents(root);
|
|
12195
|
+
if (!hasMutmutcoRegistry(root)) return { isConsumerRepo: false, components: [] };
|
|
12196
|
+
if (components.length === 0) return { isConsumerRepo: true, components: [] };
|
|
12197
|
+
if (!template) return { isConsumerRepo: true, components, registryUnreachable: true };
|
|
12198
|
+
const cacheDir = resolveCacheDir(root);
|
|
12199
|
+
const manifest = readDesignSystemManifest(root);
|
|
12200
|
+
const missing = [];
|
|
12201
|
+
const stale = [];
|
|
12202
|
+
let fetchFailed = false;
|
|
12203
|
+
for (const name of components) {
|
|
12204
|
+
const item = await fetchRegistryItem(registryItemUrl(template, name), deps);
|
|
12205
|
+
if (!item) {
|
|
12206
|
+
fetchFailed = true;
|
|
12207
|
+
continue;
|
|
12208
|
+
}
|
|
12209
|
+
let componentStale = false;
|
|
12210
|
+
for (const file of item.files) {
|
|
12211
|
+
if (!file.target || file.content == null) continue;
|
|
12212
|
+
const cachePath = (0, import_node_path16.join)(cacheDir, cacheRelativePath(file.target));
|
|
12213
|
+
if (!(0, import_node_fs18.existsSync)(cachePath)) {
|
|
12214
|
+
componentStale = true;
|
|
12215
|
+
break;
|
|
12216
|
+
}
|
|
12217
|
+
try {
|
|
12218
|
+
if (contentHash((0, import_node_fs18.readFileSync)(cachePath, "utf8")) !== contentHash(file.content)) {
|
|
12219
|
+
componentStale = true;
|
|
12220
|
+
break;
|
|
12221
|
+
}
|
|
12222
|
+
} catch {
|
|
12223
|
+
componentStale = true;
|
|
12224
|
+
break;
|
|
12225
|
+
}
|
|
12226
|
+
}
|
|
12227
|
+
if (componentStale) {
|
|
12228
|
+
if ((0, import_node_fs18.existsSync)((0, import_node_path16.join)(cacheDir, "ui", `${name}.tsx`)) || (0, import_node_fs18.existsSync)((0, import_node_path16.join)(cacheDir, `${name}.tsx`))) {
|
|
12229
|
+
stale.push(name);
|
|
12230
|
+
} else {
|
|
12231
|
+
missing.push(name);
|
|
12232
|
+
}
|
|
12233
|
+
}
|
|
12234
|
+
}
|
|
12235
|
+
return {
|
|
12236
|
+
isConsumerRepo: true,
|
|
12237
|
+
components,
|
|
12238
|
+
cacheVersion: manifest?.version,
|
|
12239
|
+
targetVersion,
|
|
12240
|
+
missingComponents: missing,
|
|
12241
|
+
staleComponents: stale,
|
|
12242
|
+
registryUnreachable: fetchFailed && missing.length === 0 && stale.length === 0
|
|
12243
|
+
};
|
|
12244
|
+
}
|
|
12245
|
+
async function applyRegistryComponentsSync(root, components, targetVersion, log, deps) {
|
|
12246
|
+
const template = resolveRegistryUrlTemplate(root);
|
|
12247
|
+
if (!template || components.length === 0) return { ok: true };
|
|
12248
|
+
const cacheDir = resolveCacheDir(root);
|
|
12249
|
+
deps.mkdir(cacheDir);
|
|
12250
|
+
for (const name of components) {
|
|
12251
|
+
log(` \u21BB syncing registry component @mutmutco/${name}\u2026`);
|
|
12252
|
+
const item = await fetchRegistryItem(registryItemUrl(template, name), deps);
|
|
12253
|
+
if (!item) return { ok: false };
|
|
12254
|
+
for (const file of item.files) {
|
|
12255
|
+
if (!file.target || file.content == null) continue;
|
|
12256
|
+
const outPath = (0, import_node_path16.join)(cacheDir, cacheRelativePath(file.target));
|
|
12257
|
+
deps.mkdir((0, import_node_path16.dirname)(outPath));
|
|
12258
|
+
const body = file.content.endsWith("\n") ? file.content : `${file.content}
|
|
12259
|
+
`;
|
|
12260
|
+
deps.writeFile(outPath, body);
|
|
12261
|
+
}
|
|
12262
|
+
}
|
|
12263
|
+
const manifestPath = (0, import_node_path16.join)(root, DESIGN_SYSTEM_MANIFEST_PATH);
|
|
12264
|
+
deps.mkdir((0, import_node_path16.dirname)(manifestPath));
|
|
12265
|
+
const manifest = {
|
|
12266
|
+
version: targetVersion,
|
|
12267
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12268
|
+
components: [...components],
|
|
12269
|
+
registryUrl: template
|
|
12270
|
+
};
|
|
12271
|
+
deps.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
12272
|
+
`);
|
|
12273
|
+
return { ok: true, cacheVersion: targetVersion };
|
|
12274
|
+
}
|
|
12275
|
+
function defaultRegistrySyncDeps() {
|
|
12276
|
+
return {
|
|
12277
|
+
fetch,
|
|
12278
|
+
writeFile: (path2, content) => atomicWriteFileSync(path2, content),
|
|
12279
|
+
mkdir: (path2) => (0, import_node_fs18.mkdirSync)(path2, { recursive: true })
|
|
12280
|
+
};
|
|
12281
|
+
}
|
|
12282
|
+
|
|
12283
|
+
// src/stage-runner.ts
|
|
12284
|
+
var import_node_child_process9 = require("node:child_process");
|
|
12285
|
+
var import_node_fs19 = require("node:fs");
|
|
12286
|
+
var import_node_path17 = require("node:path");
|
|
12287
|
+
var import_node_net2 = require("node:net");
|
|
12288
|
+
var import_node_util7 = require("node:util");
|
|
12289
|
+
var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process9.execFile);
|
|
12290
|
+
var EARLY_EXIT_GRACE_MS = 2e3;
|
|
12291
|
+
function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
|
|
12292
|
+
return new Promise((resolve, reject) => {
|
|
12293
|
+
let settled = false;
|
|
12294
|
+
const finish = (fn) => {
|
|
12295
|
+
if (settled) return;
|
|
12296
|
+
settled = true;
|
|
12297
|
+
clearTimeout(timer);
|
|
12298
|
+
child.removeAllListeners("error");
|
|
12299
|
+
child.removeAllListeners("exit");
|
|
12300
|
+
fn();
|
|
12301
|
+
};
|
|
12302
|
+
const timer = setTimeout(() => finish(resolve), graceMs);
|
|
12303
|
+
child.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
|
|
12304
|
+
child.on("exit", (code, signal) => {
|
|
12305
|
+
const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
|
|
12306
|
+
finish(() => reject(new Error(`stage process exited before health check (${detail})`)));
|
|
12307
|
+
});
|
|
12308
|
+
});
|
|
12309
|
+
}
|
|
12310
|
+
function envFileKeys(content) {
|
|
12311
|
+
const keys = /* @__PURE__ */ new Set();
|
|
12312
|
+
for (const line of content.split(/\r?\n/)) {
|
|
12313
|
+
const trimmed = line.trim();
|
|
12314
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
12315
|
+
const eq = trimmed.indexOf("=");
|
|
12316
|
+
if (eq > 0) keys.add(trimmed.slice(0, eq).trim());
|
|
12317
|
+
}
|
|
12318
|
+
return keys;
|
|
12319
|
+
}
|
|
12320
|
+
function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
|
|
12321
|
+
const example = normalizeEol(exampleContent);
|
|
12322
|
+
const target = normalizeEol(targetContent);
|
|
12323
|
+
const exampleKeys = envFileKeys(example);
|
|
12324
|
+
const targetKeys = envFileKeys(target);
|
|
12325
|
+
for (const key of exampleKeys) {
|
|
12326
|
+
if (!targetKeys.has(key)) return `missing key ${key} from .env.example`;
|
|
12327
|
+
}
|
|
12328
|
+
return void 0;
|
|
12329
|
+
}
|
|
12330
|
+
function stageStatePath(cwd = process.cwd()) {
|
|
12331
|
+
return (0, import_node_path17.join)(cwd, "tmp", "stage", "state.json");
|
|
12332
|
+
}
|
|
12333
|
+
function mergeEnvSecretsIntoFile(content, secrets) {
|
|
12334
|
+
const lines = content.split(/\r?\n/);
|
|
12335
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
12336
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12337
|
+
const trimmed = lines[i].trim();
|
|
12338
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
12339
|
+
const eq = trimmed.indexOf("=");
|
|
12340
|
+
if (eq === -1) continue;
|
|
12341
|
+
indexByKey.set(trimmed.slice(0, eq).trim(), i);
|
|
12342
|
+
}
|
|
12343
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
12344
|
+
const escaped = /[\s#"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
|
|
12345
|
+
const line = `${key}=${escaped}`;
|
|
12346
|
+
const idx = indexByKey.get(key);
|
|
12347
|
+
if (idx != null) lines[idx] = line;
|
|
12348
|
+
else lines.push(line);
|
|
12349
|
+
}
|
|
12350
|
+
const body = lines.join("\n");
|
|
12351
|
+
return body.endsWith("\n") ? body : `${body}
|
|
12352
|
+
`;
|
|
12353
|
+
}
|
|
12354
|
+
var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
|
|
12355
|
+
function posixOnlyShellProblems(command, field, platform2 = process.platform) {
|
|
12356
|
+
if (platform2 !== "win32" || !command?.trim()) return [];
|
|
12357
|
+
const problems = [];
|
|
12358
|
+
if (/(^|&&|\||;)\s*[A-Za-z_][A-Za-z0-9_]*=\S/.test(command)) {
|
|
11668
12359
|
problems.push(
|
|
11669
12360
|
`stage.${field} uses POSIX inline env assignment (VAR=value command) which fails in cmd.exe on Windows; use 'set VAR=value && command' or a cross-platform launcher`
|
|
11670
12361
|
);
|
|
@@ -11720,9 +12411,9 @@ async function shell(command, cwd, timeoutMs) {
|
|
|
11720
12411
|
});
|
|
11721
12412
|
}
|
|
11722
12413
|
function readState(path2) {
|
|
11723
|
-
if (!(0,
|
|
12414
|
+
if (!(0, import_node_fs19.existsSync)(path2)) return null;
|
|
11724
12415
|
try {
|
|
11725
|
-
return JSON.parse((0,
|
|
12416
|
+
return JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
|
|
11726
12417
|
} catch {
|
|
11727
12418
|
return null;
|
|
11728
12419
|
}
|
|
@@ -11774,7 +12465,7 @@ async function stopStage(opts = {}) {
|
|
|
11774
12465
|
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
11775
12466
|
}
|
|
11776
12467
|
await killTree(state.pid);
|
|
11777
|
-
(0,
|
|
12468
|
+
(0, import_node_fs19.rmSync)(statePath, { force: true });
|
|
11778
12469
|
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
11779
12470
|
}
|
|
11780
12471
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -11783,7 +12474,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
11783
12474
|
const cwd = opts.cwd ?? process.cwd();
|
|
11784
12475
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
11785
12476
|
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
11786
|
-
(0,
|
|
12477
|
+
(0, import_node_fs19.mkdirSync)(dir, { recursive: true });
|
|
11787
12478
|
let stagePort;
|
|
11788
12479
|
if (config.portRange) {
|
|
11789
12480
|
const [s, e] = config.portRange;
|
|
@@ -11793,14 +12484,14 @@ async function startStage(config = {}, opts = {}) {
|
|
|
11793
12484
|
}
|
|
11794
12485
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
11795
12486
|
if (config.ensureEnv) {
|
|
11796
|
-
const target = (0,
|
|
11797
|
-
const example = (0,
|
|
11798
|
-
if (!(0,
|
|
11799
|
-
(0,
|
|
11800
|
-
} else if ((0,
|
|
11801
|
-
const stale = detectStaleEnvFile((0,
|
|
11802
|
-
exampleMtimeMs: (0,
|
|
11803
|
-
targetMtimeMs: (0,
|
|
12487
|
+
const target = (0, import_node_path17.join)(cwd, config.ensureEnv.target);
|
|
12488
|
+
const example = (0, import_node_path17.join)(cwd, config.ensureEnv.example);
|
|
12489
|
+
if (!(0, import_node_fs19.existsSync)(target) && (0, import_node_fs19.existsSync)(example)) {
|
|
12490
|
+
(0, import_node_fs19.copyFileSync)(example, target);
|
|
12491
|
+
} else if ((0, import_node_fs19.existsSync)(target) && (0, import_node_fs19.existsSync)(example)) {
|
|
12492
|
+
const stale = detectStaleEnvFile((0, import_node_fs19.readFileSync)(example, "utf8"), (0, import_node_fs19.readFileSync)(target, "utf8"), {
|
|
12493
|
+
exampleMtimeMs: (0, import_node_fs19.statSync)(example).mtimeMs,
|
|
12494
|
+
targetMtimeMs: (0, import_node_fs19.statSync)(target).mtimeMs
|
|
11804
12495
|
});
|
|
11805
12496
|
if (stale) {
|
|
11806
12497
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -11808,14 +12499,14 @@ async function startStage(config = {}, opts = {}) {
|
|
|
11808
12499
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
11809
12500
|
}
|
|
11810
12501
|
}
|
|
11811
|
-
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0,
|
|
11812
|
-
(0,
|
|
12502
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs19.existsSync)(target)) {
|
|
12503
|
+
(0, import_node_fs19.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs19.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
11813
12504
|
}
|
|
11814
12505
|
}
|
|
11815
12506
|
const extraEnv = {};
|
|
11816
12507
|
for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
|
|
11817
12508
|
const up = sub(config.up.trim());
|
|
11818
|
-
const child = (0,
|
|
12509
|
+
const child = (0, import_node_child_process9.spawn)(up, {
|
|
11819
12510
|
cwd,
|
|
11820
12511
|
shell: true,
|
|
11821
12512
|
// POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
|
|
@@ -11834,13 +12525,13 @@ async function startStage(config = {}, opts = {}) {
|
|
|
11834
12525
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
11835
12526
|
port: stagePort
|
|
11836
12527
|
};
|
|
11837
|
-
(0,
|
|
12528
|
+
(0, import_node_fs19.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
11838
12529
|
try {
|
|
11839
12530
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
11840
12531
|
else await waitForProcessStability(child);
|
|
11841
12532
|
} catch (e) {
|
|
11842
12533
|
await killTree(state.pid);
|
|
11843
|
-
(0,
|
|
12534
|
+
(0, import_node_fs19.rmSync)(statePath, { force: true });
|
|
11844
12535
|
throw e;
|
|
11845
12536
|
}
|
|
11846
12537
|
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
@@ -11884,7 +12575,10 @@ async function reconcileDirtyOrgSpineBeforePull(deps, branch, options = {}) {
|
|
|
11884
12575
|
// src/git-clean-tree.ts
|
|
11885
12576
|
function isAgentScratchPath(path2) {
|
|
11886
12577
|
const normalized = path2.replace(/\\/g, "/").trim();
|
|
11887
|
-
|
|
12578
|
+
if (normalized === ".mmi" || normalized.startsWith(".mmi/")) {
|
|
12579
|
+
return true;
|
|
12580
|
+
}
|
|
12581
|
+
return /^tmp_[^/]+$/.test(normalized);
|
|
11888
12582
|
}
|
|
11889
12583
|
function porcelainHasBlockingChanges(porcelain) {
|
|
11890
12584
|
return porcelain.split("\n").some((line) => {
|
|
@@ -11895,6 +12589,40 @@ function porcelainHasBlockingChanges(porcelain) {
|
|
|
11895
12589
|
});
|
|
11896
12590
|
}
|
|
11897
12591
|
|
|
12592
|
+
// src/tenant-control-parse.ts
|
|
12593
|
+
var OUTPUT_BEGIN = "mmi-control-output-begin";
|
|
12594
|
+
var OUTPUT_END = "mmi-control-output-end";
|
|
12595
|
+
function logLinePayload(line) {
|
|
12596
|
+
const z = line.lastIndexOf("Z ");
|
|
12597
|
+
if (z >= 0) return line.slice(z + 2);
|
|
12598
|
+
return line;
|
|
12599
|
+
}
|
|
12600
|
+
function extractControlOutputFromLog(log) {
|
|
12601
|
+
const lines = log.split(/\r?\n/);
|
|
12602
|
+
const start = lines.findIndex((l) => logLinePayload(l).trim() === OUTPUT_BEGIN);
|
|
12603
|
+
if (start < 0) return "";
|
|
12604
|
+
const end = lines.findIndex((l, i) => i > start && logLinePayload(l).trim() === OUTPUT_END);
|
|
12605
|
+
const slice = end < 0 ? lines.slice(start + 1) : lines.slice(start + 1, end);
|
|
12606
|
+
return slice.map(logLinePayload).join("\n").trim();
|
|
12607
|
+
}
|
|
12608
|
+
function parseStatusSnippet(stdout) {
|
|
12609
|
+
const t = stdout.toLowerCase();
|
|
12610
|
+
const m = t.match(/service[:=]\s*(running|stopped|missing|up|down|absent)/);
|
|
12611
|
+
if (!m) return { serviceState: "unknown" };
|
|
12612
|
+
const token = m[1];
|
|
12613
|
+
if (token === "running" || token === "up") return { serviceState: "running" };
|
|
12614
|
+
if (token === "stopped" || token === "down") return { serviceState: "stopped" };
|
|
12615
|
+
return { serviceState: "missing" };
|
|
12616
|
+
}
|
|
12617
|
+
function parseVerifySecrets(stdout) {
|
|
12618
|
+
const out = [];
|
|
12619
|
+
for (const line of stdout.split("\n")) {
|
|
12620
|
+
const m = /^(\S+):\s*(match|mismatch|missing)\b/.exec(line.trim());
|
|
12621
|
+
if (m) out.push({ key: m[1], status: m[2] });
|
|
12622
|
+
}
|
|
12623
|
+
return out;
|
|
12624
|
+
}
|
|
12625
|
+
|
|
11898
12626
|
// src/train-apply.ts
|
|
11899
12627
|
function resolveDeployModel2(meta, repo) {
|
|
11900
12628
|
const m = meta?.deployModel;
|
|
@@ -11969,7 +12697,8 @@ var ORG_SPINE_FILES = [
|
|
|
11969
12697
|
".claude/settings.json",
|
|
11970
12698
|
".claude/output-styles/mmi-plain.md",
|
|
11971
12699
|
".cursor/rules/mmi-plain-language.mdc",
|
|
11972
|
-
".cursor/rules/mmi-tool-economy.mdc"
|
|
12700
|
+
".cursor/rules/mmi-tool-economy.mdc",
|
|
12701
|
+
".cursor/rules/mmi-code-economy.mdc"
|
|
11973
12702
|
];
|
|
11974
12703
|
function isSpinePath(path2) {
|
|
11975
12704
|
return ORG_SPINE_FILES.includes(path2);
|
|
@@ -12101,53 +12830,23 @@ function requireProjectMetaForTrain(load, repo) {
|
|
|
12101
12830
|
var CORRELATE_ATTEMPTS = 5;
|
|
12102
12831
|
var CORRELATE_DELAY_MS = 1500;
|
|
12103
12832
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
12833
|
+
var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12834
|
+
function resolveSleep(deps) {
|
|
12835
|
+
return deps.sleep ?? defaultSleep2;
|
|
12836
|
+
}
|
|
12104
12837
|
var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
|
|
12105
12838
|
var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
|
|
12106
12839
|
var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
12107
12840
|
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
12108
12841
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
12109
12842
|
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
12110
|
-
async function
|
|
12111
|
-
const
|
|
12112
|
-
const threshold = since - CORRELATE_SKEW_SLACK_MS;
|
|
12113
|
-
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
12114
|
-
if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
|
|
12115
|
-
let rows;
|
|
12116
|
-
try {
|
|
12117
|
-
const out = await deps.run("gh", [
|
|
12118
|
-
"run",
|
|
12119
|
-
"list",
|
|
12120
|
-
"--repo",
|
|
12121
|
-
HUB_REPO3,
|
|
12122
|
-
"--workflow",
|
|
12123
|
-
workflow,
|
|
12124
|
-
"--limit",
|
|
12125
|
-
"10",
|
|
12126
|
-
"--json",
|
|
12127
|
-
"databaseId,url,event,createdAt"
|
|
12128
|
-
]);
|
|
12129
|
-
rows = JSON.parse(out);
|
|
12130
|
-
} catch {
|
|
12131
|
-
continue;
|
|
12132
|
-
}
|
|
12133
|
-
const match = rows.filter((r) => r.event === "workflow_dispatch" && typeof r.databaseId === "number").map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
|
|
12134
|
-
if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
|
|
12135
|
-
}
|
|
12136
|
-
return {};
|
|
12137
|
-
}
|
|
12138
|
-
function correlateTenantRun(deps, since) {
|
|
12139
|
-
return correlateDispatchedRun(deps, "tenant-deploy.yml", since);
|
|
12140
|
-
}
|
|
12141
|
-
function correlatePublishRun(deps, since) {
|
|
12142
|
-
return correlateDispatchedRun(deps, "tenant-publish.yml", since);
|
|
12143
|
-
}
|
|
12144
|
-
async function correlateWorkflowRun(deps, args) {
|
|
12145
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
12843
|
+
async function correlateRun(deps, args) {
|
|
12844
|
+
const sleep2 = resolveSleep(deps);
|
|
12146
12845
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
12147
12846
|
let lastError;
|
|
12148
12847
|
let parsedAnyResponse = false;
|
|
12149
12848
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
12150
|
-
if (attempt > 0) await
|
|
12849
|
+
if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
|
|
12151
12850
|
const listArgs = [
|
|
12152
12851
|
"run",
|
|
12153
12852
|
"list",
|
|
@@ -12155,28 +12854,43 @@ async function correlateWorkflowRun(deps, args) {
|
|
|
12155
12854
|
HUB_REPO3,
|
|
12156
12855
|
"--workflow",
|
|
12157
12856
|
args.workflow,
|
|
12158
|
-
"--event",
|
|
12159
|
-
args.
|
|
12160
|
-
...args.branch ? ["--branch", args.branch] : [],
|
|
12857
|
+
...args.mode === "workflow" ? ["--event", args.event] : [],
|
|
12858
|
+
...args.mode === "workflow" && args.branch ? ["--branch", args.branch] : [],
|
|
12161
12859
|
"--limit",
|
|
12162
12860
|
"10",
|
|
12163
12861
|
"--json",
|
|
12164
|
-
"databaseId,url,event,createdAt,status,conclusion,headSha"
|
|
12862
|
+
args.mode === "dispatch" ? "databaseId,url,event,createdAt" : "databaseId,url,event,createdAt,status,conclusion,headSha"
|
|
12165
12863
|
];
|
|
12166
12864
|
let rows;
|
|
12167
12865
|
try {
|
|
12168
12866
|
rows = JSON.parse(await deps.run("gh", listArgs));
|
|
12169
12867
|
parsedAnyResponse = true;
|
|
12170
12868
|
} catch {
|
|
12171
|
-
lastError = new Error(`could not list ${args.workflow} runs`);
|
|
12869
|
+
if (args.mode === "workflow") lastError = new Error(`could not list ${args.workflow} runs`);
|
|
12172
12870
|
continue;
|
|
12173
12871
|
}
|
|
12174
|
-
const match = rows.filter((r) =>
|
|
12872
|
+
const match = rows.filter((r) => {
|
|
12873
|
+
if (typeof r.databaseId !== "number") return false;
|
|
12874
|
+
if (args.mode === "dispatch") return r.event === "workflow_dispatch";
|
|
12875
|
+
return r.event === args.event && r.headSha === args.headSha;
|
|
12876
|
+
}).map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
|
|
12175
12877
|
if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
|
|
12176
12878
|
}
|
|
12177
|
-
if (!parsedAnyResponse && lastError) throw lastError;
|
|
12879
|
+
if (args.mode === "workflow" && !parsedAnyResponse && lastError) throw lastError;
|
|
12178
12880
|
return {};
|
|
12179
12881
|
}
|
|
12882
|
+
function correlateTenantRun(deps, since) {
|
|
12883
|
+
return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch" });
|
|
12884
|
+
}
|
|
12885
|
+
function correlatePublishRun(deps, since) {
|
|
12886
|
+
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
|
|
12887
|
+
}
|
|
12888
|
+
function correlateControlRun(deps, since) {
|
|
12889
|
+
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch" });
|
|
12890
|
+
}
|
|
12891
|
+
async function correlateWorkflowRun(deps, args) {
|
|
12892
|
+
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
12893
|
+
}
|
|
12180
12894
|
async function watchTenantRun(deps, runId) {
|
|
12181
12895
|
if (runId == null) return "pending";
|
|
12182
12896
|
try {
|
|
@@ -12186,6 +12900,13 @@ async function watchTenantRun(deps, runId) {
|
|
|
12186
12900
|
return "failure";
|
|
12187
12901
|
}
|
|
12188
12902
|
}
|
|
12903
|
+
async function fetchControlRunLog(deps, runId) {
|
|
12904
|
+
try {
|
|
12905
|
+
return await deps.run("gh", ["run", "view", String(runId), "--repo", HUB_REPO3, "--log"]);
|
|
12906
|
+
} catch {
|
|
12907
|
+
return "";
|
|
12908
|
+
}
|
|
12909
|
+
}
|
|
12189
12910
|
async function watchWorkflowRun(deps, workflow, run) {
|
|
12190
12911
|
if (run.runId == null) return { workflow, conclusion: "pending" };
|
|
12191
12912
|
const conclusion = await watchTenantRun(deps, run.runId);
|
|
@@ -12286,11 +13007,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
12286
13007
|
if (required.length === 0) {
|
|
12287
13008
|
return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
|
|
12288
13009
|
}
|
|
12289
|
-
const
|
|
13010
|
+
const sleep2 = resolveSleep(deps);
|
|
12290
13011
|
let lastStatus = "not checked";
|
|
12291
13012
|
let lastError;
|
|
12292
13013
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
12293
|
-
if (attempt > 0) await
|
|
13014
|
+
if (attempt > 0) await sleep2(TRAIN_CHECK_DELAY_MS);
|
|
12294
13015
|
let checkRuns;
|
|
12295
13016
|
let statuses;
|
|
12296
13017
|
try {
|
|
@@ -12366,18 +13087,77 @@ function isTransientDispatchFailure(e) {
|
|
|
12366
13087
|
return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
|
|
12367
13088
|
}
|
|
12368
13089
|
async function dispatchTenantDeployWithRetry(deps, input) {
|
|
12369
|
-
const
|
|
13090
|
+
const sleep2 = resolveSleep(deps);
|
|
12370
13091
|
for (let attempt = 1; ; attempt++) {
|
|
12371
13092
|
try {
|
|
12372
13093
|
await deps.dispatchTenantDeploy(input);
|
|
12373
13094
|
return;
|
|
12374
13095
|
} catch (e) {
|
|
12375
13096
|
if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
|
|
12376
|
-
await
|
|
13097
|
+
await sleep2(DISPATCH_RETRY_DELAY_MS * attempt);
|
|
12377
13098
|
}
|
|
12378
13099
|
}
|
|
12379
13100
|
}
|
|
12380
|
-
|
|
13101
|
+
function tenantPublishRecoveryCommand(slug, repo, ref, stage2, publishDir) {
|
|
13102
|
+
const parts = [
|
|
13103
|
+
`gh workflow run tenant-publish.yml --repo ${HUB_REPO3}`,
|
|
13104
|
+
`-f slug=${slug}`,
|
|
13105
|
+
`-f repo=${repo}`,
|
|
13106
|
+
`-f ref=${ref}`,
|
|
13107
|
+
`-f stage=${stage2}`
|
|
13108
|
+
];
|
|
13109
|
+
if (publishDir && publishDir !== ".") parts.push(`-f publishDir=${publishDir}`);
|
|
13110
|
+
return parts.join(" ");
|
|
13111
|
+
}
|
|
13112
|
+
async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure = "throw", publishDir) {
|
|
13113
|
+
const since = (deps.now ?? Date.now)();
|
|
13114
|
+
const dispatchArgs = [
|
|
13115
|
+
"workflow",
|
|
13116
|
+
"run",
|
|
13117
|
+
"tenant-publish.yml",
|
|
13118
|
+
"--repo",
|
|
13119
|
+
HUB_REPO3,
|
|
13120
|
+
"-f",
|
|
13121
|
+
`slug=${ctx.slug}`,
|
|
13122
|
+
"-f",
|
|
13123
|
+
`repo=${ctx.repo}`,
|
|
13124
|
+
"-f",
|
|
13125
|
+
`ref=${ref}`,
|
|
13126
|
+
"-f",
|
|
13127
|
+
`stage=${stage2}`
|
|
13128
|
+
];
|
|
13129
|
+
if (publishDir && publishDir !== ".") dispatchArgs.push("-f", `publishDir=${publishDir}`);
|
|
13130
|
+
try {
|
|
13131
|
+
await deps.run("gh", dispatchArgs);
|
|
13132
|
+
} catch (e) {
|
|
13133
|
+
if (dispatchFailure === "throw") throw e;
|
|
13134
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13135
|
+
const recovery = tenantPublishRecoveryCommand(ctx.slug, ctx.repo, ref, stage2, publishDir);
|
|
13136
|
+
return {
|
|
13137
|
+
note: `tenant-publish dispatch FAILED: ${msg}. The promotion itself landed \u2014 recover with \`${recovery}\``,
|
|
13138
|
+
deployStatus: "failure"
|
|
13139
|
+
};
|
|
13140
|
+
}
|
|
13141
|
+
const { runId, runUrl } = await correlatePublishRun(deps, since);
|
|
13142
|
+
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
13143
|
+
return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
13144
|
+
}
|
|
13145
|
+
async function dispatchPublishIfRequired(deps, ctx, meta, model, stage2, publishRef, watch, dispatchFailure) {
|
|
13146
|
+
if (!meta.publishRequired || stage2 !== "main") return null;
|
|
13147
|
+
if (model !== "tenant-container" && model !== "solo-container") return null;
|
|
13148
|
+
return dispatchTenantPublish(deps, ctx, stage2, publishRef, watch, dispatchFailure, meta.publishDir);
|
|
13149
|
+
}
|
|
13150
|
+
function appendPublishDispatch(deploy, publish) {
|
|
13151
|
+
if (!publish) return deploy;
|
|
13152
|
+
return {
|
|
13153
|
+
note: `${deploy.note}; ${publish.note}`,
|
|
13154
|
+
runId: deploy.runId,
|
|
13155
|
+
runUrl: deploy.runUrl,
|
|
13156
|
+
workflowRuns: [...deploy.workflowRuns ?? [], ...publish.workflowRuns ?? [{ workflow: "tenant-publish.yml", runId: publish.runId, runUrl: publish.runUrl, conclusion: publish.deployStatus }]],
|
|
13157
|
+
deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
|
|
13158
|
+
};
|
|
13159
|
+
}
|
|
13160
|
+
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
12381
13161
|
if (model === "tenant-container" || model === "solo-container") {
|
|
12382
13162
|
const since = (deps.now ?? Date.now)();
|
|
12383
13163
|
try {
|
|
@@ -12395,25 +13175,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
12395
13175
|
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
12396
13176
|
}
|
|
12397
13177
|
if (model === "registry-publish") {
|
|
12398
|
-
|
|
12399
|
-
await deps.run("gh", [
|
|
12400
|
-
"workflow",
|
|
12401
|
-
"run",
|
|
12402
|
-
"tenant-publish.yml",
|
|
12403
|
-
"--repo",
|
|
12404
|
-
HUB_REPO3,
|
|
12405
|
-
"-f",
|
|
12406
|
-
`slug=${ctx.slug}`,
|
|
12407
|
-
"-f",
|
|
12408
|
-
`repo=${ctx.repo}`,
|
|
12409
|
-
"-f",
|
|
12410
|
-
`ref=${ref}`,
|
|
12411
|
-
"-f",
|
|
12412
|
-
`stage=${stage2}`
|
|
12413
|
-
]);
|
|
12414
|
-
const { runId, runUrl } = await correlatePublishRun(deps, since);
|
|
12415
|
-
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
12416
|
-
return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
13178
|
+
return dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure, publishDir);
|
|
12417
13179
|
}
|
|
12418
13180
|
if (model === "hub-serverless") {
|
|
12419
13181
|
const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
|
|
@@ -12455,107 +13217,99 @@ async function preflight(deps, ctx, stage2, meta) {
|
|
|
12455
13217
|
await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
|
|
12456
13218
|
return model;
|
|
12457
13219
|
}
|
|
12458
|
-
async function
|
|
12459
|
-
const
|
|
12460
|
-
const
|
|
12461
|
-
await
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
|
|
12466
|
-
if (command === "rcand") {
|
|
12467
|
-
await requireBranch(deps, "development");
|
|
12468
|
-
if (directTrack) {
|
|
12469
|
-
throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
|
|
12470
|
-
}
|
|
12471
|
-
await ffOnlyPull(deps, "development");
|
|
12472
|
-
ensurePositiveCount(
|
|
12473
|
-
await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
|
|
12474
|
-
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
12475
|
-
);
|
|
12476
|
-
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
12477
|
-
const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
12478
|
-
await deps.run("git", ["checkout", "rc"]);
|
|
12479
|
-
await ffOnlyPull(deps, "rc");
|
|
12480
|
-
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
12481
|
-
const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
12482
|
-
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
12483
|
-
const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
12484
|
-
const resumeNote = resume.tag ? resume.note : void 0;
|
|
12485
|
-
await ensureTagPushed(deps, tag2, rcSha);
|
|
12486
|
-
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
12487
|
-
const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks2);
|
|
12488
|
-
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
12489
|
-
await deps.run("git", ["push", "origin", "rc"]);
|
|
12490
|
-
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince2, rcSha);
|
|
12491
|
-
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
|
|
13220
|
+
async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
|
|
13221
|
+
const foldPaths = await resolveFoldPaths(deps, deployModel);
|
|
13222
|
+
const tolerated = [...foldPaths, ...RELEASE_TOLERATED_PATHS];
|
|
13223
|
+
const predicted = await predictMergeConflicts(deps, "origin/main", remoteRef);
|
|
13224
|
+
const predictedBlocking = predicted.filter((f) => !isSpinePath(f) && !tolerated.includes(f));
|
|
13225
|
+
if (predictedBlocking.length > 0) {
|
|
13226
|
+
throw new Error(`${blockingPrefix}: ${predictedBlocking.join(", ")} \u2014 no merge was started. ${realignMessage}`);
|
|
12492
13227
|
}
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
);
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
...ctx,
|
|
12532
|
-
command,
|
|
12533
|
-
stage: "main",
|
|
12534
|
-
ref: "main",
|
|
12535
|
-
tag: tag2,
|
|
12536
|
-
deployModel: deployModel2,
|
|
12537
|
-
promoted: true,
|
|
12538
|
-
checks: checks2,
|
|
12539
|
-
versionFold: versionFold2,
|
|
12540
|
-
dispatch: d2.note,
|
|
12541
|
-
runId: d2.runId,
|
|
12542
|
-
runUrl: d2.runUrl,
|
|
12543
|
-
workflowRuns: d2.workflowRuns,
|
|
12544
|
-
deployStatus: d2.deployStatus,
|
|
12545
|
-
rcRetirement: "not-applicable",
|
|
12546
|
-
rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
|
|
12547
|
-
devRollForward: devRollForward2,
|
|
12548
|
-
announceNote: announceNote2,
|
|
12549
|
-
// #1062: --dev on a direct-track repo is a friendly no-op — it already releases from development.
|
|
12550
|
-
devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
|
|
12551
|
-
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
13228
|
+
return { foldPaths, tolerated, predicted };
|
|
13229
|
+
}
|
|
13230
|
+
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
|
|
13231
|
+
await deps.run("git", ["checkout", "main"]);
|
|
13232
|
+
await ffOnlyPull(deps, "main");
|
|
13233
|
+
if (predicted.length === 0) {
|
|
13234
|
+
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
13235
|
+
} else {
|
|
13236
|
+
await mergeWithSpineResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
|
|
13237
|
+
}
|
|
13238
|
+
}
|
|
13239
|
+
async function mergeSourceToMain(deps, deployModel, args) {
|
|
13240
|
+
const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
|
|
13241
|
+
deps,
|
|
13242
|
+
deployModel,
|
|
13243
|
+
args.remoteRef,
|
|
13244
|
+
args.blockingPrefix,
|
|
13245
|
+
args.realignMessage
|
|
13246
|
+
);
|
|
13247
|
+
await executeMergeToMain(deps, args.sourceRef, args.mergeLabel, tolerated, predicted);
|
|
13248
|
+
return { foldPaths };
|
|
13249
|
+
}
|
|
13250
|
+
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
|
|
13251
|
+
await ensureTagPushed(deps, tag, releaseSha);
|
|
13252
|
+
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
13253
|
+
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
13254
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
13255
|
+
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
13256
|
+
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
13257
|
+
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
13258
|
+
const autoRunSince = (deps.now ?? Date.now)();
|
|
13259
|
+
const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
|
|
13260
|
+
const publishDispatch = deployDispatch.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report");
|
|
13261
|
+
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
13262
|
+
if (!publishDispatch && deployDispatch.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
13263
|
+
dispatch = {
|
|
13264
|
+
...dispatch,
|
|
13265
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
|
|
12552
13266
|
};
|
|
12553
13267
|
}
|
|
12554
|
-
|
|
13268
|
+
return { checks, releaseUrl, announceNote, dispatch };
|
|
13269
|
+
}
|
|
13270
|
+
async function pushRcAlignment(deps) {
|
|
13271
|
+
try {
|
|
13272
|
+
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
13273
|
+
return "origin/rc aligned to the released main";
|
|
13274
|
+
} catch (e) {
|
|
13275
|
+
return `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
|
|
13276
|
+
}
|
|
13277
|
+
}
|
|
13278
|
+
async function runTrainApplyPipeline(mode, input) {
|
|
13279
|
+
const { deps, ctx, command, meta, branchHints, watch, options } = input;
|
|
13280
|
+
const directTrack = input.directTrack ?? false;
|
|
13281
|
+
if (mode === "rcand") {
|
|
13282
|
+
await requireBranch(deps, "development");
|
|
13283
|
+
if (directTrack) {
|
|
13284
|
+
throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
|
|
13285
|
+
}
|
|
13286
|
+
await ffOnlyPull(deps, "development");
|
|
13287
|
+
ensurePositiveCount(
|
|
13288
|
+
await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
|
|
13289
|
+
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
13290
|
+
);
|
|
13291
|
+
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
13292
|
+
const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
13293
|
+
await deps.run("git", ["checkout", "rc"]);
|
|
13294
|
+
await ffOnlyPull(deps, "rc");
|
|
13295
|
+
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
13296
|
+
const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
13297
|
+
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
13298
|
+
const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
13299
|
+
const resumeNote = resume.tag ? resume.note : void 0;
|
|
13300
|
+
await ensureTagPushed(deps, tag2, rcSha);
|
|
13301
|
+
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
13302
|
+
const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
|
|
13303
|
+
const autoRunSince = (deps.now ?? Date.now)();
|
|
13304
|
+
await deps.run("git", ["push", "origin", "rc"]);
|
|
13305
|
+
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
|
|
13306
|
+
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
|
|
13307
|
+
}
|
|
13308
|
+
if (mode === "release-dev") {
|
|
12555
13309
|
await requireBranch(deps, "development");
|
|
12556
13310
|
await ffOnlyPull(deps, "development");
|
|
12557
13311
|
const hasRcBranch = branchHints.hasRcBranch ?? false;
|
|
12558
|
-
if (hasRcBranch) {
|
|
13312
|
+
if (!directTrack && hasRcBranch) {
|
|
12559
13313
|
const rcOnlyOut = (await deps.run("git", ["rev-list", "--count", "--right-only", "--cherry-pick", "--no-merges", "origin/development...origin/rc"])).trim();
|
|
12560
13314
|
const rcOnly = Number.parseInt(rcOnlyOut, 10);
|
|
12561
13315
|
if (!Number.isFinite(rcOnly)) throw new Error(`release --dev: could not count rc-only commits for the guard: ${rcOnlyOut || "(empty)"}`);
|
|
@@ -12571,47 +13325,44 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12571
13325
|
);
|
|
12572
13326
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
12573
13327
|
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
12574
|
-
const rcShaAtRelease = hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
12575
|
-
const foldPaths2 = await
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
);
|
|
12583
|
-
}
|
|
12584
|
-
await deps.run("git", ["checkout", "main"]);
|
|
12585
|
-
await ffOnlyPull(deps, "main");
|
|
12586
|
-
if (predicted2.length === 0) {
|
|
12587
|
-
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
12588
|
-
} else {
|
|
12589
|
-
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
12590
|
-
}
|
|
13328
|
+
const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
13329
|
+
const { foldPaths: foldPaths2 } = await mergeSourceToMain(deps, deployModel2, {
|
|
13330
|
+
sourceRef: "development",
|
|
13331
|
+
remoteRef: "origin/development",
|
|
13332
|
+
mergeLabel: "development -> main",
|
|
13333
|
+
blockingPrefix: "development -> main merge would conflict on non-spine path(s)",
|
|
13334
|
+
realignMessage: "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
|
|
13335
|
+
});
|
|
12591
13336
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
12592
13337
|
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
12593
|
-
await
|
|
12594
|
-
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
12595
|
-
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
12596
|
-
await deps.run("git", ["push", "origin", "main"]);
|
|
12597
|
-
const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
12598
|
-
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
12599
|
-
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
12600
|
-
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
12601
|
-
const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
|
|
12602
|
-
const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
|
|
13338
|
+
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
|
|
12603
13339
|
const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
13340
|
+
if (directTrack) {
|
|
13341
|
+
return {
|
|
13342
|
+
...ctx,
|
|
13343
|
+
command,
|
|
13344
|
+
stage: "main",
|
|
13345
|
+
ref: "main",
|
|
13346
|
+
tag: tag2,
|
|
13347
|
+
deployModel: deployModel2,
|
|
13348
|
+
promoted: true,
|
|
13349
|
+
checks: checks2,
|
|
13350
|
+
versionFold: versionFold2,
|
|
13351
|
+
dispatch: d2.note,
|
|
13352
|
+
runId: d2.runId,
|
|
13353
|
+
runUrl: d2.runUrl,
|
|
13354
|
+
workflowRuns: d2.workflowRuns,
|
|
13355
|
+
deployStatus: d2.deployStatus,
|
|
13356
|
+
rcRetirement: "not-applicable",
|
|
13357
|
+
rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
|
|
13358
|
+
devRollForward: devRollForward2,
|
|
13359
|
+
announceNote: announceNote2,
|
|
13360
|
+
devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
|
|
13361
|
+
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
13362
|
+
};
|
|
12614
13363
|
}
|
|
13364
|
+
const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
|
|
13365
|
+
const rcAlignment2 = hasRcBranch ? await pushRcAlignment(deps) : "no origin/rc branch \u2014 rc alignment skipped";
|
|
12615
13366
|
const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
|
|
12616
13367
|
return {
|
|
12617
13368
|
...ctx,
|
|
@@ -12645,15 +13396,13 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12645
13396
|
"nothing to release: origin/rc is not ahead of origin/main"
|
|
12646
13397
|
);
|
|
12647
13398
|
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
12648
|
-
const foldPaths = await
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
);
|
|
12656
|
-
}
|
|
13399
|
+
const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
|
|
13400
|
+
deps,
|
|
13401
|
+
deployModel,
|
|
13402
|
+
"origin/rc",
|
|
13403
|
+
"rc -> main merge would conflict on non-spine path(s)",
|
|
13404
|
+
"The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release."
|
|
13405
|
+
);
|
|
12657
13406
|
const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
|
|
12658
13407
|
if (!coverage.ok) {
|
|
12659
13408
|
const list = coverage.uncovered.map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join("; ");
|
|
@@ -12662,34 +13411,14 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12662
13411
|
);
|
|
12663
13412
|
}
|
|
12664
13413
|
const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
12665
|
-
await deps
|
|
12666
|
-
await ffOnlyPull(deps, "main");
|
|
12667
|
-
if (predicted.length === 0) {
|
|
12668
|
-
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
12669
|
-
} else {
|
|
12670
|
-
await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", tolerated);
|
|
12671
|
-
}
|
|
13414
|
+
await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted);
|
|
12672
13415
|
const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
12673
13416
|
const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
12674
13417
|
const releaseSha = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
12675
|
-
await
|
|
12676
|
-
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
12677
|
-
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
12678
|
-
await deps.run("git", ["push", "origin", "main"]);
|
|
12679
|
-
const autoRunSince = (deps.now ?? Date.now)();
|
|
12680
|
-
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
12681
|
-
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
12682
|
-
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
12683
|
-
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
|
|
13418
|
+
const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
|
|
12684
13419
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
12685
13420
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
12686
|
-
|
|
12687
|
-
try {
|
|
12688
|
-
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
12689
|
-
rcAlignment = "origin/rc aligned to the released main";
|
|
12690
|
-
} catch (e) {
|
|
12691
|
-
rcAlignment = `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
|
|
12692
|
-
}
|
|
13421
|
+
const rcAlignment = await pushRcAlignment(deps);
|
|
12693
13422
|
const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
|
|
12694
13423
|
return {
|
|
12695
13424
|
...ctx,
|
|
@@ -12716,6 +13445,29 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12716
13445
|
environments
|
|
12717
13446
|
};
|
|
12718
13447
|
}
|
|
13448
|
+
async function runTrainApply(command, deps, options = {}) {
|
|
13449
|
+
const watch = options.watch ?? false;
|
|
13450
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
13451
|
+
await requireCleanTree(deps);
|
|
13452
|
+
await deps.run("git", ["fetch", "origin"]);
|
|
13453
|
+
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
13454
|
+
const branchHints = await loadReleaseTrackBranchHints(deps);
|
|
13455
|
+
const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
|
|
13456
|
+
const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options };
|
|
13457
|
+
if (command === "rcand") {
|
|
13458
|
+
if (directTrack) {
|
|
13459
|
+
throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
|
|
13460
|
+
}
|
|
13461
|
+
return runTrainApplyPipeline("rcand", pipelineInput);
|
|
13462
|
+
}
|
|
13463
|
+
if (directTrack) {
|
|
13464
|
+
return runTrainApplyPipeline("release-dev", { ...pipelineInput, directTrack: true });
|
|
13465
|
+
}
|
|
13466
|
+
if (command === "release" && options.dev) {
|
|
13467
|
+
return runTrainApplyPipeline("release-dev", pipelineInput);
|
|
13468
|
+
}
|
|
13469
|
+
return runTrainApplyPipeline("release-full", pipelineInput);
|
|
13470
|
+
}
|
|
12719
13471
|
async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
|
|
12720
13472
|
if (model !== "tenant-container") return void 0;
|
|
12721
13473
|
const domains = deps.fetchEdgeDomains ? await deps.fetchEdgeDomains(ctx.slug).catch(() => null) : null;
|
|
@@ -12732,63 +13484,25 @@ async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
|
|
|
12732
13484
|
if (rcDomains?.length) rc.domains = rcDomains;
|
|
12733
13485
|
return { main, rc };
|
|
12734
13486
|
}
|
|
12735
|
-
var RETIRE_CATEGORIES = /* @__PURE__ */ new Set([
|
|
12736
|
-
"retired",
|
|
12737
|
-
"retired-edge-pending",
|
|
12738
|
-
"ssm-command-failed",
|
|
12739
|
-
"wait-timeout",
|
|
12740
|
-
"transport-failed"
|
|
12741
|
-
]);
|
|
12742
|
-
function retireCategoryFrom(text) {
|
|
12743
|
-
try {
|
|
12744
|
-
const c = JSON.parse(text).category;
|
|
12745
|
-
return typeof c === "string" && RETIRE_CATEGORIES.has(c) ? c : void 0;
|
|
12746
|
-
} catch {
|
|
12747
|
-
return void 0;
|
|
12748
|
-
}
|
|
12749
|
-
}
|
|
12750
|
-
var TRANSPORT_REASONS = /* @__PURE__ */ new Set(["invalid-instance", "invalid-document", "throttled", "timeout", "other"]);
|
|
12751
|
-
function retireReasonFrom(text) {
|
|
12752
|
-
try {
|
|
12753
|
-
const r = JSON.parse(text).reason;
|
|
12754
|
-
return typeof r === "string" && TRANSPORT_REASONS.has(r) ? r : void 0;
|
|
12755
|
-
} catch {
|
|
12756
|
-
return void 0;
|
|
12757
|
-
}
|
|
12758
|
-
}
|
|
12759
|
-
function isRetryableTransport(reason) {
|
|
12760
|
-
return reason === void 0 || reason === "throttled" || reason === "timeout" || reason === "other";
|
|
12761
|
-
}
|
|
12762
13487
|
var RETIRE_MAX_ATTEMPTS = 3;
|
|
12763
13488
|
var RETIRE_BACKOFF_MS = 1500;
|
|
12764
13489
|
async function attemptRetire(deps, ctx) {
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
let category = retireCategoryFrom(out);
|
|
12769
|
-
try {
|
|
12770
|
-
commandId = String(JSON.parse(out).commandId ?? "");
|
|
12771
|
-
} catch {
|
|
12772
|
-
}
|
|
12773
|
-
if (category === "retired-edge-pending") {
|
|
12774
|
-
return { result: {
|
|
12775
|
-
status: "retired",
|
|
12776
|
-
category,
|
|
12777
|
-
note: `rc runtime retired; edge vhost reconcile pending (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept`
|
|
12778
|
-
} };
|
|
12779
|
-
}
|
|
12780
|
-
category = category ?? "retired";
|
|
12781
|
-
return { result: {
|
|
13490
|
+
const r = await runTenantControl(deps, { repo: ctx.repo, stage: "rc", action: "retire", watch: true });
|
|
13491
|
+
if (r.category === "retired") {
|
|
13492
|
+
return {
|
|
12782
13493
|
status: "retired",
|
|
12783
|
-
category,
|
|
12784
|
-
note: `rc runtime retired (tenant
|
|
12785
|
-
}
|
|
12786
|
-
}
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
13494
|
+
category: "retired",
|
|
13495
|
+
note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
13496
|
+
};
|
|
13497
|
+
}
|
|
13498
|
+
if (r.category === "wait-timeout") {
|
|
13499
|
+
return {
|
|
13500
|
+
status: "failed",
|
|
13501
|
+
category: "wait-timeout",
|
|
13502
|
+
note: `rc retire dispatched but the run could not be observed \u2014 verify with: mmi-cli tenant control ${ctx.repo} rc status${r.runUrl ? ` (run ${r.runUrl})` : ""}`
|
|
13503
|
+
};
|
|
12791
13504
|
}
|
|
13505
|
+
return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
|
|
12792
13506
|
}
|
|
12793
13507
|
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
12794
13508
|
if (model !== "tenant-container") {
|
|
@@ -12812,19 +13526,18 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
12812
13526
|
note: `origin/rc moved past the released candidate (${releasedRcSha.slice(0, 7)} -> ${rcNow.slice(0, 7)}) \u2014 a new candidate is in flight; rc runtime left untouched`
|
|
12813
13527
|
};
|
|
12814
13528
|
}
|
|
12815
|
-
const
|
|
13529
|
+
const sleep2 = resolveSleep(deps);
|
|
12816
13530
|
let last;
|
|
12817
13531
|
for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
|
|
12818
13532
|
last = await attemptRetire(deps, ctx);
|
|
12819
|
-
if (last.
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
await sleep(RETIRE_BACKOFF_MS * attempt);
|
|
13533
|
+
if (last.status === "retired") return last;
|
|
13534
|
+
if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
|
|
13535
|
+
await sleep2(RETIRE_BACKOFF_MS * attempt);
|
|
12823
13536
|
}
|
|
12824
13537
|
const f = last;
|
|
12825
|
-
|
|
12826
|
-
const note = `rc retirement failed (the release itself succeeded)
|
|
12827
|
-
return { status: "failed", category: f.
|
|
13538
|
+
if (f.category === "wait-timeout") return f;
|
|
13539
|
+
const note = `rc retirement failed (the release itself succeeded): ${f.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli tenant sweep-rc --retire --yes)`;
|
|
13540
|
+
return { status: "failed", category: f.category, note };
|
|
12828
13541
|
} catch (e) {
|
|
12829
13542
|
const err = e;
|
|
12830
13543
|
return { status: "failed", category: "transport-failed", note: `rc retirement failed (the release itself succeeded): ${err.message}` };
|
|
@@ -12850,14 +13563,82 @@ async function runTenantRedeploy(deps, options) {
|
|
|
12850
13563
|
const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
|
|
12851
13564
|
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
|
|
12852
13565
|
}
|
|
13566
|
+
function tenantControlWatches(action) {
|
|
13567
|
+
return action === "status" || action === "retire" || action === "verify-secrets";
|
|
13568
|
+
}
|
|
13569
|
+
async function runTenantControl(deps, options) {
|
|
13570
|
+
const { repo, stage: stage2, action } = options;
|
|
13571
|
+
const watch = options.watch ?? tenantControlWatches(action);
|
|
13572
|
+
const base2 = { command: "tenant-control", repo, stage: stage2, action };
|
|
13573
|
+
const since = (deps.now ?? Date.now)();
|
|
13574
|
+
const d = await deps.dispatchTenantControl({ repo, stage: stage2, action });
|
|
13575
|
+
if (!d.ok) {
|
|
13576
|
+
const transport = d.category === "transport-failed";
|
|
13577
|
+
return {
|
|
13578
|
+
...base2,
|
|
13579
|
+
dispatched: false,
|
|
13580
|
+
conclusion: "failure",
|
|
13581
|
+
category: action === "retire" ? transport ? "transport-failed" : "dispatch-rejected" : void 0,
|
|
13582
|
+
note: transport ? `tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
|
|
13583
|
+
};
|
|
13584
|
+
}
|
|
13585
|
+
const { runId, runUrl } = await correlateControlRun(deps, since);
|
|
13586
|
+
const conclusion = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
13587
|
+
const result = { ...base2, dispatched: true, runId, runUrl, conclusion, note: "" };
|
|
13588
|
+
if (action === "retire") {
|
|
13589
|
+
result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
|
|
13590
|
+
}
|
|
13591
|
+
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
|
|
13592
|
+
const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
|
|
13593
|
+
if (action === "status") {
|
|
13594
|
+
result.serviceState = parseStatusSnippet(output).serviceState;
|
|
13595
|
+
} else {
|
|
13596
|
+
result.secrets = parseVerifySecrets(output);
|
|
13597
|
+
}
|
|
13598
|
+
}
|
|
13599
|
+
result.note = conclusion === "success" ? `tenant-control ${action} run succeeded` : conclusion === "failure" ? `tenant-control ${action} run failed \u2014 inspect the run` : runId == null ? `dispatched tenant-control.yml (${action}) \u2014 run not correlated; check the Actions tab` : `dispatched tenant-control.yml (${action}) \u2014 not watched`;
|
|
13600
|
+
return result;
|
|
13601
|
+
}
|
|
13602
|
+
function renderTenantControl(r) {
|
|
13603
|
+
const head = `tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
|
|
13604
|
+
const lines = [head];
|
|
13605
|
+
if (r.runUrl) lines.push(` run: ${r.runUrl}`);
|
|
13606
|
+
if (r.serviceState) lines.push(` serviceState: ${r.serviceState}`);
|
|
13607
|
+
if (r.secrets?.length) {
|
|
13608
|
+
for (const s of r.secrets) lines.push(` ${s.key}: ${s.status}`);
|
|
13609
|
+
}
|
|
13610
|
+
lines.push(` ${r.note}`);
|
|
13611
|
+
return lines.join("\n");
|
|
13612
|
+
}
|
|
13613
|
+
|
|
13614
|
+
// src/tenant-verify-secrets.ts
|
|
13615
|
+
function renderVerifySecrets(body) {
|
|
13616
|
+
const secrets = body?.secrets ?? [];
|
|
13617
|
+
const counts = {
|
|
13618
|
+
match: secrets.filter((s) => s.status === "match").length,
|
|
13619
|
+
mismatch: secrets.filter((s) => s.status === "mismatch").length,
|
|
13620
|
+
missing: secrets.filter((s) => s.status === "missing").length
|
|
13621
|
+
};
|
|
13622
|
+
const lines = secrets.map((s) => `${s.key}: ${s.status}`);
|
|
13623
|
+
lines.push(`verify-secrets: ${counts.match} match, ${counts.mismatch} mismatch, ${counts.missing} missing`);
|
|
13624
|
+
const ssmStatus = body?.ssmStatus ?? "pending";
|
|
13625
|
+
if (ssmStatus !== "Success") {
|
|
13626
|
+
return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
|
|
13627
|
+
}
|
|
13628
|
+
const bad = counts.mismatch + counts.missing;
|
|
13629
|
+
if (bad > 0) {
|
|
13630
|
+
return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
|
|
13631
|
+
}
|
|
13632
|
+
return { lines, failure: null };
|
|
13633
|
+
}
|
|
12853
13634
|
|
|
12854
13635
|
// src/hotfix-coverage.ts
|
|
12855
|
-
var
|
|
13636
|
+
var import_node_child_process10 = require("node:child_process");
|
|
12856
13637
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
12857
13638
|
function checkHotfixCoverage(options = {}) {
|
|
12858
13639
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
12859
13640
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
12860
|
-
const git = options.git ?? ((args, opts) => (0,
|
|
13641
|
+
const git = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
12861
13642
|
const revList = (range) => {
|
|
12862
13643
|
const out = git(["rev-list", "--no-merges", range]).trim();
|
|
12863
13644
|
return out ? out.split("\n") : [];
|
|
@@ -12984,6 +13765,10 @@ function renderSweep(r) {
|
|
|
12984
13765
|
if (r.running > 0 && !r.retireAttempted) {
|
|
12985
13766
|
lines.push("Retire an orphan with: mmi-cli tenant control <owner/repo> rc retire (or sweep all running: mmi-cli tenant sweep-rc --retire --yes)");
|
|
12986
13767
|
}
|
|
13768
|
+
const undetermined = r.stages.filter((s) => s.serviceState === "unknown").length;
|
|
13769
|
+
if (undetermined > 0) {
|
|
13770
|
+
lines.push(`WARNING: rc running-state could not be determined for ${undetermined} of ${r.scanned} tenant(s) \u2014 the "${r.running} running" count is a floor, not a clear bill. Reconcile the box assets (tenant-reconcile.yml) or inspect the tenant-control run log.`);
|
|
13771
|
+
}
|
|
12987
13772
|
return lines.join("\n");
|
|
12988
13773
|
}
|
|
12989
13774
|
|
|
@@ -13017,7 +13802,7 @@ function hotfixBranch(tag) {
|
|
|
13017
13802
|
}
|
|
13018
13803
|
async function resolveHotfixDeployModel(deps, ctx) {
|
|
13019
13804
|
const load = await loadProjectMeta(deps, ctx);
|
|
13020
|
-
const meta = load
|
|
13805
|
+
const meta = requireProjectMetaForTrain(load, ctx.repo);
|
|
13021
13806
|
return resolveDeployModel2(meta, ctx.repo);
|
|
13022
13807
|
}
|
|
13023
13808
|
async function findHotfixPr(deps, ctx, tag) {
|
|
@@ -13132,9 +13917,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
|
|
|
13132
13917
|
return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
|
|
13133
13918
|
}
|
|
13134
13919
|
async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
13135
|
-
const
|
|
13920
|
+
const sleep2 = sleeper(deps);
|
|
13136
13921
|
for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
|
|
13137
|
-
if (attempt > 0) await
|
|
13922
|
+
if (attempt > 0) await sleep2(HOTFIX_RUN_FIND_DELAY_MS);
|
|
13138
13923
|
let rows;
|
|
13139
13924
|
try {
|
|
13140
13925
|
const out = await deps.run("gh", [
|
|
@@ -13209,8 +13994,9 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13209
13994
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
13210
13995
|
}
|
|
13211
13996
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
13212
|
-
} else if (deployModel === "tenant-container" || deployModel === "solo-container") {
|
|
13213
|
-
const
|
|
13997
|
+
} else if (deployModel === "tenant-container" || deployModel === "solo-container" || deployModel === "registry-publish") {
|
|
13998
|
+
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
13999
|
+
const deploy = await dispatchDeploy(
|
|
13214
14000
|
deps,
|
|
13215
14001
|
ctx,
|
|
13216
14002
|
"main",
|
|
@@ -13219,14 +14005,38 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13219
14005
|
true,
|
|
13220
14006
|
(deps.now ?? Date.now)(),
|
|
13221
14007
|
mergedSha,
|
|
13222
|
-
"report"
|
|
14008
|
+
"report",
|
|
14009
|
+
meta.publishDir
|
|
13223
14010
|
);
|
|
14011
|
+
const publish = deploy.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report");
|
|
14012
|
+
let dispatch = appendPublishDispatch(deploy, publish);
|
|
14013
|
+
if (!publish && deploy.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
14014
|
+
dispatch = {
|
|
14015
|
+
...dispatch,
|
|
14016
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
|
|
14017
|
+
};
|
|
14018
|
+
}
|
|
13224
14019
|
deployNote = dispatch.note;
|
|
13225
|
-
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
14020
|
+
if (deployModel !== "registry-publish") {
|
|
14021
|
+
runs.push({
|
|
14022
|
+
workflow: "tenant-deploy.yml",
|
|
14023
|
+
url: deploy.runUrl,
|
|
14024
|
+
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
14025
|
+
});
|
|
14026
|
+
}
|
|
14027
|
+
if (publish?.runUrl) {
|
|
14028
|
+
runs.push({
|
|
14029
|
+
workflow: "tenant-publish.yml",
|
|
14030
|
+
url: publish.runUrl,
|
|
14031
|
+
conclusion: publish.deployStatus === "success" ? "success" : publish.deployStatus === "failure" ? "failure" : publish.deployStatus ?? "pending"
|
|
14032
|
+
});
|
|
14033
|
+
} else if (deployModel === "registry-publish") {
|
|
14034
|
+
runs.push({
|
|
14035
|
+
workflow: "tenant-publish.yml",
|
|
14036
|
+
url: deploy.runUrl,
|
|
14037
|
+
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
14038
|
+
});
|
|
14039
|
+
}
|
|
13230
14040
|
} else {
|
|
13231
14041
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
13232
14042
|
}
|
|
@@ -13237,7 +14047,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13237
14047
|
try {
|
|
13238
14048
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
13239
14049
|
const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
|
|
13240
|
-
const
|
|
14050
|
+
const sleep2 = sleeper(deps);
|
|
13241
14051
|
let attempt = 0;
|
|
13242
14052
|
for (; ; ) {
|
|
13243
14053
|
attempt++;
|
|
@@ -13246,7 +14056,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13246
14056
|
break;
|
|
13247
14057
|
} catch (err) {
|
|
13248
14058
|
if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
|
|
13249
|
-
await
|
|
14059
|
+
await sleep2(HOTFIX_VERIFY_RETRY_MS);
|
|
13250
14060
|
}
|
|
13251
14061
|
}
|
|
13252
14062
|
const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
|
|
@@ -13295,6 +14105,7 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
13295
14105
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
13296
14106
|
let tag;
|
|
13297
14107
|
let version;
|
|
14108
|
+
let warnings = [];
|
|
13298
14109
|
if (versionInput) {
|
|
13299
14110
|
({ tag, version } = normalizeHotfixVersion(versionInput));
|
|
13300
14111
|
} else {
|
|
@@ -13303,18 +14114,19 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
13303
14114
|
const latestFacts = await gatherHotfixFacts(deps, ctx, latest, latest.slice(1));
|
|
13304
14115
|
const latestDerived = deriveHotfixState(latestFacts);
|
|
13305
14116
|
if (latestDerived.state !== "complete") {
|
|
13306
|
-
return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived };
|
|
14117
|
+
return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived, warnings };
|
|
13307
14118
|
}
|
|
13308
14119
|
}
|
|
13309
|
-
const
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
14120
|
+
const found = await findInFlightHotfixVersion(deps, ctx, latest);
|
|
14121
|
+
warnings = supersededHotfixWarnings(found.superseded, latest);
|
|
14122
|
+
if (found.inFlight) {
|
|
14123
|
+
({ tag, version } = found.inFlight);
|
|
14124
|
+
} else {
|
|
14125
|
+
({ tag, version } = await deriveHotfixVersion(deps));
|
|
13313
14126
|
}
|
|
13314
|
-
({ tag, version } = await deriveHotfixVersion(deps));
|
|
13315
14127
|
}
|
|
13316
14128
|
const facts = await gatherHotfixFacts(deps, ctx, tag, version);
|
|
13317
|
-
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
14129
|
+
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts), warnings };
|
|
13318
14130
|
}
|
|
13319
14131
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
13320
14132
|
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
@@ -13356,7 +14168,19 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
13356
14168
|
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
13357
14169
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
13358
14170
|
}
|
|
13359
|
-
|
|
14171
|
+
function compareHotfixVersions(a, b) {
|
|
14172
|
+
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
14173
|
+
const pb = b.replace(/^v/, "").split(".").map(Number);
|
|
14174
|
+
for (let i = 0; i < 3; i++) {
|
|
14175
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
14176
|
+
}
|
|
14177
|
+
return 0;
|
|
14178
|
+
}
|
|
14179
|
+
function supersededHotfixWarnings(superseded, latestMainTag) {
|
|
14180
|
+
if (superseded.length === 0) return [];
|
|
14181
|
+
return [`skipped superseded hotfix marker(s) ${superseded.join(", ")} \u2014 merged to main but never released and at/below the latest released tag ${latestMainTag ?? "(none)"}; later releases already absorbed them (#976). Not actionable \u2014 run mmi-cli hotfix start for a new fix.`];
|
|
14182
|
+
}
|
|
14183
|
+
async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
|
|
13360
14184
|
const tags = /* @__PURE__ */ new Set();
|
|
13361
14185
|
const out = await deps.run("gh", [
|
|
13362
14186
|
"pr",
|
|
@@ -13382,20 +14206,20 @@ async function findInFlightHotfixVersion(deps, ctx) {
|
|
|
13382
14206
|
const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
|
|
13383
14207
|
if (m) tags.add(m[1]);
|
|
13384
14208
|
}
|
|
13385
|
-
const sorted = [...tags].sort((a, b) =>
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
for (let i = 0; i < 3; i++) {
|
|
13389
|
-
if (pa[i] !== pb[i]) return pb[i] - pa[i];
|
|
13390
|
-
}
|
|
13391
|
-
return 0;
|
|
13392
|
-
});
|
|
13393
|
-
for (const tag of sorted) {
|
|
14209
|
+
const sorted = [...tags].sort((a, b) => compareHotfixVersions(b, a));
|
|
14210
|
+
const fresh = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) > 0) : sorted;
|
|
14211
|
+
for (const tag of fresh) {
|
|
13394
14212
|
const version = tag.slice(1);
|
|
13395
14213
|
const facts = await gatherHotfixFacts(deps, ctx, tag, version);
|
|
13396
|
-
if (deriveHotfixState(facts).state !== "complete") return { tag, version };
|
|
14214
|
+
if (deriveHotfixState(facts).state !== "complete") return { inFlight: { tag, version }, superseded: [] };
|
|
13397
14215
|
}
|
|
13398
|
-
|
|
14216
|
+
const stale = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) <= 0) : [];
|
|
14217
|
+
const superseded = [];
|
|
14218
|
+
for (const tag of stale) {
|
|
14219
|
+
const facts = await gatherHotfixFacts(deps, ctx, tag, tag.slice(1));
|
|
14220
|
+
if (deriveHotfixState(facts).state !== "complete") superseded.push(tag);
|
|
14221
|
+
}
|
|
14222
|
+
return { inFlight: null, superseded };
|
|
13399
14223
|
}
|
|
13400
14224
|
|
|
13401
14225
|
// src/release-announce.ts
|
|
@@ -13525,7 +14349,7 @@ async function announceRelease(deps, args) {
|
|
|
13525
14349
|
}
|
|
13526
14350
|
|
|
13527
14351
|
// src/port-registry.ts
|
|
13528
|
-
var
|
|
14352
|
+
var import_node_fs20 = require("node:fs");
|
|
13529
14353
|
|
|
13530
14354
|
// ../infra/port-geometry.mjs
|
|
13531
14355
|
var PORT_BLOCK = 100;
|
|
@@ -13539,8 +14363,8 @@ function nextPortBlock(registry2) {
|
|
|
13539
14363
|
return [base2, base2 + PORT_SPAN];
|
|
13540
14364
|
}
|
|
13541
14365
|
function loadPortRegistry(path2) {
|
|
13542
|
-
if (!(0,
|
|
13543
|
-
const raw = JSON.parse((0,
|
|
14366
|
+
if (!(0, import_node_fs20.existsSync)(path2)) return {};
|
|
14367
|
+
const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
|
|
13544
14368
|
const out = {};
|
|
13545
14369
|
for (const [key, value] of Object.entries(raw)) {
|
|
13546
14370
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -13554,9 +14378,9 @@ function ensurePortRange(repo, path2) {
|
|
|
13554
14378
|
const existing = registry2[repo];
|
|
13555
14379
|
if (existing) return existing;
|
|
13556
14380
|
const range = nextPortBlock(registry2);
|
|
13557
|
-
const raw = (0,
|
|
14381
|
+
const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
|
|
13558
14382
|
raw[repo] = range;
|
|
13559
|
-
(0,
|
|
14383
|
+
(0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
13560
14384
|
return range;
|
|
13561
14385
|
}
|
|
13562
14386
|
function portCursorSeed(registry2) {
|
|
@@ -14279,17 +15103,16 @@ function renderBootstrapVerifyReport(report) {
|
|
|
14279
15103
|
var PROJECTS_LIST_PATH = "/projects/list";
|
|
14280
15104
|
var ORG_CONFIG_PATH = "/org/config";
|
|
14281
15105
|
var PROJECTS_ENVELOPE_KEY = "projects";
|
|
15106
|
+
var REGISTRY_FETCH_TIMEOUT_MS = 8e3;
|
|
14282
15107
|
|
|
14283
15108
|
// src/registry-client.ts
|
|
14284
|
-
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
14285
|
-
var WAITED_TENANT_CONTROL_TIMEOUT_MS = 13e3;
|
|
14286
15109
|
var TENANT_DEPLOY_TIMEOUT_MS = 12e4;
|
|
14287
15110
|
var RETRY_ATTEMPTS = 3;
|
|
14288
15111
|
function retriedFetch(deps, url, init) {
|
|
14289
15112
|
const headers = { ...clientVersionHeaders(), ...init.headers };
|
|
14290
15113
|
return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
|
|
14291
15114
|
attempts: RETRY_ATTEMPTS,
|
|
14292
|
-
timeoutMs: deps.timeoutMs ??
|
|
15115
|
+
timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS
|
|
14293
15116
|
});
|
|
14294
15117
|
}
|
|
14295
15118
|
async function fetchTrainAuthority(repo, deps) {
|
|
@@ -14389,7 +15212,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
|
|
|
14389
15212
|
if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
|
|
14390
15213
|
const token = await deps.token();
|
|
14391
15214
|
if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
|
|
14392
|
-
const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ??
|
|
15215
|
+
const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
|
|
14393
15216
|
const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
|
|
14394
15217
|
const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
|
|
14395
15218
|
try {
|
|
@@ -14418,38 +15241,12 @@ async function attestAppGaps(slug, repo, deps) {
|
|
|
14418
15241
|
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
14419
15242
|
}
|
|
14420
15243
|
async function tenantControl(payload, deps) {
|
|
14421
|
-
|
|
14422
|
-
const timeoutMs = payload.wait ? WAITED_TENANT_CONTROL_TIMEOUT_MS : void 0;
|
|
14423
|
-
return postJson("/tenant-control", payload, deps, "POST", { noRetry, timeoutMs });
|
|
15244
|
+
return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
|
|
14424
15245
|
}
|
|
14425
15246
|
async function tenantDeploy(payload, deps) {
|
|
14426
15247
|
return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
|
|
14427
15248
|
}
|
|
14428
15249
|
|
|
14429
|
-
// src/tenant-verify-secrets.ts
|
|
14430
|
-
function tenantControlWait(action) {
|
|
14431
|
-
return action === "status" || action === "retire" || action === "verify-secrets";
|
|
14432
|
-
}
|
|
14433
|
-
function renderVerifySecrets(body) {
|
|
14434
|
-
const secrets2 = body?.secrets ?? [];
|
|
14435
|
-
const counts = {
|
|
14436
|
-
match: secrets2.filter((s) => s.status === "match").length,
|
|
14437
|
-
mismatch: secrets2.filter((s) => s.status === "mismatch").length,
|
|
14438
|
-
missing: secrets2.filter((s) => s.status === "missing").length
|
|
14439
|
-
};
|
|
14440
|
-
const lines = secrets2.map((s) => `${s.key}: ${s.status}`);
|
|
14441
|
-
lines.push(`verify-secrets: ${counts.match} match, ${counts.mismatch} mismatch, ${counts.missing} missing`);
|
|
14442
|
-
const ssmStatus = body?.ssmStatus ?? "pending";
|
|
14443
|
-
if (ssmStatus !== "Success") {
|
|
14444
|
-
return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
|
|
14445
|
-
}
|
|
14446
|
-
const bad = counts.mismatch + counts.missing;
|
|
14447
|
-
if (bad > 0) {
|
|
14448
|
-
return { lines, failure: `${bad} of ${secrets2.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
|
|
14449
|
-
}
|
|
14450
|
-
return { lines, failure: null };
|
|
14451
|
-
}
|
|
14452
|
-
|
|
14453
15250
|
// src/project-readiness.ts
|
|
14454
15251
|
function stagesForTrack(meta) {
|
|
14455
15252
|
return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
|
|
@@ -14771,14 +15568,14 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
14771
15568
|
const required = stageInTrack(meta, stage2) && projectRequiresDeployState(model, stage2);
|
|
14772
15569
|
return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
|
|
14773
15570
|
})));
|
|
14774
|
-
const
|
|
15571
|
+
const secrets = Object.fromEntries(STAGES.map((stage2) => {
|
|
14775
15572
|
const required = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key)) : [];
|
|
14776
15573
|
const present = required.filter((key) => presentSecrets.has(key));
|
|
14777
15574
|
const missing = required.filter((key) => !presentSecrets.has(key));
|
|
14778
15575
|
return [stage2, { required, present, missing }];
|
|
14779
15576
|
}));
|
|
14780
15577
|
const metaMissing = ["class", "projectType", "deployModel", "vaultPath", "kbPointer"].filter((key) => meta[key] === void 0).concat(boardRegistryGaps(meta));
|
|
14781
|
-
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(
|
|
15578
|
+
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
|
|
14782
15579
|
const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
|
|
14783
15580
|
return {
|
|
14784
15581
|
ok,
|
|
@@ -14787,7 +15584,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
14787
15584
|
class: meta.class,
|
|
14788
15585
|
projectType,
|
|
14789
15586
|
deployModel: model,
|
|
14790
|
-
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets
|
|
15587
|
+
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
|
|
14791
15588
|
secretsError,
|
|
14792
15589
|
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
14793
15590
|
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
@@ -14837,7 +15634,7 @@ ${section}`.trim();
|
|
|
14837
15634
|
}
|
|
14838
15635
|
|
|
14839
15636
|
// src/project-set.ts
|
|
14840
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "ci", "requiredChecks", "gate"];
|
|
15637
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "ci", "requiredChecks", "gate"];
|
|
14841
15638
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
14842
15639
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
14843
15640
|
function parseRuntimeSecretsVar(raw) {
|
|
@@ -14996,6 +15793,21 @@ function parsePublishRequiredVar(raw) {
|
|
|
14996
15793
|
if (raw === "false") return false;
|
|
14997
15794
|
throw new Error("project set: publishRequired must be true or false");
|
|
14998
15795
|
}
|
|
15796
|
+
function parseDashboardVar(raw) {
|
|
15797
|
+
if (raw === "true") return true;
|
|
15798
|
+
if (raw === "false") return false;
|
|
15799
|
+
throw new Error("project set: dashboard must be true or false");
|
|
15800
|
+
}
|
|
15801
|
+
function parsePublishDirVar(raw) {
|
|
15802
|
+
const v = raw.trim();
|
|
15803
|
+
if (v === "" || v === ".") {
|
|
15804
|
+
throw new Error("project set: publishDir must be a non-empty relative subpath, e.g. packages/ui (omit it or --unset publishDir for the repo root)");
|
|
15805
|
+
}
|
|
15806
|
+
if (!/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
|
|
15807
|
+
throw new Error('project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
|
|
15808
|
+
}
|
|
15809
|
+
return v;
|
|
15810
|
+
}
|
|
14999
15811
|
function parseRequiredChecksVar(raw) {
|
|
15000
15812
|
let parsed;
|
|
15001
15813
|
try {
|
|
@@ -15046,6 +15858,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
15046
15858
|
"repos",
|
|
15047
15859
|
"oauth",
|
|
15048
15860
|
"publishRequired",
|
|
15861
|
+
"publishDir",
|
|
15862
|
+
"dashboard",
|
|
15049
15863
|
"requiredGcpApis",
|
|
15050
15864
|
"requiredRuntimeSecrets",
|
|
15051
15865
|
"edgeDomains",
|
|
@@ -15062,6 +15876,8 @@ var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
|
15062
15876
|
var SETTABLE_VAR_HINTS = {
|
|
15063
15877
|
projectNumber: "numeric",
|
|
15064
15878
|
publishRequired: "true|false",
|
|
15879
|
+
publishDir: "relative subpath, e.g. packages/ui",
|
|
15880
|
+
dashboard: "true|false",
|
|
15065
15881
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
15066
15882
|
oauth: "JSON {subdomains,domains,callbackPath}",
|
|
15067
15883
|
requiredGcpApis: "comma-string",
|
|
@@ -15135,6 +15951,10 @@ function buildProjectSetPatch(input) {
|
|
|
15135
15951
|
patch[key] = parseReposVar(raw);
|
|
15136
15952
|
} else if (key === "publishRequired") {
|
|
15137
15953
|
patch[key] = parsePublishRequiredVar(raw);
|
|
15954
|
+
} else if (key === "dashboard") {
|
|
15955
|
+
patch[key] = parseDashboardVar(raw);
|
|
15956
|
+
} else if (key === "publishDir") {
|
|
15957
|
+
patch[key] = parsePublishDirVar(raw);
|
|
15138
15958
|
} else if (key === "ci") {
|
|
15139
15959
|
if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
|
|
15140
15960
|
patch[key] = raw;
|
|
@@ -15199,11 +16019,16 @@ function parseKbTree(stdout, prefix) {
|
|
|
15199
16019
|
return tree.filter((t) => t.type === "blob" && typeof t.path === "string" && t.path.startsWith("kb/")).map((t) => t.path).filter((p) => pre ? p.startsWith(pre) : true).sort();
|
|
15200
16020
|
}
|
|
15201
16021
|
|
|
16022
|
+
// src/northstar-commands.ts
|
|
16023
|
+
var import_node_fs21 = require("node:fs");
|
|
16024
|
+
var import_node_child_process11 = require("node:child_process");
|
|
16025
|
+
var import_promises6 = require("node:fs/promises");
|
|
16026
|
+
|
|
15202
16027
|
// src/plan.ts
|
|
15203
|
-
var
|
|
16028
|
+
var import_node_path18 = require("node:path");
|
|
15204
16029
|
var PLANS_DIR = "plans";
|
|
15205
|
-
var META_FILE = (0,
|
|
15206
|
-
var planPath = (slug) => (0,
|
|
16030
|
+
var META_FILE = (0, import_node_path18.join)(PLANS_DIR, ".plan-meta.json");
|
|
16031
|
+
var planPath = (slug) => (0, import_node_path18.join)(PLANS_DIR, `${slug}.md`);
|
|
15207
16032
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
15208
16033
|
function parseMeta(raw) {
|
|
15209
16034
|
if (!raw) return {};
|
|
@@ -15228,7 +16053,7 @@ function hashContent(s) {
|
|
|
15228
16053
|
function staleHint(slug) {
|
|
15229
16054
|
return `remote "${slug}" is newer \u2014 run \`mmi-cli northstar pull ${slug}\` first (your local is based on an older version), or re-push with \`--force\` to overwrite`;
|
|
15230
16055
|
}
|
|
15231
|
-
var INDEX_FILE = (0,
|
|
16056
|
+
var INDEX_FILE = (0, import_node_path18.join)(PLANS_DIR, ".index.json");
|
|
15232
16057
|
var INDEX_TTL_MS = 6e4;
|
|
15233
16058
|
function parseIndex(raw) {
|
|
15234
16059
|
if (!raw) return null;
|
|
@@ -15257,7 +16082,7 @@ function mergeIndex(idx, scope, plans, now) {
|
|
|
15257
16082
|
const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
|
|
15258
16083
|
return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
|
|
15259
16084
|
}
|
|
15260
|
-
var QUEUE_FILE = (0,
|
|
16085
|
+
var QUEUE_FILE = (0, import_node_path18.join)(PLANS_DIR, ".sync-queue.json");
|
|
15261
16086
|
var QUEUE_MAX_ATTEMPTS = 10;
|
|
15262
16087
|
function isValidQueueEntry(e) {
|
|
15263
16088
|
if (!e || typeof e !== "object") return false;
|
|
@@ -15716,23 +16541,298 @@ async function planGraduate(deps, slug, opts = {}) {
|
|
|
15716
16541
|
if (pushed) deps.log(`graduated ${slug}`);
|
|
15717
16542
|
}
|
|
15718
16543
|
|
|
15719
|
-
// src/
|
|
15720
|
-
var
|
|
15721
|
-
function
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
16544
|
+
// src/northstar-commands.ts
|
|
16545
|
+
var planSyncDetached = false;
|
|
16546
|
+
function detachPlanSync() {
|
|
16547
|
+
if (planSyncDetached) return;
|
|
16548
|
+
planSyncDetached = true;
|
|
16549
|
+
try {
|
|
16550
|
+
(0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
|
|
16551
|
+
detached: true,
|
|
16552
|
+
stdio: "ignore",
|
|
16553
|
+
windowsHide: true,
|
|
16554
|
+
cwd: process.cwd()
|
|
16555
|
+
}).unref();
|
|
16556
|
+
} catch {
|
|
16557
|
+
}
|
|
16558
|
+
}
|
|
16559
|
+
function makePlanDeps(cfg, io = consoleIo) {
|
|
16560
|
+
const ensureDir = () => (0, import_node_fs21.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
16561
|
+
return {
|
|
16562
|
+
apiUrl: cfg.sagaApiUrl,
|
|
16563
|
+
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
16564
|
+
headers: (extra) => hubHeaders(extra),
|
|
16565
|
+
project: async () => (await sagaKey(cfg)).project,
|
|
16566
|
+
readLocal: (slug) => {
|
|
16567
|
+
try {
|
|
16568
|
+
return (0, import_node_fs21.readFileSync)(planPath(slug), "utf8");
|
|
16569
|
+
} catch {
|
|
16570
|
+
return null;
|
|
16571
|
+
}
|
|
16572
|
+
},
|
|
16573
|
+
writeLocal: (slug, content) => {
|
|
16574
|
+
ensureDir();
|
|
16575
|
+
(0, import_node_fs21.writeFileSync)(planPath(slug), content, "utf8");
|
|
16576
|
+
},
|
|
16577
|
+
removeLocal: (slug) => {
|
|
16578
|
+
try {
|
|
16579
|
+
(0, import_node_fs21.rmSync)(planPath(slug));
|
|
16580
|
+
} catch {
|
|
16581
|
+
}
|
|
16582
|
+
},
|
|
16583
|
+
readMetaRaw: () => {
|
|
16584
|
+
try {
|
|
16585
|
+
return (0, import_node_fs21.readFileSync)(META_FILE, "utf8");
|
|
16586
|
+
} catch {
|
|
16587
|
+
return null;
|
|
16588
|
+
}
|
|
16589
|
+
},
|
|
16590
|
+
writeMetaRaw: (raw) => {
|
|
16591
|
+
ensureDir();
|
|
16592
|
+
atomicWriteFileSync(META_FILE, raw);
|
|
16593
|
+
},
|
|
16594
|
+
readIndexRaw: () => {
|
|
16595
|
+
try {
|
|
16596
|
+
return (0, import_node_fs21.readFileSync)(INDEX_FILE, "utf8");
|
|
16597
|
+
} catch {
|
|
16598
|
+
return null;
|
|
16599
|
+
}
|
|
16600
|
+
},
|
|
16601
|
+
writeIndexRaw: (raw) => {
|
|
16602
|
+
ensureDir();
|
|
16603
|
+
atomicWriteFileSync(INDEX_FILE, raw);
|
|
16604
|
+
},
|
|
16605
|
+
readQueueRaw: () => {
|
|
16606
|
+
try {
|
|
16607
|
+
return (0, import_node_fs21.readFileSync)(QUEUE_FILE, "utf8");
|
|
16608
|
+
} catch {
|
|
16609
|
+
return null;
|
|
16610
|
+
}
|
|
16611
|
+
},
|
|
16612
|
+
writeQueueRaw: (raw) => {
|
|
16613
|
+
ensureDir();
|
|
16614
|
+
atomicWriteFileSync(QUEUE_FILE, raw);
|
|
16615
|
+
},
|
|
16616
|
+
detachSync: detachPlanSync,
|
|
16617
|
+
log: (m) => io.log(m),
|
|
16618
|
+
err: (m) => io.err(m),
|
|
16619
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
16620
|
+
};
|
|
16621
|
+
}
|
|
16622
|
+
function openInEditor(path2) {
|
|
16623
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
16624
|
+
if (!editor) {
|
|
16625
|
+
console.log(`plan at ${path2} (set $EDITOR to open it automatically)`);
|
|
16626
|
+
return;
|
|
16627
|
+
}
|
|
16628
|
+
try {
|
|
16629
|
+
(0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
|
|
16630
|
+
} catch {
|
|
16631
|
+
console.log(`open ${path2} manually`);
|
|
16632
|
+
}
|
|
16633
|
+
}
|
|
16634
|
+
async function withPlan(quiet, run, io = consoleIo) {
|
|
16635
|
+
const cfg = await loadConfig();
|
|
16636
|
+
if (!cfg.sagaApiUrl) {
|
|
16637
|
+
if (!quiet) fail("plan: Hub API URL not configured");
|
|
16638
|
+
return;
|
|
16639
|
+
}
|
|
16640
|
+
await run(makePlanDeps(cfg, io));
|
|
16641
|
+
}
|
|
16642
|
+
async function gatherRelevanceSignals() {
|
|
16643
|
+
const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
|
|
16644
|
+
const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
16645
|
+
const signals = { branch, changedFiles: changed.length ? changed : void 0 };
|
|
16646
|
+
const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
|
|
16647
|
+
if (issueNum) {
|
|
16648
|
+
try {
|
|
16649
|
+
const { stdout } = await execFileP2(
|
|
16650
|
+
"gh",
|
|
16651
|
+
["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
|
|
16652
|
+
{ timeout: 1e4 }
|
|
16653
|
+
);
|
|
16654
|
+
const j = JSON.parse(stdout);
|
|
16655
|
+
if (j.title) signals.issueTitle = j.title;
|
|
16656
|
+
if (j.labels?.length) signals.issueLabels = j.labels;
|
|
16657
|
+
} catch {
|
|
16658
|
+
}
|
|
16659
|
+
}
|
|
16660
|
+
return signals;
|
|
16661
|
+
}
|
|
16662
|
+
function registerNorthStarCommands(cmd) {
|
|
16663
|
+
cmd.command("push <slug>").description("push a North Star plan to the server (from plans/<slug>.md or --body-file)").option("--project <name>", "override the project key").option("--body-file <path|->", "read plan markdown from a UTF-8 file, or from stdin with -").option("--force", "overwrite the remote even if it changed since your last sync").option("--wait", "push synchronously (block until the server write lands)").action(async (slug, o) => {
|
|
16664
|
+
let content;
|
|
16665
|
+
if (o.bodyFile) {
|
|
16666
|
+
try {
|
|
16667
|
+
content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises6.readFile, readStdin }, {
|
|
16668
|
+
value: "inline content",
|
|
16669
|
+
file: "--body-file",
|
|
16670
|
+
noun: "plan"
|
|
16671
|
+
});
|
|
16672
|
+
} catch (e) {
|
|
16673
|
+
console.error(e.message);
|
|
16674
|
+
process.exitCode = 1;
|
|
16675
|
+
return;
|
|
16676
|
+
}
|
|
16677
|
+
}
|
|
16678
|
+
return withPlan(false, async (d) => {
|
|
16679
|
+
const ok = await planPush(d, slug, { project: o.project, force: o.force, wait: o.wait, content });
|
|
16680
|
+
if (!ok) process.exitCode = 1;
|
|
16681
|
+
});
|
|
16682
|
+
});
|
|
16683
|
+
cmd.command("pull <slug>").description("pull a North Star plan from the server into plans/<slug>.md").option("--project <name>", "override the project key").option("--force", "overwrite local even if it has unpushed edits").action((slug, o) => withPlan(false, async (d) => {
|
|
16684
|
+
const ok = await planPull(d, slug, o);
|
|
16685
|
+
if (!ok) process.exitCode = 1;
|
|
16686
|
+
}));
|
|
16687
|
+
cmd.command("show <slug>").alias("get").description("print a North Star plan to stdout, read-only (no local copy written \u2014 mirrors `kb get`)").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, async (d) => {
|
|
16688
|
+
const ok = await planShow(d, slug, o);
|
|
16689
|
+
if (!ok) process.exitCode = 1;
|
|
16690
|
+
}));
|
|
16691
|
+
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)));
|
|
16692
|
+
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) => {
|
|
16693
|
+
const signals = await gatherRelevanceSignals();
|
|
16694
|
+
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
|
|
16695
|
+
}));
|
|
16696
|
+
cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").option("--wait", "durable confirmation: drain, then report unresolved pushes and exit non-zero if any remain").action((o) => withPlan(o.quiet ?? false, async (d) => {
|
|
16697
|
+
const unresolved = await planSync(d, o);
|
|
16698
|
+
if (!o.wait) return;
|
|
16699
|
+
if (unresolved.length) {
|
|
16700
|
+
for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
|
|
16701
|
+
process.exitCode = 1;
|
|
16702
|
+
} else if (!o.quiet) {
|
|
16703
|
+
d.log("north star: all queued pushes landed");
|
|
16704
|
+
}
|
|
16705
|
+
}));
|
|
16706
|
+
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)));
|
|
16707
|
+
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)));
|
|
16708
|
+
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
16709
|
+
(slug, o) => withPlan(false, async (d) => {
|
|
16710
|
+
const ok = await planPull(d, slug, { project: o.project });
|
|
16711
|
+
if (!ok) {
|
|
16712
|
+
process.exitCode = 1;
|
|
16713
|
+
return;
|
|
16714
|
+
}
|
|
16715
|
+
openInEditor(planPath(slug));
|
|
16716
|
+
})
|
|
16717
|
+
);
|
|
16718
|
+
cmd.command("delete <slug>").description("delete a North Star plan from the server and the local copy").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, (d) => planDelete(d, slug, o)));
|
|
16719
|
+
cmd.command("graduate <slug>").description("mark a built-and-merged North Star plan as org-visible and push it").requiredOption("--merged-pr <url|number>", "merged PR URL or number proving the plan shipped").option("--org-visible", "confirm this plan is safe to queue for org KB curation").option("--project <name>", "override the project key").option("--force", "overwrite the remote even if it changed since your last sync").action(
|
|
16720
|
+
(slug, o) => withPlan(false, (d) => planGraduate(d, slug, o))
|
|
16721
|
+
);
|
|
16722
|
+
}
|
|
16723
|
+
|
|
16724
|
+
// src/secrets-commands.ts
|
|
16725
|
+
async function readSecretStdin() {
|
|
16726
|
+
if (process.stdin.isTTY) {
|
|
16727
|
+
process.stderr.write(
|
|
16728
|
+
'secrets set: pipe the value on stdin (it is never an argument) \u2014 e.g.\n printf %s "$VALUE" | mmi-cli secrets set <KEY>\n'
|
|
16729
|
+
);
|
|
16730
|
+
return "";
|
|
16731
|
+
}
|
|
16732
|
+
const chunks = [];
|
|
16733
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
16734
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
16735
|
+
}
|
|
16736
|
+
function makeSecretsDeps(cfg) {
|
|
16737
|
+
return {
|
|
16738
|
+
apiUrl: cfg.sagaApiUrl,
|
|
16739
|
+
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
16740
|
+
headers: (extra) => hubHeaders(extra),
|
|
16741
|
+
// Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
|
|
16742
|
+
// casing, which leaked mixed-case into `secrets where` output (#681).
|
|
16743
|
+
slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
|
|
16744
|
+
readSecretValue: () => readSecretStdin(),
|
|
16745
|
+
log: (m) => console.log(m),
|
|
16746
|
+
err: (m) => console.error(m)
|
|
16747
|
+
};
|
|
16748
|
+
}
|
|
16749
|
+
async function withSecrets(run) {
|
|
16750
|
+
const cfg = await loadConfig();
|
|
16751
|
+
if (!cfg.sagaApiUrl) {
|
|
16752
|
+
fail("secrets: Hub API URL not configured");
|
|
16753
|
+
return;
|
|
16754
|
+
}
|
|
16755
|
+
await run(makeSecretsDeps(cfg));
|
|
16756
|
+
}
|
|
16757
|
+
function registerSecretsCommands(program3) {
|
|
16758
|
+
const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
|
|
16759
|
+
secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsWhere(d, o)));
|
|
16760
|
+
secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
|
|
16761
|
+
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
|
|
16762
|
+
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
16763
|
+
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
16764
|
+
}
|
|
16765
|
+
const cfg = await loadConfig();
|
|
16766
|
+
if (!cfg.sagaApiUrl) {
|
|
16767
|
+
fail("secrets: Hub API URL not configured");
|
|
16768
|
+
return;
|
|
16769
|
+
}
|
|
16770
|
+
const d = makeSecretsDeps(cfg);
|
|
16771
|
+
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
16772
|
+
const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
|
|
16773
|
+
const repo = o.repo ?? `mutmutco/${slug}`;
|
|
16774
|
+
const meta = await fetchProjectBySlug(slug, regDeps);
|
|
16775
|
+
const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets, {
|
|
16776
|
+
includeGoogleOAuth: projectRequiresGoogleOAuth(meta, meta?.deployModel)
|
|
16777
|
+
});
|
|
16778
|
+
const centralContainer = meta?.deployModel === "tenant-container" || meta?.deployModel === "solo-container";
|
|
16779
|
+
if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
|
|
16780
|
+
d.err("secrets preflight: requiredRuntimeSecrets is unset for this deployable tenant \u2014 declare the per-stage contract in registry META (or an explicit empty map) before promoting");
|
|
16781
|
+
process.exitCode = 1;
|
|
16782
|
+
return;
|
|
16783
|
+
}
|
|
16784
|
+
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
|
|
16785
|
+
if (!ok) process.exitCode = 1;
|
|
16786
|
+
});
|
|
16787
|
+
secrets.command("get <key>").description("print one secret value over TLS (prints once, raw \u2014 do not log/paste it)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
16788
|
+
const ok = await secretsGet(d, key, o);
|
|
16789
|
+
if (!ok) process.exitCode = 1;
|
|
16790
|
+
}));
|
|
16791
|
+
secrets.command("request <key>").description("approved escalation: create a Hub issue + admin DM for a missing secret (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--priority <priority>", "urgent | high | medium | low (default: medium)").option("--reason <text>", "why the secret is needed; safe metadata only").option("--context <text>", "lookup command/context; safe metadata only").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
|
|
16792
|
+
const ok = await secretsRequest(d, key, o);
|
|
16793
|
+
if (!ok) process.exitCode = 1;
|
|
16794
|
+
}));
|
|
16795
|
+
secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
16796
|
+
const ok = await secretsVerify(d, key, o);
|
|
16797
|
+
if (!ok) process.exitCode = 1;
|
|
16798
|
+
}));
|
|
16799
|
+
secrets.command("set <key>").description("write/rotate a secret; value is read from stdin (never an argument)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
16800
|
+
const ok = await secretsSet(d, key, o);
|
|
16801
|
+
if (!ok) process.exitCode = 1;
|
|
16802
|
+
}));
|
|
16803
|
+
secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
16804
|
+
const ok = await secretsEdit(d, key, o);
|
|
16805
|
+
if (!ok) process.exitCode = 1;
|
|
16806
|
+
}));
|
|
16807
|
+
secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
|
|
16808
|
+
const stages = ["dev", "rc", "main"];
|
|
16809
|
+
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
16810
|
+
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
16811
|
+
}
|
|
16812
|
+
const ok = await secretsCopy(d, {
|
|
16813
|
+
repo: o.repo,
|
|
16814
|
+
from: o.from,
|
|
16815
|
+
to: o.to,
|
|
16816
|
+
keys: o.keys.split(","),
|
|
16817
|
+
dryRun: o.dryRun
|
|
16818
|
+
});
|
|
16819
|
+
if (!ok) process.exitCode = 1;
|
|
16820
|
+
}));
|
|
16821
|
+
secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
16822
|
+
secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
|
|
16823
|
+
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
16824
|
+
secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-tier secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
|
|
16825
|
+
}
|
|
16826
|
+
|
|
16827
|
+
// src/oauth.ts
|
|
16828
|
+
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
16829
|
+
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
16830
|
+
var ENV_PREFIXES = ["", "dev", "rc"];
|
|
16831
|
+
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
16832
|
+
var SSM_ENVS = ["dev", "rc", "main"];
|
|
16833
|
+
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
16834
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
16835
|
+
function defaultSubdomain2(slug) {
|
|
15736
16836
|
const i = slug.indexOf("-");
|
|
15737
16837
|
return i === -1 ? slug : slug.slice(i + 1);
|
|
15738
16838
|
}
|
|
@@ -15952,7 +17052,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
15952
17052
|
}
|
|
15953
17053
|
function readRepoVersion() {
|
|
15954
17054
|
try {
|
|
15955
|
-
return JSON.parse((0,
|
|
17055
|
+
return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
15956
17056
|
} catch {
|
|
15957
17057
|
return void 0;
|
|
15958
17058
|
}
|
|
@@ -15998,6 +17098,53 @@ async function fetchNpmReleasedVersion() {
|
|
|
15998
17098
|
return void 0;
|
|
15999
17099
|
}
|
|
16000
17100
|
}
|
|
17101
|
+
async function fetchUiPackageLatestVersion(packageName) {
|
|
17102
|
+
try {
|
|
17103
|
+
const { stdout } = await runHostBin("npm", npmUiPackageLatestArgs(packageName), { timeout: NPM_VIEW_TIMEOUT_MS });
|
|
17104
|
+
return parseNpmViewVersion(stdout);
|
|
17105
|
+
} catch {
|
|
17106
|
+
return void 0;
|
|
17107
|
+
}
|
|
17108
|
+
}
|
|
17109
|
+
async function applyDesignSystemUpdate(check, log) {
|
|
17110
|
+
if (check.ok || !check.packageName) return check;
|
|
17111
|
+
try {
|
|
17112
|
+
log(` \u21BB updating ${check.packageName} ${check.installedVersion ?? "(missing)"} \u2192 ${check.latestVersion ?? "latest"}\u2026`);
|
|
17113
|
+
await runHostBin("npm", ["update", check.packageName], { timeout: NPM_UPDATE_TIMEOUT_MS });
|
|
17114
|
+
const installedVersion = designSystemSnapshot(process.cwd()).installedVersion ?? check.latestVersion;
|
|
17115
|
+
if (check.latestVersion && installedVersion && compareVersions(installedVersion, check.latestVersion) >= 0) {
|
|
17116
|
+
return { ...check, ok: true, installedVersion };
|
|
17117
|
+
}
|
|
17118
|
+
return { ...check, installedVersion };
|
|
17119
|
+
} catch {
|
|
17120
|
+
return check;
|
|
17121
|
+
}
|
|
17122
|
+
}
|
|
17123
|
+
async function applyRegistryComponentsSyncCheck(check, targetVersion, log) {
|
|
17124
|
+
if (check.ok || !check.components?.length) return check;
|
|
17125
|
+
const result = await applyRegistryComponentsSync(
|
|
17126
|
+
process.cwd(),
|
|
17127
|
+
check.components,
|
|
17128
|
+
targetVersion ?? check.targetVersion,
|
|
17129
|
+
log,
|
|
17130
|
+
defaultRegistrySyncDeps()
|
|
17131
|
+
);
|
|
17132
|
+
if (!result.ok) return check;
|
|
17133
|
+
const state = await gatherRegistryComponentsState(process.cwd(), targetVersion ?? check.targetVersion, { fetch });
|
|
17134
|
+
return buildRegistryComponentsCheck({ ...state, isConsumerRepo: true });
|
|
17135
|
+
}
|
|
17136
|
+
async function resolveDashboardConsumer(cfg) {
|
|
17137
|
+
if (!cfg.sagaApiUrl || isUiFactoryCheckout(process.cwd())) return { isConsumer: false };
|
|
17138
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg));
|
|
17139
|
+
if (!read.ok) return { isConsumer: false, registryReadFailed: read.error };
|
|
17140
|
+
return { isConsumer: isDashboardMetaConsumer(read.project) };
|
|
17141
|
+
}
|
|
17142
|
+
function buildDesignSystemRegistryReadCheck(error) {
|
|
17143
|
+
return { ok: false, label: DESIGN_SYSTEM_VERSION_LABEL, fix: dashboardConsumerRegistryFix(error) };
|
|
17144
|
+
}
|
|
17145
|
+
function buildRegistryComponentsRegistryReadCheck(error) {
|
|
17146
|
+
return { ok: false, label: REGISTRY_COMPONENTS_LABEL, fix: dashboardConsumerRegistryFix(error) };
|
|
17147
|
+
}
|
|
16001
17148
|
async function requireFreshTrainCli(commandName) {
|
|
16002
17149
|
if (process.env.MMI_TRAIN_FRESH_OVERRIDE === "1") return;
|
|
16003
17150
|
const report = buildVersionLagReport({
|
|
@@ -16019,8 +17166,8 @@ async function runClaudePlugin(args) {
|
|
|
16019
17166
|
return false;
|
|
16020
17167
|
}
|
|
16021
17168
|
}
|
|
16022
|
-
async function applyClaudePluginHeal(surface, log) {
|
|
16023
|
-
if (surface !== "claude-cli" && surface !== "claude-vscode") return false;
|
|
17169
|
+
async function applyClaudePluginHeal(surface, log, opts) {
|
|
17170
|
+
if (!opts?.force && surface !== "claude-cli" && surface !== "claude-vscode") return false;
|
|
16024
17171
|
log(" \u21BB reinstalling the MMI plugin via `claude plugin` (marketplace remove \u2192 add \u2192 install)\u2026");
|
|
16025
17172
|
for (const step of CLAUDE_PLUGIN_HEAL_STEPS) {
|
|
16026
17173
|
if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
|
|
@@ -16035,8 +17182,8 @@ async function runCodexPlugin(args) {
|
|
|
16035
17182
|
return false;
|
|
16036
17183
|
}
|
|
16037
17184
|
}
|
|
16038
|
-
async function applyCodexPluginHeal(surface, log) {
|
|
16039
|
-
if (surface !== "codex") return false;
|
|
17185
|
+
async function applyCodexPluginHeal(surface, log, opts) {
|
|
17186
|
+
if (!opts?.force && surface !== "codex") return false;
|
|
16040
17187
|
log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
|
|
16041
17188
|
for (const step of CODEX_PLUGIN_HEAL_STEPS) {
|
|
16042
17189
|
if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
|
|
@@ -16060,7 +17207,8 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
16060
17207
|
".claude/settings.json",
|
|
16061
17208
|
".claude/output-styles/mmi-plain.md",
|
|
16062
17209
|
".cursor/rules/mmi-plain-language.mdc",
|
|
16063
|
-
".cursor/rules/mmi-tool-economy.mdc"
|
|
17210
|
+
".cursor/rules/mmi-tool-economy.mdc",
|
|
17211
|
+
".cursor/rules/mmi-code-economy.mdc"
|
|
16064
17212
|
];
|
|
16065
17213
|
const fetched = await Promise.all(files.map(async (file) => {
|
|
16066
17214
|
try {
|
|
@@ -16079,11 +17227,11 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
16079
17227
|
for (const entry of fetched) {
|
|
16080
17228
|
if ("error" in entry) continue;
|
|
16081
17229
|
const { file, source } = entry;
|
|
16082
|
-
const current = (0,
|
|
17230
|
+
const current = (0, import_node_fs22.existsSync)(file) ? await (0, import_promises7.readFile)(file, "utf8") : null;
|
|
16083
17231
|
if (needsUpdate(source, current)) {
|
|
16084
17232
|
const slash = file.lastIndexOf("/");
|
|
16085
|
-
if (slash > 0) (0,
|
|
16086
|
-
await (0,
|
|
17233
|
+
if (slash > 0) (0, import_node_fs22.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
17234
|
+
await (0, import_promises7.writeFile)(file, normalizeEol(source), "utf8");
|
|
16087
17235
|
changed++;
|
|
16088
17236
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
16089
17237
|
}
|
|
@@ -16108,9 +17256,9 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
16108
17256
|
return null;
|
|
16109
17257
|
}
|
|
16110
17258
|
},
|
|
16111
|
-
localContent: async (f) => (0,
|
|
17259
|
+
localContent: async (f) => (0, import_node_fs22.existsSync)(f) ? await (0, import_promises7.readFile)(f, "utf8") : null,
|
|
16112
17260
|
writeDoc: async (f, c) => {
|
|
16113
|
-
await (0,
|
|
17261
|
+
await (0, import_promises7.writeFile)(f, c, "utf8");
|
|
16114
17262
|
}
|
|
16115
17263
|
});
|
|
16116
17264
|
for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
|
|
@@ -16185,7 +17333,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
16185
17333
|
const file = isWin ? "cmd.exe" : bin;
|
|
16186
17334
|
const spawnArgs = isWin ? ["/c", bin, ...args] : args;
|
|
16187
17335
|
return new Promise((resolve, reject) => {
|
|
16188
|
-
const child = (0,
|
|
17336
|
+
const child = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
16189
17337
|
const timer = setTimeout(() => {
|
|
16190
17338
|
try {
|
|
16191
17339
|
child.kill();
|
|
@@ -16207,7 +17355,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
16207
17355
|
async function primaryCheckoutRoot(worktreeRoot) {
|
|
16208
17356
|
try {
|
|
16209
17357
|
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
16210
|
-
return out ? (0,
|
|
17358
|
+
return out ? (0, import_node_path19.dirname)(out) : void 0;
|
|
16211
17359
|
} catch {
|
|
16212
17360
|
return void 0;
|
|
16213
17361
|
}
|
|
@@ -16220,28 +17368,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
16220
17368
|
};
|
|
16221
17369
|
}
|
|
16222
17370
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
16223
|
-
const lockPath = (0,
|
|
17371
|
+
const lockPath = (0, import_node_path19.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
|
|
16224
17372
|
const take = () => {
|
|
16225
|
-
const fd = (0,
|
|
17373
|
+
const fd = (0, import_node_fs22.openSync)(lockPath, "wx");
|
|
16226
17374
|
try {
|
|
16227
|
-
(0,
|
|
17375
|
+
(0, import_node_fs22.writeSync)(fd, String(Date.now()));
|
|
16228
17376
|
} finally {
|
|
16229
|
-
(0,
|
|
17377
|
+
(0, import_node_fs22.closeSync)(fd);
|
|
16230
17378
|
}
|
|
16231
17379
|
return () => {
|
|
16232
17380
|
try {
|
|
16233
|
-
(0,
|
|
17381
|
+
(0, import_node_fs22.rmSync)(lockPath, { force: true });
|
|
16234
17382
|
} catch {
|
|
16235
17383
|
}
|
|
16236
17384
|
};
|
|
16237
17385
|
};
|
|
16238
17386
|
try {
|
|
16239
|
-
(0,
|
|
17387
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(lockPath), { recursive: true });
|
|
16240
17388
|
return take();
|
|
16241
17389
|
} catch {
|
|
16242
17390
|
try {
|
|
16243
|
-
if (Date.now() - (0,
|
|
16244
|
-
(0,
|
|
17391
|
+
if (Date.now() - (0, import_node_fs22.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
17392
|
+
(0, import_node_fs22.rmSync)(lockPath, { force: true });
|
|
16245
17393
|
return take();
|
|
16246
17394
|
}
|
|
16247
17395
|
} catch {
|
|
@@ -16320,361 +17468,90 @@ async function ghCreate(args) {
|
|
|
16320
17468
|
try {
|
|
16321
17469
|
const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
16322
17470
|
return parseCreatedUrl(stdout);
|
|
16323
|
-
} catch (e) {
|
|
16324
|
-
await swapped.cleanup();
|
|
16325
|
-
const err = e;
|
|
16326
|
-
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
16327
|
-
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
16328
|
-
} finally {
|
|
16329
|
-
await swapped.cleanup();
|
|
16330
|
-
}
|
|
16331
|
-
}
|
|
16332
|
-
async function ghJson(args, timeout = 1e4) {
|
|
16333
|
-
const { stdout } = await execFileP2("gh", args, { timeout });
|
|
16334
|
-
return JSON.parse(stdout);
|
|
16335
|
-
}
|
|
16336
|
-
async function resolveRepo(repo) {
|
|
16337
|
-
if (repo) return repo;
|
|
16338
|
-
const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
|
|
16339
|
-
if (fromOrigin) return fromOrigin;
|
|
16340
|
-
try {
|
|
16341
|
-
const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
|
|
16342
|
-
return stdout.trim() || void 0;
|
|
16343
|
-
} catch {
|
|
16344
|
-
return void 0;
|
|
16345
|
-
}
|
|
16346
|
-
}
|
|
16347
|
-
async function attachToProject(issueNumber, repo, priority) {
|
|
16348
|
-
const targetRepo2 = await resolveRepo(repo);
|
|
16349
|
-
let cfg;
|
|
16350
|
-
try {
|
|
16351
|
-
cfg = await loadConfigForRepo(targetRepo2);
|
|
16352
|
-
} catch (e) {
|
|
16353
|
-
console.error(`issue create: board attach skipped \u2014 ${e.message}`);
|
|
16354
|
-
return void 0;
|
|
16355
|
-
}
|
|
16356
|
-
if (!cfg.projectId) {
|
|
16357
|
-
console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
16358
|
-
return void 0;
|
|
16359
|
-
}
|
|
16360
|
-
try {
|
|
16361
|
-
const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
|
|
16362
|
-
if (targetRepo2) viewArgs.push("--repo", targetRepo2);
|
|
16363
|
-
const { stdout: idOut } = await execFileP2("gh", viewArgs, { timeout: 1e4 });
|
|
16364
|
-
const contentId = idOut.trim();
|
|
16365
|
-
if (!contentId) throw new Error("could not resolve issue node id");
|
|
16366
|
-
const { stdout } = await execFileP2("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
16367
|
-
const projectItemId = parseAddedItemId(stdout);
|
|
16368
|
-
if (projectItemId && priority) {
|
|
16369
|
-
try {
|
|
16370
|
-
await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
|
|
16371
|
-
} catch (e) {
|
|
16372
|
-
const err = e;
|
|
16373
|
-
process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
|
|
16374
|
-
`);
|
|
16375
|
-
}
|
|
16376
|
-
}
|
|
16377
|
-
return projectItemId;
|
|
16378
|
-
} catch (e) {
|
|
16379
|
-
const err = e;
|
|
16380
|
-
process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${(err.stderr || err.message || String(e)).trim()}
|
|
16381
|
-
`);
|
|
16382
|
-
return void 0;
|
|
16383
|
-
}
|
|
16384
|
-
}
|
|
16385
|
-
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
16386
|
-
function scheduleRelatedDiscovery(o) {
|
|
16387
|
-
try {
|
|
16388
|
-
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
16389
|
-
if (o.repo) args.push("--repo", o.repo);
|
|
16390
|
-
(0, import_node_child_process10.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
16391
|
-
detached: true,
|
|
16392
|
-
stdio: "ignore",
|
|
16393
|
-
windowsHide: true,
|
|
16394
|
-
cwd: process.cwd()
|
|
16395
|
-
}).unref();
|
|
16396
|
-
} catch {
|
|
16397
|
-
}
|
|
16398
|
-
}
|
|
16399
|
-
var planSyncDetached = false;
|
|
16400
|
-
function detachPlanSync() {
|
|
16401
|
-
if (planSyncDetached) return;
|
|
16402
|
-
planSyncDetached = true;
|
|
16403
|
-
try {
|
|
16404
|
-
(0, import_node_child_process10.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
|
|
16405
|
-
detached: true,
|
|
16406
|
-
stdio: "ignore",
|
|
16407
|
-
windowsHide: true,
|
|
16408
|
-
cwd: process.cwd()
|
|
16409
|
-
}).unref();
|
|
16410
|
-
} catch {
|
|
16411
|
-
}
|
|
16412
|
-
}
|
|
16413
|
-
function makePlanDeps(cfg, io = consoleIo) {
|
|
16414
|
-
const ensureDir = () => (0, import_node_fs19.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
16415
|
-
return {
|
|
16416
|
-
apiUrl: cfg.sagaApiUrl,
|
|
16417
|
-
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
16418
|
-
headers: (extra) => hubHeaders(extra),
|
|
16419
|
-
project: async () => (await sagaKey(cfg)).project,
|
|
16420
|
-
readLocal: (slug) => {
|
|
16421
|
-
try {
|
|
16422
|
-
return (0, import_node_fs19.readFileSync)(planPath(slug), "utf8");
|
|
16423
|
-
} catch {
|
|
16424
|
-
return null;
|
|
16425
|
-
}
|
|
16426
|
-
},
|
|
16427
|
-
writeLocal: (slug, content) => {
|
|
16428
|
-
ensureDir();
|
|
16429
|
-
(0, import_node_fs19.writeFileSync)(planPath(slug), content, "utf8");
|
|
16430
|
-
},
|
|
16431
|
-
removeLocal: (slug) => {
|
|
16432
|
-
try {
|
|
16433
|
-
(0, import_node_fs19.rmSync)(planPath(slug));
|
|
16434
|
-
} catch {
|
|
16435
|
-
}
|
|
16436
|
-
},
|
|
16437
|
-
readMetaRaw: () => {
|
|
16438
|
-
try {
|
|
16439
|
-
return (0, import_node_fs19.readFileSync)(META_FILE, "utf8");
|
|
16440
|
-
} catch {
|
|
16441
|
-
return null;
|
|
16442
|
-
}
|
|
16443
|
-
},
|
|
16444
|
-
writeMetaRaw: (raw) => {
|
|
16445
|
-
ensureDir();
|
|
16446
|
-
atomicWriteFileSync(META_FILE, raw);
|
|
16447
|
-
},
|
|
16448
|
-
readIndexRaw: () => {
|
|
16449
|
-
try {
|
|
16450
|
-
return (0, import_node_fs19.readFileSync)(INDEX_FILE, "utf8");
|
|
16451
|
-
} catch {
|
|
16452
|
-
return null;
|
|
16453
|
-
}
|
|
16454
|
-
},
|
|
16455
|
-
writeIndexRaw: (raw) => {
|
|
16456
|
-
ensureDir();
|
|
16457
|
-
atomicWriteFileSync(INDEX_FILE, raw);
|
|
16458
|
-
},
|
|
16459
|
-
readQueueRaw: () => {
|
|
16460
|
-
try {
|
|
16461
|
-
return (0, import_node_fs19.readFileSync)(QUEUE_FILE, "utf8");
|
|
16462
|
-
} catch {
|
|
16463
|
-
return null;
|
|
16464
|
-
}
|
|
16465
|
-
},
|
|
16466
|
-
writeQueueRaw: (raw) => {
|
|
16467
|
-
ensureDir();
|
|
16468
|
-
atomicWriteFileSync(QUEUE_FILE, raw);
|
|
16469
|
-
},
|
|
16470
|
-
detachSync: detachPlanSync,
|
|
16471
|
-
log: (m) => io.log(m),
|
|
16472
|
-
err: (m) => io.err(m),
|
|
16473
|
-
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
16474
|
-
};
|
|
16475
|
-
}
|
|
16476
|
-
function openInEditor(path2) {
|
|
16477
|
-
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
16478
|
-
if (!editor) {
|
|
16479
|
-
console.log(`plan at ${path2} (set $EDITOR to open it automatically)`);
|
|
16480
|
-
return;
|
|
17471
|
+
} catch (e) {
|
|
17472
|
+
await swapped.cleanup();
|
|
17473
|
+
const err = e;
|
|
17474
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
17475
|
+
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
17476
|
+
} finally {
|
|
17477
|
+
await swapped.cleanup();
|
|
16481
17478
|
}
|
|
17479
|
+
}
|
|
17480
|
+
async function ghJson(args, timeout = 1e4) {
|
|
17481
|
+
const { stdout } = await execFileP2("gh", args, { timeout });
|
|
17482
|
+
return JSON.parse(stdout);
|
|
17483
|
+
}
|
|
17484
|
+
async function resolveRepo(repo) {
|
|
17485
|
+
if (repo) return repo;
|
|
17486
|
+
const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
|
|
17487
|
+
if (fromOrigin) return fromOrigin;
|
|
16482
17488
|
try {
|
|
16483
|
-
(
|
|
17489
|
+
const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
|
|
17490
|
+
return stdout.trim() || void 0;
|
|
16484
17491
|
} catch {
|
|
16485
|
-
|
|
17492
|
+
return void 0;
|
|
16486
17493
|
}
|
|
16487
17494
|
}
|
|
16488
|
-
async function
|
|
16489
|
-
const
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
17495
|
+
async function attachToProject(issueNumber, repo, priority) {
|
|
17496
|
+
const targetRepo2 = await resolveRepo(repo);
|
|
17497
|
+
let cfg;
|
|
17498
|
+
try {
|
|
17499
|
+
cfg = await loadConfigForRepo(targetRepo2);
|
|
17500
|
+
} catch (e) {
|
|
17501
|
+
console.error(`issue create: board attach skipped \u2014 ${e.message}`);
|
|
17502
|
+
return void 0;
|
|
16493
17503
|
}
|
|
16494
|
-
|
|
16495
|
-
}
|
|
16496
|
-
|
|
16497
|
-
const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
|
|
16498
|
-
const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
16499
|
-
const signals = { branch, changedFiles: changed.length ? changed : void 0 };
|
|
16500
|
-
const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
|
|
16501
|
-
if (issueNum) {
|
|
16502
|
-
try {
|
|
16503
|
-
const { stdout } = await execFileP2(
|
|
16504
|
-
"gh",
|
|
16505
|
-
["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
|
|
16506
|
-
{ timeout: 1e4 }
|
|
16507
|
-
);
|
|
16508
|
-
const j = JSON.parse(stdout);
|
|
16509
|
-
if (j.title) signals.issueTitle = j.title;
|
|
16510
|
-
if (j.labels?.length) signals.issueLabels = j.labels;
|
|
16511
|
-
} catch {
|
|
16512
|
-
}
|
|
17504
|
+
if (!cfg.projectId) {
|
|
17505
|
+
console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
17506
|
+
return void 0;
|
|
16513
17507
|
}
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
if (
|
|
17508
|
+
try {
|
|
17509
|
+
const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
|
|
17510
|
+
if (targetRepo2) viewArgs.push("--repo", targetRepo2);
|
|
17511
|
+
const { stdout: idOut } = await execFileP2("gh", viewArgs, { timeout: 1e4 });
|
|
17512
|
+
const contentId = idOut.trim();
|
|
17513
|
+
if (!contentId) throw new Error("could not resolve issue node id");
|
|
17514
|
+
const { stdout } = await execFileP2("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
17515
|
+
const projectItemId = parseAddedItemId(stdout);
|
|
17516
|
+
if (projectItemId && priority) {
|
|
16520
17517
|
try {
|
|
16521
|
-
|
|
16522
|
-
value: "inline content",
|
|
16523
|
-
file: "--body-file",
|
|
16524
|
-
noun: "plan"
|
|
16525
|
-
});
|
|
17518
|
+
await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
|
|
16526
17519
|
} catch (e) {
|
|
16527
|
-
|
|
16528
|
-
process.
|
|
16529
|
-
|
|
17520
|
+
const err = e;
|
|
17521
|
+
process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
|
|
17522
|
+
`);
|
|
16530
17523
|
}
|
|
16531
17524
|
}
|
|
16532
|
-
return
|
|
16533
|
-
|
|
16534
|
-
|
|
16535
|
-
})
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
const ok = await planPull(d, slug, o);
|
|
16539
|
-
if (!ok) process.exitCode = 1;
|
|
16540
|
-
}));
|
|
16541
|
-
cmd.command("show <slug>").alias("get").description("print a North Star plan to stdout, read-only (no local copy written \u2014 mirrors `kb get`)").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, async (d) => {
|
|
16542
|
-
const ok = await planShow(d, slug, o);
|
|
16543
|
-
if (!ok) process.exitCode = 1;
|
|
16544
|
-
}));
|
|
16545
|
-
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)));
|
|
16546
|
-
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) => {
|
|
16547
|
-
const signals = await gatherRelevanceSignals();
|
|
16548
|
-
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
|
|
16549
|
-
}));
|
|
16550
|
-
cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").option("--wait", "durable confirmation: drain, then report unresolved pushes and exit non-zero if any remain").action((o) => withPlan(o.quiet ?? false, async (d) => {
|
|
16551
|
-
const unresolved = await planSync(d, o);
|
|
16552
|
-
if (!o.wait) return;
|
|
16553
|
-
if (unresolved.length) {
|
|
16554
|
-
for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
|
|
16555
|
-
process.exitCode = 1;
|
|
16556
|
-
} else if (!o.quiet) {
|
|
16557
|
-
d.log("north star: all queued pushes landed");
|
|
16558
|
-
}
|
|
16559
|
-
}));
|
|
16560
|
-
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)));
|
|
16561
|
-
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)));
|
|
16562
|
-
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
16563
|
-
(slug, o) => withPlan(false, async (d) => {
|
|
16564
|
-
const ok = await planPull(d, slug, { project: o.project });
|
|
16565
|
-
if (!ok) {
|
|
16566
|
-
process.exitCode = 1;
|
|
16567
|
-
return;
|
|
16568
|
-
}
|
|
16569
|
-
openInEditor(planPath(slug));
|
|
16570
|
-
})
|
|
16571
|
-
);
|
|
16572
|
-
cmd.command("delete <slug>").description("delete a North Star plan from the server and the local copy").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, (d) => planDelete(d, slug, o)));
|
|
16573
|
-
cmd.command("graduate <slug>").description("mark a built-and-merged North Star plan as org-visible and push it").requiredOption("--merged-pr <url|number>", "merged PR URL or number proving the plan shipped").option("--org-visible", "confirm this plan is safe to queue for org KB curation").option("--project <name>", "override the project key").option("--force", "overwrite the remote even if it changed since your last sync").action(
|
|
16574
|
-
(slug, o) => withPlan(false, (d) => planGraduate(d, slug, o))
|
|
16575
|
-
);
|
|
16576
|
-
}
|
|
16577
|
-
var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
|
|
16578
|
-
registerNorthStarCommands(northstar);
|
|
16579
|
-
var plan = program2.command("plan").description("Alias for `northstar` (kept for compatibility)");
|
|
16580
|
-
registerNorthStarCommands(plan);
|
|
16581
|
-
async function readSecretStdin() {
|
|
16582
|
-
if (process.stdin.isTTY) {
|
|
16583
|
-
process.stderr.write(
|
|
16584
|
-
'secrets set: pipe the value on stdin (it is never an argument) \u2014 e.g.\n printf %s "$VALUE" | mmi-cli secrets set <KEY>\n'
|
|
16585
|
-
);
|
|
16586
|
-
return "";
|
|
17525
|
+
return projectItemId;
|
|
17526
|
+
} catch (e) {
|
|
17527
|
+
const err = e;
|
|
17528
|
+
process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${(err.stderr || err.message || String(e)).trim()}
|
|
17529
|
+
`);
|
|
17530
|
+
return void 0;
|
|
16587
17531
|
}
|
|
16588
|
-
const chunks = [];
|
|
16589
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
16590
|
-
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
16591
|
-
}
|
|
16592
|
-
function makeSecretsDeps(cfg) {
|
|
16593
|
-
return {
|
|
16594
|
-
apiUrl: cfg.sagaApiUrl,
|
|
16595
|
-
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
16596
|
-
headers: (extra) => hubHeaders(extra),
|
|
16597
|
-
// Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
|
|
16598
|
-
// casing, which leaked mixed-case into `secrets where` output (#681).
|
|
16599
|
-
slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
|
|
16600
|
-
readSecretValue: () => readSecretStdin(),
|
|
16601
|
-
log: (m) => console.log(m),
|
|
16602
|
-
err: (m) => console.error(m)
|
|
16603
|
-
};
|
|
16604
17532
|
}
|
|
16605
|
-
async
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
17533
|
+
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
17534
|
+
function scheduleRelatedDiscovery(o) {
|
|
17535
|
+
try {
|
|
17536
|
+
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
17537
|
+
if (o.repo) args.push("--repo", o.repo);
|
|
17538
|
+
(0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
17539
|
+
detached: true,
|
|
17540
|
+
stdio: "ignore",
|
|
17541
|
+
windowsHide: true,
|
|
17542
|
+
cwd: process.cwd()
|
|
17543
|
+
}).unref();
|
|
17544
|
+
} catch {
|
|
16610
17545
|
}
|
|
16611
|
-
await run(makeSecretsDeps(cfg));
|
|
16612
17546
|
}
|
|
16613
|
-
var
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
16619
|
-
}
|
|
16620
|
-
const cfg = await loadConfig();
|
|
16621
|
-
if (!cfg.sagaApiUrl) {
|
|
16622
|
-
fail("secrets: Hub API URL not configured");
|
|
16623
|
-
return;
|
|
16624
|
-
}
|
|
16625
|
-
const d = makeSecretsDeps(cfg);
|
|
16626
|
-
const repo = o.repo ?? `mutmutco/${await d.slug()}`;
|
|
16627
|
-
const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(cfg));
|
|
16628
|
-
const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets, {
|
|
16629
|
-
includeGoogleOAuth: projectRequiresGoogleOAuth(meta, meta?.deployModel)
|
|
16630
|
-
});
|
|
16631
|
-
const centralContainer = meta?.deployModel === "tenant-container" || meta?.deployModel === "solo-container";
|
|
16632
|
-
if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
|
|
16633
|
-
d.err("secrets preflight: requiredRuntimeSecrets is unset for this deployable tenant \u2014 declare the per-stage contract in registry META (or an explicit empty map) before promoting");
|
|
16634
|
-
process.exitCode = 1;
|
|
16635
|
-
return;
|
|
16636
|
-
}
|
|
16637
|
-
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
|
|
16638
|
-
if (!ok) process.exitCode = 1;
|
|
17547
|
+
var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
|
|
17548
|
+
registerNorthStarCommands(northstar);
|
|
17549
|
+
var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
|
|
17550
|
+
plan.hook("preAction", () => {
|
|
17551
|
+
process.stderr.write("warning: `plan` is deprecated; use `northstar` instead\n");
|
|
16639
17552
|
});
|
|
16640
|
-
|
|
16641
|
-
|
|
16642
|
-
if (!ok) process.exitCode = 1;
|
|
16643
|
-
}));
|
|
16644
|
-
secrets.command("request <key>").description("approved escalation: create a Hub issue + admin DM for a missing secret (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--priority <priority>", "urgent | high | medium | low (default: medium)").option("--reason <text>", "why the secret is needed; safe metadata only").option("--context <text>", "lookup command/context; safe metadata only").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
|
|
16645
|
-
const ok = await secretsRequest(d, key, o);
|
|
16646
|
-
if (!ok) process.exitCode = 1;
|
|
16647
|
-
}));
|
|
16648
|
-
secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
16649
|
-
const ok = await secretsVerify(d, key, o);
|
|
16650
|
-
if (!ok) process.exitCode = 1;
|
|
16651
|
-
}));
|
|
16652
|
-
secrets.command("set <key>").description("write/rotate a secret; value is read from stdin (never an argument)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
16653
|
-
const ok = await secretsSet(d, key, o);
|
|
16654
|
-
if (!ok) process.exitCode = 1;
|
|
16655
|
-
}));
|
|
16656
|
-
secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
16657
|
-
const ok = await secretsEdit(d, key, o);
|
|
16658
|
-
if (!ok) process.exitCode = 1;
|
|
16659
|
-
}));
|
|
16660
|
-
secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
|
|
16661
|
-
const stages = ["dev", "rc", "main"];
|
|
16662
|
-
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
16663
|
-
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
16664
|
-
}
|
|
16665
|
-
const ok = await secretsCopy(d, {
|
|
16666
|
-
repo: o.repo,
|
|
16667
|
-
from: o.from,
|
|
16668
|
-
to: o.to,
|
|
16669
|
-
keys: o.keys.split(","),
|
|
16670
|
-
dryRun: o.dryRun
|
|
16671
|
-
});
|
|
16672
|
-
if (!ok) process.exitCode = 1;
|
|
16673
|
-
}));
|
|
16674
|
-
secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
16675
|
-
secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
|
|
16676
|
-
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
16677
|
-
secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-tier secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
|
|
17553
|
+
registerNorthStarCommands(plan);
|
|
17554
|
+
registerSecretsCommands(program2);
|
|
16678
17555
|
function registryClientDeps(cfg) {
|
|
16679
17556
|
return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
16680
17557
|
}
|
|
@@ -16691,23 +17568,23 @@ async function reportWrite(label, res) {
|
|
|
16691
17568
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
16692
17569
|
}
|
|
16693
17570
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
16694
|
-
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart,
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
if (
|
|
16706
|
-
|
|
16707
|
-
|
|
16708
|
-
|
|
17571
|
+
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, rc-only retire, read-only verify-secrets) for a tenant via the central tenant-control.yml workflow; project-admin dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets watch by default)").option("--json", "machine-readable output").action(async (repo, stage2, action, o) => {
|
|
17572
|
+
try {
|
|
17573
|
+
const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
|
|
17574
|
+
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
17575
|
+
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
|
|
17576
|
+
const { lines, failure } = renderVerifySecrets(body);
|
|
17577
|
+
for (const line of lines) printLine(line);
|
|
17578
|
+
if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
|
|
17579
|
+
} else {
|
|
17580
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
17581
|
+
}
|
|
17582
|
+
if (result.conclusion === "failure") {
|
|
17583
|
+
return failGraceful(`tenant control ${stage2} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
|
|
17584
|
+
}
|
|
17585
|
+
} catch (e) {
|
|
17586
|
+
return failGraceful(`tenant control: ${e.message}`);
|
|
16709
17587
|
}
|
|
16710
|
-
return reportWrite("tenant control", res);
|
|
16711
17588
|
});
|
|
16712
17589
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
|
|
16713
17590
|
if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
|
|
@@ -16724,18 +17601,18 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
16724
17601
|
}
|
|
16725
17602
|
const cfg = await loadConfig();
|
|
16726
17603
|
const cdeps = registryClientDeps(cfg);
|
|
17604
|
+
const tdeps = trainApplyDeps();
|
|
16727
17605
|
try {
|
|
16728
17606
|
const result = await sweepRcOrphans({
|
|
16729
17607
|
listProjects: () => fetchProjectsList(cdeps),
|
|
16730
17608
|
status: async (repo) => {
|
|
16731
|
-
const
|
|
16732
|
-
const
|
|
16733
|
-
return { serviceState
|
|
17609
|
+
const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "status", watch: true });
|
|
17610
|
+
const serviceState = r.conclusion === "success" ? r.serviceState ?? "unknown" : "error";
|
|
17611
|
+
return { serviceState };
|
|
16734
17612
|
},
|
|
16735
17613
|
retire: async (repo) => {
|
|
16736
|
-
const
|
|
16737
|
-
|
|
16738
|
-
return { ok: res.ok, category: b?.category, reason: b?.reason };
|
|
17614
|
+
const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "retire", watch: true });
|
|
17615
|
+
return { ok: r.category === "retired", category: r.category };
|
|
16739
17616
|
}
|
|
16740
17617
|
}, { retire: !!o.retire });
|
|
16741
17618
|
return printLine(o.json ? JSON.stringify(result) : renderSweep(result));
|
|
@@ -16922,7 +17799,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
|
|
|
16922
17799
|
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
16923
17800
|
return reportWrite("project attest", res);
|
|
16924
17801
|
});
|
|
16925
|
-
project.command("set [owner/repo]").description("
|
|
17802
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master for most fields; project-admin may set/unset dashboard only)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired, dashboard").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
16926
17803
|
const cfg = await loadConfig();
|
|
16927
17804
|
let target;
|
|
16928
17805
|
try {
|
|
@@ -16931,6 +17808,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
|
|
|
16931
17808
|
return fail(e.message);
|
|
16932
17809
|
}
|
|
16933
17810
|
const slug = slugOf(target);
|
|
17811
|
+
const repo = target.includes("/") ? target : `mutmutco/${slug}`;
|
|
16934
17812
|
let patch;
|
|
16935
17813
|
try {
|
|
16936
17814
|
patch = buildProjectSetPatch({
|
|
@@ -16948,7 +17826,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
|
|
|
16948
17826
|
const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
|
|
16949
17827
|
const boardError = boardLinkWriteError(patch, existing);
|
|
16950
17828
|
if (boardError) return fail(`project set: ${boardError}`);
|
|
16951
|
-
const res = await upsertProject(slug, patch, registryClientDeps(cfg));
|
|
17829
|
+
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
16952
17830
|
return reportWrite("project set", res);
|
|
16953
17831
|
});
|
|
16954
17832
|
var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
|
|
@@ -17053,8 +17931,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
17053
17931
|
let body;
|
|
17054
17932
|
let title;
|
|
17055
17933
|
try {
|
|
17056
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
17057
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
17934
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17935
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17058
17936
|
if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
|
|
17059
17937
|
priority = normalizePriority(o.priority);
|
|
17060
17938
|
args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
|
|
@@ -17141,8 +18019,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
17141
18019
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
17142
18020
|
const sourceRepo = await resolveRepo(void 0);
|
|
17143
18021
|
try {
|
|
17144
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
17145
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18022
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
18023
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17146
18024
|
priority = normalizePriority(o.priority);
|
|
17147
18025
|
args = buildIssueArgs({
|
|
17148
18026
|
type: o.type,
|
|
@@ -17208,8 +18086,8 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
|
|
|
17208
18086
|
try {
|
|
17209
18087
|
const routing = assertVerifyRouting(o.routing);
|
|
17210
18088
|
const lenses = o.lenses.split(",").map((s) => assertGrindLens(s.trim()));
|
|
17211
|
-
const criteria = await (0,
|
|
17212
|
-
const diff = await (0,
|
|
18089
|
+
const criteria = await (0, import_promises7.readFile)(o.criteriaFile, "utf8");
|
|
18090
|
+
const diff = await (0, import_promises7.readFile)(o.diffFile, "utf8");
|
|
17213
18091
|
const plan2 = buildPanelPlan({ routing, lenses, criteria, diff });
|
|
17214
18092
|
console.log(JSON.stringify(plan2));
|
|
17215
18093
|
} catch (e) {
|
|
@@ -17218,7 +18096,7 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
|
|
|
17218
18096
|
});
|
|
17219
18097
|
verify.command("synthesize").description("merge lens JSON array into a PanelReport").option("--input-file <path|->", "JSON lens array (use - for stdin)", "-").action(async (o) => {
|
|
17220
18098
|
try {
|
|
17221
|
-
const raw = o.inputFile === "-" ? await readStdin() : await (0,
|
|
18099
|
+
const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises7.readFile)(o.inputFile, "utf8");
|
|
17222
18100
|
const lenses = parseLensResults(JSON.parse(raw));
|
|
17223
18101
|
console.log(JSON.stringify(synthesizePanelReport(lenses)));
|
|
17224
18102
|
} catch (e) {
|
|
@@ -17291,7 +18169,7 @@ build.command("frontier").description("Evaluate external frontier exhaustion + L
|
|
|
17291
18169
|
iterationCapOverride: opts.iterationCap
|
|
17292
18170
|
};
|
|
17293
18171
|
if (opts.jsonFile) {
|
|
17294
|
-
const raw = await (0,
|
|
18172
|
+
const raw = await (0, import_promises7.readFile)(opts.jsonFile, "utf8");
|
|
17295
18173
|
state = { ...state, ...JSON.parse(raw) };
|
|
17296
18174
|
}
|
|
17297
18175
|
const result = evaluateBuildFrontier(state);
|
|
@@ -17345,8 +18223,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
17345
18223
|
let args;
|
|
17346
18224
|
try {
|
|
17347
18225
|
skill = assertSkillName(o.skill);
|
|
17348
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
17349
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18226
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
18227
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17350
18228
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
17351
18229
|
priority = normalizePriority(o.priority);
|
|
17352
18230
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -17397,8 +18275,8 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
17397
18275
|
let body;
|
|
17398
18276
|
let title;
|
|
17399
18277
|
try {
|
|
17400
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
17401
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18278
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
18279
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17402
18280
|
} catch (e) {
|
|
17403
18281
|
return fail(`pr create: ${e.message}`);
|
|
17404
18282
|
}
|
|
@@ -17406,9 +18284,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
17406
18284
|
console.log(JSON.stringify(created));
|
|
17407
18285
|
});
|
|
17408
18286
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
17409
|
-
const wfDir = (0,
|
|
17410
|
-
if (!(0,
|
|
17411
|
-
return (0,
|
|
18287
|
+
const wfDir = (0, import_node_path19.join)(cwd, ".github", "workflows");
|
|
18288
|
+
if (!(0, import_node_fs22.existsSync)(wfDir)) return [];
|
|
18289
|
+
return (0, import_node_fs22.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
17412
18290
|
}
|
|
17413
18291
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
17414
18292
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -17427,7 +18305,7 @@ function ciAuditDeps() {
|
|
|
17427
18305
|
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
|
|
17428
18306
|
// reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
|
|
17429
18307
|
// the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
|
|
17430
|
-
readSeedFile: (path2) => (0,
|
|
18308
|
+
readSeedFile: (path2) => (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null
|
|
17431
18309
|
};
|
|
17432
18310
|
}
|
|
17433
18311
|
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
@@ -17467,12 +18345,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
17467
18345
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
17468
18346
|
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
17469
18347
|
resolveRepo: async (prNumber, repoOpt) => {
|
|
17470
|
-
|
|
17471
|
-
const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...
|
|
17472
|
-
const [
|
|
18348
|
+
const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
|
|
18349
|
+
const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
18350
|
+
const [repoFromGh, base2] = viewed.split(/\s+/);
|
|
17473
18351
|
if (base2 && base2 !== "development") {
|
|
17474
18352
|
throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
|
|
17475
18353
|
}
|
|
18354
|
+
const repo = repoOpt ?? repoFromGh;
|
|
17476
18355
|
if (!repo) throw new Error("pr land: could not resolve PR repo");
|
|
17477
18356
|
return repo;
|
|
17478
18357
|
},
|
|
@@ -17497,32 +18376,50 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
17497
18376
|
}
|
|
17498
18377
|
return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
|
|
17499
18378
|
}
|
|
17500
|
-
const
|
|
17501
|
-
|
|
18379
|
+
const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS })).stdout);
|
|
18380
|
+
if (!stateRead.ok) {
|
|
18381
|
+
return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
|
|
18382
|
+
}
|
|
18383
|
+
return { mergeStatus: stateRead.state === "MERGED" ? "merged" : "auto-merge-enqueued" };
|
|
17502
18384
|
},
|
|
17503
18385
|
pollMerged: async (prNumber, repo, deadlineMs) => {
|
|
17504
18386
|
const args = repo ? ["--repo", repo] : [];
|
|
17505
18387
|
while (Date.now() < deadlineMs) {
|
|
17506
|
-
const
|
|
17507
|
-
if (state === "MERGED") return true;
|
|
18388
|
+
const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS })).stdout, { retries: 2, delayMs: 1e3 });
|
|
18389
|
+
if (stateRead.ok && stateRead.state === "MERGED") return true;
|
|
17508
18390
|
await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
|
|
17509
18391
|
}
|
|
17510
18392
|
return false;
|
|
17511
18393
|
}
|
|
17512
18394
|
});
|
|
18395
|
+
if (result.status !== "failed") {
|
|
18396
|
+
try {
|
|
18397
|
+
const { stdout } = await execFileP2(process.execPath, [
|
|
18398
|
+
process.argv[1],
|
|
18399
|
+
"pr",
|
|
18400
|
+
"merge",
|
|
18401
|
+
number,
|
|
18402
|
+
...o.repo ? ["--repo", o.repo] : [],
|
|
18403
|
+
"--squash"
|
|
18404
|
+
], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
18405
|
+
const trimmed = stdout.trim();
|
|
18406
|
+
if (trimmed) {
|
|
18407
|
+
try {
|
|
18408
|
+
result.cleanup = JSON.parse(trimmed);
|
|
18409
|
+
} catch {
|
|
18410
|
+
result.cleanupError = "cleanup output was not JSON";
|
|
18411
|
+
}
|
|
18412
|
+
}
|
|
18413
|
+
} catch (e) {
|
|
18414
|
+
result.cleanupError = String(e.message || "pr merge cleanup failed");
|
|
18415
|
+
}
|
|
18416
|
+
}
|
|
17513
18417
|
if (o.json) printLine(JSON.stringify(result));
|
|
17514
|
-
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
17515
|
-
if (result.status === "failed") process.exitCode = 1;
|
|
17516
18418
|
else {
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
"pr",
|
|
17520
|
-
"merge",
|
|
17521
|
-
number,
|
|
17522
|
-
...o.repo ? ["--repo", o.repo] : [],
|
|
17523
|
-
"--squash"
|
|
17524
|
-
], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
|
|
18419
|
+
printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
18420
|
+
if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
|
|
17525
18421
|
}
|
|
18422
|
+
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
17526
18423
|
});
|
|
17527
18424
|
async function remoteBranchExists2(branch, options = {}) {
|
|
17528
18425
|
return checkRemoteBranchExists(branch, {
|
|
@@ -17537,15 +18434,15 @@ async function createDeferredWorktreeStore() {
|
|
|
17537
18434
|
return {
|
|
17538
18435
|
read: async () => {
|
|
17539
18436
|
try {
|
|
17540
|
-
return parseDeferredWorktreesFile(await (0,
|
|
18437
|
+
return parseDeferredWorktreesFile(await (0, import_promises7.readFile)(registryPath, "utf8"));
|
|
17541
18438
|
} catch {
|
|
17542
18439
|
return [];
|
|
17543
18440
|
}
|
|
17544
18441
|
},
|
|
17545
18442
|
write: async (entries) => {
|
|
17546
18443
|
try {
|
|
17547
|
-
await (0,
|
|
17548
|
-
await (0,
|
|
18444
|
+
await (0, import_promises7.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
|
|
18445
|
+
await (0, import_promises7.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
17549
18446
|
} catch {
|
|
17550
18447
|
}
|
|
17551
18448
|
}
|
|
@@ -17558,13 +18455,13 @@ var realWorktreeDirRemover = {
|
|
|
17558
18455
|
probe: (p) => {
|
|
17559
18456
|
let st;
|
|
17560
18457
|
try {
|
|
17561
|
-
st = (0,
|
|
18458
|
+
st = (0, import_node_fs22.lstatSync)(p);
|
|
17562
18459
|
} catch {
|
|
17563
18460
|
return null;
|
|
17564
18461
|
}
|
|
17565
18462
|
if (st.isSymbolicLink()) return "link";
|
|
17566
18463
|
try {
|
|
17567
|
-
(0,
|
|
18464
|
+
(0, import_node_fs22.readlinkSync)(p);
|
|
17568
18465
|
return "link";
|
|
17569
18466
|
} catch {
|
|
17570
18467
|
}
|
|
@@ -17572,7 +18469,7 @@ var realWorktreeDirRemover = {
|
|
|
17572
18469
|
},
|
|
17573
18470
|
readdir: (p) => {
|
|
17574
18471
|
try {
|
|
17575
|
-
return (0,
|
|
18472
|
+
return (0, import_node_fs22.readdirSync)(p);
|
|
17576
18473
|
} catch {
|
|
17577
18474
|
return [];
|
|
17578
18475
|
}
|
|
@@ -17581,12 +18478,12 @@ var realWorktreeDirRemover = {
|
|
|
17581
18478
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
17582
18479
|
detachLink: (p) => {
|
|
17583
18480
|
try {
|
|
17584
|
-
(0,
|
|
18481
|
+
(0, import_node_fs22.rmdirSync)(p);
|
|
17585
18482
|
} catch {
|
|
17586
|
-
(0,
|
|
18483
|
+
(0, import_node_fs22.unlinkSync)(p);
|
|
17587
18484
|
}
|
|
17588
18485
|
},
|
|
17589
|
-
removeTree: (p) => (0,
|
|
18486
|
+
removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
17590
18487
|
};
|
|
17591
18488
|
async function resolvePrimaryCheckout(execGit) {
|
|
17592
18489
|
try {
|
|
@@ -17604,7 +18501,7 @@ function worktreeRemoveDeps(execGit) {
|
|
|
17604
18501
|
}
|
|
17605
18502
|
function teardownWorktreeStage(worktreePath) {
|
|
17606
18503
|
return runWorktreeStageTeardown(worktreePath, {
|
|
17607
|
-
hasStageState: (wt) => (0,
|
|
18504
|
+
hasStageState: (wt) => (0, import_node_fs22.existsSync)(stageStatePath(wt)),
|
|
17608
18505
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
17609
18506
|
listComposeProjects: async () => {
|
|
17610
18507
|
const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -17618,7 +18515,7 @@ function teardownWorktreeStage(worktreePath) {
|
|
|
17618
18515
|
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
|
|
17619
18516
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
17620
18517
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
17621
|
-
const headRef = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq",
|
|
18518
|
+
const [headRef, baseRef] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName", "--jq", '.headRefName + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim().split(/\s+/);
|
|
17622
18519
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
17623
18520
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
17624
18521
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
@@ -17667,11 +18564,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
17667
18564
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
17668
18565
|
beforeWorktrees,
|
|
17669
18566
|
startingPath,
|
|
17670
|
-
pathExists: (p) => (0,
|
|
18567
|
+
pathExists: (p) => (0, import_node_fs22.existsSync)(p),
|
|
17671
18568
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
17672
18569
|
teardownWorktreeStage,
|
|
17673
18570
|
deferredStore,
|
|
17674
|
-
removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir
|
|
18571
|
+
removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir,
|
|
18572
|
+
// After merge, return the local checkout to the (fast-forwarded) branch the PR merged into so
|
|
18573
|
+
// grind/build never leave the primary parked on a dead feature branch (#1606).
|
|
18574
|
+
returnToBranch: baseRef
|
|
17675
18575
|
});
|
|
17676
18576
|
} catch (e) {
|
|
17677
18577
|
localCleanup = {
|
|
@@ -17839,7 +18739,7 @@ function rawValues(flag) {
|
|
|
17839
18739
|
return out;
|
|
17840
18740
|
}
|
|
17841
18741
|
function printLine(value) {
|
|
17842
|
-
(0,
|
|
18742
|
+
(0, import_node_fs22.writeSync)(1, `${value}
|
|
17843
18743
|
`);
|
|
17844
18744
|
}
|
|
17845
18745
|
function stageKeepAlive() {
|
|
@@ -17856,8 +18756,8 @@ async function resolveStage() {
|
|
|
17856
18756
|
local,
|
|
17857
18757
|
shell: shellFor(),
|
|
17858
18758
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
17859
|
-
hasCompose: (0,
|
|
17860
|
-
hasEnvExample: (0,
|
|
18759
|
+
hasCompose: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), "docker-compose.yml")),
|
|
18760
|
+
hasEnvExample: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), ".env.example"))
|
|
17861
18761
|
});
|
|
17862
18762
|
}
|
|
17863
18763
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -17909,9 +18809,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
17909
18809
|
printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
|
|
17910
18810
|
return;
|
|
17911
18811
|
}
|
|
17912
|
-
const path2 = (0,
|
|
18812
|
+
const path2 = (0, import_node_path19.join)(process.cwd(), "infra", "port-ranges.json");
|
|
17913
18813
|
const allocate = async (seed) => {
|
|
17914
|
-
const { stdout } = await execFileP2("node", [(0,
|
|
18814
|
+
const { stdout } = await execFileP2("node", [(0, import_node_path19.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
17915
18815
|
const parsed = JSON.parse(stdout);
|
|
17916
18816
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
17917
18817
|
return parsed.range;
|
|
@@ -18097,6 +18997,15 @@ function trainApplyDeps() {
|
|
|
18097
18997
|
throw new Error(`tenant deploy dispatch failed: ${detail}`);
|
|
18098
18998
|
}
|
|
18099
18999
|
},
|
|
19000
|
+
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
19001
|
+
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
19002
|
+
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
19003
|
+
dispatchTenantControl: async ({ repo, stage: stage2, action }) => {
|
|
19004
|
+
const res = await tenantControl({ repo, stage: stage2, action }, registryClientDeps(await loadConfig()));
|
|
19005
|
+
if (res.ok) return { ok: true };
|
|
19006
|
+
const body = res.body;
|
|
19007
|
+
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
19008
|
+
},
|
|
18100
19009
|
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
18101
19010
|
// release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
|
|
18102
19011
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
@@ -18105,7 +19014,7 @@ function trainApplyDeps() {
|
|
|
18105
19014
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
18106
19015
|
announce: (args) => announceRelease({
|
|
18107
19016
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
18108
|
-
readFile: (path2) => (0,
|
|
19017
|
+
readFile: (path2) => (0, import_promises7.readFile)(path2, "utf8")
|
|
18109
19018
|
}, args),
|
|
18110
19019
|
fetchEdgeDomains: async (slug) => {
|
|
18111
19020
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -18215,7 +19124,8 @@ function renderHotfixStatus(r) {
|
|
|
18215
19124
|
` - branch: ${r.branchExists ? "pushed" : "absent"} \xB7 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \xB7 tag: ${r.tagPushed ? "pushed" : "absent"} \xB7 Release: ${r.releaseExists ? "exists" : "absent"}`,
|
|
18216
19125
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
18217
19126
|
` - npm @mutmutco/cli: ${r.npmVersion}`,
|
|
18218
|
-
` - next: ${r.next}
|
|
19127
|
+
` - next: ${r.next}`,
|
|
19128
|
+
...r.warnings.map((w) => ` - warning: ${w}`)
|
|
18219
19129
|
].join("\n");
|
|
18220
19130
|
}
|
|
18221
19131
|
async function runHotfixSub(sub, body, json, render) {
|
|
@@ -18273,12 +19183,12 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
|
|
|
18273
19183
|
}
|
|
18274
19184
|
if (!audit.ok) process.exitCode = 1;
|
|
18275
19185
|
});
|
|
18276
|
-
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
|
|
19186
|
+
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--dashboard", "dashboard repo \u2014 also seeds components.json wired to the @mutmutco registry (#1452)").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
|
|
18277
19187
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
18278
19188
|
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|
|
18279
19189
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
|
|
18280
|
-
const steps = bootstrapPlan(o.repo, o.class);
|
|
18281
|
-
console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
|
|
19190
|
+
const steps = bootstrapPlan(o.repo, o.class, { dashboard: o.dashboard });
|
|
19191
|
+
console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, dashboard: o.dashboard === true, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
|
|
18282
19192
|
});
|
|
18283
19193
|
bootstrap.command("verify <repo>").description("audit whether an existing repo is bootstrapped correctly; no mutations").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").action(async (repo) => {
|
|
18284
19194
|
const o = { class: rawValue("--class", "deployable"), json: rawFlag("--json") };
|
|
@@ -18292,7 +19202,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
18292
19202
|
client: defaultGitHubClient(),
|
|
18293
19203
|
projectMeta: meta,
|
|
18294
19204
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
18295
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
19205
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
|
|
18296
19206
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
18297
19207
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
18298
19208
|
requiredGcpApis: (() => {
|
|
@@ -18317,12 +19227,13 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
18317
19227
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
18318
19228
|
if (!report.ok) process.exitCode = 1;
|
|
18319
19229
|
});
|
|
18320
|
-
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) => {
|
|
19230
|
+
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("--dashboard", "dashboard repo \u2014 seeds components.json wired to the @mutmutco registry (#1452); changes no other axis").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) => {
|
|
18321
19231
|
const o = {
|
|
18322
19232
|
class: rawValue("--class", "deployable"),
|
|
18323
19233
|
projectType: rawValue("--project-type", ""),
|
|
18324
19234
|
deployModel: rawValue("--deploy-model", ""),
|
|
18325
19235
|
releaseTrack: rawValue("--release-track", ""),
|
|
19236
|
+
dashboard: rawFlag("--dashboard"),
|
|
18326
19237
|
execute: rawFlag("--execute"),
|
|
18327
19238
|
json: rawFlag("--json")
|
|
18328
19239
|
};
|
|
@@ -18335,20 +19246,22 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18335
19246
|
return fail(`bootstrap apply: ${e.message}`);
|
|
18336
19247
|
}
|
|
18337
19248
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
18338
|
-
if (!(0,
|
|
18339
|
-
const manifest = loadBootstrapSeeds((0,
|
|
19249
|
+
if (!(0, import_node_fs22.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
19250
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
|
|
18340
19251
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
18341
19252
|
const slug = parsedRepo.slug;
|
|
18342
19253
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
18343
|
-
const
|
|
19254
|
+
const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
|
|
18344
19255
|
const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
18345
19256
|
const rawVars = {};
|
|
18346
19257
|
for (const value of rawValues("--var")) {
|
|
18347
19258
|
const eq = value.indexOf("=");
|
|
18348
19259
|
if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
18349
19260
|
}
|
|
19261
|
+
let registryMetaDashboard = false;
|
|
18350
19262
|
try {
|
|
18351
19263
|
const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
19264
|
+
registryMetaDashboard = meta?.dashboard === true;
|
|
18352
19265
|
for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
|
|
18353
19266
|
} catch {
|
|
18354
19267
|
}
|
|
@@ -18365,16 +19278,20 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18365
19278
|
const applied = [];
|
|
18366
19279
|
let applyDeployModel;
|
|
18367
19280
|
try {
|
|
18368
|
-
|
|
19281
|
+
const payload = buildRegisterPayload(repo, o.class, vars, {
|
|
18369
19282
|
projectType: o.projectType || void 0,
|
|
18370
19283
|
deployModel: o.deployModel || void 0,
|
|
18371
|
-
releaseTrack: o.releaseTrack || void 0
|
|
18372
|
-
|
|
19284
|
+
releaseTrack: o.releaseTrack || void 0,
|
|
19285
|
+
dashboard: o.dashboard
|
|
19286
|
+
});
|
|
19287
|
+
applyDeployModel = payload.deployModel;
|
|
18373
19288
|
} catch {
|
|
18374
19289
|
}
|
|
19290
|
+
const applyDashboard = o.dashboard === true || !rawFlag("--dashboard") && registryMetaDashboard;
|
|
18375
19291
|
for (const seed of manifest.seeds) {
|
|
18376
19292
|
if (!seed.classes.includes(o.class)) continue;
|
|
18377
19293
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
19294
|
+
if (!seedMatchesDashboard(seed, applyDashboard)) continue;
|
|
18378
19295
|
const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
|
|
18379
19296
|
let exists = false;
|
|
18380
19297
|
let sha;
|
|
@@ -18397,7 +19314,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18397
19314
|
}
|
|
18398
19315
|
const planned = planSeedAction(resolved, exists);
|
|
18399
19316
|
const isBlock = resolved.source === "managed-block";
|
|
18400
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
19317
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
|
|
18401
19318
|
const action = reconcileSeedAction(planned, content, isBlock);
|
|
18402
19319
|
actions.push(action);
|
|
18403
19320
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
@@ -18414,7 +19331,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18414
19331
|
}
|
|
18415
19332
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
18416
19333
|
if (rulesetSeed) {
|
|
18417
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
19334
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
|
|
18418
19335
|
if (rulesetContent) {
|
|
18419
19336
|
try {
|
|
18420
19337
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -18450,7 +19367,8 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18450
19367
|
registerPayload = buildRegisterPayload(repo, o.class, vars, {
|
|
18451
19368
|
projectType: o.projectType || void 0,
|
|
18452
19369
|
deployModel: o.deployModel || void 0,
|
|
18453
|
-
releaseTrack: bootstrapReleaseTrack
|
|
19370
|
+
releaseTrack: bootstrapReleaseTrack,
|
|
19371
|
+
dashboard: o.dashboard
|
|
18454
19372
|
});
|
|
18455
19373
|
} catch (e) {
|
|
18456
19374
|
return fail(`bootstrap apply: ${e.message}`);
|
|
@@ -18584,38 +19502,39 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
18584
19502
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
18585
19503
|
targets = [{ repo: o.repo, class: o.class }];
|
|
18586
19504
|
} else {
|
|
18587
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
19505
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs22.existsSync)("projects.json") ? (0, import_node_fs22.readFileSync)("projects.json", "utf8") : null;
|
|
18588
19506
|
if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
|
|
18589
|
-
const fanoutJson = (0,
|
|
19507
|
+
const fanoutJson = (0, import_node_fs22.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs22.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
18590
19508
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
18591
19509
|
}
|
|
18592
19510
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
18593
|
-
const fileMatrix = (0,
|
|
19511
|
+
const fileMatrix = (0, import_node_fs22.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs22.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
18594
19512
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
18595
19513
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
18596
|
-
const fileContracts = (0,
|
|
19514
|
+
const fileContracts = (0, import_node_fs22.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs22.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
18597
19515
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
18598
19516
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
18599
19517
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
18600
19518
|
if (!report.ok) process.exitCode = 1;
|
|
18601
19519
|
});
|
|
19520
|
+
access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
|
|
18602
19521
|
var isWin = process.platform === "win32";
|
|
18603
19522
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
18604
19523
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
18605
|
-
return (0,
|
|
19524
|
+
return (0, import_node_path19.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
18606
19525
|
};
|
|
18607
19526
|
function readInstalledPlugins() {
|
|
18608
19527
|
try {
|
|
18609
|
-
return JSON.parse((0,
|
|
19528
|
+
return JSON.parse((0, import_node_fs22.readFileSync)(installedPluginsPath(), "utf8"));
|
|
18610
19529
|
} catch {
|
|
18611
19530
|
return null;
|
|
18612
19531
|
}
|
|
18613
19532
|
}
|
|
18614
19533
|
function installedPluginSources() {
|
|
18615
19534
|
return ["claude", "codex"].map((surface) => {
|
|
18616
|
-
const recordPath = (0,
|
|
19535
|
+
const recordPath = (0, import_node_path19.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
18617
19536
|
try {
|
|
18618
|
-
return { surface, installed: JSON.parse((0,
|
|
19537
|
+
return { surface, installed: JSON.parse((0, import_node_fs22.readFileSync)(recordPath, "utf8")), recordPath };
|
|
18619
19538
|
} catch {
|
|
18620
19539
|
return { surface, installed: null, recordPath };
|
|
18621
19540
|
}
|
|
@@ -18623,7 +19542,7 @@ function installedPluginSources() {
|
|
|
18623
19542
|
}
|
|
18624
19543
|
function readClaudeSettings() {
|
|
18625
19544
|
try {
|
|
18626
|
-
return JSON.parse((0,
|
|
19545
|
+
return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
18627
19546
|
} catch {
|
|
18628
19547
|
return null;
|
|
18629
19548
|
}
|
|
@@ -18645,7 +19564,7 @@ function writeProjectInstallRecord(record) {
|
|
|
18645
19564
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
18646
19565
|
list.push(record);
|
|
18647
19566
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
18648
|
-
(0,
|
|
19567
|
+
(0, import_node_fs22.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
18649
19568
|
`, "utf8");
|
|
18650
19569
|
return true;
|
|
18651
19570
|
} catch {
|
|
@@ -18658,9 +19577,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
18658
19577
|
if (!file) return false;
|
|
18659
19578
|
if (!file.plugins) file.plugins = {};
|
|
18660
19579
|
const path2 = installedPluginsPath();
|
|
18661
|
-
(0,
|
|
19580
|
+
(0, import_node_fs22.copyFileSync)(path2, `${path2}.bak`);
|
|
18662
19581
|
file.plugins[pluginId] = records;
|
|
18663
|
-
(0,
|
|
19582
|
+
(0, import_node_fs22.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
18664
19583
|
`, "utf8");
|
|
18665
19584
|
return true;
|
|
18666
19585
|
} catch {
|
|
@@ -18668,35 +19587,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
18668
19587
|
}
|
|
18669
19588
|
}
|
|
18670
19589
|
function cursorPluginCacheRoot() {
|
|
18671
|
-
return (0,
|
|
19590
|
+
return (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
|
|
18672
19591
|
}
|
|
18673
19592
|
function cursorPluginCachePinSnapshots() {
|
|
18674
19593
|
const root = cursorPluginCacheRoot();
|
|
18675
19594
|
try {
|
|
18676
|
-
return (0,
|
|
18677
|
-
const path2 = (0,
|
|
18678
|
-
const pluginJson = (0,
|
|
18679
|
-
const hooksJson = (0,
|
|
18680
|
-
const cliBundle = (0,
|
|
19595
|
+
return (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
19596
|
+
const path2 = (0, import_node_path19.join)(root, entry.name);
|
|
19597
|
+
const pluginJson = (0, import_node_path19.join)(path2, ".cursor-plugin", "plugin.json");
|
|
19598
|
+
const hooksJson = (0, import_node_path19.join)(path2, "hooks", "hooks.json");
|
|
19599
|
+
const cliBundle = (0, import_node_path19.join)(path2, "cli", "dist", "index.cjs");
|
|
18681
19600
|
let version;
|
|
18682
19601
|
try {
|
|
18683
|
-
const raw = JSON.parse((0,
|
|
19602
|
+
const raw = JSON.parse((0, import_node_fs22.readFileSync)(pluginJson, "utf8"));
|
|
18684
19603
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
18685
19604
|
} catch {
|
|
18686
19605
|
version = void 0;
|
|
18687
19606
|
}
|
|
18688
19607
|
let isEmpty = true;
|
|
18689
19608
|
try {
|
|
18690
|
-
isEmpty = (0,
|
|
19609
|
+
isEmpty = (0, import_node_fs22.readdirSync)(path2).length === 0;
|
|
18691
19610
|
} catch {
|
|
18692
19611
|
isEmpty = true;
|
|
18693
19612
|
}
|
|
18694
19613
|
return {
|
|
18695
19614
|
name: entry.name,
|
|
18696
19615
|
path: path2,
|
|
18697
|
-
hasPluginJson: (0,
|
|
18698
|
-
hasHooksJson: (0,
|
|
18699
|
-
hasCliBundle: (0,
|
|
19616
|
+
hasPluginJson: (0, import_node_fs22.existsSync)(pluginJson),
|
|
19617
|
+
hasHooksJson: (0, import_node_fs22.existsSync)(hooksJson),
|
|
19618
|
+
hasCliBundle: (0, import_node_fs22.existsSync)(cliBundle),
|
|
18700
19619
|
isEmpty,
|
|
18701
19620
|
version
|
|
18702
19621
|
};
|
|
@@ -18706,19 +19625,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
18706
19625
|
}
|
|
18707
19626
|
}
|
|
18708
19627
|
function hubCheckoutForCursorSeed() {
|
|
18709
|
-
const manifest = (0,
|
|
18710
|
-
return (0,
|
|
19628
|
+
const manifest = (0, import_node_path19.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
19629
|
+
return (0, import_node_fs22.existsSync)(manifest) ? process.cwd() : void 0;
|
|
18711
19630
|
}
|
|
18712
19631
|
function mmiPluginCacheRootSnapshots() {
|
|
18713
19632
|
const roots = [
|
|
18714
|
-
{ surface: "claude", root: (0,
|
|
18715
|
-
{ surface: "codex", root: (0,
|
|
19633
|
+
{ surface: "claude", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
|
|
19634
|
+
{ surface: "codex", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
|
|
18716
19635
|
];
|
|
18717
19636
|
return roots.flatMap(({ surface, root }) => {
|
|
18718
19637
|
try {
|
|
18719
|
-
const entries = (0,
|
|
19638
|
+
const entries = (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
18720
19639
|
name: entry.name,
|
|
18721
|
-
path: (0,
|
|
19640
|
+
path: (0, import_node_path19.join)(root, entry.name),
|
|
18722
19641
|
isDirectory: entry.isDirectory()
|
|
18723
19642
|
}));
|
|
18724
19643
|
return [{ surface, root, entries }];
|
|
@@ -18729,7 +19648,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
18729
19648
|
}
|
|
18730
19649
|
function hasNestedMmiChild(versionDir) {
|
|
18731
19650
|
try {
|
|
18732
|
-
return (0,
|
|
19651
|
+
return (0, import_node_fs22.statSync)((0, import_node_path19.join)(versionDir, "mmi")).isDirectory();
|
|
18733
19652
|
} catch {
|
|
18734
19653
|
return false;
|
|
18735
19654
|
}
|
|
@@ -18740,10 +19659,10 @@ function nestedPluginTreeSnapshot() {
|
|
|
18740
19659
|
);
|
|
18741
19660
|
}
|
|
18742
19661
|
function uniqueQuarantineTarget(path2) {
|
|
18743
|
-
if (!(0,
|
|
19662
|
+
if (!(0, import_node_fs22.existsSync)(path2)) return path2;
|
|
18744
19663
|
for (let i = 1; i < 100; i += 1) {
|
|
18745
19664
|
const candidate = `${path2}-${i}`;
|
|
18746
|
-
if (!(0,
|
|
19665
|
+
if (!(0, import_node_fs22.existsSync)(candidate)) return candidate;
|
|
18747
19666
|
}
|
|
18748
19667
|
return `${path2}-${Date.now()}`;
|
|
18749
19668
|
}
|
|
@@ -18752,10 +19671,10 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
18752
19671
|
const failed = [];
|
|
18753
19672
|
for (const move of plan2) {
|
|
18754
19673
|
try {
|
|
18755
|
-
if (!(0,
|
|
19674
|
+
if (!(0, import_node_fs22.existsSync)(move.from)) continue;
|
|
18756
19675
|
const target = uniqueQuarantineTarget(move.to);
|
|
18757
|
-
(0,
|
|
18758
|
-
(0,
|
|
19676
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(target), { recursive: true });
|
|
19677
|
+
(0, import_node_fs22.renameSync)(move.from, target);
|
|
18759
19678
|
moved += 1;
|
|
18760
19679
|
} catch {
|
|
18761
19680
|
failed.push(move);
|
|
@@ -18774,23 +19693,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
18774
19693
|
}
|
|
18775
19694
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
18776
19695
|
try {
|
|
18777
|
-
if (!(0,
|
|
19696
|
+
if (!(0, import_node_fs22.existsSync)(targetPath)) return true;
|
|
18778
19697
|
if (isWin) {
|
|
18779
|
-
const emptyDir = (0,
|
|
18780
|
-
(0,
|
|
19698
|
+
const emptyDir = (0, import_node_path19.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
19699
|
+
(0, import_node_fs22.mkdirSync)(emptyDir, { recursive: true });
|
|
18781
19700
|
try {
|
|
18782
19701
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
18783
|
-
(0,
|
|
19702
|
+
(0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
|
|
18784
19703
|
} finally {
|
|
18785
19704
|
try {
|
|
18786
|
-
(0,
|
|
19705
|
+
(0, import_node_fs22.rmSync)(emptyDir, { recursive: true, force: true });
|
|
18787
19706
|
} catch {
|
|
18788
19707
|
}
|
|
18789
19708
|
}
|
|
18790
|
-
return !(0,
|
|
19709
|
+
return !(0, import_node_fs22.existsSync)(targetPath);
|
|
18791
19710
|
}
|
|
18792
|
-
(0,
|
|
18793
|
-
return !(0,
|
|
19711
|
+
(0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
|
|
19712
|
+
return !(0, import_node_fs22.existsSync)(targetPath);
|
|
18794
19713
|
} catch {
|
|
18795
19714
|
return false;
|
|
18796
19715
|
}
|
|
@@ -18803,11 +19722,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
18803
19722
|
}
|
|
18804
19723
|
return true;
|
|
18805
19724
|
}
|
|
18806
|
-
var gitignorePath = () => (0,
|
|
19725
|
+
var gitignorePath = () => (0, import_node_path19.join)(process.cwd(), ".gitignore");
|
|
18807
19726
|
function readTextFile(path2) {
|
|
18808
19727
|
try {
|
|
18809
|
-
if (!(0,
|
|
18810
|
-
return (0,
|
|
19728
|
+
if (!(0, import_node_fs22.existsSync)(path2)) return null;
|
|
19729
|
+
return (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
18811
19730
|
} catch {
|
|
18812
19731
|
return null;
|
|
18813
19732
|
}
|
|
@@ -18816,9 +19735,9 @@ function playwrightMcpConfigSnapshots() {
|
|
|
18816
19735
|
const cwd = process.cwd();
|
|
18817
19736
|
const home = (0, import_node_os6.homedir)();
|
|
18818
19737
|
const candidates = [
|
|
18819
|
-
(0,
|
|
18820
|
-
(0,
|
|
18821
|
-
(0,
|
|
19738
|
+
(0, import_node_path19.join)(cwd, ".cursor", "mcp.json"),
|
|
19739
|
+
(0, import_node_path19.join)(home, ".cursor", "mcp.json"),
|
|
19740
|
+
(0, import_node_path19.join)(home, ".codex", "config.toml")
|
|
18822
19741
|
];
|
|
18823
19742
|
const out = [];
|
|
18824
19743
|
for (const path2 of candidates) {
|
|
@@ -18831,7 +19750,7 @@ function strayBrowserArtifactPaths() {
|
|
|
18831
19750
|
const cwd = process.cwd();
|
|
18832
19751
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
18833
19752
|
try {
|
|
18834
|
-
return (0,
|
|
19753
|
+
return (0, import_node_fs22.existsSync)((0, import_node_path19.join)(cwd, rel));
|
|
18835
19754
|
} catch {
|
|
18836
19755
|
return false;
|
|
18837
19756
|
}
|
|
@@ -18839,14 +19758,14 @@ function strayBrowserArtifactPaths() {
|
|
|
18839
19758
|
}
|
|
18840
19759
|
function readGitignore() {
|
|
18841
19760
|
try {
|
|
18842
|
-
return (0,
|
|
19761
|
+
return (0, import_node_fs22.readFileSync)(gitignorePath(), "utf8");
|
|
18843
19762
|
} catch {
|
|
18844
19763
|
return null;
|
|
18845
19764
|
}
|
|
18846
19765
|
}
|
|
18847
19766
|
function writeGitignore(content) {
|
|
18848
19767
|
try {
|
|
18849
|
-
(0,
|
|
19768
|
+
(0, import_node_fs22.writeFileSync)(gitignorePath(), content, "utf8");
|
|
18850
19769
|
return true;
|
|
18851
19770
|
} catch {
|
|
18852
19771
|
return false;
|
|
@@ -18885,7 +19804,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
18885
19804
|
let onPath = pathProbe;
|
|
18886
19805
|
if (!onPath) {
|
|
18887
19806
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
18888
|
-
if (root && (0,
|
|
19807
|
+
if (root && (0, import_node_fs22.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
18889
19808
|
}
|
|
18890
19809
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
18891
19810
|
const surface = detectSurface(process.env);
|
|
@@ -18929,10 +19848,40 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
18929
19848
|
if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
|
|
18930
19849
|
if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
|
|
18931
19850
|
pluginCheck = { ...pluginCheck, ok: true };
|
|
18932
|
-
io.err(` \u21BB repaired: registered mmi@
|
|
19851
|
+
io.err(` \u21BB repaired: registered mmi@mutmutco project install record \u2014 ${reloadHint} to load MMI commands`);
|
|
18933
19852
|
}
|
|
18934
19853
|
}
|
|
18935
19854
|
checks.push(pluginCheck);
|
|
19855
|
+
let legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19856
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19857
|
+
sources: installedPluginSources(),
|
|
19858
|
+
surface
|
|
19859
|
+
});
|
|
19860
|
+
if (!legacyPluginCheck.ok && repairLocal) {
|
|
19861
|
+
const claudeLegacy = legacyPluginCheck.staleSurfaces?.includes("claude") ?? false;
|
|
19862
|
+
const codexLegacy = legacyPluginCheck.staleSurfaces?.includes("codex") ?? false;
|
|
19863
|
+
if (claudeLegacy && await applyClaudePluginHeal(surface, (m) => io.err(m), { force: true })) {
|
|
19864
|
+
legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19865
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19866
|
+
sources: installedPluginSources(),
|
|
19867
|
+
surface
|
|
19868
|
+
});
|
|
19869
|
+
if (legacyPluginCheck.ok) {
|
|
19870
|
+
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via claude plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
19871
|
+
}
|
|
19872
|
+
}
|
|
19873
|
+
if (!legacyPluginCheck.ok && codexLegacy && await applyCodexPluginHeal(surface, (m) => io.err(m), { force: true })) {
|
|
19874
|
+
legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19875
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19876
|
+
sources: installedPluginSources(),
|
|
19877
|
+
surface
|
|
19878
|
+
});
|
|
19879
|
+
if (legacyPluginCheck.ok) {
|
|
19880
|
+
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via codex plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
19881
|
+
}
|
|
19882
|
+
}
|
|
19883
|
+
}
|
|
19884
|
+
checks.push(legacyPluginCheck);
|
|
18936
19885
|
let gitignoreCheck = buildGitignoreManagedBlockCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), content: readGitignore() });
|
|
18937
19886
|
const gitignoreDecision = decideGitignoreRepair(gitignoreCheck, { repoWritesAllowed, repairFull });
|
|
18938
19887
|
gitignoreCheck = gitignoreDecision.check;
|
|
@@ -18955,7 +19904,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
18955
19904
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
18956
19905
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
18957
19906
|
driftCheck = { ...driftCheck, ok: true };
|
|
18958
|
-
io.err(` \u21BB repaired: collapsed mmi@
|
|
19907
|
+
io.err(` \u21BB repaired: collapsed mmi@mutmutco to one user-scope entry (backup at installed_plugins.json.bak) \u2014 ${reloadHint} to load MMI commands`);
|
|
18959
19908
|
}
|
|
18960
19909
|
}
|
|
18961
19910
|
checks.push(driftCheck);
|
|
@@ -19057,7 +20006,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19057
20006
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19058
20007
|
surface,
|
|
19059
20008
|
cacheRoot: cursorCacheRoot,
|
|
19060
|
-
cacheRootExists: (0,
|
|
20009
|
+
cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
|
|
19061
20010
|
pins: cursorPins,
|
|
19062
20011
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19063
20012
|
releasedVersion
|
|
@@ -19068,7 +20017,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19068
20017
|
releasedVersion,
|
|
19069
20018
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19070
20019
|
execFileP: execFileP2,
|
|
19071
|
-
mkdtemp: (prefix) => (0,
|
|
20020
|
+
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path19.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
19072
20021
|
log: (m) => io.err(m)
|
|
19073
20022
|
});
|
|
19074
20023
|
if (seeded) {
|
|
@@ -19077,7 +20026,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19077
20026
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19078
20027
|
surface,
|
|
19079
20028
|
cacheRoot: cursorCacheRoot,
|
|
19080
|
-
cacheRootExists: (0,
|
|
20029
|
+
cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
|
|
19081
20030
|
pins: cursorPins,
|
|
19082
20031
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19083
20032
|
releasedVersion
|
|
@@ -19116,6 +20065,38 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19116
20065
|
strayPaths: strayBrowserArtifactPaths()
|
|
19117
20066
|
})
|
|
19118
20067
|
);
|
|
20068
|
+
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
20069
|
+
const isDashboardConsumer = dashboardConsumer.isConsumer;
|
|
20070
|
+
const uiSnapshot = designSystemSnapshot(process.cwd());
|
|
20071
|
+
const uiLatestVersion = isDashboardConsumer && uiSnapshot.packageName ? await fetchUiPackageLatestVersion(uiSnapshot.packageName) : void 0;
|
|
20072
|
+
let designSystemCheck = dashboardConsumer.registryReadFailed ? buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildDesignSystemVersionCheck({
|
|
20073
|
+
...uiSnapshot,
|
|
20074
|
+
isConsumerRepo: isDashboardConsumer,
|
|
20075
|
+
latestVersion: uiLatestVersion
|
|
20076
|
+
});
|
|
20077
|
+
if (!designSystemCheck.ok && (repairFull || repairLocal) && designSystemCheck.packageName) {
|
|
20078
|
+
designSystemCheck = await applyDesignSystemUpdate(designSystemCheck, (m) => io.err(m));
|
|
20079
|
+
if (designSystemCheck.ok) {
|
|
20080
|
+
io.err(` \u21BB updated ${designSystemCheck.packageName} \u2192 ${designSystemCheck.installedVersion ?? designSystemCheck.latestVersion ?? "latest"}`);
|
|
20081
|
+
}
|
|
20082
|
+
}
|
|
20083
|
+
checks.push(designSystemCheck);
|
|
20084
|
+
const registryTargetVersion = designSystemCheck.latestVersion ?? designSystemCheck.installedVersion ?? uiLatestVersion;
|
|
20085
|
+
let registryComponentsCheck = dashboardConsumer.registryReadFailed ? buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildRegistryComponentsCheck({
|
|
20086
|
+
...await gatherRegistryComponentsState(process.cwd(), registryTargetVersion, { fetch }),
|
|
20087
|
+
isConsumerRepo: isDashboardConsumer
|
|
20088
|
+
});
|
|
20089
|
+
if (!registryComponentsCheck.ok && (repairFull || repairLocal) && repoWritesAllowed && registryComponentsCheck.components?.length) {
|
|
20090
|
+
registryComponentsCheck = await applyRegistryComponentsSyncCheck(
|
|
20091
|
+
registryComponentsCheck,
|
|
20092
|
+
registryTargetVersion,
|
|
20093
|
+
(m) => io.err(m)
|
|
20094
|
+
);
|
|
20095
|
+
if (registryComponentsCheck.ok) {
|
|
20096
|
+
io.err(` \u21BB synced ${registryComponentsCheck.components?.length ?? 0} registry component(s) \u2192 .mmi/design-system/components`);
|
|
20097
|
+
}
|
|
20098
|
+
}
|
|
20099
|
+
checks.push(registryComponentsCheck);
|
|
19119
20100
|
const gaps = checks.filter((c) => !c.ok);
|
|
19120
20101
|
if (opts.banner) {
|
|
19121
20102
|
if (gaps.length) io.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
|
|
@@ -19144,7 +20125,82 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19144
20125
|
io.log(gaps.length ? `
|
|
19145
20126
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
19146
20127
|
}
|
|
19147
|
-
program2.command("
|
|
20128
|
+
var designSystem = program2.command("design-system").description("@mutmutco UI npm package + registry component freshness for dashboard consumers (#1633, #1635)");
|
|
20129
|
+
designSystem.command("status").description("compare the installed @mutmutco/ui-dashboard (or legacy @mutmutco/ui) semver in this repo vs npm @latest").option("--json", "machine-readable output").option("--apply", "run npm update when behind (same as doctor --apply for this check)").action(async (opts) => {
|
|
20130
|
+
const cfg = await loadConfig();
|
|
20131
|
+
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
20132
|
+
if (dashboardConsumer.registryReadFailed) {
|
|
20133
|
+
const check2 = buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed);
|
|
20134
|
+
if (opts.json) {
|
|
20135
|
+
console.log(JSON.stringify(check2, null, 2));
|
|
20136
|
+
} else {
|
|
20137
|
+
console.log(`\u2717 ${check2.label}`);
|
|
20138
|
+
console.log(` fix: ${check2.fix}`);
|
|
20139
|
+
}
|
|
20140
|
+
process.exitCode = 1;
|
|
20141
|
+
return;
|
|
20142
|
+
}
|
|
20143
|
+
const isDashboardConsumer = dashboardConsumer.isConsumer;
|
|
20144
|
+
const snapshot = designSystemSnapshot(process.cwd());
|
|
20145
|
+
let check = buildDesignSystemVersionCheck({
|
|
20146
|
+
...snapshot,
|
|
20147
|
+
isConsumerRepo: isDashboardConsumer,
|
|
20148
|
+
latestVersion: isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0
|
|
20149
|
+
});
|
|
20150
|
+
if (!check.ok && opts.apply && check.packageName) {
|
|
20151
|
+
check = await applyDesignSystemUpdate(check, (m) => console.error(m));
|
|
20152
|
+
}
|
|
20153
|
+
if (opts.json) {
|
|
20154
|
+
console.log(JSON.stringify(check, null, 2));
|
|
20155
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20156
|
+
return;
|
|
20157
|
+
}
|
|
20158
|
+
console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
|
|
20159
|
+
if (check.packageName) console.log(` package: ${check.packageName}`);
|
|
20160
|
+
if (check.installedVersion) console.log(` installed: ${check.installedVersion}`);
|
|
20161
|
+
if (check.latestVersion) console.log(` latest: ${check.latestVersion}`);
|
|
20162
|
+
if (!check.ok) console.log(` fix: ${check.fix}`);
|
|
20163
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20164
|
+
});
|
|
20165
|
+
designSystem.command("registry").description("compare .mmi/design-system/components cache vs the live @mutmutco registry").option("--json", "machine-readable output").option("--apply", "pull registry components into .mmi/ (same as doctor --apply for this check)").action(async (opts) => {
|
|
20166
|
+
const cfg = await loadConfig();
|
|
20167
|
+
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
20168
|
+
if (dashboardConsumer.registryReadFailed) {
|
|
20169
|
+
const check2 = buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed);
|
|
20170
|
+
if (opts.json) {
|
|
20171
|
+
console.log(JSON.stringify(check2, null, 2));
|
|
20172
|
+
} else {
|
|
20173
|
+
console.log(`\u2717 ${check2.label}`);
|
|
20174
|
+
console.log(` fix: ${check2.fix}`);
|
|
20175
|
+
}
|
|
20176
|
+
process.exitCode = 1;
|
|
20177
|
+
return;
|
|
20178
|
+
}
|
|
20179
|
+
const isDashboardConsumer = dashboardConsumer.isConsumer;
|
|
20180
|
+
const snapshot = designSystemSnapshot(process.cwd());
|
|
20181
|
+
const targetVersion = isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0;
|
|
20182
|
+
const state = await gatherRegistryComponentsState(process.cwd(), targetVersion, { fetch });
|
|
20183
|
+
let check = buildRegistryComponentsCheck({
|
|
20184
|
+
...state,
|
|
20185
|
+
isConsumerRepo: isDashboardConsumer
|
|
20186
|
+
});
|
|
20187
|
+
if (!check.ok && opts.apply && check.components?.length) {
|
|
20188
|
+
check = await applyRegistryComponentsSyncCheck(check, targetVersion, (m) => console.error(m));
|
|
20189
|
+
}
|
|
20190
|
+
if (opts.json) {
|
|
20191
|
+
console.log(JSON.stringify(check, null, 2));
|
|
20192
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20193
|
+
return;
|
|
20194
|
+
}
|
|
20195
|
+
console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
|
|
20196
|
+
if (check.components?.length) console.log(` components: ${check.components.join(", ")}`);
|
|
20197
|
+
if (check.cacheVersion) console.log(` cache version: ${check.cacheVersion}`);
|
|
20198
|
+
if (check.targetVersion) console.log(` target version: ${check.targetVersion}`);
|
|
20199
|
+
if (check.staleComponents?.length) console.log(` stale: ${check.staleComponents.join(", ")}`);
|
|
20200
|
+
if (!check.ok) console.log(` fix: ${check.fix}`);
|
|
20201
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20202
|
+
});
|
|
20203
|
+
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, @mutmutco UI npm package freshness, @mutmutco registry component cache, 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) => (
|
|
19148
20204
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
19149
20205
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
19150
20206
|
));
|
|
@@ -19159,7 +20215,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
19159
20215
|
} catch (e) {
|
|
19160
20216
|
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
19161
20217
|
}
|
|
19162
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn:
|
|
20218
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19163
20219
|
let northstarInjected = false;
|
|
19164
20220
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
19165
20221
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
@@ -19201,7 +20257,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
19201
20257
|
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
19202
20258
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
19203
20259
|
if (worktreeBanner) {
|
|
19204
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
20260
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19205
20261
|
consoleIo.log(worktreeBanner);
|
|
19206
20262
|
}
|
|
19207
20263
|
});
|