@mutmutco/cli 2.34.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +17 -8
- package/dist/main.cjs +1008 -173
- package/dist/saga.cjs +26 -15
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3392,7 +3392,7 @@ var program = new Command();
|
|
|
3392
3392
|
|
|
3393
3393
|
// src/index.ts
|
|
3394
3394
|
var import_promises5 = require("node:fs/promises");
|
|
3395
|
-
var
|
|
3395
|
+
var import_node_fs16 = require("node:fs");
|
|
3396
3396
|
|
|
3397
3397
|
// src/rules-sync.ts
|
|
3398
3398
|
function normalizeEol(s) {
|
|
@@ -3427,7 +3427,7 @@ var import_node_child_process10 = require("node:child_process");
|
|
|
3427
3427
|
|
|
3428
3428
|
// src/cli-shared.ts
|
|
3429
3429
|
var import_promises = require("node:fs/promises");
|
|
3430
|
-
var
|
|
3430
|
+
var import_node_fs7 = require("node:fs");
|
|
3431
3431
|
var import_node_crypto2 = require("node:crypto");
|
|
3432
3432
|
var import_node_child_process4 = require("node:child_process");
|
|
3433
3433
|
var import_node_util4 = require("node:util");
|
|
@@ -4109,10 +4109,19 @@ async function hubAuthToken(deps) {
|
|
|
4109
4109
|
}
|
|
4110
4110
|
|
|
4111
4111
|
// src/stdin-inject.ts
|
|
4112
|
+
var import_node_fs6 = require("node:fs");
|
|
4112
4113
|
var injectedStdin;
|
|
4114
|
+
function stdinHasPipedInput(statFd = () => (0, import_node_fs6.fstatSync)(0)) {
|
|
4115
|
+
try {
|
|
4116
|
+
const stat = statFd();
|
|
4117
|
+
return stat.isFIFO() || stat.isFile();
|
|
4118
|
+
} catch {
|
|
4119
|
+
return false;
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4113
4122
|
async function readStdin() {
|
|
4114
4123
|
if (injectedStdin !== void 0) return injectedStdin;
|
|
4115
|
-
if (
|
|
4124
|
+
if (!stdinHasPipedInput()) return "";
|
|
4116
4125
|
const chunks = [];
|
|
4117
4126
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
4118
4127
|
return Buffer.concat(chunks).toString("utf8");
|
|
@@ -4161,7 +4170,7 @@ function sessionDeps() {
|
|
|
4161
4170
|
env: process.env,
|
|
4162
4171
|
readPersisted: () => {
|
|
4163
4172
|
try {
|
|
4164
|
-
return (0,
|
|
4173
|
+
return (0, import_node_fs7.readFileSync)(SESSION_FILE, "utf8");
|
|
4165
4174
|
} catch {
|
|
4166
4175
|
return null;
|
|
4167
4176
|
}
|
|
@@ -4174,8 +4183,8 @@ function sessionDeps() {
|
|
|
4174
4183
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
4175
4184
|
function persistSession(id) {
|
|
4176
4185
|
try {
|
|
4177
|
-
(0,
|
|
4178
|
-
(0,
|
|
4186
|
+
(0, import_node_fs7.mkdirSync)(".mmi", { recursive: true });
|
|
4187
|
+
(0, import_node_fs7.writeFileSync)(SESSION_FILE, id, "utf8");
|
|
4179
4188
|
} catch {
|
|
4180
4189
|
}
|
|
4181
4190
|
}
|
|
@@ -4267,7 +4276,9 @@ async function resolveTextArg(input, deps, labels) {
|
|
|
4267
4276
|
const source = input.file ?? "";
|
|
4268
4277
|
const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
|
|
4269
4278
|
if (text.trim().length === 0) {
|
|
4270
|
-
throw new Error(
|
|
4279
|
+
throw new Error(
|
|
4280
|
+
source === "-" ? `${labels.file} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${labels.file} <path>)` : `${labels.file} produced an empty ${labels.noun}`
|
|
4281
|
+
);
|
|
4271
4282
|
}
|
|
4272
4283
|
return text;
|
|
4273
4284
|
}
|
|
@@ -4287,7 +4298,7 @@ function resolveIssueTitle(input, deps) {
|
|
|
4287
4298
|
}
|
|
4288
4299
|
|
|
4289
4300
|
// src/saga-capture.ts
|
|
4290
|
-
var
|
|
4301
|
+
var import_node_fs8 = require("node:fs");
|
|
4291
4302
|
var import_node_os2 = require("node:os");
|
|
4292
4303
|
var import_node_path6 = require("node:path");
|
|
4293
4304
|
function parseHookInput(stdin) {
|
|
@@ -4314,16 +4325,16 @@ function resolveCursorTranscriptPath(hook, env = process.env) {
|
|
|
4314
4325
|
const slug = cursorProjectSlug(workspaceRoot);
|
|
4315
4326
|
const transcriptsDir = (0, import_node_path6.join)(cursorProjectsRoot(env), slug, "agent-transcripts");
|
|
4316
4327
|
const nested = (0, import_node_path6.join)(transcriptsDir, conversationId, `${conversationId}.jsonl`);
|
|
4317
|
-
if ((0,
|
|
4328
|
+
if ((0, import_node_fs8.existsSync)(nested)) return nested;
|
|
4318
4329
|
const flat = (0, import_node_path6.join)(transcriptsDir, `${conversationId}.jsonl`);
|
|
4319
|
-
if ((0,
|
|
4330
|
+
if ((0, import_node_fs8.existsSync)(flat)) return flat;
|
|
4320
4331
|
return void 0;
|
|
4321
4332
|
}
|
|
4322
4333
|
function resolveTranscriptPath(hook, env = process.env) {
|
|
4323
4334
|
const fromHook = (hook.transcript_path ?? hook.transcriptPath)?.trim();
|
|
4324
4335
|
if (fromHook) return fromHook;
|
|
4325
4336
|
const fromEnv = env.CURSOR_TRANSCRIPT_PATH?.trim();
|
|
4326
|
-
if (fromEnv && (0,
|
|
4337
|
+
if (fromEnv && (0, import_node_fs8.existsSync)(fromEnv)) return fromEnv;
|
|
4327
4338
|
const surface = env.MMI_AGENT_SURFACE?.trim();
|
|
4328
4339
|
if (surface === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) {
|
|
4329
4340
|
return resolveCursorTranscriptPath(hook, env);
|
|
@@ -5072,24 +5083,24 @@ function registerSagaCommands(program3) {
|
|
|
5072
5083
|
// src/honcho-commands.ts
|
|
5073
5084
|
var import_node_child_process5 = require("node:child_process");
|
|
5074
5085
|
var import_promises3 = require("node:fs/promises");
|
|
5075
|
-
var
|
|
5086
|
+
var import_node_fs10 = require("node:fs");
|
|
5076
5087
|
var import_node_path8 = require("node:path");
|
|
5077
5088
|
|
|
5078
5089
|
// src/honcho-ingest-skip.ts
|
|
5079
|
-
var
|
|
5090
|
+
var import_node_fs9 = require("node:fs");
|
|
5080
5091
|
var import_node_path7 = require("node:path");
|
|
5081
5092
|
var INGEST_SKIP_FILE = ".mmi/honcho/last-skip.json";
|
|
5082
5093
|
function recordIngestSkip(record) {
|
|
5083
5094
|
try {
|
|
5084
|
-
(0,
|
|
5085
|
-
(0,
|
|
5095
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path7.dirname)(INGEST_SKIP_FILE), { recursive: true });
|
|
5096
|
+
(0, import_node_fs9.writeFileSync)(INGEST_SKIP_FILE, JSON.stringify(record), "utf8");
|
|
5086
5097
|
} catch {
|
|
5087
5098
|
}
|
|
5088
5099
|
}
|
|
5089
5100
|
function readIngestSkip() {
|
|
5090
5101
|
try {
|
|
5091
|
-
if (!(0,
|
|
5092
|
-
const o = JSON.parse((0,
|
|
5102
|
+
if (!(0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) return null;
|
|
5103
|
+
const o = JSON.parse((0, import_node_fs9.readFileSync)(INGEST_SKIP_FILE, "utf8"));
|
|
5093
5104
|
return o?.reason && o?.surface && o?.ts ? o : null;
|
|
5094
5105
|
} catch {
|
|
5095
5106
|
return null;
|
|
@@ -5097,7 +5108,7 @@ function readIngestSkip() {
|
|
|
5097
5108
|
}
|
|
5098
5109
|
function clearIngestSkip() {
|
|
5099
5110
|
try {
|
|
5100
|
-
if ((0,
|
|
5111
|
+
if ((0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) (0, import_node_fs9.unlinkSync)(INGEST_SKIP_FILE);
|
|
5101
5112
|
} catch {
|
|
5102
5113
|
}
|
|
5103
5114
|
}
|
|
@@ -5159,7 +5170,8 @@ function formatVaultPointer(p) {
|
|
|
5159
5170
|
``,
|
|
5160
5171
|
`enumerate actual keys: mmi-cli secrets list`,
|
|
5161
5172
|
`read one: mmi-cli secrets get <stage>/<KEY> (e.g. main/GOOGLE_CLIENT_ID)`,
|
|
5162
|
-
`set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)
|
|
5173
|
+
`set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)`,
|
|
5174
|
+
`copy provider keys: mmi-cli secrets copy --from rc --to dev --keys RECALL_API_KEY,GEMINI_API_KEY`
|
|
5163
5175
|
];
|
|
5164
5176
|
return lines.join("\n");
|
|
5165
5177
|
}
|
|
@@ -5503,6 +5515,54 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
|
|
|
5503
5515
|
}
|
|
5504
5516
|
deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
|
|
5505
5517
|
}
|
|
5518
|
+
var SECRET_COPY_BLOCKED_RE = /(?:ENC_KEY|ENCRYPTION_KEY|SECRET_KEY_BASE)/i;
|
|
5519
|
+
function isSecretCopyBlocked(key) {
|
|
5520
|
+
const slash = key.indexOf("/");
|
|
5521
|
+
const leaf = slash === -1 ? key : key.slice(slash + 1);
|
|
5522
|
+
return SECRET_COPY_BLOCKED_RE.test(leaf);
|
|
5523
|
+
}
|
|
5524
|
+
function copyTierKey(stage2, leaf) {
|
|
5525
|
+
return `${stage2}/${leaf}`;
|
|
5526
|
+
}
|
|
5527
|
+
async function secretsCopy(deps, opts) {
|
|
5528
|
+
if (opts.from === opts.to) {
|
|
5529
|
+
deps.err("secrets copy: --from and --to must differ");
|
|
5530
|
+
return false;
|
|
5531
|
+
}
|
|
5532
|
+
const keys = [...new Set(opts.keys.map((k) => k.trim()).filter(Boolean))];
|
|
5533
|
+
if (!keys.length) {
|
|
5534
|
+
deps.err("secrets copy: --keys required (comma-separated allowlist)");
|
|
5535
|
+
return false;
|
|
5536
|
+
}
|
|
5537
|
+
for (const key of keys) {
|
|
5538
|
+
if (!isValidSecretKey(key)) {
|
|
5539
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
5540
|
+
return false;
|
|
5541
|
+
}
|
|
5542
|
+
if (isSecretCopyBlocked(key)) {
|
|
5543
|
+
deps.err(`secrets copy: ${key} is not copyable \u2014 generate encryption/stage-distinct keys per stage`);
|
|
5544
|
+
return false;
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
for (const key of keys) {
|
|
5548
|
+
const leaf = secretLeafName(key);
|
|
5549
|
+
const srcKey = copyTierKey(opts.from, leaf);
|
|
5550
|
+
const dstKey = copyTierKey(opts.to, leaf);
|
|
5551
|
+
const value = await fetchSecretValue(deps, srcKey, opts);
|
|
5552
|
+
if (!value) {
|
|
5553
|
+
deps.err(`secrets copy: could not read source ${srcKey}`);
|
|
5554
|
+
return false;
|
|
5555
|
+
}
|
|
5556
|
+
if (opts.dryRun) {
|
|
5557
|
+
deps.log(`would copy ${srcKey} \u2192 ${dstKey}`);
|
|
5558
|
+
continue;
|
|
5559
|
+
}
|
|
5560
|
+
const ok = await putSecret(deps, dstKey, value, opts);
|
|
5561
|
+
if (!ok) return false;
|
|
5562
|
+
deps.log(`copied ${srcKey} \u2192 ${dstKey}`);
|
|
5563
|
+
}
|
|
5564
|
+
return true;
|
|
5565
|
+
}
|
|
5506
5566
|
async function secretsUse(deps, key, opts) {
|
|
5507
5567
|
const slug = await vaultSlug(deps, opts);
|
|
5508
5568
|
const tier = classifyTier(slug, key);
|
|
@@ -5873,7 +5933,7 @@ function honchoThrottlePath(key) {
|
|
|
5873
5933
|
function honchoIngestDue(path2, intervalMs, now = Date.now()) {
|
|
5874
5934
|
if (intervalMs <= 0) return true;
|
|
5875
5935
|
try {
|
|
5876
|
-
const last = Number((0,
|
|
5936
|
+
const last = Number((0, import_node_fs10.readFileSync)(path2, "utf8").trim()) || 0;
|
|
5877
5937
|
return now - last >= intervalMs;
|
|
5878
5938
|
} catch {
|
|
5879
5939
|
return true;
|
|
@@ -5881,8 +5941,8 @@ function honchoIngestDue(path2, intervalMs, now = Date.now()) {
|
|
|
5881
5941
|
}
|
|
5882
5942
|
function markHonchoIngest(path2, now = Date.now()) {
|
|
5883
5943
|
try {
|
|
5884
|
-
(0,
|
|
5885
|
-
(0,
|
|
5944
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
|
|
5945
|
+
(0, import_node_fs10.writeFileSync)(path2, String(now), "utf8");
|
|
5886
5946
|
} catch {
|
|
5887
5947
|
}
|
|
5888
5948
|
}
|
|
@@ -6165,7 +6225,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
|
6165
6225
|
}
|
|
6166
6226
|
|
|
6167
6227
|
// src/session-start.ts
|
|
6168
|
-
var
|
|
6228
|
+
var import_node_fs11 = require("node:fs");
|
|
6169
6229
|
var import_node_path9 = require("node:path");
|
|
6170
6230
|
async function runBufferedStep(step) {
|
|
6171
6231
|
const lines = [];
|
|
@@ -6218,11 +6278,11 @@ function spawnDetachedSelf(args, deps) {
|
|
|
6218
6278
|
function planStoreLines(cwd) {
|
|
6219
6279
|
const mdFiles = (dir, minSize = 0) => {
|
|
6220
6280
|
const p = (0, import_node_path9.join)(cwd, dir);
|
|
6221
|
-
if (!(0,
|
|
6281
|
+
if (!(0, import_node_fs11.existsSync)(p)) return [];
|
|
6222
6282
|
try {
|
|
6223
|
-
return (0,
|
|
6283
|
+
return (0, import_node_fs11.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
|
|
6224
6284
|
try {
|
|
6225
|
-
return (0,
|
|
6285
|
+
return (0, import_node_fs11.statSync)((0, import_node_path9.join)(p, f)).size >= minSize;
|
|
6226
6286
|
} catch {
|
|
6227
6287
|
return false;
|
|
6228
6288
|
}
|
|
@@ -6824,6 +6884,73 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
6824
6884
|
}
|
|
6825
6885
|
return result;
|
|
6826
6886
|
}
|
|
6887
|
+
async function prunePriorityLabels(options, deps = {}) {
|
|
6888
|
+
const cfg = resolveBoardConfig(options.config);
|
|
6889
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
6890
|
+
const collected = await collectBoardItems(cfg, { repo: options.repo }, deps);
|
|
6891
|
+
const issues = collected.items.filter(
|
|
6892
|
+
(item) => item.contentType === "Issue" && item.labels.some((l) => labelToFieldPriority(l))
|
|
6893
|
+
);
|
|
6894
|
+
const concurrency = Math.max(1, options.concurrency ?? 8);
|
|
6895
|
+
const result = {
|
|
6896
|
+
scanned: issues.length,
|
|
6897
|
+
pruned: 0,
|
|
6898
|
+
removedLabels: 0,
|
|
6899
|
+
skippedNoField: 0,
|
|
6900
|
+
failed: 0,
|
|
6901
|
+
details: []
|
|
6902
|
+
};
|
|
6903
|
+
async function work(item) {
|
|
6904
|
+
const priorityLabels = item.labels.filter((l) => labelToFieldPriority(l));
|
|
6905
|
+
if (!priorityLabels.length) return;
|
|
6906
|
+
if (!item.priority) {
|
|
6907
|
+
result.skippedNoField += 1;
|
|
6908
|
+
result.details.push(
|
|
6909
|
+
`${item.ref}: Priority field unset \u2014 kept ${priorityLabels.join(", ")} (run \`mmi-cli board backfill-priority\` first)`
|
|
6910
|
+
);
|
|
6911
|
+
return;
|
|
6912
|
+
}
|
|
6913
|
+
if (options.dryRun) {
|
|
6914
|
+
result.pruned += 1;
|
|
6915
|
+
result.removedLabels += priorityLabels.length;
|
|
6916
|
+
result.details.push(`${item.ref} \u2192 remove ${priorityLabels.join(", ")} (field=${item.priority}) (dry-run)`);
|
|
6917
|
+
return;
|
|
6918
|
+
}
|
|
6919
|
+
try {
|
|
6920
|
+
const removed = [];
|
|
6921
|
+
const alreadyAbsent = [];
|
|
6922
|
+
for (const label of priorityLabels) {
|
|
6923
|
+
try {
|
|
6924
|
+
await client.rest(
|
|
6925
|
+
"DELETE",
|
|
6926
|
+
`repos/${item.repository}/issues/${item.number}/labels/${encodeURIComponent(label)}`
|
|
6927
|
+
);
|
|
6928
|
+
removed.push(label);
|
|
6929
|
+
result.removedLabels += 1;
|
|
6930
|
+
} catch (e) {
|
|
6931
|
+
if (e instanceof GitHubApiError && e.status === 404) {
|
|
6932
|
+
alreadyAbsent.push(label);
|
|
6933
|
+
continue;
|
|
6934
|
+
}
|
|
6935
|
+
throw e;
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
result.pruned += 1;
|
|
6939
|
+
const parts = [
|
|
6940
|
+
...removed.length ? [`removed ${removed.join(", ")}`] : [],
|
|
6941
|
+
...alreadyAbsent.length ? [`already absent ${alreadyAbsent.join(", ")}`] : []
|
|
6942
|
+
];
|
|
6943
|
+
result.details.push(`${item.ref} \u2192 ${parts.join("; ")} (field=${item.priority})`);
|
|
6944
|
+
} catch (e) {
|
|
6945
|
+
result.failed += 1;
|
|
6946
|
+
result.details.push(`${item.ref}: ${ghError(e)}`);
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
for (let i = 0; i < issues.length; i += concurrency) {
|
|
6950
|
+
await Promise.all(issues.slice(i, i + concurrency).map(work));
|
|
6951
|
+
}
|
|
6952
|
+
return result;
|
|
6953
|
+
}
|
|
6827
6954
|
async function recoverIssuePriority(client, item) {
|
|
6828
6955
|
for (const label of item.labels) {
|
|
6829
6956
|
const fromLabel = labelToFieldPriority(label);
|
|
@@ -7278,6 +7405,412 @@ async function runNorthstarContext(io, deps) {
|
|
|
7278
7405
|
|
|
7279
7406
|
// src/index.ts
|
|
7280
7407
|
var import_node_path14 = require("node:path");
|
|
7408
|
+
|
|
7409
|
+
// src/merge-ci-policy.ts
|
|
7410
|
+
function resolveMergeCiPolicy(input) {
|
|
7411
|
+
if (input.registryCi === "none") {
|
|
7412
|
+
return { policy: "no-ci", reason: "registry META ci:none" };
|
|
7413
|
+
}
|
|
7414
|
+
if (input.registryRequiredChecks === null) {
|
|
7415
|
+
return { policy: "no-ci", reason: "registry META requiredChecks:null" };
|
|
7416
|
+
}
|
|
7417
|
+
if (Array.isArray(input.registryRequiredChecks) && input.registryRequiredChecks.length === 0) {
|
|
7418
|
+
return { policy: "no-ci", reason: "registry META requiredChecks:[]" };
|
|
7419
|
+
}
|
|
7420
|
+
const ciWorkflows = input.workflowPaths.filter(
|
|
7421
|
+
(p) => /^\.github\/workflows\/[^/]+\.(ya?ml)$/i.test(p.replace(/\\/g, "/"))
|
|
7422
|
+
);
|
|
7423
|
+
if (!ciWorkflows.length) {
|
|
7424
|
+
return { policy: "no-ci", reason: "no .github/workflows CI" };
|
|
7425
|
+
}
|
|
7426
|
+
return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
|
|
7427
|
+
}
|
|
7428
|
+
function parseGhPrChecksOutput(stdout) {
|
|
7429
|
+
const text = stdout.trim();
|
|
7430
|
+
if (!text) return "no-checks-reported";
|
|
7431
|
+
if (/^no checks reported/i.test(text)) return "no-checks-reported";
|
|
7432
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
7433
|
+
if (!lines.length) return "no-checks-reported";
|
|
7434
|
+
let anyPending = false;
|
|
7435
|
+
for (const line of lines) {
|
|
7436
|
+
const parts = line.split(/\s+/);
|
|
7437
|
+
const state = (parts[1] ?? "").toLowerCase();
|
|
7438
|
+
if (state === "fail" || state === "failure" || state === "error") return "failure";
|
|
7439
|
+
if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
|
|
7440
|
+
anyPending = true;
|
|
7441
|
+
}
|
|
7442
|
+
return anyPending ? "pending" : "success";
|
|
7443
|
+
}
|
|
7444
|
+
var PR_CHECKS_POLL_MS = 3e4;
|
|
7445
|
+
var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
|
|
7446
|
+
async function waitForPrChecks(deps) {
|
|
7447
|
+
const { policy, reason } = await deps.resolvePolicy();
|
|
7448
|
+
if (policy === "no-ci") {
|
|
7449
|
+
return { policy, status: "skipped", reason };
|
|
7450
|
+
}
|
|
7451
|
+
const now = deps.now ?? (() => Date.now());
|
|
7452
|
+
const deadline = now() + PR_CHECKS_TIMEOUT_MS;
|
|
7453
|
+
let lastDetail = "pending";
|
|
7454
|
+
while (now() < deadline) {
|
|
7455
|
+
const state = await deps.pollChecks();
|
|
7456
|
+
if (state === "success") return { policy, status: "success", detail: lastDetail };
|
|
7457
|
+
if (state === "failure") return { policy, status: "failure", detail: lastDetail };
|
|
7458
|
+
if (state === "no-checks-reported") {
|
|
7459
|
+
lastDetail = "no-checks-reported (waiting for workflow to queue)";
|
|
7460
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
7461
|
+
continue;
|
|
7462
|
+
}
|
|
7463
|
+
lastDetail = state;
|
|
7464
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
7465
|
+
}
|
|
7466
|
+
return { policy, status: "timeout", detail: lastDetail };
|
|
7467
|
+
}
|
|
7468
|
+
|
|
7469
|
+
// src/bootstrap-ruleset.ts
|
|
7470
|
+
var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
|
|
7471
|
+
var PRODUCT_GATE_CONTEXT = "gate";
|
|
7472
|
+
function stripRulesetComment(raw) {
|
|
7473
|
+
const parsed = JSON.parse(raw);
|
|
7474
|
+
delete parsed._comment;
|
|
7475
|
+
return parsed;
|
|
7476
|
+
}
|
|
7477
|
+
function rulesetHasGateContext(ruleset, context = PRODUCT_GATE_CONTEXT) {
|
|
7478
|
+
for (const rule of ruleset.rules ?? []) {
|
|
7479
|
+
if (rule.type !== "required_status_checks") continue;
|
|
7480
|
+
for (const check of rule.parameters?.required_status_checks ?? []) {
|
|
7481
|
+
if (check.context === context) return true;
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
return false;
|
|
7485
|
+
}
|
|
7486
|
+
function findProductRuleset(rulesets) {
|
|
7487
|
+
return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
|
|
7488
|
+
}
|
|
7489
|
+
async function activateProductRuleset(repo, rulesetBody, client) {
|
|
7490
|
+
const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
|
|
7491
|
+
const existing = findProductRuleset(list ?? []);
|
|
7492
|
+
if (existing?.id != null) {
|
|
7493
|
+
const detail = await client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
|
|
7494
|
+
if (detail.enforcement === "active" && rulesetHasGateContext(detail)) {
|
|
7495
|
+
return { action: "skipped", detail: "active ruleset already requires gate" };
|
|
7496
|
+
}
|
|
7497
|
+
await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: rulesetBody, timeoutMs: 2e4 });
|
|
7498
|
+
return { action: "updated", detail: `ruleset ${existing.id}` };
|
|
7499
|
+
}
|
|
7500
|
+
await client.rest("POST", `repos/${repo}/rulesets`, { body: rulesetBody, timeoutMs: 2e4 });
|
|
7501
|
+
return { action: "created" };
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7504
|
+
// src/ci-audit.ts
|
|
7505
|
+
var HUB_REPO = "mutmutco/MMI-Hub";
|
|
7506
|
+
var PRODUCT_GATE_CONTEXT2 = "gate";
|
|
7507
|
+
var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
|
|
7508
|
+
var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
|
|
7509
|
+
var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
|
|
7510
|
+
function slugFromRepo(repo) {
|
|
7511
|
+
return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
7512
|
+
}
|
|
7513
|
+
function classifyRepo(repo, meta) {
|
|
7514
|
+
if (repo.toLowerCase() === HUB_REPO.toLowerCase()) return "hub";
|
|
7515
|
+
if (meta?.class === "content") return "content";
|
|
7516
|
+
if (meta?.class === "deployable" || meta?.deployModel && meta.deployModel !== "content" && meta.deployModel !== "none") return "deployable";
|
|
7517
|
+
if (meta) return "deployable";
|
|
7518
|
+
return "unknown";
|
|
7519
|
+
}
|
|
7520
|
+
function rulesetStatusChecks(rulesets) {
|
|
7521
|
+
return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
|
|
7522
|
+
}
|
|
7523
|
+
async function restJson(deps, path2, fallback) {
|
|
7524
|
+
try {
|
|
7525
|
+
return await deps.client.rest("GET", path2) ?? fallback;
|
|
7526
|
+
} catch {
|
|
7527
|
+
return fallback;
|
|
7528
|
+
}
|
|
7529
|
+
}
|
|
7530
|
+
async function rulesetDetails(deps, repo, list) {
|
|
7531
|
+
const details = [];
|
|
7532
|
+
for (const ruleset of list) {
|
|
7533
|
+
if (ruleset.id == null || ruleset.rules != null) {
|
|
7534
|
+
details.push(ruleset);
|
|
7535
|
+
continue;
|
|
7536
|
+
}
|
|
7537
|
+
details.push(await restJson(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
|
|
7538
|
+
}
|
|
7539
|
+
return details;
|
|
7540
|
+
}
|
|
7541
|
+
async function contentExists(deps, repo, branch, path2) {
|
|
7542
|
+
try {
|
|
7543
|
+
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
7544
|
+
await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
7545
|
+
return true;
|
|
7546
|
+
} catch {
|
|
7547
|
+
return false;
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
function collectRegistryRepos(projects) {
|
|
7551
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7552
|
+
for (const project2 of projects) {
|
|
7553
|
+
for (const raw of project2.repos ?? []) {
|
|
7554
|
+
const repo = raw.includes("/") ? raw : `mutmutco/${raw}`;
|
|
7555
|
+
seen.add(repo);
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
if (!seen.has(HUB_REPO)) seen.add(HUB_REPO);
|
|
7559
|
+
return [...seen].sort((a, b) => a.localeCompare(b));
|
|
7560
|
+
}
|
|
7561
|
+
async function resolveRepoMergeCiPolicy(repo, deps) {
|
|
7562
|
+
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
7563
|
+
const repoClass = classifyRepo(repo, meta);
|
|
7564
|
+
if (repoClass === "content") {
|
|
7565
|
+
return resolveMergeCiPolicy({
|
|
7566
|
+
workflowPaths: [],
|
|
7567
|
+
registryCi: meta?.ci ?? "none",
|
|
7568
|
+
registryRequiredChecks: meta?.requiredChecks ?? []
|
|
7569
|
+
});
|
|
7570
|
+
}
|
|
7571
|
+
const baseBranch = "development";
|
|
7572
|
+
const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
|
|
7573
|
+
return resolveMergeCiPolicy({
|
|
7574
|
+
workflowPaths: hasGate ? repoClass === "hub" ? [".github/workflows/gate.yml"] : [PRODUCT_GATE_PATH] : [],
|
|
7575
|
+
registryCi: meta?.ci,
|
|
7576
|
+
registryRequiredChecks: meta?.requiredChecks
|
|
7577
|
+
});
|
|
7578
|
+
}
|
|
7579
|
+
async function auditRepoCi(repo, deps) {
|
|
7580
|
+
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
7581
|
+
const repoClass = classifyRepo(repo, meta);
|
|
7582
|
+
const checks = [];
|
|
7583
|
+
const info = await restJson(deps, `repos/${repo}`, {});
|
|
7584
|
+
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
7585
|
+
checks.push({
|
|
7586
|
+
ok: info.allow_auto_merge === true,
|
|
7587
|
+
label: "allow_auto_merge enabled",
|
|
7588
|
+
detail: info.allow_auto_merge === true ? void 0 : "false or unavailable",
|
|
7589
|
+
remediation: `gh api -X PATCH repos/${repo} -f allow_auto_merge=true -f allow_squash_merge=true -f delete_branch_on_merge=true`
|
|
7590
|
+
});
|
|
7591
|
+
checks.push({
|
|
7592
|
+
ok: info.allow_squash_merge === true,
|
|
7593
|
+
label: "allow_squash_merge enabled",
|
|
7594
|
+
detail: info.allow_squash_merge === true ? void 0 : "false or unavailable"
|
|
7595
|
+
});
|
|
7596
|
+
checks.push({
|
|
7597
|
+
ok: info.delete_branch_on_merge === true,
|
|
7598
|
+
label: "delete_branch_on_merge enabled",
|
|
7599
|
+
detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
|
|
7600
|
+
});
|
|
7601
|
+
const hasGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
|
|
7602
|
+
if (repoClass === "deployable") {
|
|
7603
|
+
checks.push({
|
|
7604
|
+
ok: hasGateWorkflow,
|
|
7605
|
+
label: "gate workflow committed",
|
|
7606
|
+
detail: hasGateWorkflow ? void 0 : `missing ${PRODUCT_GATE_PATH}`,
|
|
7607
|
+
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
|
|
7608
|
+
});
|
|
7609
|
+
checks.push({
|
|
7610
|
+
ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
|
|
7611
|
+
label: "product ruleset reference committed",
|
|
7612
|
+
detail: `expected ${PRODUCT_RULESET_REF}`,
|
|
7613
|
+
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
|
|
7614
|
+
});
|
|
7615
|
+
}
|
|
7616
|
+
const rulesetList = await restJson(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
7617
|
+
const rulesets = await rulesetDetails(deps, repo, rulesetList);
|
|
7618
|
+
const activeBranchRulesets = rulesets.filter((r) => r.target === "branch" && r.enforcement === "active");
|
|
7619
|
+
const statusChecks = rulesetStatusChecks(activeBranchRulesets);
|
|
7620
|
+
if (repoClass === "hub") {
|
|
7621
|
+
const missing = HUB_GATE_CONTEXTS.filter((c) => !statusChecks.has(c));
|
|
7622
|
+
checks.push({
|
|
7623
|
+
ok: missing.length === 0,
|
|
7624
|
+
label: "Hub required status checks active",
|
|
7625
|
+
detail: missing.length ? `missing: ${missing.join(", ")}` : void 0
|
|
7626
|
+
});
|
|
7627
|
+
} else if (repoClass === "deployable") {
|
|
7628
|
+
const missing = [PRODUCT_GATE_CONTEXT2].filter((c) => !statusChecks.has(c));
|
|
7629
|
+
checks.push({
|
|
7630
|
+
ok: missing.length === 0,
|
|
7631
|
+
label: "product gate status check active",
|
|
7632
|
+
detail: missing.length ? `missing context: ${PRODUCT_GATE_CONTEXT2} \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset` : void 0,
|
|
7633
|
+
remediation: missing.length ? `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)` : void 0
|
|
7634
|
+
});
|
|
7635
|
+
}
|
|
7636
|
+
const registryCi = meta?.ci;
|
|
7637
|
+
const registryRequiredChecks = meta?.requiredChecks;
|
|
7638
|
+
const workflowPaths = hasGateWorkflow && repoClass === "deployable" ? [PRODUCT_GATE_PATH] : [];
|
|
7639
|
+
const { policy, reason } = resolveMergeCiPolicy({
|
|
7640
|
+
workflowPaths,
|
|
7641
|
+
registryCi,
|
|
7642
|
+
registryRequiredChecks
|
|
7643
|
+
});
|
|
7644
|
+
if (repoClass === "content") {
|
|
7645
|
+
const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
|
|
7646
|
+
checks.push({
|
|
7647
|
+
ok: explicitNoCi,
|
|
7648
|
+
label: "registry META declares intentional no-ci",
|
|
7649
|
+
detail: explicitNoCi ? void 0 : "set ci:none and requiredChecks:[] in registry META",
|
|
7650
|
+
remediation: `mmi-cli project set ${repo} --var ci=none --var requiredChecks=[]`
|
|
7651
|
+
});
|
|
7652
|
+
} else if (repoClass === "deployable") {
|
|
7653
|
+
checks.push({
|
|
7654
|
+
ok: policy === "wait-for-checks",
|
|
7655
|
+
label: "merge CI policy is wait-for-checks",
|
|
7656
|
+
detail: `${policy} (${reason})`
|
|
7657
|
+
});
|
|
7658
|
+
}
|
|
7659
|
+
const ok = checks.every((c) => c.ok);
|
|
7660
|
+
return { repo, class: repoClass, mergePolicy: policy, ok, checks };
|
|
7661
|
+
}
|
|
7662
|
+
async function auditOrgCi(deps, repoFilter) {
|
|
7663
|
+
const projects = await deps.listProjects();
|
|
7664
|
+
if (!projects) {
|
|
7665
|
+
const single = repoFilter ?? HUB_REPO;
|
|
7666
|
+
const report = await auditRepoCi(single, deps);
|
|
7667
|
+
return { ok: report.ok, repos: [report] };
|
|
7668
|
+
}
|
|
7669
|
+
const targets = repoFilter ? [repoFilter] : collectRegistryRepos(projects);
|
|
7670
|
+
const repos = [];
|
|
7671
|
+
for (const repo of targets) {
|
|
7672
|
+
repos.push(await auditRepoCi(repo, deps));
|
|
7673
|
+
}
|
|
7674
|
+
return { ok: repos.every((r) => r.ok), repos };
|
|
7675
|
+
}
|
|
7676
|
+
function renderCiAuditMarkdown(report) {
|
|
7677
|
+
const lines = [
|
|
7678
|
+
`# CI merge-readiness audit`,
|
|
7679
|
+
"",
|
|
7680
|
+
`Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
|
|
7681
|
+
"",
|
|
7682
|
+
"| Repo | Class | Policy | OK | Top gap |",
|
|
7683
|
+
"|------|-------|--------|----|---------|"
|
|
7684
|
+
];
|
|
7685
|
+
for (const r of report.repos) {
|
|
7686
|
+
const gap = r.checks.find((c) => !c.ok);
|
|
7687
|
+
lines.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap?.label ?? "\u2014"} |`);
|
|
7688
|
+
}
|
|
7689
|
+
return lines.join("\n");
|
|
7690
|
+
}
|
|
7691
|
+
function renderCiAuditText(report) {
|
|
7692
|
+
const lines = [`mmi-cli ci audit: ${report.ok ? "OK" : "GAPS"} (${report.repos.length} repos)`];
|
|
7693
|
+
for (const r of report.repos) {
|
|
7694
|
+
lines.push(`
|
|
7695
|
+
${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
|
|
7696
|
+
for (const c of r.checks) {
|
|
7697
|
+
lines.push(` ${c.ok ? "OK" : "FAIL"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
7698
|
+
}
|
|
7699
|
+
}
|
|
7700
|
+
return lines.join("\n");
|
|
7701
|
+
}
|
|
7702
|
+
async function applyCiReconcileMergeSettings(repo, deps) {
|
|
7703
|
+
const report = await auditRepoCi(repo, deps);
|
|
7704
|
+
const applied = [];
|
|
7705
|
+
const skipped = [];
|
|
7706
|
+
const errors = [];
|
|
7707
|
+
const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch"));
|
|
7708
|
+
const needsPatch = mergeChecks.some((c) => !c.ok);
|
|
7709
|
+
if (!needsPatch) {
|
|
7710
|
+
skipped.push("merge settings already canonical");
|
|
7711
|
+
return { repo, applied, skipped, errors };
|
|
7712
|
+
}
|
|
7713
|
+
try {
|
|
7714
|
+
await deps.client.rest("PATCH", `repos/${repo}`, {
|
|
7715
|
+
body: {
|
|
7716
|
+
allow_auto_merge: true,
|
|
7717
|
+
allow_squash_merge: true,
|
|
7718
|
+
delete_branch_on_merge: true
|
|
7719
|
+
}
|
|
7720
|
+
});
|
|
7721
|
+
applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge");
|
|
7722
|
+
} catch (e) {
|
|
7723
|
+
errors.push(e.message);
|
|
7724
|
+
}
|
|
7725
|
+
return { repo, applied, skipped, errors };
|
|
7726
|
+
}
|
|
7727
|
+
async function fetchRulesetSeedBody(deps, repo) {
|
|
7728
|
+
try {
|
|
7729
|
+
const encodedPath = PRODUCT_RULESET_REF.split("/").map(encodeURIComponent).join("/");
|
|
7730
|
+
const file = await deps.client.rest(
|
|
7731
|
+
"GET",
|
|
7732
|
+
`repos/${repo}/contents/${encodedPath}?ref=development`
|
|
7733
|
+
);
|
|
7734
|
+
if (file.encoding !== "base64" || typeof file.content !== "string") return null;
|
|
7735
|
+
return Buffer.from(file.content, "base64").toString("utf8");
|
|
7736
|
+
} catch {
|
|
7737
|
+
return null;
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
async function applyCiReconcileRepo(repo, deps) {
|
|
7741
|
+
const merge = await applyCiReconcileMergeSettings(repo, deps);
|
|
7742
|
+
const report = await auditRepoCi(repo, deps);
|
|
7743
|
+
if (report.class !== "deployable") return merge;
|
|
7744
|
+
const gateCheck = report.checks.find((c) => c.label === "product gate status check active");
|
|
7745
|
+
if (gateCheck?.ok) {
|
|
7746
|
+
merge.skipped.push("product ruleset already active");
|
|
7747
|
+
return merge;
|
|
7748
|
+
}
|
|
7749
|
+
const raw = await fetchRulesetSeedBody(deps, repo);
|
|
7750
|
+
if (!raw) {
|
|
7751
|
+
merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
|
|
7752
|
+
return merge;
|
|
7753
|
+
}
|
|
7754
|
+
try {
|
|
7755
|
+
const body = stripRulesetComment(raw);
|
|
7756
|
+
const activation = await activateProductRuleset(repo, body, deps.client);
|
|
7757
|
+
if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
|
|
7758
|
+
else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
|
|
7759
|
+
} catch (e) {
|
|
7760
|
+
merge.errors.push(e.message);
|
|
7761
|
+
}
|
|
7762
|
+
return merge;
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
// src/pr-land.ts
|
|
7766
|
+
var PR_LAND_POLL_MS = 3e4;
|
|
7767
|
+
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
7768
|
+
async function runPrLand(prNumber, options, deps) {
|
|
7769
|
+
const repo = await deps.resolveRepo(prNumber, options.repo);
|
|
7770
|
+
const base2 = { status: "failed", repo, pr: prNumber };
|
|
7771
|
+
if (options.requireTrain !== false) {
|
|
7772
|
+
const verdict = await deps.fetchTrainAuthority(repo);
|
|
7773
|
+
if (!verdict.ok) {
|
|
7774
|
+
return { ...base2, error: `train authority unavailable: ${verdict.error}` };
|
|
7775
|
+
}
|
|
7776
|
+
base2.train = { role: verdict.authority.role, train: verdict.authority.train };
|
|
7777
|
+
if (!verdict.authority.train) {
|
|
7778
|
+
return {
|
|
7779
|
+
...base2,
|
|
7780
|
+
error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
|
|
7781
|
+
};
|
|
7782
|
+
}
|
|
7783
|
+
}
|
|
7784
|
+
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
7785
|
+
base2.ciPolicy = ciPolicy;
|
|
7786
|
+
const checksWait = await deps.waitForChecks(prNumber, repo);
|
|
7787
|
+
base2.checksWait = checksWait;
|
|
7788
|
+
if (checksWait.status === "failure" || checksWait.status === "timeout") {
|
|
7789
|
+
return {
|
|
7790
|
+
...base2,
|
|
7791
|
+
error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
|
|
7792
|
+
};
|
|
7793
|
+
}
|
|
7794
|
+
const merge = await deps.mergeAuto(prNumber, repo);
|
|
7795
|
+
base2.mergeStatus = merge.mergeStatus;
|
|
7796
|
+
if (merge.mergeStatus === "failed") {
|
|
7797
|
+
return { ...base2, error: merge.error ?? "merge failed" };
|
|
7798
|
+
}
|
|
7799
|
+
if (merge.mergeStatus === "merged") {
|
|
7800
|
+
return { ...base2, status: "merged" };
|
|
7801
|
+
}
|
|
7802
|
+
const now = deps.now ?? (() => Date.now());
|
|
7803
|
+
const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
|
|
7804
|
+
if (merged) {
|
|
7805
|
+
return { ...base2, status: "auto-merge-enqueued-then-merged", mergeStatus: "merged" };
|
|
7806
|
+
}
|
|
7807
|
+
return {
|
|
7808
|
+
...base2,
|
|
7809
|
+
error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
|
|
7810
|
+
};
|
|
7811
|
+
}
|
|
7812
|
+
|
|
7813
|
+
// src/index.ts
|
|
7281
7814
|
var import_node_os5 = require("node:os");
|
|
7282
7815
|
|
|
7283
7816
|
// src/gh-create.ts
|
|
@@ -7614,7 +8147,7 @@ function parentLinkFields(result, error) {
|
|
|
7614
8147
|
}
|
|
7615
8148
|
|
|
7616
8149
|
// src/report.ts
|
|
7617
|
-
var
|
|
8150
|
+
var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
7618
8151
|
var REPORT_LABEL = "report";
|
|
7619
8152
|
function findDuplicateReport(source, openReports, threshold = 0.6) {
|
|
7620
8153
|
const normalizedTitle = normalizeTitle(source.title);
|
|
@@ -8298,6 +8831,28 @@ function selectSafeWorktreeCwd(worktrees, targetPath, options) {
|
|
|
8298
8831
|
const exists = options?.pathExists ?? (() => true);
|
|
8299
8832
|
return worktrees.find((w) => !samePath(w.path, targetPath) && exists(w.path))?.path;
|
|
8300
8833
|
}
|
|
8834
|
+
function isPathUnderDirectory(childPath, parentPath) {
|
|
8835
|
+
const child = normPath(childPath);
|
|
8836
|
+
const parent = normPath(parentPath);
|
|
8837
|
+
if (!child || !parent) return false;
|
|
8838
|
+
if (child === parent) return true;
|
|
8839
|
+
return child.startsWith(`${parent}/`);
|
|
8840
|
+
}
|
|
8841
|
+
function planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, currentCwd) {
|
|
8842
|
+
if (!safeCwd || !isPathUnderDirectory(currentCwd, targetPath)) return void 0;
|
|
8843
|
+
if (samePath(currentCwd, safeCwd)) return void 0;
|
|
8844
|
+
return safeCwd;
|
|
8845
|
+
}
|
|
8846
|
+
function releaseCwdIfUnderWorktree(targetPath, safeCwd, options) {
|
|
8847
|
+
const getCwd = options?.getCwd ?? (() => process.cwd());
|
|
8848
|
+
const chdir = options?.chdir ?? ((path2) => {
|
|
8849
|
+
process.chdir(path2);
|
|
8850
|
+
});
|
|
8851
|
+
const releaseTo = planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, getCwd());
|
|
8852
|
+
if (!releaseTo) return false;
|
|
8853
|
+
chdir(releaseTo);
|
|
8854
|
+
return true;
|
|
8855
|
+
}
|
|
8301
8856
|
function branchMissingFromList(branch, stdout) {
|
|
8302
8857
|
const names = stdout.split(/\r?\n/).map((line) => line.replace(/^\*\s*/, "").trim()).filter(Boolean);
|
|
8303
8858
|
return !names.includes(branch);
|
|
@@ -8350,6 +8905,10 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
8350
8905
|
stageTeardown = { status: "failed", error: errorMessage(e) };
|
|
8351
8906
|
}
|
|
8352
8907
|
}
|
|
8908
|
+
releaseCwdIfUnderWorktree(wtPath, safeCwd, {
|
|
8909
|
+
getCwd: options.getCwd,
|
|
8910
|
+
chdir: options.chdir
|
|
8911
|
+
});
|
|
8353
8912
|
const outcome = await removeWorktreeWithRecovery(wtPath, {
|
|
8354
8913
|
git,
|
|
8355
8914
|
sleep: options.sleep ?? defaultSleep,
|
|
@@ -8715,7 +9274,7 @@ function decideStage(inputs) {
|
|
|
8715
9274
|
|
|
8716
9275
|
// src/cursor-plugin-seed.ts
|
|
8717
9276
|
var import_node_child_process7 = require("node:child_process");
|
|
8718
|
-
var
|
|
9277
|
+
var import_node_fs12 = require("node:fs");
|
|
8719
9278
|
var import_node_os4 = require("node:os");
|
|
8720
9279
|
var import_node_path11 = require("node:path");
|
|
8721
9280
|
var import_node_util6 = require("node:util");
|
|
@@ -8744,7 +9303,7 @@ function cursorUserGlobalStatePath() {
|
|
|
8744
9303
|
}
|
|
8745
9304
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
8746
9305
|
const dbPath = cursorUserGlobalStatePath();
|
|
8747
|
-
if (!(0,
|
|
9306
|
+
if (!(0, import_node_fs12.existsSync)(dbPath)) return void 0;
|
|
8748
9307
|
try {
|
|
8749
9308
|
const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
|
|
8750
9309
|
timeout: 5e3
|
|
@@ -8758,11 +9317,11 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
|
8758
9317
|
}
|
|
8759
9318
|
}
|
|
8760
9319
|
function syncDirContents(src, dest) {
|
|
8761
|
-
(0,
|
|
8762
|
-
for (const name of (0,
|
|
8763
|
-
(0,
|
|
9320
|
+
(0, import_node_fs12.mkdirSync)(dest, { recursive: true });
|
|
9321
|
+
for (const name of (0, import_node_fs12.readdirSync)(dest)) {
|
|
9322
|
+
(0, import_node_fs12.rmSync)((0, import_node_path11.join)(dest, name), { recursive: true, force: true });
|
|
8764
9323
|
}
|
|
8765
|
-
(0,
|
|
9324
|
+
(0, import_node_fs12.cpSync)(src, dest, { recursive: true });
|
|
8766
9325
|
}
|
|
8767
9326
|
function releaseTag(releasedVersion) {
|
|
8768
9327
|
return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
|
|
@@ -8776,7 +9335,7 @@ async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFi
|
|
|
8776
9335
|
});
|
|
8777
9336
|
await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
|
|
8778
9337
|
const pluginMmi = (0, import_node_path11.join)(tmpRoot, "plugins", "mmi");
|
|
8779
|
-
return (0,
|
|
9338
|
+
return (0, import_node_fs12.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
8780
9339
|
} catch {
|
|
8781
9340
|
return void 0;
|
|
8782
9341
|
}
|
|
@@ -8784,25 +9343,25 @@ async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFi
|
|
|
8784
9343
|
async function downloadPluginMmiViaGh(tag, tmpRoot) {
|
|
8785
9344
|
const tarPath = (0, import_node_path11.join)(tmpRoot, "repo.tgz");
|
|
8786
9345
|
try {
|
|
8787
|
-
(0,
|
|
9346
|
+
(0, import_node_fs12.mkdirSync)(tmpRoot, { recursive: true });
|
|
8788
9347
|
const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
|
|
8789
9348
|
timeout: 12e4,
|
|
8790
9349
|
maxBuffer: 100 * 1024 * 1024,
|
|
8791
9350
|
encoding: "buffer",
|
|
8792
9351
|
windowsHide: true
|
|
8793
9352
|
});
|
|
8794
|
-
(0,
|
|
9353
|
+
(0, import_node_fs12.writeFileSync)(tarPath, stdout);
|
|
8795
9354
|
await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
|
|
8796
|
-
const top = (0,
|
|
9355
|
+
const top = (0, import_node_fs12.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
|
|
8797
9356
|
if (!top) return void 0;
|
|
8798
9357
|
const pluginMmi = (0, import_node_path11.join)(tmpRoot, top, "plugins", "mmi");
|
|
8799
|
-
return (0,
|
|
9358
|
+
return (0, import_node_fs12.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
8800
9359
|
} catch {
|
|
8801
9360
|
return void 0;
|
|
8802
9361
|
}
|
|
8803
9362
|
}
|
|
8804
9363
|
async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
|
|
8805
|
-
(0,
|
|
9364
|
+
(0, import_node_fs12.mkdirSync)(tmpRoot, { recursive: true });
|
|
8806
9365
|
const tag = releaseTag(releasedVersion);
|
|
8807
9366
|
if (hubCheckout) {
|
|
8808
9367
|
const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
|
|
@@ -8829,7 +9388,7 @@ async function applyCursorPluginCacheSeed(input) {
|
|
|
8829
9388
|
for (const pin of pinsToSeed) {
|
|
8830
9389
|
syncDirContents(source, pin.path);
|
|
8831
9390
|
}
|
|
8832
|
-
(0,
|
|
9391
|
+
(0, import_node_fs12.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
8833
9392
|
return true;
|
|
8834
9393
|
}
|
|
8835
9394
|
|
|
@@ -9092,6 +9651,12 @@ function cachePathJoin(root, ...parts) {
|
|
|
9092
9651
|
const sep = root.includes("\\") ? "\\" : "/";
|
|
9093
9652
|
return [root.replace(/[\\/]+$/, ""), ...parts].join(sep);
|
|
9094
9653
|
}
|
|
9654
|
+
function cacheParentDir(root) {
|
|
9655
|
+
const sep = root.includes("\\") ? "\\" : "/";
|
|
9656
|
+
const trimmed = root.replace(/[\\/]+$/, "");
|
|
9657
|
+
const idx = trimmed.lastIndexOf(sep);
|
|
9658
|
+
return idx <= 0 ? root : trimmed.slice(0, idx);
|
|
9659
|
+
}
|
|
9095
9660
|
function isProtectedCacheDir(name, protectedVersions) {
|
|
9096
9661
|
const normalized = normalizeVersion(name);
|
|
9097
9662
|
return name.startsWith(".") || normalized !== void 0 && protectedVersions.has(normalized);
|
|
@@ -9122,7 +9687,7 @@ function buildMmiPluginCacheCleanupCheck(input) {
|
|
|
9122
9687
|
plannedCount: leftovers.length,
|
|
9123
9688
|
quarantinePlan: leftovers.map((entry) => ({
|
|
9124
9689
|
from: entry.path,
|
|
9125
|
-
to: cachePathJoin(entry.root, ".mmi-quarantine", stamp, entry.name)
|
|
9690
|
+
to: cachePathJoin(cacheParentDir(entry.root), ".mmi-quarantine", stamp, entry.name)
|
|
9126
9691
|
}))
|
|
9127
9692
|
};
|
|
9128
9693
|
}
|
|
@@ -9183,6 +9748,12 @@ var CLAUDE_PLUGIN_HEAL_STEPS = [
|
|
|
9183
9748
|
{ args: ["plugin", "install", "mmi@mmi"], gated: true },
|
|
9184
9749
|
{ args: ["plugin", "enable", "mmi@mmi"], gated: false }
|
|
9185
9750
|
];
|
|
9751
|
+
var CODEX_PLUGIN_RECOVERY = "codex plugin marketplace remove mmi && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mmi";
|
|
9752
|
+
var CODEX_PLUGIN_HEAL_STEPS = [
|
|
9753
|
+
{ args: ["plugin", "marketplace", "remove", "mmi"], gated: false },
|
|
9754
|
+
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
|
|
9755
|
+
{ args: ["plugin", "add", "mmi@mmi"], gated: true }
|
|
9756
|
+
];
|
|
9186
9757
|
function healStepAborts(step, ok) {
|
|
9187
9758
|
return !ok && step.gated;
|
|
9188
9759
|
}
|
|
@@ -9193,7 +9764,7 @@ function pluginRecoveryFix(surface) {
|
|
|
9193
9764
|
case "claude-cli":
|
|
9194
9765
|
return `${claude} # then ${reloadAction(surface)} to reload MMI commands`;
|
|
9195
9766
|
case "codex":
|
|
9196
|
-
return
|
|
9767
|
+
return `${CODEX_PLUGIN_RECOVERY} # then ${reloadAction(surface)}`;
|
|
9197
9768
|
case "cursor":
|
|
9198
9769
|
return `in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace; then ${reloadAction(surface)} to reload MMI skills + hooks`;
|
|
9199
9770
|
case "shell":
|
|
@@ -9203,7 +9774,7 @@ function pluginRecoveryFix(surface) {
|
|
|
9203
9774
|
}
|
|
9204
9775
|
var PLUGIN_UPDATE_RECIPES = {
|
|
9205
9776
|
claude: [CLAUDE_PLUGIN_RECOVERY],
|
|
9206
|
-
codex: [
|
|
9777
|
+
codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@mmi shows the released version"],
|
|
9207
9778
|
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
9208
9779
|
};
|
|
9209
9780
|
function highestSemver(versions) {
|
|
@@ -9257,7 +9828,7 @@ function isSemverVersion2(v) {
|
|
|
9257
9828
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
9258
9829
|
}
|
|
9259
9830
|
function staleRecordCommand(surface) {
|
|
9260
|
-
return surface === "codex" ?
|
|
9831
|
+
return surface === "codex" ? CODEX_PLUGIN_RECOVERY : CLAUDE_PLUGIN_RECOVERY;
|
|
9261
9832
|
}
|
|
9262
9833
|
function staleSurfacesFix(stale, releasedVersion) {
|
|
9263
9834
|
const parts = stale.map((s) => {
|
|
@@ -9535,7 +10106,7 @@ async function runStageLiveDown(deps, t) {
|
|
|
9535
10106
|
|
|
9536
10107
|
// src/stage-runner.ts
|
|
9537
10108
|
var import_node_child_process8 = require("node:child_process");
|
|
9538
|
-
var
|
|
10109
|
+
var import_node_fs13 = require("node:fs");
|
|
9539
10110
|
var import_node_path12 = require("node:path");
|
|
9540
10111
|
var import_node_net2 = require("node:net");
|
|
9541
10112
|
var import_node_util7 = require("node:util");
|
|
@@ -9583,6 +10154,27 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
|
|
|
9583
10154
|
function stageStatePath(cwd = process.cwd()) {
|
|
9584
10155
|
return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
|
|
9585
10156
|
}
|
|
10157
|
+
function mergeEnvSecretsIntoFile(content, secrets2) {
|
|
10158
|
+
const lines = content.split(/\r?\n/);
|
|
10159
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
10160
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10161
|
+
const trimmed = lines[i].trim();
|
|
10162
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
10163
|
+
const eq = trimmed.indexOf("=");
|
|
10164
|
+
if (eq === -1) continue;
|
|
10165
|
+
indexByKey.set(trimmed.slice(0, eq).trim(), i);
|
|
10166
|
+
}
|
|
10167
|
+
for (const [key, value] of Object.entries(secrets2)) {
|
|
10168
|
+
const escaped = /[\s#"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
|
|
10169
|
+
const line = `${key}=${escaped}`;
|
|
10170
|
+
const idx = indexByKey.get(key);
|
|
10171
|
+
if (idx != null) lines[idx] = line;
|
|
10172
|
+
else lines.push(line);
|
|
10173
|
+
}
|
|
10174
|
+
const body = lines.join("\n");
|
|
10175
|
+
return body.endsWith("\n") ? body : `${body}
|
|
10176
|
+
`;
|
|
10177
|
+
}
|
|
9586
10178
|
var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
|
|
9587
10179
|
function posixOnlyShellProblems(command, field, platform = process.platform) {
|
|
9588
10180
|
if (platform !== "win32" || !command?.trim()) return [];
|
|
@@ -9643,9 +10235,9 @@ async function shell(command, cwd, timeoutMs) {
|
|
|
9643
10235
|
});
|
|
9644
10236
|
}
|
|
9645
10237
|
function readState(path2) {
|
|
9646
|
-
if (!(0,
|
|
10238
|
+
if (!(0, import_node_fs13.existsSync)(path2)) return null;
|
|
9647
10239
|
try {
|
|
9648
|
-
return JSON.parse((0,
|
|
10240
|
+
return JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
|
|
9649
10241
|
} catch {
|
|
9650
10242
|
return null;
|
|
9651
10243
|
}
|
|
@@ -9697,7 +10289,7 @@ async function stopStage(opts = {}) {
|
|
|
9697
10289
|
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
9698
10290
|
}
|
|
9699
10291
|
await killTree(state.pid);
|
|
9700
|
-
(0,
|
|
10292
|
+
(0, import_node_fs13.rmSync)(statePath, { force: true });
|
|
9701
10293
|
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
9702
10294
|
}
|
|
9703
10295
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -9706,7 +10298,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9706
10298
|
const cwd = opts.cwd ?? process.cwd();
|
|
9707
10299
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
9708
10300
|
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
9709
|
-
(0,
|
|
10301
|
+
(0, import_node_fs13.mkdirSync)(dir, { recursive: true });
|
|
9710
10302
|
let stagePort;
|
|
9711
10303
|
if (config.portRange) {
|
|
9712
10304
|
const [s, e] = config.portRange;
|
|
@@ -9718,12 +10310,12 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9718
10310
|
if (config.ensureEnv) {
|
|
9719
10311
|
const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
|
|
9720
10312
|
const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
|
|
9721
|
-
if (!(0,
|
|
9722
|
-
(0,
|
|
9723
|
-
} else if ((0,
|
|
9724
|
-
const stale = detectStaleEnvFile((0,
|
|
9725
|
-
exampleMtimeMs: (0,
|
|
9726
|
-
targetMtimeMs: (0,
|
|
10313
|
+
if (!(0, import_node_fs13.existsSync)(target) && (0, import_node_fs13.existsSync)(example)) {
|
|
10314
|
+
(0, import_node_fs13.copyFileSync)(example, target);
|
|
10315
|
+
} else if ((0, import_node_fs13.existsSync)(target) && (0, import_node_fs13.existsSync)(example)) {
|
|
10316
|
+
const stale = detectStaleEnvFile((0, import_node_fs13.readFileSync)(example, "utf8"), (0, import_node_fs13.readFileSync)(target, "utf8"), {
|
|
10317
|
+
exampleMtimeMs: (0, import_node_fs13.statSync)(example).mtimeMs,
|
|
10318
|
+
targetMtimeMs: (0, import_node_fs13.statSync)(target).mtimeMs
|
|
9727
10319
|
});
|
|
9728
10320
|
if (stale) {
|
|
9729
10321
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -9731,6 +10323,9 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9731
10323
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
9732
10324
|
}
|
|
9733
10325
|
}
|
|
10326
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs13.existsSync)(target)) {
|
|
10327
|
+
(0, import_node_fs13.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs13.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
10328
|
+
}
|
|
9734
10329
|
}
|
|
9735
10330
|
const extraEnv = {};
|
|
9736
10331
|
for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
|
|
@@ -9754,13 +10349,13 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9754
10349
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
9755
10350
|
port: stagePort
|
|
9756
10351
|
};
|
|
9757
|
-
(0,
|
|
10352
|
+
(0, import_node_fs13.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
9758
10353
|
try {
|
|
9759
10354
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
9760
10355
|
else await waitForProcessStability(child);
|
|
9761
10356
|
} catch (e) {
|
|
9762
10357
|
await killTree(state.pid);
|
|
9763
|
-
(0,
|
|
10358
|
+
(0, import_node_fs13.rmSync)(statePath, { force: true });
|
|
9764
10359
|
throw e;
|
|
9765
10360
|
}
|
|
9766
10361
|
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
@@ -10030,9 +10625,9 @@ async function requireBranch(deps, branch) {
|
|
|
10030
10625
|
const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
10031
10626
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
10032
10627
|
}
|
|
10033
|
-
var
|
|
10628
|
+
var HUB_REPO3 = "mutmutco/MMI-Hub";
|
|
10034
10629
|
function isHubControlRepo(repo) {
|
|
10035
|
-
return repo.toLowerCase() ===
|
|
10630
|
+
return repo.toLowerCase() === HUB_REPO3.toLowerCase();
|
|
10036
10631
|
}
|
|
10037
10632
|
function projectGetFailureText(e) {
|
|
10038
10633
|
const err = e;
|
|
@@ -10092,7 +10687,7 @@ async function correlateDispatchedRun(deps, workflow, since) {
|
|
|
10092
10687
|
"run",
|
|
10093
10688
|
"list",
|
|
10094
10689
|
"--repo",
|
|
10095
|
-
|
|
10690
|
+
HUB_REPO3,
|
|
10096
10691
|
"--workflow",
|
|
10097
10692
|
workflow,
|
|
10098
10693
|
"--limit",
|
|
@@ -10126,7 +10721,7 @@ async function correlateWorkflowRun(deps, args) {
|
|
|
10126
10721
|
"run",
|
|
10127
10722
|
"list",
|
|
10128
10723
|
"--repo",
|
|
10129
|
-
|
|
10724
|
+
HUB_REPO3,
|
|
10130
10725
|
"--workflow",
|
|
10131
10726
|
args.workflow,
|
|
10132
10727
|
"--event",
|
|
@@ -10154,7 +10749,7 @@ async function correlateWorkflowRun(deps, args) {
|
|
|
10154
10749
|
async function watchTenantRun(deps, runId) {
|
|
10155
10750
|
if (runId == null) return "pending";
|
|
10156
10751
|
try {
|
|
10157
|
-
await deps.run("gh", ["run", "watch", String(runId), "--repo",
|
|
10752
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", HUB_REPO3, "--exit-status"]);
|
|
10158
10753
|
return "success";
|
|
10159
10754
|
} catch {
|
|
10160
10755
|
return "failure";
|
|
@@ -10375,7 +10970,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10375
10970
|
"run",
|
|
10376
10971
|
"tenant-publish.yml",
|
|
10377
10972
|
"--repo",
|
|
10378
|
-
|
|
10973
|
+
HUB_REPO3,
|
|
10379
10974
|
"-f",
|
|
10380
10975
|
`slug=${ctx.slug}`,
|
|
10381
10976
|
"-f",
|
|
@@ -11499,7 +12094,7 @@ async function announceRelease(deps, args) {
|
|
|
11499
12094
|
}
|
|
11500
12095
|
|
|
11501
12096
|
// src/port-registry.ts
|
|
11502
|
-
var
|
|
12097
|
+
var import_node_fs14 = require("node:fs");
|
|
11503
12098
|
|
|
11504
12099
|
// ../infra/port-geometry.mjs
|
|
11505
12100
|
var PORT_BLOCK = 100;
|
|
@@ -11513,8 +12108,8 @@ function nextPortBlock(registry2) {
|
|
|
11513
12108
|
return [base2, base2 + PORT_SPAN];
|
|
11514
12109
|
}
|
|
11515
12110
|
function loadPortRegistry(path2) {
|
|
11516
|
-
if (!(0,
|
|
11517
|
-
const raw = JSON.parse((0,
|
|
12111
|
+
if (!(0, import_node_fs14.existsSync)(path2)) return {};
|
|
12112
|
+
const raw = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
|
|
11518
12113
|
const out = {};
|
|
11519
12114
|
for (const [key, value] of Object.entries(raw)) {
|
|
11520
12115
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -11528,9 +12123,9 @@ function ensurePortRange(repo, path2) {
|
|
|
11528
12123
|
const existing = registry2[repo];
|
|
11529
12124
|
if (existing) return existing;
|
|
11530
12125
|
const range = nextPortBlock(registry2);
|
|
11531
|
-
const raw = (0,
|
|
12126
|
+
const raw = (0, import_node_fs14.existsSync)(path2) ? JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8")) : {};
|
|
11532
12127
|
raw[repo] = range;
|
|
11533
|
-
(0,
|
|
12128
|
+
(0, import_node_fs14.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
11534
12129
|
return range;
|
|
11535
12130
|
}
|
|
11536
12131
|
function portCursorSeed(registry2) {
|
|
@@ -11582,7 +12177,7 @@ function safeJson(text, fallback) {
|
|
|
11582
12177
|
return fallback;
|
|
11583
12178
|
}
|
|
11584
12179
|
}
|
|
11585
|
-
async function
|
|
12180
|
+
async function restJson2(deps, path2, fallback) {
|
|
11586
12181
|
try {
|
|
11587
12182
|
return await deps.client.rest("GET", path2) ?? fallback;
|
|
11588
12183
|
} catch {
|
|
@@ -11709,7 +12304,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
|
|
|
11709
12304
|
return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
11710
12305
|
}
|
|
11711
12306
|
async function auditOrgBasePermission(deps) {
|
|
11712
|
-
const org = await
|
|
12307
|
+
const org = await restJson2(deps, `orgs/${OWNER2}`, {});
|
|
11713
12308
|
const perm = org.default_repository_permission;
|
|
11714
12309
|
if (perm && perm !== "read" && perm !== "none") {
|
|
11715
12310
|
return [{
|
|
@@ -11818,8 +12413,8 @@ var requiredIssueTemplates = [
|
|
|
11818
12413
|
var requiredWorkflows = [];
|
|
11819
12414
|
var requiredProductWorkflows = [".github/workflows/gate.yml"];
|
|
11820
12415
|
var requiredProductRulesetRef = ".github/rulesets/mmi-product-required-checks.json";
|
|
11821
|
-
var
|
|
11822
|
-
var requiredLabels = ["bug", "feature", "task"
|
|
12416
|
+
var HUB_REPO4 = "mutmutco/MMI-Hub";
|
|
12417
|
+
var requiredLabels = ["bug", "feature", "task"];
|
|
11823
12418
|
var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
|
|
11824
12419
|
var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
|
|
11825
12420
|
var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
|
|
@@ -11858,7 +12453,7 @@ function safeJson2(text, fallback) {
|
|
|
11858
12453
|
return fallback;
|
|
11859
12454
|
}
|
|
11860
12455
|
}
|
|
11861
|
-
async function
|
|
12456
|
+
async function restJson3(deps, path2, fallback) {
|
|
11862
12457
|
try {
|
|
11863
12458
|
return await deps.client.rest("GET", path2) ?? fallback;
|
|
11864
12459
|
} catch {
|
|
@@ -11872,7 +12467,7 @@ async function restPagedJson2(deps, path2, fallback) {
|
|
|
11872
12467
|
return fallback;
|
|
11873
12468
|
}
|
|
11874
12469
|
}
|
|
11875
|
-
async function
|
|
12470
|
+
async function contentExists2(deps, repo, branch, path2) {
|
|
11876
12471
|
try {
|
|
11877
12472
|
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
11878
12473
|
await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
@@ -11917,17 +12512,17 @@ function missingRuleTypes(ruleset, required) {
|
|
|
11917
12512
|
const types = new Set((ruleset.rules || []).map((rule) => rule.type).filter(Boolean));
|
|
11918
12513
|
return required.filter((type) => !types.has(type));
|
|
11919
12514
|
}
|
|
11920
|
-
function
|
|
12515
|
+
function rulesetStatusChecks2(rulesets) {
|
|
11921
12516
|
return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
|
|
11922
12517
|
}
|
|
11923
|
-
async function
|
|
12518
|
+
async function rulesetDetails2(deps, repo, list) {
|
|
11924
12519
|
const details = [];
|
|
11925
12520
|
for (const ruleset of list) {
|
|
11926
12521
|
if (ruleset.id == null || ruleset.rules != null) {
|
|
11927
12522
|
details.push(ruleset);
|
|
11928
12523
|
continue;
|
|
11929
12524
|
}
|
|
11930
|
-
details.push(await
|
|
12525
|
+
details.push(await restJson3(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
|
|
11931
12526
|
}
|
|
11932
12527
|
return details;
|
|
11933
12528
|
}
|
|
@@ -11946,7 +12541,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
11946
12541
|
const branchesWanted = expectedBranches(repoClass, releaseTrack);
|
|
11947
12542
|
const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
|
|
11948
12543
|
const checks = [];
|
|
11949
|
-
const repoInfo = await
|
|
12544
|
+
const repoInfo = await restJson3(deps, `repos/${repo}`, {});
|
|
11950
12545
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
11951
12546
|
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
11952
12547
|
checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
|
|
@@ -11976,29 +12571,29 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
11976
12571
|
detail: overgrants.length ? `over-granted: ${overgrants.map((f) => f.actor).join(", ")}` : void 0
|
|
11977
12572
|
});
|
|
11978
12573
|
for (const path2 of requiredDocs) {
|
|
11979
|
-
checks.push({ ok: await
|
|
12574
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `bootstrap artifact exists: ${path2}` });
|
|
11980
12575
|
}
|
|
11981
12576
|
for (const path2 of requiredIssueTemplates) {
|
|
11982
|
-
checks.push({ ok: await
|
|
12577
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `issue template exists: ${path2}` });
|
|
11983
12578
|
}
|
|
11984
12579
|
for (const path2 of requiredWorkflows) {
|
|
11985
|
-
checks.push({ ok: await
|
|
12580
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
|
|
11986
12581
|
}
|
|
11987
|
-
if (repo !==
|
|
12582
|
+
if (repo !== HUB_REPO4 && repoClass === "deployable") {
|
|
11988
12583
|
for (const path2 of requiredProductWorkflows) {
|
|
11989
|
-
checks.push({ ok: await
|
|
12584
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
|
|
11990
12585
|
}
|
|
11991
12586
|
checks.push({
|
|
11992
|
-
ok: await
|
|
12587
|
+
ok: await contentExists2(deps, repo, baseBranch, requiredProductRulesetRef),
|
|
11993
12588
|
label: "product required-check ruleset reference exists",
|
|
11994
12589
|
detail: `expected: ${requiredProductRulesetRef} (apply as an active repo ruleset after bootstrap)`
|
|
11995
12590
|
});
|
|
11996
12591
|
}
|
|
11997
12592
|
if (repoClass === "deployable") {
|
|
11998
12593
|
const trainScript = "scripts/next-version.mjs";
|
|
11999
|
-
checks.push({ ok: await
|
|
12594
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
|
|
12000
12595
|
}
|
|
12001
|
-
checks.push({ ok: await
|
|
12596
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
|
|
12002
12597
|
const readme = await contentText(deps, repo, baseBranch, "README.md");
|
|
12003
12598
|
checks.push({
|
|
12004
12599
|
ok: readme !== null && readme.includes("## Agent context"),
|
|
@@ -12006,7 +12601,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12006
12601
|
detail: readme === null ? "README.md not readable via API" : void 0
|
|
12007
12602
|
});
|
|
12008
12603
|
const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
|
|
12009
|
-
const agentRulesOk = await
|
|
12604
|
+
const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
|
|
12010
12605
|
checks.push({
|
|
12011
12606
|
ok: agentRulesOk,
|
|
12012
12607
|
label: "Cursor agent rules file exists",
|
|
@@ -12019,7 +12614,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12019
12614
|
}
|
|
12020
12615
|
const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
|
|
12021
12616
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
12022
|
-
const actions = await
|
|
12617
|
+
const actions = await restJson3(deps, `repos/${repo}/actions/permissions`, {});
|
|
12023
12618
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
12024
12619
|
const config = deps.projectMeta ?? null;
|
|
12025
12620
|
checks.push({
|
|
@@ -12124,8 +12719,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12124
12719
|
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
12125
12720
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
|
|
12126
12721
|
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
|
|
12127
|
-
const rulesetList = await
|
|
12128
|
-
const rulesets = await
|
|
12722
|
+
const rulesetList = await restJson3(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
12723
|
+
const rulesets = await rulesetDetails2(deps, repo, rulesetList);
|
|
12129
12724
|
const activeOrgRulesets = rulesets.filter(
|
|
12130
12725
|
(r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
|
|
12131
12726
|
);
|
|
@@ -12136,16 +12731,16 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12136
12731
|
label: "covered by an active org ruleset",
|
|
12137
12732
|
detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
|
|
12138
12733
|
});
|
|
12139
|
-
if (repo ===
|
|
12140
|
-
const statusChecks =
|
|
12734
|
+
if (repo === HUB_REPO4) {
|
|
12735
|
+
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
12141
12736
|
const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
|
|
12142
12737
|
checks.push({
|
|
12143
12738
|
ok: missing.length === 0,
|
|
12144
12739
|
label: "Hub required status checks configured",
|
|
12145
12740
|
detail: optionDetail(missing)
|
|
12146
12741
|
});
|
|
12147
|
-
} else {
|
|
12148
|
-
const statusChecks =
|
|
12742
|
+
} else if (repoClass === "deployable") {
|
|
12743
|
+
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
12149
12744
|
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
12150
12745
|
checks.push({
|
|
12151
12746
|
ok: missing.length === 0,
|
|
@@ -13848,11 +14443,11 @@ async function planGraduate(deps, slug, opts = {}) {
|
|
|
13848
14443
|
}
|
|
13849
14444
|
|
|
13850
14445
|
// src/atomic-write.ts
|
|
13851
|
-
var
|
|
14446
|
+
var import_node_fs15 = require("node:fs");
|
|
13852
14447
|
function atomicWriteFileSync(path2, content) {
|
|
13853
14448
|
const tmp = `${path2}.${process.pid}.tmp`;
|
|
13854
|
-
(0,
|
|
13855
|
-
(0,
|
|
14449
|
+
(0, import_node_fs15.writeFileSync)(tmp, content, "utf8");
|
|
14450
|
+
(0, import_node_fs15.renameSync)(tmp, path2);
|
|
13856
14451
|
}
|
|
13857
14452
|
|
|
13858
14453
|
// src/oauth.ts
|
|
@@ -14083,7 +14678,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
14083
14678
|
}
|
|
14084
14679
|
function readRepoVersion() {
|
|
14085
14680
|
try {
|
|
14086
|
-
return JSON.parse((0,
|
|
14681
|
+
return JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
14087
14682
|
} catch {
|
|
14088
14683
|
return void 0;
|
|
14089
14684
|
}
|
|
@@ -14158,6 +14753,22 @@ async function applyClaudePluginHeal(surface, log) {
|
|
|
14158
14753
|
}
|
|
14159
14754
|
return true;
|
|
14160
14755
|
}
|
|
14756
|
+
async function runCodexPlugin(args) {
|
|
14757
|
+
try {
|
|
14758
|
+
await runHostBin("codex", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
14759
|
+
return true;
|
|
14760
|
+
} catch {
|
|
14761
|
+
return false;
|
|
14762
|
+
}
|
|
14763
|
+
}
|
|
14764
|
+
async function applyCodexPluginHeal(surface, log) {
|
|
14765
|
+
if (surface !== "codex") return false;
|
|
14766
|
+
log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
|
|
14767
|
+
for (const step of CODEX_PLUGIN_HEAL_STEPS) {
|
|
14768
|
+
if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
|
|
14769
|
+
}
|
|
14770
|
+
return true;
|
|
14771
|
+
}
|
|
14161
14772
|
var program2 = new Command();
|
|
14162
14773
|
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
|
|
14163
14774
|
async function runRulesSync(opts, io = consoleIo) {
|
|
@@ -14193,10 +14804,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
14193
14804
|
for (const entry of fetched) {
|
|
14194
14805
|
if ("error" in entry) continue;
|
|
14195
14806
|
const { file, source } = entry;
|
|
14196
|
-
const current = (0,
|
|
14807
|
+
const current = (0, import_node_fs16.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
|
|
14197
14808
|
if (needsUpdate(source, current)) {
|
|
14198
14809
|
const slash = file.lastIndexOf("/");
|
|
14199
|
-
if (slash > 0) (0,
|
|
14810
|
+
if (slash > 0) (0, import_node_fs16.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
14200
14811
|
await (0, import_promises5.writeFile)(file, normalizeEol(source), "utf8");
|
|
14201
14812
|
changed++;
|
|
14202
14813
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -14222,7 +14833,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
14222
14833
|
return null;
|
|
14223
14834
|
}
|
|
14224
14835
|
},
|
|
14225
|
-
localContent: async (f) => (0,
|
|
14836
|
+
localContent: async (f) => (0, import_node_fs16.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
|
|
14226
14837
|
writeDoc: async (f, c) => {
|
|
14227
14838
|
await (0, import_promises5.writeFile)(f, c, "utf8");
|
|
14228
14839
|
}
|
|
@@ -14395,7 +15006,7 @@ function detachPlanSync() {
|
|
|
14395
15006
|
}
|
|
14396
15007
|
}
|
|
14397
15008
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
14398
|
-
const ensureDir = () => (0,
|
|
15009
|
+
const ensureDir = () => (0, import_node_fs16.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
14399
15010
|
return {
|
|
14400
15011
|
apiUrl: cfg.sagaApiUrl,
|
|
14401
15012
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
@@ -14403,24 +15014,24 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
14403
15014
|
project: async () => (await sagaKey(cfg)).project,
|
|
14404
15015
|
readLocal: (slug) => {
|
|
14405
15016
|
try {
|
|
14406
|
-
return (0,
|
|
15017
|
+
return (0, import_node_fs16.readFileSync)(planPath(slug), "utf8");
|
|
14407
15018
|
} catch {
|
|
14408
15019
|
return null;
|
|
14409
15020
|
}
|
|
14410
15021
|
},
|
|
14411
15022
|
writeLocal: (slug, content) => {
|
|
14412
15023
|
ensureDir();
|
|
14413
|
-
(0,
|
|
15024
|
+
(0, import_node_fs16.writeFileSync)(planPath(slug), content, "utf8");
|
|
14414
15025
|
},
|
|
14415
15026
|
removeLocal: (slug) => {
|
|
14416
15027
|
try {
|
|
14417
|
-
(0,
|
|
15028
|
+
(0, import_node_fs16.rmSync)(planPath(slug));
|
|
14418
15029
|
} catch {
|
|
14419
15030
|
}
|
|
14420
15031
|
},
|
|
14421
15032
|
readMetaRaw: () => {
|
|
14422
15033
|
try {
|
|
14423
|
-
return (0,
|
|
15034
|
+
return (0, import_node_fs16.readFileSync)(META_FILE, "utf8");
|
|
14424
15035
|
} catch {
|
|
14425
15036
|
return null;
|
|
14426
15037
|
}
|
|
@@ -14431,7 +15042,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
14431
15042
|
},
|
|
14432
15043
|
readIndexRaw: () => {
|
|
14433
15044
|
try {
|
|
14434
|
-
return (0,
|
|
15045
|
+
return (0, import_node_fs16.readFileSync)(INDEX_FILE, "utf8");
|
|
14435
15046
|
} catch {
|
|
14436
15047
|
return null;
|
|
14437
15048
|
}
|
|
@@ -14442,7 +15053,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
14442
15053
|
},
|
|
14443
15054
|
readQueueRaw: () => {
|
|
14444
15055
|
try {
|
|
14445
|
-
return (0,
|
|
15056
|
+
return (0, import_node_fs16.readFileSync)(QUEUE_FILE, "utf8");
|
|
14446
15057
|
} catch {
|
|
14447
15058
|
return null;
|
|
14448
15059
|
}
|
|
@@ -14632,6 +15243,20 @@ secrets.command("edit <key>").description("alias for set \u2014 replace a secret
|
|
|
14632
15243
|
const ok = await secretsEdit(d, key, o);
|
|
14633
15244
|
if (!ok) process.exitCode = 1;
|
|
14634
15245
|
}));
|
|
15246
|
+
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) => {
|
|
15247
|
+
const stages = ["dev", "rc", "main"];
|
|
15248
|
+
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
15249
|
+
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
15250
|
+
}
|
|
15251
|
+
const ok = await secretsCopy(d, {
|
|
15252
|
+
repo: o.repo,
|
|
15253
|
+
from: o.from,
|
|
15254
|
+
to: o.to,
|
|
15255
|
+
keys: o.keys.split(","),
|
|
15256
|
+
dryRun: o.dryRun
|
|
15257
|
+
});
|
|
15258
|
+
if (!ok) process.exitCode = 1;
|
|
15259
|
+
}));
|
|
14635
15260
|
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)));
|
|
14636
15261
|
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)));
|
|
14637
15262
|
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, {})));
|
|
@@ -14997,7 +15622,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
14997
15622
|
if (mismatch) process.exitCode = 1;
|
|
14998
15623
|
});
|
|
14999
15624
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
15000
|
-
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").requiredOption("--priority <priority>", "urgent | high | medium | low (
|
|
15625
|
+
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").requiredOption("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment").action(async (o) => {
|
|
15001
15626
|
let args;
|
|
15002
15627
|
let priority;
|
|
15003
15628
|
let body;
|
|
@@ -15082,12 +15707,12 @@ issue.command("link-child <parent> <child>").description("link an existing issue
|
|
|
15082
15707
|
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
15083
15708
|
}
|
|
15084
15709
|
});
|
|
15085
|
-
program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (board Priority field
|
|
15710
|
+
program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
|
|
15086
15711
|
let body;
|
|
15087
15712
|
let priority;
|
|
15088
15713
|
let args;
|
|
15089
15714
|
let title;
|
|
15090
|
-
const targetRepo2 = o.repo ??
|
|
15715
|
+
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
15091
15716
|
const sourceRepo = await resolveRepo(void 0);
|
|
15092
15717
|
try {
|
|
15093
15718
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
|
|
@@ -15282,8 +15907,8 @@ grindCmd.command("estimate").description("Worst-case cost proxy (agent-call unit
|
|
|
15282
15907
|
console.log(`ceiling: ${GRIND_COST_CEILING} units \u2014 ${estimate.exceedsCeiling ? "EXCEEDS \u2192 ask human (cap/stuck path)" : "within"}`);
|
|
15283
15908
|
}
|
|
15284
15909
|
});
|
|
15285
|
-
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (board Priority field
|
|
15286
|
-
const targetRepo2 = o.repo ??
|
|
15910
|
+
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
|
|
15911
|
+
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
15287
15912
|
const sourceRepo = await resolveRepo(void 0);
|
|
15288
15913
|
const pluginSha = await resolvePluginSha();
|
|
15289
15914
|
let skill;
|
|
@@ -15354,6 +15979,111 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
15354
15979
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
15355
15980
|
console.log(JSON.stringify(created));
|
|
15356
15981
|
});
|
|
15982
|
+
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
15983
|
+
const wfDir = (0, import_node_path14.join)(cwd, ".github", "workflows");
|
|
15984
|
+
if (!(0, import_node_fs16.existsSync)(wfDir)) return [];
|
|
15985
|
+
return (0, import_node_fs16.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
15986
|
+
}
|
|
15987
|
+
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
15988
|
+
const repo = repoOpt ?? await resolveRepo();
|
|
15989
|
+
if (repo) {
|
|
15990
|
+
return resolveRepoMergeCiPolicy(repo, ciAuditDeps());
|
|
15991
|
+
}
|
|
15992
|
+
const workflowPaths = await listCiWorkflowPaths();
|
|
15993
|
+
return resolveMergeCiPolicy({ workflowPaths });
|
|
15994
|
+
}
|
|
15995
|
+
function ciAuditDeps() {
|
|
15996
|
+
const cfgPromise = loadConfig();
|
|
15997
|
+
return {
|
|
15998
|
+
client: defaultGitHubClient(),
|
|
15999
|
+
listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
|
|
16000
|
+
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise))
|
|
16001
|
+
};
|
|
16002
|
+
}
|
|
16003
|
+
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) => {
|
|
16004
|
+
const result = await resolveMergeCiPolicyForCheckout(o.repo);
|
|
16005
|
+
if (o.json) return printLine(JSON.stringify(result));
|
|
16006
|
+
printLine(`merge CI policy: ${result.policy} (${result.reason})`);
|
|
16007
|
+
});
|
|
16008
|
+
pr.command("checks-wait <number>").description("bounded wait for PR checks; skips immediately on no-ci repos (#1432)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
16009
|
+
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
16010
|
+
const result = await waitForPrChecks({
|
|
16011
|
+
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
16012
|
+
pollChecks: async () => {
|
|
16013
|
+
const { stdout } = await execFileP2("gh", ["pr", "checks", number, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS });
|
|
16014
|
+
return parseGhPrChecksOutput(stdout);
|
|
16015
|
+
},
|
|
16016
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
16017
|
+
});
|
|
16018
|
+
if (o.json) printLine(JSON.stringify(result));
|
|
16019
|
+
else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
16020
|
+
if (result.status === "failure" || result.status === "timeout") process.exitCode = 1;
|
|
16021
|
+
});
|
|
16022
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").action(async (number, o) => {
|
|
16023
|
+
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
16024
|
+
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
16025
|
+
resolveRepo: async (prNumber, repoOpt) => {
|
|
16026
|
+
if (repoOpt) return repoOpt;
|
|
16027
|
+
const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
16028
|
+
const [repo, base2] = viewed.split(/\s+/);
|
|
16029
|
+
if (base2 && base2 !== "development") {
|
|
16030
|
+
throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
|
|
16031
|
+
}
|
|
16032
|
+
if (!repo) throw new Error("pr land: could not resolve PR repo");
|
|
16033
|
+
return repo;
|
|
16034
|
+
},
|
|
16035
|
+
fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
|
|
16036
|
+
resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
16037
|
+
waitForChecks: (prNumber, repo) => waitForPrChecks({
|
|
16038
|
+
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
16039
|
+
pollChecks: async () => {
|
|
16040
|
+
const args = repo ? ["--repo", repo] : [];
|
|
16041
|
+
const { stdout } = await execFileP2("gh", ["pr", "checks", prNumber, ...args], { timeout: GC_GH_TIMEOUT_MS });
|
|
16042
|
+
return parseGhPrChecksOutput(stdout);
|
|
16043
|
+
},
|
|
16044
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
16045
|
+
}),
|
|
16046
|
+
mergeAuto: async (prNumber, repo) => {
|
|
16047
|
+
const args = repo ? ["--repo", repo] : [];
|
|
16048
|
+
try {
|
|
16049
|
+
await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method: "--squash", auto: true }), { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
16050
|
+
} catch (e) {
|
|
16051
|
+
const message = String(e.message || "");
|
|
16052
|
+
if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
|
|
16053
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
16054
|
+
if (note) return { mergeStatus: "failed", error: note };
|
|
16055
|
+
if (!basePolicyBlocksImmediateMerge(message)) {
|
|
16056
|
+
return { mergeStatus: "failed", error: message.split("\n")[0] };
|
|
16057
|
+
}
|
|
16058
|
+
return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
|
|
16059
|
+
}
|
|
16060
|
+
const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
16061
|
+
return { mergeStatus: state === "MERGED" ? "merged" : "auto-merge-enqueued" };
|
|
16062
|
+
},
|
|
16063
|
+
pollMerged: async (prNumber, repo, deadlineMs) => {
|
|
16064
|
+
const args = repo ? ["--repo", repo] : [];
|
|
16065
|
+
while (Date.now() < deadlineMs) {
|
|
16066
|
+
const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
16067
|
+
if (state === "MERGED") return true;
|
|
16068
|
+
await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
|
|
16069
|
+
}
|
|
16070
|
+
return false;
|
|
16071
|
+
}
|
|
16072
|
+
});
|
|
16073
|
+
if (o.json) printLine(JSON.stringify(result));
|
|
16074
|
+
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
16075
|
+
if (result.status === "failed") process.exitCode = 1;
|
|
16076
|
+
else {
|
|
16077
|
+
await execFileP2(process.execPath, [
|
|
16078
|
+
process.argv[1],
|
|
16079
|
+
"pr",
|
|
16080
|
+
"merge",
|
|
16081
|
+
number,
|
|
16082
|
+
...o.repo ? ["--repo", o.repo] : [],
|
|
16083
|
+
"--squash"
|
|
16084
|
+
], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
|
|
16085
|
+
}
|
|
16086
|
+
});
|
|
15357
16087
|
async function remoteBranchExists2(branch, options = {}) {
|
|
15358
16088
|
return checkRemoteBranchExists(branch, {
|
|
15359
16089
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
@@ -15362,7 +16092,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
15362
16092
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
15363
16093
|
async function createDeferredWorktreeStore() {
|
|
15364
16094
|
try {
|
|
15365
|
-
const { stdout } = await execFileP2("git", ["rev-parse", "--git-dir"], { timeout: GIT_TIMEOUT_MS });
|
|
16095
|
+
const { stdout } = await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS });
|
|
15366
16096
|
const registryPath = deferredWorktreesRegistryPath(stdout.trim());
|
|
15367
16097
|
return {
|
|
15368
16098
|
read: async () => {
|
|
@@ -15393,7 +16123,7 @@ function worktreeRemoveDeps(execGit) {
|
|
|
15393
16123
|
}
|
|
15394
16124
|
function teardownWorktreeStage(worktreePath) {
|
|
15395
16125
|
return runWorktreeStageTeardown(worktreePath, {
|
|
15396
|
-
hasStageState: (wt) => (0,
|
|
16126
|
+
hasStageState: (wt) => (0, import_node_fs16.existsSync)(stageStatePath(wt)),
|
|
15397
16127
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
15398
16128
|
listComposeProjects: async () => {
|
|
15399
16129
|
const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -15404,7 +16134,7 @@ function teardownWorktreeStage(worktreePath) {
|
|
|
15404
16134
|
}
|
|
15405
16135
|
});
|
|
15406
16136
|
}
|
|
15407
|
-
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").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) => {
|
|
16137
|
+
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) => {
|
|
15408
16138
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
15409
16139
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
15410
16140
|
const headRef = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
@@ -15456,7 +16186,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
15456
16186
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
15457
16187
|
beforeWorktrees,
|
|
15458
16188
|
startingPath,
|
|
15459
|
-
pathExists: (p) => (0,
|
|
16189
|
+
pathExists: (p) => (0, import_node_fs16.existsSync)(p),
|
|
15460
16190
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
15461
16191
|
teardownWorktreeStage,
|
|
15462
16192
|
deferredStore,
|
|
@@ -15571,6 +16301,25 @@ board.command("backfill-priority").description("set board Priority from priority
|
|
|
15571
16301
|
return failGraceful(`board backfill-priority failed: ${e.message}`);
|
|
15572
16302
|
}
|
|
15573
16303
|
});
|
|
16304
|
+
board.command("prune-priority-labels").description("remove retired priority:* labels (#416) from issues whose board Priority field is already set").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--dry-run", "report what would be removed without writing").option("--concurrency <n>", "parallel issue edits (default 8)", "8").action(async (o) => {
|
|
16305
|
+
try {
|
|
16306
|
+
const result = await prunePriorityLabels({
|
|
16307
|
+
config: await loadConfigForRepo(o.repo),
|
|
16308
|
+
repo: o.repo,
|
|
16309
|
+
dryRun: o.dryRun,
|
|
16310
|
+
concurrency: Number(o.concurrency) || 8
|
|
16311
|
+
});
|
|
16312
|
+
if (o.json) return console.log(JSON.stringify(result));
|
|
16313
|
+
console.log(
|
|
16314
|
+
`prune-priority-labels: scanned ${result.scanned}, pruned ${result.pruned} (${result.removedLabels} labels), skipped ${result.skippedNoField} (field unset), failed ${result.failed}`
|
|
16315
|
+
);
|
|
16316
|
+
for (const line of result.details.slice(0, 30)) console.log(` ${line}`);
|
|
16317
|
+
if (result.details.length > 30) console.log(` ... +${result.details.length - 30} more`);
|
|
16318
|
+
if (result.failed) process.exitCode = 1;
|
|
16319
|
+
} catch (e) {
|
|
16320
|
+
return failGraceful(`board prune-priority-labels failed: ${e.message}`);
|
|
16321
|
+
}
|
|
16322
|
+
});
|
|
15574
16323
|
board.command("done <issue>").description("set a board item's Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails").action(async (issueRef, o) => {
|
|
15575
16324
|
try {
|
|
15576
16325
|
const result = await moveBoardItem({ config: await loadConfigOrDiscover(), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
|
|
@@ -15609,7 +16358,7 @@ function rawValues(flag) {
|
|
|
15609
16358
|
return out;
|
|
15610
16359
|
}
|
|
15611
16360
|
function printLine(value) {
|
|
15612
|
-
(0,
|
|
16361
|
+
(0, import_node_fs16.writeSync)(1, `${value}
|
|
15613
16362
|
`);
|
|
15614
16363
|
}
|
|
15615
16364
|
function stageKeepAlive() {
|
|
@@ -15626,10 +16375,26 @@ async function resolveStage() {
|
|
|
15626
16375
|
local,
|
|
15627
16376
|
shell: shellFor(),
|
|
15628
16377
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
15629
|
-
hasCompose: (0,
|
|
15630
|
-
hasEnvExample: (0,
|
|
16378
|
+
hasCompose: (0, import_node_fs16.existsSync)((0, import_node_path14.join)(process.cwd(), "docker-compose.yml")),
|
|
16379
|
+
hasEnvExample: (0, import_node_fs16.existsSync)((0, import_node_path14.join)(process.cwd(), ".env.example"))
|
|
15631
16380
|
});
|
|
15632
16381
|
}
|
|
16382
|
+
async function fetchStageVaultEnvMerge() {
|
|
16383
|
+
const cfg = await loadConfig();
|
|
16384
|
+
if (!cfg.sagaApiUrl) return void 0;
|
|
16385
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch(() => null);
|
|
16386
|
+
if (!read?.ok || !read.project) return void 0;
|
|
16387
|
+
const names = requiredRuntimeSecretNames("dev", read.project.requiredRuntimeSecrets, { includeGoogleOAuth: false });
|
|
16388
|
+
if (!names.length) return void 0;
|
|
16389
|
+
const d = makeSecretsDeps(cfg);
|
|
16390
|
+
const merge = {};
|
|
16391
|
+
for (const name of names) {
|
|
16392
|
+
const key = name.includes("/") ? name : `dev/${name}`;
|
|
16393
|
+
const value = await fetchSecretValue(d, key, {});
|
|
16394
|
+
if (value != null) merge[name.includes("/") ? name.split("/").pop() : name] = value;
|
|
16395
|
+
}
|
|
16396
|
+
return Object.keys(merge).length ? merge : void 0;
|
|
16397
|
+
}
|
|
15633
16398
|
function stageStepsFor(res, stops = true) {
|
|
15634
16399
|
if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
|
|
15635
16400
|
if (res.source === "local") return stagePlan(res.config ?? {}, stops);
|
|
@@ -15761,6 +16526,7 @@ stage.command("start").description("start the configured local stage process and
|
|
|
15761
16526
|
}
|
|
15762
16527
|
if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
|
|
15763
16528
|
const cfg = res.config ?? res.derived.config;
|
|
16529
|
+
const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
|
|
15764
16530
|
try {
|
|
15765
16531
|
const hold = stageKeepAlive();
|
|
15766
16532
|
let printed = false;
|
|
@@ -15768,6 +16534,7 @@ stage.command("start").description("start the configured local stage process and
|
|
|
15768
16534
|
const result = await startStage(cfg, {
|
|
15769
16535
|
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
15770
16536
|
allowStaleEnv: o.allowStaleEnv,
|
|
16537
|
+
vaultEnvMerge,
|
|
15771
16538
|
onReady: (ready) => {
|
|
15772
16539
|
printed = true;
|
|
15773
16540
|
const reportUrl = reportedStageUrl(res, ready);
|
|
@@ -15795,6 +16562,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
15795
16562
|
}
|
|
15796
16563
|
if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
|
|
15797
16564
|
const cfg = res.config ?? res.derived.config;
|
|
16565
|
+
const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
|
|
15798
16566
|
try {
|
|
15799
16567
|
const hold = stageKeepAlive();
|
|
15800
16568
|
let printed = false;
|
|
@@ -15802,6 +16570,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
15802
16570
|
const result = await runStage(cfg, {
|
|
15803
16571
|
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
15804
16572
|
allowStaleEnv: o.allowStaleEnv,
|
|
16573
|
+
vaultEnvMerge,
|
|
15805
16574
|
onReady: (ready) => {
|
|
15806
16575
|
const reportUrl = reportedStageUrl(res, ready);
|
|
15807
16576
|
const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
|
|
@@ -15981,6 +16750,39 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
|
|
|
15981
16750
|
hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
|
|
15982
16751
|
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
|
|
15983
16752
|
hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
|
|
16753
|
+
var ci = program2.command("ci").description("org CI + merge-readiness audit and reconcile");
|
|
16754
|
+
ci.command("audit").description("read-only fleet scan: gate workflow, ruleset contexts, auto-merge, registry META (#1440)").option("--json", "machine-readable output").option("--markdown", "fleet summary table for issue comments").option("--repo <owner/repo>", "audit one repo instead of the full registry").action(async (o) => {
|
|
16755
|
+
const report = await auditOrgCi(ciAuditDeps(), o.repo);
|
|
16756
|
+
if (o.json) console.log(JSON.stringify(report, null, 2));
|
|
16757
|
+
else if (o.markdown) console.log(renderCiAuditMarkdown(report));
|
|
16758
|
+
else console.log(renderCiAuditText(report));
|
|
16759
|
+
if (!report.ok) process.exitCode = 1;
|
|
16760
|
+
});
|
|
16761
|
+
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required)").action(async (o) => {
|
|
16762
|
+
if (o.apply) {
|
|
16763
|
+
const verdict = await fetchTrainAuthority(HUB_REPO2, registryClientDeps(await loadConfig()));
|
|
16764
|
+
if (!verdict.ok || verdict.authority.role !== "master") {
|
|
16765
|
+
return fail("ci reconcile --apply: master-admin required");
|
|
16766
|
+
}
|
|
16767
|
+
}
|
|
16768
|
+
const deps = ciAuditDeps();
|
|
16769
|
+
const audit = await auditOrgCi(deps, o.repo);
|
|
16770
|
+
const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : [];
|
|
16771
|
+
const payload = { audit, apply: applyResults };
|
|
16772
|
+
if (o.json) console.log(JSON.stringify(payload, null, 2));
|
|
16773
|
+
else {
|
|
16774
|
+
console.log(renderCiAuditText(audit));
|
|
16775
|
+
if (o.apply) {
|
|
16776
|
+
for (const r of applyResults) {
|
|
16777
|
+
console.log(`
|
|
16778
|
+
${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
|
|
16779
|
+
}
|
|
16780
|
+
} else {
|
|
16781
|
+
console.log("\nDry-run \u2014 re-run with --apply to patch merge settings and activate product rulesets (master-admin).");
|
|
16782
|
+
}
|
|
16783
|
+
}
|
|
16784
|
+
if (!audit.ok) process.exitCode = 1;
|
|
16785
|
+
});
|
|
15984
16786
|
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) => {
|
|
15985
16787
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
15986
16788
|
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|
|
@@ -15999,7 +16801,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
15999
16801
|
const report = await verifyBootstrap(repo, o.class, {
|
|
16000
16802
|
client: defaultGitHubClient(),
|
|
16001
16803
|
projectMeta: meta,
|
|
16002
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
16804
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs16.existsSync)(path2) ? (0, import_node_fs16.readFileSync)(path2, "utf8") : null,
|
|
16003
16805
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
16004
16806
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
16005
16807
|
requiredGcpApis: (() => {
|
|
@@ -16042,12 +16844,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16042
16844
|
return fail(`bootstrap apply: ${e.message}`);
|
|
16043
16845
|
}
|
|
16044
16846
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
16045
|
-
if (!(0,
|
|
16046
|
-
const manifest = loadBootstrapSeeds((0,
|
|
16847
|
+
if (!(0, import_node_fs16.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
16848
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs16.readFileSync)(manifestPath, "utf8"));
|
|
16047
16849
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
16048
16850
|
const slug = parsedRepo.slug;
|
|
16049
16851
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
16050
|
-
const readFile5 = (p) => (0,
|
|
16852
|
+
const readFile5 = (p) => (0, import_node_fs16.existsSync)(p) ? (0, import_node_fs16.readFileSync)(p, "utf8") : null;
|
|
16051
16853
|
const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
16052
16854
|
const rawVars = {};
|
|
16053
16855
|
for (const value of rawValues("--var")) {
|
|
@@ -16097,6 +16899,26 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16097
16899
|
applied.push(`${action.action} ${resolved.target}`);
|
|
16098
16900
|
}
|
|
16099
16901
|
}
|
|
16902
|
+
if (o.execute && o.class === "deployable") {
|
|
16903
|
+
try {
|
|
16904
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
|
|
16905
|
+
applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
|
|
16906
|
+
} catch (e) {
|
|
16907
|
+
applied.push(`merge settings (failed: ${e.message})`);
|
|
16908
|
+
}
|
|
16909
|
+
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
16910
|
+
if (rulesetSeed) {
|
|
16911
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile5);
|
|
16912
|
+
if (rulesetContent) {
|
|
16913
|
+
try {
|
|
16914
|
+
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
16915
|
+
applied.push(`product ruleset: ${activation.action}${activation.detail ? ` (${activation.detail})` : ""}`);
|
|
16916
|
+
} catch (e) {
|
|
16917
|
+
return failGraceful(`bootstrap apply: product ruleset activation failed: ${e.message}`);
|
|
16918
|
+
}
|
|
16919
|
+
}
|
|
16920
|
+
}
|
|
16921
|
+
}
|
|
16100
16922
|
if (o.execute) {
|
|
16101
16923
|
for (const l of manifest.labels) {
|
|
16102
16924
|
try {
|
|
@@ -16150,10 +16972,10 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16150
16972
|
name: vars.NAME || parsedRepo.name
|
|
16151
16973
|
};
|
|
16152
16974
|
const readHubFile = async (path2) => {
|
|
16153
|
-
const r = await gh(["api", `repos/${
|
|
16975
|
+
const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=development`]);
|
|
16154
16976
|
const parsed = JSON.parse(r.stdout);
|
|
16155
16977
|
if (parsed.encoding !== "base64" || typeof parsed.content !== "string" || !parsed.sha) {
|
|
16156
|
-
throw new Error(`could not read ${
|
|
16978
|
+
throw new Error(`could not read ${HUB_REPO2}/${path2}`);
|
|
16157
16979
|
}
|
|
16158
16980
|
return { content: Buffer.from(parsed.content, "base64").toString("utf8"), sha: parsed.sha };
|
|
16159
16981
|
};
|
|
@@ -16165,15 +16987,15 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16165
16987
|
applied.push(`fanout: already registered (${parsedRepo.name})`);
|
|
16166
16988
|
} else {
|
|
16167
16989
|
const branchName = `bootstrap-register-fanout-${slug}`;
|
|
16168
|
-
const headSha = (await gh(["api", `repos/${
|
|
16990
|
+
const headSha = (await gh(["api", `repos/${HUB_REPO2}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
|
|
16169
16991
|
try {
|
|
16170
|
-
await gh(["api", "-X", "POST", `repos/${
|
|
16992
|
+
await gh(["api", "-X", "POST", `repos/${HUB_REPO2}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
|
|
16171
16993
|
} catch (e) {
|
|
16172
16994
|
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
16173
16995
|
}
|
|
16174
16996
|
const branchFileSha = async (path2) => {
|
|
16175
16997
|
try {
|
|
16176
|
-
const r = await gh(["api", `repos/${
|
|
16998
|
+
const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=${branchName}`, "--jq", ".sha"]);
|
|
16177
16999
|
return r.stdout.trim() || void 0;
|
|
16178
17000
|
} catch (e) {
|
|
16179
17001
|
if (/404|Not Found/i.test(String(e.message ?? ""))) return void 0;
|
|
@@ -16182,9 +17004,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16182
17004
|
};
|
|
16183
17005
|
const fanoutBranchSha = await branchFileSha(".github/fanout-targets.json");
|
|
16184
17006
|
const projectsBranchSha = await branchFileSha("projects.json");
|
|
16185
|
-
await gh(contentPutArgs(
|
|
16186
|
-
await gh(contentPutArgs(
|
|
16187
|
-
const openPrs = await gh(["pr", "list", "--repo",
|
|
17007
|
+
await gh(contentPutArgs(HUB_REPO2, ".github/fanout-targets.json", plan2.fanoutTargets, branchName, fanoutBranchSha));
|
|
17008
|
+
await gh(contentPutArgs(HUB_REPO2, "projects.json", plan2.projects, branchName, projectsBranchSha));
|
|
17009
|
+
const openPrs = await gh(["pr", "list", "--repo", HUB_REPO2, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
|
|
16188
17010
|
const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
16189
17011
|
if (prDecision.action === "reuse") {
|
|
16190
17012
|
fanoutPrUrl = prDecision.url;
|
|
@@ -16193,7 +17015,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16193
17015
|
"pr",
|
|
16194
17016
|
"create",
|
|
16195
17017
|
"--repo",
|
|
16196
|
-
|
|
17018
|
+
HUB_REPO2,
|
|
16197
17019
|
"--base",
|
|
16198
17020
|
"development",
|
|
16199
17021
|
"--head",
|
|
@@ -16205,7 +17027,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16205
17027
|
]);
|
|
16206
17028
|
fanoutPrUrl = created.url;
|
|
16207
17029
|
}
|
|
16208
|
-
await gh(["pr", "merge", fanoutPrUrl, "--repo",
|
|
17030
|
+
await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO2, "--auto", "--squash"]).catch((e) => {
|
|
16209
17031
|
if (!/already/i.test(String(e.message ?? ""))) throw e;
|
|
16210
17032
|
});
|
|
16211
17033
|
applied.push(`fanout: PR ${fanoutPrUrl} (auto-merge enabled)`);
|
|
@@ -16256,16 +17078,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
16256
17078
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
16257
17079
|
targets = [{ repo: o.repo, class: o.class }];
|
|
16258
17080
|
} else {
|
|
16259
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
17081
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs16.existsSync)("projects.json") ? (0, import_node_fs16.readFileSync)("projects.json", "utf8") : null;
|
|
16260
17082
|
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>");
|
|
16261
|
-
const fanoutJson = (0,
|
|
17083
|
+
const fanoutJson = (0, import_node_fs16.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs16.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
16262
17084
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
16263
17085
|
}
|
|
16264
17086
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
16265
|
-
const fileMatrix = (0,
|
|
17087
|
+
const fileMatrix = (0, import_node_fs16.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs16.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
16266
17088
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
16267
17089
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
16268
|
-
const fileContracts = (0,
|
|
17090
|
+
const fileContracts = (0, import_node_fs16.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs16.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
16269
17091
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
16270
17092
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
16271
17093
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -16278,7 +17100,7 @@ var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
|
16278
17100
|
};
|
|
16279
17101
|
function readInstalledPlugins() {
|
|
16280
17102
|
try {
|
|
16281
|
-
return JSON.parse((0,
|
|
17103
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(installedPluginsPath(), "utf8"));
|
|
16282
17104
|
} catch {
|
|
16283
17105
|
return null;
|
|
16284
17106
|
}
|
|
@@ -16287,7 +17109,7 @@ function installedPluginSources() {
|
|
|
16287
17109
|
return ["claude", "codex"].map((surface) => {
|
|
16288
17110
|
const recordPath = (0, import_node_path14.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
16289
17111
|
try {
|
|
16290
|
-
return { surface, installed: JSON.parse((0,
|
|
17112
|
+
return { surface, installed: JSON.parse((0, import_node_fs16.readFileSync)(recordPath, "utf8")), recordPath };
|
|
16291
17113
|
} catch {
|
|
16292
17114
|
return { surface, installed: null, recordPath };
|
|
16293
17115
|
}
|
|
@@ -16295,7 +17117,7 @@ function installedPluginSources() {
|
|
|
16295
17117
|
}
|
|
16296
17118
|
function readClaudeSettings() {
|
|
16297
17119
|
try {
|
|
16298
|
-
return JSON.parse((0,
|
|
17120
|
+
return JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
16299
17121
|
} catch {
|
|
16300
17122
|
return null;
|
|
16301
17123
|
}
|
|
@@ -16317,7 +17139,7 @@ function writeProjectInstallRecord(record) {
|
|
|
16317
17139
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
16318
17140
|
list.push(record);
|
|
16319
17141
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
16320
|
-
(0,
|
|
17142
|
+
(0, import_node_fs16.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
16321
17143
|
`, "utf8");
|
|
16322
17144
|
return true;
|
|
16323
17145
|
} catch {
|
|
@@ -16330,9 +17152,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
16330
17152
|
if (!file) return false;
|
|
16331
17153
|
if (!file.plugins) file.plugins = {};
|
|
16332
17154
|
const path2 = installedPluginsPath();
|
|
16333
|
-
(0,
|
|
17155
|
+
(0, import_node_fs16.copyFileSync)(path2, `${path2}.bak`);
|
|
16334
17156
|
file.plugins[pluginId] = records;
|
|
16335
|
-
(0,
|
|
17157
|
+
(0, import_node_fs16.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
16336
17158
|
`, "utf8");
|
|
16337
17159
|
return true;
|
|
16338
17160
|
} catch {
|
|
@@ -16345,30 +17167,30 @@ function cursorPluginCacheRoot() {
|
|
|
16345
17167
|
function cursorPluginCachePinSnapshots() {
|
|
16346
17168
|
const root = cursorPluginCacheRoot();
|
|
16347
17169
|
try {
|
|
16348
|
-
return (0,
|
|
17170
|
+
return (0, import_node_fs16.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
16349
17171
|
const path2 = (0, import_node_path14.join)(root, entry.name);
|
|
16350
17172
|
const pluginJson = (0, import_node_path14.join)(path2, ".cursor-plugin", "plugin.json");
|
|
16351
17173
|
const hooksJson = (0, import_node_path14.join)(path2, "hooks", "hooks.json");
|
|
16352
17174
|
const cliBundle = (0, import_node_path14.join)(path2, "cli", "dist", "index.cjs");
|
|
16353
17175
|
let version;
|
|
16354
17176
|
try {
|
|
16355
|
-
const raw = JSON.parse((0,
|
|
17177
|
+
const raw = JSON.parse((0, import_node_fs16.readFileSync)(pluginJson, "utf8"));
|
|
16356
17178
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
16357
17179
|
} catch {
|
|
16358
17180
|
version = void 0;
|
|
16359
17181
|
}
|
|
16360
17182
|
let isEmpty = true;
|
|
16361
17183
|
try {
|
|
16362
|
-
isEmpty = (0,
|
|
17184
|
+
isEmpty = (0, import_node_fs16.readdirSync)(path2).length === 0;
|
|
16363
17185
|
} catch {
|
|
16364
17186
|
isEmpty = true;
|
|
16365
17187
|
}
|
|
16366
17188
|
return {
|
|
16367
17189
|
name: entry.name,
|
|
16368
17190
|
path: path2,
|
|
16369
|
-
hasPluginJson: (0,
|
|
16370
|
-
hasHooksJson: (0,
|
|
16371
|
-
hasCliBundle: (0,
|
|
17191
|
+
hasPluginJson: (0, import_node_fs16.existsSync)(pluginJson),
|
|
17192
|
+
hasHooksJson: (0, import_node_fs16.existsSync)(hooksJson),
|
|
17193
|
+
hasCliBundle: (0, import_node_fs16.existsSync)(cliBundle),
|
|
16372
17194
|
isEmpty,
|
|
16373
17195
|
version
|
|
16374
17196
|
};
|
|
@@ -16379,7 +17201,7 @@ function cursorPluginCachePinSnapshots() {
|
|
|
16379
17201
|
}
|
|
16380
17202
|
function hubCheckoutForCursorSeed() {
|
|
16381
17203
|
const manifest = (0, import_node_path14.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
16382
|
-
return (0,
|
|
17204
|
+
return (0, import_node_fs16.existsSync)(manifest) ? process.cwd() : void 0;
|
|
16383
17205
|
}
|
|
16384
17206
|
function mmiPluginCacheRootSnapshots() {
|
|
16385
17207
|
const roots = [
|
|
@@ -16388,7 +17210,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
16388
17210
|
];
|
|
16389
17211
|
return roots.flatMap(({ surface, root }) => {
|
|
16390
17212
|
try {
|
|
16391
|
-
const entries = (0,
|
|
17213
|
+
const entries = (0, import_node_fs16.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
16392
17214
|
name: entry.name,
|
|
16393
17215
|
path: (0, import_node_path14.join)(root, entry.name),
|
|
16394
17216
|
isDirectory: entry.isDirectory()
|
|
@@ -16401,7 +17223,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
16401
17223
|
}
|
|
16402
17224
|
function hasNestedMmiChild(versionDir) {
|
|
16403
17225
|
try {
|
|
16404
|
-
return (0,
|
|
17226
|
+
return (0, import_node_fs16.statSync)((0, import_node_path14.join)(versionDir, "mmi")).isDirectory();
|
|
16405
17227
|
} catch {
|
|
16406
17228
|
return false;
|
|
16407
17229
|
}
|
|
@@ -16412,10 +17234,10 @@ function nestedPluginTreeSnapshot() {
|
|
|
16412
17234
|
);
|
|
16413
17235
|
}
|
|
16414
17236
|
function uniqueQuarantineTarget(path2) {
|
|
16415
|
-
if (!(0,
|
|
17237
|
+
if (!(0, import_node_fs16.existsSync)(path2)) return path2;
|
|
16416
17238
|
for (let i = 1; i < 100; i += 1) {
|
|
16417
17239
|
const candidate = `${path2}-${i}`;
|
|
16418
|
-
if (!(0,
|
|
17240
|
+
if (!(0, import_node_fs16.existsSync)(candidate)) return candidate;
|
|
16419
17241
|
}
|
|
16420
17242
|
return `${path2}-${Date.now()}`;
|
|
16421
17243
|
}
|
|
@@ -16423,10 +17245,10 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
16423
17245
|
let moved = 0;
|
|
16424
17246
|
for (const move of plan2) {
|
|
16425
17247
|
try {
|
|
16426
|
-
if (!(0,
|
|
17248
|
+
if (!(0, import_node_fs16.existsSync)(move.from)) continue;
|
|
16427
17249
|
const target = uniqueQuarantineTarget(move.to);
|
|
16428
|
-
(0,
|
|
16429
|
-
(0,
|
|
17250
|
+
(0, import_node_fs16.mkdirSync)((0, import_node_path14.dirname)(target), { recursive: true });
|
|
17251
|
+
(0, import_node_fs16.renameSync)(move.from, target);
|
|
16430
17252
|
moved += 1;
|
|
16431
17253
|
} catch {
|
|
16432
17254
|
}
|
|
@@ -16444,23 +17266,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
16444
17266
|
}
|
|
16445
17267
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
16446
17268
|
try {
|
|
16447
|
-
if (!(0,
|
|
17269
|
+
if (!(0, import_node_fs16.existsSync)(targetPath)) return true;
|
|
16448
17270
|
if (isWin) {
|
|
16449
17271
|
const emptyDir = (0, import_node_path14.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
16450
|
-
(0,
|
|
17272
|
+
(0, import_node_fs16.mkdirSync)(emptyDir, { recursive: true });
|
|
16451
17273
|
try {
|
|
16452
17274
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
16453
|
-
(0,
|
|
17275
|
+
(0, import_node_fs16.rmSync)(targetPath, { recursive: true, force: true });
|
|
16454
17276
|
} finally {
|
|
16455
17277
|
try {
|
|
16456
|
-
(0,
|
|
17278
|
+
(0, import_node_fs16.rmSync)(emptyDir, { recursive: true, force: true });
|
|
16457
17279
|
} catch {
|
|
16458
17280
|
}
|
|
16459
17281
|
}
|
|
16460
|
-
return !(0,
|
|
17282
|
+
return !(0, import_node_fs16.existsSync)(targetPath);
|
|
16461
17283
|
}
|
|
16462
|
-
(0,
|
|
16463
|
-
return !(0,
|
|
17284
|
+
(0, import_node_fs16.rmSync)(targetPath, { recursive: true, force: true });
|
|
17285
|
+
return !(0, import_node_fs16.existsSync)(targetPath);
|
|
16464
17286
|
} catch {
|
|
16465
17287
|
return false;
|
|
16466
17288
|
}
|
|
@@ -16476,8 +17298,8 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
16476
17298
|
var gitignorePath = () => (0, import_node_path14.join)(process.cwd(), ".gitignore");
|
|
16477
17299
|
function readTextFile(path2) {
|
|
16478
17300
|
try {
|
|
16479
|
-
if (!(0,
|
|
16480
|
-
return (0,
|
|
17301
|
+
if (!(0, import_node_fs16.existsSync)(path2)) return null;
|
|
17302
|
+
return (0, import_node_fs16.readFileSync)(path2, "utf8");
|
|
16481
17303
|
} catch {
|
|
16482
17304
|
return null;
|
|
16483
17305
|
}
|
|
@@ -16501,7 +17323,7 @@ function strayBrowserArtifactPaths() {
|
|
|
16501
17323
|
const cwd = process.cwd();
|
|
16502
17324
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
16503
17325
|
try {
|
|
16504
|
-
return (0,
|
|
17326
|
+
return (0, import_node_fs16.existsSync)((0, import_node_path14.join)(cwd, rel));
|
|
16505
17327
|
} catch {
|
|
16506
17328
|
return false;
|
|
16507
17329
|
}
|
|
@@ -16509,14 +17331,14 @@ function strayBrowserArtifactPaths() {
|
|
|
16509
17331
|
}
|
|
16510
17332
|
function readGitignore() {
|
|
16511
17333
|
try {
|
|
16512
|
-
return (0,
|
|
17334
|
+
return (0, import_node_fs16.readFileSync)(gitignorePath(), "utf8");
|
|
16513
17335
|
} catch {
|
|
16514
17336
|
return null;
|
|
16515
17337
|
}
|
|
16516
17338
|
}
|
|
16517
17339
|
function writeGitignore(content) {
|
|
16518
17340
|
try {
|
|
16519
|
-
(0,
|
|
17341
|
+
(0, import_node_fs16.writeFileSync)(gitignorePath(), content, "utf8");
|
|
16520
17342
|
return true;
|
|
16521
17343
|
} catch {
|
|
16522
17344
|
return false;
|
|
@@ -16555,7 +17377,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16555
17377
|
let onPath = pathProbe;
|
|
16556
17378
|
if (!onPath) {
|
|
16557
17379
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
16558
|
-
if (root && (0,
|
|
17380
|
+
if (root && (0, import_node_fs16.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
16559
17381
|
}
|
|
16560
17382
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
16561
17383
|
const surface = detectSurface(process.env);
|
|
@@ -16649,6 +17471,19 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16649
17471
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16650
17472
|
}
|
|
16651
17473
|
}
|
|
17474
|
+
const codexStale = installedVersionCheck.staleSurfaces?.some((s) => s.surface === "codex") ?? false;
|
|
17475
|
+
if (!installedVersionCheck.ok && codexStale && await applyCodexPluginHeal(surface, (m) => io.err(m))) {
|
|
17476
|
+
const healed = buildInstalledPluginVersionCheck({
|
|
17477
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
17478
|
+
sources: installedPluginSources(),
|
|
17479
|
+
releasedVersion,
|
|
17480
|
+
surface
|
|
17481
|
+
});
|
|
17482
|
+
installedVersionCheck = healed;
|
|
17483
|
+
if (healed.ok) {
|
|
17484
|
+
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
17485
|
+
}
|
|
17486
|
+
}
|
|
16652
17487
|
}
|
|
16653
17488
|
checks.push(installedVersionCheck);
|
|
16654
17489
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
@@ -16709,7 +17544,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16709
17544
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
16710
17545
|
surface,
|
|
16711
17546
|
cacheRoot: cursorCacheRoot,
|
|
16712
|
-
cacheRootExists: (0,
|
|
17547
|
+
cacheRootExists: (0, import_node_fs16.existsSync)(cursorCacheRoot),
|
|
16713
17548
|
pins: cursorPins,
|
|
16714
17549
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
16715
17550
|
releasedVersion
|
|
@@ -16729,7 +17564,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16729
17564
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
16730
17565
|
surface,
|
|
16731
17566
|
cacheRoot: cursorCacheRoot,
|
|
16732
|
-
cacheRootExists: (0,
|
|
17567
|
+
cacheRootExists: (0, import_node_fs16.existsSync)(cursorCacheRoot),
|
|
16733
17568
|
pins: cursorPins,
|
|
16734
17569
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
16735
17570
|
releasedVersion
|