@mutmutco/cli 2.39.0 → 2.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +2114 -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,35 @@ 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 extractControlOutputFromLog(log) {
|
|
12596
|
+
const lines = log.split(/\r?\n/);
|
|
12597
|
+
const start = lines.findIndex((l) => l.trim() === OUTPUT_BEGIN);
|
|
12598
|
+
if (start < 0) return "";
|
|
12599
|
+
const end = lines.findIndex((l, i) => i > start && l.trim() === OUTPUT_END);
|
|
12600
|
+
const slice = end < 0 ? lines.slice(start + 1) : lines.slice(start + 1, end);
|
|
12601
|
+
return slice.join("\n").trim();
|
|
12602
|
+
}
|
|
12603
|
+
function parseStatusSnippet(stdout) {
|
|
12604
|
+
const t = stdout.toLowerCase();
|
|
12605
|
+
const m = t.match(/service[:=]\s*(running|stopped|missing|up|down|absent)/);
|
|
12606
|
+
if (!m) return { serviceState: "unknown" };
|
|
12607
|
+
const token = m[1];
|
|
12608
|
+
if (token === "running" || token === "up") return { serviceState: "running" };
|
|
12609
|
+
if (token === "stopped" || token === "down") return { serviceState: "stopped" };
|
|
12610
|
+
return { serviceState: "missing" };
|
|
12611
|
+
}
|
|
12612
|
+
function parseVerifySecrets(stdout) {
|
|
12613
|
+
const out = [];
|
|
12614
|
+
for (const line of stdout.split("\n")) {
|
|
12615
|
+
const m = /^(\S+):\s*(match|mismatch|missing)\b/.exec(line.trim());
|
|
12616
|
+
if (m) out.push({ key: m[1], status: m[2] });
|
|
12617
|
+
}
|
|
12618
|
+
return out;
|
|
12619
|
+
}
|
|
12620
|
+
|
|
11898
12621
|
// src/train-apply.ts
|
|
11899
12622
|
function resolveDeployModel2(meta, repo) {
|
|
11900
12623
|
const m = meta?.deployModel;
|
|
@@ -11969,7 +12692,8 @@ var ORG_SPINE_FILES = [
|
|
|
11969
12692
|
".claude/settings.json",
|
|
11970
12693
|
".claude/output-styles/mmi-plain.md",
|
|
11971
12694
|
".cursor/rules/mmi-plain-language.mdc",
|
|
11972
|
-
".cursor/rules/mmi-tool-economy.mdc"
|
|
12695
|
+
".cursor/rules/mmi-tool-economy.mdc",
|
|
12696
|
+
".cursor/rules/mmi-code-economy.mdc"
|
|
11973
12697
|
];
|
|
11974
12698
|
function isSpinePath(path2) {
|
|
11975
12699
|
return ORG_SPINE_FILES.includes(path2);
|
|
@@ -12101,53 +12825,23 @@ function requireProjectMetaForTrain(load, repo) {
|
|
|
12101
12825
|
var CORRELATE_ATTEMPTS = 5;
|
|
12102
12826
|
var CORRELATE_DELAY_MS = 1500;
|
|
12103
12827
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
12828
|
+
var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12829
|
+
function resolveSleep(deps) {
|
|
12830
|
+
return deps.sleep ?? defaultSleep2;
|
|
12831
|
+
}
|
|
12104
12832
|
var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
|
|
12105
12833
|
var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
|
|
12106
12834
|
var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
12107
12835
|
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
12108
12836
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
12109
12837
|
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)));
|
|
12838
|
+
async function correlateRun(deps, args) {
|
|
12839
|
+
const sleep2 = resolveSleep(deps);
|
|
12146
12840
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
12147
12841
|
let lastError;
|
|
12148
12842
|
let parsedAnyResponse = false;
|
|
12149
12843
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
12150
|
-
if (attempt > 0) await
|
|
12844
|
+
if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
|
|
12151
12845
|
const listArgs = [
|
|
12152
12846
|
"run",
|
|
12153
12847
|
"list",
|
|
@@ -12155,28 +12849,43 @@ async function correlateWorkflowRun(deps, args) {
|
|
|
12155
12849
|
HUB_REPO3,
|
|
12156
12850
|
"--workflow",
|
|
12157
12851
|
args.workflow,
|
|
12158
|
-
"--event",
|
|
12159
|
-
args.
|
|
12160
|
-
...args.branch ? ["--branch", args.branch] : [],
|
|
12852
|
+
...args.mode === "workflow" ? ["--event", args.event] : [],
|
|
12853
|
+
...args.mode === "workflow" && args.branch ? ["--branch", args.branch] : [],
|
|
12161
12854
|
"--limit",
|
|
12162
12855
|
"10",
|
|
12163
12856
|
"--json",
|
|
12164
|
-
"databaseId,url,event,createdAt,status,conclusion,headSha"
|
|
12857
|
+
args.mode === "dispatch" ? "databaseId,url,event,createdAt" : "databaseId,url,event,createdAt,status,conclusion,headSha"
|
|
12165
12858
|
];
|
|
12166
12859
|
let rows;
|
|
12167
12860
|
try {
|
|
12168
12861
|
rows = JSON.parse(await deps.run("gh", listArgs));
|
|
12169
12862
|
parsedAnyResponse = true;
|
|
12170
12863
|
} catch {
|
|
12171
|
-
lastError = new Error(`could not list ${args.workflow} runs`);
|
|
12864
|
+
if (args.mode === "workflow") lastError = new Error(`could not list ${args.workflow} runs`);
|
|
12172
12865
|
continue;
|
|
12173
12866
|
}
|
|
12174
|
-
const match = rows.filter((r) =>
|
|
12867
|
+
const match = rows.filter((r) => {
|
|
12868
|
+
if (typeof r.databaseId !== "number") return false;
|
|
12869
|
+
if (args.mode === "dispatch") return r.event === "workflow_dispatch";
|
|
12870
|
+
return r.event === args.event && r.headSha === args.headSha;
|
|
12871
|
+
}).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
12872
|
if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
|
|
12176
12873
|
}
|
|
12177
|
-
if (!parsedAnyResponse && lastError) throw lastError;
|
|
12874
|
+
if (args.mode === "workflow" && !parsedAnyResponse && lastError) throw lastError;
|
|
12178
12875
|
return {};
|
|
12179
12876
|
}
|
|
12877
|
+
function correlateTenantRun(deps, since) {
|
|
12878
|
+
return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch" });
|
|
12879
|
+
}
|
|
12880
|
+
function correlatePublishRun(deps, since) {
|
|
12881
|
+
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
|
|
12882
|
+
}
|
|
12883
|
+
function correlateControlRun(deps, since) {
|
|
12884
|
+
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch" });
|
|
12885
|
+
}
|
|
12886
|
+
async function correlateWorkflowRun(deps, args) {
|
|
12887
|
+
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
12888
|
+
}
|
|
12180
12889
|
async function watchTenantRun(deps, runId) {
|
|
12181
12890
|
if (runId == null) return "pending";
|
|
12182
12891
|
try {
|
|
@@ -12186,6 +12895,13 @@ async function watchTenantRun(deps, runId) {
|
|
|
12186
12895
|
return "failure";
|
|
12187
12896
|
}
|
|
12188
12897
|
}
|
|
12898
|
+
async function fetchControlRunLog(deps, runId) {
|
|
12899
|
+
try {
|
|
12900
|
+
return await deps.run("gh", ["run", "view", String(runId), "--repo", HUB_REPO3, "--log"]);
|
|
12901
|
+
} catch {
|
|
12902
|
+
return "";
|
|
12903
|
+
}
|
|
12904
|
+
}
|
|
12189
12905
|
async function watchWorkflowRun(deps, workflow, run) {
|
|
12190
12906
|
if (run.runId == null) return { workflow, conclusion: "pending" };
|
|
12191
12907
|
const conclusion = await watchTenantRun(deps, run.runId);
|
|
@@ -12286,11 +13002,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
12286
13002
|
if (required.length === 0) {
|
|
12287
13003
|
return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
|
|
12288
13004
|
}
|
|
12289
|
-
const
|
|
13005
|
+
const sleep2 = resolveSleep(deps);
|
|
12290
13006
|
let lastStatus = "not checked";
|
|
12291
13007
|
let lastError;
|
|
12292
13008
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
12293
|
-
if (attempt > 0) await
|
|
13009
|
+
if (attempt > 0) await sleep2(TRAIN_CHECK_DELAY_MS);
|
|
12294
13010
|
let checkRuns;
|
|
12295
13011
|
let statuses;
|
|
12296
13012
|
try {
|
|
@@ -12366,18 +13082,77 @@ function isTransientDispatchFailure(e) {
|
|
|
12366
13082
|
return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
|
|
12367
13083
|
}
|
|
12368
13084
|
async function dispatchTenantDeployWithRetry(deps, input) {
|
|
12369
|
-
const
|
|
13085
|
+
const sleep2 = resolveSleep(deps);
|
|
12370
13086
|
for (let attempt = 1; ; attempt++) {
|
|
12371
13087
|
try {
|
|
12372
13088
|
await deps.dispatchTenantDeploy(input);
|
|
12373
13089
|
return;
|
|
12374
13090
|
} catch (e) {
|
|
12375
13091
|
if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
|
|
12376
|
-
await
|
|
13092
|
+
await sleep2(DISPATCH_RETRY_DELAY_MS * attempt);
|
|
12377
13093
|
}
|
|
12378
13094
|
}
|
|
12379
13095
|
}
|
|
12380
|
-
|
|
13096
|
+
function tenantPublishRecoveryCommand(slug, repo, ref, stage2, publishDir) {
|
|
13097
|
+
const parts = [
|
|
13098
|
+
`gh workflow run tenant-publish.yml --repo ${HUB_REPO3}`,
|
|
13099
|
+
`-f slug=${slug}`,
|
|
13100
|
+
`-f repo=${repo}`,
|
|
13101
|
+
`-f ref=${ref}`,
|
|
13102
|
+
`-f stage=${stage2}`
|
|
13103
|
+
];
|
|
13104
|
+
if (publishDir && publishDir !== ".") parts.push(`-f publishDir=${publishDir}`);
|
|
13105
|
+
return parts.join(" ");
|
|
13106
|
+
}
|
|
13107
|
+
async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure = "throw", publishDir) {
|
|
13108
|
+
const since = (deps.now ?? Date.now)();
|
|
13109
|
+
const dispatchArgs = [
|
|
13110
|
+
"workflow",
|
|
13111
|
+
"run",
|
|
13112
|
+
"tenant-publish.yml",
|
|
13113
|
+
"--repo",
|
|
13114
|
+
HUB_REPO3,
|
|
13115
|
+
"-f",
|
|
13116
|
+
`slug=${ctx.slug}`,
|
|
13117
|
+
"-f",
|
|
13118
|
+
`repo=${ctx.repo}`,
|
|
13119
|
+
"-f",
|
|
13120
|
+
`ref=${ref}`,
|
|
13121
|
+
"-f",
|
|
13122
|
+
`stage=${stage2}`
|
|
13123
|
+
];
|
|
13124
|
+
if (publishDir && publishDir !== ".") dispatchArgs.push("-f", `publishDir=${publishDir}`);
|
|
13125
|
+
try {
|
|
13126
|
+
await deps.run("gh", dispatchArgs);
|
|
13127
|
+
} catch (e) {
|
|
13128
|
+
if (dispatchFailure === "throw") throw e;
|
|
13129
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13130
|
+
const recovery = tenantPublishRecoveryCommand(ctx.slug, ctx.repo, ref, stage2, publishDir);
|
|
13131
|
+
return {
|
|
13132
|
+
note: `tenant-publish dispatch FAILED: ${msg}. The promotion itself landed \u2014 recover with \`${recovery}\``,
|
|
13133
|
+
deployStatus: "failure"
|
|
13134
|
+
};
|
|
13135
|
+
}
|
|
13136
|
+
const { runId, runUrl } = await correlatePublishRun(deps, since);
|
|
13137
|
+
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
13138
|
+
return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
13139
|
+
}
|
|
13140
|
+
async function dispatchPublishIfRequired(deps, ctx, meta, model, stage2, publishRef, watch, dispatchFailure) {
|
|
13141
|
+
if (!meta.publishRequired || stage2 !== "main") return null;
|
|
13142
|
+
if (model !== "tenant-container" && model !== "solo-container") return null;
|
|
13143
|
+
return dispatchTenantPublish(deps, ctx, stage2, publishRef, watch, dispatchFailure, meta.publishDir);
|
|
13144
|
+
}
|
|
13145
|
+
function appendPublishDispatch(deploy, publish) {
|
|
13146
|
+
if (!publish) return deploy;
|
|
13147
|
+
return {
|
|
13148
|
+
note: `${deploy.note}; ${publish.note}`,
|
|
13149
|
+
runId: deploy.runId,
|
|
13150
|
+
runUrl: deploy.runUrl,
|
|
13151
|
+
workflowRuns: [...deploy.workflowRuns ?? [], ...publish.workflowRuns ?? [{ workflow: "tenant-publish.yml", runId: publish.runId, runUrl: publish.runUrl, conclusion: publish.deployStatus }]],
|
|
13152
|
+
deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
|
|
13153
|
+
};
|
|
13154
|
+
}
|
|
13155
|
+
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
12381
13156
|
if (model === "tenant-container" || model === "solo-container") {
|
|
12382
13157
|
const since = (deps.now ?? Date.now)();
|
|
12383
13158
|
try {
|
|
@@ -12395,25 +13170,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
12395
13170
|
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
12396
13171
|
}
|
|
12397
13172
|
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 };
|
|
13173
|
+
return dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure, publishDir);
|
|
12417
13174
|
}
|
|
12418
13175
|
if (model === "hub-serverless") {
|
|
12419
13176
|
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 +13212,99 @@ async function preflight(deps, ctx, stage2, meta) {
|
|
|
12455
13212
|
await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
|
|
12456
13213
|
return model;
|
|
12457
13214
|
}
|
|
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 };
|
|
13215
|
+
async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
|
|
13216
|
+
const foldPaths = await resolveFoldPaths(deps, deployModel);
|
|
13217
|
+
const tolerated = [...foldPaths, ...RELEASE_TOLERATED_PATHS];
|
|
13218
|
+
const predicted = await predictMergeConflicts(deps, "origin/main", remoteRef);
|
|
13219
|
+
const predictedBlocking = predicted.filter((f) => !isSpinePath(f) && !tolerated.includes(f));
|
|
13220
|
+
if (predictedBlocking.length > 0) {
|
|
13221
|
+
throw new Error(`${blockingPrefix}: ${predictedBlocking.join(", ")} \u2014 no merge was started. ${realignMessage}`);
|
|
12492
13222
|
}
|
|
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 }
|
|
13223
|
+
return { foldPaths, tolerated, predicted };
|
|
13224
|
+
}
|
|
13225
|
+
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
|
|
13226
|
+
await deps.run("git", ["checkout", "main"]);
|
|
13227
|
+
await ffOnlyPull(deps, "main");
|
|
13228
|
+
if (predicted.length === 0) {
|
|
13229
|
+
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
13230
|
+
} else {
|
|
13231
|
+
await mergeWithSpineResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
|
|
13232
|
+
}
|
|
13233
|
+
}
|
|
13234
|
+
async function mergeSourceToMain(deps, deployModel, args) {
|
|
13235
|
+
const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
|
|
13236
|
+
deps,
|
|
13237
|
+
deployModel,
|
|
13238
|
+
args.remoteRef,
|
|
13239
|
+
args.blockingPrefix,
|
|
13240
|
+
args.realignMessage
|
|
13241
|
+
);
|
|
13242
|
+
await executeMergeToMain(deps, args.sourceRef, args.mergeLabel, tolerated, predicted);
|
|
13243
|
+
return { foldPaths };
|
|
13244
|
+
}
|
|
13245
|
+
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
|
|
13246
|
+
await ensureTagPushed(deps, tag, releaseSha);
|
|
13247
|
+
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
13248
|
+
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
13249
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
13250
|
+
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
13251
|
+
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
13252
|
+
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
13253
|
+
const autoRunSince = (deps.now ?? Date.now)();
|
|
13254
|
+
const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
|
|
13255
|
+
const publishDispatch = deployDispatch.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report");
|
|
13256
|
+
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
13257
|
+
if (!publishDispatch && deployDispatch.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
13258
|
+
dispatch = {
|
|
13259
|
+
...dispatch,
|
|
13260
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
|
|
12552
13261
|
};
|
|
12553
13262
|
}
|
|
12554
|
-
|
|
13263
|
+
return { checks, releaseUrl, announceNote, dispatch };
|
|
13264
|
+
}
|
|
13265
|
+
async function pushRcAlignment(deps) {
|
|
13266
|
+
try {
|
|
13267
|
+
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
13268
|
+
return "origin/rc aligned to the released main";
|
|
13269
|
+
} catch (e) {
|
|
13270
|
+
return `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
|
|
13271
|
+
}
|
|
13272
|
+
}
|
|
13273
|
+
async function runTrainApplyPipeline(mode, input) {
|
|
13274
|
+
const { deps, ctx, command, meta, branchHints, watch, options } = input;
|
|
13275
|
+
const directTrack = input.directTrack ?? false;
|
|
13276
|
+
if (mode === "rcand") {
|
|
13277
|
+
await requireBranch(deps, "development");
|
|
13278
|
+
if (directTrack) {
|
|
13279
|
+
throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
|
|
13280
|
+
}
|
|
13281
|
+
await ffOnlyPull(deps, "development");
|
|
13282
|
+
ensurePositiveCount(
|
|
13283
|
+
await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
|
|
13284
|
+
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
13285
|
+
);
|
|
13286
|
+
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
13287
|
+
const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
13288
|
+
await deps.run("git", ["checkout", "rc"]);
|
|
13289
|
+
await ffOnlyPull(deps, "rc");
|
|
13290
|
+
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
13291
|
+
const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
13292
|
+
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
13293
|
+
const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
13294
|
+
const resumeNote = resume.tag ? resume.note : void 0;
|
|
13295
|
+
await ensureTagPushed(deps, tag2, rcSha);
|
|
13296
|
+
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
13297
|
+
const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
|
|
13298
|
+
const autoRunSince = (deps.now ?? Date.now)();
|
|
13299
|
+
await deps.run("git", ["push", "origin", "rc"]);
|
|
13300
|
+
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
|
|
13301
|
+
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 };
|
|
13302
|
+
}
|
|
13303
|
+
if (mode === "release-dev") {
|
|
12555
13304
|
await requireBranch(deps, "development");
|
|
12556
13305
|
await ffOnlyPull(deps, "development");
|
|
12557
13306
|
const hasRcBranch = branchHints.hasRcBranch ?? false;
|
|
12558
|
-
if (hasRcBranch) {
|
|
13307
|
+
if (!directTrack && hasRcBranch) {
|
|
12559
13308
|
const rcOnlyOut = (await deps.run("git", ["rev-list", "--count", "--right-only", "--cherry-pick", "--no-merges", "origin/development...origin/rc"])).trim();
|
|
12560
13309
|
const rcOnly = Number.parseInt(rcOnlyOut, 10);
|
|
12561
13310
|
if (!Number.isFinite(rcOnly)) throw new Error(`release --dev: could not count rc-only commits for the guard: ${rcOnlyOut || "(empty)"}`);
|
|
@@ -12571,47 +13320,44 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12571
13320
|
);
|
|
12572
13321
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
12573
13322
|
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
|
-
}
|
|
13323
|
+
const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
13324
|
+
const { foldPaths: foldPaths2 } = await mergeSourceToMain(deps, deployModel2, {
|
|
13325
|
+
sourceRef: "development",
|
|
13326
|
+
remoteRef: "origin/development",
|
|
13327
|
+
mergeLabel: "development -> main",
|
|
13328
|
+
blockingPrefix: "development -> main merge would conflict on non-spine path(s)",
|
|
13329
|
+
realignMessage: "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
|
|
13330
|
+
});
|
|
12591
13331
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
12592
13332
|
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" };
|
|
13333
|
+
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
|
|
12603
13334
|
const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
13335
|
+
if (directTrack) {
|
|
13336
|
+
return {
|
|
13337
|
+
...ctx,
|
|
13338
|
+
command,
|
|
13339
|
+
stage: "main",
|
|
13340
|
+
ref: "main",
|
|
13341
|
+
tag: tag2,
|
|
13342
|
+
deployModel: deployModel2,
|
|
13343
|
+
promoted: true,
|
|
13344
|
+
checks: checks2,
|
|
13345
|
+
versionFold: versionFold2,
|
|
13346
|
+
dispatch: d2.note,
|
|
13347
|
+
runId: d2.runId,
|
|
13348
|
+
runUrl: d2.runUrl,
|
|
13349
|
+
workflowRuns: d2.workflowRuns,
|
|
13350
|
+
deployStatus: d2.deployStatus,
|
|
13351
|
+
rcRetirement: "not-applicable",
|
|
13352
|
+
rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
|
|
13353
|
+
devRollForward: devRollForward2,
|
|
13354
|
+
announceNote: announceNote2,
|
|
13355
|
+
devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
|
|
13356
|
+
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
13357
|
+
};
|
|
12614
13358
|
}
|
|
13359
|
+
const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
|
|
13360
|
+
const rcAlignment2 = hasRcBranch ? await pushRcAlignment(deps) : "no origin/rc branch \u2014 rc alignment skipped";
|
|
12615
13361
|
const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
|
|
12616
13362
|
return {
|
|
12617
13363
|
...ctx,
|
|
@@ -12645,15 +13391,13 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12645
13391
|
"nothing to release: origin/rc is not ahead of origin/main"
|
|
12646
13392
|
);
|
|
12647
13393
|
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
12648
|
-
const foldPaths = await
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
);
|
|
12656
|
-
}
|
|
13394
|
+
const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
|
|
13395
|
+
deps,
|
|
13396
|
+
deployModel,
|
|
13397
|
+
"origin/rc",
|
|
13398
|
+
"rc -> main merge would conflict on non-spine path(s)",
|
|
13399
|
+
"The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release."
|
|
13400
|
+
);
|
|
12657
13401
|
const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
|
|
12658
13402
|
if (!coverage.ok) {
|
|
12659
13403
|
const list = coverage.uncovered.map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join("; ");
|
|
@@ -12662,34 +13406,14 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12662
13406
|
);
|
|
12663
13407
|
}
|
|
12664
13408
|
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
|
-
}
|
|
13409
|
+
await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted);
|
|
12672
13410
|
const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
12673
13411
|
const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
12674
13412
|
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");
|
|
13413
|
+
const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
|
|
12684
13414
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
12685
13415
|
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
|
-
}
|
|
13416
|
+
const rcAlignment = await pushRcAlignment(deps);
|
|
12693
13417
|
const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
|
|
12694
13418
|
return {
|
|
12695
13419
|
...ctx,
|
|
@@ -12716,6 +13440,29 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
12716
13440
|
environments
|
|
12717
13441
|
};
|
|
12718
13442
|
}
|
|
13443
|
+
async function runTrainApply(command, deps, options = {}) {
|
|
13444
|
+
const watch = options.watch ?? false;
|
|
13445
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
13446
|
+
await requireCleanTree(deps);
|
|
13447
|
+
await deps.run("git", ["fetch", "origin"]);
|
|
13448
|
+
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
13449
|
+
const branchHints = await loadReleaseTrackBranchHints(deps);
|
|
13450
|
+
const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
|
|
13451
|
+
const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options };
|
|
13452
|
+
if (command === "rcand") {
|
|
13453
|
+
if (directTrack) {
|
|
13454
|
+
throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
|
|
13455
|
+
}
|
|
13456
|
+
return runTrainApplyPipeline("rcand", pipelineInput);
|
|
13457
|
+
}
|
|
13458
|
+
if (directTrack) {
|
|
13459
|
+
return runTrainApplyPipeline("release-dev", { ...pipelineInput, directTrack: true });
|
|
13460
|
+
}
|
|
13461
|
+
if (command === "release" && options.dev) {
|
|
13462
|
+
return runTrainApplyPipeline("release-dev", pipelineInput);
|
|
13463
|
+
}
|
|
13464
|
+
return runTrainApplyPipeline("release-full", pipelineInput);
|
|
13465
|
+
}
|
|
12719
13466
|
async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
|
|
12720
13467
|
if (model !== "tenant-container") return void 0;
|
|
12721
13468
|
const domains = deps.fetchEdgeDomains ? await deps.fetchEdgeDomains(ctx.slug).catch(() => null) : null;
|
|
@@ -12732,63 +13479,25 @@ async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
|
|
|
12732
13479
|
if (rcDomains?.length) rc.domains = rcDomains;
|
|
12733
13480
|
return { main, rc };
|
|
12734
13481
|
}
|
|
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
13482
|
var RETIRE_MAX_ATTEMPTS = 3;
|
|
12763
13483
|
var RETIRE_BACKOFF_MS = 1500;
|
|
12764
13484
|
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: {
|
|
13485
|
+
const r = await runTenantControl(deps, { repo: ctx.repo, stage: "rc", action: "retire", watch: true });
|
|
13486
|
+
if (r.category === "retired") {
|
|
13487
|
+
return {
|
|
12782
13488
|
status: "retired",
|
|
12783
|
-
category,
|
|
12784
|
-
note: `rc runtime retired (tenant
|
|
12785
|
-
}
|
|
12786
|
-
}
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
13489
|
+
category: "retired",
|
|
13490
|
+
note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
13491
|
+
};
|
|
13492
|
+
}
|
|
13493
|
+
if (r.category === "wait-timeout") {
|
|
13494
|
+
return {
|
|
13495
|
+
status: "failed",
|
|
13496
|
+
category: "wait-timeout",
|
|
13497
|
+
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})` : ""}`
|
|
13498
|
+
};
|
|
12791
13499
|
}
|
|
13500
|
+
return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
|
|
12792
13501
|
}
|
|
12793
13502
|
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
12794
13503
|
if (model !== "tenant-container") {
|
|
@@ -12812,19 +13521,18 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
12812
13521
|
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
13522
|
};
|
|
12814
13523
|
}
|
|
12815
|
-
const
|
|
13524
|
+
const sleep2 = resolveSleep(deps);
|
|
12816
13525
|
let last;
|
|
12817
13526
|
for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
|
|
12818
13527
|
last = await attemptRetire(deps, ctx);
|
|
12819
|
-
if (last.
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
await sleep(RETIRE_BACKOFF_MS * attempt);
|
|
13528
|
+
if (last.status === "retired") return last;
|
|
13529
|
+
if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
|
|
13530
|
+
await sleep2(RETIRE_BACKOFF_MS * attempt);
|
|
12823
13531
|
}
|
|
12824
13532
|
const f = last;
|
|
12825
|
-
|
|
12826
|
-
const note = `rc retirement failed (the release itself succeeded)
|
|
12827
|
-
return { status: "failed", category: f.
|
|
13533
|
+
if (f.category === "wait-timeout") return f;
|
|
13534
|
+
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)`;
|
|
13535
|
+
return { status: "failed", category: f.category, note };
|
|
12828
13536
|
} catch (e) {
|
|
12829
13537
|
const err = e;
|
|
12830
13538
|
return { status: "failed", category: "transport-failed", note: `rc retirement failed (the release itself succeeded): ${err.message}` };
|
|
@@ -12850,14 +13558,82 @@ async function runTenantRedeploy(deps, options) {
|
|
|
12850
13558
|
const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
|
|
12851
13559
|
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
13560
|
}
|
|
13561
|
+
function tenantControlWatches(action) {
|
|
13562
|
+
return action === "status" || action === "retire" || action === "verify-secrets";
|
|
13563
|
+
}
|
|
13564
|
+
async function runTenantControl(deps, options) {
|
|
13565
|
+
const { repo, stage: stage2, action } = options;
|
|
13566
|
+
const watch = options.watch ?? tenantControlWatches(action);
|
|
13567
|
+
const base2 = { command: "tenant-control", repo, stage: stage2, action };
|
|
13568
|
+
const since = (deps.now ?? Date.now)();
|
|
13569
|
+
const d = await deps.dispatchTenantControl({ repo, stage: stage2, action });
|
|
13570
|
+
if (!d.ok) {
|
|
13571
|
+
const transport = d.category === "transport-failed";
|
|
13572
|
+
return {
|
|
13573
|
+
...base2,
|
|
13574
|
+
dispatched: false,
|
|
13575
|
+
conclusion: "failure",
|
|
13576
|
+
category: action === "retire" ? transport ? "transport-failed" : "dispatch-rejected" : void 0,
|
|
13577
|
+
note: transport ? `tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
|
|
13578
|
+
};
|
|
13579
|
+
}
|
|
13580
|
+
const { runId, runUrl } = await correlateControlRun(deps, since);
|
|
13581
|
+
const conclusion = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
13582
|
+
const result = { ...base2, dispatched: true, runId, runUrl, conclusion, note: "" };
|
|
13583
|
+
if (action === "retire") {
|
|
13584
|
+
result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
|
|
13585
|
+
}
|
|
13586
|
+
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
|
|
13587
|
+
const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
|
|
13588
|
+
if (action === "status") {
|
|
13589
|
+
result.serviceState = parseStatusSnippet(output).serviceState;
|
|
13590
|
+
} else {
|
|
13591
|
+
result.secrets = parseVerifySecrets(output);
|
|
13592
|
+
}
|
|
13593
|
+
}
|
|
13594
|
+
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`;
|
|
13595
|
+
return result;
|
|
13596
|
+
}
|
|
13597
|
+
function renderTenantControl(r) {
|
|
13598
|
+
const head = `tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
|
|
13599
|
+
const lines = [head];
|
|
13600
|
+
if (r.runUrl) lines.push(` run: ${r.runUrl}`);
|
|
13601
|
+
if (r.serviceState) lines.push(` serviceState: ${r.serviceState}`);
|
|
13602
|
+
if (r.secrets?.length) {
|
|
13603
|
+
for (const s of r.secrets) lines.push(` ${s.key}: ${s.status}`);
|
|
13604
|
+
}
|
|
13605
|
+
lines.push(` ${r.note}`);
|
|
13606
|
+
return lines.join("\n");
|
|
13607
|
+
}
|
|
13608
|
+
|
|
13609
|
+
// src/tenant-verify-secrets.ts
|
|
13610
|
+
function renderVerifySecrets(body) {
|
|
13611
|
+
const secrets = body?.secrets ?? [];
|
|
13612
|
+
const counts = {
|
|
13613
|
+
match: secrets.filter((s) => s.status === "match").length,
|
|
13614
|
+
mismatch: secrets.filter((s) => s.status === "mismatch").length,
|
|
13615
|
+
missing: secrets.filter((s) => s.status === "missing").length
|
|
13616
|
+
};
|
|
13617
|
+
const lines = secrets.map((s) => `${s.key}: ${s.status}`);
|
|
13618
|
+
lines.push(`verify-secrets: ${counts.match} match, ${counts.mismatch} mismatch, ${counts.missing} missing`);
|
|
13619
|
+
const ssmStatus = body?.ssmStatus ?? "pending";
|
|
13620
|
+
if (ssmStatus !== "Success") {
|
|
13621
|
+
return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
|
|
13622
|
+
}
|
|
13623
|
+
const bad = counts.mismatch + counts.missing;
|
|
13624
|
+
if (bad > 0) {
|
|
13625
|
+
return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
|
|
13626
|
+
}
|
|
13627
|
+
return { lines, failure: null };
|
|
13628
|
+
}
|
|
12853
13629
|
|
|
12854
13630
|
// src/hotfix-coverage.ts
|
|
12855
|
-
var
|
|
13631
|
+
var import_node_child_process10 = require("node:child_process");
|
|
12856
13632
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
12857
13633
|
function checkHotfixCoverage(options = {}) {
|
|
12858
13634
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
12859
13635
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
12860
|
-
const git = options.git ?? ((args, opts) => (0,
|
|
13636
|
+
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
13637
|
const revList = (range) => {
|
|
12862
13638
|
const out = git(["rev-list", "--no-merges", range]).trim();
|
|
12863
13639
|
return out ? out.split("\n") : [];
|
|
@@ -12984,6 +13760,10 @@ function renderSweep(r) {
|
|
|
12984
13760
|
if (r.running > 0 && !r.retireAttempted) {
|
|
12985
13761
|
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
13762
|
}
|
|
13763
|
+
const undetermined = r.stages.filter((s) => s.serviceState === "unknown").length;
|
|
13764
|
+
if (undetermined > 0) {
|
|
13765
|
+
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.`);
|
|
13766
|
+
}
|
|
12987
13767
|
return lines.join("\n");
|
|
12988
13768
|
}
|
|
12989
13769
|
|
|
@@ -13017,7 +13797,7 @@ function hotfixBranch(tag) {
|
|
|
13017
13797
|
}
|
|
13018
13798
|
async function resolveHotfixDeployModel(deps, ctx) {
|
|
13019
13799
|
const load = await loadProjectMeta(deps, ctx);
|
|
13020
|
-
const meta = load
|
|
13800
|
+
const meta = requireProjectMetaForTrain(load, ctx.repo);
|
|
13021
13801
|
return resolveDeployModel2(meta, ctx.repo);
|
|
13022
13802
|
}
|
|
13023
13803
|
async function findHotfixPr(deps, ctx, tag) {
|
|
@@ -13132,9 +13912,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
|
|
|
13132
13912
|
return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
|
|
13133
13913
|
}
|
|
13134
13914
|
async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
13135
|
-
const
|
|
13915
|
+
const sleep2 = sleeper(deps);
|
|
13136
13916
|
for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
|
|
13137
|
-
if (attempt > 0) await
|
|
13917
|
+
if (attempt > 0) await sleep2(HOTFIX_RUN_FIND_DELAY_MS);
|
|
13138
13918
|
let rows;
|
|
13139
13919
|
try {
|
|
13140
13920
|
const out = await deps.run("gh", [
|
|
@@ -13209,8 +13989,9 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13209
13989
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
13210
13990
|
}
|
|
13211
13991
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
13212
|
-
} else if (deployModel === "tenant-container" || deployModel === "solo-container") {
|
|
13213
|
-
const
|
|
13992
|
+
} else if (deployModel === "tenant-container" || deployModel === "solo-container" || deployModel === "registry-publish") {
|
|
13993
|
+
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
13994
|
+
const deploy = await dispatchDeploy(
|
|
13214
13995
|
deps,
|
|
13215
13996
|
ctx,
|
|
13216
13997
|
"main",
|
|
@@ -13219,14 +14000,38 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13219
14000
|
true,
|
|
13220
14001
|
(deps.now ?? Date.now)(),
|
|
13221
14002
|
mergedSha,
|
|
13222
|
-
"report"
|
|
14003
|
+
"report",
|
|
14004
|
+
meta.publishDir
|
|
13223
14005
|
);
|
|
14006
|
+
const publish = deploy.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report");
|
|
14007
|
+
let dispatch = appendPublishDispatch(deploy, publish);
|
|
14008
|
+
if (!publish && deploy.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
14009
|
+
dispatch = {
|
|
14010
|
+
...dispatch,
|
|
14011
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
|
|
14012
|
+
};
|
|
14013
|
+
}
|
|
13224
14014
|
deployNote = dispatch.note;
|
|
13225
|
-
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
14015
|
+
if (deployModel !== "registry-publish") {
|
|
14016
|
+
runs.push({
|
|
14017
|
+
workflow: "tenant-deploy.yml",
|
|
14018
|
+
url: deploy.runUrl,
|
|
14019
|
+
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
14020
|
+
});
|
|
14021
|
+
}
|
|
14022
|
+
if (publish?.runUrl) {
|
|
14023
|
+
runs.push({
|
|
14024
|
+
workflow: "tenant-publish.yml",
|
|
14025
|
+
url: publish.runUrl,
|
|
14026
|
+
conclusion: publish.deployStatus === "success" ? "success" : publish.deployStatus === "failure" ? "failure" : publish.deployStatus ?? "pending"
|
|
14027
|
+
});
|
|
14028
|
+
} else if (deployModel === "registry-publish") {
|
|
14029
|
+
runs.push({
|
|
14030
|
+
workflow: "tenant-publish.yml",
|
|
14031
|
+
url: deploy.runUrl,
|
|
14032
|
+
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
14033
|
+
});
|
|
14034
|
+
}
|
|
13230
14035
|
} else {
|
|
13231
14036
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
13232
14037
|
}
|
|
@@ -13237,7 +14042,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13237
14042
|
try {
|
|
13238
14043
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
13239
14044
|
const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
|
|
13240
|
-
const
|
|
14045
|
+
const sleep2 = sleeper(deps);
|
|
13241
14046
|
let attempt = 0;
|
|
13242
14047
|
for (; ; ) {
|
|
13243
14048
|
attempt++;
|
|
@@ -13246,7 +14051,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
13246
14051
|
break;
|
|
13247
14052
|
} catch (err) {
|
|
13248
14053
|
if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
|
|
13249
|
-
await
|
|
14054
|
+
await sleep2(HOTFIX_VERIFY_RETRY_MS);
|
|
13250
14055
|
}
|
|
13251
14056
|
}
|
|
13252
14057
|
const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
|
|
@@ -13295,6 +14100,7 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
13295
14100
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
13296
14101
|
let tag;
|
|
13297
14102
|
let version;
|
|
14103
|
+
let warnings = [];
|
|
13298
14104
|
if (versionInput) {
|
|
13299
14105
|
({ tag, version } = normalizeHotfixVersion(versionInput));
|
|
13300
14106
|
} else {
|
|
@@ -13303,18 +14109,19 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
13303
14109
|
const latestFacts = await gatherHotfixFacts(deps, ctx, latest, latest.slice(1));
|
|
13304
14110
|
const latestDerived = deriveHotfixState(latestFacts);
|
|
13305
14111
|
if (latestDerived.state !== "complete") {
|
|
13306
|
-
return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived };
|
|
14112
|
+
return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived, warnings };
|
|
13307
14113
|
}
|
|
13308
14114
|
}
|
|
13309
|
-
const
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
14115
|
+
const found = await findInFlightHotfixVersion(deps, ctx, latest);
|
|
14116
|
+
warnings = supersededHotfixWarnings(found.superseded, latest);
|
|
14117
|
+
if (found.inFlight) {
|
|
14118
|
+
({ tag, version } = found.inFlight);
|
|
14119
|
+
} else {
|
|
14120
|
+
({ tag, version } = await deriveHotfixVersion(deps));
|
|
13313
14121
|
}
|
|
13314
|
-
({ tag, version } = await deriveHotfixVersion(deps));
|
|
13315
14122
|
}
|
|
13316
14123
|
const facts = await gatherHotfixFacts(deps, ctx, tag, version);
|
|
13317
|
-
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
14124
|
+
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts), warnings };
|
|
13318
14125
|
}
|
|
13319
14126
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
13320
14127
|
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
@@ -13356,7 +14163,19 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
13356
14163
|
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
13357
14164
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
13358
14165
|
}
|
|
13359
|
-
|
|
14166
|
+
function compareHotfixVersions(a, b) {
|
|
14167
|
+
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
14168
|
+
const pb = b.replace(/^v/, "").split(".").map(Number);
|
|
14169
|
+
for (let i = 0; i < 3; i++) {
|
|
14170
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
14171
|
+
}
|
|
14172
|
+
return 0;
|
|
14173
|
+
}
|
|
14174
|
+
function supersededHotfixWarnings(superseded, latestMainTag) {
|
|
14175
|
+
if (superseded.length === 0) return [];
|
|
14176
|
+
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.`];
|
|
14177
|
+
}
|
|
14178
|
+
async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
|
|
13360
14179
|
const tags = /* @__PURE__ */ new Set();
|
|
13361
14180
|
const out = await deps.run("gh", [
|
|
13362
14181
|
"pr",
|
|
@@ -13382,20 +14201,20 @@ async function findInFlightHotfixVersion(deps, ctx) {
|
|
|
13382
14201
|
const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
|
|
13383
14202
|
if (m) tags.add(m[1]);
|
|
13384
14203
|
}
|
|
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) {
|
|
14204
|
+
const sorted = [...tags].sort((a, b) => compareHotfixVersions(b, a));
|
|
14205
|
+
const fresh = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) > 0) : sorted;
|
|
14206
|
+
for (const tag of fresh) {
|
|
13394
14207
|
const version = tag.slice(1);
|
|
13395
14208
|
const facts = await gatherHotfixFacts(deps, ctx, tag, version);
|
|
13396
|
-
if (deriveHotfixState(facts).state !== "complete") return { tag, version };
|
|
14209
|
+
if (deriveHotfixState(facts).state !== "complete") return { inFlight: { tag, version }, superseded: [] };
|
|
13397
14210
|
}
|
|
13398
|
-
|
|
14211
|
+
const stale = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) <= 0) : [];
|
|
14212
|
+
const superseded = [];
|
|
14213
|
+
for (const tag of stale) {
|
|
14214
|
+
const facts = await gatherHotfixFacts(deps, ctx, tag, tag.slice(1));
|
|
14215
|
+
if (deriveHotfixState(facts).state !== "complete") superseded.push(tag);
|
|
14216
|
+
}
|
|
14217
|
+
return { inFlight: null, superseded };
|
|
13399
14218
|
}
|
|
13400
14219
|
|
|
13401
14220
|
// src/release-announce.ts
|
|
@@ -13525,7 +14344,7 @@ async function announceRelease(deps, args) {
|
|
|
13525
14344
|
}
|
|
13526
14345
|
|
|
13527
14346
|
// src/port-registry.ts
|
|
13528
|
-
var
|
|
14347
|
+
var import_node_fs20 = require("node:fs");
|
|
13529
14348
|
|
|
13530
14349
|
// ../infra/port-geometry.mjs
|
|
13531
14350
|
var PORT_BLOCK = 100;
|
|
@@ -13539,8 +14358,8 @@ function nextPortBlock(registry2) {
|
|
|
13539
14358
|
return [base2, base2 + PORT_SPAN];
|
|
13540
14359
|
}
|
|
13541
14360
|
function loadPortRegistry(path2) {
|
|
13542
|
-
if (!(0,
|
|
13543
|
-
const raw = JSON.parse((0,
|
|
14361
|
+
if (!(0, import_node_fs20.existsSync)(path2)) return {};
|
|
14362
|
+
const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
|
|
13544
14363
|
const out = {};
|
|
13545
14364
|
for (const [key, value] of Object.entries(raw)) {
|
|
13546
14365
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -13554,9 +14373,9 @@ function ensurePortRange(repo, path2) {
|
|
|
13554
14373
|
const existing = registry2[repo];
|
|
13555
14374
|
if (existing) return existing;
|
|
13556
14375
|
const range = nextPortBlock(registry2);
|
|
13557
|
-
const raw = (0,
|
|
14376
|
+
const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
|
|
13558
14377
|
raw[repo] = range;
|
|
13559
|
-
(0,
|
|
14378
|
+
(0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
13560
14379
|
return range;
|
|
13561
14380
|
}
|
|
13562
14381
|
function portCursorSeed(registry2) {
|
|
@@ -14279,17 +15098,16 @@ function renderBootstrapVerifyReport(report) {
|
|
|
14279
15098
|
var PROJECTS_LIST_PATH = "/projects/list";
|
|
14280
15099
|
var ORG_CONFIG_PATH = "/org/config";
|
|
14281
15100
|
var PROJECTS_ENVELOPE_KEY = "projects";
|
|
15101
|
+
var REGISTRY_FETCH_TIMEOUT_MS = 8e3;
|
|
14282
15102
|
|
|
14283
15103
|
// src/registry-client.ts
|
|
14284
|
-
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
14285
|
-
var WAITED_TENANT_CONTROL_TIMEOUT_MS = 13e3;
|
|
14286
15104
|
var TENANT_DEPLOY_TIMEOUT_MS = 12e4;
|
|
14287
15105
|
var RETRY_ATTEMPTS = 3;
|
|
14288
15106
|
function retriedFetch(deps, url, init) {
|
|
14289
15107
|
const headers = { ...clientVersionHeaders(), ...init.headers };
|
|
14290
15108
|
return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
|
|
14291
15109
|
attempts: RETRY_ATTEMPTS,
|
|
14292
|
-
timeoutMs: deps.timeoutMs ??
|
|
15110
|
+
timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS
|
|
14293
15111
|
});
|
|
14294
15112
|
}
|
|
14295
15113
|
async function fetchTrainAuthority(repo, deps) {
|
|
@@ -14389,7 +15207,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
|
|
|
14389
15207
|
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
15208
|
const token = await deps.token();
|
|
14391
15209
|
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 ??
|
|
15210
|
+
const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
|
|
14393
15211
|
const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
|
|
14394
15212
|
const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
|
|
14395
15213
|
try {
|
|
@@ -14418,38 +15236,12 @@ async function attestAppGaps(slug, repo, deps) {
|
|
|
14418
15236
|
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
14419
15237
|
}
|
|
14420
15238
|
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 });
|
|
15239
|
+
return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
|
|
14424
15240
|
}
|
|
14425
15241
|
async function tenantDeploy(payload, deps) {
|
|
14426
15242
|
return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
|
|
14427
15243
|
}
|
|
14428
15244
|
|
|
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
15245
|
// src/project-readiness.ts
|
|
14454
15246
|
function stagesForTrack(meta) {
|
|
14455
15247
|
return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
|
|
@@ -14771,14 +15563,14 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
14771
15563
|
const required = stageInTrack(meta, stage2) && projectRequiresDeployState(model, stage2);
|
|
14772
15564
|
return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
|
|
14773
15565
|
})));
|
|
14774
|
-
const
|
|
15566
|
+
const secrets = Object.fromEntries(STAGES.map((stage2) => {
|
|
14775
15567
|
const required = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key)) : [];
|
|
14776
15568
|
const present = required.filter((key) => presentSecrets.has(key));
|
|
14777
15569
|
const missing = required.filter((key) => !presentSecrets.has(key));
|
|
14778
15570
|
return [stage2, { required, present, missing }];
|
|
14779
15571
|
}));
|
|
14780
15572
|
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(
|
|
15573
|
+
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
|
|
14782
15574
|
const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
|
|
14783
15575
|
return {
|
|
14784
15576
|
ok,
|
|
@@ -14787,7 +15579,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
14787
15579
|
class: meta.class,
|
|
14788
15580
|
projectType,
|
|
14789
15581
|
deployModel: model,
|
|
14790
|
-
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets
|
|
15582
|
+
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
|
|
14791
15583
|
secretsError,
|
|
14792
15584
|
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
14793
15585
|
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
@@ -14837,7 +15629,7 @@ ${section}`.trim();
|
|
|
14837
15629
|
}
|
|
14838
15630
|
|
|
14839
15631
|
// src/project-set.ts
|
|
14840
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "ci", "requiredChecks", "gate"];
|
|
15632
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "ci", "requiredChecks", "gate"];
|
|
14841
15633
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
14842
15634
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
14843
15635
|
function parseRuntimeSecretsVar(raw) {
|
|
@@ -14996,6 +15788,21 @@ function parsePublishRequiredVar(raw) {
|
|
|
14996
15788
|
if (raw === "false") return false;
|
|
14997
15789
|
throw new Error("project set: publishRequired must be true or false");
|
|
14998
15790
|
}
|
|
15791
|
+
function parseDashboardVar(raw) {
|
|
15792
|
+
if (raw === "true") return true;
|
|
15793
|
+
if (raw === "false") return false;
|
|
15794
|
+
throw new Error("project set: dashboard must be true or false");
|
|
15795
|
+
}
|
|
15796
|
+
function parsePublishDirVar(raw) {
|
|
15797
|
+
const v = raw.trim();
|
|
15798
|
+
if (v === "" || v === ".") {
|
|
15799
|
+
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)");
|
|
15800
|
+
}
|
|
15801
|
+
if (!/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
|
|
15802
|
+
throw new Error('project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
|
|
15803
|
+
}
|
|
15804
|
+
return v;
|
|
15805
|
+
}
|
|
14999
15806
|
function parseRequiredChecksVar(raw) {
|
|
15000
15807
|
let parsed;
|
|
15001
15808
|
try {
|
|
@@ -15046,6 +15853,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
15046
15853
|
"repos",
|
|
15047
15854
|
"oauth",
|
|
15048
15855
|
"publishRequired",
|
|
15856
|
+
"publishDir",
|
|
15857
|
+
"dashboard",
|
|
15049
15858
|
"requiredGcpApis",
|
|
15050
15859
|
"requiredRuntimeSecrets",
|
|
15051
15860
|
"edgeDomains",
|
|
@@ -15062,6 +15871,8 @@ var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
|
15062
15871
|
var SETTABLE_VAR_HINTS = {
|
|
15063
15872
|
projectNumber: "numeric",
|
|
15064
15873
|
publishRequired: "true|false",
|
|
15874
|
+
publishDir: "relative subpath, e.g. packages/ui",
|
|
15875
|
+
dashboard: "true|false",
|
|
15065
15876
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
15066
15877
|
oauth: "JSON {subdomains,domains,callbackPath}",
|
|
15067
15878
|
requiredGcpApis: "comma-string",
|
|
@@ -15135,6 +15946,10 @@ function buildProjectSetPatch(input) {
|
|
|
15135
15946
|
patch[key] = parseReposVar(raw);
|
|
15136
15947
|
} else if (key === "publishRequired") {
|
|
15137
15948
|
patch[key] = parsePublishRequiredVar(raw);
|
|
15949
|
+
} else if (key === "dashboard") {
|
|
15950
|
+
patch[key] = parseDashboardVar(raw);
|
|
15951
|
+
} else if (key === "publishDir") {
|
|
15952
|
+
patch[key] = parsePublishDirVar(raw);
|
|
15138
15953
|
} else if (key === "ci") {
|
|
15139
15954
|
if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
|
|
15140
15955
|
patch[key] = raw;
|
|
@@ -15199,11 +16014,16 @@ function parseKbTree(stdout, prefix) {
|
|
|
15199
16014
|
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
16015
|
}
|
|
15201
16016
|
|
|
16017
|
+
// src/northstar-commands.ts
|
|
16018
|
+
var import_node_fs21 = require("node:fs");
|
|
16019
|
+
var import_node_child_process11 = require("node:child_process");
|
|
16020
|
+
var import_promises6 = require("node:fs/promises");
|
|
16021
|
+
|
|
15202
16022
|
// src/plan.ts
|
|
15203
|
-
var
|
|
16023
|
+
var import_node_path18 = require("node:path");
|
|
15204
16024
|
var PLANS_DIR = "plans";
|
|
15205
|
-
var META_FILE = (0,
|
|
15206
|
-
var planPath = (slug) => (0,
|
|
16025
|
+
var META_FILE = (0, import_node_path18.join)(PLANS_DIR, ".plan-meta.json");
|
|
16026
|
+
var planPath = (slug) => (0, import_node_path18.join)(PLANS_DIR, `${slug}.md`);
|
|
15207
16027
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
15208
16028
|
function parseMeta(raw) {
|
|
15209
16029
|
if (!raw) return {};
|
|
@@ -15228,7 +16048,7 @@ function hashContent(s) {
|
|
|
15228
16048
|
function staleHint(slug) {
|
|
15229
16049
|
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
16050
|
}
|
|
15231
|
-
var INDEX_FILE = (0,
|
|
16051
|
+
var INDEX_FILE = (0, import_node_path18.join)(PLANS_DIR, ".index.json");
|
|
15232
16052
|
var INDEX_TTL_MS = 6e4;
|
|
15233
16053
|
function parseIndex(raw) {
|
|
15234
16054
|
if (!raw) return null;
|
|
@@ -15257,7 +16077,7 @@ function mergeIndex(idx, scope, plans, now) {
|
|
|
15257
16077
|
const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
|
|
15258
16078
|
return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
|
|
15259
16079
|
}
|
|
15260
|
-
var QUEUE_FILE = (0,
|
|
16080
|
+
var QUEUE_FILE = (0, import_node_path18.join)(PLANS_DIR, ".sync-queue.json");
|
|
15261
16081
|
var QUEUE_MAX_ATTEMPTS = 10;
|
|
15262
16082
|
function isValidQueueEntry(e) {
|
|
15263
16083
|
if (!e || typeof e !== "object") return false;
|
|
@@ -15716,23 +16536,298 @@ async function planGraduate(deps, slug, opts = {}) {
|
|
|
15716
16536
|
if (pushed) deps.log(`graduated ${slug}`);
|
|
15717
16537
|
}
|
|
15718
16538
|
|
|
15719
|
-
// src/
|
|
15720
|
-
var
|
|
15721
|
-
function
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
16539
|
+
// src/northstar-commands.ts
|
|
16540
|
+
var planSyncDetached = false;
|
|
16541
|
+
function detachPlanSync() {
|
|
16542
|
+
if (planSyncDetached) return;
|
|
16543
|
+
planSyncDetached = true;
|
|
16544
|
+
try {
|
|
16545
|
+
(0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
|
|
16546
|
+
detached: true,
|
|
16547
|
+
stdio: "ignore",
|
|
16548
|
+
windowsHide: true,
|
|
16549
|
+
cwd: process.cwd()
|
|
16550
|
+
}).unref();
|
|
16551
|
+
} catch {
|
|
16552
|
+
}
|
|
16553
|
+
}
|
|
16554
|
+
function makePlanDeps(cfg, io = consoleIo) {
|
|
16555
|
+
const ensureDir = () => (0, import_node_fs21.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
16556
|
+
return {
|
|
16557
|
+
apiUrl: cfg.sagaApiUrl,
|
|
16558
|
+
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
16559
|
+
headers: (extra) => hubHeaders(extra),
|
|
16560
|
+
project: async () => (await sagaKey(cfg)).project,
|
|
16561
|
+
readLocal: (slug) => {
|
|
16562
|
+
try {
|
|
16563
|
+
return (0, import_node_fs21.readFileSync)(planPath(slug), "utf8");
|
|
16564
|
+
} catch {
|
|
16565
|
+
return null;
|
|
16566
|
+
}
|
|
16567
|
+
},
|
|
16568
|
+
writeLocal: (slug, content) => {
|
|
16569
|
+
ensureDir();
|
|
16570
|
+
(0, import_node_fs21.writeFileSync)(planPath(slug), content, "utf8");
|
|
16571
|
+
},
|
|
16572
|
+
removeLocal: (slug) => {
|
|
16573
|
+
try {
|
|
16574
|
+
(0, import_node_fs21.rmSync)(planPath(slug));
|
|
16575
|
+
} catch {
|
|
16576
|
+
}
|
|
16577
|
+
},
|
|
16578
|
+
readMetaRaw: () => {
|
|
16579
|
+
try {
|
|
16580
|
+
return (0, import_node_fs21.readFileSync)(META_FILE, "utf8");
|
|
16581
|
+
} catch {
|
|
16582
|
+
return null;
|
|
16583
|
+
}
|
|
16584
|
+
},
|
|
16585
|
+
writeMetaRaw: (raw) => {
|
|
16586
|
+
ensureDir();
|
|
16587
|
+
atomicWriteFileSync(META_FILE, raw);
|
|
16588
|
+
},
|
|
16589
|
+
readIndexRaw: () => {
|
|
16590
|
+
try {
|
|
16591
|
+
return (0, import_node_fs21.readFileSync)(INDEX_FILE, "utf8");
|
|
16592
|
+
} catch {
|
|
16593
|
+
return null;
|
|
16594
|
+
}
|
|
16595
|
+
},
|
|
16596
|
+
writeIndexRaw: (raw) => {
|
|
16597
|
+
ensureDir();
|
|
16598
|
+
atomicWriteFileSync(INDEX_FILE, raw);
|
|
16599
|
+
},
|
|
16600
|
+
readQueueRaw: () => {
|
|
16601
|
+
try {
|
|
16602
|
+
return (0, import_node_fs21.readFileSync)(QUEUE_FILE, "utf8");
|
|
16603
|
+
} catch {
|
|
16604
|
+
return null;
|
|
16605
|
+
}
|
|
16606
|
+
},
|
|
16607
|
+
writeQueueRaw: (raw) => {
|
|
16608
|
+
ensureDir();
|
|
16609
|
+
atomicWriteFileSync(QUEUE_FILE, raw);
|
|
16610
|
+
},
|
|
16611
|
+
detachSync: detachPlanSync,
|
|
16612
|
+
log: (m) => io.log(m),
|
|
16613
|
+
err: (m) => io.err(m),
|
|
16614
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
16615
|
+
};
|
|
16616
|
+
}
|
|
16617
|
+
function openInEditor(path2) {
|
|
16618
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
16619
|
+
if (!editor) {
|
|
16620
|
+
console.log(`plan at ${path2} (set $EDITOR to open it automatically)`);
|
|
16621
|
+
return;
|
|
16622
|
+
}
|
|
16623
|
+
try {
|
|
16624
|
+
(0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
|
|
16625
|
+
} catch {
|
|
16626
|
+
console.log(`open ${path2} manually`);
|
|
16627
|
+
}
|
|
16628
|
+
}
|
|
16629
|
+
async function withPlan(quiet, run, io = consoleIo) {
|
|
16630
|
+
const cfg = await loadConfig();
|
|
16631
|
+
if (!cfg.sagaApiUrl) {
|
|
16632
|
+
if (!quiet) fail("plan: Hub API URL not configured");
|
|
16633
|
+
return;
|
|
16634
|
+
}
|
|
16635
|
+
await run(makePlanDeps(cfg, io));
|
|
16636
|
+
}
|
|
16637
|
+
async function gatherRelevanceSignals() {
|
|
16638
|
+
const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
|
|
16639
|
+
const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
16640
|
+
const signals = { branch, changedFiles: changed.length ? changed : void 0 };
|
|
16641
|
+
const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
|
|
16642
|
+
if (issueNum) {
|
|
16643
|
+
try {
|
|
16644
|
+
const { stdout } = await execFileP2(
|
|
16645
|
+
"gh",
|
|
16646
|
+
["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
|
|
16647
|
+
{ timeout: 1e4 }
|
|
16648
|
+
);
|
|
16649
|
+
const j = JSON.parse(stdout);
|
|
16650
|
+
if (j.title) signals.issueTitle = j.title;
|
|
16651
|
+
if (j.labels?.length) signals.issueLabels = j.labels;
|
|
16652
|
+
} catch {
|
|
16653
|
+
}
|
|
16654
|
+
}
|
|
16655
|
+
return signals;
|
|
16656
|
+
}
|
|
16657
|
+
function registerNorthStarCommands(cmd) {
|
|
16658
|
+
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) => {
|
|
16659
|
+
let content;
|
|
16660
|
+
if (o.bodyFile) {
|
|
16661
|
+
try {
|
|
16662
|
+
content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises6.readFile, readStdin }, {
|
|
16663
|
+
value: "inline content",
|
|
16664
|
+
file: "--body-file",
|
|
16665
|
+
noun: "plan"
|
|
16666
|
+
});
|
|
16667
|
+
} catch (e) {
|
|
16668
|
+
console.error(e.message);
|
|
16669
|
+
process.exitCode = 1;
|
|
16670
|
+
return;
|
|
16671
|
+
}
|
|
16672
|
+
}
|
|
16673
|
+
return withPlan(false, async (d) => {
|
|
16674
|
+
const ok = await planPush(d, slug, { project: o.project, force: o.force, wait: o.wait, content });
|
|
16675
|
+
if (!ok) process.exitCode = 1;
|
|
16676
|
+
});
|
|
16677
|
+
});
|
|
16678
|
+
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) => {
|
|
16679
|
+
const ok = await planPull(d, slug, o);
|
|
16680
|
+
if (!ok) process.exitCode = 1;
|
|
16681
|
+
}));
|
|
16682
|
+
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) => {
|
|
16683
|
+
const ok = await planShow(d, slug, o);
|
|
16684
|
+
if (!ok) process.exitCode = 1;
|
|
16685
|
+
}));
|
|
16686
|
+
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)));
|
|
16687
|
+
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) => {
|
|
16688
|
+
const signals = await gatherRelevanceSignals();
|
|
16689
|
+
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
|
|
16690
|
+
}));
|
|
16691
|
+
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) => {
|
|
16692
|
+
const unresolved = await planSync(d, o);
|
|
16693
|
+
if (!o.wait) return;
|
|
16694
|
+
if (unresolved.length) {
|
|
16695
|
+
for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
|
|
16696
|
+
process.exitCode = 1;
|
|
16697
|
+
} else if (!o.quiet) {
|
|
16698
|
+
d.log("north star: all queued pushes landed");
|
|
16699
|
+
}
|
|
16700
|
+
}));
|
|
16701
|
+
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)));
|
|
16702
|
+
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)));
|
|
16703
|
+
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
16704
|
+
(slug, o) => withPlan(false, async (d) => {
|
|
16705
|
+
const ok = await planPull(d, slug, { project: o.project });
|
|
16706
|
+
if (!ok) {
|
|
16707
|
+
process.exitCode = 1;
|
|
16708
|
+
return;
|
|
16709
|
+
}
|
|
16710
|
+
openInEditor(planPath(slug));
|
|
16711
|
+
})
|
|
16712
|
+
);
|
|
16713
|
+
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)));
|
|
16714
|
+
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(
|
|
16715
|
+
(slug, o) => withPlan(false, (d) => planGraduate(d, slug, o))
|
|
16716
|
+
);
|
|
16717
|
+
}
|
|
16718
|
+
|
|
16719
|
+
// src/secrets-commands.ts
|
|
16720
|
+
async function readSecretStdin() {
|
|
16721
|
+
if (process.stdin.isTTY) {
|
|
16722
|
+
process.stderr.write(
|
|
16723
|
+
'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'
|
|
16724
|
+
);
|
|
16725
|
+
return "";
|
|
16726
|
+
}
|
|
16727
|
+
const chunks = [];
|
|
16728
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
16729
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
16730
|
+
}
|
|
16731
|
+
function makeSecretsDeps(cfg) {
|
|
16732
|
+
return {
|
|
16733
|
+
apiUrl: cfg.sagaApiUrl,
|
|
16734
|
+
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
16735
|
+
headers: (extra) => hubHeaders(extra),
|
|
16736
|
+
// Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
|
|
16737
|
+
// casing, which leaked mixed-case into `secrets where` output (#681).
|
|
16738
|
+
slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
|
|
16739
|
+
readSecretValue: () => readSecretStdin(),
|
|
16740
|
+
log: (m) => console.log(m),
|
|
16741
|
+
err: (m) => console.error(m)
|
|
16742
|
+
};
|
|
16743
|
+
}
|
|
16744
|
+
async function withSecrets(run) {
|
|
16745
|
+
const cfg = await loadConfig();
|
|
16746
|
+
if (!cfg.sagaApiUrl) {
|
|
16747
|
+
fail("secrets: Hub API URL not configured");
|
|
16748
|
+
return;
|
|
16749
|
+
}
|
|
16750
|
+
await run(makeSecretsDeps(cfg));
|
|
16751
|
+
}
|
|
16752
|
+
function registerSecretsCommands(program3) {
|
|
16753
|
+
const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
|
|
16754
|
+
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)));
|
|
16755
|
+
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)));
|
|
16756
|
+
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) => {
|
|
16757
|
+
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
16758
|
+
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
16759
|
+
}
|
|
16760
|
+
const cfg = await loadConfig();
|
|
16761
|
+
if (!cfg.sagaApiUrl) {
|
|
16762
|
+
fail("secrets: Hub API URL not configured");
|
|
16763
|
+
return;
|
|
16764
|
+
}
|
|
16765
|
+
const d = makeSecretsDeps(cfg);
|
|
16766
|
+
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
16767
|
+
const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
|
|
16768
|
+
const repo = o.repo ?? `mutmutco/${slug}`;
|
|
16769
|
+
const meta = await fetchProjectBySlug(slug, regDeps);
|
|
16770
|
+
const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets, {
|
|
16771
|
+
includeGoogleOAuth: projectRequiresGoogleOAuth(meta, meta?.deployModel)
|
|
16772
|
+
});
|
|
16773
|
+
const centralContainer = meta?.deployModel === "tenant-container" || meta?.deployModel === "solo-container";
|
|
16774
|
+
if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
|
|
16775
|
+
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");
|
|
16776
|
+
process.exitCode = 1;
|
|
16777
|
+
return;
|
|
16778
|
+
}
|
|
16779
|
+
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
|
|
16780
|
+
if (!ok) process.exitCode = 1;
|
|
16781
|
+
});
|
|
16782
|
+
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) => {
|
|
16783
|
+
const ok = await secretsGet(d, key, o);
|
|
16784
|
+
if (!ok) process.exitCode = 1;
|
|
16785
|
+
}));
|
|
16786
|
+
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) => {
|
|
16787
|
+
const ok = await secretsRequest(d, key, o);
|
|
16788
|
+
if (!ok) process.exitCode = 1;
|
|
16789
|
+
}));
|
|
16790
|
+
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) => {
|
|
16791
|
+
const ok = await secretsVerify(d, key, o);
|
|
16792
|
+
if (!ok) process.exitCode = 1;
|
|
16793
|
+
}));
|
|
16794
|
+
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) => {
|
|
16795
|
+
const ok = await secretsSet(d, key, o);
|
|
16796
|
+
if (!ok) process.exitCode = 1;
|
|
16797
|
+
}));
|
|
16798
|
+
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) => {
|
|
16799
|
+
const ok = await secretsEdit(d, key, o);
|
|
16800
|
+
if (!ok) process.exitCode = 1;
|
|
16801
|
+
}));
|
|
16802
|
+
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) => {
|
|
16803
|
+
const stages = ["dev", "rc", "main"];
|
|
16804
|
+
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
16805
|
+
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
16806
|
+
}
|
|
16807
|
+
const ok = await secretsCopy(d, {
|
|
16808
|
+
repo: o.repo,
|
|
16809
|
+
from: o.from,
|
|
16810
|
+
to: o.to,
|
|
16811
|
+
keys: o.keys.split(","),
|
|
16812
|
+
dryRun: o.dryRun
|
|
16813
|
+
});
|
|
16814
|
+
if (!ok) process.exitCode = 1;
|
|
16815
|
+
}));
|
|
16816
|
+
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)));
|
|
16817
|
+
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)));
|
|
16818
|
+
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, {})));
|
|
16819
|
+
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, {})));
|
|
16820
|
+
}
|
|
16821
|
+
|
|
16822
|
+
// src/oauth.ts
|
|
16823
|
+
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
16824
|
+
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
16825
|
+
var ENV_PREFIXES = ["", "dev", "rc"];
|
|
16826
|
+
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
16827
|
+
var SSM_ENVS = ["dev", "rc", "main"];
|
|
16828
|
+
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
16829
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
16830
|
+
function defaultSubdomain2(slug) {
|
|
15736
16831
|
const i = slug.indexOf("-");
|
|
15737
16832
|
return i === -1 ? slug : slug.slice(i + 1);
|
|
15738
16833
|
}
|
|
@@ -15952,7 +17047,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
15952
17047
|
}
|
|
15953
17048
|
function readRepoVersion() {
|
|
15954
17049
|
try {
|
|
15955
|
-
return JSON.parse((0,
|
|
17050
|
+
return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
15956
17051
|
} catch {
|
|
15957
17052
|
return void 0;
|
|
15958
17053
|
}
|
|
@@ -15998,6 +17093,53 @@ async function fetchNpmReleasedVersion() {
|
|
|
15998
17093
|
return void 0;
|
|
15999
17094
|
}
|
|
16000
17095
|
}
|
|
17096
|
+
async function fetchUiPackageLatestVersion(packageName) {
|
|
17097
|
+
try {
|
|
17098
|
+
const { stdout } = await runHostBin("npm", npmUiPackageLatestArgs(packageName), { timeout: NPM_VIEW_TIMEOUT_MS });
|
|
17099
|
+
return parseNpmViewVersion(stdout);
|
|
17100
|
+
} catch {
|
|
17101
|
+
return void 0;
|
|
17102
|
+
}
|
|
17103
|
+
}
|
|
17104
|
+
async function applyDesignSystemUpdate(check, log) {
|
|
17105
|
+
if (check.ok || !check.packageName) return check;
|
|
17106
|
+
try {
|
|
17107
|
+
log(` \u21BB updating ${check.packageName} ${check.installedVersion ?? "(missing)"} \u2192 ${check.latestVersion ?? "latest"}\u2026`);
|
|
17108
|
+
await runHostBin("npm", ["update", check.packageName], { timeout: NPM_UPDATE_TIMEOUT_MS });
|
|
17109
|
+
const installedVersion = designSystemSnapshot(process.cwd()).installedVersion ?? check.latestVersion;
|
|
17110
|
+
if (check.latestVersion && installedVersion && compareVersions(installedVersion, check.latestVersion) >= 0) {
|
|
17111
|
+
return { ...check, ok: true, installedVersion };
|
|
17112
|
+
}
|
|
17113
|
+
return { ...check, installedVersion };
|
|
17114
|
+
} catch {
|
|
17115
|
+
return check;
|
|
17116
|
+
}
|
|
17117
|
+
}
|
|
17118
|
+
async function applyRegistryComponentsSyncCheck(check, targetVersion, log) {
|
|
17119
|
+
if (check.ok || !check.components?.length) return check;
|
|
17120
|
+
const result = await applyRegistryComponentsSync(
|
|
17121
|
+
process.cwd(),
|
|
17122
|
+
check.components,
|
|
17123
|
+
targetVersion ?? check.targetVersion,
|
|
17124
|
+
log,
|
|
17125
|
+
defaultRegistrySyncDeps()
|
|
17126
|
+
);
|
|
17127
|
+
if (!result.ok) return check;
|
|
17128
|
+
const state = await gatherRegistryComponentsState(process.cwd(), targetVersion ?? check.targetVersion, { fetch });
|
|
17129
|
+
return buildRegistryComponentsCheck({ ...state, isConsumerRepo: true });
|
|
17130
|
+
}
|
|
17131
|
+
async function resolveDashboardConsumer(cfg) {
|
|
17132
|
+
if (!cfg.sagaApiUrl || isUiFactoryCheckout(process.cwd())) return { isConsumer: false };
|
|
17133
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg));
|
|
17134
|
+
if (!read.ok) return { isConsumer: false, registryReadFailed: read.error };
|
|
17135
|
+
return { isConsumer: isDashboardMetaConsumer(read.project) };
|
|
17136
|
+
}
|
|
17137
|
+
function buildDesignSystemRegistryReadCheck(error) {
|
|
17138
|
+
return { ok: false, label: DESIGN_SYSTEM_VERSION_LABEL, fix: dashboardConsumerRegistryFix(error) };
|
|
17139
|
+
}
|
|
17140
|
+
function buildRegistryComponentsRegistryReadCheck(error) {
|
|
17141
|
+
return { ok: false, label: REGISTRY_COMPONENTS_LABEL, fix: dashboardConsumerRegistryFix(error) };
|
|
17142
|
+
}
|
|
16001
17143
|
async function requireFreshTrainCli(commandName) {
|
|
16002
17144
|
if (process.env.MMI_TRAIN_FRESH_OVERRIDE === "1") return;
|
|
16003
17145
|
const report = buildVersionLagReport({
|
|
@@ -16019,8 +17161,8 @@ async function runClaudePlugin(args) {
|
|
|
16019
17161
|
return false;
|
|
16020
17162
|
}
|
|
16021
17163
|
}
|
|
16022
|
-
async function applyClaudePluginHeal(surface, log) {
|
|
16023
|
-
if (surface !== "claude-cli" && surface !== "claude-vscode") return false;
|
|
17164
|
+
async function applyClaudePluginHeal(surface, log, opts) {
|
|
17165
|
+
if (!opts?.force && surface !== "claude-cli" && surface !== "claude-vscode") return false;
|
|
16024
17166
|
log(" \u21BB reinstalling the MMI plugin via `claude plugin` (marketplace remove \u2192 add \u2192 install)\u2026");
|
|
16025
17167
|
for (const step of CLAUDE_PLUGIN_HEAL_STEPS) {
|
|
16026
17168
|
if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
|
|
@@ -16035,8 +17177,8 @@ async function runCodexPlugin(args) {
|
|
|
16035
17177
|
return false;
|
|
16036
17178
|
}
|
|
16037
17179
|
}
|
|
16038
|
-
async function applyCodexPluginHeal(surface, log) {
|
|
16039
|
-
if (surface !== "codex") return false;
|
|
17180
|
+
async function applyCodexPluginHeal(surface, log, opts) {
|
|
17181
|
+
if (!opts?.force && surface !== "codex") return false;
|
|
16040
17182
|
log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
|
|
16041
17183
|
for (const step of CODEX_PLUGIN_HEAL_STEPS) {
|
|
16042
17184
|
if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
|
|
@@ -16060,7 +17202,8 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
16060
17202
|
".claude/settings.json",
|
|
16061
17203
|
".claude/output-styles/mmi-plain.md",
|
|
16062
17204
|
".cursor/rules/mmi-plain-language.mdc",
|
|
16063
|
-
".cursor/rules/mmi-tool-economy.mdc"
|
|
17205
|
+
".cursor/rules/mmi-tool-economy.mdc",
|
|
17206
|
+
".cursor/rules/mmi-code-economy.mdc"
|
|
16064
17207
|
];
|
|
16065
17208
|
const fetched = await Promise.all(files.map(async (file) => {
|
|
16066
17209
|
try {
|
|
@@ -16079,11 +17222,11 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
16079
17222
|
for (const entry of fetched) {
|
|
16080
17223
|
if ("error" in entry) continue;
|
|
16081
17224
|
const { file, source } = entry;
|
|
16082
|
-
const current = (0,
|
|
17225
|
+
const current = (0, import_node_fs22.existsSync)(file) ? await (0, import_promises7.readFile)(file, "utf8") : null;
|
|
16083
17226
|
if (needsUpdate(source, current)) {
|
|
16084
17227
|
const slash = file.lastIndexOf("/");
|
|
16085
|
-
if (slash > 0) (0,
|
|
16086
|
-
await (0,
|
|
17228
|
+
if (slash > 0) (0, import_node_fs22.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
17229
|
+
await (0, import_promises7.writeFile)(file, normalizeEol(source), "utf8");
|
|
16087
17230
|
changed++;
|
|
16088
17231
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
16089
17232
|
}
|
|
@@ -16108,9 +17251,9 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
16108
17251
|
return null;
|
|
16109
17252
|
}
|
|
16110
17253
|
},
|
|
16111
|
-
localContent: async (f) => (0,
|
|
17254
|
+
localContent: async (f) => (0, import_node_fs22.existsSync)(f) ? await (0, import_promises7.readFile)(f, "utf8") : null,
|
|
16112
17255
|
writeDoc: async (f, c) => {
|
|
16113
|
-
await (0,
|
|
17256
|
+
await (0, import_promises7.writeFile)(f, c, "utf8");
|
|
16114
17257
|
}
|
|
16115
17258
|
});
|
|
16116
17259
|
for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
|
|
@@ -16185,7 +17328,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
16185
17328
|
const file = isWin ? "cmd.exe" : bin;
|
|
16186
17329
|
const spawnArgs = isWin ? ["/c", bin, ...args] : args;
|
|
16187
17330
|
return new Promise((resolve, reject) => {
|
|
16188
|
-
const child = (0,
|
|
17331
|
+
const child = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
16189
17332
|
const timer = setTimeout(() => {
|
|
16190
17333
|
try {
|
|
16191
17334
|
child.kill();
|
|
@@ -16207,7 +17350,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
16207
17350
|
async function primaryCheckoutRoot(worktreeRoot) {
|
|
16208
17351
|
try {
|
|
16209
17352
|
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,
|
|
17353
|
+
return out ? (0, import_node_path19.dirname)(out) : void 0;
|
|
16211
17354
|
} catch {
|
|
16212
17355
|
return void 0;
|
|
16213
17356
|
}
|
|
@@ -16220,28 +17363,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
16220
17363
|
};
|
|
16221
17364
|
}
|
|
16222
17365
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
16223
|
-
const lockPath = (0,
|
|
17366
|
+
const lockPath = (0, import_node_path19.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
|
|
16224
17367
|
const take = () => {
|
|
16225
|
-
const fd = (0,
|
|
17368
|
+
const fd = (0, import_node_fs22.openSync)(lockPath, "wx");
|
|
16226
17369
|
try {
|
|
16227
|
-
(0,
|
|
17370
|
+
(0, import_node_fs22.writeSync)(fd, String(Date.now()));
|
|
16228
17371
|
} finally {
|
|
16229
|
-
(0,
|
|
17372
|
+
(0, import_node_fs22.closeSync)(fd);
|
|
16230
17373
|
}
|
|
16231
17374
|
return () => {
|
|
16232
17375
|
try {
|
|
16233
|
-
(0,
|
|
17376
|
+
(0, import_node_fs22.rmSync)(lockPath, { force: true });
|
|
16234
17377
|
} catch {
|
|
16235
17378
|
}
|
|
16236
17379
|
};
|
|
16237
17380
|
};
|
|
16238
17381
|
try {
|
|
16239
|
-
(0,
|
|
17382
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(lockPath), { recursive: true });
|
|
16240
17383
|
return take();
|
|
16241
17384
|
} catch {
|
|
16242
17385
|
try {
|
|
16243
|
-
if (Date.now() - (0,
|
|
16244
|
-
(0,
|
|
17386
|
+
if (Date.now() - (0, import_node_fs22.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
17387
|
+
(0, import_node_fs22.rmSync)(lockPath, { force: true });
|
|
16245
17388
|
return take();
|
|
16246
17389
|
}
|
|
16247
17390
|
} catch {
|
|
@@ -16320,361 +17463,90 @@ async function ghCreate(args) {
|
|
|
16320
17463
|
try {
|
|
16321
17464
|
const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
16322
17465
|
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;
|
|
17466
|
+
} catch (e) {
|
|
17467
|
+
await swapped.cleanup();
|
|
17468
|
+
const err = e;
|
|
17469
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
17470
|
+
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
17471
|
+
} finally {
|
|
17472
|
+
await swapped.cleanup();
|
|
16481
17473
|
}
|
|
17474
|
+
}
|
|
17475
|
+
async function ghJson(args, timeout = 1e4) {
|
|
17476
|
+
const { stdout } = await execFileP2("gh", args, { timeout });
|
|
17477
|
+
return JSON.parse(stdout);
|
|
17478
|
+
}
|
|
17479
|
+
async function resolveRepo(repo) {
|
|
17480
|
+
if (repo) return repo;
|
|
17481
|
+
const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
|
|
17482
|
+
if (fromOrigin) return fromOrigin;
|
|
16482
17483
|
try {
|
|
16483
|
-
(
|
|
17484
|
+
const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
|
|
17485
|
+
return stdout.trim() || void 0;
|
|
16484
17486
|
} catch {
|
|
16485
|
-
|
|
17487
|
+
return void 0;
|
|
16486
17488
|
}
|
|
16487
17489
|
}
|
|
16488
|
-
async function
|
|
16489
|
-
const
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
17490
|
+
async function attachToProject(issueNumber, repo, priority) {
|
|
17491
|
+
const targetRepo2 = await resolveRepo(repo);
|
|
17492
|
+
let cfg;
|
|
17493
|
+
try {
|
|
17494
|
+
cfg = await loadConfigForRepo(targetRepo2);
|
|
17495
|
+
} catch (e) {
|
|
17496
|
+
console.error(`issue create: board attach skipped \u2014 ${e.message}`);
|
|
17497
|
+
return void 0;
|
|
16493
17498
|
}
|
|
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
|
-
}
|
|
17499
|
+
if (!cfg.projectId) {
|
|
17500
|
+
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`);
|
|
17501
|
+
return void 0;
|
|
16513
17502
|
}
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
if (
|
|
17503
|
+
try {
|
|
17504
|
+
const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
|
|
17505
|
+
if (targetRepo2) viewArgs.push("--repo", targetRepo2);
|
|
17506
|
+
const { stdout: idOut } = await execFileP2("gh", viewArgs, { timeout: 1e4 });
|
|
17507
|
+
const contentId = idOut.trim();
|
|
17508
|
+
if (!contentId) throw new Error("could not resolve issue node id");
|
|
17509
|
+
const { stdout } = await execFileP2("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
17510
|
+
const projectItemId = parseAddedItemId(stdout);
|
|
17511
|
+
if (projectItemId && priority) {
|
|
16520
17512
|
try {
|
|
16521
|
-
|
|
16522
|
-
value: "inline content",
|
|
16523
|
-
file: "--body-file",
|
|
16524
|
-
noun: "plan"
|
|
16525
|
-
});
|
|
17513
|
+
await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
|
|
16526
17514
|
} catch (e) {
|
|
16527
|
-
|
|
16528
|
-
process.
|
|
16529
|
-
|
|
17515
|
+
const err = e;
|
|
17516
|
+
process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
|
|
17517
|
+
`);
|
|
16530
17518
|
}
|
|
16531
17519
|
}
|
|
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 "";
|
|
17520
|
+
return projectItemId;
|
|
17521
|
+
} catch (e) {
|
|
17522
|
+
const err = e;
|
|
17523
|
+
process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${(err.stderr || err.message || String(e)).trim()}
|
|
17524
|
+
`);
|
|
17525
|
+
return void 0;
|
|
16587
17526
|
}
|
|
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
17527
|
}
|
|
16605
|
-
async
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
17528
|
+
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
17529
|
+
function scheduleRelatedDiscovery(o) {
|
|
17530
|
+
try {
|
|
17531
|
+
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
17532
|
+
if (o.repo) args.push("--repo", o.repo);
|
|
17533
|
+
(0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
17534
|
+
detached: true,
|
|
17535
|
+
stdio: "ignore",
|
|
17536
|
+
windowsHide: true,
|
|
17537
|
+
cwd: process.cwd()
|
|
17538
|
+
}).unref();
|
|
17539
|
+
} catch {
|
|
16610
17540
|
}
|
|
16611
|
-
await run(makeSecretsDeps(cfg));
|
|
16612
17541
|
}
|
|
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;
|
|
17542
|
+
var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
|
|
17543
|
+
registerNorthStarCommands(northstar);
|
|
17544
|
+
var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
|
|
17545
|
+
plan.hook("preAction", () => {
|
|
17546
|
+
process.stderr.write("warning: `plan` is deprecated; use `northstar` instead\n");
|
|
16639
17547
|
});
|
|
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, {})));
|
|
17548
|
+
registerNorthStarCommands(plan);
|
|
17549
|
+
registerSecretsCommands(program2);
|
|
16678
17550
|
function registryClientDeps(cfg) {
|
|
16679
17551
|
return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
16680
17552
|
}
|
|
@@ -16691,23 +17563,23 @@ async function reportWrite(label, res) {
|
|
|
16691
17563
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
16692
17564
|
}
|
|
16693
17565
|
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
|
-
|
|
17566
|
+
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) => {
|
|
17567
|
+
try {
|
|
17568
|
+
const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
|
|
17569
|
+
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
17570
|
+
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
|
|
17571
|
+
const { lines, failure } = renderVerifySecrets(body);
|
|
17572
|
+
for (const line of lines) printLine(line);
|
|
17573
|
+
if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
|
|
17574
|
+
} else {
|
|
17575
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
17576
|
+
}
|
|
17577
|
+
if (result.conclusion === "failure") {
|
|
17578
|
+
return failGraceful(`tenant control ${stage2} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
|
|
17579
|
+
}
|
|
17580
|
+
} catch (e) {
|
|
17581
|
+
return failGraceful(`tenant control: ${e.message}`);
|
|
16709
17582
|
}
|
|
16710
|
-
return reportWrite("tenant control", res);
|
|
16711
17583
|
});
|
|
16712
17584
|
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
17585
|
if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
|
|
@@ -16724,18 +17596,18 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
16724
17596
|
}
|
|
16725
17597
|
const cfg = await loadConfig();
|
|
16726
17598
|
const cdeps = registryClientDeps(cfg);
|
|
17599
|
+
const tdeps = trainApplyDeps();
|
|
16727
17600
|
try {
|
|
16728
17601
|
const result = await sweepRcOrphans({
|
|
16729
17602
|
listProjects: () => fetchProjectsList(cdeps),
|
|
16730
17603
|
status: async (repo) => {
|
|
16731
|
-
const
|
|
16732
|
-
const
|
|
16733
|
-
return { serviceState
|
|
17604
|
+
const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "status", watch: true });
|
|
17605
|
+
const serviceState = r.conclusion === "success" ? r.serviceState ?? "unknown" : "error";
|
|
17606
|
+
return { serviceState };
|
|
16734
17607
|
},
|
|
16735
17608
|
retire: async (repo) => {
|
|
16736
|
-
const
|
|
16737
|
-
|
|
16738
|
-
return { ok: res.ok, category: b?.category, reason: b?.reason };
|
|
17609
|
+
const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "retire", watch: true });
|
|
17610
|
+
return { ok: r.category === "retired", category: r.category };
|
|
16739
17611
|
}
|
|
16740
17612
|
}, { retire: !!o.retire });
|
|
16741
17613
|
return printLine(o.json ? JSON.stringify(result) : renderSweep(result));
|
|
@@ -16922,7 +17794,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
|
|
|
16922
17794
|
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
16923
17795
|
return reportWrite("project attest", res);
|
|
16924
17796
|
});
|
|
16925
|
-
project.command("set [owner/repo]").description("
|
|
17797
|
+
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
17798
|
const cfg = await loadConfig();
|
|
16927
17799
|
let target;
|
|
16928
17800
|
try {
|
|
@@ -16931,6 +17803,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
|
|
|
16931
17803
|
return fail(e.message);
|
|
16932
17804
|
}
|
|
16933
17805
|
const slug = slugOf(target);
|
|
17806
|
+
const repo = target.includes("/") ? target : `mutmutco/${slug}`;
|
|
16934
17807
|
let patch;
|
|
16935
17808
|
try {
|
|
16936
17809
|
patch = buildProjectSetPatch({
|
|
@@ -16948,7 +17821,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
|
|
|
16948
17821
|
const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
|
|
16949
17822
|
const boardError = boardLinkWriteError(patch, existing);
|
|
16950
17823
|
if (boardError) return fail(`project set: ${boardError}`);
|
|
16951
|
-
const res = await upsertProject(slug, patch, registryClientDeps(cfg));
|
|
17824
|
+
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
16952
17825
|
return reportWrite("project set", res);
|
|
16953
17826
|
});
|
|
16954
17827
|
var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
|
|
@@ -17053,8 +17926,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
17053
17926
|
let body;
|
|
17054
17927
|
let title;
|
|
17055
17928
|
try {
|
|
17056
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
17057
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
17929
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17930
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17058
17931
|
if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
|
|
17059
17932
|
priority = normalizePriority(o.priority);
|
|
17060
17933
|
args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
|
|
@@ -17141,8 +18014,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
17141
18014
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
17142
18015
|
const sourceRepo = await resolveRepo(void 0);
|
|
17143
18016
|
try {
|
|
17144
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
17145
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18017
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
18018
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17146
18019
|
priority = normalizePriority(o.priority);
|
|
17147
18020
|
args = buildIssueArgs({
|
|
17148
18021
|
type: o.type,
|
|
@@ -17208,8 +18081,8 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
|
|
|
17208
18081
|
try {
|
|
17209
18082
|
const routing = assertVerifyRouting(o.routing);
|
|
17210
18083
|
const lenses = o.lenses.split(",").map((s) => assertGrindLens(s.trim()));
|
|
17211
|
-
const criteria = await (0,
|
|
17212
|
-
const diff = await (0,
|
|
18084
|
+
const criteria = await (0, import_promises7.readFile)(o.criteriaFile, "utf8");
|
|
18085
|
+
const diff = await (0, import_promises7.readFile)(o.diffFile, "utf8");
|
|
17213
18086
|
const plan2 = buildPanelPlan({ routing, lenses, criteria, diff });
|
|
17214
18087
|
console.log(JSON.stringify(plan2));
|
|
17215
18088
|
} catch (e) {
|
|
@@ -17218,7 +18091,7 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
|
|
|
17218
18091
|
});
|
|
17219
18092
|
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
18093
|
try {
|
|
17221
|
-
const raw = o.inputFile === "-" ? await readStdin() : await (0,
|
|
18094
|
+
const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises7.readFile)(o.inputFile, "utf8");
|
|
17222
18095
|
const lenses = parseLensResults(JSON.parse(raw));
|
|
17223
18096
|
console.log(JSON.stringify(synthesizePanelReport(lenses)));
|
|
17224
18097
|
} catch (e) {
|
|
@@ -17291,7 +18164,7 @@ build.command("frontier").description("Evaluate external frontier exhaustion + L
|
|
|
17291
18164
|
iterationCapOverride: opts.iterationCap
|
|
17292
18165
|
};
|
|
17293
18166
|
if (opts.jsonFile) {
|
|
17294
|
-
const raw = await (0,
|
|
18167
|
+
const raw = await (0, import_promises7.readFile)(opts.jsonFile, "utf8");
|
|
17295
18168
|
state = { ...state, ...JSON.parse(raw) };
|
|
17296
18169
|
}
|
|
17297
18170
|
const result = evaluateBuildFrontier(state);
|
|
@@ -17345,8 +18218,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
17345
18218
|
let args;
|
|
17346
18219
|
try {
|
|
17347
18220
|
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:
|
|
18221
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
18222
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17350
18223
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
17351
18224
|
priority = normalizePriority(o.priority);
|
|
17352
18225
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -17397,8 +18270,8 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
17397
18270
|
let body;
|
|
17398
18271
|
let title;
|
|
17399
18272
|
try {
|
|
17400
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
17401
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18273
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
18274
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
17402
18275
|
} catch (e) {
|
|
17403
18276
|
return fail(`pr create: ${e.message}`);
|
|
17404
18277
|
}
|
|
@@ -17406,9 +18279,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
17406
18279
|
console.log(JSON.stringify(created));
|
|
17407
18280
|
});
|
|
17408
18281
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
17409
|
-
const wfDir = (0,
|
|
17410
|
-
if (!(0,
|
|
17411
|
-
return (0,
|
|
18282
|
+
const wfDir = (0, import_node_path19.join)(cwd, ".github", "workflows");
|
|
18283
|
+
if (!(0, import_node_fs22.existsSync)(wfDir)) return [];
|
|
18284
|
+
return (0, import_node_fs22.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
17412
18285
|
}
|
|
17413
18286
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
17414
18287
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -17427,7 +18300,7 @@ function ciAuditDeps() {
|
|
|
17427
18300
|
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
|
|
17428
18301
|
// reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
|
|
17429
18302
|
// the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
|
|
17430
|
-
readSeedFile: (path2) => (0,
|
|
18303
|
+
readSeedFile: (path2) => (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null
|
|
17431
18304
|
};
|
|
17432
18305
|
}
|
|
17433
18306
|
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 +18340,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
17467
18340
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
17468
18341
|
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
17469
18342
|
resolveRepo: async (prNumber, repoOpt) => {
|
|
17470
|
-
|
|
17471
|
-
const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...
|
|
17472
|
-
const [
|
|
18343
|
+
const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
|
|
18344
|
+
const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
18345
|
+
const [repoFromGh, base2] = viewed.split(/\s+/);
|
|
17473
18346
|
if (base2 && base2 !== "development") {
|
|
17474
18347
|
throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
|
|
17475
18348
|
}
|
|
18349
|
+
const repo = repoOpt ?? repoFromGh;
|
|
17476
18350
|
if (!repo) throw new Error("pr land: could not resolve PR repo");
|
|
17477
18351
|
return repo;
|
|
17478
18352
|
},
|
|
@@ -17497,32 +18371,50 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
17497
18371
|
}
|
|
17498
18372
|
return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
|
|
17499
18373
|
}
|
|
17500
|
-
const
|
|
17501
|
-
|
|
18374
|
+
const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS })).stdout);
|
|
18375
|
+
if (!stateRead.ok) {
|
|
18376
|
+
return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
|
|
18377
|
+
}
|
|
18378
|
+
return { mergeStatus: stateRead.state === "MERGED" ? "merged" : "auto-merge-enqueued" };
|
|
17502
18379
|
},
|
|
17503
18380
|
pollMerged: async (prNumber, repo, deadlineMs) => {
|
|
17504
18381
|
const args = repo ? ["--repo", repo] : [];
|
|
17505
18382
|
while (Date.now() < deadlineMs) {
|
|
17506
|
-
const
|
|
17507
|
-
if (state === "MERGED") return true;
|
|
18383
|
+
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 });
|
|
18384
|
+
if (stateRead.ok && stateRead.state === "MERGED") return true;
|
|
17508
18385
|
await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
|
|
17509
18386
|
}
|
|
17510
18387
|
return false;
|
|
17511
18388
|
}
|
|
17512
18389
|
});
|
|
18390
|
+
if (result.status !== "failed") {
|
|
18391
|
+
try {
|
|
18392
|
+
const { stdout } = await execFileP2(process.execPath, [
|
|
18393
|
+
process.argv[1],
|
|
18394
|
+
"pr",
|
|
18395
|
+
"merge",
|
|
18396
|
+
number,
|
|
18397
|
+
...o.repo ? ["--repo", o.repo] : [],
|
|
18398
|
+
"--squash"
|
|
18399
|
+
], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
18400
|
+
const trimmed = stdout.trim();
|
|
18401
|
+
if (trimmed) {
|
|
18402
|
+
try {
|
|
18403
|
+
result.cleanup = JSON.parse(trimmed);
|
|
18404
|
+
} catch {
|
|
18405
|
+
result.cleanupError = "cleanup output was not JSON";
|
|
18406
|
+
}
|
|
18407
|
+
}
|
|
18408
|
+
} catch (e) {
|
|
18409
|
+
result.cleanupError = String(e.message || "pr merge cleanup failed");
|
|
18410
|
+
}
|
|
18411
|
+
}
|
|
17513
18412
|
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
18413
|
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);
|
|
18414
|
+
printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
18415
|
+
if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
|
|
17525
18416
|
}
|
|
18417
|
+
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
17526
18418
|
});
|
|
17527
18419
|
async function remoteBranchExists2(branch, options = {}) {
|
|
17528
18420
|
return checkRemoteBranchExists(branch, {
|
|
@@ -17537,15 +18429,15 @@ async function createDeferredWorktreeStore() {
|
|
|
17537
18429
|
return {
|
|
17538
18430
|
read: async () => {
|
|
17539
18431
|
try {
|
|
17540
|
-
return parseDeferredWorktreesFile(await (0,
|
|
18432
|
+
return parseDeferredWorktreesFile(await (0, import_promises7.readFile)(registryPath, "utf8"));
|
|
17541
18433
|
} catch {
|
|
17542
18434
|
return [];
|
|
17543
18435
|
}
|
|
17544
18436
|
},
|
|
17545
18437
|
write: async (entries) => {
|
|
17546
18438
|
try {
|
|
17547
|
-
await (0,
|
|
17548
|
-
await (0,
|
|
18439
|
+
await (0, import_promises7.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
|
|
18440
|
+
await (0, import_promises7.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
17549
18441
|
} catch {
|
|
17550
18442
|
}
|
|
17551
18443
|
}
|
|
@@ -17558,13 +18450,13 @@ var realWorktreeDirRemover = {
|
|
|
17558
18450
|
probe: (p) => {
|
|
17559
18451
|
let st;
|
|
17560
18452
|
try {
|
|
17561
|
-
st = (0,
|
|
18453
|
+
st = (0, import_node_fs22.lstatSync)(p);
|
|
17562
18454
|
} catch {
|
|
17563
18455
|
return null;
|
|
17564
18456
|
}
|
|
17565
18457
|
if (st.isSymbolicLink()) return "link";
|
|
17566
18458
|
try {
|
|
17567
|
-
(0,
|
|
18459
|
+
(0, import_node_fs22.readlinkSync)(p);
|
|
17568
18460
|
return "link";
|
|
17569
18461
|
} catch {
|
|
17570
18462
|
}
|
|
@@ -17572,7 +18464,7 @@ var realWorktreeDirRemover = {
|
|
|
17572
18464
|
},
|
|
17573
18465
|
readdir: (p) => {
|
|
17574
18466
|
try {
|
|
17575
|
-
return (0,
|
|
18467
|
+
return (0, import_node_fs22.readdirSync)(p);
|
|
17576
18468
|
} catch {
|
|
17577
18469
|
return [];
|
|
17578
18470
|
}
|
|
@@ -17581,12 +18473,12 @@ var realWorktreeDirRemover = {
|
|
|
17581
18473
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
17582
18474
|
detachLink: (p) => {
|
|
17583
18475
|
try {
|
|
17584
|
-
(0,
|
|
18476
|
+
(0, import_node_fs22.rmdirSync)(p);
|
|
17585
18477
|
} catch {
|
|
17586
|
-
(0,
|
|
18478
|
+
(0, import_node_fs22.unlinkSync)(p);
|
|
17587
18479
|
}
|
|
17588
18480
|
},
|
|
17589
|
-
removeTree: (p) => (0,
|
|
18481
|
+
removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
17590
18482
|
};
|
|
17591
18483
|
async function resolvePrimaryCheckout(execGit) {
|
|
17592
18484
|
try {
|
|
@@ -17604,7 +18496,7 @@ function worktreeRemoveDeps(execGit) {
|
|
|
17604
18496
|
}
|
|
17605
18497
|
function teardownWorktreeStage(worktreePath) {
|
|
17606
18498
|
return runWorktreeStageTeardown(worktreePath, {
|
|
17607
|
-
hasStageState: (wt) => (0,
|
|
18499
|
+
hasStageState: (wt) => (0, import_node_fs22.existsSync)(stageStatePath(wt)),
|
|
17608
18500
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
17609
18501
|
listComposeProjects: async () => {
|
|
17610
18502
|
const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -17618,7 +18510,7 @@ function teardownWorktreeStage(worktreePath) {
|
|
|
17618
18510
|
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
18511
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
17620
18512
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
17621
|
-
const headRef = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq",
|
|
18513
|
+
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
18514
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
17623
18515
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
17624
18516
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
@@ -17667,11 +18559,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
17667
18559
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
17668
18560
|
beforeWorktrees,
|
|
17669
18561
|
startingPath,
|
|
17670
|
-
pathExists: (p) => (0,
|
|
18562
|
+
pathExists: (p) => (0, import_node_fs22.existsSync)(p),
|
|
17671
18563
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
17672
18564
|
teardownWorktreeStage,
|
|
17673
18565
|
deferredStore,
|
|
17674
|
-
removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir
|
|
18566
|
+
removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir,
|
|
18567
|
+
// After merge, return the local checkout to the (fast-forwarded) branch the PR merged into so
|
|
18568
|
+
// grind/build never leave the primary parked on a dead feature branch (#1606).
|
|
18569
|
+
returnToBranch: baseRef
|
|
17675
18570
|
});
|
|
17676
18571
|
} catch (e) {
|
|
17677
18572
|
localCleanup = {
|
|
@@ -17839,7 +18734,7 @@ function rawValues(flag) {
|
|
|
17839
18734
|
return out;
|
|
17840
18735
|
}
|
|
17841
18736
|
function printLine(value) {
|
|
17842
|
-
(0,
|
|
18737
|
+
(0, import_node_fs22.writeSync)(1, `${value}
|
|
17843
18738
|
`);
|
|
17844
18739
|
}
|
|
17845
18740
|
function stageKeepAlive() {
|
|
@@ -17856,8 +18751,8 @@ async function resolveStage() {
|
|
|
17856
18751
|
local,
|
|
17857
18752
|
shell: shellFor(),
|
|
17858
18753
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
17859
|
-
hasCompose: (0,
|
|
17860
|
-
hasEnvExample: (0,
|
|
18754
|
+
hasCompose: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), "docker-compose.yml")),
|
|
18755
|
+
hasEnvExample: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), ".env.example"))
|
|
17861
18756
|
});
|
|
17862
18757
|
}
|
|
17863
18758
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -17909,9 +18804,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
17909
18804
|
printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
|
|
17910
18805
|
return;
|
|
17911
18806
|
}
|
|
17912
|
-
const path2 = (0,
|
|
18807
|
+
const path2 = (0, import_node_path19.join)(process.cwd(), "infra", "port-ranges.json");
|
|
17913
18808
|
const allocate = async (seed) => {
|
|
17914
|
-
const { stdout } = await execFileP2("node", [(0,
|
|
18809
|
+
const { stdout } = await execFileP2("node", [(0, import_node_path19.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
17915
18810
|
const parsed = JSON.parse(stdout);
|
|
17916
18811
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
17917
18812
|
return parsed.range;
|
|
@@ -18097,6 +18992,15 @@ function trainApplyDeps() {
|
|
|
18097
18992
|
throw new Error(`tenant deploy dispatch failed: ${detail}`);
|
|
18098
18993
|
}
|
|
18099
18994
|
},
|
|
18995
|
+
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
18996
|
+
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
18997
|
+
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
18998
|
+
dispatchTenantControl: async ({ repo, stage: stage2, action }) => {
|
|
18999
|
+
const res = await tenantControl({ repo, stage: stage2, action }, registryClientDeps(await loadConfig()));
|
|
19000
|
+
if (res.ok) return { ok: true };
|
|
19001
|
+
const body = res.body;
|
|
19002
|
+
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
19003
|
+
},
|
|
18100
19004
|
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
18101
19005
|
// release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
|
|
18102
19006
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
@@ -18105,7 +19009,7 @@ function trainApplyDeps() {
|
|
|
18105
19009
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
18106
19010
|
announce: (args) => announceRelease({
|
|
18107
19011
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
18108
|
-
readFile: (path2) => (0,
|
|
19012
|
+
readFile: (path2) => (0, import_promises7.readFile)(path2, "utf8")
|
|
18109
19013
|
}, args),
|
|
18110
19014
|
fetchEdgeDomains: async (slug) => {
|
|
18111
19015
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -18215,7 +19119,8 @@ function renderHotfixStatus(r) {
|
|
|
18215
19119
|
` - 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
19120
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
18217
19121
|
` - npm @mutmutco/cli: ${r.npmVersion}`,
|
|
18218
|
-
` - next: ${r.next}
|
|
19122
|
+
` - next: ${r.next}`,
|
|
19123
|
+
...r.warnings.map((w) => ` - warning: ${w}`)
|
|
18219
19124
|
].join("\n");
|
|
18220
19125
|
}
|
|
18221
19126
|
async function runHotfixSub(sub, body, json, render) {
|
|
@@ -18273,12 +19178,12 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
|
|
|
18273
19178
|
}
|
|
18274
19179
|
if (!audit.ok) process.exitCode = 1;
|
|
18275
19180
|
});
|
|
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) => {
|
|
19181
|
+
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
19182
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
18278
19183
|
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|
|
18279
19184
|
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));
|
|
19185
|
+
const steps = bootstrapPlan(o.repo, o.class, { dashboard: o.dashboard });
|
|
19186
|
+
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
19187
|
});
|
|
18283
19188
|
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
19189
|
const o = { class: rawValue("--class", "deployable"), json: rawFlag("--json") };
|
|
@@ -18292,7 +19197,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
18292
19197
|
client: defaultGitHubClient(),
|
|
18293
19198
|
projectMeta: meta,
|
|
18294
19199
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
18295
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
19200
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
|
|
18296
19201
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
18297
19202
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
18298
19203
|
requiredGcpApis: (() => {
|
|
@@ -18317,12 +19222,13 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
18317
19222
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
18318
19223
|
if (!report.ok) process.exitCode = 1;
|
|
18319
19224
|
});
|
|
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) => {
|
|
19225
|
+
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
19226
|
const o = {
|
|
18322
19227
|
class: rawValue("--class", "deployable"),
|
|
18323
19228
|
projectType: rawValue("--project-type", ""),
|
|
18324
19229
|
deployModel: rawValue("--deploy-model", ""),
|
|
18325
19230
|
releaseTrack: rawValue("--release-track", ""),
|
|
19231
|
+
dashboard: rawFlag("--dashboard"),
|
|
18326
19232
|
execute: rawFlag("--execute"),
|
|
18327
19233
|
json: rawFlag("--json")
|
|
18328
19234
|
};
|
|
@@ -18335,20 +19241,22 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18335
19241
|
return fail(`bootstrap apply: ${e.message}`);
|
|
18336
19242
|
}
|
|
18337
19243
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
18338
|
-
if (!(0,
|
|
18339
|
-
const manifest = loadBootstrapSeeds((0,
|
|
19244
|
+
if (!(0, import_node_fs22.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
19245
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
|
|
18340
19246
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
18341
19247
|
const slug = parsedRepo.slug;
|
|
18342
19248
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
18343
|
-
const
|
|
19249
|
+
const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
|
|
18344
19250
|
const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
18345
19251
|
const rawVars = {};
|
|
18346
19252
|
for (const value of rawValues("--var")) {
|
|
18347
19253
|
const eq = value.indexOf("=");
|
|
18348
19254
|
if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
18349
19255
|
}
|
|
19256
|
+
let registryMetaDashboard = false;
|
|
18350
19257
|
try {
|
|
18351
19258
|
const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
19259
|
+
registryMetaDashboard = meta?.dashboard === true;
|
|
18352
19260
|
for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
|
|
18353
19261
|
} catch {
|
|
18354
19262
|
}
|
|
@@ -18365,16 +19273,20 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18365
19273
|
const applied = [];
|
|
18366
19274
|
let applyDeployModel;
|
|
18367
19275
|
try {
|
|
18368
|
-
|
|
19276
|
+
const payload = buildRegisterPayload(repo, o.class, vars, {
|
|
18369
19277
|
projectType: o.projectType || void 0,
|
|
18370
19278
|
deployModel: o.deployModel || void 0,
|
|
18371
|
-
releaseTrack: o.releaseTrack || void 0
|
|
18372
|
-
|
|
19279
|
+
releaseTrack: o.releaseTrack || void 0,
|
|
19280
|
+
dashboard: o.dashboard
|
|
19281
|
+
});
|
|
19282
|
+
applyDeployModel = payload.deployModel;
|
|
18373
19283
|
} catch {
|
|
18374
19284
|
}
|
|
19285
|
+
const applyDashboard = o.dashboard === true || !rawFlag("--dashboard") && registryMetaDashboard;
|
|
18375
19286
|
for (const seed of manifest.seeds) {
|
|
18376
19287
|
if (!seed.classes.includes(o.class)) continue;
|
|
18377
19288
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
19289
|
+
if (!seedMatchesDashboard(seed, applyDashboard)) continue;
|
|
18378
19290
|
const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
|
|
18379
19291
|
let exists = false;
|
|
18380
19292
|
let sha;
|
|
@@ -18397,7 +19309,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18397
19309
|
}
|
|
18398
19310
|
const planned = planSeedAction(resolved, exists);
|
|
18399
19311
|
const isBlock = resolved.source === "managed-block";
|
|
18400
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
19312
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
|
|
18401
19313
|
const action = reconcileSeedAction(planned, content, isBlock);
|
|
18402
19314
|
actions.push(action);
|
|
18403
19315
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
@@ -18414,7 +19326,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18414
19326
|
}
|
|
18415
19327
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
18416
19328
|
if (rulesetSeed) {
|
|
18417
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
19329
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
|
|
18418
19330
|
if (rulesetContent) {
|
|
18419
19331
|
try {
|
|
18420
19332
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -18450,7 +19362,8 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
18450
19362
|
registerPayload = buildRegisterPayload(repo, o.class, vars, {
|
|
18451
19363
|
projectType: o.projectType || void 0,
|
|
18452
19364
|
deployModel: o.deployModel || void 0,
|
|
18453
|
-
releaseTrack: bootstrapReleaseTrack
|
|
19365
|
+
releaseTrack: bootstrapReleaseTrack,
|
|
19366
|
+
dashboard: o.dashboard
|
|
18454
19367
|
});
|
|
18455
19368
|
} catch (e) {
|
|
18456
19369
|
return fail(`bootstrap apply: ${e.message}`);
|
|
@@ -18584,38 +19497,39 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
18584
19497
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
18585
19498
|
targets = [{ repo: o.repo, class: o.class }];
|
|
18586
19499
|
} else {
|
|
18587
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
19500
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs22.existsSync)("projects.json") ? (0, import_node_fs22.readFileSync)("projects.json", "utf8") : null;
|
|
18588
19501
|
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,
|
|
19502
|
+
const fanoutJson = (0, import_node_fs22.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs22.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
18590
19503
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
18591
19504
|
}
|
|
18592
19505
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
18593
|
-
const fileMatrix = (0,
|
|
19506
|
+
const fileMatrix = (0, import_node_fs22.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs22.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
18594
19507
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
18595
19508
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
18596
|
-
const fileContracts = (0,
|
|
19509
|
+
const fileContracts = (0, import_node_fs22.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs22.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
18597
19510
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
18598
19511
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
18599
19512
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
18600
19513
|
if (!report.ok) process.exitCode = 1;
|
|
18601
19514
|
});
|
|
19515
|
+
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
19516
|
var isWin = process.platform === "win32";
|
|
18603
19517
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
18604
19518
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
18605
|
-
return (0,
|
|
19519
|
+
return (0, import_node_path19.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
18606
19520
|
};
|
|
18607
19521
|
function readInstalledPlugins() {
|
|
18608
19522
|
try {
|
|
18609
|
-
return JSON.parse((0,
|
|
19523
|
+
return JSON.parse((0, import_node_fs22.readFileSync)(installedPluginsPath(), "utf8"));
|
|
18610
19524
|
} catch {
|
|
18611
19525
|
return null;
|
|
18612
19526
|
}
|
|
18613
19527
|
}
|
|
18614
19528
|
function installedPluginSources() {
|
|
18615
19529
|
return ["claude", "codex"].map((surface) => {
|
|
18616
|
-
const recordPath = (0,
|
|
19530
|
+
const recordPath = (0, import_node_path19.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
18617
19531
|
try {
|
|
18618
|
-
return { surface, installed: JSON.parse((0,
|
|
19532
|
+
return { surface, installed: JSON.parse((0, import_node_fs22.readFileSync)(recordPath, "utf8")), recordPath };
|
|
18619
19533
|
} catch {
|
|
18620
19534
|
return { surface, installed: null, recordPath };
|
|
18621
19535
|
}
|
|
@@ -18623,7 +19537,7 @@ function installedPluginSources() {
|
|
|
18623
19537
|
}
|
|
18624
19538
|
function readClaudeSettings() {
|
|
18625
19539
|
try {
|
|
18626
|
-
return JSON.parse((0,
|
|
19540
|
+
return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
18627
19541
|
} catch {
|
|
18628
19542
|
return null;
|
|
18629
19543
|
}
|
|
@@ -18645,7 +19559,7 @@ function writeProjectInstallRecord(record) {
|
|
|
18645
19559
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
18646
19560
|
list.push(record);
|
|
18647
19561
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
18648
|
-
(0,
|
|
19562
|
+
(0, import_node_fs22.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
18649
19563
|
`, "utf8");
|
|
18650
19564
|
return true;
|
|
18651
19565
|
} catch {
|
|
@@ -18658,9 +19572,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
18658
19572
|
if (!file) return false;
|
|
18659
19573
|
if (!file.plugins) file.plugins = {};
|
|
18660
19574
|
const path2 = installedPluginsPath();
|
|
18661
|
-
(0,
|
|
19575
|
+
(0, import_node_fs22.copyFileSync)(path2, `${path2}.bak`);
|
|
18662
19576
|
file.plugins[pluginId] = records;
|
|
18663
|
-
(0,
|
|
19577
|
+
(0, import_node_fs22.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
18664
19578
|
`, "utf8");
|
|
18665
19579
|
return true;
|
|
18666
19580
|
} catch {
|
|
@@ -18668,35 +19582,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
18668
19582
|
}
|
|
18669
19583
|
}
|
|
18670
19584
|
function cursorPluginCacheRoot() {
|
|
18671
|
-
return (0,
|
|
19585
|
+
return (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
|
|
18672
19586
|
}
|
|
18673
19587
|
function cursorPluginCachePinSnapshots() {
|
|
18674
19588
|
const root = cursorPluginCacheRoot();
|
|
18675
19589
|
try {
|
|
18676
|
-
return (0,
|
|
18677
|
-
const path2 = (0,
|
|
18678
|
-
const pluginJson = (0,
|
|
18679
|
-
const hooksJson = (0,
|
|
18680
|
-
const cliBundle = (0,
|
|
19590
|
+
return (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
19591
|
+
const path2 = (0, import_node_path19.join)(root, entry.name);
|
|
19592
|
+
const pluginJson = (0, import_node_path19.join)(path2, ".cursor-plugin", "plugin.json");
|
|
19593
|
+
const hooksJson = (0, import_node_path19.join)(path2, "hooks", "hooks.json");
|
|
19594
|
+
const cliBundle = (0, import_node_path19.join)(path2, "cli", "dist", "index.cjs");
|
|
18681
19595
|
let version;
|
|
18682
19596
|
try {
|
|
18683
|
-
const raw = JSON.parse((0,
|
|
19597
|
+
const raw = JSON.parse((0, import_node_fs22.readFileSync)(pluginJson, "utf8"));
|
|
18684
19598
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
18685
19599
|
} catch {
|
|
18686
19600
|
version = void 0;
|
|
18687
19601
|
}
|
|
18688
19602
|
let isEmpty = true;
|
|
18689
19603
|
try {
|
|
18690
|
-
isEmpty = (0,
|
|
19604
|
+
isEmpty = (0, import_node_fs22.readdirSync)(path2).length === 0;
|
|
18691
19605
|
} catch {
|
|
18692
19606
|
isEmpty = true;
|
|
18693
19607
|
}
|
|
18694
19608
|
return {
|
|
18695
19609
|
name: entry.name,
|
|
18696
19610
|
path: path2,
|
|
18697
|
-
hasPluginJson: (0,
|
|
18698
|
-
hasHooksJson: (0,
|
|
18699
|
-
hasCliBundle: (0,
|
|
19611
|
+
hasPluginJson: (0, import_node_fs22.existsSync)(pluginJson),
|
|
19612
|
+
hasHooksJson: (0, import_node_fs22.existsSync)(hooksJson),
|
|
19613
|
+
hasCliBundle: (0, import_node_fs22.existsSync)(cliBundle),
|
|
18700
19614
|
isEmpty,
|
|
18701
19615
|
version
|
|
18702
19616
|
};
|
|
@@ -18706,19 +19620,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
18706
19620
|
}
|
|
18707
19621
|
}
|
|
18708
19622
|
function hubCheckoutForCursorSeed() {
|
|
18709
|
-
const manifest = (0,
|
|
18710
|
-
return (0,
|
|
19623
|
+
const manifest = (0, import_node_path19.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
19624
|
+
return (0, import_node_fs22.existsSync)(manifest) ? process.cwd() : void 0;
|
|
18711
19625
|
}
|
|
18712
19626
|
function mmiPluginCacheRootSnapshots() {
|
|
18713
19627
|
const roots = [
|
|
18714
|
-
{ surface: "claude", root: (0,
|
|
18715
|
-
{ surface: "codex", root: (0,
|
|
19628
|
+
{ surface: "claude", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
|
|
19629
|
+
{ surface: "codex", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
|
|
18716
19630
|
];
|
|
18717
19631
|
return roots.flatMap(({ surface, root }) => {
|
|
18718
19632
|
try {
|
|
18719
|
-
const entries = (0,
|
|
19633
|
+
const entries = (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
18720
19634
|
name: entry.name,
|
|
18721
|
-
path: (0,
|
|
19635
|
+
path: (0, import_node_path19.join)(root, entry.name),
|
|
18722
19636
|
isDirectory: entry.isDirectory()
|
|
18723
19637
|
}));
|
|
18724
19638
|
return [{ surface, root, entries }];
|
|
@@ -18729,7 +19643,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
18729
19643
|
}
|
|
18730
19644
|
function hasNestedMmiChild(versionDir) {
|
|
18731
19645
|
try {
|
|
18732
|
-
return (0,
|
|
19646
|
+
return (0, import_node_fs22.statSync)((0, import_node_path19.join)(versionDir, "mmi")).isDirectory();
|
|
18733
19647
|
} catch {
|
|
18734
19648
|
return false;
|
|
18735
19649
|
}
|
|
@@ -18740,10 +19654,10 @@ function nestedPluginTreeSnapshot() {
|
|
|
18740
19654
|
);
|
|
18741
19655
|
}
|
|
18742
19656
|
function uniqueQuarantineTarget(path2) {
|
|
18743
|
-
if (!(0,
|
|
19657
|
+
if (!(0, import_node_fs22.existsSync)(path2)) return path2;
|
|
18744
19658
|
for (let i = 1; i < 100; i += 1) {
|
|
18745
19659
|
const candidate = `${path2}-${i}`;
|
|
18746
|
-
if (!(0,
|
|
19660
|
+
if (!(0, import_node_fs22.existsSync)(candidate)) return candidate;
|
|
18747
19661
|
}
|
|
18748
19662
|
return `${path2}-${Date.now()}`;
|
|
18749
19663
|
}
|
|
@@ -18752,10 +19666,10 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
18752
19666
|
const failed = [];
|
|
18753
19667
|
for (const move of plan2) {
|
|
18754
19668
|
try {
|
|
18755
|
-
if (!(0,
|
|
19669
|
+
if (!(0, import_node_fs22.existsSync)(move.from)) continue;
|
|
18756
19670
|
const target = uniqueQuarantineTarget(move.to);
|
|
18757
|
-
(0,
|
|
18758
|
-
(0,
|
|
19671
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(target), { recursive: true });
|
|
19672
|
+
(0, import_node_fs22.renameSync)(move.from, target);
|
|
18759
19673
|
moved += 1;
|
|
18760
19674
|
} catch {
|
|
18761
19675
|
failed.push(move);
|
|
@@ -18774,23 +19688,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
18774
19688
|
}
|
|
18775
19689
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
18776
19690
|
try {
|
|
18777
|
-
if (!(0,
|
|
19691
|
+
if (!(0, import_node_fs22.existsSync)(targetPath)) return true;
|
|
18778
19692
|
if (isWin) {
|
|
18779
|
-
const emptyDir = (0,
|
|
18780
|
-
(0,
|
|
19693
|
+
const emptyDir = (0, import_node_path19.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
19694
|
+
(0, import_node_fs22.mkdirSync)(emptyDir, { recursive: true });
|
|
18781
19695
|
try {
|
|
18782
19696
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
18783
|
-
(0,
|
|
19697
|
+
(0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
|
|
18784
19698
|
} finally {
|
|
18785
19699
|
try {
|
|
18786
|
-
(0,
|
|
19700
|
+
(0, import_node_fs22.rmSync)(emptyDir, { recursive: true, force: true });
|
|
18787
19701
|
} catch {
|
|
18788
19702
|
}
|
|
18789
19703
|
}
|
|
18790
|
-
return !(0,
|
|
19704
|
+
return !(0, import_node_fs22.existsSync)(targetPath);
|
|
18791
19705
|
}
|
|
18792
|
-
(0,
|
|
18793
|
-
return !(0,
|
|
19706
|
+
(0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
|
|
19707
|
+
return !(0, import_node_fs22.existsSync)(targetPath);
|
|
18794
19708
|
} catch {
|
|
18795
19709
|
return false;
|
|
18796
19710
|
}
|
|
@@ -18803,11 +19717,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
18803
19717
|
}
|
|
18804
19718
|
return true;
|
|
18805
19719
|
}
|
|
18806
|
-
var gitignorePath = () => (0,
|
|
19720
|
+
var gitignorePath = () => (0, import_node_path19.join)(process.cwd(), ".gitignore");
|
|
18807
19721
|
function readTextFile(path2) {
|
|
18808
19722
|
try {
|
|
18809
|
-
if (!(0,
|
|
18810
|
-
return (0,
|
|
19723
|
+
if (!(0, import_node_fs22.existsSync)(path2)) return null;
|
|
19724
|
+
return (0, import_node_fs22.readFileSync)(path2, "utf8");
|
|
18811
19725
|
} catch {
|
|
18812
19726
|
return null;
|
|
18813
19727
|
}
|
|
@@ -18816,9 +19730,9 @@ function playwrightMcpConfigSnapshots() {
|
|
|
18816
19730
|
const cwd = process.cwd();
|
|
18817
19731
|
const home = (0, import_node_os6.homedir)();
|
|
18818
19732
|
const candidates = [
|
|
18819
|
-
(0,
|
|
18820
|
-
(0,
|
|
18821
|
-
(0,
|
|
19733
|
+
(0, import_node_path19.join)(cwd, ".cursor", "mcp.json"),
|
|
19734
|
+
(0, import_node_path19.join)(home, ".cursor", "mcp.json"),
|
|
19735
|
+
(0, import_node_path19.join)(home, ".codex", "config.toml")
|
|
18822
19736
|
];
|
|
18823
19737
|
const out = [];
|
|
18824
19738
|
for (const path2 of candidates) {
|
|
@@ -18831,7 +19745,7 @@ function strayBrowserArtifactPaths() {
|
|
|
18831
19745
|
const cwd = process.cwd();
|
|
18832
19746
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
18833
19747
|
try {
|
|
18834
|
-
return (0,
|
|
19748
|
+
return (0, import_node_fs22.existsSync)((0, import_node_path19.join)(cwd, rel));
|
|
18835
19749
|
} catch {
|
|
18836
19750
|
return false;
|
|
18837
19751
|
}
|
|
@@ -18839,14 +19753,14 @@ function strayBrowserArtifactPaths() {
|
|
|
18839
19753
|
}
|
|
18840
19754
|
function readGitignore() {
|
|
18841
19755
|
try {
|
|
18842
|
-
return (0,
|
|
19756
|
+
return (0, import_node_fs22.readFileSync)(gitignorePath(), "utf8");
|
|
18843
19757
|
} catch {
|
|
18844
19758
|
return null;
|
|
18845
19759
|
}
|
|
18846
19760
|
}
|
|
18847
19761
|
function writeGitignore(content) {
|
|
18848
19762
|
try {
|
|
18849
|
-
(0,
|
|
19763
|
+
(0, import_node_fs22.writeFileSync)(gitignorePath(), content, "utf8");
|
|
18850
19764
|
return true;
|
|
18851
19765
|
} catch {
|
|
18852
19766
|
return false;
|
|
@@ -18885,7 +19799,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
18885
19799
|
let onPath = pathProbe;
|
|
18886
19800
|
if (!onPath) {
|
|
18887
19801
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
18888
|
-
if (root && (0,
|
|
19802
|
+
if (root && (0, import_node_fs22.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
18889
19803
|
}
|
|
18890
19804
|
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
19805
|
const surface = detectSurface(process.env);
|
|
@@ -18929,10 +19843,40 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
18929
19843
|
if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
|
|
18930
19844
|
if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
|
|
18931
19845
|
pluginCheck = { ...pluginCheck, ok: true };
|
|
18932
|
-
io.err(` \u21BB repaired: registered mmi@
|
|
19846
|
+
io.err(` \u21BB repaired: registered mmi@mutmutco project install record \u2014 ${reloadHint} to load MMI commands`);
|
|
18933
19847
|
}
|
|
18934
19848
|
}
|
|
18935
19849
|
checks.push(pluginCheck);
|
|
19850
|
+
let legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19851
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19852
|
+
sources: installedPluginSources(),
|
|
19853
|
+
surface
|
|
19854
|
+
});
|
|
19855
|
+
if (!legacyPluginCheck.ok && repairLocal) {
|
|
19856
|
+
const claudeLegacy = legacyPluginCheck.staleSurfaces?.includes("claude") ?? false;
|
|
19857
|
+
const codexLegacy = legacyPluginCheck.staleSurfaces?.includes("codex") ?? false;
|
|
19858
|
+
if (claudeLegacy && await applyClaudePluginHeal(surface, (m) => io.err(m), { force: true })) {
|
|
19859
|
+
legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19860
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19861
|
+
sources: installedPluginSources(),
|
|
19862
|
+
surface
|
|
19863
|
+
});
|
|
19864
|
+
if (legacyPluginCheck.ok) {
|
|
19865
|
+
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via claude plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
19866
|
+
}
|
|
19867
|
+
}
|
|
19868
|
+
if (!legacyPluginCheck.ok && codexLegacy && await applyCodexPluginHeal(surface, (m) => io.err(m), { force: true })) {
|
|
19869
|
+
legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19870
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19871
|
+
sources: installedPluginSources(),
|
|
19872
|
+
surface
|
|
19873
|
+
});
|
|
19874
|
+
if (legacyPluginCheck.ok) {
|
|
19875
|
+
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via codex plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
19876
|
+
}
|
|
19877
|
+
}
|
|
19878
|
+
}
|
|
19879
|
+
checks.push(legacyPluginCheck);
|
|
18936
19880
|
let gitignoreCheck = buildGitignoreManagedBlockCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), content: readGitignore() });
|
|
18937
19881
|
const gitignoreDecision = decideGitignoreRepair(gitignoreCheck, { repoWritesAllowed, repairFull });
|
|
18938
19882
|
gitignoreCheck = gitignoreDecision.check;
|
|
@@ -18955,7 +19899,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
18955
19899
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
18956
19900
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
18957
19901
|
driftCheck = { ...driftCheck, ok: true };
|
|
18958
|
-
io.err(` \u21BB repaired: collapsed mmi@
|
|
19902
|
+
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
19903
|
}
|
|
18960
19904
|
}
|
|
18961
19905
|
checks.push(driftCheck);
|
|
@@ -19057,7 +20001,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19057
20001
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19058
20002
|
surface,
|
|
19059
20003
|
cacheRoot: cursorCacheRoot,
|
|
19060
|
-
cacheRootExists: (0,
|
|
20004
|
+
cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
|
|
19061
20005
|
pins: cursorPins,
|
|
19062
20006
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19063
20007
|
releasedVersion
|
|
@@ -19068,7 +20012,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19068
20012
|
releasedVersion,
|
|
19069
20013
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19070
20014
|
execFileP: execFileP2,
|
|
19071
|
-
mkdtemp: (prefix) => (0,
|
|
20015
|
+
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path19.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
19072
20016
|
log: (m) => io.err(m)
|
|
19073
20017
|
});
|
|
19074
20018
|
if (seeded) {
|
|
@@ -19077,7 +20021,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19077
20021
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19078
20022
|
surface,
|
|
19079
20023
|
cacheRoot: cursorCacheRoot,
|
|
19080
|
-
cacheRootExists: (0,
|
|
20024
|
+
cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
|
|
19081
20025
|
pins: cursorPins,
|
|
19082
20026
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19083
20027
|
releasedVersion
|
|
@@ -19116,6 +20060,38 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19116
20060
|
strayPaths: strayBrowserArtifactPaths()
|
|
19117
20061
|
})
|
|
19118
20062
|
);
|
|
20063
|
+
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
20064
|
+
const isDashboardConsumer = dashboardConsumer.isConsumer;
|
|
20065
|
+
const uiSnapshot = designSystemSnapshot(process.cwd());
|
|
20066
|
+
const uiLatestVersion = isDashboardConsumer && uiSnapshot.packageName ? await fetchUiPackageLatestVersion(uiSnapshot.packageName) : void 0;
|
|
20067
|
+
let designSystemCheck = dashboardConsumer.registryReadFailed ? buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildDesignSystemVersionCheck({
|
|
20068
|
+
...uiSnapshot,
|
|
20069
|
+
isConsumerRepo: isDashboardConsumer,
|
|
20070
|
+
latestVersion: uiLatestVersion
|
|
20071
|
+
});
|
|
20072
|
+
if (!designSystemCheck.ok && (repairFull || repairLocal) && designSystemCheck.packageName) {
|
|
20073
|
+
designSystemCheck = await applyDesignSystemUpdate(designSystemCheck, (m) => io.err(m));
|
|
20074
|
+
if (designSystemCheck.ok) {
|
|
20075
|
+
io.err(` \u21BB updated ${designSystemCheck.packageName} \u2192 ${designSystemCheck.installedVersion ?? designSystemCheck.latestVersion ?? "latest"}`);
|
|
20076
|
+
}
|
|
20077
|
+
}
|
|
20078
|
+
checks.push(designSystemCheck);
|
|
20079
|
+
const registryTargetVersion = designSystemCheck.latestVersion ?? designSystemCheck.installedVersion ?? uiLatestVersion;
|
|
20080
|
+
let registryComponentsCheck = dashboardConsumer.registryReadFailed ? buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildRegistryComponentsCheck({
|
|
20081
|
+
...await gatherRegistryComponentsState(process.cwd(), registryTargetVersion, { fetch }),
|
|
20082
|
+
isConsumerRepo: isDashboardConsumer
|
|
20083
|
+
});
|
|
20084
|
+
if (!registryComponentsCheck.ok && (repairFull || repairLocal) && repoWritesAllowed && registryComponentsCheck.components?.length) {
|
|
20085
|
+
registryComponentsCheck = await applyRegistryComponentsSyncCheck(
|
|
20086
|
+
registryComponentsCheck,
|
|
20087
|
+
registryTargetVersion,
|
|
20088
|
+
(m) => io.err(m)
|
|
20089
|
+
);
|
|
20090
|
+
if (registryComponentsCheck.ok) {
|
|
20091
|
+
io.err(` \u21BB synced ${registryComponentsCheck.components?.length ?? 0} registry component(s) \u2192 .mmi/design-system/components`);
|
|
20092
|
+
}
|
|
20093
|
+
}
|
|
20094
|
+
checks.push(registryComponentsCheck);
|
|
19119
20095
|
const gaps = checks.filter((c) => !c.ok);
|
|
19120
20096
|
if (opts.banner) {
|
|
19121
20097
|
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 +20120,82 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19144
20120
|
io.log(gaps.length ? `
|
|
19145
20121
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
19146
20122
|
}
|
|
19147
|
-
program2.command("
|
|
20123
|
+
var designSystem = program2.command("design-system").description("@mutmutco UI npm package + registry component freshness for dashboard consumers (#1633, #1635)");
|
|
20124
|
+
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) => {
|
|
20125
|
+
const cfg = await loadConfig();
|
|
20126
|
+
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
20127
|
+
if (dashboardConsumer.registryReadFailed) {
|
|
20128
|
+
const check2 = buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed);
|
|
20129
|
+
if (opts.json) {
|
|
20130
|
+
console.log(JSON.stringify(check2, null, 2));
|
|
20131
|
+
} else {
|
|
20132
|
+
console.log(`\u2717 ${check2.label}`);
|
|
20133
|
+
console.log(` fix: ${check2.fix}`);
|
|
20134
|
+
}
|
|
20135
|
+
process.exitCode = 1;
|
|
20136
|
+
return;
|
|
20137
|
+
}
|
|
20138
|
+
const isDashboardConsumer = dashboardConsumer.isConsumer;
|
|
20139
|
+
const snapshot = designSystemSnapshot(process.cwd());
|
|
20140
|
+
let check = buildDesignSystemVersionCheck({
|
|
20141
|
+
...snapshot,
|
|
20142
|
+
isConsumerRepo: isDashboardConsumer,
|
|
20143
|
+
latestVersion: isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0
|
|
20144
|
+
});
|
|
20145
|
+
if (!check.ok && opts.apply && check.packageName) {
|
|
20146
|
+
check = await applyDesignSystemUpdate(check, (m) => console.error(m));
|
|
20147
|
+
}
|
|
20148
|
+
if (opts.json) {
|
|
20149
|
+
console.log(JSON.stringify(check, null, 2));
|
|
20150
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20151
|
+
return;
|
|
20152
|
+
}
|
|
20153
|
+
console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
|
|
20154
|
+
if (check.packageName) console.log(` package: ${check.packageName}`);
|
|
20155
|
+
if (check.installedVersion) console.log(` installed: ${check.installedVersion}`);
|
|
20156
|
+
if (check.latestVersion) console.log(` latest: ${check.latestVersion}`);
|
|
20157
|
+
if (!check.ok) console.log(` fix: ${check.fix}`);
|
|
20158
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20159
|
+
});
|
|
20160
|
+
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) => {
|
|
20161
|
+
const cfg = await loadConfig();
|
|
20162
|
+
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
20163
|
+
if (dashboardConsumer.registryReadFailed) {
|
|
20164
|
+
const check2 = buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed);
|
|
20165
|
+
if (opts.json) {
|
|
20166
|
+
console.log(JSON.stringify(check2, null, 2));
|
|
20167
|
+
} else {
|
|
20168
|
+
console.log(`\u2717 ${check2.label}`);
|
|
20169
|
+
console.log(` fix: ${check2.fix}`);
|
|
20170
|
+
}
|
|
20171
|
+
process.exitCode = 1;
|
|
20172
|
+
return;
|
|
20173
|
+
}
|
|
20174
|
+
const isDashboardConsumer = dashboardConsumer.isConsumer;
|
|
20175
|
+
const snapshot = designSystemSnapshot(process.cwd());
|
|
20176
|
+
const targetVersion = isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0;
|
|
20177
|
+
const state = await gatherRegistryComponentsState(process.cwd(), targetVersion, { fetch });
|
|
20178
|
+
let check = buildRegistryComponentsCheck({
|
|
20179
|
+
...state,
|
|
20180
|
+
isConsumerRepo: isDashboardConsumer
|
|
20181
|
+
});
|
|
20182
|
+
if (!check.ok && opts.apply && check.components?.length) {
|
|
20183
|
+
check = await applyRegistryComponentsSyncCheck(check, targetVersion, (m) => console.error(m));
|
|
20184
|
+
}
|
|
20185
|
+
if (opts.json) {
|
|
20186
|
+
console.log(JSON.stringify(check, null, 2));
|
|
20187
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20188
|
+
return;
|
|
20189
|
+
}
|
|
20190
|
+
console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
|
|
20191
|
+
if (check.components?.length) console.log(` components: ${check.components.join(", ")}`);
|
|
20192
|
+
if (check.cacheVersion) console.log(` cache version: ${check.cacheVersion}`);
|
|
20193
|
+
if (check.targetVersion) console.log(` target version: ${check.targetVersion}`);
|
|
20194
|
+
if (check.staleComponents?.length) console.log(` stale: ${check.staleComponents.join(", ")}`);
|
|
20195
|
+
if (!check.ok) console.log(` fix: ${check.fix}`);
|
|
20196
|
+
process.exitCode = check.ok ? 0 : 1;
|
|
20197
|
+
});
|
|
20198
|
+
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
20199
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
19149
20200
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
19150
20201
|
));
|
|
@@ -19159,7 +20210,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
19159
20210
|
} catch (e) {
|
|
19160
20211
|
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
19161
20212
|
}
|
|
19162
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn:
|
|
20213
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19163
20214
|
let northstarInjected = false;
|
|
19164
20215
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
19165
20216
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
@@ -19201,7 +20252,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
19201
20252
|
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
19202
20253
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
19203
20254
|
if (worktreeBanner) {
|
|
19204
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
20255
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19205
20256
|
consoleIo.log(worktreeBanner);
|
|
19206
20257
|
}
|
|
19207
20258
|
});
|