@mutmutco/cli 2.35.0 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/main.cjs +1008 -203
- package/dist/saga.cjs +1 -0
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3392,7 +3392,7 @@ var program = new Command();
|
|
|
3392
3392
|
|
|
3393
3393
|
// src/index.ts
|
|
3394
3394
|
var import_promises5 = require("node:fs/promises");
|
|
3395
|
-
var
|
|
3395
|
+
var import_node_fs18 = require("node:fs");
|
|
3396
3396
|
|
|
3397
3397
|
// src/rules-sync.ts
|
|
3398
3398
|
function normalizeEol(s) {
|
|
@@ -4447,6 +4447,7 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
4447
4447
|
next: o.next,
|
|
4448
4448
|
decision: o.decision,
|
|
4449
4449
|
queueOp,
|
|
4450
|
+
handoffClose: o.handoffClose,
|
|
4450
4451
|
state,
|
|
4451
4452
|
source,
|
|
4452
4453
|
evidence: Object.keys(ev).length ? ev : void 0,
|
|
@@ -5080,6 +5081,260 @@ function registerSagaCommands(program3) {
|
|
|
5080
5081
|
});
|
|
5081
5082
|
}
|
|
5082
5083
|
|
|
5084
|
+
// src/handoff-commands.ts
|
|
5085
|
+
var import_node_crypto4 = require("node:crypto");
|
|
5086
|
+
|
|
5087
|
+
// src/handoff.ts
|
|
5088
|
+
var HANDOFF_PREFIX = "handoff:";
|
|
5089
|
+
function clean(value) {
|
|
5090
|
+
return value?.trim() ?? "";
|
|
5091
|
+
}
|
|
5092
|
+
function normalizeHandoffKey(value) {
|
|
5093
|
+
const key = clean(value);
|
|
5094
|
+
if (!key) throw new Error("handoff key is required");
|
|
5095
|
+
return key;
|
|
5096
|
+
}
|
|
5097
|
+
function serializeHandoff(record) {
|
|
5098
|
+
return `${HANDOFF_PREFIX}${JSON.stringify(record)}`;
|
|
5099
|
+
}
|
|
5100
|
+
function parseHandoffText(text) {
|
|
5101
|
+
if (!text.startsWith(HANDOFF_PREFIX)) return null;
|
|
5102
|
+
try {
|
|
5103
|
+
const raw = JSON.parse(text.slice(HANDOFF_PREFIX.length));
|
|
5104
|
+
const state = raw.state;
|
|
5105
|
+
const key = clean(raw.key);
|
|
5106
|
+
const northStarSlug = clean(raw.northStarSlug);
|
|
5107
|
+
const summary = clean(raw.summary);
|
|
5108
|
+
const sourceSessionId = clean(raw.sourceSessionId);
|
|
5109
|
+
const createdAt = clean(raw.createdAt);
|
|
5110
|
+
if (state !== "open" && state !== "claimed" && state !== "cancelled") return null;
|
|
5111
|
+
if (!key || !northStarSlug || !summary || !sourceSessionId || !createdAt) return null;
|
|
5112
|
+
return {
|
|
5113
|
+
state,
|
|
5114
|
+
key,
|
|
5115
|
+
northStarSlug,
|
|
5116
|
+
summary,
|
|
5117
|
+
sourceSessionId,
|
|
5118
|
+
createdAt,
|
|
5119
|
+
claimedBySessionId: clean(raw.claimedBySessionId) || void 0,
|
|
5120
|
+
closedAt: clean(raw.closedAt) || void 0
|
|
5121
|
+
};
|
|
5122
|
+
} catch {
|
|
5123
|
+
return null;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
function listHandoffs(head, opts = {}) {
|
|
5127
|
+
const parsed = (head.queued ?? []).map((item, index) => ({ done: item.done, index, record: parseHandoffText(item.text) })).filter((x) => x.record !== null);
|
|
5128
|
+
return parsed.filter((x) => opts.includeClosed || x.record.state === "open" && !x.done).map((x) => ({ index: x.index, done: x.done, record: x.record }));
|
|
5129
|
+
}
|
|
5130
|
+
function findOpenHandoff(head, key) {
|
|
5131
|
+
const normalized = normalizeHandoffKey(key).toLowerCase();
|
|
5132
|
+
return listHandoffs(head).find((item) => item.record.key.toLowerCase() === normalized || item.record.northStarSlug.toLowerCase() === normalized);
|
|
5133
|
+
}
|
|
5134
|
+
function planOpenHandoff(input) {
|
|
5135
|
+
const key = normalizeHandoffKey(input.key);
|
|
5136
|
+
const northStarSlug = clean(input.northStarSlug);
|
|
5137
|
+
const summary = clean(input.summary);
|
|
5138
|
+
const sourceSessionId = clean(input.sourceSessionId);
|
|
5139
|
+
if (!northStarSlug) throw new Error("north star slug is required");
|
|
5140
|
+
if (!summary) throw new Error("handoff summary is required");
|
|
5141
|
+
if (!sourceSessionId) throw new Error("source session id is required");
|
|
5142
|
+
return {
|
|
5143
|
+
state: "open",
|
|
5144
|
+
key,
|
|
5145
|
+
northStarSlug,
|
|
5146
|
+
summary,
|
|
5147
|
+
sourceSessionId,
|
|
5148
|
+
createdAt: input.createdAt
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
function closeHandoff(record, state, at, claimedBySessionId) {
|
|
5152
|
+
return {
|
|
5153
|
+
...record,
|
|
5154
|
+
state,
|
|
5155
|
+
closedAt: at,
|
|
5156
|
+
claimedBySessionId: claimedBySessionId || record.claimedBySessionId
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
5159
|
+
function formatHandoffLine(item) {
|
|
5160
|
+
const r = item.record;
|
|
5161
|
+
const suffix = r.state === "claimed" && r.claimedBySessionId ? ` -> ${r.claimedBySessionId}` : "";
|
|
5162
|
+
return `${r.key} [${r.state}] northstar:${r.northStarSlug} source:${r.sourceSessionId}${suffix} - ${r.summary}`;
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
// src/handoff-commands.ts
|
|
5166
|
+
var FOREGROUND_FETCH = { attempts: 2, timeoutMs: 8e3 };
|
|
5167
|
+
var SESSION_START_FETCH = { attempts: 1, timeoutMs: 3e3 };
|
|
5168
|
+
async function fetchState(url, qs, retry = FOREGROUND_FETCH) {
|
|
5169
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/state?${qs}`, { headers: await hubHeaders() }, retry);
|
|
5170
|
+
if (!res.ok) return null;
|
|
5171
|
+
const body = await res.json();
|
|
5172
|
+
if ("state" in body) return body;
|
|
5173
|
+
return { key: null, state: { head: body.head } };
|
|
5174
|
+
}
|
|
5175
|
+
async function fetchScopedSessions(url, project2, branch, retry = FOREGROUND_FETCH) {
|
|
5176
|
+
const qs = new URLSearchParams({ project: project2, branch });
|
|
5177
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/sessions?${qs}`, { headers: await hubHeaders() }, retry);
|
|
5178
|
+
if (!res.ok) return [];
|
|
5179
|
+
const body = await res.json();
|
|
5180
|
+
return body.sessions ?? [];
|
|
5181
|
+
}
|
|
5182
|
+
async function fetchSessionHead(url, key, retry = FOREGROUND_FETCH) {
|
|
5183
|
+
const qs = new URLSearchParams(key);
|
|
5184
|
+
const got = await fetchState(url, qs, retry);
|
|
5185
|
+
return got?.state?.head ?? null;
|
|
5186
|
+
}
|
|
5187
|
+
async function fetchCurrentState() {
|
|
5188
|
+
const cfg = await loadConfig();
|
|
5189
|
+
if (!cfg.sagaApiUrl) return null;
|
|
5190
|
+
const key = await sagaKey(cfg);
|
|
5191
|
+
const got = await fetchState(cfg.sagaApiUrl, new URLSearchParams(key));
|
|
5192
|
+
return got ? { key, head: got.state?.head ?? {} } : null;
|
|
5193
|
+
}
|
|
5194
|
+
async function collectScopedHandoffs(opts = {}) {
|
|
5195
|
+
const cfg = await loadConfig();
|
|
5196
|
+
if (!cfg.sagaApiUrl) return { handoffs: [], sessions: [] };
|
|
5197
|
+
const key = await sagaKey(cfg);
|
|
5198
|
+
const retry = opts.retry ?? FOREGROUND_FETCH;
|
|
5199
|
+
const sessions = await fetchScopedSessions(cfg.sagaApiUrl, key.project, key.branch, retry);
|
|
5200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5201
|
+
const handoffs = [];
|
|
5202
|
+
for (const session of sessions) {
|
|
5203
|
+
const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
|
|
5204
|
+
if (!head) continue;
|
|
5205
|
+
for (const item of listHandoffs(head, { includeClosed: opts.includeClosed })) {
|
|
5206
|
+
const dedupe = `${item.record.sourceSessionId}:${item.record.key}:${item.record.state}`;
|
|
5207
|
+
if (seen.has(dedupe)) continue;
|
|
5208
|
+
seen.add(dedupe);
|
|
5209
|
+
handoffs.push(item);
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
return { handoffs, sessions };
|
|
5213
|
+
}
|
|
5214
|
+
async function locateOpenHandoff(key, retry = FOREGROUND_FETCH) {
|
|
5215
|
+
const cfg = await loadConfig();
|
|
5216
|
+
if (!cfg.sagaApiUrl) return null;
|
|
5217
|
+
const scopeKey = await sagaKey(cfg);
|
|
5218
|
+
const sessions = await fetchScopedSessions(cfg.sagaApiUrl, scopeKey.project, scopeKey.branch, retry);
|
|
5219
|
+
for (const session of sessions) {
|
|
5220
|
+
const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
|
|
5221
|
+
if (!head) continue;
|
|
5222
|
+
const item = findOpenHandoff(head, key);
|
|
5223
|
+
if (item) {
|
|
5224
|
+
return {
|
|
5225
|
+
key: { project: session.project, branch: session.branch, sessionId: session.sessionId },
|
|
5226
|
+
item
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
return null;
|
|
5231
|
+
}
|
|
5232
|
+
async function locateClaimedHandoff(key, claimedBySessionId, retry = FOREGROUND_FETCH) {
|
|
5233
|
+
const { handoffs } = await collectScopedHandoffs({ includeClosed: true, retry });
|
|
5234
|
+
const normalized = key.toLowerCase();
|
|
5235
|
+
return handoffs.find((item) => {
|
|
5236
|
+
const r = item.record;
|
|
5237
|
+
return r.state === "claimed" && r.claimedBySessionId === claimedBySessionId && (r.key.toLowerCase() === normalized || r.northStarSlug.toLowerCase() === normalized);
|
|
5238
|
+
}) ?? null;
|
|
5239
|
+
}
|
|
5240
|
+
async function postKeyedNote(key, summary, options) {
|
|
5241
|
+
const cfg = await loadConfig();
|
|
5242
|
+
if (!cfg.sagaApiUrl) fail("handoff: Hub API URL not configured");
|
|
5243
|
+
const sha = await gitOut(["rev-parse", "--short", "HEAD"]);
|
|
5244
|
+
const capture = buildNoteCapture(summary, options, (0, import_node_crypto4.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
5245
|
+
const result = await postCaptureOnce(cfg.sagaApiUrl, { ...capture, ...key });
|
|
5246
|
+
if (!result.ok) fail(`handoff: write failed${result.status ? ` (HTTP ${result.status})` : ""}${result.message ? `: ${result.message}` : ""}`);
|
|
5247
|
+
}
|
|
5248
|
+
function deriveOpenFields(summary, opts, head, key) {
|
|
5249
|
+
const northStarSlug = opts.northStarSlug ?? head.anchor?.slug;
|
|
5250
|
+
const handoffKey = opts.key ?? northStarSlug;
|
|
5251
|
+
const text = summary?.trim() || head.next?.trim() || head.anchor?.intent?.trim();
|
|
5252
|
+
return planOpenHandoff({
|
|
5253
|
+
key: handoffKey ?? "",
|
|
5254
|
+
northStarSlug: northStarSlug ?? "",
|
|
5255
|
+
summary: text ?? "",
|
|
5256
|
+
sourceSessionId: key.sessionId,
|
|
5257
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
async function runHandoffOpen(summary, opts, io = consoleIo) {
|
|
5261
|
+
const current = await fetchCurrentState();
|
|
5262
|
+
if (!current) return fail("handoff open: saga state unavailable");
|
|
5263
|
+
let record;
|
|
5264
|
+
try {
|
|
5265
|
+
record = deriveOpenFields(summary, opts, current.head, current.key);
|
|
5266
|
+
} catch (e) {
|
|
5267
|
+
return fail(`handoff open: ${e.message}`);
|
|
5268
|
+
}
|
|
5269
|
+
await postKeyedNote(current.key, `handoff opened ${record.key}`, { queueAdd: serializeHandoff(record), anchorSlug: record.northStarSlug });
|
|
5270
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: record }));
|
|
5271
|
+
io.log(`handoff open: ${formatHandoffLine({ index: -1, done: false, record })}`);
|
|
5272
|
+
}
|
|
5273
|
+
async function runHandoffList(opts, io = consoleIo) {
|
|
5274
|
+
const { handoffs } = await collectScopedHandoffs({ includeClosed: opts.all });
|
|
5275
|
+
if (opts.json) {
|
|
5276
|
+
io.log(JSON.stringify({ handoffs }, null, 2));
|
|
5277
|
+
} else if (!handoffs.length) {
|
|
5278
|
+
io.log("handoff list: none");
|
|
5279
|
+
} else {
|
|
5280
|
+
for (const h of handoffs) io.log(formatHandoffLine(h));
|
|
5281
|
+
}
|
|
5282
|
+
return handoffs;
|
|
5283
|
+
}
|
|
5284
|
+
async function runHandoffOffer(io = consoleIo, opts = {}) {
|
|
5285
|
+
const retry = opts.fast ? SESSION_START_FETCH : FOREGROUND_FETCH;
|
|
5286
|
+
const { handoffs } = await collectScopedHandoffs({ retry });
|
|
5287
|
+
if (!handoffs.length) return;
|
|
5288
|
+
io.log("Open handoffs:");
|
|
5289
|
+
for (const h of handoffs) io.log(`- ${formatHandoffLine(h)}`);
|
|
5290
|
+
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.");
|
|
5291
|
+
}
|
|
5292
|
+
async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
|
|
5293
|
+
const current = await sagaKey(await loadConfig(), resolveSessionId());
|
|
5294
|
+
const located = await locateOpenHandoff(key);
|
|
5295
|
+
if (!located) {
|
|
5296
|
+
if (state === "claimed") {
|
|
5297
|
+
const prior = await locateClaimedHandoff(key, current.sessionId);
|
|
5298
|
+
if (prior) {
|
|
5299
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: prior.record, idempotent: true }));
|
|
5300
|
+
return io.log(`handoff claimed: ${formatHandoffLine(prior)} (already accepted)`);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
return fail(`handoff ${state}: no open handoff matching ${key}`);
|
|
5304
|
+
}
|
|
5305
|
+
const closed = closeHandoff(located.item.record, state, (/* @__PURE__ */ new Date()).toISOString(), state === "claimed" ? current.sessionId : void 0);
|
|
5306
|
+
await postKeyedNote(located.key, `handoff ${state} ${located.item.record.key}`, {
|
|
5307
|
+
handoffClose: { index: located.item.index, closedText: serializeHandoff(closed) }
|
|
5308
|
+
});
|
|
5309
|
+
if (state === "claimed") {
|
|
5310
|
+
await postKeyedNote(current, `handoff accepted ${located.item.record.key}`, {
|
|
5311
|
+
anchor: located.item.record.summary,
|
|
5312
|
+
anchorSlug: located.item.record.northStarSlug,
|
|
5313
|
+
anchorForce: true,
|
|
5314
|
+
decision: `accepted handoff ${located.item.record.key} from session ${located.item.record.sourceSessionId}; northstar ${located.item.record.northStarSlug}`,
|
|
5315
|
+
verified: true
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
5318
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: closed }));
|
|
5319
|
+
io.log(`handoff ${state}: ${formatHandoffLine({ index: located.item.index, done: true, record: closed })}`);
|
|
5320
|
+
}
|
|
5321
|
+
async function runHandoffDecline(key, opts = {}, io = consoleIo) {
|
|
5322
|
+
const located = await locateOpenHandoff(key);
|
|
5323
|
+
if (!located) return fail(`handoff decline: no open handoff matching ${key}`);
|
|
5324
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, unchanged: true, handoff: located.item.record }));
|
|
5325
|
+
io.log(`handoff decline: left open for re-offer \u2014 ${formatHandoffLine(located.item)}`);
|
|
5326
|
+
}
|
|
5327
|
+
function registerHandoffCommands(program3) {
|
|
5328
|
+
const handoff = program3.command("handoff").description("explicit saga + North Star handoff lifecycle");
|
|
5329
|
+
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("--json", "machine-readable output").action((summary, opts) => runHandoffOpen(summary, opts));
|
|
5330
|
+
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) => {
|
|
5331
|
+
await runHandoffList(opts);
|
|
5332
|
+
});
|
|
5333
|
+
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));
|
|
5334
|
+
handoff.command("cancel <key>").description("close an open handoff without claiming it").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "cancelled", opts));
|
|
5335
|
+
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));
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5083
5338
|
// src/honcho-commands.ts
|
|
5084
5339
|
var import_node_child_process5 = require("node:child_process");
|
|
5085
5340
|
var import_promises3 = require("node:fs/promises");
|
|
@@ -5640,7 +5895,7 @@ async function resolveHonchoConfig(cfg = {}, opts = {}) {
|
|
|
5640
5895
|
}
|
|
5641
5896
|
|
|
5642
5897
|
// src/honcho-ingest.ts
|
|
5643
|
-
var
|
|
5898
|
+
var import_node_crypto5 = require("node:crypto");
|
|
5644
5899
|
var DEFAULT_HONCHO_MAX_CHARS = 4e3;
|
|
5645
5900
|
var DEFAULT_HONCHO_CARD_MAX_CHARS = 1200;
|
|
5646
5901
|
var REDACTED = "[REDACTED]";
|
|
@@ -5726,7 +5981,7 @@ function buildIngestPayload(args) {
|
|
|
5726
5981
|
const maxChars = args.maxChars ?? DEFAULT_HONCHO_MAX_CHARS;
|
|
5727
5982
|
const messages = args.messages.map((m) => ({ role: m.role, content: capContent(redactSecrets(m.content), maxChars) })).filter((m) => m.content.trim().length > 0);
|
|
5728
5983
|
return {
|
|
5729
|
-
id: args.id ?? (0,
|
|
5984
|
+
id: args.id ?? (0, import_node_crypto5.randomUUID)(),
|
|
5730
5985
|
workspace: args.workspace,
|
|
5731
5986
|
peer: args.peer,
|
|
5732
5987
|
session: args.session,
|
|
@@ -6225,8 +6480,205 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
|
6225
6480
|
}
|
|
6226
6481
|
|
|
6227
6482
|
// src/session-start.ts
|
|
6483
|
+
var import_node_fs12 = require("node:fs");
|
|
6484
|
+
var import_node_path10 = require("node:path");
|
|
6485
|
+
|
|
6486
|
+
// src/scratch-gc.ts
|
|
6228
6487
|
var import_node_fs11 = require("node:fs");
|
|
6229
6488
|
var import_node_path9 = require("node:path");
|
|
6489
|
+
var TMP_STALE_MS = 6e4;
|
|
6490
|
+
var FLUSH_LOCK_GC_MS = 6e5;
|
|
6491
|
+
var HEAD_TS_STALE_MS = 48 * 36e5;
|
|
6492
|
+
var LAST_SKIP_STALE_MS = 24 * 36e5;
|
|
6493
|
+
var CONFLICT_COPY_STALE_MS = 24 * 36e5;
|
|
6494
|
+
var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
|
|
6495
|
+
var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
|
|
6496
|
+
function scratchGcThrottlePath(mmiRoot) {
|
|
6497
|
+
return (0, import_node_path9.join)(mmiRoot, "head-ts", ".scratch-gc-last");
|
|
6498
|
+
}
|
|
6499
|
+
function scratchGcDue(stampPath, now = Date.now(), read = import_node_fs11.readFileSync) {
|
|
6500
|
+
try {
|
|
6501
|
+
return now - (Number(read(stampPath, "utf8").trim()) || 0) >= SCRATCH_GC_THROTTLE_MS;
|
|
6502
|
+
} catch {
|
|
6503
|
+
return true;
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
function markScratchGcRun(stampPath, now = Date.now()) {
|
|
6507
|
+
try {
|
|
6508
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(stampPath), { recursive: true });
|
|
6509
|
+
(0, import_node_fs11.writeFileSync)(stampPath, String(now), "utf8");
|
|
6510
|
+
} catch {
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
function executeScratchGc(repoRoot, opts, now = Date.now()) {
|
|
6514
|
+
const snap = collectScratchSnapshot(repoRoot);
|
|
6515
|
+
const plan2 = planScratchGc(snap, now);
|
|
6516
|
+
if (!opts.apply) return { plan: plan2 };
|
|
6517
|
+
return { plan: plan2, applied: applyScratchGc(plan2, snap.mmiRoot, now) };
|
|
6518
|
+
}
|
|
6519
|
+
var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json", "saga-pending.jsonl"]);
|
|
6520
|
+
var TMP_SIDECAR_BASES = ["saga-pending.jsonl", "last-skip.json"];
|
|
6521
|
+
var CONFLICT_COPY_ALLOWLIST = /* @__PURE__ */ new Set(["saga-pending.jsonl", "last-skip.json"]);
|
|
6522
|
+
function conflictCopyOriginal(name) {
|
|
6523
|
+
const m = /^(.+) \d+(\.[^.]+)$/.exec(name);
|
|
6524
|
+
return m ? `${m[1]}${m[2]}` : null;
|
|
6525
|
+
}
|
|
6526
|
+
function isTmpSidecar(name) {
|
|
6527
|
+
return name.endsWith(".tmp") && TMP_SIDECAR_BASES.some((b) => name.startsWith(`${b}.`));
|
|
6528
|
+
}
|
|
6529
|
+
function planScratchGc(snap, now = Date.now()) {
|
|
6530
|
+
const candidates = [];
|
|
6531
|
+
const normalizePath = (p) => p.replace(/\\/g, "/");
|
|
6532
|
+
const mmiPaths = new Set(snap.mmiFiles.map((f) => normalizePath(f.path)));
|
|
6533
|
+
const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
|
|
6534
|
+
const days = (ms) => `${Math.floor(ms / 864e5)}d`;
|
|
6535
|
+
for (const f of snap.mmiFiles) {
|
|
6536
|
+
const age = now - f.mtimeMs;
|
|
6537
|
+
const add = (family, reason) => candidates.push({ path: f.path, family, tier: "safe-auto", reason, bytes: f.bytes });
|
|
6538
|
+
if (isTmpSidecar(f.name)) {
|
|
6539
|
+
if (age > TMP_STALE_MS) add("tmp-sidecar", `crashed atomic-write sidecar (${days(age)} old)`);
|
|
6540
|
+
continue;
|
|
6541
|
+
}
|
|
6542
|
+
if (f.name === "saga-flush.lock") {
|
|
6543
|
+
if (age > FLUSH_LOCK_GC_MS) add("flush-lock", `abandoned flush lock (${days(age)} old)`);
|
|
6544
|
+
continue;
|
|
6545
|
+
}
|
|
6546
|
+
if (NEVER_BASENAMES.has(f.name)) continue;
|
|
6547
|
+
if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
|
|
6548
|
+
if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
|
|
6549
|
+
continue;
|
|
6550
|
+
}
|
|
6551
|
+
if (f.name === "last-skip.json") {
|
|
6552
|
+
if (age > LAST_SKIP_STALE_MS) add("last-skip", `stale ingest-skip hint (${days(age)} old)`);
|
|
6553
|
+
continue;
|
|
6554
|
+
}
|
|
6555
|
+
const orig = conflictCopyOriginal(f.name);
|
|
6556
|
+
if (orig && CONFLICT_COPY_ALLOWLIST.has(orig) && mmiPaths.has(normalizePath((0, import_node_path9.join)(f.dir, orig))) && age > CONFLICT_COPY_STALE_MS) {
|
|
6557
|
+
add("conflict-copy", `cloud-sync conflict copy of ${orig} (${days(age)} old)`);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
if (snap.syncQueueSlugs) {
|
|
6561
|
+
for (const f of snap.planMdFiles) {
|
|
6562
|
+
const slug = f.name.replace(/\.md$/, "");
|
|
6563
|
+
if (snap.syncQueueSlugs.has(slug)) continue;
|
|
6564
|
+
if (now - f.mtimeMs > PLAN_ADVISORY_AGE_MS) {
|
|
6565
|
+
candidates.push({
|
|
6566
|
+
path: f.path,
|
|
6567
|
+
family: "plan",
|
|
6568
|
+
tier: "advisory",
|
|
6569
|
+
reason: `synced plan older than ${Math.floor(PLAN_ADVISORY_AGE_MS / 864e5)}d`,
|
|
6570
|
+
bytes: f.bytes
|
|
6571
|
+
});
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
return {
|
|
6576
|
+
candidates,
|
|
6577
|
+
safeAuto: candidates.filter((c) => c.tier === "safe-auto"),
|
|
6578
|
+
advisory: candidates.filter((c) => c.tier === "advisory")
|
|
6579
|
+
};
|
|
6580
|
+
}
|
|
6581
|
+
function formatScratchGcPlan(plan2, applied) {
|
|
6582
|
+
if (plan2.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local saga/plan/honcho scratch is clean.";
|
|
6583
|
+
const lines = [];
|
|
6584
|
+
const bytes = plan2.safeAuto.reduce((n, c) => n + c.bytes, 0);
|
|
6585
|
+
lines.push(
|
|
6586
|
+
`scratch GC: ${applied ? "pruned" : "would prune"} ${plan2.safeAuto.length} stale file(s) (${bytes} bytes)` + (plan2.advisory.length ? `; ${plan2.advisory.length} advisory (kept):` : ":")
|
|
6587
|
+
);
|
|
6588
|
+
for (const c of plan2.safeAuto.slice(0, 40)) lines.push(` - [${c.family}] ${c.path} \u2014 ${c.reason}`);
|
|
6589
|
+
if (plan2.safeAuto.length > 40) lines.push(` ... +${plan2.safeAuto.length - 40} more`);
|
|
6590
|
+
for (const c of plan2.advisory.slice(0, 20)) lines.push(` \xB7 [advisory] ${c.path} \u2014 ${c.reason} (kept; review manually)`);
|
|
6591
|
+
return lines.join("\n");
|
|
6592
|
+
}
|
|
6593
|
+
function scratchGcBannerLine(safeAutoPruned, advisory) {
|
|
6594
|
+
const parts = [];
|
|
6595
|
+
if (safeAutoPruned > 0) parts.push(`pruned ${safeAutoPruned} stale local file(s)`);
|
|
6596
|
+
if (advisory > 0) parts.push(`${advisory} old synced plan(s) prunable \u2014 \`mmi-cli gc --scratch --dry-run\` to review`);
|
|
6597
|
+
return parts.length ? `[cleanup] ${parts.join("; ")}` : void 0;
|
|
6598
|
+
}
|
|
6599
|
+
function applyScratchGc(plan2, mmiRoot, now = Date.now()) {
|
|
6600
|
+
const result = { pruned: [], skipped: 0, bytes: 0 };
|
|
6601
|
+
let anchor;
|
|
6602
|
+
try {
|
|
6603
|
+
anchor = (0, import_node_fs11.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
6604
|
+
} catch {
|
|
6605
|
+
return result;
|
|
6606
|
+
}
|
|
6607
|
+
for (const c of plan2.safeAuto) {
|
|
6608
|
+
try {
|
|
6609
|
+
const real = (0, import_node_fs11.realpathSync)(c.path).replace(/\\/g, "/");
|
|
6610
|
+
if (real !== anchor && !real.startsWith(`${anchor}/`)) {
|
|
6611
|
+
result.skipped += 1;
|
|
6612
|
+
continue;
|
|
6613
|
+
}
|
|
6614
|
+
const st = (0, import_node_fs11.statSync)(c.path);
|
|
6615
|
+
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;
|
|
6616
|
+
if (now - st.mtimeMs <= floor) {
|
|
6617
|
+
result.skipped += 1;
|
|
6618
|
+
continue;
|
|
6619
|
+
}
|
|
6620
|
+
(0, import_node_fs11.unlinkSync)(c.path);
|
|
6621
|
+
result.pruned.push(c.path);
|
|
6622
|
+
result.bytes += c.bytes;
|
|
6623
|
+
} catch {
|
|
6624
|
+
result.skipped += 1;
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
return result;
|
|
6628
|
+
}
|
|
6629
|
+
function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
6630
|
+
const readdir = deps.readdir ?? import_node_fs11.readdirSync;
|
|
6631
|
+
const stat = deps.stat ?? import_node_fs11.statSync;
|
|
6632
|
+
const readFile5 = deps.readFile ?? import_node_fs11.readFileSync;
|
|
6633
|
+
const mmiRoot = (0, import_node_path9.join)(repoRoot, ".mmi");
|
|
6634
|
+
const plansRoot = (0, import_node_path9.join)(repoRoot, "plans");
|
|
6635
|
+
const mmiFiles = [];
|
|
6636
|
+
try {
|
|
6637
|
+
for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
|
|
6638
|
+
if (!ent.isFile()) continue;
|
|
6639
|
+
const dir = ent.parentPath ?? ent.path ?? mmiRoot;
|
|
6640
|
+
const full = (0, import_node_path9.join)(dir, ent.name);
|
|
6641
|
+
try {
|
|
6642
|
+
const st = stat(full);
|
|
6643
|
+
mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
6644
|
+
} catch {
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
} catch {
|
|
6648
|
+
}
|
|
6649
|
+
const planMdFiles = [];
|
|
6650
|
+
try {
|
|
6651
|
+
for (const ent of readdir(plansRoot, { withFileTypes: true })) {
|
|
6652
|
+
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
6653
|
+
const full = (0, import_node_path9.join)(plansRoot, ent.name);
|
|
6654
|
+
try {
|
|
6655
|
+
const st = stat(full);
|
|
6656
|
+
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
6657
|
+
} catch {
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
} catch {
|
|
6661
|
+
}
|
|
6662
|
+
let syncQueueSlugs = null;
|
|
6663
|
+
let queueRaw;
|
|
6664
|
+
try {
|
|
6665
|
+
queueRaw = readFile5((0, import_node_path9.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
6666
|
+
} catch {
|
|
6667
|
+
syncQueueSlugs = /* @__PURE__ */ new Set();
|
|
6668
|
+
}
|
|
6669
|
+
if (queueRaw !== void 0) {
|
|
6670
|
+
try {
|
|
6671
|
+
const parsed = JSON.parse(queueRaw);
|
|
6672
|
+
const entries = Array.isArray(parsed) ? parsed : parsed.entries ?? [];
|
|
6673
|
+
syncQueueSlugs = new Set(entries.map((e) => e.slug).filter((s) => typeof s === "string"));
|
|
6674
|
+
} catch {
|
|
6675
|
+
syncQueueSlugs = null;
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
return { mmiRoot, mmiFiles, planMdFiles, syncQueueSlugs };
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
// src/session-start.ts
|
|
6230
6682
|
async function runBufferedStep(step) {
|
|
6231
6683
|
const lines = [];
|
|
6232
6684
|
const io = {
|
|
@@ -6254,6 +6706,7 @@ function buildSessionStartPlan(verbs) {
|
|
|
6254
6706
|
parallel: [
|
|
6255
6707
|
{ name: "rules sync", run: verbs.rulesSync },
|
|
6256
6708
|
{ name: "saga show", run: verbs.sagaShow },
|
|
6709
|
+
{ name: "handoff offer", run: verbs.handoffOffer },
|
|
6257
6710
|
// honcho profile (#1162): the behavioral-memory prior, flushed right after the saga resume. A fast
|
|
6258
6711
|
// peer-card GET, fail-soft + silent when off/empty — it never hangs or noises the banner.
|
|
6259
6712
|
{ name: "honcho profile", run: verbs.honchoContext },
|
|
@@ -6277,12 +6730,12 @@ function spawnDetachedSelf(args, deps) {
|
|
|
6277
6730
|
}
|
|
6278
6731
|
function planStoreLines(cwd) {
|
|
6279
6732
|
const mdFiles = (dir, minSize = 0) => {
|
|
6280
|
-
const p = (0,
|
|
6281
|
-
if (!(0,
|
|
6733
|
+
const p = (0, import_node_path10.join)(cwd, dir);
|
|
6734
|
+
if (!(0, import_node_fs12.existsSync)(p)) return [];
|
|
6282
6735
|
try {
|
|
6283
|
-
return (0,
|
|
6736
|
+
return (0, import_node_fs12.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
|
|
6284
6737
|
try {
|
|
6285
|
-
return (0,
|
|
6738
|
+
return (0, import_node_fs12.statSync)((0, import_node_path10.join)(p, f)).size >= minSize;
|
|
6286
6739
|
} catch {
|
|
6287
6740
|
return false;
|
|
6288
6741
|
}
|
|
@@ -6300,6 +6753,19 @@ function planStoreLines(cwd) {
|
|
|
6300
6753
|
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
6754
|
return out;
|
|
6302
6755
|
}
|
|
6756
|
+
function scratchGcLines(cwd, env = process.env, now = Date.now()) {
|
|
6757
|
+
if (env.MMI_NO_AUTO_GC) return [];
|
|
6758
|
+
try {
|
|
6759
|
+
const stamp = scratchGcThrottlePath((0, import_node_path10.join)(cwd, ".mmi"));
|
|
6760
|
+
if (!scratchGcDue(stamp, now)) return [];
|
|
6761
|
+
const run = executeScratchGc(cwd, { apply: true }, now);
|
|
6762
|
+
markScratchGcRun(stamp, now);
|
|
6763
|
+
const line = scratchGcBannerLine(run.applied?.pruned.length ?? 0, run.plan.advisory.length);
|
|
6764
|
+
return line ? [line] : [];
|
|
6765
|
+
} catch {
|
|
6766
|
+
return [];
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6303
6769
|
function northstarPointer(injected = false) {
|
|
6304
6770
|
if (injected) {
|
|
6305
6771
|
return "North Stars: `mmi-cli northstar relevant` for more matches; `northstar pull <slug>` for the full SSOT.";
|
|
@@ -6347,6 +6813,71 @@ function recoverPriorityFromEvents(events) {
|
|
|
6347
6813
|
return found;
|
|
6348
6814
|
}
|
|
6349
6815
|
|
|
6816
|
+
// src/board-dependency.ts
|
|
6817
|
+
var DEPENDS_ON_LINE = /^\s*[-*]?\s*\*\*Depends on:\*\*\s*(.+)$/im;
|
|
6818
|
+
var ISSUE_REF = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)#(\d+)/g;
|
|
6819
|
+
function parseDependsOnRefs(body) {
|
|
6820
|
+
const match = body.match(DEPENDS_ON_LINE);
|
|
6821
|
+
if (!match) return [];
|
|
6822
|
+
const refs = [];
|
|
6823
|
+
for (const m of match[1].matchAll(ISSUE_REF)) {
|
|
6824
|
+
refs.push({ repo: m[1], number: Number(m[2]) });
|
|
6825
|
+
}
|
|
6826
|
+
return refs;
|
|
6827
|
+
}
|
|
6828
|
+
async function issueOpen(client, repo, number) {
|
|
6829
|
+
try {
|
|
6830
|
+
const data = await client.rest("GET", `repos/${repo}/issues/${number}`);
|
|
6831
|
+
return data?.state === "OPEN";
|
|
6832
|
+
} catch {
|
|
6833
|
+
return void 0;
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
async function dependencyBlocksClaim(client, body) {
|
|
6837
|
+
const refs = parseDependsOnRefs(body);
|
|
6838
|
+
if (!refs.length) return { blocked: false, openDependencies: [] };
|
|
6839
|
+
const openDependencies = [];
|
|
6840
|
+
for (const ref of refs) {
|
|
6841
|
+
const open = await issueOpen(client, ref.repo, ref.number);
|
|
6842
|
+
if (open === true) openDependencies.push(`${ref.repo}#${ref.number}`);
|
|
6843
|
+
}
|
|
6844
|
+
return { blocked: openDependencies.length > 0, openDependencies };
|
|
6845
|
+
}
|
|
6846
|
+
async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
6847
|
+
const claimable = [];
|
|
6848
|
+
const blocked = [];
|
|
6849
|
+
const warnings = [];
|
|
6850
|
+
for (const item of items) {
|
|
6851
|
+
if (item.contentType !== "Issue") {
|
|
6852
|
+
claimable.push(item);
|
|
6853
|
+
continue;
|
|
6854
|
+
}
|
|
6855
|
+
let body = item.details?.body;
|
|
6856
|
+
if (body === void 0) {
|
|
6857
|
+
if (opts.requireBundledBody) {
|
|
6858
|
+
claimable.push(item);
|
|
6859
|
+
continue;
|
|
6860
|
+
}
|
|
6861
|
+
try {
|
|
6862
|
+
const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
|
|
6863
|
+
body = data?.body ?? "";
|
|
6864
|
+
} catch (e) {
|
|
6865
|
+
warnings.push(`dependency check skipped for ${item.ref}: ${e.message}`);
|
|
6866
|
+
claimable.push(item);
|
|
6867
|
+
continue;
|
|
6868
|
+
}
|
|
6869
|
+
}
|
|
6870
|
+
const gate = await dependencyBlocksClaim(client, body);
|
|
6871
|
+
if (gate.blocked) {
|
|
6872
|
+
blocked.push(item);
|
|
6873
|
+
warnings.push(`${item.ref} blocked on open ${gate.openDependencies.join(", ")}`);
|
|
6874
|
+
} else {
|
|
6875
|
+
claimable.push(item);
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
return { claimable, blocked, warnings };
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6350
6881
|
// ../infra/board-vocab.mjs
|
|
6351
6882
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
6352
6883
|
|
|
@@ -6670,6 +7201,13 @@ async function readBoard(options, deps = {}) {
|
|
|
6670
7201
|
if (options.includeBundleDetails) {
|
|
6671
7202
|
await attachBundleDetails(report, client, options.allowPartial ?? false);
|
|
6672
7203
|
}
|
|
7204
|
+
for (const scope of ["primary", "secondary"]) {
|
|
7205
|
+
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
|
|
7206
|
+
requireBundledBody: Boolean(options.includeBundleDetails)
|
|
7207
|
+
});
|
|
7208
|
+
report[scope].claimable = filtered.claimable;
|
|
7209
|
+
report.warnings.push(...filtered.warnings);
|
|
7210
|
+
}
|
|
6673
7211
|
return report;
|
|
6674
7212
|
}
|
|
6675
7213
|
function findBoardItem(items, selector) {
|
|
@@ -6751,6 +7289,11 @@ async function prepareClaimContext(options, selectors, deps, collected) {
|
|
|
6751
7289
|
warnings: collected.warnings,
|
|
6752
7290
|
partial: collected.partial
|
|
6753
7291
|
};
|
|
7292
|
+
for (const scope of ["primary", "secondary"]) {
|
|
7293
|
+
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client);
|
|
7294
|
+
report[scope].claimable = filtered.claimable;
|
|
7295
|
+
report.warnings.push(...filtered.warnings);
|
|
7296
|
+
}
|
|
6754
7297
|
return { cfg, client, items: collected.items, writable: writable.repos, report };
|
|
6755
7298
|
}
|
|
6756
7299
|
async function claimOneBoardItem(ctx, selector, options) {
|
|
@@ -6759,6 +7302,21 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
6759
7302
|
if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
|
|
6760
7303
|
throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
|
|
6761
7304
|
}
|
|
7305
|
+
if (flatItem.contentType === "Issue") {
|
|
7306
|
+
let body = flatItem.details?.body;
|
|
7307
|
+
if (body === void 0) {
|
|
7308
|
+
try {
|
|
7309
|
+
const data = await client.rest("GET", `repos/${flatItem.repository}/issues/${flatItem.number}`);
|
|
7310
|
+
body = data?.body ?? "";
|
|
7311
|
+
} catch {
|
|
7312
|
+
body = "";
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
const gate = await dependencyBlocksClaim(client, body);
|
|
7316
|
+
if (gate.blocked) {
|
|
7317
|
+
throw new Error(`${flatItem.ref} is not claimable: blocked on open ${gate.openDependencies.join(", ")}`);
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
6762
7320
|
let item = findClaimableItem(report, selector);
|
|
6763
7321
|
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
6764
7322
|
const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
|
|
@@ -7209,6 +7767,109 @@ async function runBoardSlice(io, deps, opts) {
|
|
|
7209
7767
|
}
|
|
7210
7768
|
}
|
|
7211
7769
|
|
|
7770
|
+
// src/worktree.ts
|
|
7771
|
+
var import_node_fs13 = require("node:fs");
|
|
7772
|
+
var import_node_path11 = require("node:path");
|
|
7773
|
+
var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
7774
|
+
var PKG = "package.json";
|
|
7775
|
+
var LOCKFILE = "package-lock.json";
|
|
7776
|
+
var NODE_MODULES = "node_modules";
|
|
7777
|
+
var realFsProbe = {
|
|
7778
|
+
isDir: (p) => {
|
|
7779
|
+
try {
|
|
7780
|
+
return (0, import_node_fs13.statSync)(p).isDirectory();
|
|
7781
|
+
} catch {
|
|
7782
|
+
return false;
|
|
7783
|
+
}
|
|
7784
|
+
},
|
|
7785
|
+
isFile: (p) => {
|
|
7786
|
+
try {
|
|
7787
|
+
return (0, import_node_fs13.statSync)(p).isFile();
|
|
7788
|
+
} catch {
|
|
7789
|
+
return false;
|
|
7790
|
+
}
|
|
7791
|
+
},
|
|
7792
|
+
listDirs: (p) => {
|
|
7793
|
+
try {
|
|
7794
|
+
return (0, import_node_fs13.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7795
|
+
} catch {
|
|
7796
|
+
return [];
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
};
|
|
7800
|
+
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
7801
|
+
const factsFor = (dir) => {
|
|
7802
|
+
const abs = dir ? (0, import_node_path11.join)(root, dir) : root;
|
|
7803
|
+
return {
|
|
7804
|
+
dir,
|
|
7805
|
+
hasPackageJson: fs2.isFile((0, import_node_path11.join)(abs, PKG)),
|
|
7806
|
+
hasLockfile: fs2.isFile((0, import_node_path11.join)(abs, LOCKFILE)),
|
|
7807
|
+
hasNodeModules: fs2.isDir((0, import_node_path11.join)(abs, NODE_MODULES))
|
|
7808
|
+
};
|
|
7809
|
+
};
|
|
7810
|
+
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
7811
|
+
return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
|
|
7812
|
+
}
|
|
7813
|
+
function npmInstallTargets(dirs) {
|
|
7814
|
+
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
7815
|
+
}
|
|
7816
|
+
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
7817
|
+
return fs2.isFile((0, import_node_path11.join)(root, ".git"));
|
|
7818
|
+
}
|
|
7819
|
+
function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
7820
|
+
if (!isLinkedWorktree(root, fs2)) return null;
|
|
7821
|
+
const pending = npmInstallTargets(scanInstallDirs(root, fs2));
|
|
7822
|
+
if (!pending.length) return null;
|
|
7823
|
+
const where = pending.map((t) => t.dir || ".").join(", ");
|
|
7824
|
+
return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
|
|
7825
|
+
}
|
|
7826
|
+
function defaultCopyFile(from, to) {
|
|
7827
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(to), { recursive: true });
|
|
7828
|
+
(0, import_node_fs13.copyFileSync)(from, to);
|
|
7829
|
+
}
|
|
7830
|
+
async function provisionWorktree(worktreeRoot, deps) {
|
|
7831
|
+
const fs2 = deps.fs ?? realFsProbe;
|
|
7832
|
+
const copyFile = deps.copyFile ?? defaultCopyFile;
|
|
7833
|
+
const log = deps.log ?? (() => {
|
|
7834
|
+
});
|
|
7835
|
+
const allDirs = scanInstallDirs(worktreeRoot, fs2);
|
|
7836
|
+
const targets = npmInstallTargets(allDirs);
|
|
7837
|
+
const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
|
|
7838
|
+
const installed = [];
|
|
7839
|
+
for (const target of targets) {
|
|
7840
|
+
const cwd = target.dir ? (0, import_node_path11.join)(worktreeRoot, target.dir) : worktreeRoot;
|
|
7841
|
+
log(`installing deps: ${target.command} in ${target.dir || "."}`);
|
|
7842
|
+
await deps.runInstall(target.command, cwd);
|
|
7843
|
+
installed.push(target);
|
|
7844
|
+
}
|
|
7845
|
+
const copied = [];
|
|
7846
|
+
const copySkipped = [];
|
|
7847
|
+
const primary = await deps.primaryCheckout();
|
|
7848
|
+
for (const rel of LOCAL_ONLY_FILES) {
|
|
7849
|
+
const dest = (0, import_node_path11.join)(worktreeRoot, rel);
|
|
7850
|
+
if (fs2.isFile(dest)) {
|
|
7851
|
+
copySkipped.push({ file: rel, reason: "already-present" });
|
|
7852
|
+
continue;
|
|
7853
|
+
}
|
|
7854
|
+
if (!primary) {
|
|
7855
|
+
copySkipped.push({ file: rel, reason: "no-primary" });
|
|
7856
|
+
continue;
|
|
7857
|
+
}
|
|
7858
|
+
if (!fs2.isFile((0, import_node_path11.join)(primary, rel))) {
|
|
7859
|
+
copySkipped.push({ file: rel, reason: "absent-in-primary" });
|
|
7860
|
+
continue;
|
|
7861
|
+
}
|
|
7862
|
+
copyFile((0, import_node_path11.join)(primary, rel), dest);
|
|
7863
|
+
copied.push(rel);
|
|
7864
|
+
log(`copied local config: ${rel}`);
|
|
7865
|
+
}
|
|
7866
|
+
return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
|
|
7867
|
+
}
|
|
7868
|
+
function defaultWorktreePath(repoRoot, branch) {
|
|
7869
|
+
const safe = branch.replace(/[/\\]+/g, "-");
|
|
7870
|
+
return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot), "mmi-worktrees", safe);
|
|
7871
|
+
}
|
|
7872
|
+
|
|
7212
7873
|
// src/frontmatter.ts
|
|
7213
7874
|
function splitFrontmatter(content) {
|
|
7214
7875
|
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
@@ -7404,7 +8065,7 @@ async function runNorthstarContext(io, deps) {
|
|
|
7404
8065
|
}
|
|
7405
8066
|
|
|
7406
8067
|
// src/index.ts
|
|
7407
|
-
var
|
|
8068
|
+
var import_node_path16 = require("node:path");
|
|
7408
8069
|
|
|
7409
8070
|
// src/merge-ci-policy.ts
|
|
7410
8071
|
function resolveMergeCiPolicy(input) {
|
|
@@ -7816,8 +8477,8 @@ var import_node_os5 = require("node:os");
|
|
|
7816
8477
|
// src/gh-create.ts
|
|
7817
8478
|
var import_promises4 = require("node:fs/promises");
|
|
7818
8479
|
var import_node_os3 = require("node:os");
|
|
7819
|
-
var
|
|
7820
|
-
var
|
|
8480
|
+
var import_node_path12 = require("node:path");
|
|
8481
|
+
var import_node_crypto6 = require("node:crypto");
|
|
7821
8482
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
7822
8483
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
7823
8484
|
function timeoutKillNote(err, timeoutMs) {
|
|
@@ -7857,7 +8518,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
7857
8518
|
} };
|
|
7858
8519
|
const write = deps.write ?? import_promises4.writeFile;
|
|
7859
8520
|
const remove = deps.remove ?? import_promises4.unlink;
|
|
7860
|
-
const file = (0,
|
|
8521
|
+
const file = (0, import_node_path12.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
|
|
7861
8522
|
await write(file, args[i + 1], "utf8");
|
|
7862
8523
|
return {
|
|
7863
8524
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -8182,7 +8843,7 @@ ${buildReportBody(body, sourceRepo)}`;
|
|
|
8182
8843
|
|
|
8183
8844
|
// src/skill-lesson.ts
|
|
8184
8845
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
8185
|
-
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
8846
|
+
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "handoff", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
8186
8847
|
function assertSkillName(name) {
|
|
8187
8848
|
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
8188
8849
|
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
@@ -8682,12 +9343,12 @@ function buildGcPlan(inputs) {
|
|
|
8682
9343
|
skipped.push({ branch, reason: "current-branch" });
|
|
8683
9344
|
continue;
|
|
8684
9345
|
}
|
|
8685
|
-
const
|
|
8686
|
-
if (
|
|
8687
|
-
skipped.push({ branch, reason: "dirty-worktree", detail:
|
|
9346
|
+
const worktree2 = worktrees.get(branch);
|
|
9347
|
+
if (worktree2?.dirty) {
|
|
9348
|
+
skipped.push({ branch, reason: "dirty-worktree", detail: worktree2.path });
|
|
8688
9349
|
continue;
|
|
8689
9350
|
}
|
|
8690
|
-
branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath:
|
|
9351
|
+
branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath: worktree2?.path });
|
|
8691
9352
|
}
|
|
8692
9353
|
const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
|
|
8693
9354
|
const branch = branchForTrackingRef(ref, remote);
|
|
@@ -9226,9 +9887,9 @@ function stalePosixFields(config, shell2) {
|
|
|
9226
9887
|
}
|
|
9227
9888
|
function sanitizeLocalStage(local, stale) {
|
|
9228
9889
|
if (!stale.length) return local;
|
|
9229
|
-
const
|
|
9230
|
-
for (const field of stale) delete
|
|
9231
|
-
return
|
|
9890
|
+
const clean4 = { ...local };
|
|
9891
|
+
for (const field of stale) delete clean4[field];
|
|
9892
|
+
return clean4;
|
|
9232
9893
|
}
|
|
9233
9894
|
function staleNote(staleFields, outcome) {
|
|
9234
9895
|
const list = staleFields.join(", ");
|
|
@@ -9274,9 +9935,9 @@ function decideStage(inputs) {
|
|
|
9274
9935
|
|
|
9275
9936
|
// src/cursor-plugin-seed.ts
|
|
9276
9937
|
var import_node_child_process7 = require("node:child_process");
|
|
9277
|
-
var
|
|
9938
|
+
var import_node_fs14 = require("node:fs");
|
|
9278
9939
|
var import_node_os4 = require("node:os");
|
|
9279
|
-
var
|
|
9940
|
+
var import_node_path13 = require("node:path");
|
|
9280
9941
|
var import_node_util6 = require("node:util");
|
|
9281
9942
|
function isSemverVersion(v) {
|
|
9282
9943
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
@@ -9293,17 +9954,17 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
9293
9954
|
}
|
|
9294
9955
|
function cursorUserGlobalStatePath() {
|
|
9295
9956
|
if (process.platform === "win32") {
|
|
9296
|
-
const base2 = process.env.APPDATA || (0,
|
|
9297
|
-
return (0,
|
|
9957
|
+
const base2 = process.env.APPDATA || (0, import_node_path13.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
|
|
9958
|
+
return (0, import_node_path13.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
9298
9959
|
}
|
|
9299
9960
|
if (process.platform === "darwin") {
|
|
9300
|
-
return (0,
|
|
9961
|
+
return (0, import_node_path13.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
9301
9962
|
}
|
|
9302
|
-
return (0,
|
|
9963
|
+
return (0, import_node_path13.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
9303
9964
|
}
|
|
9304
9965
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
9305
9966
|
const dbPath = cursorUserGlobalStatePath();
|
|
9306
|
-
if (!(0,
|
|
9967
|
+
if (!(0, import_node_fs14.existsSync)(dbPath)) return void 0;
|
|
9307
9968
|
try {
|
|
9308
9969
|
const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
|
|
9309
9970
|
timeout: 5e3
|
|
@@ -9317,57 +9978,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
|
9317
9978
|
}
|
|
9318
9979
|
}
|
|
9319
9980
|
function syncDirContents(src, dest) {
|
|
9320
|
-
(0,
|
|
9321
|
-
for (const name of (0,
|
|
9322
|
-
(0,
|
|
9981
|
+
(0, import_node_fs14.mkdirSync)(dest, { recursive: true });
|
|
9982
|
+
for (const name of (0, import_node_fs14.readdirSync)(dest)) {
|
|
9983
|
+
(0, import_node_fs14.rmSync)((0, import_node_path13.join)(dest, name), { recursive: true, force: true });
|
|
9323
9984
|
}
|
|
9324
|
-
(0,
|
|
9985
|
+
(0, import_node_fs14.cpSync)(src, dest, { recursive: true });
|
|
9325
9986
|
}
|
|
9326
9987
|
function releaseTag(releasedVersion) {
|
|
9327
9988
|
return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
|
|
9328
9989
|
}
|
|
9329
9990
|
async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
|
|
9330
|
-
const tarFile = (0,
|
|
9991
|
+
const tarFile = (0, import_node_path13.join)(tmpRoot, "archive.tar");
|
|
9331
9992
|
try {
|
|
9332
9993
|
await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
|
|
9333
9994
|
await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
|
|
9334
9995
|
timeout: 6e4
|
|
9335
9996
|
});
|
|
9336
9997
|
await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
|
|
9337
|
-
const pluginMmi = (0,
|
|
9338
|
-
return (0,
|
|
9998
|
+
const pluginMmi = (0, import_node_path13.join)(tmpRoot, "plugins", "mmi");
|
|
9999
|
+
return (0, import_node_fs14.existsSync)((0, import_node_path13.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
9339
10000
|
} catch {
|
|
9340
10001
|
return void 0;
|
|
9341
10002
|
}
|
|
9342
10003
|
}
|
|
9343
10004
|
async function downloadPluginMmiViaGh(tag, tmpRoot) {
|
|
9344
|
-
const tarPath = (0,
|
|
10005
|
+
const tarPath = (0, import_node_path13.join)(tmpRoot, "repo.tgz");
|
|
9345
10006
|
try {
|
|
9346
|
-
(0,
|
|
10007
|
+
(0, import_node_fs14.mkdirSync)(tmpRoot, { recursive: true });
|
|
9347
10008
|
const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
|
|
9348
10009
|
timeout: 12e4,
|
|
9349
10010
|
maxBuffer: 100 * 1024 * 1024,
|
|
9350
10011
|
encoding: "buffer",
|
|
9351
10012
|
windowsHide: true
|
|
9352
10013
|
});
|
|
9353
|
-
(0,
|
|
10014
|
+
(0, import_node_fs14.writeFileSync)(tarPath, stdout);
|
|
9354
10015
|
await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
|
|
9355
|
-
const top = (0,
|
|
10016
|
+
const top = (0, import_node_fs14.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
|
|
9356
10017
|
if (!top) return void 0;
|
|
9357
|
-
const pluginMmi = (0,
|
|
9358
|
-
return (0,
|
|
10018
|
+
const pluginMmi = (0, import_node_path13.join)(tmpRoot, top, "plugins", "mmi");
|
|
10019
|
+
return (0, import_node_fs14.existsSync)((0, import_node_path13.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
9359
10020
|
} catch {
|
|
9360
10021
|
return void 0;
|
|
9361
10022
|
}
|
|
9362
10023
|
}
|
|
9363
10024
|
async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
|
|
9364
|
-
(0,
|
|
10025
|
+
(0, import_node_fs14.mkdirSync)(tmpRoot, { recursive: true });
|
|
9365
10026
|
const tag = releaseTag(releasedVersion);
|
|
9366
10027
|
if (hubCheckout) {
|
|
9367
10028
|
const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
|
|
9368
10029
|
if (fromHub) return fromHub;
|
|
9369
10030
|
}
|
|
9370
|
-
return downloadPluginMmiViaGh(tag, (0,
|
|
10031
|
+
return downloadPluginMmiViaGh(tag, (0, import_node_path13.join)(tmpRoot, "gh"));
|
|
9371
10032
|
}
|
|
9372
10033
|
function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
9373
10034
|
if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
|
|
@@ -9388,7 +10049,7 @@ async function applyCursorPluginCacheSeed(input) {
|
|
|
9388
10049
|
for (const pin of pinsToSeed) {
|
|
9389
10050
|
syncDirContents(source, pin.path);
|
|
9390
10051
|
}
|
|
9391
|
-
(0,
|
|
10052
|
+
(0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
9392
10053
|
return true;
|
|
9393
10054
|
}
|
|
9394
10055
|
|
|
@@ -9436,8 +10097,12 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9436
10097
|
".claude/worktrees/",
|
|
9437
10098
|
".mmi/.session",
|
|
9438
10099
|
".mmi/head-ts/",
|
|
9439
|
-
//
|
|
9440
|
-
//
|
|
10100
|
+
// Honcho's whole runtime dir — its ingest-throttle stamp (`last-skip.json`) is NOT a queue file, so the
|
|
10101
|
+
// recursive patterns below miss it and it dirtied the working tree (blocked a release clean-tree check,
|
|
10102
|
+
// #1472). The dir is pure scratch, so ignore it wholesale, like `.mmi/head-ts/`.
|
|
10103
|
+
".mmi/honcho/",
|
|
10104
|
+
// Recursive so any reuse of the parameterized queue engine in a new .mmi subdir is ignored too —
|
|
10105
|
+
// `**` matches zero dirs, so this still covers the root saga queue.
|
|
9441
10106
|
".mmi/**/saga-pending.jsonl*",
|
|
9442
10107
|
".mmi/**/saga-flush.lock",
|
|
9443
10108
|
".aws-sam/",
|
|
@@ -10106,8 +10771,8 @@ async function runStageLiveDown(deps, t) {
|
|
|
10106
10771
|
|
|
10107
10772
|
// src/stage-runner.ts
|
|
10108
10773
|
var import_node_child_process8 = require("node:child_process");
|
|
10109
|
-
var
|
|
10110
|
-
var
|
|
10774
|
+
var import_node_fs15 = require("node:fs");
|
|
10775
|
+
var import_node_path14 = require("node:path");
|
|
10111
10776
|
var import_node_net2 = require("node:net");
|
|
10112
10777
|
var import_node_util7 = require("node:util");
|
|
10113
10778
|
var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process8.execFile);
|
|
@@ -10152,7 +10817,7 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
|
|
|
10152
10817
|
return void 0;
|
|
10153
10818
|
}
|
|
10154
10819
|
function stageStatePath(cwd = process.cwd()) {
|
|
10155
|
-
return (0,
|
|
10820
|
+
return (0, import_node_path14.join)(cwd, "tmp", "stage", "state.json");
|
|
10156
10821
|
}
|
|
10157
10822
|
function mergeEnvSecretsIntoFile(content, secrets2) {
|
|
10158
10823
|
const lines = content.split(/\r?\n/);
|
|
@@ -10235,9 +10900,9 @@ async function shell(command, cwd, timeoutMs) {
|
|
|
10235
10900
|
});
|
|
10236
10901
|
}
|
|
10237
10902
|
function readState(path2) {
|
|
10238
|
-
if (!(0,
|
|
10903
|
+
if (!(0, import_node_fs15.existsSync)(path2)) return null;
|
|
10239
10904
|
try {
|
|
10240
|
-
return JSON.parse((0,
|
|
10905
|
+
return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
|
|
10241
10906
|
} catch {
|
|
10242
10907
|
return null;
|
|
10243
10908
|
}
|
|
@@ -10289,7 +10954,7 @@ async function stopStage(opts = {}) {
|
|
|
10289
10954
|
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
10290
10955
|
}
|
|
10291
10956
|
await killTree(state.pid);
|
|
10292
|
-
(0,
|
|
10957
|
+
(0, import_node_fs15.rmSync)(statePath, { force: true });
|
|
10293
10958
|
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
10294
10959
|
}
|
|
10295
10960
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -10298,7 +10963,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10298
10963
|
const cwd = opts.cwd ?? process.cwd();
|
|
10299
10964
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
10300
10965
|
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
10301
|
-
(0,
|
|
10966
|
+
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
10302
10967
|
let stagePort;
|
|
10303
10968
|
if (config.portRange) {
|
|
10304
10969
|
const [s, e] = config.portRange;
|
|
@@ -10308,14 +10973,14 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10308
10973
|
}
|
|
10309
10974
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
10310
10975
|
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,
|
|
10976
|
+
const target = (0, import_node_path14.join)(cwd, config.ensureEnv.target);
|
|
10977
|
+
const example = (0, import_node_path14.join)(cwd, config.ensureEnv.example);
|
|
10978
|
+
if (!(0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
|
|
10979
|
+
(0, import_node_fs15.copyFileSync)(example, target);
|
|
10980
|
+
} else if ((0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
|
|
10981
|
+
const stale = detectStaleEnvFile((0, import_node_fs15.readFileSync)(example, "utf8"), (0, import_node_fs15.readFileSync)(target, "utf8"), {
|
|
10982
|
+
exampleMtimeMs: (0, import_node_fs15.statSync)(example).mtimeMs,
|
|
10983
|
+
targetMtimeMs: (0, import_node_fs15.statSync)(target).mtimeMs
|
|
10319
10984
|
});
|
|
10320
10985
|
if (stale) {
|
|
10321
10986
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -10323,8 +10988,8 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10323
10988
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
10324
10989
|
}
|
|
10325
10990
|
}
|
|
10326
|
-
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0,
|
|
10327
|
-
(0,
|
|
10991
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs15.existsSync)(target)) {
|
|
10992
|
+
(0, import_node_fs15.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs15.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
10328
10993
|
}
|
|
10329
10994
|
}
|
|
10330
10995
|
const extraEnv = {};
|
|
@@ -10349,13 +11014,13 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10349
11014
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
10350
11015
|
port: stagePort
|
|
10351
11016
|
};
|
|
10352
|
-
(0,
|
|
11017
|
+
(0, import_node_fs15.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
10353
11018
|
try {
|
|
10354
11019
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
10355
11020
|
else await waitForProcessStability(child);
|
|
10356
11021
|
} catch (e) {
|
|
10357
11022
|
await killTree(state.pid);
|
|
10358
|
-
(0,
|
|
11023
|
+
(0, import_node_fs15.rmSync)(statePath, { force: true });
|
|
10359
11024
|
throw e;
|
|
10360
11025
|
}
|
|
10361
11026
|
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
@@ -10471,7 +11136,7 @@ function resolveDeployModel2(meta, repo) {
|
|
|
10471
11136
|
if (isDeployModel(m)) return m;
|
|
10472
11137
|
return resolveDeployModel(meta, repo);
|
|
10473
11138
|
}
|
|
10474
|
-
function
|
|
11139
|
+
function clean2(out) {
|
|
10475
11140
|
return out.trim();
|
|
10476
11141
|
}
|
|
10477
11142
|
function requireValue(value, label) {
|
|
@@ -10522,11 +11187,11 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
|
|
|
10522
11187
|
if (release.targetCommitish !== expectedTarget) {
|
|
10523
11188
|
throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
|
|
10524
11189
|
}
|
|
10525
|
-
const tagSha = requireValue(
|
|
11190
|
+
const tagSha = requireValue(clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
|
|
10526
11191
|
if (tagSha !== expectedSha) {
|
|
10527
11192
|
throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
|
|
10528
11193
|
}
|
|
10529
|
-
const branchOut =
|
|
11194
|
+
const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
|
|
10530
11195
|
const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
|
|
10531
11196
|
if (branchSha !== expectedSha) {
|
|
10532
11197
|
throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
|
|
@@ -10586,7 +11251,7 @@ function ensurePositiveCount(out, emptyMessage) {
|
|
|
10586
11251
|
if (count <= 0) throw new Error(emptyMessage);
|
|
10587
11252
|
}
|
|
10588
11253
|
async function remoteBranchExists(deps, branch) {
|
|
10589
|
-
const out =
|
|
11254
|
+
const out = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
10590
11255
|
return out.length > 0;
|
|
10591
11256
|
}
|
|
10592
11257
|
async function loadReleaseTrackBranchHints(deps) {
|
|
@@ -10598,10 +11263,10 @@ async function loadReleaseTrackBranchHints(deps) {
|
|
|
10598
11263
|
return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
|
|
10599
11264
|
}
|
|
10600
11265
|
async function buildTrainApplyContext(deps) {
|
|
10601
|
-
const repo = requireValue(
|
|
11266
|
+
const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
10602
11267
|
const [owner, name] = repo.split("/");
|
|
10603
11268
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
10604
|
-
const login = requireValue(
|
|
11269
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
10605
11270
|
const verdict = await deps.trainAuthority(repo);
|
|
10606
11271
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
10607
11272
|
if (!verdict.train) {
|
|
@@ -10622,7 +11287,7 @@ async function requireCleanTree(deps) {
|
|
|
10622
11287
|
if (status.trim()) throw new Error("working tree must be clean before train apply");
|
|
10623
11288
|
}
|
|
10624
11289
|
async function requireBranch(deps, branch) {
|
|
10625
|
-
const current =
|
|
11290
|
+
const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
10626
11291
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
10627
11292
|
}
|
|
10628
11293
|
var HUB_REPO3 = "mutmutco/MMI-Hub";
|
|
@@ -10791,7 +11456,7 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
|
|
|
10791
11456
|
return [...contexts];
|
|
10792
11457
|
}
|
|
10793
11458
|
async function findOpenAlignmentPr(deps, ctx) {
|
|
10794
|
-
const out =
|
|
11459
|
+
const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
|
|
10795
11460
|
if (!out) return void 0;
|
|
10796
11461
|
const rows = JSON.parse(out);
|
|
10797
11462
|
const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
|
|
@@ -10819,14 +11484,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10819
11484
|
note: `alignment PR already open: ${existing.url} \u2014 land it with \`gh pr merge ${existing.number} --merge\``
|
|
10820
11485
|
};
|
|
10821
11486
|
}
|
|
10822
|
-
const ahead =
|
|
11487
|
+
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
|
|
10823
11488
|
if (ahead === "0") {
|
|
10824
11489
|
return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
|
|
10825
11490
|
}
|
|
10826
11491
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
10827
11492
|
|
|
10828
11493
|
\`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 =
|
|
11494
|
+
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
11495
|
const number = parsePrNumber(url);
|
|
10831
11496
|
return {
|
|
10832
11497
|
status: "pr-pending",
|
|
@@ -10892,10 +11557,10 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10892
11557
|
}
|
|
10893
11558
|
async function ensureTagPushed(deps, tag, sha) {
|
|
10894
11559
|
const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
|
|
10895
|
-
const remoteSha =
|
|
11560
|
+
const remoteSha = clean2(remoteOut).split(/\s+/)[0] || "";
|
|
10896
11561
|
let localSha = "";
|
|
10897
11562
|
try {
|
|
10898
|
-
localSha =
|
|
11563
|
+
localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
|
|
10899
11564
|
} catch {
|
|
10900
11565
|
}
|
|
10901
11566
|
if (remoteSha) {
|
|
@@ -10918,7 +11583,7 @@ async function ensureTagPushed(deps, tag, sha) {
|
|
|
10918
11583
|
}
|
|
10919
11584
|
async function resolveRcResumeTag(deps, base2, sha) {
|
|
10920
11585
|
const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base2}-rc.*`]);
|
|
10921
|
-
const onSha =
|
|
11586
|
+
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
11587
|
const unique = [...new Set(onSha)];
|
|
10923
11588
|
if (unique.length === 0) return { tag: null };
|
|
10924
11589
|
const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
|
|
@@ -11043,13 +11708,13 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11043
11708
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
11044
11709
|
);
|
|
11045
11710
|
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
11046
|
-
const releaseBase = requireValue(
|
|
11711
|
+
const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
11047
11712
|
await deps.run("git", ["checkout", "rc"]);
|
|
11048
11713
|
await ffOnlyPull(deps, "rc");
|
|
11049
11714
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
11050
|
-
const rcSha = requireValue(
|
|
11715
|
+
const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
11051
11716
|
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
11052
|
-
const tag2 = resume.tag ?? requireValue(
|
|
11717
|
+
const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
11053
11718
|
const resumeNote = resume.tag ? resume.note : void 0;
|
|
11054
11719
|
await ensureTagPushed(deps, tag2, rcSha);
|
|
11055
11720
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
@@ -11067,7 +11732,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11067
11732
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
11068
11733
|
);
|
|
11069
11734
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
11070
|
-
const tag2 = requireValue(
|
|
11735
|
+
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
11071
11736
|
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
11072
11737
|
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
11073
11738
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -11085,12 +11750,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11085
11750
|
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
11086
11751
|
}
|
|
11087
11752
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
11088
|
-
const releaseSha2 = requireValue(
|
|
11753
|
+
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
11089
11754
|
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
11090
11755
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
11091
11756
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
11092
11757
|
await deps.run("git", ["push", "origin", "main"]);
|
|
11093
|
-
const releaseUrl2 =
|
|
11758
|
+
const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
11094
11759
|
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
11095
11760
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
11096
11761
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
@@ -11139,8 +11804,8 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11139
11804
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
11140
11805
|
);
|
|
11141
11806
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
11142
|
-
const tag2 = requireValue(
|
|
11143
|
-
const rcShaAtRelease = hasRcBranch ?
|
|
11807
|
+
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
11808
|
+
const rcShaAtRelease = hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
11144
11809
|
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
11145
11810
|
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
11146
11811
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -11158,12 +11823,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11158
11823
|
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
11159
11824
|
}
|
|
11160
11825
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
11161
|
-
const releaseSha2 = requireValue(
|
|
11826
|
+
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
11162
11827
|
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
11163
11828
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
11164
11829
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
11165
11830
|
await deps.run("git", ["push", "origin", "main"]);
|
|
11166
|
-
const releaseUrl2 =
|
|
11831
|
+
const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
11167
11832
|
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
11168
11833
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
11169
11834
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
@@ -11230,7 +11895,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11230
11895
|
`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
11896
|
);
|
|
11232
11897
|
}
|
|
11233
|
-
const releasedRcSha =
|
|
11898
|
+
const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
11234
11899
|
await deps.run("git", ["checkout", "main"]);
|
|
11235
11900
|
await ffOnlyPull(deps, "main");
|
|
11236
11901
|
if (predicted.length === 0) {
|
|
@@ -11238,15 +11903,15 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
11238
11903
|
} else {
|
|
11239
11904
|
await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", tolerated);
|
|
11240
11905
|
}
|
|
11241
|
-
const tag = requireValue(
|
|
11906
|
+
const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
11242
11907
|
const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
11243
|
-
const releaseSha = requireValue(
|
|
11908
|
+
const releaseSha = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
11244
11909
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
11245
11910
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
11246
11911
|
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
11247
11912
|
await deps.run("git", ["push", "origin", "main"]);
|
|
11248
11913
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
11249
|
-
const releaseUrl =
|
|
11914
|
+
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
11250
11915
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
11251
11916
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
11252
11917
|
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
|
|
@@ -11374,7 +12039,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
11374
12039
|
}
|
|
11375
12040
|
try {
|
|
11376
12041
|
await deps.run("git", ["fetch", "origin", "rc"]);
|
|
11377
|
-
const rcNow =
|
|
12042
|
+
const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
11378
12043
|
if (rcNow !== releasedRcSha) {
|
|
11379
12044
|
return {
|
|
11380
12045
|
status: "skipped",
|
|
@@ -11406,7 +12071,7 @@ async function runTenantRedeploy(deps, options) {
|
|
|
11406
12071
|
const repo = options.repo;
|
|
11407
12072
|
const [owner, name] = repo.split("/");
|
|
11408
12073
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
11409
|
-
const login = requireValue(
|
|
12074
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
11410
12075
|
const verdict = await deps.trainAuthority(repo);
|
|
11411
12076
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
11412
12077
|
if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
|
|
@@ -11562,7 +12227,7 @@ var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
|
11562
12227
|
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
11563
12228
|
var HOTFIX_VERIFY_ATTEMPTS = 5;
|
|
11564
12229
|
var HOTFIX_VERIFY_RETRY_MS = 6e3;
|
|
11565
|
-
function
|
|
12230
|
+
function clean3(out) {
|
|
11566
12231
|
return out.trim();
|
|
11567
12232
|
}
|
|
11568
12233
|
function sleeper(deps) {
|
|
@@ -11620,7 +12285,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
11620
12285
|
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
11621
12286
|
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
11622
12287
|
}
|
|
11623
|
-
const sha =
|
|
12288
|
+
const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
11624
12289
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
11625
12290
|
return { sha, label: sha.slice(0, 7) };
|
|
11626
12291
|
}
|
|
@@ -11648,7 +12313,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11648
12313
|
};
|
|
11649
12314
|
}
|
|
11650
12315
|
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
11651
|
-
const remoteBranch =
|
|
12316
|
+
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
11652
12317
|
if (remoteBranch) {
|
|
11653
12318
|
await deps.run("git", ["checkout", branch]);
|
|
11654
12319
|
const isRulesSource2 = deps.isRulesSource ? await deps.isRulesSource() : false;
|
|
@@ -11681,7 +12346,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11681
12346
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
11682
12347
|
}
|
|
11683
12348
|
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
|
|
11684
|
-
const prUrl =
|
|
12349
|
+
const prUrl = clean3(await deps.run("gh", [
|
|
11685
12350
|
"pr",
|
|
11686
12351
|
"create",
|
|
11687
12352
|
"--repo",
|
|
@@ -11801,7 +12466,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11801
12466
|
}
|
|
11802
12467
|
let verifyNote;
|
|
11803
12468
|
if (deployModel === "hub-serverless") {
|
|
11804
|
-
const previousRef =
|
|
12469
|
+
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
11805
12470
|
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
11806
12471
|
try {
|
|
11807
12472
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
@@ -11886,9 +12551,9 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
11886
12551
|
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
11887
12552
|
}
|
|
11888
12553
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
11889
|
-
const branchExists = Boolean(
|
|
12554
|
+
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
11890
12555
|
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
11891
|
-
const remoteTag =
|
|
12556
|
+
const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
11892
12557
|
const tagPushed = Boolean(remoteTag);
|
|
11893
12558
|
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
11894
12559
|
let releaseExists = false;
|
|
@@ -11922,7 +12587,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
11922
12587
|
}
|
|
11923
12588
|
}
|
|
11924
12589
|
}
|
|
11925
|
-
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(
|
|
12590
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
11926
12591
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
11927
12592
|
}
|
|
11928
12593
|
async function findInFlightHotfixVersion(deps, ctx) {
|
|
@@ -11945,7 +12610,7 @@ async function findInFlightHotfixVersion(deps, ctx) {
|
|
|
11945
12610
|
const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
|
|
11946
12611
|
if (m) tags.add(m[1]);
|
|
11947
12612
|
}
|
|
11948
|
-
const branchOut =
|
|
12613
|
+
const branchOut = clean3(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
|
|
11949
12614
|
for (const line of branchOut.split("\n").filter(Boolean)) {
|
|
11950
12615
|
const ref = line.split(/\s+/)[1] ?? "";
|
|
11951
12616
|
const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
|
|
@@ -12094,7 +12759,7 @@ async function announceRelease(deps, args) {
|
|
|
12094
12759
|
}
|
|
12095
12760
|
|
|
12096
12761
|
// src/port-registry.ts
|
|
12097
|
-
var
|
|
12762
|
+
var import_node_fs16 = require("node:fs");
|
|
12098
12763
|
|
|
12099
12764
|
// ../infra/port-geometry.mjs
|
|
12100
12765
|
var PORT_BLOCK = 100;
|
|
@@ -12108,8 +12773,8 @@ function nextPortBlock(registry2) {
|
|
|
12108
12773
|
return [base2, base2 + PORT_SPAN];
|
|
12109
12774
|
}
|
|
12110
12775
|
function loadPortRegistry(path2) {
|
|
12111
|
-
if (!(0,
|
|
12112
|
-
const raw = JSON.parse((0,
|
|
12776
|
+
if (!(0, import_node_fs16.existsSync)(path2)) return {};
|
|
12777
|
+
const raw = JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
|
|
12113
12778
|
const out = {};
|
|
12114
12779
|
for (const [key, value] of Object.entries(raw)) {
|
|
12115
12780
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -12123,9 +12788,9 @@ function ensurePortRange(repo, path2) {
|
|
|
12123
12788
|
const existing = registry2[repo];
|
|
12124
12789
|
if (existing) return existing;
|
|
12125
12790
|
const range = nextPortBlock(registry2);
|
|
12126
|
-
const raw = (0,
|
|
12791
|
+
const raw = (0, import_node_fs16.existsSync)(path2) ? JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8")) : {};
|
|
12127
12792
|
raw[repo] = range;
|
|
12128
|
-
(0,
|
|
12793
|
+
(0, import_node_fs16.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
12129
12794
|
return range;
|
|
12130
12795
|
}
|
|
12131
12796
|
function portCursorSeed(registry2) {
|
|
@@ -13919,8 +14584,8 @@ function resolveKbSource(rawBase) {
|
|
|
13919
14584
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
13920
14585
|
}
|
|
13921
14586
|
function buildKbGetArgs(src, path2) {
|
|
13922
|
-
const
|
|
13923
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
14587
|
+
const clean4 = path2.replace(/^\/+/, "");
|
|
14588
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
13924
14589
|
}
|
|
13925
14590
|
function buildKbTreeArgs(src) {
|
|
13926
14591
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -13937,10 +14602,10 @@ function parseKbTree(stdout, prefix) {
|
|
|
13937
14602
|
}
|
|
13938
14603
|
|
|
13939
14604
|
// src/plan.ts
|
|
13940
|
-
var
|
|
14605
|
+
var import_node_path15 = require("node:path");
|
|
13941
14606
|
var PLANS_DIR = "plans";
|
|
13942
|
-
var META_FILE = (0,
|
|
13943
|
-
var planPath = (slug) => (0,
|
|
14607
|
+
var META_FILE = (0, import_node_path15.join)(PLANS_DIR, ".plan-meta.json");
|
|
14608
|
+
var planPath = (slug) => (0, import_node_path15.join)(PLANS_DIR, `${slug}.md`);
|
|
13944
14609
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
13945
14610
|
function parseMeta(raw) {
|
|
13946
14611
|
if (!raw) return {};
|
|
@@ -13965,7 +14630,7 @@ function hashContent(s) {
|
|
|
13965
14630
|
function staleHint(slug) {
|
|
13966
14631
|
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
14632
|
}
|
|
13968
|
-
var INDEX_FILE = (0,
|
|
14633
|
+
var INDEX_FILE = (0, import_node_path15.join)(PLANS_DIR, ".index.json");
|
|
13969
14634
|
var INDEX_TTL_MS = 6e4;
|
|
13970
14635
|
function parseIndex(raw) {
|
|
13971
14636
|
if (!raw) return null;
|
|
@@ -13994,7 +14659,7 @@ function mergeIndex(idx, scope, plans, now) {
|
|
|
13994
14659
|
const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
|
|
13995
14660
|
return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
|
|
13996
14661
|
}
|
|
13997
|
-
var QUEUE_FILE = (0,
|
|
14662
|
+
var QUEUE_FILE = (0, import_node_path15.join)(PLANS_DIR, ".sync-queue.json");
|
|
13998
14663
|
var QUEUE_MAX_ATTEMPTS = 10;
|
|
13999
14664
|
function isValidQueueEntry(e) {
|
|
14000
14665
|
if (!e || typeof e !== "object") return false;
|
|
@@ -14443,11 +15108,11 @@ async function planGraduate(deps, slug, opts = {}) {
|
|
|
14443
15108
|
}
|
|
14444
15109
|
|
|
14445
15110
|
// src/atomic-write.ts
|
|
14446
|
-
var
|
|
15111
|
+
var import_node_fs17 = require("node:fs");
|
|
14447
15112
|
function atomicWriteFileSync(path2, content) {
|
|
14448
15113
|
const tmp = `${path2}.${process.pid}.tmp`;
|
|
14449
|
-
(0,
|
|
14450
|
-
(0,
|
|
15114
|
+
(0, import_node_fs17.writeFileSync)(tmp, content, "utf8");
|
|
15115
|
+
(0, import_node_fs17.renameSync)(tmp, path2);
|
|
14451
15116
|
}
|
|
14452
15117
|
|
|
14453
15118
|
// src/oauth.ts
|
|
@@ -14678,7 +15343,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
14678
15343
|
}
|
|
14679
15344
|
function readRepoVersion() {
|
|
14680
15345
|
try {
|
|
14681
|
-
return JSON.parse((0,
|
|
15346
|
+
return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
14682
15347
|
} catch {
|
|
14683
15348
|
return void 0;
|
|
14684
15349
|
}
|
|
@@ -14804,10 +15469,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
14804
15469
|
for (const entry of fetched) {
|
|
14805
15470
|
if ("error" in entry) continue;
|
|
14806
15471
|
const { file, source } = entry;
|
|
14807
|
-
const current = (0,
|
|
15472
|
+
const current = (0, import_node_fs18.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
|
|
14808
15473
|
if (needsUpdate(source, current)) {
|
|
14809
15474
|
const slash = file.lastIndexOf("/");
|
|
14810
|
-
if (slash > 0) (0,
|
|
15475
|
+
if (slash > 0) (0, import_node_fs18.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
14811
15476
|
await (0, import_promises5.writeFile)(file, normalizeEol(source), "utf8");
|
|
14812
15477
|
changed++;
|
|
14813
15478
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -14833,7 +15498,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
14833
15498
|
return null;
|
|
14834
15499
|
}
|
|
14835
15500
|
},
|
|
14836
|
-
localContent: async (f) => (0,
|
|
15501
|
+
localContent: async (f) => (0, import_node_fs18.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
|
|
14837
15502
|
writeDoc: async (f, c) => {
|
|
14838
15503
|
await (0, import_promises5.writeFile)(f, c, "utf8");
|
|
14839
15504
|
}
|
|
@@ -14845,6 +15510,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
14845
15510
|
var docs = program2.command("docs").description("repo-owned authoritative docs");
|
|
14846
15511
|
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
15512
|
registerSagaCommands(program2);
|
|
15513
|
+
registerHandoffCommands(program2);
|
|
14848
15514
|
registerHonchoCommands(program2);
|
|
14849
15515
|
async function runWhoami(io = consoleIo) {
|
|
14850
15516
|
const cfg = await loadConfig();
|
|
@@ -14858,8 +15524,18 @@ async function runWhoami(io = consoleIo) {
|
|
|
14858
15524
|
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
15525
|
await runWhoami();
|
|
14860
15526
|
});
|
|
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) => {
|
|
15527
|
+
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
15528
|
if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
|
|
15529
|
+
if (o.scratch) {
|
|
15530
|
+
try {
|
|
15531
|
+
const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
15532
|
+
const run = executeScratchGc(root, { apply: Boolean(o.apply) });
|
|
15533
|
+
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
|
|
15534
|
+
return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply)));
|
|
15535
|
+
} catch (e) {
|
|
15536
|
+
return fail(`gc --scratch: ${e.message}`);
|
|
15537
|
+
}
|
|
15538
|
+
}
|
|
14863
15539
|
const limit = Number.parseInt(o.limit, 10);
|
|
14864
15540
|
if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
|
|
14865
15541
|
try {
|
|
@@ -14887,6 +15563,120 @@ ${renderGcApplyResult(applyResult)}`);
|
|
|
14887
15563
|
fail(`gc: ${e.message}`);
|
|
14888
15564
|
}
|
|
14889
15565
|
});
|
|
15566
|
+
var NPM_PROVISION_TIMEOUT_MS = 3e5;
|
|
15567
|
+
var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
|
|
15568
|
+
function runWorktreeInstall(command, cwd, quiet) {
|
|
15569
|
+
const [bin, ...args] = command.split(" ");
|
|
15570
|
+
const file = isWin ? "cmd.exe" : bin;
|
|
15571
|
+
const spawnArgs = isWin ? ["/c", bin, ...args] : args;
|
|
15572
|
+
return new Promise((resolve, reject) => {
|
|
15573
|
+
const child = (0, import_node_child_process10.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
15574
|
+
const timer = setTimeout(() => {
|
|
15575
|
+
try {
|
|
15576
|
+
child.kill();
|
|
15577
|
+
} catch {
|
|
15578
|
+
}
|
|
15579
|
+
reject(new Error(`${command} timed out after ${NPM_PROVISION_TIMEOUT_MS}ms in ${cwd}`));
|
|
15580
|
+
}, NPM_PROVISION_TIMEOUT_MS);
|
|
15581
|
+
child.on("error", (e) => {
|
|
15582
|
+
clearTimeout(timer);
|
|
15583
|
+
reject(e);
|
|
15584
|
+
});
|
|
15585
|
+
child.on("exit", (code) => {
|
|
15586
|
+
clearTimeout(timer);
|
|
15587
|
+
if (code === 0) resolve();
|
|
15588
|
+
else reject(new Error(`${command} exited ${code} in ${cwd}`));
|
|
15589
|
+
});
|
|
15590
|
+
});
|
|
15591
|
+
}
|
|
15592
|
+
async function primaryCheckoutRoot(worktreeRoot) {
|
|
15593
|
+
try {
|
|
15594
|
+
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
15595
|
+
return out ? (0, import_node_path16.dirname)(out) : void 0;
|
|
15596
|
+
} catch {
|
|
15597
|
+
return void 0;
|
|
15598
|
+
}
|
|
15599
|
+
}
|
|
15600
|
+
function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
15601
|
+
return {
|
|
15602
|
+
runInstall: (command, cwd) => runWorktreeInstall(command, cwd, quiet),
|
|
15603
|
+
primaryCheckout: () => primaryCheckoutRoot(worktreeRoot),
|
|
15604
|
+
log
|
|
15605
|
+
};
|
|
15606
|
+
}
|
|
15607
|
+
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
15608
|
+
const lockPath = (0, import_node_path16.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
|
|
15609
|
+
const take = () => {
|
|
15610
|
+
const fd = (0, import_node_fs18.openSync)(lockPath, "wx");
|
|
15611
|
+
try {
|
|
15612
|
+
(0, import_node_fs18.writeSync)(fd, String(Date.now()));
|
|
15613
|
+
} finally {
|
|
15614
|
+
(0, import_node_fs18.closeSync)(fd);
|
|
15615
|
+
}
|
|
15616
|
+
return () => {
|
|
15617
|
+
try {
|
|
15618
|
+
(0, import_node_fs18.rmSync)(lockPath, { force: true });
|
|
15619
|
+
} catch {
|
|
15620
|
+
}
|
|
15621
|
+
};
|
|
15622
|
+
};
|
|
15623
|
+
try {
|
|
15624
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(lockPath), { recursive: true });
|
|
15625
|
+
return take();
|
|
15626
|
+
} catch {
|
|
15627
|
+
try {
|
|
15628
|
+
if (Date.now() - (0, import_node_fs18.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
15629
|
+
(0, import_node_fs18.rmSync)(lockPath, { force: true });
|
|
15630
|
+
return take();
|
|
15631
|
+
}
|
|
15632
|
+
} catch {
|
|
15633
|
+
}
|
|
15634
|
+
return null;
|
|
15635
|
+
}
|
|
15636
|
+
}
|
|
15637
|
+
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
15638
|
+
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) => {
|
|
15639
|
+
try {
|
|
15640
|
+
const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
15641
|
+
const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
|
|
15642
|
+
const baseBranch = o.from.startsWith(`${o.remote}/`) ? o.from.slice(o.remote.length + 1) : void 0;
|
|
15643
|
+
if (baseBranch) await execFileP2("git", ["fetch", o.remote, baseBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
|
|
15644
|
+
await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, o.from], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
15645
|
+
const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
|
|
15646
|
+
if (!o.json) console.error(` ${m}`);
|
|
15647
|
+
}));
|
|
15648
|
+
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base: o.from, ...report }, null, 2));
|
|
15649
|
+
console.log(`worktree ready: ${wtPath} (branch ${branch} from ${o.from})`);
|
|
15650
|
+
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
15651
|
+
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
15652
|
+
} catch (e) {
|
|
15653
|
+
fail(`worktree create: ${e.message}`);
|
|
15654
|
+
}
|
|
15655
|
+
});
|
|
15656
|
+
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) => {
|
|
15657
|
+
const root = path2 ?? process.cwd();
|
|
15658
|
+
const release = acquireWorktreeSetupLock(root);
|
|
15659
|
+
if (!release) {
|
|
15660
|
+
if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress \u2014 skipping");
|
|
15661
|
+
return;
|
|
15662
|
+
}
|
|
15663
|
+
try {
|
|
15664
|
+
const report = await provisionWorktree(root, makeProvisionDeps(root, Boolean(o.quiet), (m) => {
|
|
15665
|
+
if (!o.quiet && !o.json) console.error(` ${m}`);
|
|
15666
|
+
}));
|
|
15667
|
+
if (o.json) return console.log(JSON.stringify({ path: root, ...report }, null, 2));
|
|
15668
|
+
if (!o.quiet) {
|
|
15669
|
+
console.log(`worktree provisioned: ${root}`);
|
|
15670
|
+
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
15671
|
+
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
15672
|
+
}
|
|
15673
|
+
} catch (e) {
|
|
15674
|
+
if (o.quiet) return void console.error(`[worktree-setup] ${e.message}`);
|
|
15675
|
+
fail(`worktree setup: ${e.message}`);
|
|
15676
|
+
} finally {
|
|
15677
|
+
release();
|
|
15678
|
+
}
|
|
15679
|
+
});
|
|
14890
15680
|
var kb = program2.command("kb").description("org knowledgebase (read-only)");
|
|
14891
15681
|
kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
|
|
14892
15682
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
@@ -15006,7 +15796,7 @@ function detachPlanSync() {
|
|
|
15006
15796
|
}
|
|
15007
15797
|
}
|
|
15008
15798
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
15009
|
-
const ensureDir = () => (0,
|
|
15799
|
+
const ensureDir = () => (0, import_node_fs18.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
15010
15800
|
return {
|
|
15011
15801
|
apiUrl: cfg.sagaApiUrl,
|
|
15012
15802
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
@@ -15014,24 +15804,24 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
15014
15804
|
project: async () => (await sagaKey(cfg)).project,
|
|
15015
15805
|
readLocal: (slug) => {
|
|
15016
15806
|
try {
|
|
15017
|
-
return (0,
|
|
15807
|
+
return (0, import_node_fs18.readFileSync)(planPath(slug), "utf8");
|
|
15018
15808
|
} catch {
|
|
15019
15809
|
return null;
|
|
15020
15810
|
}
|
|
15021
15811
|
},
|
|
15022
15812
|
writeLocal: (slug, content) => {
|
|
15023
15813
|
ensureDir();
|
|
15024
|
-
(0,
|
|
15814
|
+
(0, import_node_fs18.writeFileSync)(planPath(slug), content, "utf8");
|
|
15025
15815
|
},
|
|
15026
15816
|
removeLocal: (slug) => {
|
|
15027
15817
|
try {
|
|
15028
|
-
(0,
|
|
15818
|
+
(0, import_node_fs18.rmSync)(planPath(slug));
|
|
15029
15819
|
} catch {
|
|
15030
15820
|
}
|
|
15031
15821
|
},
|
|
15032
15822
|
readMetaRaw: () => {
|
|
15033
15823
|
try {
|
|
15034
|
-
return (0,
|
|
15824
|
+
return (0, import_node_fs18.readFileSync)(META_FILE, "utf8");
|
|
15035
15825
|
} catch {
|
|
15036
15826
|
return null;
|
|
15037
15827
|
}
|
|
@@ -15042,7 +15832,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
15042
15832
|
},
|
|
15043
15833
|
readIndexRaw: () => {
|
|
15044
15834
|
try {
|
|
15045
|
-
return (0,
|
|
15835
|
+
return (0, import_node_fs18.readFileSync)(INDEX_FILE, "utf8");
|
|
15046
15836
|
} catch {
|
|
15047
15837
|
return null;
|
|
15048
15838
|
}
|
|
@@ -15053,7 +15843,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
15053
15843
|
},
|
|
15054
15844
|
readQueueRaw: () => {
|
|
15055
15845
|
try {
|
|
15056
|
-
return (0,
|
|
15846
|
+
return (0, import_node_fs18.readFileSync)(QUEUE_FILE, "utf8");
|
|
15057
15847
|
} catch {
|
|
15058
15848
|
return null;
|
|
15059
15849
|
}
|
|
@@ -15622,7 +16412,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
15622
16412
|
if (mismatch) process.exitCode = 1;
|
|
15623
16413
|
});
|
|
15624
16414
|
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 -").
|
|
16415
|
+
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
16416
|
let args;
|
|
15627
16417
|
let priority;
|
|
15628
16418
|
let body;
|
|
@@ -15630,6 +16420,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
15630
16420
|
try {
|
|
15631
16421
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
|
|
15632
16422
|
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
|
|
16423
|
+
if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
|
|
15633
16424
|
priority = normalizePriority(o.priority);
|
|
15634
16425
|
args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
|
|
15635
16426
|
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
@@ -15980,9 +16771,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
15980
16771
|
console.log(JSON.stringify(created));
|
|
15981
16772
|
});
|
|
15982
16773
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
15983
|
-
const wfDir = (0,
|
|
15984
|
-
if (!(0,
|
|
15985
|
-
return (0,
|
|
16774
|
+
const wfDir = (0, import_node_path16.join)(cwd, ".github", "workflows");
|
|
16775
|
+
if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
|
|
16776
|
+
return (0, import_node_fs18.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
15986
16777
|
}
|
|
15987
16778
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
15988
16779
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -16104,7 +16895,7 @@ async function createDeferredWorktreeStore() {
|
|
|
16104
16895
|
},
|
|
16105
16896
|
write: async (entries) => {
|
|
16106
16897
|
try {
|
|
16107
|
-
await (0, import_promises5.mkdir)((0,
|
|
16898
|
+
await (0, import_promises5.mkdir)((0, import_node_path16.dirname)(registryPath), { recursive: true });
|
|
16108
16899
|
await (0, import_promises5.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
16109
16900
|
} catch {
|
|
16110
16901
|
}
|
|
@@ -16123,7 +16914,7 @@ function worktreeRemoveDeps(execGit) {
|
|
|
16123
16914
|
}
|
|
16124
16915
|
function teardownWorktreeStage(worktreePath) {
|
|
16125
16916
|
return runWorktreeStageTeardown(worktreePath, {
|
|
16126
|
-
hasStageState: (wt) => (0,
|
|
16917
|
+
hasStageState: (wt) => (0, import_node_fs18.existsSync)(stageStatePath(wt)),
|
|
16127
16918
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
16128
16919
|
listComposeProjects: async () => {
|
|
16129
16920
|
const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -16186,7 +16977,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
16186
16977
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
16187
16978
|
beforeWorktrees,
|
|
16188
16979
|
startingPath,
|
|
16189
|
-
pathExists: (p) => (0,
|
|
16980
|
+
pathExists: (p) => (0, import_node_fs18.existsSync)(p),
|
|
16190
16981
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
16191
16982
|
teardownWorktreeStage,
|
|
16192
16983
|
deferredStore,
|
|
@@ -16358,7 +17149,7 @@ function rawValues(flag) {
|
|
|
16358
17149
|
return out;
|
|
16359
17150
|
}
|
|
16360
17151
|
function printLine(value) {
|
|
16361
|
-
(0,
|
|
17152
|
+
(0, import_node_fs18.writeSync)(1, `${value}
|
|
16362
17153
|
`);
|
|
16363
17154
|
}
|
|
16364
17155
|
function stageKeepAlive() {
|
|
@@ -16375,8 +17166,8 @@ async function resolveStage() {
|
|
|
16375
17166
|
local,
|
|
16376
17167
|
shell: shellFor(),
|
|
16377
17168
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
16378
|
-
hasCompose: (0,
|
|
16379
|
-
hasEnvExample: (0,
|
|
17169
|
+
hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path16.join)(process.cwd(), "docker-compose.yml")),
|
|
17170
|
+
hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path16.join)(process.cwd(), ".env.example"))
|
|
16380
17171
|
});
|
|
16381
17172
|
}
|
|
16382
17173
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -16428,9 +17219,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
16428
17219
|
printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
|
|
16429
17220
|
return;
|
|
16430
17221
|
}
|
|
16431
|
-
const path2 = (0,
|
|
17222
|
+
const path2 = (0, import_node_path16.join)(process.cwd(), "infra", "port-ranges.json");
|
|
16432
17223
|
const allocate = async (seed) => {
|
|
16433
|
-
const { stdout } = await execFileP2("node", [(0,
|
|
17224
|
+
const { stdout } = await execFileP2("node", [(0, import_node_path16.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
16434
17225
|
const parsed = JSON.parse(stdout);
|
|
16435
17226
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
16436
17227
|
return parsed.range;
|
|
@@ -16801,7 +17592,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
16801
17592
|
const report = await verifyBootstrap(repo, o.class, {
|
|
16802
17593
|
client: defaultGitHubClient(),
|
|
16803
17594
|
projectMeta: meta,
|
|
16804
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
17595
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
|
|
16805
17596
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
16806
17597
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
16807
17598
|
requiredGcpApis: (() => {
|
|
@@ -16844,12 +17635,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16844
17635
|
return fail(`bootstrap apply: ${e.message}`);
|
|
16845
17636
|
}
|
|
16846
17637
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
16847
|
-
if (!(0,
|
|
16848
|
-
const manifest = loadBootstrapSeeds((0,
|
|
17638
|
+
if (!(0, import_node_fs18.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
17639
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs18.readFileSync)(manifestPath, "utf8"));
|
|
16849
17640
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
16850
17641
|
const slug = parsedRepo.slug;
|
|
16851
17642
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
16852
|
-
const readFile5 = (p) => (0,
|
|
17643
|
+
const readFile5 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
|
|
16853
17644
|
const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
16854
17645
|
const rawVars = {};
|
|
16855
17646
|
for (const value of rawValues("--var")) {
|
|
@@ -17078,16 +17869,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
17078
17869
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
17079
17870
|
targets = [{ repo: o.repo, class: o.class }];
|
|
17080
17871
|
} else {
|
|
17081
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
17872
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs18.existsSync)("projects.json") ? (0, import_node_fs18.readFileSync)("projects.json", "utf8") : null;
|
|
17082
17873
|
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,
|
|
17874
|
+
const fanoutJson = (0, import_node_fs18.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs18.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
17084
17875
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
17085
17876
|
}
|
|
17086
17877
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
17087
|
-
const fileMatrix = (0,
|
|
17878
|
+
const fileMatrix = (0, import_node_fs18.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs18.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
17088
17879
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
17089
17880
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
17090
|
-
const fileContracts = (0,
|
|
17881
|
+
const fileContracts = (0, import_node_fs18.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs18.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
17091
17882
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
17092
17883
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
17093
17884
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -17096,20 +17887,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
17096
17887
|
var isWin = process.platform === "win32";
|
|
17097
17888
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
17098
17889
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
17099
|
-
return (0,
|
|
17890
|
+
return (0, import_node_path16.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
17100
17891
|
};
|
|
17101
17892
|
function readInstalledPlugins() {
|
|
17102
17893
|
try {
|
|
17103
|
-
return JSON.parse((0,
|
|
17894
|
+
return JSON.parse((0, import_node_fs18.readFileSync)(installedPluginsPath(), "utf8"));
|
|
17104
17895
|
} catch {
|
|
17105
17896
|
return null;
|
|
17106
17897
|
}
|
|
17107
17898
|
}
|
|
17108
17899
|
function installedPluginSources() {
|
|
17109
17900
|
return ["claude", "codex"].map((surface) => {
|
|
17110
|
-
const recordPath = (0,
|
|
17901
|
+
const recordPath = (0, import_node_path16.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
17111
17902
|
try {
|
|
17112
|
-
return { surface, installed: JSON.parse((0,
|
|
17903
|
+
return { surface, installed: JSON.parse((0, import_node_fs18.readFileSync)(recordPath, "utf8")), recordPath };
|
|
17113
17904
|
} catch {
|
|
17114
17905
|
return { surface, installed: null, recordPath };
|
|
17115
17906
|
}
|
|
@@ -17117,7 +17908,7 @@ function installedPluginSources() {
|
|
|
17117
17908
|
}
|
|
17118
17909
|
function readClaudeSettings() {
|
|
17119
17910
|
try {
|
|
17120
|
-
return JSON.parse((0,
|
|
17911
|
+
return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
17121
17912
|
} catch {
|
|
17122
17913
|
return null;
|
|
17123
17914
|
}
|
|
@@ -17139,7 +17930,7 @@ function writeProjectInstallRecord(record) {
|
|
|
17139
17930
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
17140
17931
|
list.push(record);
|
|
17141
17932
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
17142
|
-
(0,
|
|
17933
|
+
(0, import_node_fs18.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
17143
17934
|
`, "utf8");
|
|
17144
17935
|
return true;
|
|
17145
17936
|
} catch {
|
|
@@ -17152,9 +17943,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
17152
17943
|
if (!file) return false;
|
|
17153
17944
|
if (!file.plugins) file.plugins = {};
|
|
17154
17945
|
const path2 = installedPluginsPath();
|
|
17155
|
-
(0,
|
|
17946
|
+
(0, import_node_fs18.copyFileSync)(path2, `${path2}.bak`);
|
|
17156
17947
|
file.plugins[pluginId] = records;
|
|
17157
|
-
(0,
|
|
17948
|
+
(0, import_node_fs18.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
17158
17949
|
`, "utf8");
|
|
17159
17950
|
return true;
|
|
17160
17951
|
} catch {
|
|
@@ -17162,35 +17953,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
17162
17953
|
}
|
|
17163
17954
|
}
|
|
17164
17955
|
function cursorPluginCacheRoot() {
|
|
17165
|
-
return (0,
|
|
17956
|
+
return (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
|
|
17166
17957
|
}
|
|
17167
17958
|
function cursorPluginCachePinSnapshots() {
|
|
17168
17959
|
const root = cursorPluginCacheRoot();
|
|
17169
17960
|
try {
|
|
17170
|
-
return (0,
|
|
17171
|
-
const path2 = (0,
|
|
17172
|
-
const pluginJson = (0,
|
|
17173
|
-
const hooksJson = (0,
|
|
17174
|
-
const cliBundle = (0,
|
|
17961
|
+
return (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
17962
|
+
const path2 = (0, import_node_path16.join)(root, entry.name);
|
|
17963
|
+
const pluginJson = (0, import_node_path16.join)(path2, ".cursor-plugin", "plugin.json");
|
|
17964
|
+
const hooksJson = (0, import_node_path16.join)(path2, "hooks", "hooks.json");
|
|
17965
|
+
const cliBundle = (0, import_node_path16.join)(path2, "cli", "dist", "index.cjs");
|
|
17175
17966
|
let version;
|
|
17176
17967
|
try {
|
|
17177
|
-
const raw = JSON.parse((0,
|
|
17968
|
+
const raw = JSON.parse((0, import_node_fs18.readFileSync)(pluginJson, "utf8"));
|
|
17178
17969
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
17179
17970
|
} catch {
|
|
17180
17971
|
version = void 0;
|
|
17181
17972
|
}
|
|
17182
17973
|
let isEmpty = true;
|
|
17183
17974
|
try {
|
|
17184
|
-
isEmpty = (0,
|
|
17975
|
+
isEmpty = (0, import_node_fs18.readdirSync)(path2).length === 0;
|
|
17185
17976
|
} catch {
|
|
17186
17977
|
isEmpty = true;
|
|
17187
17978
|
}
|
|
17188
17979
|
return {
|
|
17189
17980
|
name: entry.name,
|
|
17190
17981
|
path: path2,
|
|
17191
|
-
hasPluginJson: (0,
|
|
17192
|
-
hasHooksJson: (0,
|
|
17193
|
-
hasCliBundle: (0,
|
|
17982
|
+
hasPluginJson: (0, import_node_fs18.existsSync)(pluginJson),
|
|
17983
|
+
hasHooksJson: (0, import_node_fs18.existsSync)(hooksJson),
|
|
17984
|
+
hasCliBundle: (0, import_node_fs18.existsSync)(cliBundle),
|
|
17194
17985
|
isEmpty,
|
|
17195
17986
|
version
|
|
17196
17987
|
};
|
|
@@ -17200,19 +17991,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
17200
17991
|
}
|
|
17201
17992
|
}
|
|
17202
17993
|
function hubCheckoutForCursorSeed() {
|
|
17203
|
-
const manifest = (0,
|
|
17204
|
-
return (0,
|
|
17994
|
+
const manifest = (0, import_node_path16.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
17995
|
+
return (0, import_node_fs18.existsSync)(manifest) ? process.cwd() : void 0;
|
|
17205
17996
|
}
|
|
17206
17997
|
function mmiPluginCacheRootSnapshots() {
|
|
17207
17998
|
const roots = [
|
|
17208
|
-
{ surface: "claude", root: (0,
|
|
17209
|
-
{ surface: "codex", root: (0,
|
|
17999
|
+
{ surface: "claude", root: (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
18000
|
+
{ surface: "codex", root: (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
17210
18001
|
];
|
|
17211
18002
|
return roots.flatMap(({ surface, root }) => {
|
|
17212
18003
|
try {
|
|
17213
|
-
const entries = (0,
|
|
18004
|
+
const entries = (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
17214
18005
|
name: entry.name,
|
|
17215
|
-
path: (0,
|
|
18006
|
+
path: (0, import_node_path16.join)(root, entry.name),
|
|
17216
18007
|
isDirectory: entry.isDirectory()
|
|
17217
18008
|
}));
|
|
17218
18009
|
return [{ surface, root, entries }];
|
|
@@ -17223,7 +18014,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
17223
18014
|
}
|
|
17224
18015
|
function hasNestedMmiChild(versionDir) {
|
|
17225
18016
|
try {
|
|
17226
|
-
return (0,
|
|
18017
|
+
return (0, import_node_fs18.statSync)((0, import_node_path16.join)(versionDir, "mmi")).isDirectory();
|
|
17227
18018
|
} catch {
|
|
17228
18019
|
return false;
|
|
17229
18020
|
}
|
|
@@ -17234,26 +18025,28 @@ function nestedPluginTreeSnapshot() {
|
|
|
17234
18025
|
);
|
|
17235
18026
|
}
|
|
17236
18027
|
function uniqueQuarantineTarget(path2) {
|
|
17237
|
-
if (!(0,
|
|
18028
|
+
if (!(0, import_node_fs18.existsSync)(path2)) return path2;
|
|
17238
18029
|
for (let i = 1; i < 100; i += 1) {
|
|
17239
18030
|
const candidate = `${path2}-${i}`;
|
|
17240
|
-
if (!(0,
|
|
18031
|
+
if (!(0, import_node_fs18.existsSync)(candidate)) return candidate;
|
|
17241
18032
|
}
|
|
17242
18033
|
return `${path2}-${Date.now()}`;
|
|
17243
18034
|
}
|
|
17244
18035
|
function quarantinePluginCacheDirs(plan2) {
|
|
17245
18036
|
let moved = 0;
|
|
18037
|
+
const failed = [];
|
|
17246
18038
|
for (const move of plan2) {
|
|
17247
18039
|
try {
|
|
17248
|
-
if (!(0,
|
|
18040
|
+
if (!(0, import_node_fs18.existsSync)(move.from)) continue;
|
|
17249
18041
|
const target = uniqueQuarantineTarget(move.to);
|
|
17250
|
-
(0,
|
|
17251
|
-
(0,
|
|
18042
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(target), { recursive: true });
|
|
18043
|
+
(0, import_node_fs18.renameSync)(move.from, target);
|
|
17252
18044
|
moved += 1;
|
|
17253
18045
|
} catch {
|
|
18046
|
+
failed.push(move);
|
|
17254
18047
|
}
|
|
17255
18048
|
}
|
|
17256
|
-
return moved;
|
|
18049
|
+
return { moved, failed };
|
|
17257
18050
|
}
|
|
17258
18051
|
async function robocopyMirrorEmpty(emptyDir, target) {
|
|
17259
18052
|
try {
|
|
@@ -17266,23 +18059,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
17266
18059
|
}
|
|
17267
18060
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
17268
18061
|
try {
|
|
17269
|
-
if (!(0,
|
|
18062
|
+
if (!(0, import_node_fs18.existsSync)(targetPath)) return true;
|
|
17270
18063
|
if (isWin) {
|
|
17271
|
-
const emptyDir = (0,
|
|
17272
|
-
(0,
|
|
18064
|
+
const emptyDir = (0, import_node_path16.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
18065
|
+
(0, import_node_fs18.mkdirSync)(emptyDir, { recursive: true });
|
|
17273
18066
|
try {
|
|
17274
18067
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
17275
|
-
(0,
|
|
18068
|
+
(0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
|
|
17276
18069
|
} finally {
|
|
17277
18070
|
try {
|
|
17278
|
-
(0,
|
|
18071
|
+
(0, import_node_fs18.rmSync)(emptyDir, { recursive: true, force: true });
|
|
17279
18072
|
} catch {
|
|
17280
18073
|
}
|
|
17281
18074
|
}
|
|
17282
|
-
return !(0,
|
|
18075
|
+
return !(0, import_node_fs18.existsSync)(targetPath);
|
|
17283
18076
|
}
|
|
17284
|
-
(0,
|
|
17285
|
-
return !(0,
|
|
18077
|
+
(0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
|
|
18078
|
+
return !(0, import_node_fs18.existsSync)(targetPath);
|
|
17286
18079
|
} catch {
|
|
17287
18080
|
return false;
|
|
17288
18081
|
}
|
|
@@ -17295,11 +18088,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
17295
18088
|
}
|
|
17296
18089
|
return true;
|
|
17297
18090
|
}
|
|
17298
|
-
var gitignorePath = () => (0,
|
|
18091
|
+
var gitignorePath = () => (0, import_node_path16.join)(process.cwd(), ".gitignore");
|
|
17299
18092
|
function readTextFile(path2) {
|
|
17300
18093
|
try {
|
|
17301
|
-
if (!(0,
|
|
17302
|
-
return (0,
|
|
18094
|
+
if (!(0, import_node_fs18.existsSync)(path2)) return null;
|
|
18095
|
+
return (0, import_node_fs18.readFileSync)(path2, "utf8");
|
|
17303
18096
|
} catch {
|
|
17304
18097
|
return null;
|
|
17305
18098
|
}
|
|
@@ -17308,9 +18101,9 @@ function playwrightMcpConfigSnapshots() {
|
|
|
17308
18101
|
const cwd = process.cwd();
|
|
17309
18102
|
const home = (0, import_node_os5.homedir)();
|
|
17310
18103
|
const candidates = [
|
|
17311
|
-
(0,
|
|
17312
|
-
(0,
|
|
17313
|
-
(0,
|
|
18104
|
+
(0, import_node_path16.join)(cwd, ".cursor", "mcp.json"),
|
|
18105
|
+
(0, import_node_path16.join)(home, ".cursor", "mcp.json"),
|
|
18106
|
+
(0, import_node_path16.join)(home, ".codex", "config.toml")
|
|
17314
18107
|
];
|
|
17315
18108
|
const out = [];
|
|
17316
18109
|
for (const path2 of candidates) {
|
|
@@ -17323,7 +18116,7 @@ function strayBrowserArtifactPaths() {
|
|
|
17323
18116
|
const cwd = process.cwd();
|
|
17324
18117
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
17325
18118
|
try {
|
|
17326
|
-
return (0,
|
|
18119
|
+
return (0, import_node_fs18.existsSync)((0, import_node_path16.join)(cwd, rel));
|
|
17327
18120
|
} catch {
|
|
17328
18121
|
return false;
|
|
17329
18122
|
}
|
|
@@ -17331,14 +18124,14 @@ function strayBrowserArtifactPaths() {
|
|
|
17331
18124
|
}
|
|
17332
18125
|
function readGitignore() {
|
|
17333
18126
|
try {
|
|
17334
|
-
return (0,
|
|
18127
|
+
return (0, import_node_fs18.readFileSync)(gitignorePath(), "utf8");
|
|
17335
18128
|
} catch {
|
|
17336
18129
|
return null;
|
|
17337
18130
|
}
|
|
17338
18131
|
}
|
|
17339
18132
|
function writeGitignore(content) {
|
|
17340
18133
|
try {
|
|
17341
|
-
(0,
|
|
18134
|
+
(0, import_node_fs18.writeFileSync)(gitignorePath(), content, "utf8");
|
|
17342
18135
|
return true;
|
|
17343
18136
|
} catch {
|
|
17344
18137
|
return false;
|
|
@@ -17377,7 +18170,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17377
18170
|
let onPath = pathProbe;
|
|
17378
18171
|
if (!onPath) {
|
|
17379
18172
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
17380
|
-
if (root && (0,
|
|
18173
|
+
if (root && (0, import_node_fs18.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
17381
18174
|
}
|
|
17382
18175
|
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
18176
|
const surface = detectSurface(process.env);
|
|
@@ -17494,7 +18287,8 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17494
18287
|
installedVersions: installedPluginVersions(installed)
|
|
17495
18288
|
});
|
|
17496
18289
|
if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && repairLocal) {
|
|
17497
|
-
const moved = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
18290
|
+
const { moved, failed } = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
18291
|
+
const attempted = moved + failed.length;
|
|
17498
18292
|
if (moved > 0) {
|
|
17499
18293
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
17500
18294
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
@@ -17505,8 +18299,12 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17505
18299
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
17506
18300
|
roots: mmiPluginCacheRootSnapshots(),
|
|
17507
18301
|
activeVersion: resolveClientVersion(),
|
|
17508
|
-
releasedVersion
|
|
18302
|
+
releasedVersion,
|
|
18303
|
+
installedVersions: installedPluginVersions(installed)
|
|
17509
18304
|
}),
|
|
18305
|
+
attemptedCount: attempted,
|
|
18306
|
+
failedCount: failed.length,
|
|
18307
|
+
...failed.length > 0 ? { failedMoves: failed } : {},
|
|
17510
18308
|
...moved > 0 ? { cleanedCount: moved } : {}
|
|
17511
18309
|
};
|
|
17512
18310
|
}
|
|
@@ -17544,7 +18342,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17544
18342
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
17545
18343
|
surface,
|
|
17546
18344
|
cacheRoot: cursorCacheRoot,
|
|
17547
|
-
cacheRootExists: (0,
|
|
18345
|
+
cacheRootExists: (0, import_node_fs18.existsSync)(cursorCacheRoot),
|
|
17548
18346
|
pins: cursorPins,
|
|
17549
18347
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17550
18348
|
releasedVersion
|
|
@@ -17555,7 +18353,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17555
18353
|
releasedVersion,
|
|
17556
18354
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17557
18355
|
execFileP: execFileP2,
|
|
17558
|
-
mkdtemp: (prefix) => (0, import_promises5.mkdtemp)((0,
|
|
18356
|
+
mkdtemp: (prefix) => (0, import_promises5.mkdtemp)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), prefix)),
|
|
17559
18357
|
log: (m) => io.err(m)
|
|
17560
18358
|
});
|
|
17561
18359
|
if (seeded) {
|
|
@@ -17564,7 +18362,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
17564
18362
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
17565
18363
|
surface,
|
|
17566
18364
|
cacheRoot: cursorCacheRoot,
|
|
17567
|
-
cacheRootExists: (0,
|
|
18365
|
+
cacheRootExists: (0, import_node_fs18.existsSync)(cursorCacheRoot),
|
|
17568
18366
|
pins: cursorPins,
|
|
17569
18367
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17570
18368
|
releasedVersion
|
|
@@ -17647,6 +18445,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
17647
18445
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
17648
18446
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
17649
18447
|
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
18448
|
+
handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
|
|
17650
18449
|
// honcho profile (#1162): inject the behavioral-memory prior (peer card). Bounded + fail-soft +
|
|
17651
18450
|
// silent when off/empty, so it never delays or noises the session banner.
|
|
17652
18451
|
honchoContext: (io) => runHonchoContext({ quiet: true, banner: true }, io),
|
|
@@ -17680,6 +18479,12 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
17680
18479
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
17681
18480
|
consoleIo.log(northstarPointer(northstarInjected));
|
|
17682
18481
|
for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
|
|
18482
|
+
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
18483
|
+
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
18484
|
+
if (worktreeBanner) {
|
|
18485
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
18486
|
+
consoleIo.log(worktreeBanner);
|
|
18487
|
+
}
|
|
17683
18488
|
});
|
|
17684
18489
|
installProcessBackstop();
|
|
17685
18490
|
program2.parseAsync().catch((e) => failGraceful(e.message));
|