@mutmutco/cli 2.35.0 → 2.37.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 +2 -0
- package/dist/main.cjs +2173 -790
- package/dist/saga.cjs +22 -14
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -2160,10 +2160,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2160
2160
|
_executeSubCommand(subcommand, args) {
|
|
2161
2161
|
args = args.slice();
|
|
2162
2162
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2163
|
-
function findFile(baseDir,
|
|
2164
|
-
const localBin = import_node_path.default.resolve(baseDir,
|
|
2163
|
+
function findFile(baseDir, baseName2) {
|
|
2164
|
+
const localBin = import_node_path.default.resolve(baseDir, baseName2);
|
|
2165
2165
|
if (import_node_fs.default.existsSync(localBin)) return localBin;
|
|
2166
|
-
if (sourceExt.includes(import_node_path.default.extname(
|
|
2166
|
+
if (sourceExt.includes(import_node_path.default.extname(baseName2))) return void 0;
|
|
2167
2167
|
const foundExt = sourceExt.find(
|
|
2168
2168
|
(ext) => import_node_fs.default.existsSync(`${localBin}${ext}`)
|
|
2169
2169
|
);
|
|
@@ -3391,8 +3391,8 @@ function useColor() {
|
|
|
3391
3391
|
var program = new Command();
|
|
3392
3392
|
|
|
3393
3393
|
// src/index.ts
|
|
3394
|
-
var
|
|
3395
|
-
var
|
|
3394
|
+
var import_promises6 = require("node:fs/promises");
|
|
3395
|
+
var import_node_fs19 = require("node:fs");
|
|
3396
3396
|
|
|
3397
3397
|
// src/rules-sync.ts
|
|
3398
3398
|
function normalizeEol(s) {
|
|
@@ -3833,6 +3833,7 @@ function createGitHubClient(options = {}) {
|
|
|
3833
3833
|
const token = options.token ?? githubToken;
|
|
3834
3834
|
const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3835
3835
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
3836
|
+
const externalSignal = options.signal;
|
|
3836
3837
|
async function request2(method, url, init = {}) {
|
|
3837
3838
|
const t = await token();
|
|
3838
3839
|
const headers = {
|
|
@@ -3843,11 +3844,12 @@ function createGitHubClient(options = {}) {
|
|
|
3843
3844
|
...init.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
3844
3845
|
...init.headers
|
|
3845
3846
|
};
|
|
3847
|
+
const timeoutSignal = AbortSignal.timeout(init.timeoutMs ?? defaultTimeoutMs);
|
|
3846
3848
|
const res = await fetchImpl(url, {
|
|
3847
3849
|
method,
|
|
3848
3850
|
headers,
|
|
3849
3851
|
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
3850
|
-
signal: AbortSignal.
|
|
3852
|
+
signal: externalSignal ? AbortSignal.any([externalSignal, timeoutSignal]) : timeoutSignal
|
|
3851
3853
|
});
|
|
3852
3854
|
if (!res.ok) throw await errorFromResponse(res);
|
|
3853
3855
|
return res;
|
|
@@ -3905,9 +3907,9 @@ var HEAD_MIN_INTERVAL_MS = 5 * 60 * 1e3;
|
|
|
3905
3907
|
var HEAD_ENGINE_TIMEOUT_MS = 15e3;
|
|
3906
3908
|
var HEAD_PROMPT_ACTION_LIMIT = 50;
|
|
3907
3909
|
var HEAD_PROMPT_DECISION_LIMIT = 80;
|
|
3908
|
-
function resolveEngine(
|
|
3910
|
+
function resolveEngine(platform2, custom) {
|
|
3909
3911
|
if (custom) return { cmd: custom, args: [], shell: true };
|
|
3910
|
-
return { cmd: "claude", args: ["-p", "--no-session-persistence"], shell:
|
|
3912
|
+
return { cmd: "claude", args: ["-p", "--no-session-persistence"], shell: platform2 === "win32" };
|
|
3911
3913
|
}
|
|
3912
3914
|
function headTsPath(key) {
|
|
3913
3915
|
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -4263,6 +4265,13 @@ var import_node_crypto3 = require("node:crypto");
|
|
|
4263
4265
|
var import_promises2 = require("node:fs/promises");
|
|
4264
4266
|
|
|
4265
4267
|
// src/issue-body.ts
|
|
4268
|
+
var import_node_os2 = require("node:os");
|
|
4269
|
+
function emptyStdinMessage(fileFlag) {
|
|
4270
|
+
if ((0, import_node_os2.platform)() === "win32") {
|
|
4271
|
+
return `${fileFlag} - read empty stdin (on Windows, ${fileFlag} - is unreliable through the npm .cmd shim \u2014 use ${fileFlag} <path>, or pipe to \`node cli/dist/index.cjs\` directly)`;
|
|
4272
|
+
}
|
|
4273
|
+
return `${fileFlag} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${fileFlag} <path>)`;
|
|
4274
|
+
}
|
|
4266
4275
|
async function resolveTextArg(input, deps, labels) {
|
|
4267
4276
|
const hasValue = input.value !== void 0;
|
|
4268
4277
|
const hasFile = input.file !== void 0;
|
|
@@ -4277,7 +4286,7 @@ async function resolveTextArg(input, deps, labels) {
|
|
|
4277
4286
|
const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
|
|
4278
4287
|
if (text.trim().length === 0) {
|
|
4279
4288
|
throw new Error(
|
|
4280
|
-
source === "-" ?
|
|
4289
|
+
source === "-" ? emptyStdinMessage(labels.file) : `${labels.file} produced an empty ${labels.noun}`
|
|
4281
4290
|
);
|
|
4282
4291
|
}
|
|
4283
4292
|
return text;
|
|
@@ -4299,7 +4308,7 @@ function resolveIssueTitle(input, deps) {
|
|
|
4299
4308
|
|
|
4300
4309
|
// src/saga-capture.ts
|
|
4301
4310
|
var import_node_fs8 = require("node:fs");
|
|
4302
|
-
var
|
|
4311
|
+
var import_node_os3 = require("node:os");
|
|
4303
4312
|
var import_node_path6 = require("node:path");
|
|
4304
4313
|
function parseHookInput(stdin) {
|
|
4305
4314
|
try {
|
|
@@ -4315,7 +4324,17 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
4315
4324
|
return p.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
4316
4325
|
}
|
|
4317
4326
|
function cursorProjectsRoot(env) {
|
|
4318
|
-
return (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0,
|
|
4327
|
+
return (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".cursor", "projects");
|
|
4328
|
+
}
|
|
4329
|
+
function resolveClaudeTranscriptPath(hook, env = process.env) {
|
|
4330
|
+
const sessionId = hook.session_id?.trim();
|
|
4331
|
+
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return void 0;
|
|
4332
|
+
const cwd = hook.cwd?.trim() || env.CLAUDE_PROJECT_DIR?.trim();
|
|
4333
|
+
if (!cwd) return void 0;
|
|
4334
|
+
const encoded = cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
4335
|
+
const root = (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".claude", "projects");
|
|
4336
|
+
const candidate = (0, import_node_path6.join)(root, encoded, `${sessionId}.jsonl`);
|
|
4337
|
+
return (0, import_node_fs8.existsSync)(candidate) ? candidate : void 0;
|
|
4319
4338
|
}
|
|
4320
4339
|
function resolveCursorTranscriptPath(hook, env = process.env) {
|
|
4321
4340
|
const conversationId = (hook.conversation_id ?? hook.conversationId ?? hook.session_id)?.trim();
|
|
@@ -4339,7 +4358,7 @@ function resolveTranscriptPath(hook, env = process.env) {
|
|
|
4339
4358
|
if (surface === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) {
|
|
4340
4359
|
return resolveCursorTranscriptPath(hook, env);
|
|
4341
4360
|
}
|
|
4342
|
-
return
|
|
4361
|
+
return resolveClaudeTranscriptPath(hook, env);
|
|
4343
4362
|
}
|
|
4344
4363
|
function ingestTranscriptFallbackHint(surface) {
|
|
4345
4364
|
if (surface === "cursor") {
|
|
@@ -4348,6 +4367,9 @@ function ingestTranscriptFallbackHint(surface) {
|
|
|
4348
4367
|
if (surface === "codex") {
|
|
4349
4368
|
return 'Codex Stop hook may omit transcript_path \u2014 run: mmi-cli honcho ingest --source note --summary "<turn summary>"';
|
|
4350
4369
|
}
|
|
4370
|
+
if (surface === "claude") {
|
|
4371
|
+
return 'Claude Stop hook omitted transcript_path and no transcript file was found \u2014 run: mmi-cli honcho ingest --source note --summary "<turn summary>"';
|
|
4372
|
+
}
|
|
4351
4373
|
return void 0;
|
|
4352
4374
|
}
|
|
4353
4375
|
|
|
@@ -4447,6 +4469,7 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
4447
4469
|
next: o.next,
|
|
4448
4470
|
decision: o.decision,
|
|
4449
4471
|
queueOp,
|
|
4472
|
+
handoffClose: o.handoffClose,
|
|
4450
4473
|
state,
|
|
4451
4474
|
source,
|
|
4452
4475
|
evidence: Object.keys(ev).length ? ev : void 0,
|
|
@@ -5080,9 +5103,276 @@ function registerSagaCommands(program3) {
|
|
|
5080
5103
|
});
|
|
5081
5104
|
}
|
|
5082
5105
|
|
|
5106
|
+
// src/handoff-commands.ts
|
|
5107
|
+
var import_node_crypto4 = require("node:crypto");
|
|
5108
|
+
var import_promises3 = require("node:fs/promises");
|
|
5109
|
+
|
|
5110
|
+
// src/handoff.ts
|
|
5111
|
+
var HANDOFF_PREFIX = "handoff:";
|
|
5112
|
+
function clean(value) {
|
|
5113
|
+
return value?.trim() ?? "";
|
|
5114
|
+
}
|
|
5115
|
+
function normalizeHandoffKey(value) {
|
|
5116
|
+
const key = clean(value);
|
|
5117
|
+
if (!key) throw new Error("handoff key is required");
|
|
5118
|
+
return key;
|
|
5119
|
+
}
|
|
5120
|
+
function serializeHandoff(record) {
|
|
5121
|
+
return `${HANDOFF_PREFIX}${JSON.stringify(record)}`;
|
|
5122
|
+
}
|
|
5123
|
+
function parseHandoffText(text) {
|
|
5124
|
+
if (!text.startsWith(HANDOFF_PREFIX)) return null;
|
|
5125
|
+
try {
|
|
5126
|
+
const raw = JSON.parse(text.slice(HANDOFF_PREFIX.length));
|
|
5127
|
+
const state = raw.state;
|
|
5128
|
+
const key = clean(raw.key);
|
|
5129
|
+
const northStarSlug = clean(raw.northStarSlug);
|
|
5130
|
+
const summary = clean(raw.summary);
|
|
5131
|
+
const sourceSessionId = clean(raw.sourceSessionId);
|
|
5132
|
+
const createdAt = clean(raw.createdAt);
|
|
5133
|
+
if (state !== "open" && state !== "claimed" && state !== "cancelled") return null;
|
|
5134
|
+
if (!key || !northStarSlug || !summary || !sourceSessionId || !createdAt) return null;
|
|
5135
|
+
return {
|
|
5136
|
+
state,
|
|
5137
|
+
key,
|
|
5138
|
+
northStarSlug,
|
|
5139
|
+
summary,
|
|
5140
|
+
sourceSessionId,
|
|
5141
|
+
createdAt,
|
|
5142
|
+
claimedBySessionId: clean(raw.claimedBySessionId) || void 0,
|
|
5143
|
+
closedAt: clean(raw.closedAt) || void 0
|
|
5144
|
+
};
|
|
5145
|
+
} catch {
|
|
5146
|
+
return null;
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
function listHandoffs(head, opts = {}) {
|
|
5150
|
+
const parsed = (head.queued ?? []).map((item, index) => ({ done: item.done, index, record: parseHandoffText(item.text) })).filter((x) => x.record !== null);
|
|
5151
|
+
return parsed.filter((x) => opts.includeClosed || x.record.state === "open" && !x.done).map((x) => ({ index: x.index, done: x.done, record: x.record }));
|
|
5152
|
+
}
|
|
5153
|
+
function findOpenHandoff(head, key) {
|
|
5154
|
+
const normalized = normalizeHandoffKey(key).toLowerCase();
|
|
5155
|
+
return listHandoffs(head).find((item) => item.record.key.toLowerCase() === normalized || item.record.northStarSlug.toLowerCase() === normalized);
|
|
5156
|
+
}
|
|
5157
|
+
function planOpenHandoff(input) {
|
|
5158
|
+
const key = normalizeHandoffKey(input.key);
|
|
5159
|
+
const northStarSlug = clean(input.northStarSlug);
|
|
5160
|
+
const summary = clean(input.summary);
|
|
5161
|
+
const sourceSessionId = clean(input.sourceSessionId);
|
|
5162
|
+
if (!northStarSlug) throw new Error("north star slug is required");
|
|
5163
|
+
if (!summary) throw new Error("handoff summary is required");
|
|
5164
|
+
if (!sourceSessionId) throw new Error("source session id is required");
|
|
5165
|
+
return {
|
|
5166
|
+
state: "open",
|
|
5167
|
+
key,
|
|
5168
|
+
northStarSlug,
|
|
5169
|
+
summary,
|
|
5170
|
+
sourceSessionId,
|
|
5171
|
+
createdAt: input.createdAt
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
function closeHandoff(record, state, at, claimedBySessionId) {
|
|
5175
|
+
return {
|
|
5176
|
+
...record,
|
|
5177
|
+
state,
|
|
5178
|
+
closedAt: at,
|
|
5179
|
+
claimedBySessionId: claimedBySessionId || record.claimedBySessionId
|
|
5180
|
+
};
|
|
5181
|
+
}
|
|
5182
|
+
function formatHandoffLine(item) {
|
|
5183
|
+
const r = item.record;
|
|
5184
|
+
const suffix = r.state === "claimed" && r.claimedBySessionId ? ` -> ${r.claimedBySessionId}` : "";
|
|
5185
|
+
return `${r.key} [${r.state}] northstar:${r.northStarSlug} source:${r.sourceSessionId}${suffix} - ${r.summary}`;
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
// src/handoff-commands.ts
|
|
5189
|
+
var FOREGROUND_FETCH = { attempts: 2, timeoutMs: 8e3 };
|
|
5190
|
+
var SESSION_START_FETCH = { attempts: 1, timeoutMs: 3e3 };
|
|
5191
|
+
async function fetchState(url, qs, retry = FOREGROUND_FETCH) {
|
|
5192
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/state?${qs}`, { headers: await hubHeaders() }, retry);
|
|
5193
|
+
if (!res.ok) return null;
|
|
5194
|
+
const body = await res.json();
|
|
5195
|
+
if ("state" in body) return body;
|
|
5196
|
+
return { key: null, state: { head: body.head } };
|
|
5197
|
+
}
|
|
5198
|
+
async function fetchScopedSessions(url, project2, branch, retry = FOREGROUND_FETCH) {
|
|
5199
|
+
const qs = new URLSearchParams({ project: project2, branch });
|
|
5200
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/sessions?${qs}`, { headers: await hubHeaders() }, retry);
|
|
5201
|
+
if (!res.ok) return [];
|
|
5202
|
+
const body = await res.json();
|
|
5203
|
+
return body.sessions ?? [];
|
|
5204
|
+
}
|
|
5205
|
+
async function fetchSessionHead(url, key, retry = FOREGROUND_FETCH) {
|
|
5206
|
+
const qs = new URLSearchParams(key);
|
|
5207
|
+
const got = await fetchState(url, qs, retry);
|
|
5208
|
+
return got?.state?.head ?? null;
|
|
5209
|
+
}
|
|
5210
|
+
async function fetchCurrentState() {
|
|
5211
|
+
const cfg = await loadConfig();
|
|
5212
|
+
if (!cfg.sagaApiUrl) return null;
|
|
5213
|
+
const key = await sagaKey(cfg);
|
|
5214
|
+
const got = await fetchState(cfg.sagaApiUrl, new URLSearchParams(key));
|
|
5215
|
+
return got ? { key, head: got.state?.head ?? {} } : null;
|
|
5216
|
+
}
|
|
5217
|
+
async function collectScopedHandoffs(opts = {}) {
|
|
5218
|
+
const cfg = await loadConfig();
|
|
5219
|
+
if (!cfg.sagaApiUrl) return { handoffs: [], sessions: [] };
|
|
5220
|
+
const key = await sagaKey(cfg);
|
|
5221
|
+
const retry = opts.retry ?? FOREGROUND_FETCH;
|
|
5222
|
+
const sessions = await fetchScopedSessions(cfg.sagaApiUrl, key.project, key.branch, retry);
|
|
5223
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5224
|
+
const handoffs = [];
|
|
5225
|
+
for (const session of sessions) {
|
|
5226
|
+
const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
|
|
5227
|
+
if (!head) continue;
|
|
5228
|
+
for (const item of listHandoffs(head, { includeClosed: opts.includeClosed })) {
|
|
5229
|
+
const dedupe = `${item.record.sourceSessionId}:${item.record.key}:${item.record.state}`;
|
|
5230
|
+
if (seen.has(dedupe)) continue;
|
|
5231
|
+
seen.add(dedupe);
|
|
5232
|
+
handoffs.push(item);
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
return { handoffs, sessions };
|
|
5236
|
+
}
|
|
5237
|
+
async function locateOpenHandoff(key, retry = FOREGROUND_FETCH) {
|
|
5238
|
+
const cfg = await loadConfig();
|
|
5239
|
+
if (!cfg.sagaApiUrl) return null;
|
|
5240
|
+
const scopeKey = await sagaKey(cfg);
|
|
5241
|
+
const sessions = await fetchScopedSessions(cfg.sagaApiUrl, scopeKey.project, scopeKey.branch, retry);
|
|
5242
|
+
for (const session of sessions) {
|
|
5243
|
+
const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
|
|
5244
|
+
if (!head) continue;
|
|
5245
|
+
const item = findOpenHandoff(head, key);
|
|
5246
|
+
if (item) {
|
|
5247
|
+
return {
|
|
5248
|
+
key: { project: session.project, branch: session.branch, sessionId: session.sessionId },
|
|
5249
|
+
item
|
|
5250
|
+
};
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
return null;
|
|
5254
|
+
}
|
|
5255
|
+
async function locateClaimedHandoff(key, claimedBySessionId, retry = FOREGROUND_FETCH) {
|
|
5256
|
+
const { handoffs } = await collectScopedHandoffs({ includeClosed: true, retry });
|
|
5257
|
+
const normalized = key.toLowerCase();
|
|
5258
|
+
return handoffs.find((item) => {
|
|
5259
|
+
const r = item.record;
|
|
5260
|
+
return r.state === "claimed" && r.claimedBySessionId === claimedBySessionId && (r.key.toLowerCase() === normalized || r.northStarSlug.toLowerCase() === normalized);
|
|
5261
|
+
}) ?? null;
|
|
5262
|
+
}
|
|
5263
|
+
async function postKeyedNote(key, summary, options) {
|
|
5264
|
+
const cfg = await loadConfig();
|
|
5265
|
+
if (!cfg.sagaApiUrl) fail("handoff: Hub API URL not configured");
|
|
5266
|
+
const sha = await gitOut(["rev-parse", "--short", "HEAD"]);
|
|
5267
|
+
const capture = buildNoteCapture(summary, options, (0, import_node_crypto4.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
5268
|
+
const result = await postCaptureOnce(cfg.sagaApiUrl, { ...capture, ...key });
|
|
5269
|
+
if (!result.ok) fail(`handoff: write failed${result.status ? ` (HTTP ${result.status})` : ""}${result.message ? `: ${result.message}` : ""}`);
|
|
5270
|
+
}
|
|
5271
|
+
function deriveOpenFields(summary, opts, head, key) {
|
|
5272
|
+
const northStarSlug = opts.northStarSlug ?? head.anchor?.slug;
|
|
5273
|
+
const handoffKey = opts.key ?? northStarSlug;
|
|
5274
|
+
const text = summary?.trim() || head.next?.trim() || head.anchor?.intent?.trim();
|
|
5275
|
+
return planOpenHandoff({
|
|
5276
|
+
key: handoffKey ?? "",
|
|
5277
|
+
northStarSlug: northStarSlug ?? "",
|
|
5278
|
+
summary: text ?? "",
|
|
5279
|
+
sourceSessionId: key.sessionId,
|
|
5280
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
async function runHandoffOpen(summary, opts, io = consoleIo) {
|
|
5284
|
+
let resolvedSummary = summary;
|
|
5285
|
+
if (summary !== void 0 || opts.messageFile !== void 0) {
|
|
5286
|
+
try {
|
|
5287
|
+
resolvedSummary = await resolveTextArg({ value: summary, file: opts.messageFile }, { readFile: import_promises3.readFile, readStdin }, {
|
|
5288
|
+
value: "a summary argument",
|
|
5289
|
+
file: "--message-file",
|
|
5290
|
+
noun: "message"
|
|
5291
|
+
});
|
|
5292
|
+
} catch (e) {
|
|
5293
|
+
return fail(`handoff open: ${e.message}`);
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
const current = await fetchCurrentState();
|
|
5297
|
+
if (!current) return fail("handoff open: saga state unavailable");
|
|
5298
|
+
let record;
|
|
5299
|
+
try {
|
|
5300
|
+
record = deriveOpenFields(resolvedSummary, opts, current.head, current.key);
|
|
5301
|
+
} catch (e) {
|
|
5302
|
+
return fail(`handoff open: ${e.message}`);
|
|
5303
|
+
}
|
|
5304
|
+
await postKeyedNote(current.key, `handoff opened ${record.key}`, { queueAdd: serializeHandoff(record), anchorSlug: record.northStarSlug });
|
|
5305
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: record }));
|
|
5306
|
+
io.log(`handoff open: ${formatHandoffLine({ index: -1, done: false, record })}`);
|
|
5307
|
+
}
|
|
5308
|
+
async function runHandoffList(opts, io = consoleIo) {
|
|
5309
|
+
const { handoffs } = await collectScopedHandoffs({ includeClosed: opts.all });
|
|
5310
|
+
if (opts.json) {
|
|
5311
|
+
io.log(JSON.stringify({ handoffs }, null, 2));
|
|
5312
|
+
} else if (!handoffs.length) {
|
|
5313
|
+
io.log("handoff list: none");
|
|
5314
|
+
} else {
|
|
5315
|
+
for (const h of handoffs) io.log(formatHandoffLine(h));
|
|
5316
|
+
}
|
|
5317
|
+
return handoffs;
|
|
5318
|
+
}
|
|
5319
|
+
async function runHandoffOffer(io = consoleIo, opts = {}) {
|
|
5320
|
+
const retry = opts.fast ? SESSION_START_FETCH : FOREGROUND_FETCH;
|
|
5321
|
+
const { handoffs } = await collectScopedHandoffs({ retry });
|
|
5322
|
+
if (!handoffs.length) return;
|
|
5323
|
+
io.log("Open handoffs:");
|
|
5324
|
+
for (const h of handoffs) io.log(`- ${formatHandoffLine(h)}`);
|
|
5325
|
+
io.log("Use `mmi-cli handoff accept <key>` to claim, `mmi-cli handoff cancel <key>` to close, or decline in chat to leave it open.");
|
|
5326
|
+
}
|
|
5327
|
+
async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
|
|
5328
|
+
const current = await sagaKey(await loadConfig(), resolveSessionId());
|
|
5329
|
+
const located = await locateOpenHandoff(key);
|
|
5330
|
+
if (!located) {
|
|
5331
|
+
if (state === "claimed") {
|
|
5332
|
+
const prior = await locateClaimedHandoff(key, current.sessionId);
|
|
5333
|
+
if (prior) {
|
|
5334
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: prior.record, idempotent: true }));
|
|
5335
|
+
return io.log(`handoff claimed: ${formatHandoffLine(prior)} (already accepted)`);
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
return fail(`handoff ${state}: no open handoff matching ${key}`);
|
|
5339
|
+
}
|
|
5340
|
+
const closed = closeHandoff(located.item.record, state, (/* @__PURE__ */ new Date()).toISOString(), state === "claimed" ? current.sessionId : void 0);
|
|
5341
|
+
await postKeyedNote(located.key, `handoff ${state} ${located.item.record.key}`, {
|
|
5342
|
+
handoffClose: { index: located.item.index, closedText: serializeHandoff(closed) }
|
|
5343
|
+
});
|
|
5344
|
+
if (state === "claimed") {
|
|
5345
|
+
await postKeyedNote(current, `handoff accepted ${located.item.record.key}`, {
|
|
5346
|
+
anchor: located.item.record.summary,
|
|
5347
|
+
anchorSlug: located.item.record.northStarSlug,
|
|
5348
|
+
anchorForce: true,
|
|
5349
|
+
decision: `accepted handoff ${located.item.record.key} from session ${located.item.record.sourceSessionId}; northstar ${located.item.record.northStarSlug}`,
|
|
5350
|
+
verified: true
|
|
5351
|
+
});
|
|
5352
|
+
}
|
|
5353
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: closed }));
|
|
5354
|
+
io.log(`handoff ${state}: ${formatHandoffLine({ index: located.item.index, done: true, record: closed })}`);
|
|
5355
|
+
}
|
|
5356
|
+
async function runHandoffDecline(key, opts = {}, io = consoleIo) {
|
|
5357
|
+
const located = await locateOpenHandoff(key);
|
|
5358
|
+
if (!located) return fail(`handoff decline: no open handoff matching ${key}`);
|
|
5359
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, unchanged: true, handoff: located.item.record }));
|
|
5360
|
+
io.log(`handoff decline: left open for re-offer \u2014 ${formatHandoffLine(located.item)}`);
|
|
5361
|
+
}
|
|
5362
|
+
function registerHandoffCommands(program3) {
|
|
5363
|
+
const handoff = program3.command("handoff").description("explicit saga + North Star handoff lifecycle");
|
|
5364
|
+
handoff.command("open [summary]").description("open a handoff bound to the current North Star slug").option("--key <slug|#issue>", "handoff key (defaults to the current North Star slug)").option("--north-star-slug <slug>", "North Star slug to bind (defaults to current saga anchor slug)").option("--message-file <path|->", "read the handoff summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").option("--json", "machine-readable output").action((summary, opts) => runHandoffOpen(summary, opts));
|
|
5365
|
+
handoff.command("list").description("list open handoffs for the current repo/branch").option("--all", "include claimed/cancelled records").option("--json", "machine-readable output").action(async (opts) => {
|
|
5366
|
+
await runHandoffList(opts);
|
|
5367
|
+
});
|
|
5368
|
+
handoff.command("accept <key>").description("claim an open handoff and bind this session to its North Star").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "claimed", opts));
|
|
5369
|
+
handoff.command("cancel <key>").description("close an open handoff without claiming it").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "cancelled", opts));
|
|
5370
|
+
handoff.command("decline <key>").description("leave an open handoff unchanged so it is re-offered later").option("--json", "machine-readable output").action((key, opts) => runHandoffDecline(key, opts));
|
|
5371
|
+
}
|
|
5372
|
+
|
|
5083
5373
|
// src/honcho-commands.ts
|
|
5084
5374
|
var import_node_child_process5 = require("node:child_process");
|
|
5085
|
-
var
|
|
5375
|
+
var import_promises4 = require("node:fs/promises");
|
|
5086
5376
|
var import_node_fs10 = require("node:fs");
|
|
5087
5377
|
var import_node_path8 = require("node:path");
|
|
5088
5378
|
|
|
@@ -5631,6 +5921,28 @@ async function honchoApiKey(env = process.env, fetcher = vaultKeyFetcher) {
|
|
|
5631
5921
|
cachedVaultKey ??= fetcher().catch(() => null);
|
|
5632
5922
|
return cachedVaultKey;
|
|
5633
5923
|
}
|
|
5924
|
+
function honchoKeyReasonFromStatus(status) {
|
|
5925
|
+
if (status >= 200 && status < 300) return "ok";
|
|
5926
|
+
if (status === 401 || status === 403) return "forbidden";
|
|
5927
|
+
if (status === 404) return "missing";
|
|
5928
|
+
return "unreachable";
|
|
5929
|
+
}
|
|
5930
|
+
async function diagnoseHonchoKey(env = process.env, fetchImpl = fetch) {
|
|
5931
|
+
if ((env.HONCHO_API_KEY || "").trim()) return { reason: "ok" };
|
|
5932
|
+
const cfg = await loadConfig();
|
|
5933
|
+
if (!cfg.sagaApiUrl) return { reason: "unreachable" };
|
|
5934
|
+
try {
|
|
5935
|
+
const res = await fetchImpl(`${cfg.sagaApiUrl}/secrets/get`, {
|
|
5936
|
+
method: "POST",
|
|
5937
|
+
headers: await hubHeaders({ "content-type": "application/json" }),
|
|
5938
|
+
body: JSON.stringify({ repo: HONCHO_SECRET_REPO, key: HONCHO_SECRET_KEY }),
|
|
5939
|
+
signal: AbortSignal.timeout(8e3)
|
|
5940
|
+
});
|
|
5941
|
+
return { reason: honchoKeyReasonFromStatus(res.status), httpStatus: res.status };
|
|
5942
|
+
} catch {
|
|
5943
|
+
return { reason: "unreachable" };
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5634
5946
|
async function resolveHonchoConfig(cfg = {}, opts = {}) {
|
|
5635
5947
|
const { apiUrl, workspace } = honchoEndpoint(cfg, opts.env);
|
|
5636
5948
|
if (!apiUrl) return null;
|
|
@@ -5640,7 +5952,7 @@ async function resolveHonchoConfig(cfg = {}, opts = {}) {
|
|
|
5640
5952
|
}
|
|
5641
5953
|
|
|
5642
5954
|
// src/honcho-ingest.ts
|
|
5643
|
-
var
|
|
5955
|
+
var import_node_crypto5 = require("node:crypto");
|
|
5644
5956
|
var DEFAULT_HONCHO_MAX_CHARS = 4e3;
|
|
5645
5957
|
var DEFAULT_HONCHO_CARD_MAX_CHARS = 1200;
|
|
5646
5958
|
var REDACTED = "[REDACTED]";
|
|
@@ -5726,7 +6038,7 @@ function buildIngestPayload(args) {
|
|
|
5726
6038
|
const maxChars = args.maxChars ?? DEFAULT_HONCHO_MAX_CHARS;
|
|
5727
6039
|
const messages = args.messages.map((m) => ({ role: m.role, content: capContent(redactSecrets(m.content), maxChars) })).filter((m) => m.content.trim().length > 0);
|
|
5728
6040
|
return {
|
|
5729
|
-
id: args.id ?? (0,
|
|
6041
|
+
id: args.id ?? (0, import_node_crypto5.randomUUID)(),
|
|
5730
6042
|
workspace: args.workspace,
|
|
5731
6043
|
peer: args.peer,
|
|
5732
6044
|
session: args.session,
|
|
@@ -5950,7 +6262,7 @@ async function resolveSummaryText(summary, messageFile) {
|
|
|
5950
6262
|
if (summary && summary.trim()) return summary.trim();
|
|
5951
6263
|
if (messageFile) {
|
|
5952
6264
|
try {
|
|
5953
|
-
return (messageFile === "-" ? await readStdin() : await (0,
|
|
6265
|
+
return (messageFile === "-" ? await readStdin() : await (0, import_promises4.readFile)(messageFile, "utf8")).trim();
|
|
5954
6266
|
} catch {
|
|
5955
6267
|
return "";
|
|
5956
6268
|
}
|
|
@@ -5978,7 +6290,7 @@ async function runHonchoIngest(opts) {
|
|
|
5978
6290
|
const transcriptPath = resolveTranscriptPath(hook);
|
|
5979
6291
|
if (transcriptPath) {
|
|
5980
6292
|
try {
|
|
5981
|
-
messages = extractTurnMessages(await (0,
|
|
6293
|
+
messages = extractTurnMessages(await (0, import_promises4.readFile)(transcriptPath, "utf8"));
|
|
5982
6294
|
} catch {
|
|
5983
6295
|
recordIngestSkip({
|
|
5984
6296
|
reason: "transcript-unreadable",
|
|
@@ -6127,6 +6439,7 @@ async function runHonchoHealth(o, io = consoleIo) {
|
|
|
6127
6439
|
const { apiUrl, workspace } = honchoEndpoint(cfg);
|
|
6128
6440
|
const apiKey = await honchoApiKey();
|
|
6129
6441
|
const hc = apiKey ? { apiUrl, apiKey, workspace } : null;
|
|
6442
|
+
const keyReason = apiKey ? "ok" : (await diagnoseHonchoKey()).reason;
|
|
6130
6443
|
const peer = honchoPeerId(await honchoLogin(cfg), cfg);
|
|
6131
6444
|
const liveness = hc ? await probeHoncho(hc, fetch, 3e3, { peer: peer ?? void 0 }) : {
|
|
6132
6445
|
reachable: false,
|
|
@@ -6144,6 +6457,7 @@ async function runHonchoHealth(o, io = consoleIo) {
|
|
|
6144
6457
|
configured: !!hc,
|
|
6145
6458
|
apiUrl,
|
|
6146
6459
|
apiKeyConfigured: !!apiKey,
|
|
6460
|
+
keyReason,
|
|
6147
6461
|
reachable: liveness.reachable,
|
|
6148
6462
|
authOk: liveness.authOk,
|
|
6149
6463
|
status: liveness.status,
|
|
@@ -6168,7 +6482,11 @@ async function runHonchoHealth(o, io = consoleIo) {
|
|
|
6168
6482
|
return;
|
|
6169
6483
|
}
|
|
6170
6484
|
io.log(`honcho health: ${report.ok ? "OK" : "NOT OK"}`);
|
|
6171
|
-
if (!apiKey)
|
|
6485
|
+
if (!apiKey) {
|
|
6486
|
+
if (keyReason === "forbidden") io.log(" - off: client key forbidden (401/403) \u2014 your account lacks Honcho access");
|
|
6487
|
+
else if (keyReason === "missing") io.log(" - off: client key not found in the vault (404)");
|
|
6488
|
+
else io.log(" - off: vault unreachable \u2014 could not fetch the client key");
|
|
6489
|
+
}
|
|
6172
6490
|
if (report.configured && !report.reachable) io.log(" - service unreachable");
|
|
6173
6491
|
if (report.configured && report.reachable && !report.authOk) io.log(" - API key rejected (401/403) \u2014 rotate or fix vault path");
|
|
6174
6492
|
if (!report.peer) io.log(" - no peer identity resolved (gh login) \u2014 ingest will be skipped");
|
|
@@ -6203,6 +6521,89 @@ function registerHonchoCommands(program3) {
|
|
|
6203
6521
|
honcho.command("key").option("--json", "machine-readable output").description("print the resolved honcho identity (workspace, peer, session) \u2014 no write, no key").action((o) => runHonchoKey(o));
|
|
6204
6522
|
}
|
|
6205
6523
|
|
|
6524
|
+
// src/scrooge-commands.ts
|
|
6525
|
+
var import_node_fs11 = require("node:fs");
|
|
6526
|
+
var import_node_path9 = require("node:path");
|
|
6527
|
+
var SCROOGE_TRACE_PATH = (0, import_node_path9.join)(".mmi", "scrooge", "trace.jsonl");
|
|
6528
|
+
function resolveModeFromEnv() {
|
|
6529
|
+
const v = String(process.env.MMI_SCROOGE ?? "conservative").trim().toLowerCase();
|
|
6530
|
+
if (!v || v === "conservative") return "conservative";
|
|
6531
|
+
if (v === "0" || v === "off" || v === "false") return "off";
|
|
6532
|
+
if (v === "normal") return "normal";
|
|
6533
|
+
if (v === "spike") return "spike";
|
|
6534
|
+
return "conservative";
|
|
6535
|
+
}
|
|
6536
|
+
function parseTraceLines(raw) {
|
|
6537
|
+
const out = [];
|
|
6538
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
6539
|
+
const t = line.trim();
|
|
6540
|
+
if (!t) continue;
|
|
6541
|
+
try {
|
|
6542
|
+
const o = JSON.parse(t);
|
|
6543
|
+
if (o && typeof o === "object") out.push(o);
|
|
6544
|
+
} catch {
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
return out;
|
|
6548
|
+
}
|
|
6549
|
+
function summarizeTrace(entries) {
|
|
6550
|
+
let tools = 0;
|
|
6551
|
+
let charsIn = 0;
|
|
6552
|
+
let charsOut = 0;
|
|
6553
|
+
const passCounts = {};
|
|
6554
|
+
for (const e of entries) {
|
|
6555
|
+
tools += 1;
|
|
6556
|
+
charsIn += Number(e.charsIn) || 0;
|
|
6557
|
+
charsOut += Number(e.charsOut) || 0;
|
|
6558
|
+
for (const p of e.passes ?? []) passCounts[p] = (passCounts[p] ?? 0) + 1;
|
|
6559
|
+
}
|
|
6560
|
+
const saved = Math.max(0, charsIn - charsOut);
|
|
6561
|
+
return {
|
|
6562
|
+
tools,
|
|
6563
|
+
charsIn,
|
|
6564
|
+
charsOut,
|
|
6565
|
+
charsSaved: saved,
|
|
6566
|
+
estTokensSaved: Math.round(saved / 4),
|
|
6567
|
+
passCounts
|
|
6568
|
+
};
|
|
6569
|
+
}
|
|
6570
|
+
function runScroogeReport(io, tracePath = SCROOGE_TRACE_PATH) {
|
|
6571
|
+
const mode = resolveModeFromEnv();
|
|
6572
|
+
if (!(0, import_node_fs11.existsSync)(tracePath)) {
|
|
6573
|
+
io.log(`Scrooge: no trace at ${tracePath} (hook has not compacted anything yet).`);
|
|
6574
|
+
io.log(`Active mode: ${mode} (MMI_SCROOGE env; default conservative).`);
|
|
6575
|
+
return 0;
|
|
6576
|
+
}
|
|
6577
|
+
let raw = "";
|
|
6578
|
+
try {
|
|
6579
|
+
raw = (0, import_node_fs11.readFileSync)(tracePath, "utf8");
|
|
6580
|
+
} catch (e) {
|
|
6581
|
+
io.err(`Scrooge: could not read trace: ${e.message}`);
|
|
6582
|
+
return 1;
|
|
6583
|
+
}
|
|
6584
|
+
const entries = parseTraceLines(raw);
|
|
6585
|
+
const s = summarizeTrace(entries);
|
|
6586
|
+
io.log("Scrooge session summary");
|
|
6587
|
+
io.log(` mode (current env): ${mode}`);
|
|
6588
|
+
io.log(` compacted tool results: ${s.tools}`);
|
|
6589
|
+
io.log(` chars in / out: ${s.charsIn} / ${s.charsOut}`);
|
|
6590
|
+
io.log(` chars saved: ${s.charsSaved}`);
|
|
6591
|
+
io.log(` est tokens saved (chars/4): ~${s.estTokensSaved}`);
|
|
6592
|
+
const passes = Object.entries(s.passCounts).sort((a, b) => b[1] - a[1]);
|
|
6593
|
+
if (passes.length) {
|
|
6594
|
+
io.log(" passes:");
|
|
6595
|
+
for (const [name, count] of passes) io.log(` ${name}: ${count}`);
|
|
6596
|
+
}
|
|
6597
|
+
return 0;
|
|
6598
|
+
}
|
|
6599
|
+
function registerScroogeCommands(program3) {
|
|
6600
|
+
const scrooge = program3.command("scrooge").description("Scrooge \u2014 org tool-output compaction (trace + report)");
|
|
6601
|
+
scrooge.command("report").description("print char/token savings from .mmi/scrooge/trace.jsonl").action(() => {
|
|
6602
|
+
const code = runScroogeReport({ log: (s) => console.log(s), err: (s) => console.error(s) });
|
|
6603
|
+
process.exit(code);
|
|
6604
|
+
});
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6206
6607
|
// src/docs-sync.ts
|
|
6207
6608
|
var SYNCED_DOCS = ["README.md", "architecture.md"];
|
|
6208
6609
|
async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
@@ -6225,8 +6626,205 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
|
6225
6626
|
}
|
|
6226
6627
|
|
|
6227
6628
|
// src/session-start.ts
|
|
6228
|
-
var
|
|
6229
|
-
var
|
|
6629
|
+
var import_node_fs13 = require("node:fs");
|
|
6630
|
+
var import_node_path11 = require("node:path");
|
|
6631
|
+
|
|
6632
|
+
// src/scratch-gc.ts
|
|
6633
|
+
var import_node_fs12 = require("node:fs");
|
|
6634
|
+
var import_node_path10 = require("node:path");
|
|
6635
|
+
var TMP_STALE_MS = 6e4;
|
|
6636
|
+
var FLUSH_LOCK_GC_MS = 6e5;
|
|
6637
|
+
var HEAD_TS_STALE_MS = 48 * 36e5;
|
|
6638
|
+
var LAST_SKIP_STALE_MS = 24 * 36e5;
|
|
6639
|
+
var CONFLICT_COPY_STALE_MS = 24 * 36e5;
|
|
6640
|
+
var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
|
|
6641
|
+
var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
|
|
6642
|
+
function scratchGcThrottlePath(mmiRoot) {
|
|
6643
|
+
return (0, import_node_path10.join)(mmiRoot, "head-ts", ".scratch-gc-last");
|
|
6644
|
+
}
|
|
6645
|
+
function scratchGcDue(stampPath, now = Date.now(), read = import_node_fs12.readFileSync) {
|
|
6646
|
+
try {
|
|
6647
|
+
return now - (Number(read(stampPath, "utf8").trim()) || 0) >= SCRATCH_GC_THROTTLE_MS;
|
|
6648
|
+
} catch {
|
|
6649
|
+
return true;
|
|
6650
|
+
}
|
|
6651
|
+
}
|
|
6652
|
+
function markScratchGcRun(stampPath, now = Date.now()) {
|
|
6653
|
+
try {
|
|
6654
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(stampPath), { recursive: true });
|
|
6655
|
+
(0, import_node_fs12.writeFileSync)(stampPath, String(now), "utf8");
|
|
6656
|
+
} catch {
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
function executeScratchGc(repoRoot, opts, now = Date.now()) {
|
|
6660
|
+
const snap = collectScratchSnapshot(repoRoot);
|
|
6661
|
+
const plan2 = planScratchGc(snap, now);
|
|
6662
|
+
if (!opts.apply) return { plan: plan2 };
|
|
6663
|
+
return { plan: plan2, applied: applyScratchGc(plan2, snap.mmiRoot, now) };
|
|
6664
|
+
}
|
|
6665
|
+
var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json", "saga-pending.jsonl"]);
|
|
6666
|
+
var TMP_SIDECAR_BASES = ["saga-pending.jsonl", "last-skip.json"];
|
|
6667
|
+
var CONFLICT_COPY_ALLOWLIST = /* @__PURE__ */ new Set(["saga-pending.jsonl", "last-skip.json"]);
|
|
6668
|
+
function conflictCopyOriginal(name) {
|
|
6669
|
+
const m = /^(.+) \d+(\.[^.]+)$/.exec(name);
|
|
6670
|
+
return m ? `${m[1]}${m[2]}` : null;
|
|
6671
|
+
}
|
|
6672
|
+
function isTmpSidecar(name) {
|
|
6673
|
+
return name.endsWith(".tmp") && TMP_SIDECAR_BASES.some((b) => name.startsWith(`${b}.`));
|
|
6674
|
+
}
|
|
6675
|
+
function planScratchGc(snap, now = Date.now()) {
|
|
6676
|
+
const candidates = [];
|
|
6677
|
+
const normalizePath = (p) => p.replace(/\\/g, "/");
|
|
6678
|
+
const mmiPaths = new Set(snap.mmiFiles.map((f) => normalizePath(f.path)));
|
|
6679
|
+
const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
|
|
6680
|
+
const days = (ms) => `${Math.floor(ms / 864e5)}d`;
|
|
6681
|
+
for (const f of snap.mmiFiles) {
|
|
6682
|
+
const age = now - f.mtimeMs;
|
|
6683
|
+
const add = (family, reason) => candidates.push({ path: f.path, family, tier: "safe-auto", reason, bytes: f.bytes });
|
|
6684
|
+
if (isTmpSidecar(f.name)) {
|
|
6685
|
+
if (age > TMP_STALE_MS) add("tmp-sidecar", `crashed atomic-write sidecar (${days(age)} old)`);
|
|
6686
|
+
continue;
|
|
6687
|
+
}
|
|
6688
|
+
if (f.name === "saga-flush.lock") {
|
|
6689
|
+
if (age > FLUSH_LOCK_GC_MS) add("flush-lock", `abandoned flush lock (${days(age)} old)`);
|
|
6690
|
+
continue;
|
|
6691
|
+
}
|
|
6692
|
+
if (NEVER_BASENAMES.has(f.name)) continue;
|
|
6693
|
+
if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
|
|
6694
|
+
if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
|
|
6695
|
+
continue;
|
|
6696
|
+
}
|
|
6697
|
+
if (f.name === "last-skip.json") {
|
|
6698
|
+
if (age > LAST_SKIP_STALE_MS) add("last-skip", `stale ingest-skip hint (${days(age)} old)`);
|
|
6699
|
+
continue;
|
|
6700
|
+
}
|
|
6701
|
+
const orig = conflictCopyOriginal(f.name);
|
|
6702
|
+
if (orig && CONFLICT_COPY_ALLOWLIST.has(orig) && mmiPaths.has(normalizePath((0, import_node_path10.join)(f.dir, orig))) && age > CONFLICT_COPY_STALE_MS) {
|
|
6703
|
+
add("conflict-copy", `cloud-sync conflict copy of ${orig} (${days(age)} old)`);
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
if (snap.syncQueueSlugs) {
|
|
6707
|
+
for (const f of snap.planMdFiles) {
|
|
6708
|
+
const slug = f.name.replace(/\.md$/, "");
|
|
6709
|
+
if (snap.syncQueueSlugs.has(slug)) continue;
|
|
6710
|
+
if (now - f.mtimeMs > PLAN_ADVISORY_AGE_MS) {
|
|
6711
|
+
candidates.push({
|
|
6712
|
+
path: f.path,
|
|
6713
|
+
family: "plan",
|
|
6714
|
+
tier: "advisory",
|
|
6715
|
+
reason: `synced plan older than ${Math.floor(PLAN_ADVISORY_AGE_MS / 864e5)}d`,
|
|
6716
|
+
bytes: f.bytes
|
|
6717
|
+
});
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
return {
|
|
6722
|
+
candidates,
|
|
6723
|
+
safeAuto: candidates.filter((c) => c.tier === "safe-auto"),
|
|
6724
|
+
advisory: candidates.filter((c) => c.tier === "advisory")
|
|
6725
|
+
};
|
|
6726
|
+
}
|
|
6727
|
+
function formatScratchGcPlan(plan2, applied) {
|
|
6728
|
+
if (plan2.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local saga/plan/honcho scratch is clean.";
|
|
6729
|
+
const lines = [];
|
|
6730
|
+
const bytes = plan2.safeAuto.reduce((n, c) => n + c.bytes, 0);
|
|
6731
|
+
lines.push(
|
|
6732
|
+
`scratch GC: ${applied ? "pruned" : "would prune"} ${plan2.safeAuto.length} stale file(s) (${bytes} bytes)` + (plan2.advisory.length ? `; ${plan2.advisory.length} advisory (kept):` : ":")
|
|
6733
|
+
);
|
|
6734
|
+
for (const c of plan2.safeAuto.slice(0, 40)) lines.push(` - [${c.family}] ${c.path} \u2014 ${c.reason}`);
|
|
6735
|
+
if (plan2.safeAuto.length > 40) lines.push(` ... +${plan2.safeAuto.length - 40} more`);
|
|
6736
|
+
for (const c of plan2.advisory.slice(0, 20)) lines.push(` \xB7 [advisory] ${c.path} \u2014 ${c.reason} (kept; review manually)`);
|
|
6737
|
+
return lines.join("\n");
|
|
6738
|
+
}
|
|
6739
|
+
function scratchGcBannerLine(safeAutoPruned, advisory) {
|
|
6740
|
+
const parts = [];
|
|
6741
|
+
if (safeAutoPruned > 0) parts.push(`pruned ${safeAutoPruned} stale local file(s)`);
|
|
6742
|
+
if (advisory > 0) parts.push(`${advisory} old synced plan(s) prunable \u2014 \`mmi-cli gc --scratch --dry-run\` to review`);
|
|
6743
|
+
return parts.length ? `[cleanup] ${parts.join("; ")}` : void 0;
|
|
6744
|
+
}
|
|
6745
|
+
function applyScratchGc(plan2, mmiRoot, now = Date.now()) {
|
|
6746
|
+
const result = { pruned: [], skipped: 0, bytes: 0 };
|
|
6747
|
+
let anchor;
|
|
6748
|
+
try {
|
|
6749
|
+
anchor = (0, import_node_fs12.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
6750
|
+
} catch {
|
|
6751
|
+
return result;
|
|
6752
|
+
}
|
|
6753
|
+
for (const c of plan2.safeAuto) {
|
|
6754
|
+
try {
|
|
6755
|
+
const real = (0, import_node_fs12.realpathSync)(c.path).replace(/\\/g, "/");
|
|
6756
|
+
if (real !== anchor && !real.startsWith(`${anchor}/`)) {
|
|
6757
|
+
result.skipped += 1;
|
|
6758
|
+
continue;
|
|
6759
|
+
}
|
|
6760
|
+
const st = (0, import_node_fs12.statSync)(c.path);
|
|
6761
|
+
const floor = c.family === "flush-lock" ? FLUSH_LOCK_GC_MS : c.family === "tmp-sidecar" ? TMP_STALE_MS : c.family === "head-ts" ? HEAD_TS_STALE_MS : c.family === "last-skip" ? LAST_SKIP_STALE_MS : CONFLICT_COPY_STALE_MS;
|
|
6762
|
+
if (now - st.mtimeMs <= floor) {
|
|
6763
|
+
result.skipped += 1;
|
|
6764
|
+
continue;
|
|
6765
|
+
}
|
|
6766
|
+
(0, import_node_fs12.unlinkSync)(c.path);
|
|
6767
|
+
result.pruned.push(c.path);
|
|
6768
|
+
result.bytes += c.bytes;
|
|
6769
|
+
} catch {
|
|
6770
|
+
result.skipped += 1;
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
return result;
|
|
6774
|
+
}
|
|
6775
|
+
function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
6776
|
+
const readdir = deps.readdir ?? import_node_fs12.readdirSync;
|
|
6777
|
+
const stat = deps.stat ?? import_node_fs12.statSync;
|
|
6778
|
+
const readFile6 = deps.readFile ?? import_node_fs12.readFileSync;
|
|
6779
|
+
const mmiRoot = (0, import_node_path10.join)(repoRoot, ".mmi");
|
|
6780
|
+
const plansRoot = (0, import_node_path10.join)(repoRoot, "plans");
|
|
6781
|
+
const mmiFiles = [];
|
|
6782
|
+
try {
|
|
6783
|
+
for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
|
|
6784
|
+
if (!ent.isFile()) continue;
|
|
6785
|
+
const dir = ent.parentPath ?? ent.path ?? mmiRoot;
|
|
6786
|
+
const full = (0, import_node_path10.join)(dir, ent.name);
|
|
6787
|
+
try {
|
|
6788
|
+
const st = stat(full);
|
|
6789
|
+
mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
6790
|
+
} catch {
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
} catch {
|
|
6794
|
+
}
|
|
6795
|
+
const planMdFiles = [];
|
|
6796
|
+
try {
|
|
6797
|
+
for (const ent of readdir(plansRoot, { withFileTypes: true })) {
|
|
6798
|
+
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
6799
|
+
const full = (0, import_node_path10.join)(plansRoot, ent.name);
|
|
6800
|
+
try {
|
|
6801
|
+
const st = stat(full);
|
|
6802
|
+
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
6803
|
+
} catch {
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
} catch {
|
|
6807
|
+
}
|
|
6808
|
+
let syncQueueSlugs = null;
|
|
6809
|
+
let queueRaw;
|
|
6810
|
+
try {
|
|
6811
|
+
queueRaw = readFile6((0, import_node_path10.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
6812
|
+
} catch {
|
|
6813
|
+
syncQueueSlugs = /* @__PURE__ */ new Set();
|
|
6814
|
+
}
|
|
6815
|
+
if (queueRaw !== void 0) {
|
|
6816
|
+
try {
|
|
6817
|
+
const parsed = JSON.parse(queueRaw);
|
|
6818
|
+
const entries = Array.isArray(parsed) ? parsed : parsed.entries ?? [];
|
|
6819
|
+
syncQueueSlugs = new Set(entries.map((e) => e.slug).filter((s) => typeof s === "string"));
|
|
6820
|
+
} catch {
|
|
6821
|
+
syncQueueSlugs = null;
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
return { mmiRoot, mmiFiles, planMdFiles, syncQueueSlugs };
|
|
6825
|
+
}
|
|
6826
|
+
|
|
6827
|
+
// src/session-start.ts
|
|
6230
6828
|
async function runBufferedStep(step) {
|
|
6231
6829
|
const lines = [];
|
|
6232
6830
|
const io = {
|
|
@@ -6254,6 +6852,7 @@ function buildSessionStartPlan(verbs) {
|
|
|
6254
6852
|
parallel: [
|
|
6255
6853
|
{ name: "rules sync", run: verbs.rulesSync },
|
|
6256
6854
|
{ name: "saga show", run: verbs.sagaShow },
|
|
6855
|
+
{ name: "handoff offer", run: verbs.handoffOffer },
|
|
6257
6856
|
// honcho profile (#1162): the behavioral-memory prior, flushed right after the saga resume. A fast
|
|
6258
6857
|
// peer-card GET, fail-soft + silent when off/empty — it never hangs or noises the banner.
|
|
6259
6858
|
{ name: "honcho profile", run: verbs.honchoContext },
|
|
@@ -6275,14 +6874,24 @@ function spawnDetachedSelf(args, deps) {
|
|
|
6275
6874
|
} catch {
|
|
6276
6875
|
}
|
|
6277
6876
|
}
|
|
6877
|
+
function isInsideRepoSubdir(cwd, exists = import_node_fs13.existsSync) {
|
|
6878
|
+
if (exists((0, import_node_path11.join)(cwd, ".git"))) return false;
|
|
6879
|
+
let dir = cwd;
|
|
6880
|
+
for (; ; ) {
|
|
6881
|
+
const parent = (0, import_node_path11.dirname)(dir);
|
|
6882
|
+
if (parent === dir) return false;
|
|
6883
|
+
if (exists((0, import_node_path11.join)(parent, ".git"))) return true;
|
|
6884
|
+
dir = parent;
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6278
6887
|
function planStoreLines(cwd) {
|
|
6279
6888
|
const mdFiles = (dir, minSize = 0) => {
|
|
6280
|
-
const p = (0,
|
|
6281
|
-
if (!(0,
|
|
6889
|
+
const p = (0, import_node_path11.join)(cwd, dir);
|
|
6890
|
+
if (!(0, import_node_fs13.existsSync)(p)) return [];
|
|
6282
6891
|
try {
|
|
6283
|
-
return (0,
|
|
6892
|
+
return (0, import_node_fs13.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
|
|
6284
6893
|
try {
|
|
6285
|
-
return (0,
|
|
6894
|
+
return (0, import_node_fs13.statSync)((0, import_node_path11.join)(p, f)).size >= minSize;
|
|
6286
6895
|
} catch {
|
|
6287
6896
|
return false;
|
|
6288
6897
|
}
|
|
@@ -6300,6 +6909,19 @@ function planStoreLines(cwd) {
|
|
|
6300
6909
|
out.push(`[plan-store] ${localPlans.length} local plan(s) in plans/ \u2014 ensure new/changed ones are \`mmi-cli plan push\`ed (S3-backed, not git).`);
|
|
6301
6910
|
return out;
|
|
6302
6911
|
}
|
|
6912
|
+
function scratchGcLines(cwd, env = process.env, now = Date.now()) {
|
|
6913
|
+
if (env.MMI_NO_AUTO_GC) return [];
|
|
6914
|
+
try {
|
|
6915
|
+
const stamp = scratchGcThrottlePath((0, import_node_path11.join)(cwd, ".mmi"));
|
|
6916
|
+
if (!scratchGcDue(stamp, now)) return [];
|
|
6917
|
+
const run = executeScratchGc(cwd, { apply: true }, now);
|
|
6918
|
+
markScratchGcRun(stamp, now);
|
|
6919
|
+
const line = scratchGcBannerLine(run.applied?.pruned.length ?? 0, run.plan.advisory.length);
|
|
6920
|
+
return line ? [line] : [];
|
|
6921
|
+
} catch {
|
|
6922
|
+
return [];
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6303
6925
|
function northstarPointer(injected = false) {
|
|
6304
6926
|
if (injected) {
|
|
6305
6927
|
return "North Stars: `mmi-cli northstar relevant` for more matches; `northstar pull <slug>` for the full SSOT.";
|
|
@@ -6347,6 +6969,71 @@ function recoverPriorityFromEvents(events) {
|
|
|
6347
6969
|
return found;
|
|
6348
6970
|
}
|
|
6349
6971
|
|
|
6972
|
+
// src/board-dependency.ts
|
|
6973
|
+
var DEPENDS_ON_LINE = /^\s*[-*]?\s*\*\*Depends on:\*\*\s*(.+)$/im;
|
|
6974
|
+
var ISSUE_REF = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)#(\d+)/g;
|
|
6975
|
+
function parseDependsOnRefs(body) {
|
|
6976
|
+
const match = body.match(DEPENDS_ON_LINE);
|
|
6977
|
+
if (!match) return [];
|
|
6978
|
+
const refs = [];
|
|
6979
|
+
for (const m of match[1].matchAll(ISSUE_REF)) {
|
|
6980
|
+
refs.push({ repo: m[1], number: Number(m[2]) });
|
|
6981
|
+
}
|
|
6982
|
+
return refs;
|
|
6983
|
+
}
|
|
6984
|
+
async function issueOpen(client, repo, number) {
|
|
6985
|
+
try {
|
|
6986
|
+
const data = await client.rest("GET", `repos/${repo}/issues/${number}`);
|
|
6987
|
+
return data?.state === "OPEN";
|
|
6988
|
+
} catch {
|
|
6989
|
+
return void 0;
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
async function dependencyBlocksClaim(client, body) {
|
|
6993
|
+
const refs = parseDependsOnRefs(body);
|
|
6994
|
+
if (!refs.length) return { blocked: false, openDependencies: [] };
|
|
6995
|
+
const openDependencies = [];
|
|
6996
|
+
for (const ref of refs) {
|
|
6997
|
+
const open = await issueOpen(client, ref.repo, ref.number);
|
|
6998
|
+
if (open === true) openDependencies.push(`${ref.repo}#${ref.number}`);
|
|
6999
|
+
}
|
|
7000
|
+
return { blocked: openDependencies.length > 0, openDependencies };
|
|
7001
|
+
}
|
|
7002
|
+
async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
7003
|
+
const claimable = [];
|
|
7004
|
+
const blocked = [];
|
|
7005
|
+
const warnings = [];
|
|
7006
|
+
for (const item of items) {
|
|
7007
|
+
if (item.contentType !== "Issue") {
|
|
7008
|
+
claimable.push(item);
|
|
7009
|
+
continue;
|
|
7010
|
+
}
|
|
7011
|
+
let body = item.details?.body;
|
|
7012
|
+
if (body === void 0) {
|
|
7013
|
+
if (opts.requireBundledBody) {
|
|
7014
|
+
claimable.push(item);
|
|
7015
|
+
continue;
|
|
7016
|
+
}
|
|
7017
|
+
try {
|
|
7018
|
+
const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
|
|
7019
|
+
body = data?.body ?? "";
|
|
7020
|
+
} catch (e) {
|
|
7021
|
+
warnings.push(`dependency check skipped for ${item.ref}: ${e.message}`);
|
|
7022
|
+
claimable.push(item);
|
|
7023
|
+
continue;
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
const gate = await dependencyBlocksClaim(client, body);
|
|
7027
|
+
if (gate.blocked) {
|
|
7028
|
+
blocked.push(item);
|
|
7029
|
+
warnings.push(`${item.ref} blocked on open ${gate.openDependencies.join(", ")}`);
|
|
7030
|
+
} else {
|
|
7031
|
+
claimable.push(item);
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
return { claimable, blocked, warnings };
|
|
7035
|
+
}
|
|
7036
|
+
|
|
6350
7037
|
// ../infra/board-vocab.mjs
|
|
6351
7038
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
6352
7039
|
|
|
@@ -6670,6 +7357,13 @@ async function readBoard(options, deps = {}) {
|
|
|
6670
7357
|
if (options.includeBundleDetails) {
|
|
6671
7358
|
await attachBundleDetails(report, client, options.allowPartial ?? false);
|
|
6672
7359
|
}
|
|
7360
|
+
for (const scope of ["primary", "secondary"]) {
|
|
7361
|
+
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
|
|
7362
|
+
requireBundledBody: Boolean(options.includeBundleDetails)
|
|
7363
|
+
});
|
|
7364
|
+
report[scope].claimable = filtered.claimable;
|
|
7365
|
+
report.warnings.push(...filtered.warnings);
|
|
7366
|
+
}
|
|
6673
7367
|
return report;
|
|
6674
7368
|
}
|
|
6675
7369
|
function findBoardItem(items, selector) {
|
|
@@ -6751,6 +7445,11 @@ async function prepareClaimContext(options, selectors, deps, collected) {
|
|
|
6751
7445
|
warnings: collected.warnings,
|
|
6752
7446
|
partial: collected.partial
|
|
6753
7447
|
};
|
|
7448
|
+
for (const scope of ["primary", "secondary"]) {
|
|
7449
|
+
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client);
|
|
7450
|
+
report[scope].claimable = filtered.claimable;
|
|
7451
|
+
report.warnings.push(...filtered.warnings);
|
|
7452
|
+
}
|
|
6754
7453
|
return { cfg, client, items: collected.items, writable: writable.repos, report };
|
|
6755
7454
|
}
|
|
6756
7455
|
async function claimOneBoardItem(ctx, selector, options) {
|
|
@@ -6759,6 +7458,21 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
6759
7458
|
if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
|
|
6760
7459
|
throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
|
|
6761
7460
|
}
|
|
7461
|
+
if (flatItem.contentType === "Issue") {
|
|
7462
|
+
let body = flatItem.details?.body;
|
|
7463
|
+
if (body === void 0) {
|
|
7464
|
+
try {
|
|
7465
|
+
const data = await client.rest("GET", `repos/${flatItem.repository}/issues/${flatItem.number}`);
|
|
7466
|
+
body = data?.body ?? "";
|
|
7467
|
+
} catch {
|
|
7468
|
+
body = "";
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
const gate = await dependencyBlocksClaim(client, body);
|
|
7472
|
+
if (gate.blocked) {
|
|
7473
|
+
throw new Error(`${flatItem.ref} is not claimable: blocked on open ${gate.openDependencies.join(", ")}`);
|
|
7474
|
+
}
|
|
7475
|
+
}
|
|
6762
7476
|
let item = findClaimableItem(report, selector);
|
|
6763
7477
|
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
6764
7478
|
const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
|
|
@@ -7195,33 +7909,142 @@ async function withTimeout(promise, ms) {
|
|
|
7195
7909
|
}
|
|
7196
7910
|
}
|
|
7197
7911
|
async function runBoardSlice(io, deps, opts) {
|
|
7912
|
+
const budget = deps.timeoutMs ?? SESSION_START_BOARD_TIMEOUT_MS;
|
|
7913
|
+
const controller = new AbortController();
|
|
7914
|
+
const deadline = setTimeout(() => controller.abort(), budget);
|
|
7198
7915
|
try {
|
|
7199
7916
|
const cfg = await deps.loadConfig();
|
|
7200
7917
|
if (!boardMetaConfigured(cfg)) return;
|
|
7918
|
+
const client = deps.client ?? createGitHubClient({ defaultTimeoutMs: budget, signal: controller.signal });
|
|
7201
7919
|
const report = await withTimeout(
|
|
7202
|
-
deps.readBoard({ config: cfg, allowPartial: true }),
|
|
7203
|
-
|
|
7920
|
+
deps.readBoard({ config: cfg, allowPartial: true }, { client }),
|
|
7921
|
+
budget
|
|
7204
7922
|
);
|
|
7205
7923
|
if (opts?.expectedLogin && report.viewer && opts.expectedLogin !== report.viewer) return;
|
|
7206
7924
|
const block = renderBoardSlice(report);
|
|
7207
7925
|
if (block) io.log(block);
|
|
7208
7926
|
} catch {
|
|
7927
|
+
} finally {
|
|
7928
|
+
clearTimeout(deadline);
|
|
7209
7929
|
}
|
|
7210
7930
|
}
|
|
7211
7931
|
|
|
7212
|
-
// src/
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7932
|
+
// src/worktree.ts
|
|
7933
|
+
var import_node_fs14 = require("node:fs");
|
|
7934
|
+
var import_node_path12 = require("node:path");
|
|
7935
|
+
var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
7936
|
+
var PKG = "package.json";
|
|
7937
|
+
var LOCKFILE = "package-lock.json";
|
|
7938
|
+
var NODE_MODULES = "node_modules";
|
|
7939
|
+
var realFsProbe = {
|
|
7940
|
+
isDir: (p) => {
|
|
7941
|
+
try {
|
|
7942
|
+
return (0, import_node_fs14.statSync)(p).isDirectory();
|
|
7943
|
+
} catch {
|
|
7944
|
+
return false;
|
|
7945
|
+
}
|
|
7946
|
+
},
|
|
7947
|
+
isFile: (p) => {
|
|
7948
|
+
try {
|
|
7949
|
+
return (0, import_node_fs14.statSync)(p).isFile();
|
|
7950
|
+
} catch {
|
|
7951
|
+
return false;
|
|
7952
|
+
}
|
|
7953
|
+
},
|
|
7954
|
+
listDirs: (p) => {
|
|
7955
|
+
try {
|
|
7956
|
+
return (0, import_node_fs14.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7957
|
+
} catch {
|
|
7958
|
+
return [];
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
};
|
|
7962
|
+
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
7963
|
+
const factsFor = (dir) => {
|
|
7964
|
+
const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
|
|
7965
|
+
return {
|
|
7966
|
+
dir,
|
|
7967
|
+
hasPackageJson: fs2.isFile((0, import_node_path12.join)(abs, PKG)),
|
|
7968
|
+
hasLockfile: fs2.isFile((0, import_node_path12.join)(abs, LOCKFILE)),
|
|
7969
|
+
hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES))
|
|
7970
|
+
};
|
|
7971
|
+
};
|
|
7972
|
+
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
7973
|
+
return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
|
|
7974
|
+
}
|
|
7975
|
+
function npmInstallTargets(dirs) {
|
|
7976
|
+
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
7977
|
+
}
|
|
7978
|
+
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
7979
|
+
return fs2.isFile((0, import_node_path12.join)(root, ".git"));
|
|
7980
|
+
}
|
|
7981
|
+
function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
7982
|
+
if (!isLinkedWorktree(root, fs2)) return null;
|
|
7983
|
+
const pending = npmInstallTargets(scanInstallDirs(root, fs2));
|
|
7984
|
+
if (!pending.length) return null;
|
|
7985
|
+
const where = pending.map((t) => t.dir || ".").join(", ");
|
|
7986
|
+
return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
|
|
7987
|
+
}
|
|
7988
|
+
function defaultCopyFile(from, to) {
|
|
7989
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
|
|
7990
|
+
(0, import_node_fs14.copyFileSync)(from, to);
|
|
7991
|
+
}
|
|
7992
|
+
async function provisionWorktree(worktreeRoot, deps) {
|
|
7993
|
+
const fs2 = deps.fs ?? realFsProbe;
|
|
7994
|
+
const copyFile = deps.copyFile ?? defaultCopyFile;
|
|
7995
|
+
const log = deps.log ?? (() => {
|
|
7996
|
+
});
|
|
7997
|
+
const allDirs = scanInstallDirs(worktreeRoot, fs2);
|
|
7998
|
+
const targets = npmInstallTargets(allDirs);
|
|
7999
|
+
const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
|
|
8000
|
+
const installed = [];
|
|
8001
|
+
for (const target of targets) {
|
|
8002
|
+
const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
|
|
8003
|
+
log(`installing deps: ${target.command} in ${target.dir || "."}`);
|
|
8004
|
+
await deps.runInstall(target.command, cwd);
|
|
8005
|
+
installed.push(target);
|
|
8006
|
+
}
|
|
8007
|
+
const copied = [];
|
|
8008
|
+
const copySkipped = [];
|
|
8009
|
+
const primary = await deps.primaryCheckout();
|
|
8010
|
+
for (const rel of LOCAL_ONLY_FILES) {
|
|
8011
|
+
const dest = (0, import_node_path12.join)(worktreeRoot, rel);
|
|
8012
|
+
if (fs2.isFile(dest)) {
|
|
8013
|
+
copySkipped.push({ file: rel, reason: "already-present" });
|
|
8014
|
+
continue;
|
|
8015
|
+
}
|
|
8016
|
+
if (!primary) {
|
|
8017
|
+
copySkipped.push({ file: rel, reason: "no-primary" });
|
|
8018
|
+
continue;
|
|
8019
|
+
}
|
|
8020
|
+
if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
|
|
8021
|
+
copySkipped.push({ file: rel, reason: "absent-in-primary" });
|
|
8022
|
+
continue;
|
|
8023
|
+
}
|
|
8024
|
+
copyFile((0, import_node_path12.join)(primary, rel), dest);
|
|
8025
|
+
copied.push(rel);
|
|
8026
|
+
log(`copied local config: ${rel}`);
|
|
8027
|
+
}
|
|
8028
|
+
return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
|
|
8029
|
+
}
|
|
8030
|
+
function defaultWorktreePath(repoRoot, branch) {
|
|
8031
|
+
const safe = branch.replace(/[/\\]+/g, "-");
|
|
8032
|
+
return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot), "mmi-worktrees", safe);
|
|
8033
|
+
}
|
|
8034
|
+
|
|
8035
|
+
// src/frontmatter.ts
|
|
8036
|
+
function splitFrontmatter(content) {
|
|
8037
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
8038
|
+
if (!match) return { entries: [], body: content };
|
|
8039
|
+
return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
|
|
8040
|
+
}
|
|
8041
|
+
function entryKeyValue(line) {
|
|
8042
|
+
const i = line.indexOf(":");
|
|
8043
|
+
if (i < 0) return null;
|
|
8044
|
+
return { key: line.slice(0, i).trim().toLowerCase(), value: line.slice(i + 1).trim() };
|
|
8045
|
+
}
|
|
8046
|
+
function frontmatterValue(content, key) {
|
|
8047
|
+
const want = key.trim().toLowerCase();
|
|
7225
8048
|
for (const line of splitFrontmatter(content).entries) {
|
|
7226
8049
|
const kv = entryKeyValue(line);
|
|
7227
8050
|
if (kv && kv.key === want) return kv.value || void 0;
|
|
@@ -7404,7 +8227,7 @@ async function runNorthstarContext(io, deps) {
|
|
|
7404
8227
|
}
|
|
7405
8228
|
|
|
7406
8229
|
// src/index.ts
|
|
7407
|
-
var
|
|
8230
|
+
var import_node_path17 = require("node:path");
|
|
7408
8231
|
|
|
7409
8232
|
// src/merge-ci-policy.ts
|
|
7410
8233
|
function resolveMergeCiPolicy(input) {
|
|
@@ -7441,6 +8264,13 @@ function parseGhPrChecksOutput(stdout) {
|
|
|
7441
8264
|
}
|
|
7442
8265
|
return anyPending ? "pending" : "success";
|
|
7443
8266
|
}
|
|
8267
|
+
function parseGhPrChecksResult(stdout, stderr) {
|
|
8268
|
+
if (stdout.trim()) return parseGhPrChecksOutput(stdout);
|
|
8269
|
+
const err = (stderr ?? "").trim();
|
|
8270
|
+
if (/no checks reported/i.test(err)) return "no-checks-reported";
|
|
8271
|
+
if (!err) return "no-checks-reported";
|
|
8272
|
+
return "error";
|
|
8273
|
+
}
|
|
7444
8274
|
var PR_CHECKS_POLL_MS = 3e4;
|
|
7445
8275
|
var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
|
|
7446
8276
|
async function waitForPrChecks(deps) {
|
|
@@ -7501,12 +8331,500 @@ async function activateProductRuleset(repo, rulesetBody, client) {
|
|
|
7501
8331
|
return { action: "created" };
|
|
7502
8332
|
}
|
|
7503
8333
|
|
|
8334
|
+
// src/bootstrap-seeds.ts
|
|
8335
|
+
var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
8336
|
+
function loadBootstrapSeeds(manifestJson) {
|
|
8337
|
+
let parsed;
|
|
8338
|
+
try {
|
|
8339
|
+
parsed = JSON.parse(manifestJson);
|
|
8340
|
+
} catch {
|
|
8341
|
+
throw new Error("bootstrap seed manifest is not valid JSON");
|
|
8342
|
+
}
|
|
8343
|
+
const obj = parsed ?? {};
|
|
8344
|
+
const seeds = obj.seeds ?? [];
|
|
8345
|
+
for (const s of seeds) {
|
|
8346
|
+
if (!s || !s.target || !s.source || !Array.isArray(s.classes)) {
|
|
8347
|
+
throw new Error(`invalid seed entry (needs target, source, classes): ${JSON.stringify(s)}`);
|
|
8348
|
+
}
|
|
8349
|
+
if (s.ownership !== "org" && s.ownership !== "repo") {
|
|
8350
|
+
throw new Error(`invalid seed ownership '${s.ownership}' for ${s.target} (must be 'org' or 'repo')`);
|
|
8351
|
+
}
|
|
8352
|
+
}
|
|
8353
|
+
return {
|
|
8354
|
+
seeds,
|
|
8355
|
+
labels: obj.labels ?? [],
|
|
8356
|
+
placeholders: obj.placeholders ?? []
|
|
8357
|
+
};
|
|
8358
|
+
}
|
|
8359
|
+
function renderSeed(template, vars) {
|
|
8360
|
+
return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key] : match);
|
|
8361
|
+
}
|
|
8362
|
+
function missingPlaceholders(rendered) {
|
|
8363
|
+
const out = /* @__PURE__ */ new Set();
|
|
8364
|
+
for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
|
|
8365
|
+
return [...out];
|
|
8366
|
+
}
|
|
8367
|
+
var GITIGNORE_MANAGED_BEGIN = "# >>> mmi-managed >>>";
|
|
8368
|
+
var GITIGNORE_MANAGED_END = "# <<< mmi-managed <<<";
|
|
8369
|
+
var MANAGED_GITIGNORE_LINES = [
|
|
8370
|
+
'# Org-wide cleanliness (AGENTS.md "Repo cleanliness") \u2014 enforced by `mmi-cli doctor`.',
|
|
8371
|
+
"# Do not edit inside these markers; this block is regenerated on the next doctor run.",
|
|
8372
|
+
"/tmp/",
|
|
8373
|
+
// Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
|
|
8374
|
+
// via `mmi-cli plan push`, never git-tracked (AGENTS.md "Repo cleanliness", #1550).
|
|
8375
|
+
"**/plans/",
|
|
8376
|
+
".playwright-mcp/",
|
|
8377
|
+
".claude/worktrees/",
|
|
8378
|
+
// .mmi is agent/CI scratch — ignore the WHOLE tree at any depth (root + cli/.mmi, .github/workflows/.mmi,
|
|
8379
|
+
// and any future feature's subdir) so new scratch NEVER needs a new ignore line. `**/.mmi/*` ignores the
|
|
8380
|
+
// dir's contents (not the dir itself), so the one tracked file is re-included on the next line (#1550).
|
|
8381
|
+
// This replaces the brittle per-path enumeration (saga/honcho/head-ts/scrooge/… — #1472 was one such miss).
|
|
8382
|
+
"**/.mmi/*",
|
|
8383
|
+
// The single tracked .mmi file. A NEW tracked .mmi file would add its own `!` line here; everything else
|
|
8384
|
+
// under .mmi stays ignored automatically.
|
|
8385
|
+
"!**/.mmi/config.json",
|
|
8386
|
+
".aws-sam/",
|
|
8387
|
+
"/*.png",
|
|
8388
|
+
// Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
|
|
8389
|
+
// .env.example reference file stays tracked.
|
|
8390
|
+
".env",
|
|
8391
|
+
".env.*",
|
|
8392
|
+
"!.env.example"
|
|
8393
|
+
];
|
|
8394
|
+
function renderManagedGitignoreBlock() {
|
|
8395
|
+
return [GITIGNORE_MANAGED_BEGIN, ...MANAGED_GITIGNORE_LINES, GITIGNORE_MANAGED_END].join("\n");
|
|
8396
|
+
}
|
|
8397
|
+
function upsertManagedGitignoreBlock(current) {
|
|
8398
|
+
const block = renderManagedGitignoreBlock();
|
|
8399
|
+
const src = (current ?? "").replace(/\r\n/g, "\n");
|
|
8400
|
+
if (src.trim() === "") {
|
|
8401
|
+
const next2 = `${block}
|
|
8402
|
+
`;
|
|
8403
|
+
return { content: next2, changed: src !== next2 };
|
|
8404
|
+
}
|
|
8405
|
+
const managed = /* @__PURE__ */ new Set([GITIGNORE_MANAGED_BEGIN, GITIGNORE_MANAGED_END, ...MANAGED_GITIGNORE_LINES]);
|
|
8406
|
+
const lines = src.split("\n");
|
|
8407
|
+
const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
|
|
8408
|
+
const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
|
|
8409
|
+
let next;
|
|
8410
|
+
if (beginAt !== -1 && endAt !== -1) {
|
|
8411
|
+
const before = lines.slice(0, beginAt).filter((l) => !managed.has(l.trim()));
|
|
8412
|
+
const after = lines.slice(endAt + 1).filter((l) => !managed.has(l.trim()));
|
|
8413
|
+
next = `${[...before, ...block.split("\n"), ...after].join("\n").replace(/\n+$/, "")}
|
|
8414
|
+
`;
|
|
8415
|
+
} else {
|
|
8416
|
+
const kept = lines.filter((l) => !managed.has(l.trim())).join("\n").replace(/\n+$/, "");
|
|
8417
|
+
next = kept === "" ? `${block}
|
|
8418
|
+
` : `${kept}
|
|
8419
|
+
|
|
8420
|
+
${block}
|
|
8421
|
+
`;
|
|
8422
|
+
}
|
|
8423
|
+
return { content: next, changed: src !== next };
|
|
8424
|
+
}
|
|
8425
|
+
function diffManagedGitignoreBlock(current) {
|
|
8426
|
+
const lines = (current ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
8427
|
+
const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
|
|
8428
|
+
const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
|
|
8429
|
+
const hasMarker = beginAt !== -1 || lines.some((l) => l === GITIGNORE_MANAGED_END);
|
|
8430
|
+
if (!hasMarker) return { added: [], removed: [], seeded: true };
|
|
8431
|
+
const wellOrdered = beginAt !== -1 && endAt !== -1;
|
|
8432
|
+
if (!wellOrdered) return { added: [], removed: [], seeded: false };
|
|
8433
|
+
const isRule = (l) => l.trim() !== "" && !l.trim().startsWith("#");
|
|
8434
|
+
const oldBody = lines.slice(beginAt + 1, endAt).filter(isRule);
|
|
8435
|
+
const newBody = MANAGED_GITIGNORE_LINES.filter(isRule);
|
|
8436
|
+
return {
|
|
8437
|
+
added: newBody.filter((l) => !oldBody.includes(l)),
|
|
8438
|
+
removed: oldBody.filter((l) => !newBody.includes(l)),
|
|
8439
|
+
seeded: false
|
|
8440
|
+
};
|
|
8441
|
+
}
|
|
8442
|
+
|
|
8443
|
+
// src/project-model.ts
|
|
8444
|
+
var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
|
|
8445
|
+
var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
|
|
8446
|
+
var RELEASE_TRACKS = ["full", "direct", "trunk"];
|
|
8447
|
+
var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
|
|
8448
|
+
var DEPLOY_MODEL_SET = new Set(DEPLOY_MODELS);
|
|
8449
|
+
var RELEASE_TRACK_SET = new Set(RELEASE_TRACKS);
|
|
8450
|
+
function isProjectType(value) {
|
|
8451
|
+
return Boolean(value && PROJECT_TYPE_SET.has(value));
|
|
8452
|
+
}
|
|
8453
|
+
function isDeployModel(value) {
|
|
8454
|
+
return Boolean(value && DEPLOY_MODEL_SET.has(value));
|
|
8455
|
+
}
|
|
8456
|
+
function isReleaseTrack(value) {
|
|
8457
|
+
return Boolean(value && RELEASE_TRACK_SET.has(value));
|
|
8458
|
+
}
|
|
8459
|
+
function repoIsHub(repo) {
|
|
8460
|
+
return repo.toLowerCase().endsWith("/mmi-hub") || repo.toLowerCase() === "mmi-hub";
|
|
8461
|
+
}
|
|
8462
|
+
function resolveProjectTypeConfident(meta, repo) {
|
|
8463
|
+
const rawType = typeof meta?.projectType === "string" ? meta.projectType : void 0;
|
|
8464
|
+
if (isProjectType(rawType)) return rawType;
|
|
8465
|
+
if (meta?.class === "content" || meta?.deployModel === "content") return "content";
|
|
8466
|
+
if (meta?.deployModel === "hub-serverless" || repoIsHub(repo)) return "hub-service";
|
|
8467
|
+
if (meta?.deployModel === "registry-publish") return "cli-tool";
|
|
8468
|
+
if (meta?.deployModel === "solo-container") return "worker";
|
|
8469
|
+
if (meta?.deployModel === "none") return "non-deployable";
|
|
8470
|
+
return void 0;
|
|
8471
|
+
}
|
|
8472
|
+
function resolveProjectType(meta, repo) {
|
|
8473
|
+
return resolveProjectTypeConfident(meta, repo) ?? "web-app";
|
|
8474
|
+
}
|
|
8475
|
+
function resolveDeployModel(meta, repo) {
|
|
8476
|
+
const rawModel = typeof meta?.deployModel === "string" ? meta.deployModel : void 0;
|
|
8477
|
+
if (isDeployModel(rawModel)) return rawModel;
|
|
8478
|
+
const projectType = resolveProjectType(meta, repo);
|
|
8479
|
+
if (projectType === "content" || meta?.class === "content") return "content";
|
|
8480
|
+
if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
|
|
8481
|
+
if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
|
|
8482
|
+
if (projectType === "cli-tool") return "registry-publish";
|
|
8483
|
+
return "tenant-container";
|
|
8484
|
+
}
|
|
8485
|
+
function projectTypeClearsWebProfile(projectType, deployModel) {
|
|
8486
|
+
return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
|
|
8487
|
+
}
|
|
8488
|
+
function inferReleaseTrackFromBranches(hints) {
|
|
8489
|
+
if (hints.hasRcBranch) return void 0;
|
|
8490
|
+
if (hints.hasDevelopmentBranch && hints.hasMainBranch) return "direct";
|
|
8491
|
+
return void 0;
|
|
8492
|
+
}
|
|
8493
|
+
function resolveBootstrapReleaseTrack(cls, explicit) {
|
|
8494
|
+
if (isReleaseTrack(explicit)) return explicit;
|
|
8495
|
+
if (cls === "content") return "trunk";
|
|
8496
|
+
return "full";
|
|
8497
|
+
}
|
|
8498
|
+
function resolveReleaseTrack(meta, hints) {
|
|
8499
|
+
const raw = typeof meta?.releaseTrack === "string" ? meta.releaseTrack : void 0;
|
|
8500
|
+
if (isReleaseTrack(raw)) return raw;
|
|
8501
|
+
if (meta?.class === "content" || meta?.deployModel === "content") return "trunk";
|
|
8502
|
+
const inferred = hints ? inferReleaseTrackFromBranches(hints) : void 0;
|
|
8503
|
+
if (inferred) return inferred;
|
|
8504
|
+
return "full";
|
|
8505
|
+
}
|
|
8506
|
+
function branchesForTrack(track) {
|
|
8507
|
+
if (track === "trunk") return ["main"];
|
|
8508
|
+
if (track === "direct") return ["development", "main"];
|
|
8509
|
+
return ["development", "rc", "main"];
|
|
8510
|
+
}
|
|
8511
|
+
|
|
8512
|
+
// src/bootstrap-apply.ts
|
|
8513
|
+
function parseOwnerRepo(repo) {
|
|
8514
|
+
const trimmed = repo.trim();
|
|
8515
|
+
const parts = trimmed.split("/");
|
|
8516
|
+
if (parts.length !== 2 || !parts[0]?.trim() || !parts[1]?.trim()) {
|
|
8517
|
+
throw new Error("repo must be owner/repo");
|
|
8518
|
+
}
|
|
8519
|
+
const owner = parts[0].trim();
|
|
8520
|
+
const name = parts[1].trim();
|
|
8521
|
+
if (owner.includes("\\") || name.includes("\\")) throw new Error("repo must be owner/repo");
|
|
8522
|
+
return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
|
|
8523
|
+
}
|
|
8524
|
+
var DEFAULT_INSTALL_CMD = "npm ci";
|
|
8525
|
+
var DEFAULT_GATE_CMD = "npm run check";
|
|
8526
|
+
var DEFAULT_GATE_PY_VERSION = "3.11";
|
|
8527
|
+
var GATE_RUNTIME_DEFAULTS = {
|
|
8528
|
+
node: { cmd: DEFAULT_GATE_CMD, install: DEFAULT_INSTALL_CMD },
|
|
8529
|
+
python: { cmd: "pytest", install: 'pip install -e ".[dev]"' }
|
|
8530
|
+
};
|
|
8531
|
+
function gateSeedVars(cls, releaseTrack, runtime = "node") {
|
|
8532
|
+
const rt = GATE_RUNTIME_DEFAULTS[runtime] ?? GATE_RUNTIME_DEFAULTS.node;
|
|
8533
|
+
const runtimeVars = {
|
|
8534
|
+
GATE_RUNTIME: runtime,
|
|
8535
|
+
GATE_CMD: rt.cmd,
|
|
8536
|
+
GATE_INSTALL_CMD: rt.install,
|
|
8537
|
+
GATE_WORKDIR: ".",
|
|
8538
|
+
GATE_CACHE_DEP_PATH: "package-lock.json",
|
|
8539
|
+
GATE_PY_VERSION: DEFAULT_GATE_PY_VERSION
|
|
8540
|
+
};
|
|
8541
|
+
const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
|
|
8542
|
+
if (track === "trunk") {
|
|
8543
|
+
return {
|
|
8544
|
+
...runtimeVars,
|
|
8545
|
+
GATE_PUSH_BRANCHES_YAML: "[main]",
|
|
8546
|
+
GATE_FULL_RUN_BRANCH: "main",
|
|
8547
|
+
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/main"]'
|
|
8548
|
+
};
|
|
8549
|
+
}
|
|
8550
|
+
if (track === "direct") {
|
|
8551
|
+
return {
|
|
8552
|
+
...runtimeVars,
|
|
8553
|
+
GATE_PUSH_BRANCHES_YAML: "[development, main]",
|
|
8554
|
+
GATE_FULL_RUN_BRANCH: "development",
|
|
8555
|
+
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/main"]'
|
|
8556
|
+
};
|
|
8557
|
+
}
|
|
8558
|
+
return {
|
|
8559
|
+
...runtimeVars,
|
|
8560
|
+
GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
|
|
8561
|
+
GATE_FULL_RUN_BRANCH: "development",
|
|
8562
|
+
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/rc", "refs/heads/main"]'
|
|
8563
|
+
};
|
|
8564
|
+
}
|
|
8565
|
+
function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
|
|
8566
|
+
const out = { ...vars };
|
|
8567
|
+
out.REPO_NAME ??= parsed.name;
|
|
8568
|
+
out.REPO_SLUG ??= parsed.slug;
|
|
8569
|
+
out.CLASS ??= cls;
|
|
8570
|
+
out.INSTALL_CMD ??= DEFAULT_INSTALL_CMD;
|
|
8571
|
+
const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
|
|
8572
|
+
const runtime = out.GATE_RUNTIME === "python" ? "python" : "node";
|
|
8573
|
+
for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime))) {
|
|
8574
|
+
out[key] ??= value;
|
|
8575
|
+
}
|
|
8576
|
+
return out;
|
|
8577
|
+
}
|
|
8578
|
+
function gateConfigToVars(gate) {
|
|
8579
|
+
const out = {};
|
|
8580
|
+
if (!gate || typeof gate !== "object") return out;
|
|
8581
|
+
if (gate.runtime === "node" || gate.runtime === "python") out.GATE_RUNTIME = gate.runtime;
|
|
8582
|
+
if (typeof gate.cmd === "string" && gate.cmd.trim()) out.GATE_CMD = gate.cmd;
|
|
8583
|
+
if (typeof gate.workdir === "string" && gate.workdir.trim()) out.GATE_WORKDIR = gate.workdir;
|
|
8584
|
+
if (typeof gate.cacheDepPath === "string" && gate.cacheDepPath.trim()) out.GATE_CACHE_DEP_PATH = gate.cacheDepPath;
|
|
8585
|
+
if (typeof gate.pyVersion === "string" && gate.pyVersion.trim()) out.GATE_PY_VERSION = gate.pyVersion;
|
|
8586
|
+
return out;
|
|
8587
|
+
}
|
|
8588
|
+
function planSeedAction(seed, exists) {
|
|
8589
|
+
if (seed.source === "fanout") {
|
|
8590
|
+
return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
|
|
8591
|
+
}
|
|
8592
|
+
if (seed.source === "managed-block") {
|
|
8593
|
+
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
|
|
8594
|
+
}
|
|
8595
|
+
if (seed.ownership === "repo") {
|
|
8596
|
+
return exists ? { target: seed.target, action: "skip", ownership: "repo", reason: "repo-owned, already present (never clobbered)" } : { target: seed.target, action: "create", ownership: "repo", reason: "repo-owned, missing" };
|
|
8597
|
+
}
|
|
8598
|
+
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
|
|
8599
|
+
}
|
|
8600
|
+
function reconcileSeedAction(action, content, isManagedBlock) {
|
|
8601
|
+
if (action.action === "skip") return action;
|
|
8602
|
+
if (content == null) {
|
|
8603
|
+
return { ...action, action: "skip", reason: "no resolvable content" };
|
|
8604
|
+
}
|
|
8605
|
+
if (isManagedBlock) return action;
|
|
8606
|
+
const unfilled = missingPlaceholders(content);
|
|
8607
|
+
if (unfilled.length) {
|
|
8608
|
+
return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
|
|
8609
|
+
}
|
|
8610
|
+
return action;
|
|
8611
|
+
}
|
|
8612
|
+
function renderSeedPlan(actions) {
|
|
8613
|
+
const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
|
|
8614
|
+
for (const a of actions) {
|
|
8615
|
+
lines.push(` ${a.action.toUpperCase().padEnd(6)} ${a.target} (${a.ownership}: ${a.reason})`);
|
|
8616
|
+
}
|
|
8617
|
+
const order = ["create", "update", "skip"];
|
|
8618
|
+
lines.push(` \u2014 ${order.map((k) => `${actions.filter((a) => a.action === k).length} ${k}`).join(", ")}`);
|
|
8619
|
+
return lines.join("\n");
|
|
8620
|
+
}
|
|
8621
|
+
var GITHUB_DEFAULT_LABELS = [
|
|
8622
|
+
"documentation",
|
|
8623
|
+
"duplicate",
|
|
8624
|
+
"enhancement",
|
|
8625
|
+
"good first issue",
|
|
8626
|
+
"help wanted",
|
|
8627
|
+
"invalid",
|
|
8628
|
+
"question",
|
|
8629
|
+
"wontfix"
|
|
8630
|
+
];
|
|
8631
|
+
function labelsToPrune(orgLabelNames) {
|
|
8632
|
+
const org = new Set(orgLabelNames);
|
|
8633
|
+
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
8634
|
+
}
|
|
8635
|
+
function resolveSeedContent(seed, vars, readFile6) {
|
|
8636
|
+
if (seed.source === "self") return readFile6(seed.target);
|
|
8637
|
+
if (seed.source.startsWith("seed:")) {
|
|
8638
|
+
const tmpl = readFile6(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
8639
|
+
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
8640
|
+
}
|
|
8641
|
+
return null;
|
|
8642
|
+
}
|
|
8643
|
+
function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
8644
|
+
const parsedRepo = parseOwnerRepo(repo);
|
|
8645
|
+
const slug = parsedRepo.slug;
|
|
8646
|
+
if (options.projectType && !isProjectType(options.projectType)) {
|
|
8647
|
+
throw new Error(`projectType must be one of: ${PROJECT_TYPES.join(", ")}`);
|
|
8648
|
+
}
|
|
8649
|
+
if (options.deployModel && !isDeployModel(options.deployModel)) {
|
|
8650
|
+
throw new Error(`deployModel must be one of: ${DEPLOY_MODELS.join(", ")}`);
|
|
8651
|
+
}
|
|
8652
|
+
if (options.releaseTrack && !isReleaseTrack(options.releaseTrack)) {
|
|
8653
|
+
throw new Error("releaseTrack must be full, direct, or trunk");
|
|
8654
|
+
}
|
|
8655
|
+
const shape = {
|
|
8656
|
+
class: cls,
|
|
8657
|
+
projectType: options.projectType,
|
|
8658
|
+
deployModel: options.deployModel
|
|
8659
|
+
};
|
|
8660
|
+
const projectType = resolveProjectTypeConfident(shape, parsedRepo.fullName);
|
|
8661
|
+
if (!projectType) {
|
|
8662
|
+
throw new Error(
|
|
8663
|
+
`Project type for ${parsedRepo.fullName} is unset and not derivable \u2014 pass --project-type <${PROJECT_TYPES.join("|")}> and --deploy-model <${DEPLOY_MODELS.join("|")}> (prevents defaulting a non-web repo to tenant-container).`
|
|
8664
|
+
);
|
|
8665
|
+
}
|
|
8666
|
+
const deployModel = resolveDeployModel({ ...shape, projectType }, parsedRepo.fullName);
|
|
8667
|
+
const num = (v) => {
|
|
8668
|
+
if (v == null || v === "") return void 0;
|
|
8669
|
+
const n = Number(v);
|
|
8670
|
+
return Number.isFinite(n) ? n : void 0;
|
|
8671
|
+
};
|
|
8672
|
+
const statusOptions = vars.STATUS_TODO || vars.STATUS_IN_PROGRESS || vars.STATUS_IN_REVIEW || vars.STATUS_DONE ? {
|
|
8673
|
+
Todo: vars.STATUS_TODO,
|
|
8674
|
+
"In Progress": vars.STATUS_IN_PROGRESS,
|
|
8675
|
+
"In Review": vars.STATUS_IN_REVIEW,
|
|
8676
|
+
Done: vars.STATUS_DONE
|
|
8677
|
+
} : void 0;
|
|
8678
|
+
const priorityOptions = vars.PRIORITY_URGENT || vars.PRIORITY_HIGH || vars.PRIORITY_MEDIUM || vars.PRIORITY_LOW ? {
|
|
8679
|
+
Urgent: vars.PRIORITY_URGENT,
|
|
8680
|
+
High: vars.PRIORITY_HIGH,
|
|
8681
|
+
Medium: vars.PRIORITY_MEDIUM,
|
|
8682
|
+
Low: vars.PRIORITY_LOW
|
|
8683
|
+
} : void 0;
|
|
8684
|
+
const payload = {
|
|
8685
|
+
slug,
|
|
8686
|
+
// Identity. name/division default off the repo name when the skill didn't pass them.
|
|
8687
|
+
name: vars.NAME || parsedRepo.name,
|
|
8688
|
+
division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
|
|
8689
|
+
repos: [`${parsedRepo.owner}/${slug}`],
|
|
8690
|
+
wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
|
|
8691
|
+
branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
|
|
8692
|
+
class: cls,
|
|
8693
|
+
projectType,
|
|
8694
|
+
deployModel,
|
|
8695
|
+
// #1359: always persist an explicit track so release tooling never guesses from absence alone.
|
|
8696
|
+
releaseTrack: resolveBootstrapReleaseTrack(cls, options.releaseTrack),
|
|
8697
|
+
// Board coords (from GraphQL at bootstrap, passed as --var by the skill).
|
|
8698
|
+
projectOwner: vars.PROJECT_OWNER || void 0,
|
|
8699
|
+
projectNumber: num(vars.PROJECT_NUMBER),
|
|
8700
|
+
projectId: vars.PROJECT_ID || void 0,
|
|
8701
|
+
statusFieldId: vars.STATUS_FIELD_ID || void 0,
|
|
8702
|
+
statusOptions,
|
|
8703
|
+
priorityFieldId: vars.PRIORITY_FIELD_ID || void 0,
|
|
8704
|
+
priorityOptions,
|
|
8705
|
+
// Pointers. vaultPath is explicit + canonical; kbPointer is the per-project KB doc path.
|
|
8706
|
+
vaultPath: `/mmi-future/${slug}`,
|
|
8707
|
+
kbPointer: `kb/projects/${slug}.md`
|
|
8708
|
+
};
|
|
8709
|
+
for (const k of Object.keys(payload)) if (payload[k] === void 0) delete payload[k];
|
|
8710
|
+
if (payload.projectId && payload.projectNumber == null) {
|
|
8711
|
+
throw new Error(
|
|
8712
|
+
"bootstrap apply: PROJECT_ID is set but PROJECT_NUMBER is missing \u2014 pass --var PROJECT_NUMBER=<N> or ensure the live board GraphQL query succeeds"
|
|
8713
|
+
);
|
|
8714
|
+
}
|
|
8715
|
+
return payload;
|
|
8716
|
+
}
|
|
8717
|
+
var BOARD_FIELD_VAR_MAP = {
|
|
8718
|
+
Status: {
|
|
8719
|
+
fieldIdVar: "STATUS_FIELD_ID",
|
|
8720
|
+
options: { Todo: "STATUS_TODO", "In Progress": "STATUS_IN_PROGRESS", "In Review": "STATUS_IN_REVIEW", Done: "STATUS_DONE" }
|
|
8721
|
+
},
|
|
8722
|
+
Priority: {
|
|
8723
|
+
fieldIdVar: "PRIORITY_FIELD_ID",
|
|
8724
|
+
options: { Urgent: "PRIORITY_URGENT", High: "PRIORITY_HIGH", Medium: "PRIORITY_MEDIUM", Low: "PRIORITY_LOW" }
|
|
8725
|
+
}
|
|
8726
|
+
};
|
|
8727
|
+
function projectV2BoardNode(fieldsJson) {
|
|
8728
|
+
if (Array.isArray(fieldsJson)) return { fields: { nodes: fieldsJson } };
|
|
8729
|
+
const wrapped = fieldsJson;
|
|
8730
|
+
return wrapped?.data?.node ?? wrapped?.node;
|
|
8731
|
+
}
|
|
8732
|
+
function extractBoardFieldVars(fieldsJson) {
|
|
8733
|
+
const out = {};
|
|
8734
|
+
const projectNode = projectV2BoardNode(fieldsJson);
|
|
8735
|
+
if (typeof projectNode?.number === "number" && Number.isFinite(projectNode.number)) {
|
|
8736
|
+
out.PROJECT_NUMBER = String(projectNode.number);
|
|
8737
|
+
}
|
|
8738
|
+
const nodes = projectNode?.fields?.nodes;
|
|
8739
|
+
if (!Array.isArray(nodes)) return out;
|
|
8740
|
+
for (const node of nodes) {
|
|
8741
|
+
const field = node;
|
|
8742
|
+
const name = typeof field.name === "string" ? field.name : void 0;
|
|
8743
|
+
const map = name ? BOARD_FIELD_VAR_MAP[name] : void 0;
|
|
8744
|
+
if (!map || typeof field.id !== "string" || !field.id) continue;
|
|
8745
|
+
out[map.fieldIdVar] = field.id;
|
|
8746
|
+
const options = Array.isArray(field.options) ? field.options : [];
|
|
8747
|
+
for (const opt of options) {
|
|
8748
|
+
const o = opt;
|
|
8749
|
+
const varName = typeof o.name === "string" ? map.options[o.name] : void 0;
|
|
8750
|
+
if (varName && typeof o.id === "string" && o.id) out[varName] = o.id;
|
|
8751
|
+
}
|
|
8752
|
+
}
|
|
8753
|
+
return out;
|
|
8754
|
+
}
|
|
8755
|
+
function boardFieldsQueryArgs(projectId) {
|
|
8756
|
+
const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
|
|
8757
|
+
return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
|
|
8758
|
+
}
|
|
8759
|
+
function serializeRegistry(obj) {
|
|
8760
|
+
return `${JSON.stringify(obj, null, 2)}
|
|
8761
|
+
`;
|
|
8762
|
+
}
|
|
8763
|
+
function planFanoutRegistration(fanoutTargetsRaw, projectsRaw, entry) {
|
|
8764
|
+
const fanout = JSON.parse(fanoutTargetsRaw);
|
|
8765
|
+
const projects = JSON.parse(projectsRaw);
|
|
8766
|
+
const fanoutRepos = Array.isArray(fanout.repos) ? fanout.repos : [];
|
|
8767
|
+
const projectEntries = Array.isArray(projects.projects) ? projects.projects : [];
|
|
8768
|
+
const name = entry.name ?? entry.repo;
|
|
8769
|
+
const canonName = entry.repo.toLowerCase();
|
|
8770
|
+
const inFanout = fanoutRepos.some((r) => typeof r.repo === "string" && r.repo.toLowerCase() === canonName);
|
|
8771
|
+
const inProjects = projectEntries.some(
|
|
8772
|
+
(p) => p.slug === entry.slug || Array.isArray(p.repos) && p.repos.some((full) => String(full).split("/").pop()?.toLowerCase() === canonName)
|
|
8773
|
+
);
|
|
8774
|
+
if (inFanout || inProjects) {
|
|
8775
|
+
return { changed: false, fanoutTargets: fanoutTargetsRaw, projects: projectsRaw };
|
|
8776
|
+
}
|
|
8777
|
+
const projectEntry = {
|
|
8778
|
+
name,
|
|
8779
|
+
slug: entry.slug,
|
|
8780
|
+
projectId: entry.projectId,
|
|
8781
|
+
wikiRepo: entry.wikiRepo,
|
|
8782
|
+
repos: [`mutmutco/${entry.repo}`]
|
|
8783
|
+
};
|
|
8784
|
+
if (entry.cls === "content") projectEntry.branch = "main";
|
|
8785
|
+
for (const k of Object.keys(projectEntry)) if (projectEntry[k] === void 0) delete projectEntry[k];
|
|
8786
|
+
const nextProjects = { ...projects, projects: [...projectEntries, projectEntry] };
|
|
8787
|
+
const fanoutEntry = { repo: entry.repo, branch: entry.branch, class: entry.cls };
|
|
8788
|
+
const nextFanout = { ...fanout, repos: [...fanoutRepos, fanoutEntry] };
|
|
8789
|
+
return {
|
|
8790
|
+
changed: true,
|
|
8791
|
+
fanoutTargets: serializeRegistry(nextFanout),
|
|
8792
|
+
projects: serializeRegistry(nextProjects)
|
|
8793
|
+
};
|
|
8794
|
+
}
|
|
8795
|
+
function decideFanoutPrAction(openPrs) {
|
|
8796
|
+
const list = Array.isArray(openPrs) ? openPrs : [];
|
|
8797
|
+
for (const pr2 of list) {
|
|
8798
|
+
const url = pr2?.url;
|
|
8799
|
+
if (typeof url === "string" && url.trim()) return { action: "reuse", url: url.trim() };
|
|
8800
|
+
}
|
|
8801
|
+
return { action: "create" };
|
|
8802
|
+
}
|
|
8803
|
+
function contentPutArgs(repo, path2, content, branch, sha) {
|
|
8804
|
+
const args = [
|
|
8805
|
+
"api",
|
|
8806
|
+
"-X",
|
|
8807
|
+
"PUT",
|
|
8808
|
+
`repos/${repo}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
|
|
8809
|
+
"-f",
|
|
8810
|
+
`message=bootstrap: seed ${path2}`,
|
|
8811
|
+
"-f",
|
|
8812
|
+
`content=${Buffer.from(content, "utf8").toString("base64")}`,
|
|
8813
|
+
"-f",
|
|
8814
|
+
`branch=${branch}`
|
|
8815
|
+
];
|
|
8816
|
+
if (sha) args.push("-f", `sha=${sha}`);
|
|
8817
|
+
return args;
|
|
8818
|
+
}
|
|
8819
|
+
|
|
7504
8820
|
// src/ci-audit.ts
|
|
7505
8821
|
var HUB_REPO = "mutmutco/MMI-Hub";
|
|
7506
8822
|
var PRODUCT_GATE_CONTEXT2 = "gate";
|
|
7507
8823
|
var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
|
|
7508
8824
|
var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
|
|
7509
8825
|
var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
|
|
8826
|
+
var GATE_TEMPLATE_SEED = "seed:gate.template.yml";
|
|
8827
|
+
var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
|
|
7510
8828
|
function slugFromRepo(repo) {
|
|
7511
8829
|
return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
7512
8830
|
}
|
|
@@ -7737,10 +9055,76 @@ async function fetchRulesetSeedBody(deps, repo) {
|
|
|
7737
9055
|
return null;
|
|
7738
9056
|
}
|
|
7739
9057
|
}
|
|
9058
|
+
async function contentSha(deps, repo, branch, path2) {
|
|
9059
|
+
try {
|
|
9060
|
+
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
9061
|
+
const file = await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
9062
|
+
return typeof file?.sha === "string" ? file.sha : void 0;
|
|
9063
|
+
} catch {
|
|
9064
|
+
return void 0;
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
function renderSeedBody(deps, seedSource, target, vars) {
|
|
9068
|
+
if (deps.renderSeed) return deps.renderSeed(seedSource, target, vars);
|
|
9069
|
+
if (!deps.readSeedFile) return null;
|
|
9070
|
+
return resolveSeedContent({ target, source: seedSource, ownership: "org", classes: ["deployable"] }, vars, deps.readSeedFile);
|
|
9071
|
+
}
|
|
9072
|
+
async function putSeedFile(deps, repo, path2, content, branch) {
|
|
9073
|
+
const sha = await contentSha(deps, repo, branch, path2);
|
|
9074
|
+
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
9075
|
+
const body = {
|
|
9076
|
+
message: `ci reconcile: re-seed ${path2} (#1550)`,
|
|
9077
|
+
content: Buffer.from(content, "utf8").toString("base64"),
|
|
9078
|
+
branch
|
|
9079
|
+
};
|
|
9080
|
+
if (sha) body.sha = sha;
|
|
9081
|
+
await deps.client.rest("PUT", `repos/${repo}/contents/${encodedPath}`, { body });
|
|
9082
|
+
}
|
|
9083
|
+
async function seedGateYml(repo, deps, meta, result) {
|
|
9084
|
+
const baseBranch = "development";
|
|
9085
|
+
if (await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH)) return;
|
|
9086
|
+
if (!deps.readSeedFile && !deps.renderSeed) {
|
|
9087
|
+
result.skipped.push(`gate.yml missing but no seed-template source wired \u2014 run bootstrap apply`);
|
|
9088
|
+
return;
|
|
9089
|
+
}
|
|
9090
|
+
const gate = meta?.gate;
|
|
9091
|
+
if (!gate || typeof gate === "object" && Object.keys(gate).length === 0) {
|
|
9092
|
+
result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli project set ${repo} --var gate={...}\` first, then re-run reconcile`);
|
|
9093
|
+
return;
|
|
9094
|
+
}
|
|
9095
|
+
const releaseTrack = isReleaseTrack(meta?.releaseTrack) ? meta?.releaseTrack : void 0;
|
|
9096
|
+
const parsed = parseOwnerRepo(repo);
|
|
9097
|
+
const derivedVars = withDerivedRepoVars({ ...gateConfigToVars(gate), REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
|
|
9098
|
+
const rendered = renderSeedBody(deps, GATE_TEMPLATE_SEED, PRODUCT_GATE_PATH, derivedVars);
|
|
9099
|
+
if (rendered == null) {
|
|
9100
|
+
result.errors.push(`gate.yml re-seed: could not render ${GATE_TEMPLATE_SEED} (template unreadable or unfilled)`);
|
|
9101
|
+
return;
|
|
9102
|
+
}
|
|
9103
|
+
try {
|
|
9104
|
+
await putSeedFile(deps, repo, PRODUCT_GATE_PATH, rendered, baseBranch);
|
|
9105
|
+
result.applied.push(`seeded ${PRODUCT_GATE_PATH}`);
|
|
9106
|
+
} catch (e) {
|
|
9107
|
+
result.errors.push(`gate.yml re-seed failed: ${e.message}`);
|
|
9108
|
+
return;
|
|
9109
|
+
}
|
|
9110
|
+
if (!await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF)) {
|
|
9111
|
+
const rulesetBody = renderSeedBody(deps, RULESET_TEMPLATE_SEED, PRODUCT_RULESET_REF, derivedVars);
|
|
9112
|
+
if (rulesetBody != null) {
|
|
9113
|
+
try {
|
|
9114
|
+
await putSeedFile(deps, repo, PRODUCT_RULESET_REF, rulesetBody, baseBranch);
|
|
9115
|
+
result.applied.push(`seeded ${PRODUCT_RULESET_REF}`);
|
|
9116
|
+
} catch (e) {
|
|
9117
|
+
result.errors.push(`ruleset reference re-seed failed: ${e.message}`);
|
|
9118
|
+
}
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
7740
9122
|
async function applyCiReconcileRepo(repo, deps) {
|
|
7741
9123
|
const merge = await applyCiReconcileMergeSettings(repo, deps);
|
|
9124
|
+
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
7742
9125
|
const report = await auditRepoCi(repo, deps);
|
|
7743
9126
|
if (report.class !== "deployable") return merge;
|
|
9127
|
+
await seedGateYml(repo, deps, meta, merge);
|
|
7744
9128
|
const gateCheck = report.checks.find((c) => c.label === "product gate status check active");
|
|
7745
9129
|
if (gateCheck?.ok) {
|
|
7746
9130
|
merge.skipped.push("product ruleset already active");
|
|
@@ -7811,13 +9195,13 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
7811
9195
|
}
|
|
7812
9196
|
|
|
7813
9197
|
// src/index.ts
|
|
7814
|
-
var
|
|
9198
|
+
var import_node_os6 = require("node:os");
|
|
7815
9199
|
|
|
7816
9200
|
// src/gh-create.ts
|
|
7817
|
-
var
|
|
7818
|
-
var
|
|
7819
|
-
var
|
|
7820
|
-
var
|
|
9201
|
+
var import_promises5 = require("node:fs/promises");
|
|
9202
|
+
var import_node_os4 = require("node:os");
|
|
9203
|
+
var import_node_path13 = require("node:path");
|
|
9204
|
+
var import_node_crypto6 = require("node:crypto");
|
|
7821
9205
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
7822
9206
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
7823
9207
|
function timeoutKillNote(err, timeoutMs) {
|
|
@@ -7855,9 +9239,9 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
7855
9239
|
const i = args.indexOf("--body");
|
|
7856
9240
|
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
7857
9241
|
} };
|
|
7858
|
-
const write = deps.write ??
|
|
7859
|
-
const remove = deps.remove ??
|
|
7860
|
-
const file = (0,
|
|
9242
|
+
const write = deps.write ?? import_promises5.writeFile;
|
|
9243
|
+
const remove = deps.remove ?? import_promises5.unlink;
|
|
9244
|
+
const file = (0, import_node_path13.join)(deps.dir ?? (0, import_node_os4.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
|
|
7861
9245
|
await write(file, args[i + 1], "utf8");
|
|
7862
9246
|
return {
|
|
7863
9247
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -7899,6 +9283,76 @@ function buildPrArgs({ title, body, base: base2, head, repo }) {
|
|
|
7899
9283
|
return args;
|
|
7900
9284
|
}
|
|
7901
9285
|
|
|
9286
|
+
// src/command-manifest.ts
|
|
9287
|
+
function buildArgument(arg) {
|
|
9288
|
+
const out = { name: arg.name(), required: arg.required, variadic: arg.variadic };
|
|
9289
|
+
if (arg.description) out.description = arg.description;
|
|
9290
|
+
return out;
|
|
9291
|
+
}
|
|
9292
|
+
function buildOption(opt) {
|
|
9293
|
+
const out = {
|
|
9294
|
+
flags: opt.flags,
|
|
9295
|
+
mandatory: Boolean(opt.mandatory),
|
|
9296
|
+
valueRequired: Boolean(opt.required),
|
|
9297
|
+
optional: Boolean(opt.optional),
|
|
9298
|
+
takesValue: Boolean(opt.required || opt.optional),
|
|
9299
|
+
variadic: Boolean(opt.variadic),
|
|
9300
|
+
negate: Boolean(opt.negate)
|
|
9301
|
+
};
|
|
9302
|
+
if (opt.description) out.description = opt.description;
|
|
9303
|
+
if (opt.defaultValue !== void 0) out.default = opt.defaultValue;
|
|
9304
|
+
return out;
|
|
9305
|
+
}
|
|
9306
|
+
function buildCommand(cmd, path2) {
|
|
9307
|
+
const out = {
|
|
9308
|
+
name: cmd.name(),
|
|
9309
|
+
path: path2,
|
|
9310
|
+
arguments: cmd.registeredArguments.map(buildArgument),
|
|
9311
|
+
options: cmd.options.map(buildOption),
|
|
9312
|
+
subcommands: cmd.commands.map(
|
|
9313
|
+
(child) => buildCommand(child, path2 ? `${path2} ${child.name()}` : child.name())
|
|
9314
|
+
)
|
|
9315
|
+
};
|
|
9316
|
+
const description = cmd.description();
|
|
9317
|
+
if (description) out.description = description;
|
|
9318
|
+
return out;
|
|
9319
|
+
}
|
|
9320
|
+
function collectLeaves(node, acc) {
|
|
9321
|
+
if (node.subcommands.length === 0) {
|
|
9322
|
+
if (node.path) acc.push(node);
|
|
9323
|
+
return;
|
|
9324
|
+
}
|
|
9325
|
+
for (const child of node.subcommands) collectLeaves(child, acc);
|
|
9326
|
+
}
|
|
9327
|
+
function buildCommandManifest(program3) {
|
|
9328
|
+
const tree = buildCommand(program3, "");
|
|
9329
|
+
const index = [];
|
|
9330
|
+
collectLeaves(tree, index);
|
|
9331
|
+
const manifest = { name: tree.name, tree, index };
|
|
9332
|
+
const version = program3.version();
|
|
9333
|
+
if (version) manifest.version = version;
|
|
9334
|
+
return manifest;
|
|
9335
|
+
}
|
|
9336
|
+
function argSignature(arg) {
|
|
9337
|
+
const inner = arg.variadic ? `${arg.name}...` : arg.name;
|
|
9338
|
+
return arg.required ? `<${inner}>` : `[${inner}]`;
|
|
9339
|
+
}
|
|
9340
|
+
function formatManifestHuman(manifest) {
|
|
9341
|
+
const lines = [];
|
|
9342
|
+
const render = (node, depth) => {
|
|
9343
|
+
const indent = " ".repeat(depth);
|
|
9344
|
+
const args = node.arguments.map(argSignature).join(" ");
|
|
9345
|
+
const head = depth === 0 ? `${node.name}${manifest.version ? ` v${manifest.version}` : ""}` : node.name + (args ? ` ${args}` : "");
|
|
9346
|
+
lines.push(node.description ? `${indent}${head} \u2014 ${node.description}` : `${indent}${head}`);
|
|
9347
|
+
for (const opt of node.options) {
|
|
9348
|
+
lines.push(`${indent} ${opt.flags}${opt.description ? ` ${opt.description}` : ""}`);
|
|
9349
|
+
}
|
|
9350
|
+
for (const child of node.subcommands) render(child, depth + 1);
|
|
9351
|
+
};
|
|
9352
|
+
render(manifest.tree, 0);
|
|
9353
|
+
return lines.join("\n");
|
|
9354
|
+
}
|
|
9355
|
+
|
|
7902
9356
|
// src/config-discovery.ts
|
|
7903
9357
|
function stripMutableBoardConfig(cfg) {
|
|
7904
9358
|
const {
|
|
@@ -8182,7 +9636,7 @@ ${buildReportBody(body, sourceRepo)}`;
|
|
|
8182
9636
|
|
|
8183
9637
|
// src/skill-lesson.ts
|
|
8184
9638
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
8185
|
-
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
9639
|
+
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "handoff", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
8186
9640
|
function assertSkillName(name) {
|
|
8187
9641
|
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
8188
9642
|
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
@@ -8563,6 +10017,36 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
8563
10017
|
}
|
|
8564
10018
|
return { status: "failed", attempts, error: errorMessage(lastError) };
|
|
8565
10019
|
}
|
|
10020
|
+
var NO_NESTED_LINKS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
10021
|
+
function baseName(p) {
|
|
10022
|
+
return p.replace(/[/\\]+$/, "").split(/[/\\]/).pop() ?? "";
|
|
10023
|
+
}
|
|
10024
|
+
function assertSafeWorktreeTarget(worktreePath, primaryCheckout) {
|
|
10025
|
+
const wt = normPath(worktreePath);
|
|
10026
|
+
if (!wt) throw new Error("worktree teardown refused: empty target path");
|
|
10027
|
+
if (!primaryCheckout) return;
|
|
10028
|
+
const primary = normPath(primaryCheckout);
|
|
10029
|
+
if (wt === primary) throw new Error(`worktree teardown refused: target is the primary checkout (${worktreePath})`);
|
|
10030
|
+
if (primary.startsWith(`${wt}/`)) {
|
|
10031
|
+
throw new Error(`worktree teardown refused: target ${worktreePath} contains the primary checkout`);
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
function detachReparsePoints(path2, rmv) {
|
|
10035
|
+
const kind = rmv.probe(path2);
|
|
10036
|
+
if (kind === "link") {
|
|
10037
|
+
rmv.detachLink(path2);
|
|
10038
|
+
return;
|
|
10039
|
+
}
|
|
10040
|
+
if (kind !== "dir") return;
|
|
10041
|
+
if (NO_NESTED_LINKS.has(baseName(path2))) return;
|
|
10042
|
+
const dir = path2.replace(/[/\\]+$/, "");
|
|
10043
|
+
for (const name of rmv.readdir(path2)) detachReparsePoints(`${dir}/${name}`, rmv);
|
|
10044
|
+
}
|
|
10045
|
+
async function removeWorktreeTree(worktreePath, primaryCheckout, rmv) {
|
|
10046
|
+
assertSafeWorktreeTarget(worktreePath, primaryCheckout);
|
|
10047
|
+
detachReparsePoints(worktreePath, rmv);
|
|
10048
|
+
await rmv.removeTree(worktreePath);
|
|
10049
|
+
}
|
|
8566
10050
|
function buildRemoteBranchCleanupReport(branch, input) {
|
|
8567
10051
|
if (!input.attempted) return { name: branch, status: "not-attempted", reason: input.reason };
|
|
8568
10052
|
if (input.existsAfter === true) return { name: branch, status: "failed", reason: "still-present-after-delete" };
|
|
@@ -8682,12 +10166,12 @@ function buildGcPlan(inputs) {
|
|
|
8682
10166
|
skipped.push({ branch, reason: "current-branch" });
|
|
8683
10167
|
continue;
|
|
8684
10168
|
}
|
|
8685
|
-
const
|
|
8686
|
-
if (
|
|
8687
|
-
skipped.push({ branch, reason: "dirty-worktree", detail:
|
|
10169
|
+
const worktree2 = worktrees.get(branch);
|
|
10170
|
+
if (worktree2?.dirty) {
|
|
10171
|
+
skipped.push({ branch, reason: "dirty-worktree", detail: worktree2.path });
|
|
8688
10172
|
continue;
|
|
8689
10173
|
}
|
|
8690
|
-
branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath:
|
|
10174
|
+
branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath: worktree2?.path });
|
|
8691
10175
|
}
|
|
8692
10176
|
const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
|
|
8693
10177
|
const branch = branchForTrackingRef(ref, remote);
|
|
@@ -9050,8 +10534,8 @@ function stagePlan(stage2 = {}, stops = true) {
|
|
|
9050
10534
|
}
|
|
9051
10535
|
function derivedStagePlan(derived, shell2, stops = true) {
|
|
9052
10536
|
const { port } = derived;
|
|
9053
|
-
const envOrder = ["MMI_STAGE", "MMI_PORT", "
|
|
9054
|
-
const resolved = { MMI_STAGE: "development", MMI_PORT: String(port),
|
|
10537
|
+
const envOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
|
|
10538
|
+
const resolved = { MMI_STAGE: "development", MMI_PORT: String(port), COMPOSE_PROFILES: "local" };
|
|
9055
10539
|
const ensureEnv = shell2 === "powershell" ? `if (-not (Test-Path -LiteralPath '.env')) { Copy-Item -LiteralPath '.env.example' -Destination '.env' }` : `[ -f .env ] || cp .env.example .env`;
|
|
9056
10540
|
const up = shell2 === "powershell" ? `${envOrder.map((k) => `$env:${k}='${resolved[k]}'`).join("; ")}; docker compose up -d --build` : `${envOrder.map((k) => `${k}=${resolved[k]}`).join(" ")} docker compose up -d --build`;
|
|
9057
10541
|
const health = shell2 === "powershell" ? `curl.exe --fail ${derived.url}` : `curl --fail ${derived.url}`;
|
|
@@ -9162,8 +10646,8 @@ function bootstrapPlan(repo, repoClass) {
|
|
|
9162
10646
|
}
|
|
9163
10647
|
|
|
9164
10648
|
// src/stage-default.ts
|
|
9165
|
-
function shellFor(
|
|
9166
|
-
return
|
|
10649
|
+
function shellFor(platform2 = process.platform) {
|
|
10650
|
+
return platform2 === "win32" ? "powershell" : "bash";
|
|
9167
10651
|
}
|
|
9168
10652
|
function isCentralContainerModel(model) {
|
|
9169
10653
|
return model === "tenant-container" || model === "solo-container";
|
|
@@ -9189,10 +10673,11 @@ function deriveStage(inputs) {
|
|
|
9189
10673
|
healthAnyStatus: true,
|
|
9190
10674
|
portRange: inputs.portRange,
|
|
9191
10675
|
ensureEnv: { example: ".env.example", target: ".env" },
|
|
10676
|
+
// MMI_PORT is the host-published port (the free STAGE_PORT); PORT is intentionally absent so compose
|
|
10677
|
+
// reads the container's bind port from .env rather than this overriding it to the publish port (#1505).
|
|
9192
10678
|
env: {
|
|
9193
10679
|
MMI_STAGE: "development",
|
|
9194
10680
|
MMI_PORT: "$STAGE_PORT",
|
|
9195
|
-
PORT: "$STAGE_PORT",
|
|
9196
10681
|
COMPOSE_PROFILES: "local"
|
|
9197
10682
|
}
|
|
9198
10683
|
};
|
|
@@ -9226,9 +10711,9 @@ function stalePosixFields(config, shell2) {
|
|
|
9226
10711
|
}
|
|
9227
10712
|
function sanitizeLocalStage(local, stale) {
|
|
9228
10713
|
if (!stale.length) return local;
|
|
9229
|
-
const
|
|
9230
|
-
for (const field of stale) delete
|
|
9231
|
-
return
|
|
10714
|
+
const clean4 = { ...local };
|
|
10715
|
+
for (const field of stale) delete clean4[field];
|
|
10716
|
+
return clean4;
|
|
9232
10717
|
}
|
|
9233
10718
|
function staleNote(staleFields, outcome) {
|
|
9234
10719
|
const list = staleFields.join(", ");
|
|
@@ -9274,9 +10759,9 @@ function decideStage(inputs) {
|
|
|
9274
10759
|
|
|
9275
10760
|
// src/cursor-plugin-seed.ts
|
|
9276
10761
|
var import_node_child_process7 = require("node:child_process");
|
|
9277
|
-
var
|
|
9278
|
-
var
|
|
9279
|
-
var
|
|
10762
|
+
var import_node_fs15 = require("node:fs");
|
|
10763
|
+
var import_node_os5 = require("node:os");
|
|
10764
|
+
var import_node_path14 = require("node:path");
|
|
9280
10765
|
var import_node_util6 = require("node:util");
|
|
9281
10766
|
function isSemverVersion(v) {
|
|
9282
10767
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
@@ -9293,17 +10778,17 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
9293
10778
|
}
|
|
9294
10779
|
function cursorUserGlobalStatePath() {
|
|
9295
10780
|
if (process.platform === "win32") {
|
|
9296
|
-
const base2 = process.env.APPDATA || (0,
|
|
9297
|
-
return (0,
|
|
10781
|
+
const base2 = process.env.APPDATA || (0, import_node_path14.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
|
|
10782
|
+
return (0, import_node_path14.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
9298
10783
|
}
|
|
9299
10784
|
if (process.platform === "darwin") {
|
|
9300
|
-
return (0,
|
|
10785
|
+
return (0, import_node_path14.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
9301
10786
|
}
|
|
9302
|
-
return (0,
|
|
10787
|
+
return (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
9303
10788
|
}
|
|
9304
10789
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
9305
10790
|
const dbPath = cursorUserGlobalStatePath();
|
|
9306
|
-
if (!(0,
|
|
10791
|
+
if (!(0, import_node_fs15.existsSync)(dbPath)) return void 0;
|
|
9307
10792
|
try {
|
|
9308
10793
|
const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
|
|
9309
10794
|
timeout: 5e3
|
|
@@ -9317,57 +10802,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
|
9317
10802
|
}
|
|
9318
10803
|
}
|
|
9319
10804
|
function syncDirContents(src, dest) {
|
|
9320
|
-
(0,
|
|
9321
|
-
for (const name of (0,
|
|
9322
|
-
(0,
|
|
10805
|
+
(0, import_node_fs15.mkdirSync)(dest, { recursive: true });
|
|
10806
|
+
for (const name of (0, import_node_fs15.readdirSync)(dest)) {
|
|
10807
|
+
(0, import_node_fs15.rmSync)((0, import_node_path14.join)(dest, name), { recursive: true, force: true });
|
|
9323
10808
|
}
|
|
9324
|
-
(0,
|
|
10809
|
+
(0, import_node_fs15.cpSync)(src, dest, { recursive: true });
|
|
9325
10810
|
}
|
|
9326
10811
|
function releaseTag(releasedVersion) {
|
|
9327
10812
|
return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
|
|
9328
10813
|
}
|
|
9329
10814
|
async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
|
|
9330
|
-
const tarFile = (0,
|
|
10815
|
+
const tarFile = (0, import_node_path14.join)(tmpRoot, "archive.tar");
|
|
9331
10816
|
try {
|
|
9332
10817
|
await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
|
|
9333
10818
|
await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
|
|
9334
10819
|
timeout: 6e4
|
|
9335
10820
|
});
|
|
9336
10821
|
await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
|
|
9337
|
-
const pluginMmi = (0,
|
|
9338
|
-
return (0,
|
|
10822
|
+
const pluginMmi = (0, import_node_path14.join)(tmpRoot, "plugins", "mmi");
|
|
10823
|
+
return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
9339
10824
|
} catch {
|
|
9340
10825
|
return void 0;
|
|
9341
10826
|
}
|
|
9342
10827
|
}
|
|
9343
10828
|
async function downloadPluginMmiViaGh(tag, tmpRoot) {
|
|
9344
|
-
const tarPath = (0,
|
|
10829
|
+
const tarPath = (0, import_node_path14.join)(tmpRoot, "repo.tgz");
|
|
9345
10830
|
try {
|
|
9346
|
-
(0,
|
|
10831
|
+
(0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
|
|
9347
10832
|
const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
|
|
9348
10833
|
timeout: 12e4,
|
|
9349
10834
|
maxBuffer: 100 * 1024 * 1024,
|
|
9350
10835
|
encoding: "buffer",
|
|
9351
10836
|
windowsHide: true
|
|
9352
10837
|
});
|
|
9353
|
-
(0,
|
|
10838
|
+
(0, import_node_fs15.writeFileSync)(tarPath, stdout);
|
|
9354
10839
|
await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
|
|
9355
|
-
const top = (0,
|
|
10840
|
+
const top = (0, import_node_fs15.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
|
|
9356
10841
|
if (!top) return void 0;
|
|
9357
|
-
const pluginMmi = (0,
|
|
9358
|
-
return (0,
|
|
10842
|
+
const pluginMmi = (0, import_node_path14.join)(tmpRoot, top, "plugins", "mmi");
|
|
10843
|
+
return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
9359
10844
|
} catch {
|
|
9360
10845
|
return void 0;
|
|
9361
10846
|
}
|
|
9362
10847
|
}
|
|
9363
10848
|
async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
|
|
9364
|
-
(0,
|
|
10849
|
+
(0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
|
|
9365
10850
|
const tag = releaseTag(releasedVersion);
|
|
9366
10851
|
if (hubCheckout) {
|
|
9367
10852
|
const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
|
|
9368
10853
|
if (fromHub) return fromHub;
|
|
9369
10854
|
}
|
|
9370
|
-
return downloadPluginMmiViaGh(tag, (0,
|
|
10855
|
+
return downloadPluginMmiViaGh(tag, (0, import_node_path14.join)(tmpRoot, "gh"));
|
|
9371
10856
|
}
|
|
9372
10857
|
function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
9373
10858
|
if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
|
|
@@ -9388,115 +10873,10 @@ async function applyCursorPluginCacheSeed(input) {
|
|
|
9388
10873
|
for (const pin of pinsToSeed) {
|
|
9389
10874
|
syncDirContents(source, pin.path);
|
|
9390
10875
|
}
|
|
9391
|
-
(0,
|
|
10876
|
+
(0, import_node_fs15.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
9392
10877
|
return true;
|
|
9393
10878
|
}
|
|
9394
10879
|
|
|
9395
|
-
// src/bootstrap-seeds.ts
|
|
9396
|
-
var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
9397
|
-
function loadBootstrapSeeds(manifestJson) {
|
|
9398
|
-
let parsed;
|
|
9399
|
-
try {
|
|
9400
|
-
parsed = JSON.parse(manifestJson);
|
|
9401
|
-
} catch {
|
|
9402
|
-
throw new Error("bootstrap seed manifest is not valid JSON");
|
|
9403
|
-
}
|
|
9404
|
-
const obj = parsed ?? {};
|
|
9405
|
-
const seeds = obj.seeds ?? [];
|
|
9406
|
-
for (const s of seeds) {
|
|
9407
|
-
if (!s || !s.target || !s.source || !Array.isArray(s.classes)) {
|
|
9408
|
-
throw new Error(`invalid seed entry (needs target, source, classes): ${JSON.stringify(s)}`);
|
|
9409
|
-
}
|
|
9410
|
-
if (s.ownership !== "org" && s.ownership !== "repo") {
|
|
9411
|
-
throw new Error(`invalid seed ownership '${s.ownership}' for ${s.target} (must be 'org' or 'repo')`);
|
|
9412
|
-
}
|
|
9413
|
-
}
|
|
9414
|
-
return {
|
|
9415
|
-
seeds,
|
|
9416
|
-
labels: obj.labels ?? [],
|
|
9417
|
-
placeholders: obj.placeholders ?? []
|
|
9418
|
-
};
|
|
9419
|
-
}
|
|
9420
|
-
function renderSeed(template, vars) {
|
|
9421
|
-
return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key] : match);
|
|
9422
|
-
}
|
|
9423
|
-
function missingPlaceholders(rendered) {
|
|
9424
|
-
const out = /* @__PURE__ */ new Set();
|
|
9425
|
-
for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
|
|
9426
|
-
return [...out];
|
|
9427
|
-
}
|
|
9428
|
-
var GITIGNORE_MANAGED_BEGIN = "# >>> mmi-managed >>>";
|
|
9429
|
-
var GITIGNORE_MANAGED_END = "# <<< mmi-managed <<<";
|
|
9430
|
-
var MANAGED_GITIGNORE_LINES = [
|
|
9431
|
-
'# Org-wide cleanliness (AGENTS.md "Repo cleanliness") \u2014 enforced by `mmi-cli doctor`.',
|
|
9432
|
-
"# Do not edit inside these markers; this block is regenerated on the next doctor run.",
|
|
9433
|
-
"/tmp/",
|
|
9434
|
-
"/plans/",
|
|
9435
|
-
".playwright-mcp/",
|
|
9436
|
-
".claude/worktrees/",
|
|
9437
|
-
".mmi/.session",
|
|
9438
|
-
".mmi/head-ts/",
|
|
9439
|
-
// Recursive so any reuse of the parameterized queue engine in a new .mmi subdir (honcho today, others
|
|
9440
|
-
// later) is ignored too — `**` matches zero dirs, so this still covers the root saga queue.
|
|
9441
|
-
".mmi/**/saga-pending.jsonl*",
|
|
9442
|
-
".mmi/**/saga-flush.lock",
|
|
9443
|
-
".aws-sam/",
|
|
9444
|
-
"/*.png",
|
|
9445
|
-
// Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
|
|
9446
|
-
// .env.example reference file stays tracked.
|
|
9447
|
-
".env",
|
|
9448
|
-
".env.*",
|
|
9449
|
-
"!.env.example"
|
|
9450
|
-
];
|
|
9451
|
-
function renderManagedGitignoreBlock() {
|
|
9452
|
-
return [GITIGNORE_MANAGED_BEGIN, ...MANAGED_GITIGNORE_LINES, GITIGNORE_MANAGED_END].join("\n");
|
|
9453
|
-
}
|
|
9454
|
-
function upsertManagedGitignoreBlock(current) {
|
|
9455
|
-
const block = renderManagedGitignoreBlock();
|
|
9456
|
-
const src = (current ?? "").replace(/\r\n/g, "\n");
|
|
9457
|
-
if (src.trim() === "") {
|
|
9458
|
-
const next2 = `${block}
|
|
9459
|
-
`;
|
|
9460
|
-
return { content: next2, changed: src !== next2 };
|
|
9461
|
-
}
|
|
9462
|
-
const managed = /* @__PURE__ */ new Set([GITIGNORE_MANAGED_BEGIN, GITIGNORE_MANAGED_END, ...MANAGED_GITIGNORE_LINES]);
|
|
9463
|
-
const lines = src.split("\n");
|
|
9464
|
-
const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
|
|
9465
|
-
const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
|
|
9466
|
-
let next;
|
|
9467
|
-
if (beginAt !== -1 && endAt !== -1) {
|
|
9468
|
-
const before = lines.slice(0, beginAt).filter((l) => !managed.has(l.trim()));
|
|
9469
|
-
const after = lines.slice(endAt + 1).filter((l) => !managed.has(l.trim()));
|
|
9470
|
-
next = `${[...before, ...block.split("\n"), ...after].join("\n").replace(/\n+$/, "")}
|
|
9471
|
-
`;
|
|
9472
|
-
} else {
|
|
9473
|
-
const kept = lines.filter((l) => !managed.has(l.trim())).join("\n").replace(/\n+$/, "");
|
|
9474
|
-
next = kept === "" ? `${block}
|
|
9475
|
-
` : `${kept}
|
|
9476
|
-
|
|
9477
|
-
${block}
|
|
9478
|
-
`;
|
|
9479
|
-
}
|
|
9480
|
-
return { content: next, changed: src !== next };
|
|
9481
|
-
}
|
|
9482
|
-
function diffManagedGitignoreBlock(current) {
|
|
9483
|
-
const lines = (current ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
9484
|
-
const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
|
|
9485
|
-
const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
|
|
9486
|
-
const hasMarker = beginAt !== -1 || lines.some((l) => l === GITIGNORE_MANAGED_END);
|
|
9487
|
-
if (!hasMarker) return { added: [], removed: [], seeded: true };
|
|
9488
|
-
const wellOrdered = beginAt !== -1 && endAt !== -1;
|
|
9489
|
-
if (!wellOrdered) return { added: [], removed: [], seeded: false };
|
|
9490
|
-
const isRule = (l) => l.trim() !== "" && !l.trim().startsWith("#");
|
|
9491
|
-
const oldBody = lines.slice(beginAt + 1, endAt).filter(isRule);
|
|
9492
|
-
const newBody = MANAGED_GITIGNORE_LINES.filter(isRule);
|
|
9493
|
-
return {
|
|
9494
|
-
added: newBody.filter((l) => !oldBody.includes(l)),
|
|
9495
|
-
removed: oldBody.filter((l) => !newBody.includes(l)),
|
|
9496
|
-
seeded: false
|
|
9497
|
-
};
|
|
9498
|
-
}
|
|
9499
|
-
|
|
9500
10880
|
// src/doctor.ts
|
|
9501
10881
|
var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
|
|
9502
10882
|
var AWS_CROSS_ACCOUNT_FIX = "use a non-root IAM user/session profile for master-agent AWS checks; set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), then verify `aws sts get-caller-identity` does not end in :root";
|
|
@@ -10106,8 +11486,8 @@ async function runStageLiveDown(deps, t) {
|
|
|
10106
11486
|
|
|
10107
11487
|
// src/stage-runner.ts
|
|
10108
11488
|
var import_node_child_process8 = require("node:child_process");
|
|
10109
|
-
var
|
|
10110
|
-
var
|
|
11489
|
+
var import_node_fs16 = require("node:fs");
|
|
11490
|
+
var import_node_path15 = require("node:path");
|
|
10111
11491
|
var import_node_net2 = require("node:net");
|
|
10112
11492
|
var import_node_util7 = require("node:util");
|
|
10113
11493
|
var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process8.execFile);
|
|
@@ -10152,7 +11532,7 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
|
|
|
10152
11532
|
return void 0;
|
|
10153
11533
|
}
|
|
10154
11534
|
function stageStatePath(cwd = process.cwd()) {
|
|
10155
|
-
return (0,
|
|
11535
|
+
return (0, import_node_path15.join)(cwd, "tmp", "stage", "state.json");
|
|
10156
11536
|
}
|
|
10157
11537
|
function mergeEnvSecretsIntoFile(content, secrets2) {
|
|
10158
11538
|
const lines = content.split(/\r?\n/);
|
|
@@ -10176,8 +11556,8 @@ function mergeEnvSecretsIntoFile(content, secrets2) {
|
|
|
10176
11556
|
`;
|
|
10177
11557
|
}
|
|
10178
11558
|
var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
|
|
10179
|
-
function posixOnlyShellProblems(command, field,
|
|
10180
|
-
if (
|
|
11559
|
+
function posixOnlyShellProblems(command, field, platform2 = process.platform) {
|
|
11560
|
+
if (platform2 !== "win32" || !command?.trim()) return [];
|
|
10181
11561
|
const problems = [];
|
|
10182
11562
|
if (/(^|&&|\||;)\s*[A-Za-z_][A-Za-z0-9_]*=\S/.test(command)) {
|
|
10183
11563
|
problems.push(
|
|
@@ -10235,9 +11615,9 @@ async function shell(command, cwd, timeoutMs) {
|
|
|
10235
11615
|
});
|
|
10236
11616
|
}
|
|
10237
11617
|
function readState(path2) {
|
|
10238
|
-
if (!(0,
|
|
11618
|
+
if (!(0, import_node_fs16.existsSync)(path2)) return null;
|
|
10239
11619
|
try {
|
|
10240
|
-
return JSON.parse((0,
|
|
11620
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
|
|
10241
11621
|
} catch {
|
|
10242
11622
|
return null;
|
|
10243
11623
|
}
|
|
@@ -10289,7 +11669,7 @@ async function stopStage(opts = {}) {
|
|
|
10289
11669
|
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
10290
11670
|
}
|
|
10291
11671
|
await killTree(state.pid);
|
|
10292
|
-
(0,
|
|
11672
|
+
(0, import_node_fs16.rmSync)(statePath, { force: true });
|
|
10293
11673
|
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
10294
11674
|
}
|
|
10295
11675
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -10298,7 +11678,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10298
11678
|
const cwd = opts.cwd ?? process.cwd();
|
|
10299
11679
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
10300
11680
|
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
10301
|
-
(0,
|
|
11681
|
+
(0, import_node_fs16.mkdirSync)(dir, { recursive: true });
|
|
10302
11682
|
let stagePort;
|
|
10303
11683
|
if (config.portRange) {
|
|
10304
11684
|
const [s, e] = config.portRange;
|
|
@@ -10308,14 +11688,14 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10308
11688
|
}
|
|
10309
11689
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
10310
11690
|
if (config.ensureEnv) {
|
|
10311
|
-
const target = (0,
|
|
10312
|
-
const example = (0,
|
|
10313
|
-
if (!(0,
|
|
10314
|
-
(0,
|
|
10315
|
-
} else if ((0,
|
|
10316
|
-
const stale = detectStaleEnvFile((0,
|
|
10317
|
-
exampleMtimeMs: (0,
|
|
10318
|
-
targetMtimeMs: (0,
|
|
11691
|
+
const target = (0, import_node_path15.join)(cwd, config.ensureEnv.target);
|
|
11692
|
+
const example = (0, import_node_path15.join)(cwd, config.ensureEnv.example);
|
|
11693
|
+
if (!(0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
|
|
11694
|
+
(0, import_node_fs16.copyFileSync)(example, target);
|
|
11695
|
+
} else if ((0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
|
|
11696
|
+
const stale = detectStaleEnvFile((0, import_node_fs16.readFileSync)(example, "utf8"), (0, import_node_fs16.readFileSync)(target, "utf8"), {
|
|
11697
|
+
exampleMtimeMs: (0, import_node_fs16.statSync)(example).mtimeMs,
|
|
11698
|
+
targetMtimeMs: (0, import_node_fs16.statSync)(target).mtimeMs
|
|
10319
11699
|
});
|
|
10320
11700
|
if (stale) {
|
|
10321
11701
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -10323,8 +11703,8 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10323
11703
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
10324
11704
|
}
|
|
10325
11705
|
}
|
|
10326
|
-
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0,
|
|
10327
|
-
(0,
|
|
11706
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs16.existsSync)(target)) {
|
|
11707
|
+
(0, import_node_fs16.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs16.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
10328
11708
|
}
|
|
10329
11709
|
}
|
|
10330
11710
|
const extraEnv = {};
|
|
@@ -10349,13 +11729,13 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10349
11729
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
10350
11730
|
port: stagePort
|
|
10351
11731
|
};
|
|
10352
|
-
(0,
|
|
11732
|
+
(0, import_node_fs16.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
10353
11733
|
try {
|
|
10354
11734
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
10355
11735
|
else await waitForProcessStability(child);
|
|
10356
11736
|
} catch (e) {
|
|
10357
11737
|
await killTree(state.pid);
|
|
10358
|
-
(0,
|
|
11738
|
+
(0, import_node_fs16.rmSync)(statePath, { force: true });
|
|
10359
11739
|
throw e;
|
|
10360
11740
|
}
|
|
10361
11741
|
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
@@ -10374,75 +11754,6 @@ async function runStage(config = {}, opts = {}) {
|
|
|
10374
11754
|
return { ...started, action: "run", message: `built and ${started.message}` };
|
|
10375
11755
|
}
|
|
10376
11756
|
|
|
10377
|
-
// src/project-model.ts
|
|
10378
|
-
var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
|
|
10379
|
-
var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
|
|
10380
|
-
var RELEASE_TRACKS = ["full", "direct", "trunk"];
|
|
10381
|
-
var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
|
|
10382
|
-
var DEPLOY_MODEL_SET = new Set(DEPLOY_MODELS);
|
|
10383
|
-
var RELEASE_TRACK_SET = new Set(RELEASE_TRACKS);
|
|
10384
|
-
function isProjectType(value) {
|
|
10385
|
-
return Boolean(value && PROJECT_TYPE_SET.has(value));
|
|
10386
|
-
}
|
|
10387
|
-
function isDeployModel(value) {
|
|
10388
|
-
return Boolean(value && DEPLOY_MODEL_SET.has(value));
|
|
10389
|
-
}
|
|
10390
|
-
function isReleaseTrack(value) {
|
|
10391
|
-
return Boolean(value && RELEASE_TRACK_SET.has(value));
|
|
10392
|
-
}
|
|
10393
|
-
function repoIsHub(repo) {
|
|
10394
|
-
return repo.toLowerCase().endsWith("/mmi-hub") || repo.toLowerCase() === "mmi-hub";
|
|
10395
|
-
}
|
|
10396
|
-
function resolveProjectTypeConfident(meta, repo) {
|
|
10397
|
-
const rawType = typeof meta?.projectType === "string" ? meta.projectType : void 0;
|
|
10398
|
-
if (isProjectType(rawType)) return rawType;
|
|
10399
|
-
if (meta?.class === "content" || meta?.deployModel === "content") return "content";
|
|
10400
|
-
if (meta?.deployModel === "hub-serverless" || repoIsHub(repo)) return "hub-service";
|
|
10401
|
-
if (meta?.deployModel === "registry-publish") return "cli-tool";
|
|
10402
|
-
if (meta?.deployModel === "solo-container") return "worker";
|
|
10403
|
-
if (meta?.deployModel === "none") return "non-deployable";
|
|
10404
|
-
return void 0;
|
|
10405
|
-
}
|
|
10406
|
-
function resolveProjectType(meta, repo) {
|
|
10407
|
-
return resolveProjectTypeConfident(meta, repo) ?? "web-app";
|
|
10408
|
-
}
|
|
10409
|
-
function resolveDeployModel(meta, repo) {
|
|
10410
|
-
const rawModel = typeof meta?.deployModel === "string" ? meta.deployModel : void 0;
|
|
10411
|
-
if (isDeployModel(rawModel)) return rawModel;
|
|
10412
|
-
const projectType = resolveProjectType(meta, repo);
|
|
10413
|
-
if (projectType === "content" || meta?.class === "content") return "content";
|
|
10414
|
-
if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
|
|
10415
|
-
if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
|
|
10416
|
-
if (projectType === "cli-tool") return "registry-publish";
|
|
10417
|
-
return "tenant-container";
|
|
10418
|
-
}
|
|
10419
|
-
function projectTypeClearsWebProfile(projectType, deployModel) {
|
|
10420
|
-
return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
|
|
10421
|
-
}
|
|
10422
|
-
function inferReleaseTrackFromBranches(hints) {
|
|
10423
|
-
if (hints.hasRcBranch) return void 0;
|
|
10424
|
-
if (hints.hasDevelopmentBranch && hints.hasMainBranch) return "direct";
|
|
10425
|
-
return void 0;
|
|
10426
|
-
}
|
|
10427
|
-
function resolveBootstrapReleaseTrack(cls, explicit) {
|
|
10428
|
-
if (isReleaseTrack(explicit)) return explicit;
|
|
10429
|
-
if (cls === "content") return "trunk";
|
|
10430
|
-
return "full";
|
|
10431
|
-
}
|
|
10432
|
-
function resolveReleaseTrack(meta, hints) {
|
|
10433
|
-
const raw = typeof meta?.releaseTrack === "string" ? meta.releaseTrack : void 0;
|
|
10434
|
-
if (isReleaseTrack(raw)) return raw;
|
|
10435
|
-
if (meta?.class === "content" || meta?.deployModel === "content") return "trunk";
|
|
10436
|
-
const inferred = hints ? inferReleaseTrackFromBranches(hints) : void 0;
|
|
10437
|
-
if (inferred) return inferred;
|
|
10438
|
-
return "full";
|
|
10439
|
-
}
|
|
10440
|
-
function branchesForTrack(track) {
|
|
10441
|
-
if (track === "trunk") return ["main"];
|
|
10442
|
-
if (track === "direct") return ["development", "main"];
|
|
10443
|
-
return ["development", "rc", "main"];
|
|
10444
|
-
}
|
|
10445
|
-
|
|
10446
11757
|
// src/org-spine-pull.ts
|
|
10447
11758
|
async function reconcileDirtyOrgSpineBeforePull(deps, branch, options = {}) {
|
|
10448
11759
|
await deps.run("git", ["fetch", "origin", branch]).catch(() => "");
|
|
@@ -10471,7 +11782,7 @@ function resolveDeployModel2(meta, repo) {
|
|
|
10471
11782
|
if (isDeployModel(m)) return m;
|
|
10472
11783
|
return resolveDeployModel(meta, repo);
|
|
10473
11784
|
}
|
|
10474
|
-
function
|
|
11785
|
+
function clean2(out) {
|
|
10475
11786
|
return out.trim();
|
|
10476
11787
|
}
|
|
10477
11788
|
function requireValue(value, label) {
|
|
@@ -10522,11 +11833,11 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
|
|
|
10522
11833
|
if (release.targetCommitish !== expectedTarget) {
|
|
10523
11834
|
throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
|
|
10524
11835
|
}
|
|
10525
|
-
const tagSha = requireValue(
|
|
11836
|
+
const tagSha = requireValue(clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
|
|
10526
11837
|
if (tagSha !== expectedSha) {
|
|
10527
11838
|
throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
|
|
10528
11839
|
}
|
|
10529
|
-
const branchOut =
|
|
11840
|
+
const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
|
|
10530
11841
|
const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
|
|
10531
11842
|
if (branchSha !== expectedSha) {
|
|
10532
11843
|
throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
|
|
@@ -10586,7 +11897,7 @@ function ensurePositiveCount(out, emptyMessage) {
|
|
|
10586
11897
|
if (count <= 0) throw new Error(emptyMessage);
|
|
10587
11898
|
}
|
|
10588
11899
|
async function remoteBranchExists(deps, branch) {
|
|
10589
|
-
const out =
|
|
11900
|
+
const out = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
10590
11901
|
return out.length > 0;
|
|
10591
11902
|
}
|
|
10592
11903
|
async function loadReleaseTrackBranchHints(deps) {
|
|
@@ -10598,10 +11909,10 @@ async function loadReleaseTrackBranchHints(deps) {
|
|
|
10598
11909
|
return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
|
|
10599
11910
|
}
|
|
10600
11911
|
async function buildTrainApplyContext(deps) {
|
|
10601
|
-
const repo = requireValue(
|
|
11912
|
+
const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
10602
11913
|
const [owner, name] = repo.split("/");
|
|
10603
11914
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
10604
|
-
const login = requireValue(
|
|
11915
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
10605
11916
|
const verdict = await deps.trainAuthority(repo);
|
|
10606
11917
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
10607
11918
|
if (!verdict.train) {
|
|
@@ -10622,7 +11933,7 @@ async function requireCleanTree(deps) {
|
|
|
10622
11933
|
if (status.trim()) throw new Error("working tree must be clean before train apply");
|
|
10623
11934
|
}
|
|
10624
11935
|
async function requireBranch(deps, branch) {
|
|
10625
|
-
const current =
|
|
11936
|
+
const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
10626
11937
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
10627
11938
|
}
|
|
10628
11939
|
var HUB_REPO3 = "mutmutco/MMI-Hub";
|
|
@@ -10791,7 +12102,7 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
|
|
|
10791
12102
|
return [...contexts];
|
|
10792
12103
|
}
|
|
10793
12104
|
async function findOpenAlignmentPr(deps, ctx) {
|
|
10794
|
-
const out =
|
|
12105
|
+
const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
|
|
10795
12106
|
if (!out) return void 0;
|
|
10796
12107
|
const rows = JSON.parse(out);
|
|
10797
12108
|
const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
|
|
@@ -10819,14 +12130,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10819
12130
|
note: `alignment PR already open: ${existing.url} \u2014 land it with \`gh pr merge ${existing.number} --merge\``
|
|
10820
12131
|
};
|
|
10821
12132
|
}
|
|
10822
|
-
const ahead =
|
|
12133
|
+
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
|
|
10823
12134
|
if (ahead === "0") {
|
|
10824
12135
|
return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
|
|
10825
12136
|
}
|
|
10826
12137
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
10827
12138
|
|
|
10828
12139
|
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** (\`gh pr merge --merge\`, not squash) so the merge parentage survives and the misalignment guard stays satisfied.`;
|
|
10829
|
-
const url =
|
|
12140
|
+
const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
|
|
10830
12141
|
const number = parsePrNumber(url);
|
|
10831
12142
|
return {
|
|
10832
12143
|
status: "pr-pending",
|
|
@@ -10892,10 +12203,10 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10892
12203
|
}
|
|
10893
12204
|
async function ensureTagPushed(deps, tag, sha) {
|
|
10894
12205
|
const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
|
|
10895
|
-
const remoteSha =
|
|
12206
|
+
const remoteSha = clean2(remoteOut).split(/\s+/)[0] || "";
|
|
10896
12207
|
let localSha = "";
|
|
10897
12208
|
try {
|
|
10898
|
-
localSha =
|
|
12209
|
+
localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
|
|
10899
12210
|
} catch {
|
|
10900
12211
|
}
|
|
10901
12212
|
if (remoteSha) {
|
|
@@ -10918,7 +12229,7 @@ async function ensureTagPushed(deps, tag, sha) {
|
|
|
10918
12229
|
}
|
|
10919
12230
|
async function resolveRcResumeTag(deps, base2, sha) {
|
|
10920
12231
|
const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base2}-rc.*`]);
|
|
10921
|
-
const onSha =
|
|
12232
|
+
const onSha = clean2(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base2.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
|
|
10922
12233
|
const unique = [...new Set(onSha)];
|
|
10923
12234
|
if (unique.length === 0) return { tag: null };
|
|
10924
12235
|
const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
|
|
@@ -11043,13 +12354,13 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11043
12354
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
11044
12355
|
);
|
|
11045
12356
|
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
11046
|
-
const releaseBase = requireValue(
|
|
12357
|
+
const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
11047
12358
|
await deps.run("git", ["checkout", "rc"]);
|
|
11048
12359
|
await ffOnlyPull(deps, "rc");
|
|
11049
12360
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
11050
|
-
const rcSha = requireValue(
|
|
12361
|
+
const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
11051
12362
|
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
11052
|
-
const tag2 = resume.tag ?? requireValue(
|
|
12363
|
+
const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
11053
12364
|
const resumeNote = resume.tag ? resume.note : void 0;
|
|
11054
12365
|
await ensureTagPushed(deps, tag2, rcSha);
|
|
11055
12366
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
@@ -11067,7 +12378,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11067
12378
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
11068
12379
|
);
|
|
11069
12380
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
11070
|
-
const tag2 = requireValue(
|
|
12381
|
+
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
11071
12382
|
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
11072
12383
|
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
11073
12384
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -11085,12 +12396,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11085
12396
|
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
11086
12397
|
}
|
|
11087
12398
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
11088
|
-
const releaseSha2 = requireValue(
|
|
12399
|
+
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
11089
12400
|
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
11090
12401
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
11091
12402
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
11092
12403
|
await deps.run("git", ["push", "origin", "main"]);
|
|
11093
|
-
const releaseUrl2 =
|
|
12404
|
+
const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
11094
12405
|
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
11095
12406
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
11096
12407
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
@@ -11139,8 +12450,8 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11139
12450
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
11140
12451
|
);
|
|
11141
12452
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
11142
|
-
const tag2 = requireValue(
|
|
11143
|
-
const rcShaAtRelease = hasRcBranch ?
|
|
12453
|
+
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
12454
|
+
const rcShaAtRelease = hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
11144
12455
|
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
11145
12456
|
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
11146
12457
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -11158,12 +12469,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11158
12469
|
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
11159
12470
|
}
|
|
11160
12471
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
11161
|
-
const releaseSha2 = requireValue(
|
|
12472
|
+
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
11162
12473
|
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
11163
12474
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
11164
12475
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
11165
12476
|
await deps.run("git", ["push", "origin", "main"]);
|
|
11166
|
-
const releaseUrl2 =
|
|
12477
|
+
const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
11167
12478
|
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
11168
12479
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
11169
12480
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
@@ -11230,7 +12541,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11230
12541
|
`hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026].`
|
|
11231
12542
|
);
|
|
11232
12543
|
}
|
|
11233
|
-
const releasedRcSha =
|
|
12544
|
+
const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
11234
12545
|
await deps.run("git", ["checkout", "main"]);
|
|
11235
12546
|
await ffOnlyPull(deps, "main");
|
|
11236
12547
|
if (predicted.length === 0) {
|
|
@@ -11238,15 +12549,15 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11238
12549
|
} else {
|
|
11239
12550
|
await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", tolerated);
|
|
11240
12551
|
}
|
|
11241
|
-
const tag = requireValue(
|
|
12552
|
+
const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
11242
12553
|
const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
11243
|
-
const releaseSha = requireValue(
|
|
12554
|
+
const releaseSha = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
11244
12555
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
11245
12556
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
11246
12557
|
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
11247
12558
|
await deps.run("git", ["push", "origin", "main"]);
|
|
11248
12559
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
11249
|
-
const releaseUrl =
|
|
12560
|
+
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
11250
12561
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
11251
12562
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
11252
12563
|
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
|
|
@@ -11374,7 +12685,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
11374
12685
|
}
|
|
11375
12686
|
try {
|
|
11376
12687
|
await deps.run("git", ["fetch", "origin", "rc"]);
|
|
11377
|
-
const rcNow =
|
|
12688
|
+
const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
11378
12689
|
if (rcNow !== releasedRcSha) {
|
|
11379
12690
|
return {
|
|
11380
12691
|
status: "skipped",
|
|
@@ -11406,7 +12717,7 @@ async function runTenantRedeploy(deps, options) {
|
|
|
11406
12717
|
const repo = options.repo;
|
|
11407
12718
|
const [owner, name] = repo.split("/");
|
|
11408
12719
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
11409
|
-
const login = requireValue(
|
|
12720
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
11410
12721
|
const verdict = await deps.trainAuthority(repo);
|
|
11411
12722
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
11412
12723
|
if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
|
|
@@ -11562,7 +12873,7 @@ var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
|
11562
12873
|
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
11563
12874
|
var HOTFIX_VERIFY_ATTEMPTS = 5;
|
|
11564
12875
|
var HOTFIX_VERIFY_RETRY_MS = 6e3;
|
|
11565
|
-
function
|
|
12876
|
+
function clean3(out) {
|
|
11566
12877
|
return out.trim();
|
|
11567
12878
|
}
|
|
11568
12879
|
function sleeper(deps) {
|
|
@@ -11620,7 +12931,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
11620
12931
|
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
11621
12932
|
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
11622
12933
|
}
|
|
11623
|
-
const sha =
|
|
12934
|
+
const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
11624
12935
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
11625
12936
|
return { sha, label: sha.slice(0, 7) };
|
|
11626
12937
|
}
|
|
@@ -11648,7 +12959,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11648
12959
|
};
|
|
11649
12960
|
}
|
|
11650
12961
|
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
11651
|
-
const remoteBranch =
|
|
12962
|
+
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
11652
12963
|
if (remoteBranch) {
|
|
11653
12964
|
await deps.run("git", ["checkout", branch]);
|
|
11654
12965
|
const isRulesSource2 = deps.isRulesSource ? await deps.isRulesSource() : false;
|
|
@@ -11681,7 +12992,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11681
12992
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
11682
12993
|
}
|
|
11683
12994
|
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
|
|
11684
|
-
const prUrl =
|
|
12995
|
+
const prUrl = clean3(await deps.run("gh", [
|
|
11685
12996
|
"pr",
|
|
11686
12997
|
"create",
|
|
11687
12998
|
"--repo",
|
|
@@ -11801,7 +13112,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11801
13112
|
}
|
|
11802
13113
|
let verifyNote;
|
|
11803
13114
|
if (deployModel === "hub-serverless") {
|
|
11804
|
-
const previousRef =
|
|
13115
|
+
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
11805
13116
|
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
11806
13117
|
try {
|
|
11807
13118
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
@@ -11886,9 +13197,9 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
11886
13197
|
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
11887
13198
|
}
|
|
11888
13199
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
11889
|
-
const branchExists = Boolean(
|
|
13200
|
+
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
11890
13201
|
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
11891
|
-
const remoteTag =
|
|
13202
|
+
const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
11892
13203
|
const tagPushed = Boolean(remoteTag);
|
|
11893
13204
|
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
11894
13205
|
let releaseExists = false;
|
|
@@ -11922,7 +13233,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
11922
13233
|
}
|
|
11923
13234
|
}
|
|
11924
13235
|
}
|
|
11925
|
-
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(
|
|
13236
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
11926
13237
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
11927
13238
|
}
|
|
11928
13239
|
async function findInFlightHotfixVersion(deps, ctx) {
|
|
@@ -11945,7 +13256,7 @@ async function findInFlightHotfixVersion(deps, ctx) {
|
|
|
11945
13256
|
const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
|
|
11946
13257
|
if (m) tags.add(m[1]);
|
|
11947
13258
|
}
|
|
11948
|
-
const branchOut =
|
|
13259
|
+
const branchOut = clean3(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
|
|
11949
13260
|
for (const line of branchOut.split("\n").filter(Boolean)) {
|
|
11950
13261
|
const ref = line.split(/\s+/)[1] ?? "";
|
|
11951
13262
|
const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
|
|
@@ -12094,7 +13405,7 @@ async function announceRelease(deps, args) {
|
|
|
12094
13405
|
}
|
|
12095
13406
|
|
|
12096
13407
|
// src/port-registry.ts
|
|
12097
|
-
var
|
|
13408
|
+
var import_node_fs17 = require("node:fs");
|
|
12098
13409
|
|
|
12099
13410
|
// ../infra/port-geometry.mjs
|
|
12100
13411
|
var PORT_BLOCK = 100;
|
|
@@ -12108,8 +13419,8 @@ function nextPortBlock(registry2) {
|
|
|
12108
13419
|
return [base2, base2 + PORT_SPAN];
|
|
12109
13420
|
}
|
|
12110
13421
|
function loadPortRegistry(path2) {
|
|
12111
|
-
if (!(0,
|
|
12112
|
-
const raw = JSON.parse((0,
|
|
13422
|
+
if (!(0, import_node_fs17.existsSync)(path2)) return {};
|
|
13423
|
+
const raw = JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8"));
|
|
12113
13424
|
const out = {};
|
|
12114
13425
|
for (const [key, value] of Object.entries(raw)) {
|
|
12115
13426
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -12123,9 +13434,9 @@ function ensurePortRange(repo, path2) {
|
|
|
12123
13434
|
const existing = registry2[repo];
|
|
12124
13435
|
if (existing) return existing;
|
|
12125
13436
|
const range = nextPortBlock(registry2);
|
|
12126
|
-
const raw = (0,
|
|
13437
|
+
const raw = (0, import_node_fs17.existsSync)(path2) ? JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8")) : {};
|
|
12127
13438
|
raw[repo] = range;
|
|
12128
|
-
(0,
|
|
13439
|
+
(0, import_node_fs17.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
12129
13440
|
return range;
|
|
12130
13441
|
}
|
|
12131
13442
|
function portCursorSeed(registry2) {
|
|
@@ -12417,7 +13728,13 @@ var HUB_REPO4 = "mutmutco/MMI-Hub";
|
|
|
12417
13728
|
var requiredLabels = ["bug", "feature", "task"];
|
|
12418
13729
|
var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
|
|
12419
13730
|
var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
|
|
13731
|
+
var retiredPriorityLabels = ["priority:urgent", "priority:high", "priority:medium", "priority:low"];
|
|
12420
13732
|
var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
|
|
13733
|
+
var canonicalStatusColors = { Todo: "GRAY", "In Progress": "YELLOW", "In Review": "ORANGE", Done: "GREEN" };
|
|
13734
|
+
var canonicalPriorityColors = { Urgent: "RED", High: "ORANGE", Medium: "YELLOW", Low: "GRAY" };
|
|
13735
|
+
function miscoloredOptions(options, canon) {
|
|
13736
|
+
return (options ?? []).filter((o) => canon[o.name] != null && o.color != null && o.color !== canon[o.name]).map((o) => `${o.name}=${o.color} (want ${canon[o.name]})`);
|
|
13737
|
+
}
|
|
12421
13738
|
var requiredProjectWorkflows = [
|
|
12422
13739
|
"Auto-add sub-issues to project",
|
|
12423
13740
|
"Auto-archive items",
|
|
@@ -12537,6 +13854,23 @@ function localRegistryCheck(deps, path2, predicate) {
|
|
|
12537
13854
|
if (text == null) return null;
|
|
12538
13855
|
return predicate(safeJson2(text, null));
|
|
12539
13856
|
}
|
|
13857
|
+
var DOC_PLACEHOLDER_PROMPTS = [
|
|
13858
|
+
"languages, frameworks, major services",
|
|
13859
|
+
"install, dev server",
|
|
13860
|
+
"lint, typecheck, repo gate script",
|
|
13861
|
+
"ports, env from vault not files",
|
|
13862
|
+
"Describe the product/service",
|
|
13863
|
+
"How to run, build, and test locally",
|
|
13864
|
+
"System shape: components, data flow, deploy model",
|
|
13865
|
+
"Build/test commands, CI gate, deploy target"
|
|
13866
|
+
];
|
|
13867
|
+
function unfilledDocPlaceholders(text) {
|
|
13868
|
+
if (!text) return [];
|
|
13869
|
+
const found = /* @__PURE__ */ new Set();
|
|
13870
|
+
for (const m of text.match(/\{\{[A-Za-z0-9_]+\}\}/g) ?? []) found.add(m);
|
|
13871
|
+
for (const prompt of DOC_PLACEHOLDER_PROMPTS) if (text.includes(prompt)) found.add(prompt);
|
|
13872
|
+
return [...found];
|
|
13873
|
+
}
|
|
12540
13874
|
async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
12541
13875
|
const branchesWanted = expectedBranches(repoClass, releaseTrack);
|
|
12542
13876
|
const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
|
|
@@ -12600,6 +13934,19 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12600
13934
|
label: "README has Agent context section",
|
|
12601
13935
|
detail: readme === null ? "README.md not readable via API" : void 0
|
|
12602
13936
|
});
|
|
13937
|
+
const readmePlaceholders = unfilledDocPlaceholders(readme);
|
|
13938
|
+
checks.push({
|
|
13939
|
+
ok: readmePlaceholders.length === 0,
|
|
13940
|
+
label: "README Agent context is filled (no template placeholders)",
|
|
13941
|
+
detail: readmePlaceholders.length ? `unfilled: ${readmePlaceholders.join(", ")}` : void 0
|
|
13942
|
+
});
|
|
13943
|
+
const architecture = await contentText(deps, repo, baseBranch, "architecture.md");
|
|
13944
|
+
const archPlaceholders = unfilledDocPlaceholders(architecture);
|
|
13945
|
+
checks.push({
|
|
13946
|
+
ok: archPlaceholders.length === 0,
|
|
13947
|
+
label: "architecture.md is filled (no template placeholders)",
|
|
13948
|
+
detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
|
|
13949
|
+
});
|
|
12603
13950
|
const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
|
|
12604
13951
|
const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
|
|
12605
13952
|
checks.push({
|
|
@@ -12614,6 +13961,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12614
13961
|
}
|
|
12615
13962
|
const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
|
|
12616
13963
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
13964
|
+
const priorityStrays = retiredPriorityLabels.filter((l) => labelNames.has(l));
|
|
13965
|
+
checks.push({ ok: priorityStrays.length === 0, label: "no retired priority:* labels (#416)", detail: presentDetail(priorityStrays) });
|
|
12617
13966
|
const actions = await restJson3(deps, `repos/${repo}/actions/permissions`, {});
|
|
12618
13967
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
12619
13968
|
const config = deps.projectMeta ?? null;
|
|
@@ -12629,7 +13978,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12629
13978
|
});
|
|
12630
13979
|
}
|
|
12631
13980
|
if (config?.projectOwner && config.projectNumber != null) {
|
|
12632
|
-
const fieldsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name } ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }`;
|
|
13981
|
+
const fieldsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name } ... on ProjectV2SingleSelectField { id name options { id name color } } } } } } }`;
|
|
12633
13982
|
const fields = await (async () => {
|
|
12634
13983
|
try {
|
|
12635
13984
|
const data = await deps.client.graphql(fieldsQuery, {
|
|
@@ -12670,6 +14019,12 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12670
14019
|
label: `registry status option matches project: ${optionName}`
|
|
12671
14020
|
});
|
|
12672
14021
|
}
|
|
14022
|
+
const statusColorDrift = miscoloredOptions(statusField.options, canonicalStatusColors);
|
|
14023
|
+
checks.push({
|
|
14024
|
+
ok: statusColorDrift.length === 0,
|
|
14025
|
+
label: "Project Status option colors canonical (#1543)",
|
|
14026
|
+
detail: statusColorDrift.length ? `drifted: ${statusColorDrift.join(", ")}` : void 0
|
|
14027
|
+
});
|
|
12673
14028
|
}
|
|
12674
14029
|
const priorityField = fields.find((field) => field.name === "Priority" && (field.options?.length ?? 0) > 0);
|
|
12675
14030
|
checks.push({
|
|
@@ -12695,6 +14050,12 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12695
14050
|
label: `registry priority option matches project: ${optionName}`
|
|
12696
14051
|
});
|
|
12697
14052
|
}
|
|
14053
|
+
const priorityColorDrift = miscoloredOptions(priorityField.options, canonicalPriorityColors);
|
|
14054
|
+
checks.push({
|
|
14055
|
+
ok: priorityColorDrift.length === 0,
|
|
14056
|
+
label: "Project Priority option colors canonical (#1543)",
|
|
14057
|
+
detail: priorityColorDrift.length ? `drifted: ${priorityColorDrift.join(", ")}` : void 0
|
|
14058
|
+
});
|
|
12698
14059
|
}
|
|
12699
14060
|
const workflowQuery = "query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { workflows(first: 30) { nodes { name enabled } } } } }";
|
|
12700
14061
|
const workflowResponse = await (async () => {
|
|
@@ -12732,340 +14093,57 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12732
14093
|
detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
|
|
12733
14094
|
});
|
|
12734
14095
|
if (repo === HUB_REPO4) {
|
|
12735
|
-
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
12736
|
-
const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
|
|
12737
|
-
checks.push({
|
|
12738
|
-
ok: missing.length === 0,
|
|
12739
|
-
label: "Hub required status checks configured",
|
|
12740
|
-
detail: optionDetail(missing)
|
|
12741
|
-
});
|
|
12742
|
-
} else if (repoClass === "deployable") {
|
|
12743
|
-
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
12744
|
-
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
12745
|
-
checks.push({
|
|
12746
|
-
ok: missing.length === 0,
|
|
12747
|
-
label: "product required status checks configured",
|
|
12748
|
-
detail: missing.length ? `missing contexts: ${missing.join(", ")} \u2014 apply ${requiredProductRulesetRef} as an active repo ruleset` : void 0
|
|
12749
|
-
});
|
|
12750
|
-
}
|
|
12751
|
-
const declaredApis = (deps.requiredGcpApis ?? []).filter((a) => a && a.trim());
|
|
12752
|
-
if (declaredApis.length > 0) {
|
|
12753
|
-
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
12754
|
-
const gcpProject = gcpProjectForSlug(slug);
|
|
12755
|
-
const enabled = deps.listEnabledGcpApis ? await deps.listEnabledGcpApis(gcpProject) : null;
|
|
12756
|
-
if (enabled == null) {
|
|
12757
|
-
checks.push({
|
|
12758
|
-
ok: false,
|
|
12759
|
-
label: `required GCP APIs enabled in ${gcpProject}`,
|
|
12760
|
-
detail: `could not list enabled APIs (gcloud unavailable, not authed, or no project ${gcpProject})`
|
|
12761
|
-
});
|
|
12762
|
-
} else {
|
|
12763
|
-
const missing = findMissingGcpApis(declaredApis, enabled);
|
|
12764
|
-
checks.push({
|
|
12765
|
-
ok: missing.length === 0,
|
|
12766
|
-
label: `required GCP APIs enabled in ${gcpProject}`,
|
|
12767
|
-
detail: missing.length ? `disabled: ${missing.join(", ")}` : void 0
|
|
12768
|
-
});
|
|
12769
|
-
}
|
|
12770
|
-
}
|
|
12771
|
-
const waived = applyWaivers(checks, config?.verifyWaivers ?? []);
|
|
12772
|
-
return { ok: waived.every((c) => c.ok || c.waived), repo, class: repoClass, baseBranch, checks: waived };
|
|
12773
|
-
}
|
|
12774
|
-
function applyWaivers(checks, waivers) {
|
|
12775
|
-
if (!waivers?.length) return checks;
|
|
12776
|
-
const set = new Set(waivers);
|
|
12777
|
-
return checks.map((c) => !c.ok && set.has(c.label) ? { ...c, waived: true } : c);
|
|
12778
|
-
}
|
|
12779
|
-
function renderBootstrapVerifyReport(report) {
|
|
12780
|
-
const lines = [`mmi-cli bootstrap verify: ${report.ok ? "OK" : "CHECK"} ${report.repo} (${report.class}, ${report.baseBranch})`];
|
|
12781
|
-
for (const check of report.checks) {
|
|
12782
|
-
const status = check.ok ? "OK" : check.waived ? "WAIVE" : "FAIL";
|
|
12783
|
-
lines.push(`${status} ${check.label}${check.detail ? ` - ${check.detail}` : ""}`);
|
|
12784
|
-
}
|
|
12785
|
-
return lines.join("\n");
|
|
12786
|
-
}
|
|
12787
|
-
|
|
12788
|
-
// src/bootstrap-apply.ts
|
|
12789
|
-
function parseOwnerRepo(repo) {
|
|
12790
|
-
const trimmed = repo.trim();
|
|
12791
|
-
const parts = trimmed.split("/");
|
|
12792
|
-
if (parts.length !== 2 || !parts[0]?.trim() || !parts[1]?.trim()) {
|
|
12793
|
-
throw new Error("repo must be owner/repo");
|
|
12794
|
-
}
|
|
12795
|
-
const owner = parts[0].trim();
|
|
12796
|
-
const name = parts[1].trim();
|
|
12797
|
-
if (owner.includes("\\") || name.includes("\\")) throw new Error("repo must be owner/repo");
|
|
12798
|
-
return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
|
|
12799
|
-
}
|
|
12800
|
-
var DEFAULT_INSTALL_CMD = "npm ci";
|
|
12801
|
-
var DEFAULT_GATE_CMD = "npm run check";
|
|
12802
|
-
function gateSeedVars(cls, releaseTrack) {
|
|
12803
|
-
const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
|
|
12804
|
-
if (track === "trunk") {
|
|
12805
|
-
return {
|
|
12806
|
-
GATE_CMD: DEFAULT_GATE_CMD,
|
|
12807
|
-
GATE_PUSH_BRANCHES_YAML: "[main]",
|
|
12808
|
-
GATE_FULL_RUN_BRANCH: "main",
|
|
12809
|
-
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/main"]'
|
|
12810
|
-
};
|
|
12811
|
-
}
|
|
12812
|
-
if (track === "direct") {
|
|
12813
|
-
return {
|
|
12814
|
-
GATE_CMD: DEFAULT_GATE_CMD,
|
|
12815
|
-
GATE_PUSH_BRANCHES_YAML: "[development, main]",
|
|
12816
|
-
GATE_FULL_RUN_BRANCH: "development",
|
|
12817
|
-
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/main"]'
|
|
12818
|
-
};
|
|
12819
|
-
}
|
|
12820
|
-
return {
|
|
12821
|
-
GATE_CMD: DEFAULT_GATE_CMD,
|
|
12822
|
-
GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
|
|
12823
|
-
GATE_FULL_RUN_BRANCH: "development",
|
|
12824
|
-
GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/rc", "refs/heads/main"]'
|
|
12825
|
-
};
|
|
12826
|
-
}
|
|
12827
|
-
function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
|
|
12828
|
-
const out = { ...vars };
|
|
12829
|
-
out.REPO_NAME ??= parsed.name;
|
|
12830
|
-
out.REPO_SLUG ??= parsed.slug;
|
|
12831
|
-
out.CLASS ??= cls;
|
|
12832
|
-
out.INSTALL_CMD ??= DEFAULT_INSTALL_CMD;
|
|
12833
|
-
const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
|
|
12834
|
-
for (const [key, value] of Object.entries(gateSeedVars(cls, track))) {
|
|
12835
|
-
out[key] ??= value;
|
|
12836
|
-
}
|
|
12837
|
-
return out;
|
|
12838
|
-
}
|
|
12839
|
-
function planSeedAction(seed, exists) {
|
|
12840
|
-
if (seed.source === "fanout") {
|
|
12841
|
-
return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
|
|
12842
|
-
}
|
|
12843
|
-
if (seed.source === "managed-block") {
|
|
12844
|
-
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
|
|
12845
|
-
}
|
|
12846
|
-
if (seed.ownership === "repo") {
|
|
12847
|
-
return exists ? { target: seed.target, action: "skip", ownership: "repo", reason: "repo-owned, already present (never clobbered)" } : { target: seed.target, action: "create", ownership: "repo", reason: "repo-owned, missing" };
|
|
12848
|
-
}
|
|
12849
|
-
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
|
|
12850
|
-
}
|
|
12851
|
-
function reconcileSeedAction(action, content, isManagedBlock) {
|
|
12852
|
-
if (action.action === "skip") return action;
|
|
12853
|
-
if (content == null) {
|
|
12854
|
-
return { ...action, action: "skip", reason: "no resolvable content" };
|
|
12855
|
-
}
|
|
12856
|
-
if (isManagedBlock) return action;
|
|
12857
|
-
const unfilled = missingPlaceholders(content);
|
|
12858
|
-
if (unfilled.length) {
|
|
12859
|
-
return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
|
|
12860
|
-
}
|
|
12861
|
-
return action;
|
|
12862
|
-
}
|
|
12863
|
-
function renderSeedPlan(actions) {
|
|
12864
|
-
const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
|
|
12865
|
-
for (const a of actions) {
|
|
12866
|
-
lines.push(` ${a.action.toUpperCase().padEnd(6)} ${a.target} (${a.ownership}: ${a.reason})`);
|
|
12867
|
-
}
|
|
12868
|
-
const order = ["create", "update", "skip"];
|
|
12869
|
-
lines.push(` \u2014 ${order.map((k) => `${actions.filter((a) => a.action === k).length} ${k}`).join(", ")}`);
|
|
12870
|
-
return lines.join("\n");
|
|
12871
|
-
}
|
|
12872
|
-
var GITHUB_DEFAULT_LABELS = [
|
|
12873
|
-
"documentation",
|
|
12874
|
-
"duplicate",
|
|
12875
|
-
"enhancement",
|
|
12876
|
-
"good first issue",
|
|
12877
|
-
"help wanted",
|
|
12878
|
-
"invalid",
|
|
12879
|
-
"question",
|
|
12880
|
-
"wontfix"
|
|
12881
|
-
];
|
|
12882
|
-
function labelsToPrune(orgLabelNames) {
|
|
12883
|
-
const org = new Set(orgLabelNames);
|
|
12884
|
-
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
12885
|
-
}
|
|
12886
|
-
function resolveSeedContent(seed, vars, readFile5) {
|
|
12887
|
-
if (seed.source === "self") return readFile5(seed.target);
|
|
12888
|
-
if (seed.source.startsWith("seed:")) {
|
|
12889
|
-
const tmpl = readFile5(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
12890
|
-
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
12891
|
-
}
|
|
12892
|
-
return null;
|
|
12893
|
-
}
|
|
12894
|
-
function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
12895
|
-
const parsedRepo = parseOwnerRepo(repo);
|
|
12896
|
-
const slug = parsedRepo.slug;
|
|
12897
|
-
if (options.projectType && !isProjectType(options.projectType)) {
|
|
12898
|
-
throw new Error(`projectType must be one of: ${PROJECT_TYPES.join(", ")}`);
|
|
12899
|
-
}
|
|
12900
|
-
if (options.deployModel && !isDeployModel(options.deployModel)) {
|
|
12901
|
-
throw new Error(`deployModel must be one of: ${DEPLOY_MODELS.join(", ")}`);
|
|
12902
|
-
}
|
|
12903
|
-
if (options.releaseTrack && !isReleaseTrack(options.releaseTrack)) {
|
|
12904
|
-
throw new Error("releaseTrack must be full, direct, or trunk");
|
|
12905
|
-
}
|
|
12906
|
-
const shape = {
|
|
12907
|
-
class: cls,
|
|
12908
|
-
projectType: options.projectType,
|
|
12909
|
-
deployModel: options.deployModel
|
|
12910
|
-
};
|
|
12911
|
-
const projectType = resolveProjectTypeConfident(shape, parsedRepo.fullName);
|
|
12912
|
-
if (!projectType) {
|
|
12913
|
-
throw new Error(
|
|
12914
|
-
`Project type for ${parsedRepo.fullName} is unset and not derivable \u2014 pass --project-type <${PROJECT_TYPES.join("|")}> and --deploy-model <${DEPLOY_MODELS.join("|")}> (prevents defaulting a non-web repo to tenant-container).`
|
|
12915
|
-
);
|
|
12916
|
-
}
|
|
12917
|
-
const deployModel = resolveDeployModel({ ...shape, projectType }, parsedRepo.fullName);
|
|
12918
|
-
const num = (v) => {
|
|
12919
|
-
if (v == null || v === "") return void 0;
|
|
12920
|
-
const n = Number(v);
|
|
12921
|
-
return Number.isFinite(n) ? n : void 0;
|
|
12922
|
-
};
|
|
12923
|
-
const statusOptions = vars.STATUS_TODO || vars.STATUS_IN_PROGRESS || vars.STATUS_IN_REVIEW || vars.STATUS_DONE ? {
|
|
12924
|
-
Todo: vars.STATUS_TODO,
|
|
12925
|
-
"In Progress": vars.STATUS_IN_PROGRESS,
|
|
12926
|
-
"In Review": vars.STATUS_IN_REVIEW,
|
|
12927
|
-
Done: vars.STATUS_DONE
|
|
12928
|
-
} : void 0;
|
|
12929
|
-
const priorityOptions = vars.PRIORITY_URGENT || vars.PRIORITY_HIGH || vars.PRIORITY_MEDIUM || vars.PRIORITY_LOW ? {
|
|
12930
|
-
Urgent: vars.PRIORITY_URGENT,
|
|
12931
|
-
High: vars.PRIORITY_HIGH,
|
|
12932
|
-
Medium: vars.PRIORITY_MEDIUM,
|
|
12933
|
-
Low: vars.PRIORITY_LOW
|
|
12934
|
-
} : void 0;
|
|
12935
|
-
const payload = {
|
|
12936
|
-
slug,
|
|
12937
|
-
// Identity. name/division default off the repo name when the skill didn't pass them.
|
|
12938
|
-
name: vars.NAME || parsedRepo.name,
|
|
12939
|
-
division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
|
|
12940
|
-
repos: [`${parsedRepo.owner}/${slug}`],
|
|
12941
|
-
wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
|
|
12942
|
-
branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
|
|
12943
|
-
class: cls,
|
|
12944
|
-
projectType,
|
|
12945
|
-
deployModel,
|
|
12946
|
-
// #1359: always persist an explicit track so release tooling never guesses from absence alone.
|
|
12947
|
-
releaseTrack: resolveBootstrapReleaseTrack(cls, options.releaseTrack),
|
|
12948
|
-
// Board coords (from GraphQL at bootstrap, passed as --var by the skill).
|
|
12949
|
-
projectOwner: vars.PROJECT_OWNER || void 0,
|
|
12950
|
-
projectNumber: num(vars.PROJECT_NUMBER),
|
|
12951
|
-
projectId: vars.PROJECT_ID || void 0,
|
|
12952
|
-
statusFieldId: vars.STATUS_FIELD_ID || void 0,
|
|
12953
|
-
statusOptions,
|
|
12954
|
-
priorityFieldId: vars.PRIORITY_FIELD_ID || void 0,
|
|
12955
|
-
priorityOptions,
|
|
12956
|
-
// Pointers. vaultPath is explicit + canonical; kbPointer is the per-project KB doc path.
|
|
12957
|
-
vaultPath: `/mmi-future/${slug}`,
|
|
12958
|
-
kbPointer: `kb/projects/${slug}.md`
|
|
12959
|
-
};
|
|
12960
|
-
for (const k of Object.keys(payload)) if (payload[k] === void 0) delete payload[k];
|
|
12961
|
-
if (payload.projectId && payload.projectNumber == null) {
|
|
12962
|
-
throw new Error(
|
|
12963
|
-
"bootstrap apply: PROJECT_ID is set but PROJECT_NUMBER is missing \u2014 pass --var PROJECT_NUMBER=<N> or ensure the live board GraphQL query succeeds"
|
|
12964
|
-
);
|
|
12965
|
-
}
|
|
12966
|
-
return payload;
|
|
12967
|
-
}
|
|
12968
|
-
var BOARD_FIELD_VAR_MAP = {
|
|
12969
|
-
Status: {
|
|
12970
|
-
fieldIdVar: "STATUS_FIELD_ID",
|
|
12971
|
-
options: { Todo: "STATUS_TODO", "In Progress": "STATUS_IN_PROGRESS", "In Review": "STATUS_IN_REVIEW", Done: "STATUS_DONE" }
|
|
12972
|
-
},
|
|
12973
|
-
Priority: {
|
|
12974
|
-
fieldIdVar: "PRIORITY_FIELD_ID",
|
|
12975
|
-
options: { Urgent: "PRIORITY_URGENT", High: "PRIORITY_HIGH", Medium: "PRIORITY_MEDIUM", Low: "PRIORITY_LOW" }
|
|
12976
|
-
}
|
|
12977
|
-
};
|
|
12978
|
-
function projectV2BoardNode(fieldsJson) {
|
|
12979
|
-
if (Array.isArray(fieldsJson)) return { fields: { nodes: fieldsJson } };
|
|
12980
|
-
const wrapped = fieldsJson;
|
|
12981
|
-
return wrapped?.data?.node ?? wrapped?.node;
|
|
12982
|
-
}
|
|
12983
|
-
function extractBoardFieldVars(fieldsJson) {
|
|
12984
|
-
const out = {};
|
|
12985
|
-
const projectNode = projectV2BoardNode(fieldsJson);
|
|
12986
|
-
if (typeof projectNode?.number === "number" && Number.isFinite(projectNode.number)) {
|
|
12987
|
-
out.PROJECT_NUMBER = String(projectNode.number);
|
|
14096
|
+
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
14097
|
+
const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
|
|
14098
|
+
checks.push({
|
|
14099
|
+
ok: missing.length === 0,
|
|
14100
|
+
label: "Hub required status checks configured",
|
|
14101
|
+
detail: optionDetail(missing)
|
|
14102
|
+
});
|
|
14103
|
+
} else if (repoClass === "deployable") {
|
|
14104
|
+
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
14105
|
+
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
14106
|
+
checks.push({
|
|
14107
|
+
ok: missing.length === 0,
|
|
14108
|
+
label: "product required status checks configured",
|
|
14109
|
+
detail: missing.length ? `missing contexts: ${missing.join(", ")} \u2014 apply ${requiredProductRulesetRef} as an active repo ruleset` : void 0
|
|
14110
|
+
});
|
|
12988
14111
|
}
|
|
12989
|
-
const
|
|
12990
|
-
if (
|
|
12991
|
-
|
|
12992
|
-
const
|
|
12993
|
-
const
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
14112
|
+
const declaredApis = (deps.requiredGcpApis ?? []).filter((a) => a && a.trim());
|
|
14113
|
+
if (declaredApis.length > 0) {
|
|
14114
|
+
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
14115
|
+
const gcpProject = gcpProjectForSlug(slug);
|
|
14116
|
+
const enabled = deps.listEnabledGcpApis ? await deps.listEnabledGcpApis(gcpProject) : null;
|
|
14117
|
+
if (enabled == null) {
|
|
14118
|
+
checks.push({
|
|
14119
|
+
ok: false,
|
|
14120
|
+
label: `required GCP APIs enabled in ${gcpProject}`,
|
|
14121
|
+
detail: `could not list enabled APIs (gcloud unavailable, not authed, or no project ${gcpProject})`
|
|
14122
|
+
});
|
|
14123
|
+
} else {
|
|
14124
|
+
const missing = findMissingGcpApis(declaredApis, enabled);
|
|
14125
|
+
checks.push({
|
|
14126
|
+
ok: missing.length === 0,
|
|
14127
|
+
label: `required GCP APIs enabled in ${gcpProject}`,
|
|
14128
|
+
detail: missing.length ? `disabled: ${missing.join(", ")}` : void 0
|
|
14129
|
+
});
|
|
13002
14130
|
}
|
|
13003
14131
|
}
|
|
13004
|
-
|
|
13005
|
-
}
|
|
13006
|
-
function boardFieldsQueryArgs(projectId) {
|
|
13007
|
-
const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
|
|
13008
|
-
return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
|
|
13009
|
-
}
|
|
13010
|
-
function serializeRegistry(obj) {
|
|
13011
|
-
return `${JSON.stringify(obj, null, 2)}
|
|
13012
|
-
`;
|
|
14132
|
+
const waived = applyWaivers(checks, config?.verifyWaivers ?? []);
|
|
14133
|
+
return { ok: waived.every((c) => c.ok || c.waived), repo, class: repoClass, baseBranch, checks: waived };
|
|
13013
14134
|
}
|
|
13014
|
-
function
|
|
13015
|
-
|
|
13016
|
-
const
|
|
13017
|
-
|
|
13018
|
-
const projectEntries = Array.isArray(projects.projects) ? projects.projects : [];
|
|
13019
|
-
const name = entry.name ?? entry.repo;
|
|
13020
|
-
const canonName = entry.repo.toLowerCase();
|
|
13021
|
-
const inFanout = fanoutRepos.some((r) => typeof r.repo === "string" && r.repo.toLowerCase() === canonName);
|
|
13022
|
-
const inProjects = projectEntries.some(
|
|
13023
|
-
(p) => p.slug === entry.slug || Array.isArray(p.repos) && p.repos.some((full) => String(full).split("/").pop()?.toLowerCase() === canonName)
|
|
13024
|
-
);
|
|
13025
|
-
if (inFanout || inProjects) {
|
|
13026
|
-
return { changed: false, fanoutTargets: fanoutTargetsRaw, projects: projectsRaw };
|
|
13027
|
-
}
|
|
13028
|
-
const projectEntry = {
|
|
13029
|
-
name,
|
|
13030
|
-
slug: entry.slug,
|
|
13031
|
-
projectId: entry.projectId,
|
|
13032
|
-
wikiRepo: entry.wikiRepo,
|
|
13033
|
-
repos: [`mutmutco/${entry.repo}`]
|
|
13034
|
-
};
|
|
13035
|
-
if (entry.cls === "content") projectEntry.branch = "main";
|
|
13036
|
-
for (const k of Object.keys(projectEntry)) if (projectEntry[k] === void 0) delete projectEntry[k];
|
|
13037
|
-
const nextProjects = { ...projects, projects: [...projectEntries, projectEntry] };
|
|
13038
|
-
const fanoutEntry = { repo: entry.repo, branch: entry.branch, class: entry.cls };
|
|
13039
|
-
const nextFanout = { ...fanout, repos: [...fanoutRepos, fanoutEntry] };
|
|
13040
|
-
return {
|
|
13041
|
-
changed: true,
|
|
13042
|
-
fanoutTargets: serializeRegistry(nextFanout),
|
|
13043
|
-
projects: serializeRegistry(nextProjects)
|
|
13044
|
-
};
|
|
14135
|
+
function applyWaivers(checks, waivers) {
|
|
14136
|
+
if (!waivers?.length) return checks;
|
|
14137
|
+
const set = new Set(waivers);
|
|
14138
|
+
return checks.map((c) => !c.ok && set.has(c.label) ? { ...c, waived: true } : c);
|
|
13045
14139
|
}
|
|
13046
|
-
function
|
|
13047
|
-
const
|
|
13048
|
-
for (const
|
|
13049
|
-
const
|
|
13050
|
-
|
|
14140
|
+
function renderBootstrapVerifyReport(report) {
|
|
14141
|
+
const lines = [`mmi-cli bootstrap verify: ${report.ok ? "OK" : "CHECK"} ${report.repo} (${report.class}, ${report.baseBranch})`];
|
|
14142
|
+
for (const check of report.checks) {
|
|
14143
|
+
const status = check.ok ? "OK" : check.waived ? "WAIVE" : "FAIL";
|
|
14144
|
+
lines.push(`${status} ${check.label}${check.detail ? ` - ${check.detail}` : ""}`);
|
|
13051
14145
|
}
|
|
13052
|
-
return
|
|
13053
|
-
}
|
|
13054
|
-
function contentPutArgs(repo, path2, content, branch, sha) {
|
|
13055
|
-
const args = [
|
|
13056
|
-
"api",
|
|
13057
|
-
"-X",
|
|
13058
|
-
"PUT",
|
|
13059
|
-
`repos/${repo}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
|
|
13060
|
-
"-f",
|
|
13061
|
-
`message=bootstrap: seed ${path2}`,
|
|
13062
|
-
"-f",
|
|
13063
|
-
`content=${Buffer.from(content, "utf8").toString("base64")}`,
|
|
13064
|
-
"-f",
|
|
13065
|
-
`branch=${branch}`
|
|
13066
|
-
];
|
|
13067
|
-
if (sha) args.push("-f", `sha=${sha}`);
|
|
13068
|
-
return args;
|
|
14146
|
+
return lines.join("\n");
|
|
13069
14147
|
}
|
|
13070
14148
|
|
|
13071
14149
|
// ../infra/registry-endpoints.mjs
|
|
@@ -13244,6 +14322,12 @@ function renderVerifySecrets(body) {
|
|
|
13244
14322
|
}
|
|
13245
14323
|
|
|
13246
14324
|
// src/project-readiness.ts
|
|
14325
|
+
function stagesForTrack(meta) {
|
|
14326
|
+
return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
|
|
14327
|
+
}
|
|
14328
|
+
function stageInTrack(meta, stage2) {
|
|
14329
|
+
return stagesForTrack(meta).includes(stage2);
|
|
14330
|
+
}
|
|
13247
14331
|
function dnsErrorToResolution(code) {
|
|
13248
14332
|
return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
|
|
13249
14333
|
}
|
|
@@ -13551,15 +14635,15 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
13551
14635
|
secretsError = e?.message || "secrets list failed";
|
|
13552
14636
|
}
|
|
13553
14637
|
const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
|
|
13554
|
-
const required = projectRequiresDeployCoords(model, stage2, meta);
|
|
14638
|
+
const required = stageInTrack(meta, stage2) && projectRequiresDeployCoords(model, stage2, meta);
|
|
13555
14639
|
return [stage2, { required, ok: required ? await deps.hasDeployCoords(slug, stage2) : true }];
|
|
13556
14640
|
})));
|
|
13557
14641
|
const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
|
|
13558
|
-
const required = projectRequiresDeployState(model, stage2);
|
|
14642
|
+
const required = stageInTrack(meta, stage2) && projectRequiresDeployState(model, stage2);
|
|
13559
14643
|
return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
|
|
13560
14644
|
})));
|
|
13561
14645
|
const secrets2 = Object.fromEntries(STAGES.map((stage2) => {
|
|
13562
|
-
const required = stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key));
|
|
14646
|
+
const required = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key)) : [];
|
|
13563
14647
|
const present = required.filter((key) => presentSecrets.has(key));
|
|
13564
14648
|
const missing = required.filter((key) => !presentSecrets.has(key));
|
|
13565
14649
|
return [stage2, { required, present, missing }];
|
|
@@ -13624,7 +14708,7 @@ ${section}`.trim();
|
|
|
13624
14708
|
}
|
|
13625
14709
|
|
|
13626
14710
|
// src/project-set.ts
|
|
13627
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired"];
|
|
14711
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "ci", "requiredChecks", "gate"];
|
|
13628
14712
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
13629
14713
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
13630
14714
|
function parseRuntimeSecretsVar(raw) {
|
|
@@ -13783,6 +14867,43 @@ function parsePublishRequiredVar(raw) {
|
|
|
13783
14867
|
if (raw === "false") return false;
|
|
13784
14868
|
throw new Error("project set: publishRequired must be true or false");
|
|
13785
14869
|
}
|
|
14870
|
+
function parseRequiredChecksVar(raw) {
|
|
14871
|
+
let parsed;
|
|
14872
|
+
try {
|
|
14873
|
+
parsed = JSON.parse(raw);
|
|
14874
|
+
} catch {
|
|
14875
|
+
throw new Error('project set: requiredChecks must be a JSON array, e.g. ["gate"] or [] for an intentional no-ci repo');
|
|
14876
|
+
}
|
|
14877
|
+
if (!Array.isArray(parsed) || parsed.some((c) => typeof c !== "string" || !c.trim())) {
|
|
14878
|
+
throw new Error("project set: requiredChecks must be a JSON array of non-empty check-context strings (use [] for none)");
|
|
14879
|
+
}
|
|
14880
|
+
return parsed.map((c) => c.trim());
|
|
14881
|
+
}
|
|
14882
|
+
function parseGateVar(raw) {
|
|
14883
|
+
let parsed;
|
|
14884
|
+
try {
|
|
14885
|
+
parsed = JSON.parse(raw);
|
|
14886
|
+
} catch {
|
|
14887
|
+
throw new Error('project set: gate must be JSON, e.g. {"runtime":"python","cmd":"pytest","workdir":"app"}');
|
|
14888
|
+
}
|
|
14889
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14890
|
+
throw new Error("project set: gate must be a {runtime,cmd,workdir,cacheDepPath,pyVersion} object");
|
|
14891
|
+
}
|
|
14892
|
+
const map = parsed;
|
|
14893
|
+
const out = {};
|
|
14894
|
+
for (const [key, value] of Object.entries(map)) {
|
|
14895
|
+
if (key === "runtime") {
|
|
14896
|
+
if (value !== "node" && value !== "python") throw new Error('project set: gate.runtime must be "node" or "python"');
|
|
14897
|
+
out.runtime = value;
|
|
14898
|
+
} else if (key === "cmd" || key === "workdir" || key === "cacheDepPath" || key === "pyVersion") {
|
|
14899
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`project set: gate.${key} must be a non-empty string`);
|
|
14900
|
+
out[key] = value.trim();
|
|
14901
|
+
} else {
|
|
14902
|
+
throw new Error(`project set: gate key "${key}" \u2014 expected only runtime/cmd/workdir/cacheDepPath/pyVersion`);
|
|
14903
|
+
}
|
|
14904
|
+
}
|
|
14905
|
+
return out;
|
|
14906
|
+
}
|
|
13786
14907
|
var SETTABLE_VAR_KEYS = [
|
|
13787
14908
|
"name",
|
|
13788
14909
|
"division",
|
|
@@ -13803,7 +14924,10 @@ var SETTABLE_VAR_KEYS = [
|
|
|
13803
14924
|
"statusOptions",
|
|
13804
14925
|
"priorityFieldId",
|
|
13805
14926
|
"priorityOptions",
|
|
13806
|
-
"portRange"
|
|
14927
|
+
"portRange",
|
|
14928
|
+
"ci",
|
|
14929
|
+
"requiredChecks",
|
|
14930
|
+
"gate"
|
|
13807
14931
|
];
|
|
13808
14932
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
13809
14933
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -13816,7 +14940,10 @@ var SETTABLE_VAR_HINTS = {
|
|
|
13816
14940
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
13817
14941
|
statusOptions: "JSON name\u2192id map",
|
|
13818
14942
|
priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
|
|
13819
|
-
portRange: "JSON {start,end} or [start,end]"
|
|
14943
|
+
portRange: "JSON {start,end} or [start,end]",
|
|
14944
|
+
ci: "none \u2014 declare intentional no-ci",
|
|
14945
|
+
requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
|
|
14946
|
+
gate: "JSON {runtime,cmd,workdir,cacheDepPath,pyVersion}"
|
|
13820
14947
|
};
|
|
13821
14948
|
function settableVarHelp() {
|
|
13822
14949
|
const keys = SETTABLE_VAR_KEYS.map((k) => {
|
|
@@ -13879,6 +15006,13 @@ function buildProjectSetPatch(input) {
|
|
|
13879
15006
|
patch[key] = parseReposVar(raw);
|
|
13880
15007
|
} else if (key === "publishRequired") {
|
|
13881
15008
|
patch[key] = parsePublishRequiredVar(raw);
|
|
15009
|
+
} else if (key === "ci") {
|
|
15010
|
+
if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
|
|
15011
|
+
patch[key] = raw;
|
|
15012
|
+
} else if (key === "requiredChecks") {
|
|
15013
|
+
patch[key] = parseRequiredChecksVar(raw);
|
|
15014
|
+
} else if (key === "gate") {
|
|
15015
|
+
patch[key] = parseGateVar(raw);
|
|
13882
15016
|
} else {
|
|
13883
15017
|
patch[key] = raw;
|
|
13884
15018
|
}
|
|
@@ -13919,8 +15053,8 @@ function resolveKbSource(rawBase) {
|
|
|
13919
15053
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
13920
15054
|
}
|
|
13921
15055
|
function buildKbGetArgs(src, path2) {
|
|
13922
|
-
const
|
|
13923
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
15056
|
+
const clean4 = path2.replace(/^\/+/, "");
|
|
15057
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
13924
15058
|
}
|
|
13925
15059
|
function buildKbTreeArgs(src) {
|
|
13926
15060
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -13937,10 +15071,10 @@ function parseKbTree(stdout, prefix) {
|
|
|
13937
15071
|
}
|
|
13938
15072
|
|
|
13939
15073
|
// src/plan.ts
|
|
13940
|
-
var
|
|
15074
|
+
var import_node_path16 = require("node:path");
|
|
13941
15075
|
var PLANS_DIR = "plans";
|
|
13942
|
-
var META_FILE = (0,
|
|
13943
|
-
var planPath = (slug) => (0,
|
|
15076
|
+
var META_FILE = (0, import_node_path16.join)(PLANS_DIR, ".plan-meta.json");
|
|
15077
|
+
var planPath = (slug) => (0, import_node_path16.join)(PLANS_DIR, `${slug}.md`);
|
|
13944
15078
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
13945
15079
|
function parseMeta(raw) {
|
|
13946
15080
|
if (!raw) return {};
|
|
@@ -13965,7 +15099,7 @@ function hashContent(s) {
|
|
|
13965
15099
|
function staleHint(slug) {
|
|
13966
15100
|
return `remote "${slug}" is newer \u2014 run \`mmi-cli northstar pull ${slug}\` first (your local is based on an older version), or re-push with \`--force\` to overwrite`;
|
|
13967
15101
|
}
|
|
13968
|
-
var INDEX_FILE = (0,
|
|
15102
|
+
var INDEX_FILE = (0, import_node_path16.join)(PLANS_DIR, ".index.json");
|
|
13969
15103
|
var INDEX_TTL_MS = 6e4;
|
|
13970
15104
|
function parseIndex(raw) {
|
|
13971
15105
|
if (!raw) return null;
|
|
@@ -13994,7 +15128,7 @@ function mergeIndex(idx, scope, plans, now) {
|
|
|
13994
15128
|
const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
|
|
13995
15129
|
return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
|
|
13996
15130
|
}
|
|
13997
|
-
var QUEUE_FILE = (0,
|
|
15131
|
+
var QUEUE_FILE = (0, import_node_path16.join)(PLANS_DIR, ".sync-queue.json");
|
|
13998
15132
|
var QUEUE_MAX_ATTEMPTS = 10;
|
|
13999
15133
|
function isValidQueueEntry(e) {
|
|
14000
15134
|
if (!e || typeof e !== "object") return false;
|
|
@@ -14163,6 +15297,16 @@ async function planPull(deps, slug, opts = {}) {
|
|
|
14163
15297
|
deps.err(`local ${planPath(slug)} has unpushed edits \u2014 push it, or pull with --force to overwrite`);
|
|
14164
15298
|
return false;
|
|
14165
15299
|
}
|
|
15300
|
+
if (local != null && !opts.force) {
|
|
15301
|
+
const queued = parseQueue(deps.readQueueRaw()).find((e) => e.project === project2 && e.slug === slug);
|
|
15302
|
+
if (queued) {
|
|
15303
|
+
const state = queued.conflict ?? queued.deadLettered ?? "still pending";
|
|
15304
|
+
deps.err(
|
|
15305
|
+
`local ${planPath(slug)} has an unpushed edit queued (${state}) \u2014 land it with \`mmi-cli northstar push ${slug} --wait\`, or pull with --force to discard your local edit`
|
|
15306
|
+
);
|
|
15307
|
+
return false;
|
|
15308
|
+
}
|
|
15309
|
+
}
|
|
14166
15310
|
const qs = new URLSearchParams({ project: project2, slug }).toString();
|
|
14167
15311
|
const res = await deps.fetch(`${deps.apiUrl}/plan/get?${qs}`, {
|
|
14168
15312
|
method: "GET",
|
|
@@ -14372,6 +15516,7 @@ async function planSync(deps, opts = {}) {
|
|
|
14372
15516
|
} catch (e) {
|
|
14373
15517
|
if (!opts.quiet) deps.err(`northstar sync: list refresh failed: ${e.message}`);
|
|
14374
15518
|
}
|
|
15519
|
+
return kept;
|
|
14375
15520
|
}
|
|
14376
15521
|
async function planStatus(deps, opts = {}) {
|
|
14377
15522
|
const queue = parseQueue(deps.readQueueRaw());
|
|
@@ -14443,11 +15588,11 @@ async function planGraduate(deps, slug, opts = {}) {
|
|
|
14443
15588
|
}
|
|
14444
15589
|
|
|
14445
15590
|
// src/atomic-write.ts
|
|
14446
|
-
var
|
|
15591
|
+
var import_node_fs18 = require("node:fs");
|
|
14447
15592
|
function atomicWriteFileSync(path2, content) {
|
|
14448
15593
|
const tmp = `${path2}.${process.pid}.tmp`;
|
|
14449
|
-
(0,
|
|
14450
|
-
(0,
|
|
15594
|
+
(0, import_node_fs18.writeFileSync)(tmp, content, "utf8");
|
|
15595
|
+
(0, import_node_fs18.renameSync)(tmp, path2);
|
|
14451
15596
|
}
|
|
14452
15597
|
|
|
14453
15598
|
// src/oauth.ts
|
|
@@ -14678,7 +15823,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
14678
15823
|
}
|
|
14679
15824
|
function readRepoVersion() {
|
|
14680
15825
|
try {
|
|
14681
|
-
return JSON.parse((0,
|
|
15826
|
+
return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
14682
15827
|
} catch {
|
|
14683
15828
|
return void 0;
|
|
14684
15829
|
}
|
|
@@ -14770,7 +15915,7 @@ async function applyCodexPluginHeal(surface, log) {
|
|
|
14770
15915
|
return true;
|
|
14771
15916
|
}
|
|
14772
15917
|
var program2 = new Command();
|
|
14773
|
-
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
|
|
15918
|
+
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
|
|
14774
15919
|
async function runRulesSync(opts, io = consoleIo) {
|
|
14775
15920
|
const cfg = await loadConfig();
|
|
14776
15921
|
if (isRulesSource(cfg.orgRulesSource)) {
|
|
@@ -14804,11 +15949,11 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
14804
15949
|
for (const entry of fetched) {
|
|
14805
15950
|
if ("error" in entry) continue;
|
|
14806
15951
|
const { file, source } = entry;
|
|
14807
|
-
const current = (0,
|
|
15952
|
+
const current = (0, import_node_fs19.existsSync)(file) ? await (0, import_promises6.readFile)(file, "utf8") : null;
|
|
14808
15953
|
if (needsUpdate(source, current)) {
|
|
14809
15954
|
const slash = file.lastIndexOf("/");
|
|
14810
|
-
if (slash > 0) (0,
|
|
14811
|
-
await (0,
|
|
15955
|
+
if (slash > 0) (0, import_node_fs19.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
15956
|
+
await (0, import_promises6.writeFile)(file, normalizeEol(source), "utf8");
|
|
14812
15957
|
changed++;
|
|
14813
15958
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
14814
15959
|
}
|
|
@@ -14833,9 +15978,9 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
14833
15978
|
return null;
|
|
14834
15979
|
}
|
|
14835
15980
|
},
|
|
14836
|
-
localContent: async (f) => (0,
|
|
15981
|
+
localContent: async (f) => (0, import_node_fs19.existsSync)(f) ? await (0, import_promises6.readFile)(f, "utf8") : null,
|
|
14837
15982
|
writeDoc: async (f, c) => {
|
|
14838
|
-
await (0,
|
|
15983
|
+
await (0, import_promises6.writeFile)(f, c, "utf8");
|
|
14839
15984
|
}
|
|
14840
15985
|
});
|
|
14841
15986
|
for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
|
|
@@ -14845,7 +15990,13 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
14845
15990
|
var docs = program2.command("docs").description("repo-owned authoritative docs");
|
|
14846
15991
|
docs.command("sync").option("--quiet", "stay silent unless something changed or errored").description("refresh README.md / architecture.md from the repo default branch (keeper-authored); never clobbers uncommitted edits").action((opts) => runDocsSync(opts));
|
|
14847
15992
|
registerSagaCommands(program2);
|
|
15993
|
+
registerHandoffCommands(program2);
|
|
14848
15994
|
registerHonchoCommands(program2);
|
|
15995
|
+
registerScroogeCommands(program2);
|
|
15996
|
+
program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
|
|
15997
|
+
const manifest = buildCommandManifest(program2);
|
|
15998
|
+
consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
|
|
15999
|
+
});
|
|
14849
16000
|
async function runWhoami(io = consoleIo) {
|
|
14850
16001
|
const cfg = await loadConfig();
|
|
14851
16002
|
const report = await resolveWhoami({
|
|
@@ -14858,8 +16009,18 @@ async function runWhoami(io = consoleIo) {
|
|
|
14858
16009
|
program2.command("whoami").description('resolve the logged-in human: {login, source, sessionExpiresAt} JSON; source "unknown" (exit 0) when neither the Hub session nor gh can name them').option("--json", "machine-readable output (default)").action(async () => {
|
|
14859
16010
|
await runWhoami();
|
|
14860
16011
|
});
|
|
14861
|
-
program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
16012
|
+
program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs (--scratch: stale local saga/plan/honcho files)").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune stale local saga/plan/honcho scratch already recorded remotely (#1474) instead of git branches/refs").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
14862
16013
|
if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
|
|
16014
|
+
if (o.scratch) {
|
|
16015
|
+
try {
|
|
16016
|
+
const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
16017
|
+
const run = executeScratchGc(root, { apply: Boolean(o.apply) });
|
|
16018
|
+
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
|
|
16019
|
+
return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply)));
|
|
16020
|
+
} catch (e) {
|
|
16021
|
+
return fail(`gc --scratch: ${e.message}`);
|
|
16022
|
+
}
|
|
16023
|
+
}
|
|
14863
16024
|
const limit = Number.parseInt(o.limit, 10);
|
|
14864
16025
|
if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
|
|
14865
16026
|
try {
|
|
@@ -14887,6 +16048,120 @@ ${renderGcApplyResult(applyResult)}`);
|
|
|
14887
16048
|
fail(`gc: ${e.message}`);
|
|
14888
16049
|
}
|
|
14889
16050
|
});
|
|
16051
|
+
var NPM_PROVISION_TIMEOUT_MS = 3e5;
|
|
16052
|
+
var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
|
|
16053
|
+
function runWorktreeInstall(command, cwd, quiet) {
|
|
16054
|
+
const [bin, ...args] = command.split(" ");
|
|
16055
|
+
const file = isWin ? "cmd.exe" : bin;
|
|
16056
|
+
const spawnArgs = isWin ? ["/c", bin, ...args] : args;
|
|
16057
|
+
return new Promise((resolve, reject) => {
|
|
16058
|
+
const child = (0, import_node_child_process10.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
16059
|
+
const timer = setTimeout(() => {
|
|
16060
|
+
try {
|
|
16061
|
+
child.kill();
|
|
16062
|
+
} catch {
|
|
16063
|
+
}
|
|
16064
|
+
reject(new Error(`${command} timed out after ${NPM_PROVISION_TIMEOUT_MS}ms in ${cwd}`));
|
|
16065
|
+
}, NPM_PROVISION_TIMEOUT_MS);
|
|
16066
|
+
child.on("error", (e) => {
|
|
16067
|
+
clearTimeout(timer);
|
|
16068
|
+
reject(e);
|
|
16069
|
+
});
|
|
16070
|
+
child.on("exit", (code) => {
|
|
16071
|
+
clearTimeout(timer);
|
|
16072
|
+
if (code === 0) resolve();
|
|
16073
|
+
else reject(new Error(`${command} exited ${code} in ${cwd}`));
|
|
16074
|
+
});
|
|
16075
|
+
});
|
|
16076
|
+
}
|
|
16077
|
+
async function primaryCheckoutRoot(worktreeRoot) {
|
|
16078
|
+
try {
|
|
16079
|
+
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
16080
|
+
return out ? (0, import_node_path17.dirname)(out) : void 0;
|
|
16081
|
+
} catch {
|
|
16082
|
+
return void 0;
|
|
16083
|
+
}
|
|
16084
|
+
}
|
|
16085
|
+
function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
16086
|
+
return {
|
|
16087
|
+
runInstall: (command, cwd) => runWorktreeInstall(command, cwd, quiet),
|
|
16088
|
+
primaryCheckout: () => primaryCheckoutRoot(worktreeRoot),
|
|
16089
|
+
log
|
|
16090
|
+
};
|
|
16091
|
+
}
|
|
16092
|
+
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
16093
|
+
const lockPath = (0, import_node_path17.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
|
|
16094
|
+
const take = () => {
|
|
16095
|
+
const fd = (0, import_node_fs19.openSync)(lockPath, "wx");
|
|
16096
|
+
try {
|
|
16097
|
+
(0, import_node_fs19.writeSync)(fd, String(Date.now()));
|
|
16098
|
+
} finally {
|
|
16099
|
+
(0, import_node_fs19.closeSync)(fd);
|
|
16100
|
+
}
|
|
16101
|
+
return () => {
|
|
16102
|
+
try {
|
|
16103
|
+
(0, import_node_fs19.rmSync)(lockPath, { force: true });
|
|
16104
|
+
} catch {
|
|
16105
|
+
}
|
|
16106
|
+
};
|
|
16107
|
+
};
|
|
16108
|
+
try {
|
|
16109
|
+
(0, import_node_fs19.mkdirSync)((0, import_node_path17.dirname)(lockPath), { recursive: true });
|
|
16110
|
+
return take();
|
|
16111
|
+
} catch {
|
|
16112
|
+
try {
|
|
16113
|
+
if (Date.now() - (0, import_node_fs19.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
16114
|
+
(0, import_node_fs19.rmSync)(lockPath, { force: true });
|
|
16115
|
+
return take();
|
|
16116
|
+
}
|
|
16117
|
+
} catch {
|
|
16118
|
+
}
|
|
16119
|
+
return null;
|
|
16120
|
+
}
|
|
16121
|
+
}
|
|
16122
|
+
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
16123
|
+
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch the base from", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
|
|
16124
|
+
try {
|
|
16125
|
+
const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
16126
|
+
const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
|
|
16127
|
+
const baseBranch = o.from.startsWith(`${o.remote}/`) ? o.from.slice(o.remote.length + 1) : void 0;
|
|
16128
|
+
if (baseBranch) await execFileP2("git", ["fetch", o.remote, baseBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
|
|
16129
|
+
await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, o.from], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
16130
|
+
const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
|
|
16131
|
+
if (!o.json) console.error(` ${m}`);
|
|
16132
|
+
}));
|
|
16133
|
+
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base: o.from, ...report }, null, 2));
|
|
16134
|
+
console.log(`worktree ready: ${wtPath} (branch ${branch} from ${o.from})`);
|
|
16135
|
+
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
16136
|
+
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
16137
|
+
} catch (e) {
|
|
16138
|
+
fail(`worktree create: ${e.message}`);
|
|
16139
|
+
}
|
|
16140
|
+
});
|
|
16141
|
+
worktree.command("setup [path]").description("provision an existing worktree (install missing deps + copy local-only config); fired automatically by SessionStart").option("--quiet", "no output on success (for the detached SessionStart auto-fire worker)").option("--json", "machine-readable output").action(async (path2, o) => {
|
|
16142
|
+
const root = path2 ?? process.cwd();
|
|
16143
|
+
const release = acquireWorktreeSetupLock(root);
|
|
16144
|
+
if (!release) {
|
|
16145
|
+
if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress \u2014 skipping");
|
|
16146
|
+
return;
|
|
16147
|
+
}
|
|
16148
|
+
try {
|
|
16149
|
+
const report = await provisionWorktree(root, makeProvisionDeps(root, Boolean(o.quiet), (m) => {
|
|
16150
|
+
if (!o.quiet && !o.json) console.error(` ${m}`);
|
|
16151
|
+
}));
|
|
16152
|
+
if (o.json) return console.log(JSON.stringify({ path: root, ...report }, null, 2));
|
|
16153
|
+
if (!o.quiet) {
|
|
16154
|
+
console.log(`worktree provisioned: ${root}`);
|
|
16155
|
+
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
16156
|
+
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
16157
|
+
}
|
|
16158
|
+
} catch (e) {
|
|
16159
|
+
if (o.quiet) return void console.error(`[worktree-setup] ${e.message}`);
|
|
16160
|
+
fail(`worktree setup: ${e.message}`);
|
|
16161
|
+
} finally {
|
|
16162
|
+
release();
|
|
16163
|
+
}
|
|
16164
|
+
});
|
|
14890
16165
|
var kb = program2.command("kb").description("org knowledgebase (read-only)");
|
|
14891
16166
|
kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
|
|
14892
16167
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
@@ -15006,7 +16281,7 @@ function detachPlanSync() {
|
|
|
15006
16281
|
}
|
|
15007
16282
|
}
|
|
15008
16283
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
15009
|
-
const ensureDir = () => (0,
|
|
16284
|
+
const ensureDir = () => (0, import_node_fs19.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
15010
16285
|
return {
|
|
15011
16286
|
apiUrl: cfg.sagaApiUrl,
|
|
15012
16287
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
@@ -15014,24 +16289,24 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
15014
16289
|
project: async () => (await sagaKey(cfg)).project,
|
|
15015
16290
|
readLocal: (slug) => {
|
|
15016
16291
|
try {
|
|
15017
|
-
return (0,
|
|
16292
|
+
return (0, import_node_fs19.readFileSync)(planPath(slug), "utf8");
|
|
15018
16293
|
} catch {
|
|
15019
16294
|
return null;
|
|
15020
16295
|
}
|
|
15021
16296
|
},
|
|
15022
16297
|
writeLocal: (slug, content) => {
|
|
15023
16298
|
ensureDir();
|
|
15024
|
-
(0,
|
|
16299
|
+
(0, import_node_fs19.writeFileSync)(planPath(slug), content, "utf8");
|
|
15025
16300
|
},
|
|
15026
16301
|
removeLocal: (slug) => {
|
|
15027
16302
|
try {
|
|
15028
|
-
(0,
|
|
16303
|
+
(0, import_node_fs19.rmSync)(planPath(slug));
|
|
15029
16304
|
} catch {
|
|
15030
16305
|
}
|
|
15031
16306
|
},
|
|
15032
16307
|
readMetaRaw: () => {
|
|
15033
16308
|
try {
|
|
15034
|
-
return (0,
|
|
16309
|
+
return (0, import_node_fs19.readFileSync)(META_FILE, "utf8");
|
|
15035
16310
|
} catch {
|
|
15036
16311
|
return null;
|
|
15037
16312
|
}
|
|
@@ -15042,7 +16317,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
15042
16317
|
},
|
|
15043
16318
|
readIndexRaw: () => {
|
|
15044
16319
|
try {
|
|
15045
|
-
return (0,
|
|
16320
|
+
return (0, import_node_fs19.readFileSync)(INDEX_FILE, "utf8");
|
|
15046
16321
|
} catch {
|
|
15047
16322
|
return null;
|
|
15048
16323
|
}
|
|
@@ -15053,7 +16328,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
15053
16328
|
},
|
|
15054
16329
|
readQueueRaw: () => {
|
|
15055
16330
|
try {
|
|
15056
|
-
return (0,
|
|
16331
|
+
return (0, import_node_fs19.readFileSync)(QUEUE_FILE, "utf8");
|
|
15057
16332
|
} catch {
|
|
15058
16333
|
return null;
|
|
15059
16334
|
}
|
|
@@ -15113,7 +16388,7 @@ function registerNorthStarCommands(cmd) {
|
|
|
15113
16388
|
let content;
|
|
15114
16389
|
if (o.bodyFile) {
|
|
15115
16390
|
try {
|
|
15116
|
-
content = await resolveTextArg({ file: o.bodyFile }, { readFile:
|
|
16391
|
+
content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises6.readFile, readStdin }, {
|
|
15117
16392
|
value: "inline content",
|
|
15118
16393
|
file: "--body-file",
|
|
15119
16394
|
noun: "plan"
|
|
@@ -15142,7 +16417,16 @@ function registerNorthStarCommands(cmd) {
|
|
|
15142
16417
|
const signals = await gatherRelevanceSignals();
|
|
15143
16418
|
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
|
|
15144
16419
|
}));
|
|
15145
|
-
cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").action((o) => withPlan(o.quiet ?? false, (d) =>
|
|
16420
|
+
cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").option("--wait", "durable confirmation: drain, then report unresolved pushes and exit non-zero if any remain").action((o) => withPlan(o.quiet ?? false, async (d) => {
|
|
16421
|
+
const unresolved = await planSync(d, o);
|
|
16422
|
+
if (!o.wait) return;
|
|
16423
|
+
if (unresolved.length) {
|
|
16424
|
+
for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
|
|
16425
|
+
process.exitCode = 1;
|
|
16426
|
+
} else if (!o.quiet) {
|
|
16427
|
+
d.log("north star: all queued pushes landed");
|
|
16428
|
+
}
|
|
16429
|
+
}));
|
|
15146
16430
|
cmd.command("status").description("show pending/conflicted background pushes and the plan-cache age").option("--json", "machine-readable output").action((o) => withPlan(false, (d) => planStatus(d, o)));
|
|
15147
16431
|
cmd.command("reconcile").description("refresh stale local etags from the server without --force (recovers from an object-store re-stamp)").action(() => withPlan(false, (d) => planReconcile(d)));
|
|
15148
16432
|
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
@@ -15425,6 +16709,17 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
15425
16709
|
return failGraceful(`project get: no registry META for ${target} (unknown or unbootstrapped)`);
|
|
15426
16710
|
}
|
|
15427
16711
|
console.log(JSON.stringify(read.project));
|
|
16712
|
+
if (!o.json) {
|
|
16713
|
+
const m = read.project;
|
|
16714
|
+
const track = resolveReleaseTrack(m);
|
|
16715
|
+
const stages = branchesForTrack(track).join(" -> ");
|
|
16716
|
+
const note = track === "direct" ? " (direct \u2014 no rc; /rcand refuses, /release ships development -> main)" : track === "trunk" ? " (trunk \u2014 main only)" : "";
|
|
16717
|
+
console.error(
|
|
16718
|
+
`${m.name ?? target} \xB7 class ${m.class ?? "?"} \xB7 deploy ${m.deployModel ?? "?"}
|
|
16719
|
+
release track: ${track} \u2014 stages: ${stages}${note}
|
|
16720
|
+
deploys run centrally (tenant-deploy.yml); product repos carry no deploy files. Deploy coords are OIDC-gated \u2014 see \`project resolve\`.`
|
|
16721
|
+
);
|
|
16722
|
+
}
|
|
15428
16723
|
});
|
|
15429
16724
|
project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
|
|
15430
16725
|
const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect the DEPLOY# item via the AWS console / a master DDB read instead.";
|
|
@@ -15622,14 +16917,15 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
15622
16917
|
if (mismatch) process.exitCode = 1;
|
|
15623
16918
|
});
|
|
15624
16919
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
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 -").
|
|
16920
|
+
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 -").option("--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) => {
|
|
15626
16921
|
let args;
|
|
15627
16922
|
let priority;
|
|
15628
16923
|
let body;
|
|
15629
16924
|
let title;
|
|
15630
16925
|
try {
|
|
15631
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
15632
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
16926
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
16927
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
16928
|
+
if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
|
|
15633
16929
|
priority = normalizePriority(o.priority);
|
|
15634
16930
|
args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
|
|
15635
16931
|
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
@@ -15715,8 +17011,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
15715
17011
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
15716
17012
|
const sourceRepo = await resolveRepo(void 0);
|
|
15717
17013
|
try {
|
|
15718
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
15719
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
17014
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
17015
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
15720
17016
|
priority = normalizePriority(o.priority);
|
|
15721
17017
|
args = buildIssueArgs({
|
|
15722
17018
|
type: o.type,
|
|
@@ -15782,8 +17078,8 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
|
|
|
15782
17078
|
try {
|
|
15783
17079
|
const routing = assertVerifyRouting(o.routing);
|
|
15784
17080
|
const lenses = o.lenses.split(",").map((s) => assertGrindLens(s.trim()));
|
|
15785
|
-
const criteria = await (0,
|
|
15786
|
-
const diff = await (0,
|
|
17081
|
+
const criteria = await (0, import_promises6.readFile)(o.criteriaFile, "utf8");
|
|
17082
|
+
const diff = await (0, import_promises6.readFile)(o.diffFile, "utf8");
|
|
15787
17083
|
const plan2 = buildPanelPlan({ routing, lenses, criteria, diff });
|
|
15788
17084
|
console.log(JSON.stringify(plan2));
|
|
15789
17085
|
} catch (e) {
|
|
@@ -15792,7 +17088,7 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
|
|
|
15792
17088
|
});
|
|
15793
17089
|
verify.command("synthesize").description("merge lens JSON array into a PanelReport").option("--input-file <path|->", "JSON lens array (use - for stdin)", "-").action(async (o) => {
|
|
15794
17090
|
try {
|
|
15795
|
-
const raw = o.inputFile === "-" ? await readStdin() : await (0,
|
|
17091
|
+
const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises6.readFile)(o.inputFile, "utf8");
|
|
15796
17092
|
const lenses = parseLensResults(JSON.parse(raw));
|
|
15797
17093
|
console.log(JSON.stringify(synthesizePanelReport(lenses)));
|
|
15798
17094
|
} catch (e) {
|
|
@@ -15865,7 +17161,7 @@ build.command("frontier").description("Evaluate external frontier exhaustion + L
|
|
|
15865
17161
|
iterationCapOverride: opts.iterationCap
|
|
15866
17162
|
};
|
|
15867
17163
|
if (opts.jsonFile) {
|
|
15868
|
-
const raw = await (0,
|
|
17164
|
+
const raw = await (0, import_promises6.readFile)(opts.jsonFile, "utf8");
|
|
15869
17165
|
state = { ...state, ...JSON.parse(raw) };
|
|
15870
17166
|
}
|
|
15871
17167
|
const result = evaluateBuildFrontier(state);
|
|
@@ -15919,8 +17215,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
15919
17215
|
let args;
|
|
15920
17216
|
try {
|
|
15921
17217
|
skill = assertSkillName(o.skill);
|
|
15922
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
15923
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
17218
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
17219
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
15924
17220
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
15925
17221
|
priority = normalizePriority(o.priority);
|
|
15926
17222
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -15971,8 +17267,8 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
15971
17267
|
let body;
|
|
15972
17268
|
let title;
|
|
15973
17269
|
try {
|
|
15974
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
15975
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
17270
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
17271
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
15976
17272
|
} catch (e) {
|
|
15977
17273
|
return fail(`pr create: ${e.message}`);
|
|
15978
17274
|
}
|
|
@@ -15980,9 +17276,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
15980
17276
|
console.log(JSON.stringify(created));
|
|
15981
17277
|
});
|
|
15982
17278
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
15983
|
-
const wfDir = (0,
|
|
15984
|
-
if (!(0,
|
|
15985
|
-
return (0,
|
|
17279
|
+
const wfDir = (0, import_node_path17.join)(cwd, ".github", "workflows");
|
|
17280
|
+
if (!(0, import_node_fs19.existsSync)(wfDir)) return [];
|
|
17281
|
+
return (0, import_node_fs19.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
15986
17282
|
}
|
|
15987
17283
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
15988
17284
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -15997,7 +17293,11 @@ function ciAuditDeps() {
|
|
|
15997
17293
|
return {
|
|
15998
17294
|
client: defaultGitHubClient(),
|
|
15999
17295
|
listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
|
|
16000
|
-
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise))
|
|
17296
|
+
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
|
|
17297
|
+
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
|
|
17298
|
+
// reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
|
|
17299
|
+
// the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
|
|
17300
|
+
readSeedFile: (path2) => (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null
|
|
16001
17301
|
};
|
|
16002
17302
|
}
|
|
16003
17303
|
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) => {
|
|
@@ -16005,14 +17305,28 @@ pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs
|
|
|
16005
17305
|
if (o.json) return printLine(JSON.stringify(result));
|
|
16006
17306
|
printLine(`merge CI policy: ${result.policy} (${result.reason})`);
|
|
16007
17307
|
});
|
|
17308
|
+
async function pollGhPrChecks(prNumber, repoArgs) {
|
|
17309
|
+
let stdout = "";
|
|
17310
|
+
let stderr = "";
|
|
17311
|
+
try {
|
|
17312
|
+
({ stdout, stderr } = await execFileP2("gh", ["pr", "checks", prNumber, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS }));
|
|
17313
|
+
} catch (e) {
|
|
17314
|
+
const err = e;
|
|
17315
|
+
if (err.killed || err.stdout == null && err.stderr == null) throw e;
|
|
17316
|
+
stdout = err.stdout ?? "";
|
|
17317
|
+
stderr = err.stderr ?? "";
|
|
17318
|
+
}
|
|
17319
|
+
const state = parseGhPrChecksResult(stdout, stderr);
|
|
17320
|
+
if (state === "error") {
|
|
17321
|
+
throw new Error(`gh pr checks ${prNumber}: ${(stderr || stdout).trim() || "unknown error"}`);
|
|
17322
|
+
}
|
|
17323
|
+
return state;
|
|
17324
|
+
}
|
|
16008
17325
|
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
17326
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
16010
17327
|
const result = await waitForPrChecks({
|
|
16011
17328
|
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
16012
|
-
pollChecks:
|
|
16013
|
-
const { stdout } = await execFileP2("gh", ["pr", "checks", number, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS });
|
|
16014
|
-
return parseGhPrChecksOutput(stdout);
|
|
16015
|
-
},
|
|
17329
|
+
pollChecks: () => pollGhPrChecks(number, repoArgs),
|
|
16016
17330
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
16017
17331
|
});
|
|
16018
17332
|
if (o.json) printLine(JSON.stringify(result));
|
|
@@ -16036,11 +17350,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
16036
17350
|
resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
16037
17351
|
waitForChecks: (prNumber, repo) => waitForPrChecks({
|
|
16038
17352
|
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
16039
|
-
pollChecks:
|
|
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
|
-
},
|
|
17353
|
+
pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
|
|
16044
17354
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
16045
17355
|
}),
|
|
16046
17356
|
mergeAuto: async (prNumber, repo) => {
|
|
@@ -16097,15 +17407,15 @@ async function createDeferredWorktreeStore() {
|
|
|
16097
17407
|
return {
|
|
16098
17408
|
read: async () => {
|
|
16099
17409
|
try {
|
|
16100
|
-
return parseDeferredWorktreesFile(await (0,
|
|
17410
|
+
return parseDeferredWorktreesFile(await (0, import_promises6.readFile)(registryPath, "utf8"));
|
|
16101
17411
|
} catch {
|
|
16102
17412
|
return [];
|
|
16103
17413
|
}
|
|
16104
17414
|
},
|
|
16105
17415
|
write: async (entries) => {
|
|
16106
17416
|
try {
|
|
16107
|
-
await (0,
|
|
16108
|
-
await (0,
|
|
17417
|
+
await (0, import_promises6.mkdir)((0, import_node_path17.dirname)(registryPath), { recursive: true });
|
|
17418
|
+
await (0, import_promises6.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
16109
17419
|
} catch {
|
|
16110
17420
|
}
|
|
16111
17421
|
}
|
|
@@ -16114,16 +17424,57 @@ async function createDeferredWorktreeStore() {
|
|
|
16114
17424
|
return void 0;
|
|
16115
17425
|
}
|
|
16116
17426
|
}
|
|
17427
|
+
var realWorktreeDirRemover = {
|
|
17428
|
+
probe: (p) => {
|
|
17429
|
+
let st;
|
|
17430
|
+
try {
|
|
17431
|
+
st = (0, import_node_fs19.lstatSync)(p);
|
|
17432
|
+
} catch {
|
|
17433
|
+
return null;
|
|
17434
|
+
}
|
|
17435
|
+
if (st.isSymbolicLink()) return "link";
|
|
17436
|
+
try {
|
|
17437
|
+
(0, import_node_fs19.readlinkSync)(p);
|
|
17438
|
+
return "link";
|
|
17439
|
+
} catch {
|
|
17440
|
+
}
|
|
17441
|
+
return st.isDirectory() ? "dir" : "file";
|
|
17442
|
+
},
|
|
17443
|
+
readdir: (p) => {
|
|
17444
|
+
try {
|
|
17445
|
+
return (0, import_node_fs19.readdirSync)(p);
|
|
17446
|
+
} catch {
|
|
17447
|
+
return [];
|
|
17448
|
+
}
|
|
17449
|
+
},
|
|
17450
|
+
// A directory reparse point (junction / dir-symlink) is detached with rmdir (unlinks the mount point,
|
|
17451
|
+
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
17452
|
+
detachLink: (p) => {
|
|
17453
|
+
try {
|
|
17454
|
+
(0, import_node_fs19.rmdirSync)(p);
|
|
17455
|
+
} catch {
|
|
17456
|
+
(0, import_node_fs19.unlinkSync)(p);
|
|
17457
|
+
}
|
|
17458
|
+
},
|
|
17459
|
+
removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
17460
|
+
};
|
|
17461
|
+
async function resolvePrimaryCheckout(execGit) {
|
|
17462
|
+
try {
|
|
17463
|
+
return parseWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"]))[0]?.path;
|
|
17464
|
+
} catch {
|
|
17465
|
+
return void 0;
|
|
17466
|
+
}
|
|
17467
|
+
}
|
|
16117
17468
|
function worktreeRemoveDeps(execGit) {
|
|
16118
17469
|
return {
|
|
16119
17470
|
git: execGit,
|
|
16120
17471
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
16121
|
-
removeWorktreeDir: async (worktreePath) => (
|
|
17472
|
+
removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
|
|
16122
17473
|
};
|
|
16123
17474
|
}
|
|
16124
17475
|
function teardownWorktreeStage(worktreePath) {
|
|
16125
17476
|
return runWorktreeStageTeardown(worktreePath, {
|
|
16126
|
-
hasStageState: (wt) => (0,
|
|
17477
|
+
hasStageState: (wt) => (0, import_node_fs19.existsSync)(stageStatePath(wt)),
|
|
16127
17478
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
16128
17479
|
listComposeProjects: async () => {
|
|
16129
17480
|
const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -16186,7 +17537,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
16186
17537
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
16187
17538
|
beforeWorktrees,
|
|
16188
17539
|
startingPath,
|
|
16189
|
-
pathExists: (p) => (0,
|
|
17540
|
+
pathExists: (p) => (0, import_node_fs19.existsSync)(p),
|
|
16190
17541
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
16191
17542
|
teardownWorktreeStage,
|
|
16192
17543
|
deferredStore,
|
|
@@ -16358,7 +17709,7 @@ function rawValues(flag) {
|
|
|
16358
17709
|
return out;
|
|
16359
17710
|
}
|
|
16360
17711
|
function printLine(value) {
|
|
16361
|
-
(0,
|
|
17712
|
+
(0, import_node_fs19.writeSync)(1, `${value}
|
|
16362
17713
|
`);
|
|
16363
17714
|
}
|
|
16364
17715
|
function stageKeepAlive() {
|
|
@@ -16375,8 +17726,8 @@ async function resolveStage() {
|
|
|
16375
17726
|
local,
|
|
16376
17727
|
shell: shellFor(),
|
|
16377
17728
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
16378
|
-
hasCompose: (0,
|
|
16379
|
-
hasEnvExample: (0,
|
|
17729
|
+
hasCompose: (0, import_node_fs19.existsSync)((0, import_node_path17.join)(process.cwd(), "docker-compose.yml")),
|
|
17730
|
+
hasEnvExample: (0, import_node_fs19.existsSync)((0, import_node_path17.join)(process.cwd(), ".env.example"))
|
|
16380
17731
|
});
|
|
16381
17732
|
}
|
|
16382
17733
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -16428,9 +17779,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
16428
17779
|
printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
|
|
16429
17780
|
return;
|
|
16430
17781
|
}
|
|
16431
|
-
const path2 = (0,
|
|
17782
|
+
const path2 = (0, import_node_path17.join)(process.cwd(), "infra", "port-ranges.json");
|
|
16432
17783
|
const allocate = async (seed) => {
|
|
16433
|
-
const { stdout } = await execFileP2("node", [(0,
|
|
17784
|
+
const { stdout } = await execFileP2("node", [(0, import_node_path17.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
16434
17785
|
const parsed = JSON.parse(stdout);
|
|
16435
17786
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
16436
17787
|
return parsed.range;
|
|
@@ -16624,7 +17975,7 @@ function trainApplyDeps() {
|
|
|
16624
17975
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
16625
17976
|
announce: (args) => announceRelease({
|
|
16626
17977
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
16627
|
-
readFile: (path2) => (0,
|
|
17978
|
+
readFile: (path2) => (0, import_promises6.readFile)(path2, "utf8")
|
|
16628
17979
|
}, args),
|
|
16629
17980
|
fetchEdgeDomains: async (slug) => {
|
|
16630
17981
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -16697,7 +18048,16 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
16697
18048
|
}
|
|
16698
18049
|
const repo = await resolveRepo();
|
|
16699
18050
|
const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
|
|
16700
|
-
|
|
18051
|
+
let releaseTrack;
|
|
18052
|
+
if (repo) {
|
|
18053
|
+
try {
|
|
18054
|
+
const read = await fetchProjectBySlugChecked(slugOf(repo), registryClientDeps(await loadConfig()));
|
|
18055
|
+
const raw = read.ok ? read.project?.releaseTrack : void 0;
|
|
18056
|
+
if (isReleaseTrack(raw)) releaseTrack = raw;
|
|
18057
|
+
} catch {
|
|
18058
|
+
}
|
|
18059
|
+
}
|
|
18060
|
+
const steps = trainPlan(commandName, { ...targets ?? {}, repo, dev: o.dev, releaseTrack });
|
|
16701
18061
|
console.log(
|
|
16702
18062
|
o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps)
|
|
16703
18063
|
);
|
|
@@ -16801,7 +18161,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
16801
18161
|
const report = await verifyBootstrap(repo, o.class, {
|
|
16802
18162
|
client: defaultGitHubClient(),
|
|
16803
18163
|
projectMeta: meta,
|
|
16804
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
18164
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null,
|
|
16805
18165
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
16806
18166
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
16807
18167
|
requiredGcpApis: (() => {
|
|
@@ -16844,18 +18204,23 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16844
18204
|
return fail(`bootstrap apply: ${e.message}`);
|
|
16845
18205
|
}
|
|
16846
18206
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
16847
|
-
if (!(0,
|
|
16848
|
-
const manifest = loadBootstrapSeeds((0,
|
|
18207
|
+
if (!(0, import_node_fs19.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
18208
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs19.readFileSync)(manifestPath, "utf8"));
|
|
16849
18209
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
16850
18210
|
const slug = parsedRepo.slug;
|
|
16851
18211
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
16852
|
-
const
|
|
18212
|
+
const readFile6 = (p) => (0, import_node_fs19.existsSync)(p) ? (0, import_node_fs19.readFileSync)(p, "utf8") : null;
|
|
16853
18213
|
const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
16854
18214
|
const rawVars = {};
|
|
16855
18215
|
for (const value of rawValues("--var")) {
|
|
16856
18216
|
const eq = value.indexOf("=");
|
|
16857
18217
|
if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
16858
18218
|
}
|
|
18219
|
+
try {
|
|
18220
|
+
const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
18221
|
+
for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
|
|
18222
|
+
} catch {
|
|
18223
|
+
}
|
|
16859
18224
|
const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack);
|
|
16860
18225
|
if (vars.PROJECT_ID) {
|
|
16861
18226
|
try {
|
|
@@ -16891,7 +18256,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16891
18256
|
}
|
|
16892
18257
|
const planned = planSeedAction(resolved, exists);
|
|
16893
18258
|
const isBlock = resolved.source === "managed-block";
|
|
16894
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
18259
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile6) : null;
|
|
16895
18260
|
const action = reconcileSeedAction(planned, content, isBlock);
|
|
16896
18261
|
actions.push(action);
|
|
16897
18262
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
@@ -16908,7 +18273,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16908
18273
|
}
|
|
16909
18274
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
16910
18275
|
if (rulesetSeed) {
|
|
16911
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
18276
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
|
|
16912
18277
|
if (rulesetContent) {
|
|
16913
18278
|
try {
|
|
16914
18279
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -17078,16 +18443,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
17078
18443
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
17079
18444
|
targets = [{ repo: o.repo, class: o.class }];
|
|
17080
18445
|
} else {
|
|
17081
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
18446
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs19.existsSync)("projects.json") ? (0, import_node_fs19.readFileSync)("projects.json", "utf8") : null;
|
|
17082
18447
|
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>");
|
|
17083
|
-
const fanoutJson = (0,
|
|
18448
|
+
const fanoutJson = (0, import_node_fs19.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs19.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
17084
18449
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
17085
18450
|
}
|
|
17086
18451
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
17087
|
-
const fileMatrix = (0,
|
|
18452
|
+
const fileMatrix = (0, import_node_fs19.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs19.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
17088
18453
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
17089
18454
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
17090
|
-
const fileContracts = (0,
|
|
18455
|
+
const fileContracts = (0, import_node_fs19.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs19.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
17091
18456
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
17092
18457
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
17093
18458
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -17096,20 +18461,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
17096
18461
|
var isWin = process.platform === "win32";
|
|
17097
18462
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
17098
18463
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
17099
|
-
return (0,
|
|
18464
|
+
return (0, import_node_path17.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
17100
18465
|
};
|
|
17101
18466
|
function readInstalledPlugins() {
|
|
17102
18467
|
try {
|
|
17103
|
-
return JSON.parse((0,
|
|
18468
|
+
return JSON.parse((0, import_node_fs19.readFileSync)(installedPluginsPath(), "utf8"));
|
|
17104
18469
|
} catch {
|
|
17105
18470
|
return null;
|
|
17106
18471
|
}
|
|
17107
18472
|
}
|
|
17108
18473
|
function installedPluginSources() {
|
|
17109
18474
|
return ["claude", "codex"].map((surface) => {
|
|
17110
|
-
const recordPath = (0,
|
|
18475
|
+
const recordPath = (0, import_node_path17.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
17111
18476
|
try {
|
|
17112
|
-
return { surface, installed: JSON.parse((0,
|
|
18477
|
+
return { surface, installed: JSON.parse((0, import_node_fs19.readFileSync)(recordPath, "utf8")), recordPath };
|
|
17113
18478
|
} catch {
|
|
17114
18479
|
return { surface, installed: null, recordPath };
|
|
17115
18480
|
}
|
|
@@ -17117,7 +18482,7 @@ function installedPluginSources() {
|
|
|
17117
18482
|
}
|
|
17118
18483
|
function readClaudeSettings() {
|
|
17119
18484
|
try {
|
|
17120
|
-
return JSON.parse((0,
|
|
18485
|
+
return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
17121
18486
|
} catch {
|
|
17122
18487
|
return null;
|
|
17123
18488
|
}
|
|
@@ -17139,7 +18504,7 @@ function writeProjectInstallRecord(record) {
|
|
|
17139
18504
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
17140
18505
|
list.push(record);
|
|
17141
18506
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
17142
|
-
(0,
|
|
18507
|
+
(0, import_node_fs19.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
17143
18508
|
`, "utf8");
|
|
17144
18509
|
return true;
|
|
17145
18510
|
} catch {
|
|
@@ -17152,9 +18517,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
17152
18517
|
if (!file) return false;
|
|
17153
18518
|
if (!file.plugins) file.plugins = {};
|
|
17154
18519
|
const path2 = installedPluginsPath();
|
|
17155
|
-
(0,
|
|
18520
|
+
(0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
|
|
17156
18521
|
file.plugins[pluginId] = records;
|
|
17157
|
-
(0,
|
|
18522
|
+
(0, import_node_fs19.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
17158
18523
|
`, "utf8");
|
|
17159
18524
|
return true;
|
|
17160
18525
|
} catch {
|
|
@@ -17162,35 +18527,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
17162
18527
|
}
|
|
17163
18528
|
}
|
|
17164
18529
|
function cursorPluginCacheRoot() {
|
|
17165
|
-
return (0,
|
|
18530
|
+
return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
|
|
17166
18531
|
}
|
|
17167
18532
|
function cursorPluginCachePinSnapshots() {
|
|
17168
18533
|
const root = cursorPluginCacheRoot();
|
|
17169
18534
|
try {
|
|
17170
|
-
return (0,
|
|
17171
|
-
const path2 = (0,
|
|
17172
|
-
const pluginJson = (0,
|
|
17173
|
-
const hooksJson = (0,
|
|
17174
|
-
const cliBundle = (0,
|
|
18535
|
+
return (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
18536
|
+
const path2 = (0, import_node_path17.join)(root, entry.name);
|
|
18537
|
+
const pluginJson = (0, import_node_path17.join)(path2, ".cursor-plugin", "plugin.json");
|
|
18538
|
+
const hooksJson = (0, import_node_path17.join)(path2, "hooks", "hooks.json");
|
|
18539
|
+
const cliBundle = (0, import_node_path17.join)(path2, "cli", "dist", "index.cjs");
|
|
17175
18540
|
let version;
|
|
17176
18541
|
try {
|
|
17177
|
-
const raw = JSON.parse((0,
|
|
18542
|
+
const raw = JSON.parse((0, import_node_fs19.readFileSync)(pluginJson, "utf8"));
|
|
17178
18543
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
17179
18544
|
} catch {
|
|
17180
18545
|
version = void 0;
|
|
17181
18546
|
}
|
|
17182
18547
|
let isEmpty = true;
|
|
17183
18548
|
try {
|
|
17184
|
-
isEmpty = (0,
|
|
18549
|
+
isEmpty = (0, import_node_fs19.readdirSync)(path2).length === 0;
|
|
17185
18550
|
} catch {
|
|
17186
18551
|
isEmpty = true;
|
|
17187
18552
|
}
|
|
17188
18553
|
return {
|
|
17189
18554
|
name: entry.name,
|
|
17190
18555
|
path: path2,
|
|
17191
|
-
hasPluginJson: (0,
|
|
17192
|
-
hasHooksJson: (0,
|
|
17193
|
-
hasCliBundle: (0,
|
|
18556
|
+
hasPluginJson: (0, import_node_fs19.existsSync)(pluginJson),
|
|
18557
|
+
hasHooksJson: (0, import_node_fs19.existsSync)(hooksJson),
|
|
18558
|
+
hasCliBundle: (0, import_node_fs19.existsSync)(cliBundle),
|
|
17194
18559
|
isEmpty,
|
|
17195
18560
|
version
|
|
17196
18561
|
};
|
|
@@ -17200,19 +18565,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
17200
18565
|
}
|
|
17201
18566
|
}
|
|
17202
18567
|
function hubCheckoutForCursorSeed() {
|
|
17203
|
-
const manifest = (0,
|
|
17204
|
-
return (0,
|
|
18568
|
+
const manifest = (0, import_node_path17.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
18569
|
+
return (0, import_node_fs19.existsSync)(manifest) ? process.cwd() : void 0;
|
|
17205
18570
|
}
|
|
17206
18571
|
function mmiPluginCacheRootSnapshots() {
|
|
17207
18572
|
const roots = [
|
|
17208
|
-
{ surface: "claude", root: (0,
|
|
17209
|
-
{ surface: "codex", root: (0,
|
|
18573
|
+
{ surface: "claude", root: (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
18574
|
+
{ surface: "codex", root: (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
17210
18575
|
];
|
|
17211
18576
|
return roots.flatMap(({ surface, root }) => {
|
|
17212
18577
|
try {
|
|
17213
|
-
const entries = (0,
|
|
18578
|
+
const entries = (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
17214
18579
|
name: entry.name,
|
|
17215
|
-
path: (0,
|
|
18580
|
+
path: (0, import_node_path17.join)(root, entry.name),
|
|
17216
18581
|
isDirectory: entry.isDirectory()
|
|
17217
18582
|
}));
|
|
17218
18583
|
return [{ surface, root, entries }];
|
|
@@ -17223,7 +18588,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
17223
18588
|
}
|
|
17224
18589
|
function hasNestedMmiChild(versionDir) {
|
|
17225
18590
|
try {
|
|
17226
|
-
return (0,
|
|
18591
|
+
return (0, import_node_fs19.statSync)((0, import_node_path17.join)(versionDir, "mmi")).isDirectory();
|
|
17227
18592
|
} catch {
|
|
17228
18593
|
return false;
|
|
17229
18594
|
}
|
|
@@ -17234,26 +18599,28 @@ function nestedPluginTreeSnapshot() {
|
|
|
17234
18599
|
);
|
|
17235
18600
|
}
|
|
17236
18601
|
function uniqueQuarantineTarget(path2) {
|
|
17237
|
-
if (!(0,
|
|
18602
|
+
if (!(0, import_node_fs19.existsSync)(path2)) return path2;
|
|
17238
18603
|
for (let i = 1; i < 100; i += 1) {
|
|
17239
18604
|
const candidate = `${path2}-${i}`;
|
|
17240
|
-
if (!(0,
|
|
18605
|
+
if (!(0, import_node_fs19.existsSync)(candidate)) return candidate;
|
|
17241
18606
|
}
|
|
17242
18607
|
return `${path2}-${Date.now()}`;
|
|
17243
18608
|
}
|
|
17244
18609
|
function quarantinePluginCacheDirs(plan2) {
|
|
17245
18610
|
let moved = 0;
|
|
18611
|
+
const failed = [];
|
|
17246
18612
|
for (const move of plan2) {
|
|
17247
18613
|
try {
|
|
17248
|
-
if (!(0,
|
|
18614
|
+
if (!(0, import_node_fs19.existsSync)(move.from)) continue;
|
|
17249
18615
|
const target = uniqueQuarantineTarget(move.to);
|
|
17250
|
-
(0,
|
|
17251
|
-
(0,
|
|
18616
|
+
(0, import_node_fs19.mkdirSync)((0, import_node_path17.dirname)(target), { recursive: true });
|
|
18617
|
+
(0, import_node_fs19.renameSync)(move.from, target);
|
|
17252
18618
|
moved += 1;
|
|
17253
18619
|
} catch {
|
|
18620
|
+
failed.push(move);
|
|
17254
18621
|
}
|
|
17255
18622
|
}
|
|
17256
|
-
return moved;
|
|
18623
|
+
return { moved, failed };
|
|
17257
18624
|
}
|
|
17258
18625
|
async function robocopyMirrorEmpty(emptyDir, target) {
|
|
17259
18626
|
try {
|
|
@@ -17266,23 +18633,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
17266
18633
|
}
|
|
17267
18634
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
17268
18635
|
try {
|
|
17269
|
-
if (!(0,
|
|
18636
|
+
if (!(0, import_node_fs19.existsSync)(targetPath)) return true;
|
|
17270
18637
|
if (isWin) {
|
|
17271
|
-
const emptyDir = (0,
|
|
17272
|
-
(0,
|
|
18638
|
+
const emptyDir = (0, import_node_path17.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
18639
|
+
(0, import_node_fs19.mkdirSync)(emptyDir, { recursive: true });
|
|
17273
18640
|
try {
|
|
17274
18641
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
17275
|
-
(0,
|
|
18642
|
+
(0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
|
|
17276
18643
|
} finally {
|
|
17277
18644
|
try {
|
|
17278
|
-
(0,
|
|
18645
|
+
(0, import_node_fs19.rmSync)(emptyDir, { recursive: true, force: true });
|
|
17279
18646
|
} catch {
|
|
17280
18647
|
}
|
|
17281
18648
|
}
|
|
17282
|
-
return !(0,
|
|
18649
|
+
return !(0, import_node_fs19.existsSync)(targetPath);
|
|
17283
18650
|
}
|
|
17284
|
-
(0,
|
|
17285
|
-
return !(0,
|
|
18651
|
+
(0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
|
|
18652
|
+
return !(0, import_node_fs19.existsSync)(targetPath);
|
|
17286
18653
|
} catch {
|
|
17287
18654
|
return false;
|
|
17288
18655
|
}
|
|
@@ -17295,22 +18662,22 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
17295
18662
|
}
|
|
17296
18663
|
return true;
|
|
17297
18664
|
}
|
|
17298
|
-
var gitignorePath = () => (0,
|
|
18665
|
+
var gitignorePath = () => (0, import_node_path17.join)(process.cwd(), ".gitignore");
|
|
17299
18666
|
function readTextFile(path2) {
|
|
17300
18667
|
try {
|
|
17301
|
-
if (!(0,
|
|
17302
|
-
return (0,
|
|
18668
|
+
if (!(0, import_node_fs19.existsSync)(path2)) return null;
|
|
18669
|
+
return (0, import_node_fs19.readFileSync)(path2, "utf8");
|
|
17303
18670
|
} catch {
|
|
17304
18671
|
return null;
|
|
17305
18672
|
}
|
|
17306
18673
|
}
|
|
17307
18674
|
function playwrightMcpConfigSnapshots() {
|
|
17308
18675
|
const cwd = process.cwd();
|
|
17309
|
-
const home = (0,
|
|
18676
|
+
const home = (0, import_node_os6.homedir)();
|
|
17310
18677
|
const candidates = [
|
|
17311
|
-
(0,
|
|
17312
|
-
(0,
|
|
17313
|
-
(0,
|
|
18678
|
+
(0, import_node_path17.join)(cwd, ".cursor", "mcp.json"),
|
|
18679
|
+
(0, import_node_path17.join)(home, ".cursor", "mcp.json"),
|
|
18680
|
+
(0, import_node_path17.join)(home, ".codex", "config.toml")
|
|
17314
18681
|
];
|
|
17315
18682
|
const out = [];
|
|
17316
18683
|
for (const path2 of candidates) {
|
|
@@ -17323,7 +18690,7 @@ function strayBrowserArtifactPaths() {
|
|
|
17323
18690
|
const cwd = process.cwd();
|
|
17324
18691
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
17325
18692
|
try {
|
|
17326
|
-
return (0,
|
|
18693
|
+
return (0, import_node_fs19.existsSync)((0, import_node_path17.join)(cwd, rel));
|
|
17327
18694
|
} catch {
|
|
17328
18695
|
return false;
|
|
17329
18696
|
}
|
|
@@ -17331,14 +18698,14 @@ function strayBrowserArtifactPaths() {
|
|
|
17331
18698
|
}
|
|
17332
18699
|
function readGitignore() {
|
|
17333
18700
|
try {
|
|
17334
|
-
return (0,
|
|
18701
|
+
return (0, import_node_fs19.readFileSync)(gitignorePath(), "utf8");
|
|
17335
18702
|
} catch {
|
|
17336
18703
|
return null;
|
|
17337
18704
|
}
|
|
17338
18705
|
}
|
|
17339
18706
|
function writeGitignore(content) {
|
|
17340
18707
|
try {
|
|
17341
|
-
(0,
|
|
18708
|
+
(0, import_node_fs19.writeFileSync)(gitignorePath(), content, "utf8");
|
|
17342
18709
|
return true;
|
|
17343
18710
|
} catch {
|
|
17344
18711
|
return false;
|
|
@@ -17377,7 +18744,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17377
18744
|
let onPath = pathProbe;
|
|
17378
18745
|
if (!onPath) {
|
|
17379
18746
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
17380
|
-
if (root && (0,
|
|
18747
|
+
if (root && (0, import_node_fs19.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
17381
18748
|
}
|
|
17382
18749
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
17383
18750
|
const surface = detectSurface(process.env);
|
|
@@ -17494,7 +18861,8 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17494
18861
|
installedVersions: installedPluginVersions(installed)
|
|
17495
18862
|
});
|
|
17496
18863
|
if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && repairLocal) {
|
|
17497
|
-
const moved = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
18864
|
+
const { moved, failed } = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
18865
|
+
const attempted = moved + failed.length;
|
|
17498
18866
|
if (moved > 0) {
|
|
17499
18867
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
17500
18868
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
@@ -17505,8 +18873,12 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17505
18873
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
17506
18874
|
roots: mmiPluginCacheRootSnapshots(),
|
|
17507
18875
|
activeVersion: resolveClientVersion(),
|
|
17508
|
-
releasedVersion
|
|
18876
|
+
releasedVersion,
|
|
18877
|
+
installedVersions: installedPluginVersions(installed)
|
|
17509
18878
|
}),
|
|
18879
|
+
attemptedCount: attempted,
|
|
18880
|
+
failedCount: failed.length,
|
|
18881
|
+
...failed.length > 0 ? { failedMoves: failed } : {},
|
|
17510
18882
|
...moved > 0 ? { cleanedCount: moved } : {}
|
|
17511
18883
|
};
|
|
17512
18884
|
}
|
|
@@ -17544,7 +18916,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17544
18916
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
17545
18917
|
surface,
|
|
17546
18918
|
cacheRoot: cursorCacheRoot,
|
|
17547
|
-
cacheRootExists: (0,
|
|
18919
|
+
cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
|
|
17548
18920
|
pins: cursorPins,
|
|
17549
18921
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17550
18922
|
releasedVersion
|
|
@@ -17555,7 +18927,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17555
18927
|
releasedVersion,
|
|
17556
18928
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17557
18929
|
execFileP: execFileP2,
|
|
17558
|
-
mkdtemp: (prefix) => (0,
|
|
18930
|
+
mkdtemp: (prefix) => (0, import_promises6.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
17559
18931
|
log: (m) => io.err(m)
|
|
17560
18932
|
});
|
|
17561
18933
|
if (seeded) {
|
|
@@ -17564,7 +18936,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17564
18936
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
17565
18937
|
surface,
|
|
17566
18938
|
cacheRoot: cursorCacheRoot,
|
|
17567
|
-
cacheRootExists: (0,
|
|
18939
|
+
cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
|
|
17568
18940
|
pins: cursorPins,
|
|
17569
18941
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17570
18942
|
releasedVersion
|
|
@@ -17636,6 +19008,10 @@ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi
|
|
|
17636
19008
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
17637
19009
|
));
|
|
17638
19010
|
program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
|
|
19011
|
+
if (isInsideRepoSubdir(process.cwd())) {
|
|
19012
|
+
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
19013
|
+
return;
|
|
19014
|
+
}
|
|
17639
19015
|
try {
|
|
17640
19016
|
const hook = parseHookInput(await readStdin());
|
|
17641
19017
|
if (hook.session_id) persistSession(hook.session_id);
|
|
@@ -17647,6 +19023,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
17647
19023
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
17648
19024
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
17649
19025
|
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
19026
|
+
handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
|
|
17650
19027
|
// honcho profile (#1162): inject the behavioral-memory prior (peer card). Bounded + fail-soft +
|
|
17651
19028
|
// silent when off/empty, so it never delays or noises the session banner.
|
|
17652
19029
|
honchoContext: (io) => runHonchoContext({ quiet: true, banner: true }, io),
|
|
@@ -17680,6 +19057,12 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
17680
19057
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
17681
19058
|
consoleIo.log(northstarPointer(northstarInjected));
|
|
17682
19059
|
for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
|
|
19060
|
+
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
19061
|
+
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
19062
|
+
if (worktreeBanner) {
|
|
19063
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19064
|
+
consoleIo.log(worktreeBanner);
|
|
19065
|
+
}
|
|
17683
19066
|
});
|
|
17684
19067
|
installProcessBackstop();
|
|
17685
19068
|
program2.parseAsync().catch((e) => failGraceful(e.message));
|