@mutmutco/cli 2.12.0 → 2.14.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 +9 -3
- package/dist/index.cjs +953 -150
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3393,8 +3393,8 @@ var program = new Command();
|
|
|
3393
3393
|
|
|
3394
3394
|
// src/index.ts
|
|
3395
3395
|
var import_promises2 = require("node:fs/promises");
|
|
3396
|
-
var
|
|
3397
|
-
var
|
|
3396
|
+
var import_node_fs6 = require("node:fs");
|
|
3397
|
+
var import_node_crypto3 = require("node:crypto");
|
|
3398
3398
|
|
|
3399
3399
|
// src/rules-sync.ts
|
|
3400
3400
|
function normalizeEol(s) {
|
|
@@ -3501,8 +3501,8 @@ function parseHookInput(stdin) {
|
|
|
3501
3501
|
// src/index.ts
|
|
3502
3502
|
var import_node_child_process6 = require("node:child_process");
|
|
3503
3503
|
var import_node_util6 = require("node:util");
|
|
3504
|
-
var
|
|
3505
|
-
var
|
|
3504
|
+
var import_node_path7 = require("node:path");
|
|
3505
|
+
var import_node_os3 = require("node:os");
|
|
3506
3506
|
|
|
3507
3507
|
// src/saga-head-maintainer.ts
|
|
3508
3508
|
var import_node_child_process2 = require("node:child_process");
|
|
@@ -3827,10 +3827,10 @@ function defaultHubUrl() {
|
|
|
3827
3827
|
function buildHealth(i) {
|
|
3828
3828
|
const problems = [];
|
|
3829
3829
|
if (!i.sagaApiUrl) problems.push("Hub API URL not configured");
|
|
3830
|
-
if (!i.identity) problems.push("no
|
|
3830
|
+
if (!i.identity) problems.push("no Hub session identity (run `gh auth login`, then retry)");
|
|
3831
3831
|
if (!i.reachable) problems.push("saga backend unreachable");
|
|
3832
3832
|
if (i.reachable && i.livenessStatus === 403 && i.livenessMessage === "Forbidden") {
|
|
3833
|
-
problems.push("saga API route-level 403 from
|
|
3833
|
+
problems.push("saga API route-level 403 from HubSessionAuthorizer/session policy");
|
|
3834
3834
|
}
|
|
3835
3835
|
if (i.reachable && i.authorized === false) problems.push("saga backend rejected authenticated state access");
|
|
3836
3836
|
if (!i.key.sessionId || i.key.sessionId === "-") problems.push("unsafe session id");
|
|
@@ -3887,7 +3887,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3887
3887
|
|
|
3888
3888
|
// src/saga-note.ts
|
|
3889
3889
|
var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
|
|
3890
|
-
var ROUTE_LEVEL_403 = "saga API route-level 403 from
|
|
3890
|
+
var ROUTE_LEVEL_403 = "saga API route-level 403 from HubSessionAuthorizer/session policy";
|
|
3891
3891
|
function agentSurface(env = process.env) {
|
|
3892
3892
|
const surface = env.MMI_AGENT_SURFACE?.trim() || (env.CODEX_THREAD_ID?.trim() && !env.CLAUDE_SESSION_ID?.trim() ? "codex" : "claude");
|
|
3893
3893
|
if (AGENT_SURFACE_TOKENS.includes(surface)) return surface;
|
|
@@ -4064,6 +4064,72 @@ Related work discovered by mmi-cli:
|
|
|
4064
4064
|
${lines.join("\n")}`;
|
|
4065
4065
|
}
|
|
4066
4066
|
|
|
4067
|
+
// src/sub-issue.ts
|
|
4068
|
+
function parseIssueRef(ref) {
|
|
4069
|
+
const trimmed = ref.trim();
|
|
4070
|
+
const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/i);
|
|
4071
|
+
if (url) return { repo: url[1], number: Number(url[2]) };
|
|
4072
|
+
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
4073
|
+
if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
|
|
4074
|
+
const bare = trimmed.match(/^#?(\d+)$/);
|
|
4075
|
+
if (bare) return { number: Number(bare[1]) };
|
|
4076
|
+
throw new Error(`invalid issue reference "${ref}" \u2014 expected #123, 123, owner/repo#123, or an issue URL`);
|
|
4077
|
+
}
|
|
4078
|
+
function buildResolveIdArgs(ref) {
|
|
4079
|
+
const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
|
|
4080
|
+
if (ref.repo) args.push("--repo", ref.repo);
|
|
4081
|
+
return args;
|
|
4082
|
+
}
|
|
4083
|
+
function buildAddSubIssueArgs(parentId, subIssueId) {
|
|
4084
|
+
if (!parentId) throw new Error("addSubIssue: parentId is required");
|
|
4085
|
+
if (!subIssueId) throw new Error("addSubIssue: subIssueId is required");
|
|
4086
|
+
return [
|
|
4087
|
+
"api",
|
|
4088
|
+
"graphql",
|
|
4089
|
+
"-f",
|
|
4090
|
+
"query=mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
|
|
4091
|
+
"-f",
|
|
4092
|
+
`p=${parentId}`,
|
|
4093
|
+
"-f",
|
|
4094
|
+
`c=${subIssueId}`
|
|
4095
|
+
];
|
|
4096
|
+
}
|
|
4097
|
+
function parseAddSubIssueResult(stdout) {
|
|
4098
|
+
try {
|
|
4099
|
+
const issue2 = JSON.parse(stdout)?.data?.addSubIssue;
|
|
4100
|
+
const parentNumber = issue2?.issue?.number;
|
|
4101
|
+
const subIssueNumber = issue2?.subIssue?.number;
|
|
4102
|
+
const totalCount = issue2?.issue?.subIssues?.totalCount;
|
|
4103
|
+
if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
|
|
4104
|
+
return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
|
|
4105
|
+
} catch {
|
|
4106
|
+
return void 0;
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
var RESOLVE_ID_TIMEOUT_MS = 1e4;
|
|
4110
|
+
async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
|
|
4111
|
+
const resolved = ref.repo ? ref : { ...ref, repo: fallbackRepo };
|
|
4112
|
+
const id = (await runGh(buildResolveIdArgs(resolved), RESOLVE_ID_TIMEOUT_MS)).trim();
|
|
4113
|
+
if (!id) throw new Error(`could not resolve node id for issue #${ref.number}${resolved.repo ? ` in ${resolved.repo}` : ""}`);
|
|
4114
|
+
return id;
|
|
4115
|
+
}
|
|
4116
|
+
async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
4117
|
+
const parent = parseIssueRef(parentRef);
|
|
4118
|
+
const child = parseIssueRef(childRef);
|
|
4119
|
+
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
4120
|
+
const subIssueId = await resolveIssueNodeId(runGh, child, defaultRepo);
|
|
4121
|
+
const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
4122
|
+
const result = parseAddSubIssueResult(stdout);
|
|
4123
|
+
if (!result) throw new Error(`addSubIssue returned an unexpected response:
|
|
4124
|
+
${stdout.trim() || "(empty)"}`);
|
|
4125
|
+
return result;
|
|
4126
|
+
}
|
|
4127
|
+
function parentLinkFields(result, error) {
|
|
4128
|
+
if (result) return { parent: result };
|
|
4129
|
+
if (error) return { parentLinkError: error };
|
|
4130
|
+
return {};
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4067
4133
|
// src/report.ts
|
|
4068
4134
|
var HUB_REPO = "mutmutco/MMI-Hub";
|
|
4069
4135
|
var REPORT_LABEL = "report";
|
|
@@ -4997,6 +5063,99 @@ function parseWorktreePorcelain(stdout) {
|
|
|
4997
5063
|
function samePath(a, b) {
|
|
4998
5064
|
return a.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase() === b.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
4999
5065
|
}
|
|
5066
|
+
function normPath(p) {
|
|
5067
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
5068
|
+
}
|
|
5069
|
+
function parseComposeLs(stdout) {
|
|
5070
|
+
const text = stdout.trim();
|
|
5071
|
+
if (!text) return [];
|
|
5072
|
+
const rows = [];
|
|
5073
|
+
const pushIfObject = (v) => {
|
|
5074
|
+
if (v && typeof v === "object" && !Array.isArray(v)) rows.push(v);
|
|
5075
|
+
};
|
|
5076
|
+
try {
|
|
5077
|
+
const parsed = JSON.parse(text);
|
|
5078
|
+
if (Array.isArray(parsed)) parsed.forEach(pushIfObject);
|
|
5079
|
+
else pushIfObject(parsed);
|
|
5080
|
+
} catch {
|
|
5081
|
+
for (const line of text.split(/\r?\n/)) {
|
|
5082
|
+
const trimmed = line.trim();
|
|
5083
|
+
if (!trimmed) continue;
|
|
5084
|
+
try {
|
|
5085
|
+
pushIfObject(JSON.parse(trimmed));
|
|
5086
|
+
} catch {
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
return rows.map((row) => {
|
|
5091
|
+
const name = typeof row.Name === "string" ? row.Name.trim() : "";
|
|
5092
|
+
if (!name) return null;
|
|
5093
|
+
const raw = typeof row.ConfigFiles === "string" ? row.ConfigFiles : "";
|
|
5094
|
+
const configFiles = raw.split(",").map((f) => f.trim()).filter(Boolean);
|
|
5095
|
+
return { name, configFiles };
|
|
5096
|
+
}).filter((p) => Boolean(p));
|
|
5097
|
+
}
|
|
5098
|
+
function selectWorktreeComposeProjects(worktreePath, projects) {
|
|
5099
|
+
const root = normPath(worktreePath);
|
|
5100
|
+
if (!root) return [];
|
|
5101
|
+
const names = [];
|
|
5102
|
+
for (const project2 of projects) {
|
|
5103
|
+
const inside = project2.configFiles.some((file) => {
|
|
5104
|
+
const f = normPath(file);
|
|
5105
|
+
return f === root || f.startsWith(`${root}/`);
|
|
5106
|
+
});
|
|
5107
|
+
if (inside && !names.includes(project2.name)) names.push(project2.name);
|
|
5108
|
+
}
|
|
5109
|
+
return names;
|
|
5110
|
+
}
|
|
5111
|
+
function deriveComposeProjectName(worktreePath) {
|
|
5112
|
+
const norm = normPath(worktreePath);
|
|
5113
|
+
if (!norm) return void 0;
|
|
5114
|
+
const base = norm.slice(norm.lastIndexOf("/") + 1);
|
|
5115
|
+
const name = base.replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
|
|
5116
|
+
return name || void 0;
|
|
5117
|
+
}
|
|
5118
|
+
function planWorktreeComposeTeardown(worktreePath, discovered, hasStageState) {
|
|
5119
|
+
const names = [...discovered];
|
|
5120
|
+
if (!hasStageState && discovered.length === 0) return names;
|
|
5121
|
+
const recovery = deriveComposeProjectName(worktreePath);
|
|
5122
|
+
if (recovery && !names.includes(recovery)) names.push(recovery);
|
|
5123
|
+
return names;
|
|
5124
|
+
}
|
|
5125
|
+
async function runWorktreeStageTeardown(worktreePath, deps) {
|
|
5126
|
+
const hasStageState = deps.hasStageState(worktreePath);
|
|
5127
|
+
const stoppedPid = hasStageState ? await deps.stopRecordedStage(worktreePath).catch(() => void 0) : void 0;
|
|
5128
|
+
let discovered;
|
|
5129
|
+
try {
|
|
5130
|
+
discovered = selectWorktreeComposeProjects(worktreePath, await deps.listComposeProjects());
|
|
5131
|
+
} catch {
|
|
5132
|
+
return { status: stoppedPid != null ? "torn-down" : "none", stoppedPid };
|
|
5133
|
+
}
|
|
5134
|
+
const projects = planWorktreeComposeTeardown(worktreePath, discovered, hasStageState);
|
|
5135
|
+
const brought = [];
|
|
5136
|
+
const failed = [];
|
|
5137
|
+
const errors = [];
|
|
5138
|
+
for (const project2 of projects) {
|
|
5139
|
+
try {
|
|
5140
|
+
await deps.composeDown(project2);
|
|
5141
|
+
brought.push(project2);
|
|
5142
|
+
} catch (e) {
|
|
5143
|
+
failed.push(project2);
|
|
5144
|
+
errors.push(`docker compose -p ${project2} down -v: ${errorMessage(e)}`);
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
if (failed.length) {
|
|
5148
|
+
return {
|
|
5149
|
+
status: "failed",
|
|
5150
|
+
stoppedPid,
|
|
5151
|
+
composeProjects: brought.length ? brought : void 0,
|
|
5152
|
+
failedProjects: failed,
|
|
5153
|
+
error: errors.join("; ")
|
|
5154
|
+
};
|
|
5155
|
+
}
|
|
5156
|
+
if (stoppedPid == null && brought.length === 0) return { status: "none" };
|
|
5157
|
+
return { status: "torn-down", stoppedPid, composeProjects: brought.length ? brought : void 0 };
|
|
5158
|
+
}
|
|
5000
5159
|
function selectPrMergeCleanupWorktree(branch, before, after, startingPath) {
|
|
5001
5160
|
if (!branch) return void 0;
|
|
5002
5161
|
const current = after.find((w) => w.branch === branch)?.path;
|
|
@@ -5042,15 +5201,24 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
5042
5201
|
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath);
|
|
5043
5202
|
const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
|
|
5044
5203
|
if (wtPath) {
|
|
5204
|
+
let stageTeardown;
|
|
5205
|
+
if (options.teardownWorktreeStage) {
|
|
5206
|
+
try {
|
|
5207
|
+
stageTeardown = await options.teardownWorktreeStage(wtPath);
|
|
5208
|
+
} catch (e) {
|
|
5209
|
+
stageTeardown = { status: "failed", error: errorMessage(e) };
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5045
5212
|
try {
|
|
5046
5213
|
await git(["worktree", "remove", "--force", wtPath]);
|
|
5047
|
-
report.worktree = { path: wtPath, status: "removed" };
|
|
5214
|
+
report.worktree = { path: wtPath, status: "removed", stageTeardown };
|
|
5048
5215
|
} catch (e) {
|
|
5049
5216
|
report.worktree = {
|
|
5050
5217
|
path: wtPath,
|
|
5051
5218
|
status: "failed",
|
|
5052
5219
|
error: errorMessage(e),
|
|
5053
|
-
safeCleanupCommand: safeWorktreeRemoveCommand(safeCwd, wtPath)
|
|
5220
|
+
safeCleanupCommand: safeWorktreeRemoveCommand(safeCwd, wtPath),
|
|
5221
|
+
stageTeardown
|
|
5054
5222
|
};
|
|
5055
5223
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-failed" };
|
|
5056
5224
|
return report;
|
|
@@ -5100,14 +5268,28 @@ function formatGcPlan(plan2, apply) {
|
|
|
5100
5268
|
}
|
|
5101
5269
|
|
|
5102
5270
|
// src/command-plans.ts
|
|
5103
|
-
function stagePlan(stage2 = {}) {
|
|
5271
|
+
function stagePlan(stage2 = {}, stops = true) {
|
|
5104
5272
|
return [
|
|
5105
|
-
{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" },
|
|
5273
|
+
...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
|
|
5106
5274
|
{ label: "run local build", command: stage2.build || "(no stage.build configured)" },
|
|
5107
5275
|
{ label: "start local stage", command: stage2.up || "(no stage.up configured)" },
|
|
5108
5276
|
{ label: "check health", command: stage2.healthUrl ? `curl --fail ${stage2.healthUrl}` : "(no stage.healthUrl configured)" }
|
|
5109
5277
|
];
|
|
5110
5278
|
}
|
|
5279
|
+
function derivedStagePlan(derived, shell2, stops = true) {
|
|
5280
|
+
const { port } = derived;
|
|
5281
|
+
const envOrder = ["MMI_STAGE", "MMI_PORT", "PORT", "COMPOSE_PROFILES"];
|
|
5282
|
+
const resolved = { MMI_STAGE: "development", MMI_PORT: String(port), PORT: String(port), COMPOSE_PROFILES: "local" };
|
|
5283
|
+
const ensureEnv = shell2 === "powershell" ? `if (-not (Test-Path -LiteralPath '.env')) { Copy-Item -LiteralPath '.env.example' -Destination '.env' }` : `[ -f .env ] || cp .env.example .env`;
|
|
5284
|
+
const up = shell2 === "powershell" ? `${envOrder.map((k) => `$env:${k}='${resolved[k]}'`).join("; ")}; docker compose up -d --build` : `${envOrder.map((k) => `${k}=${resolved[k]}`).join(" ")} docker compose up -d --build`;
|
|
5285
|
+
const health = shell2 === "powershell" ? `curl.exe --fail ${derived.url}` : `curl --fail ${derived.url}`;
|
|
5286
|
+
return [
|
|
5287
|
+
...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
|
|
5288
|
+
{ label: "bootstrap .env from .env.example", command: ensureEnv },
|
|
5289
|
+
{ label: `start local stage on port ${port} (registry portRange)`, command: up },
|
|
5290
|
+
{ label: `check health at ${derived.url}`, command: health }
|
|
5291
|
+
];
|
|
5292
|
+
}
|
|
5111
5293
|
function stageLivePlan() {
|
|
5112
5294
|
return [
|
|
5113
5295
|
{ label: "stage-live is not an org command; /stage is local only", command: "mmi-cli stage run --apply" },
|
|
@@ -5123,7 +5305,8 @@ function trainPlan(command) {
|
|
|
5123
5305
|
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
5124
5306
|
{ label: "for Hub distribution changes, verify the release bump is already landed before touching rc", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5125
5307
|
{ label: "merge development to rc", gated: true },
|
|
5126
|
-
{ label: "trigger the deploy path for this repo model", command: "tenant-container: gh workflow run tenant-deploy.yml
|
|
5308
|
+
{ label: "trigger the deploy path for this repo model, returning the Hub Actions run id/url (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml auto-fires on rc push", gated: true },
|
|
5309
|
+
{ label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli tenant redeploy <owner/repo> rc --watch", gated: true }
|
|
5127
5310
|
];
|
|
5128
5311
|
}
|
|
5129
5312
|
if (command === "release") {
|
|
@@ -5135,7 +5318,7 @@ function trainPlan(command) {
|
|
|
5135
5318
|
{ label: "merge rc to main", gated: true },
|
|
5136
5319
|
{ label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5137
5320
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5138
|
-
{ label: "trigger the deploy path for this repo model", command: "tenant-container: gh workflow run tenant-deploy.yml
|
|
5321
|
+
{ label: "trigger the deploy path for this repo model, returning the Hub Actions run id/url (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release", gated: true },
|
|
5139
5322
|
{ label: "roll development forward", gated: true }
|
|
5140
5323
|
];
|
|
5141
5324
|
}
|
|
@@ -5160,6 +5343,114 @@ function bootstrapPlan(repo, repoClass) {
|
|
|
5160
5343
|
];
|
|
5161
5344
|
}
|
|
5162
5345
|
|
|
5346
|
+
// src/stage-default.ts
|
|
5347
|
+
function shellFor(platform = process.platform) {
|
|
5348
|
+
return platform === "win32" ? "powershell" : "bash";
|
|
5349
|
+
}
|
|
5350
|
+
function deriveStageGap(inputs) {
|
|
5351
|
+
const missing = [];
|
|
5352
|
+
if (inputs.deployModel !== "tenant-container") {
|
|
5353
|
+
return `local stage default applies to tenant-container repos only (registry deployModel = ${inputs.deployModel ?? "unset"})`;
|
|
5354
|
+
}
|
|
5355
|
+
if (!inputs.hasCompose) missing.push("docker-compose.yml");
|
|
5356
|
+
if (!inputs.hasEnvExample) missing.push(".env.example");
|
|
5357
|
+
if (!inputs.portRange) missing.push("Hub registry portRange");
|
|
5358
|
+
return missing.length ? `cannot derive a default local stage \u2014 missing: ${missing.join(", ")}` : null;
|
|
5359
|
+
}
|
|
5360
|
+
function deriveStage(inputs) {
|
|
5361
|
+
if (deriveStageGap(inputs) || !inputs.portRange) return null;
|
|
5362
|
+
const port = inputs.portRange[0];
|
|
5363
|
+
const config = {
|
|
5364
|
+
up: "docker compose up -d --build",
|
|
5365
|
+
healthUrl: "http://127.0.0.1:$STAGE_PORT/",
|
|
5366
|
+
// A generic compose app's landing route is unknown (e.g. MM-Website serves /tr/), so "the server
|
|
5367
|
+
// answered" — any HTTP status — is the health signal, not a 2xx on the bare root.
|
|
5368
|
+
healthAnyStatus: true,
|
|
5369
|
+
portRange: inputs.portRange,
|
|
5370
|
+
ensureEnv: { example: ".env.example", target: ".env" },
|
|
5371
|
+
env: {
|
|
5372
|
+
MMI_STAGE: "development",
|
|
5373
|
+
MMI_PORT: "$STAGE_PORT",
|
|
5374
|
+
PORT: "$STAGE_PORT",
|
|
5375
|
+
COMPOSE_PROFILES: "local"
|
|
5376
|
+
}
|
|
5377
|
+
};
|
|
5378
|
+
return { config, port, url: stageUrlForPort(port) };
|
|
5379
|
+
}
|
|
5380
|
+
function stageUrlForPort(port) {
|
|
5381
|
+
return `http://127.0.0.1:${port}/`;
|
|
5382
|
+
}
|
|
5383
|
+
var POSIX_ONLY = [
|
|
5384
|
+
/(^|[^&|])&&([^&]|$)/,
|
|
5385
|
+
// && chaining (cmd.exe uses it too, but PowerShell <7 / the typical agent shell does not)
|
|
5386
|
+
/\|\|/,
|
|
5387
|
+
// || chaining
|
|
5388
|
+
// `VAR=value cmd` LEADING env prefix only — anchored to a command boundary (start-of-string or after a
|
|
5389
|
+
// ; & | separator) so it never fires on a flag value mid-command (`-e DEBUG=true app`, `--env X=y svc`),
|
|
5390
|
+
// which are valid cross-shell. The value excludes separators so it can't span past the command boundary.
|
|
5391
|
+
/(?:^|[;&|])\s*[A-Za-z_][A-Za-z0-9_]*=[^\s;&|]+\s+\S/,
|
|
5392
|
+
/(^|[\s;&|])(cp|rm|mv|ln|cat|touch|export)\s/
|
|
5393
|
+
// bare POSIX file/env builtins
|
|
5394
|
+
];
|
|
5395
|
+
function looksPosixOnly(command) {
|
|
5396
|
+
if (!command?.trim()) return false;
|
|
5397
|
+
return POSIX_ONLY.some((re) => re.test(command));
|
|
5398
|
+
}
|
|
5399
|
+
function stalePosixFields(config, shell2) {
|
|
5400
|
+
if (shell2 !== "powershell") return [];
|
|
5401
|
+
const fields = [];
|
|
5402
|
+
if (looksPosixOnly(config?.build)) fields.push("build");
|
|
5403
|
+
if (looksPosixOnly(config?.up)) fields.push("up");
|
|
5404
|
+
return fields;
|
|
5405
|
+
}
|
|
5406
|
+
function sanitizeLocalStage(local, stale) {
|
|
5407
|
+
if (!stale.length) return local;
|
|
5408
|
+
const clean2 = { ...local };
|
|
5409
|
+
for (const field of stale) delete clean2[field];
|
|
5410
|
+
return clean2;
|
|
5411
|
+
}
|
|
5412
|
+
function staleNote(staleFields, outcome) {
|
|
5413
|
+
const list = staleFields.join(", ");
|
|
5414
|
+
const plural = staleFields.length > 1 ? "fields" : "field";
|
|
5415
|
+
return `local .mmi stage ${plural} ${list} ${staleFields.length > 1 ? "are" : "is"} POSIX-only and unusable on PowerShell \u2014 ${outcome}`;
|
|
5416
|
+
}
|
|
5417
|
+
function decideStage(inputs) {
|
|
5418
|
+
const { local, shell: shell2, registry: registry2, hasCompose, hasEnvExample } = inputs;
|
|
5419
|
+
const staleFields = stalePosixFields(local, shell2);
|
|
5420
|
+
const stale = staleFields.length > 0;
|
|
5421
|
+
const upStale = staleFields.includes("up");
|
|
5422
|
+
if (local?.up?.trim() && !upStale) {
|
|
5423
|
+
if (!stale) return { source: "local", config: local };
|
|
5424
|
+
return {
|
|
5425
|
+
source: "local",
|
|
5426
|
+
config: sanitizeLocalStage(local, staleFields),
|
|
5427
|
+
staleIgnored: true,
|
|
5428
|
+
staleFields,
|
|
5429
|
+
gap: staleNote(staleFields, `kept the cross-shell parts of the local recipe and ignored ${staleFields.join(", ")}`)
|
|
5430
|
+
};
|
|
5431
|
+
}
|
|
5432
|
+
const deriveInputs = {
|
|
5433
|
+
portRange: registry2.portRange,
|
|
5434
|
+
deployModel: registry2.deployModel,
|
|
5435
|
+
hasCompose,
|
|
5436
|
+
hasEnvExample
|
|
5437
|
+
};
|
|
5438
|
+
const derived = deriveStage(deriveInputs);
|
|
5439
|
+
if (derived) {
|
|
5440
|
+
return {
|
|
5441
|
+
source: "derived",
|
|
5442
|
+
derived,
|
|
5443
|
+
registryError: registry2.error,
|
|
5444
|
+
staleIgnored: stale || void 0,
|
|
5445
|
+
staleFields: stale ? staleFields : void 0
|
|
5446
|
+
};
|
|
5447
|
+
}
|
|
5448
|
+
const registryGap = registry2.error ? `Hub registry read failed (${registry2.error}) \u2014 cannot derive a default local stage` : null;
|
|
5449
|
+
const baseGap = registryGap ?? deriveStageGap(deriveInputs);
|
|
5450
|
+
const gap = stale ? `local .mmi stage recipe is POSIX-only (${staleFields.join(", ")}) and unusable on PowerShell; ${baseGap ?? "no registry-derived default available"}` : baseGap ?? "no stage.up configured and no registry-derived default available";
|
|
5451
|
+
return { source: "none", gap, staleIgnored: stale || void 0, staleFields: stale ? staleFields : void 0, registryError: registry2.error };
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5163
5454
|
// src/bootstrap-seeds.ts
|
|
5164
5455
|
var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
5165
5456
|
function loadBootstrapSeeds(manifestJson) {
|
|
@@ -5204,6 +5495,7 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
5204
5495
|
".claude/worktrees/",
|
|
5205
5496
|
".mmi/.session",
|
|
5206
5497
|
".mmi/head-ts/",
|
|
5498
|
+
".aws-sam/",
|
|
5207
5499
|
"/*.png"
|
|
5208
5500
|
];
|
|
5209
5501
|
function renderManagedGitignoreBlock() {
|
|
@@ -5237,6 +5529,23 @@ ${block}
|
|
|
5237
5529
|
}
|
|
5238
5530
|
return { content: next, changed: src !== next };
|
|
5239
5531
|
}
|
|
5532
|
+
function diffManagedGitignoreBlock(current) {
|
|
5533
|
+
const lines = (current ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
5534
|
+
const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
|
|
5535
|
+
const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
|
|
5536
|
+
const hasMarker = beginAt !== -1 || lines.some((l) => l === GITIGNORE_MANAGED_END);
|
|
5537
|
+
if (!hasMarker) return { added: [], removed: [], seeded: true };
|
|
5538
|
+
const wellOrdered = beginAt !== -1 && endAt !== -1;
|
|
5539
|
+
if (!wellOrdered) return { added: [], removed: [], seeded: false };
|
|
5540
|
+
const isRule = (l) => l.trim() !== "" && !l.trim().startsWith("#");
|
|
5541
|
+
const oldBody = lines.slice(beginAt + 1, endAt).filter(isRule);
|
|
5542
|
+
const newBody = MANAGED_GITIGNORE_LINES.filter(isRule);
|
|
5543
|
+
return {
|
|
5544
|
+
added: newBody.filter((l) => !oldBody.includes(l)),
|
|
5545
|
+
removed: oldBody.filter((l) => !newBody.includes(l)),
|
|
5546
|
+
seeded: false
|
|
5547
|
+
};
|
|
5548
|
+
}
|
|
5240
5549
|
|
|
5241
5550
|
// src/doctor.ts
|
|
5242
5551
|
var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
|
|
@@ -5267,7 +5576,7 @@ function buildAwsCrossAccountCheck(input) {
|
|
|
5267
5576
|
var MMI_PLUGIN_ID = "mmi@mmi";
|
|
5268
5577
|
var PLUGIN_LABEL = "plugin install record (mmi@mmi for this project)";
|
|
5269
5578
|
function pluginInstallManualFix(projectPath, surface = "claude-cli") {
|
|
5270
|
-
const register = surface === "codex" ? `\`codex plugin add ${MMI_PLUGIN_ID}\`` : surface === "shell" ? `enable the MMI plugin in your client` : surface === "claude-vscode" ? `\`claude plugin enable ${MMI_PLUGIN_ID}\`` : `\`/plugin install ${MMI_PLUGIN_ID}\``;
|
|
5579
|
+
const register = surface === "codex" ? `\`codex plugin add ${MMI_PLUGIN_ID}\`` : surface === "cursor" ? `import the MMI Team Marketplace in Cursor Dashboard \u2192 Settings \u2192 Plugins (or enable the MMI plugin from the marketplace panel)` : surface === "shell" ? `enable the MMI plugin in your client` : surface === "claude-vscode" ? `\`claude plugin enable ${MMI_PLUGIN_ID}\`` : `\`/plugin install ${MMI_PLUGIN_ID}\``;
|
|
5271
5580
|
return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
|
|
5272
5581
|
}
|
|
5273
5582
|
function isMmiPluginEnabled(settings) {
|
|
@@ -5322,6 +5631,9 @@ function bestRecord(records) {
|
|
|
5322
5631
|
});
|
|
5323
5632
|
}
|
|
5324
5633
|
function pluginConfigDriftFix(pluginId, surface = "claude-cli") {
|
|
5634
|
+
if (surface === "cursor") {
|
|
5635
|
+
return `\`${pluginId}\` is registered through Cursor's Team Marketplace \u2014 refresh it in Dashboard \u2192 Settings \u2192 Plugins (Cursor manages its own plugin records; mmi-cli cannot rewrite them), then ${reloadAction(surface)}`;
|
|
5636
|
+
}
|
|
5325
5637
|
const file = surface === "codex" ? "~/.codex/plugins/installed_plugins.json" : "~/.claude/plugins/installed_plugins.json";
|
|
5326
5638
|
return `\`${pluginId}\` has duplicate install rows or stale gitCommitSha in ${file} \u2014 run \`mmi-cli doctor\` interactively to collapse them to one user-scope row (a .bak backup is written first), then ${reloadAction(surface)}`;
|
|
5327
5639
|
}
|
|
@@ -5363,7 +5675,8 @@ function buildGitignoreManagedBlockCheck(input) {
|
|
|
5363
5675
|
if (!input.isOrgRepo) return base;
|
|
5364
5676
|
const { content, changed } = upsertManagedGitignoreBlock(input.content);
|
|
5365
5677
|
if (!changed) return base;
|
|
5366
|
-
|
|
5678
|
+
const { added, removed, seeded } = diffManagedGitignoreBlock(input.content);
|
|
5679
|
+
return { ...base, ok: false, contentToWrite: content, added, removed, seeded };
|
|
5367
5680
|
}
|
|
5368
5681
|
var MMI_PLUGIN_CACHE_CLEANUP_LABEL = "stale MMI plugin cache dirs (Claude/Codex)";
|
|
5369
5682
|
var MMI_PLUGIN_CACHE_CLEANUP_FIX = "run `mmi-cli doctor` to quarantine stale MMI-only plugin cache dirs, then reload the affected agent surface";
|
|
@@ -5413,6 +5726,9 @@ function detectSurface(env) {
|
|
|
5413
5726
|
if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
|
|
5414
5727
|
return "codex";
|
|
5415
5728
|
}
|
|
5729
|
+
if (env.MMI_AGENT_SURFACE === "cursor" || has("CURSOR_TRACE_ID") || has("CURSOR_USER") || has("CURSOR_SESSION_ID")) {
|
|
5730
|
+
return "cursor";
|
|
5731
|
+
}
|
|
5416
5732
|
const isClaude = has("CLAUDECODE") || has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_PLUGIN_ROOT") || env.MMI_AGENT_SURFACE === "claude";
|
|
5417
5733
|
const isVscode = env.TERM_PROGRAM === "vscode" || has("VSCODE_PID") || has("VSCODE_GIT_ASKPASS_NODE");
|
|
5418
5734
|
if (isClaude && isVscode) return "claude-vscode";
|
|
@@ -5425,6 +5741,8 @@ function reloadAction(surface) {
|
|
|
5425
5741
|
return "restart VS Code";
|
|
5426
5742
|
case "codex":
|
|
5427
5743
|
return "restart Codex";
|
|
5744
|
+
case "cursor":
|
|
5745
|
+
return "restart Cursor (or refresh the Team Marketplace from Dashboard \u2192 Settings \u2192 Plugins)";
|
|
5428
5746
|
case "claude-cli":
|
|
5429
5747
|
case "shell":
|
|
5430
5748
|
default:
|
|
@@ -5439,6 +5757,8 @@ function pluginRecoveryFix(surface) {
|
|
|
5439
5757
|
return `${claude} # then ${reloadAction(surface)} to reload MMI commands`;
|
|
5440
5758
|
case "codex":
|
|
5441
5759
|
return "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi # then restart Codex";
|
|
5760
|
+
case "cursor":
|
|
5761
|
+
return `in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace; then ${reloadAction(surface)} to reload MMI skills + hooks`;
|
|
5442
5762
|
case "shell":
|
|
5443
5763
|
default:
|
|
5444
5764
|
return "npm install -g @mutmutco/cli@latest";
|
|
@@ -5482,13 +5802,33 @@ var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.exe
|
|
|
5482
5802
|
function stageStatePath(cwd = process.cwd()) {
|
|
5483
5803
|
return (0, import_node_path4.join)(cwd, "tmp", "stage", "state.json");
|
|
5484
5804
|
}
|
|
5805
|
+
var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
|
|
5806
|
+
function posixOnlyShellProblems(command, field, platform = process.platform) {
|
|
5807
|
+
if (platform !== "win32" || !command?.trim()) return [];
|
|
5808
|
+
const problems = [];
|
|
5809
|
+
if (/(^|&&|\||;)\s*[A-Za-z_][A-Za-z0-9_]*=\S/.test(command)) {
|
|
5810
|
+
problems.push(
|
|
5811
|
+
`stage.${field} uses POSIX inline env assignment (VAR=value command) which fails in cmd.exe on Windows; use 'set VAR=value && command' or a cross-platform launcher`
|
|
5812
|
+
);
|
|
5813
|
+
}
|
|
5814
|
+
for (const verb of POSIX_ONLY_VERBS) {
|
|
5815
|
+
if (new RegExp(`(^|&&|\\||;|\\()\\s*${verb}\\b`).test(command)) {
|
|
5816
|
+
problems.push(
|
|
5817
|
+
`stage.${field} calls POSIX '${verb}' which does not exist in cmd.exe on Windows; use the cmd/PowerShell equivalent or a cross-platform script`
|
|
5818
|
+
);
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
return problems;
|
|
5822
|
+
}
|
|
5485
5823
|
function validateStageConfig(config = {}, action) {
|
|
5486
5824
|
const problems = [];
|
|
5487
|
-
if (action === "run" && !config.build?.trim()) problems.push("stage.build is required for stage run");
|
|
5825
|
+
if (action === "run" && !config.build?.trim() && !config.ensureEnv) problems.push("stage.build is required for stage run");
|
|
5488
5826
|
if (!config.up?.trim()) problems.push("stage.up is required to start the local stage");
|
|
5489
5827
|
if (config.healthUrl != null && config.healthUrl.trim() && !/^https?:\/\//.test(config.healthUrl.trim())) {
|
|
5490
5828
|
problems.push("stage.healthUrl must be an http(s) URL");
|
|
5491
5829
|
}
|
|
5830
|
+
if (action === "run") problems.push(...posixOnlyShellProblems(config.build, "build"));
|
|
5831
|
+
problems.push(...posixOnlyShellProblems(config.up, "up"));
|
|
5492
5832
|
if (config.portRange != null) {
|
|
5493
5833
|
const r = config.portRange;
|
|
5494
5834
|
const ok = Array.isArray(r) && r.length === 2 && r.every((n) => Number.isInteger(n) && n >= 1024 && n <= 65535) && r[0] <= r[1];
|
|
@@ -5553,13 +5893,13 @@ async function killTree(pid) {
|
|
|
5553
5893
|
}
|
|
5554
5894
|
}
|
|
5555
5895
|
}
|
|
5556
|
-
async function waitForHealth(url, timeoutMs) {
|
|
5896
|
+
async function waitForHealth(url, timeoutMs, anyStatus = false) {
|
|
5557
5897
|
const deadline = Date.now() + timeoutMs;
|
|
5558
5898
|
let last = "";
|
|
5559
5899
|
while (Date.now() < deadline) {
|
|
5560
5900
|
try {
|
|
5561
5901
|
const res = await fetch(url, { signal: AbortSignal.timeout(Math.min(5e3, timeoutMs)) });
|
|
5562
|
-
if (res.ok) return;
|
|
5902
|
+
if (anyStatus || res.ok) return;
|
|
5563
5903
|
last = `HTTP ${res.status}`;
|
|
5564
5904
|
} catch (e) {
|
|
5565
5905
|
last = e.message;
|
|
@@ -5594,14 +5934,24 @@ async function startStage(config = {}, opts = {}) {
|
|
|
5594
5934
|
stagePort = pickStagePort(config.portRange, (p) => free.has(p));
|
|
5595
5935
|
}
|
|
5596
5936
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
5937
|
+
if (config.ensureEnv) {
|
|
5938
|
+
const target = (0, import_node_path4.join)(cwd, config.ensureEnv.target);
|
|
5939
|
+
const example = (0, import_node_path4.join)(cwd, config.ensureEnv.example);
|
|
5940
|
+
if (!(0, import_node_fs3.existsSync)(target) && (0, import_node_fs3.existsSync)(example)) (0, import_node_fs3.copyFileSync)(example, target);
|
|
5941
|
+
}
|
|
5942
|
+
const extraEnv = {};
|
|
5943
|
+
for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
|
|
5597
5944
|
const up = sub(config.up.trim());
|
|
5598
5945
|
const child = (0, import_node_child_process5.spawn)(up, {
|
|
5599
5946
|
cwd,
|
|
5600
5947
|
shell: true,
|
|
5601
|
-
|
|
5948
|
+
// POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
|
|
5949
|
+
// `taskkill /T /F` (no group needed), and detached+shell defeats windowsHide — every spawn would
|
|
5950
|
+
// flash a Windows Terminal window (0x800700e8) on dev machines.
|
|
5951
|
+
detached: process.platform !== "win32",
|
|
5602
5952
|
windowsHide: true,
|
|
5603
5953
|
stdio: "ignore",
|
|
5604
|
-
env: stagePort != null ? {
|
|
5954
|
+
env: { ...process.env, ...stagePort != null ? { STAGE_PORT: String(stagePort) } : {}, ...extraEnv }
|
|
5605
5955
|
});
|
|
5606
5956
|
const state = {
|
|
5607
5957
|
pid: child.pid ?? 0,
|
|
@@ -5613,13 +5963,13 @@ async function startStage(config = {}, opts = {}) {
|
|
|
5613
5963
|
};
|
|
5614
5964
|
(0, import_node_fs3.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
5615
5965
|
try {
|
|
5616
|
-
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4);
|
|
5966
|
+
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
5617
5967
|
} catch (e) {
|
|
5618
5968
|
await killTree(state.pid);
|
|
5619
5969
|
(0, import_node_fs3.rmSync)(statePath, { force: true });
|
|
5620
5970
|
throw e;
|
|
5621
5971
|
}
|
|
5622
|
-
const result = { ok: true, action: "start", statePath, pid: state.pid, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
5972
|
+
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
5623
5973
|
opts.onReady?.(result);
|
|
5624
5974
|
child.unref();
|
|
5625
5975
|
return result;
|
|
@@ -5630,7 +5980,7 @@ async function runStage(config = {}, opts = {}) {
|
|
|
5630
5980
|
const cwd = opts.cwd ?? process.cwd();
|
|
5631
5981
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
5632
5982
|
await stopStage({ ...opts, cwd });
|
|
5633
|
-
await shell(config.build.trim(), cwd, timeoutMs);
|
|
5983
|
+
if (config.build?.trim()) await shell(config.build.trim(), cwd, timeoutMs);
|
|
5634
5984
|
const started = await startStage(config, { ...opts, cwd, timeoutMs });
|
|
5635
5985
|
return { ...started, action: "run", message: `built and ${started.message}` };
|
|
5636
5986
|
}
|
|
@@ -5693,6 +6043,41 @@ async function verifyHubDistributionVersion(deps, model, releaseTag) {
|
|
|
5693
6043
|
if (model !== "hub-serverless") return;
|
|
5694
6044
|
await deps.run("node", ["scripts/release-distribution.mjs", "verify", releaseTag, "--skip-npm-view"]);
|
|
5695
6045
|
}
|
|
6046
|
+
var ORG_SPINE_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".claude/settings.json"];
|
|
6047
|
+
function isSpinePath(path2) {
|
|
6048
|
+
return ORG_SPINE_FILES.includes(path2);
|
|
6049
|
+
}
|
|
6050
|
+
async function predictMergeConflicts(deps, ours, theirs) {
|
|
6051
|
+
try {
|
|
6052
|
+
await deps.run("git", ["merge-tree", "--write-tree", "--name-only", "--no-messages", ours, theirs]);
|
|
6053
|
+
return [];
|
|
6054
|
+
} catch (e) {
|
|
6055
|
+
const out = String(e.stdout ?? "");
|
|
6056
|
+
const files = out.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
|
|
6057
|
+
if (files.length === 0) {
|
|
6058
|
+
throw new Error(`could not preflight the ${theirs} -> ${ours} merge (git merge-tree failed: ${e.message ?? e})`);
|
|
6059
|
+
}
|
|
6060
|
+
return files;
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
async function mergeRcWithSpineResolution(deps) {
|
|
6064
|
+
try {
|
|
6065
|
+
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
6066
|
+
return;
|
|
6067
|
+
} catch {
|
|
6068
|
+
}
|
|
6069
|
+
const unmerged = (await deps.run("git", ["diff", "--name-only", "--diff-filter=U"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
6070
|
+
const nonSpine = unmerged.filter((f) => !isSpinePath(f));
|
|
6071
|
+
if (unmerged.length === 0 || nonSpine.length > 0) {
|
|
6072
|
+
await deps.run("git", ["merge", "--abort"]);
|
|
6073
|
+
throw new Error(
|
|
6074
|
+
unmerged.length === 0 ? "rc -> main merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun" : `rc -> main merge conflicts on non-spine path(s): ${nonSpine.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile main and rc via an approved alignment PR, then rerun release)`
|
|
6075
|
+
);
|
|
6076
|
+
}
|
|
6077
|
+
await deps.run("git", ["checkout", "--theirs", "--", ...unmerged]);
|
|
6078
|
+
await deps.run("git", ["add", "--", ...unmerged]);
|
|
6079
|
+
await deps.run("git", ["commit", "--no-edit"]);
|
|
6080
|
+
}
|
|
5696
6081
|
function ensurePositiveCount(out, emptyMessage) {
|
|
5697
6082
|
const count = Number.parseInt(out.trim(), 10);
|
|
5698
6083
|
if (!Number.isFinite(count)) throw new Error(`could not parse ahead count: ${out.trim() || "(empty)"}`);
|
|
@@ -5726,14 +6111,56 @@ async function requireBranch(deps, branch) {
|
|
|
5726
6111
|
const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
5727
6112
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
5728
6113
|
}
|
|
5729
|
-
|
|
6114
|
+
var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
6115
|
+
var CORRELATE_ATTEMPTS = 5;
|
|
6116
|
+
var CORRELATE_DELAY_MS = 1500;
|
|
6117
|
+
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
6118
|
+
async function correlateTenantRun(deps, since) {
|
|
6119
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6120
|
+
const threshold = since - CORRELATE_SKEW_SLACK_MS;
|
|
6121
|
+
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
6122
|
+
if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
|
|
6123
|
+
let rows;
|
|
6124
|
+
try {
|
|
6125
|
+
const out = await deps.run("gh", [
|
|
6126
|
+
"run",
|
|
6127
|
+
"list",
|
|
6128
|
+
"--repo",
|
|
6129
|
+
HUB_REPO2,
|
|
6130
|
+
"--workflow",
|
|
6131
|
+
"tenant-deploy.yml",
|
|
6132
|
+
"--limit",
|
|
6133
|
+
"10",
|
|
6134
|
+
"--json",
|
|
6135
|
+
"databaseId,url,event,createdAt"
|
|
6136
|
+
]);
|
|
6137
|
+
rows = JSON.parse(out);
|
|
6138
|
+
} catch {
|
|
6139
|
+
continue;
|
|
6140
|
+
}
|
|
6141
|
+
const match = rows.filter((r) => r.event === "workflow_dispatch" && typeof r.databaseId === "number").map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
|
|
6142
|
+
if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
|
|
6143
|
+
}
|
|
6144
|
+
return {};
|
|
6145
|
+
}
|
|
6146
|
+
async function watchTenantRun(deps, runId) {
|
|
6147
|
+
if (runId == null) return "pending";
|
|
6148
|
+
try {
|
|
6149
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", HUB_REPO2, "--exit-status"]);
|
|
6150
|
+
return "success";
|
|
6151
|
+
} catch {
|
|
6152
|
+
return "failure";
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
|
|
5730
6156
|
if (model === "tenant-container") {
|
|
6157
|
+
const since = (deps.now ?? Date.now)();
|
|
5731
6158
|
await deps.run("gh", [
|
|
5732
6159
|
"workflow",
|
|
5733
6160
|
"run",
|
|
5734
6161
|
"tenant-deploy.yml",
|
|
5735
6162
|
"--repo",
|
|
5736
|
-
|
|
6163
|
+
HUB_REPO2,
|
|
5737
6164
|
"-f",
|
|
5738
6165
|
`slug=${ctx.slug}`,
|
|
5739
6166
|
"-f",
|
|
@@ -5743,12 +6170,17 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model) {
|
|
|
5743
6170
|
"-f",
|
|
5744
6171
|
`stage=${stage2}`
|
|
5745
6172
|
]);
|
|
5746
|
-
|
|
6173
|
+
const { runId, runUrl } = await correlateTenantRun(deps, since);
|
|
6174
|
+
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
6175
|
+
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
5747
6176
|
}
|
|
5748
6177
|
if (model === "hub-serverless") {
|
|
5749
|
-
return
|
|
6178
|
+
return {
|
|
6179
|
+
note: ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)",
|
|
6180
|
+
deployStatus: "pending"
|
|
6181
|
+
};
|
|
5750
6182
|
}
|
|
5751
|
-
return `no manual dispatch: ${model} repo deploys via its own push-triggered workflow
|
|
6183
|
+
return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
|
|
5752
6184
|
}
|
|
5753
6185
|
async function preflight(deps, ctx, stage2) {
|
|
5754
6186
|
let meta = null;
|
|
@@ -5767,7 +6199,8 @@ async function preflight(deps, ctx, stage2) {
|
|
|
5767
6199
|
await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
|
|
5768
6200
|
return model;
|
|
5769
6201
|
}
|
|
5770
|
-
async function runTrainApply(command, deps) {
|
|
6202
|
+
async function runTrainApply(command, deps, options = {}) {
|
|
6203
|
+
const watch = options.watch ?? false;
|
|
5771
6204
|
const ctx = await buildTrainApplyContext(deps);
|
|
5772
6205
|
await requireCleanTree(deps);
|
|
5773
6206
|
await deps.run("git", ["fetch", "origin"]);
|
|
@@ -5787,8 +6220,8 @@ async function runTrainApply(command, deps) {
|
|
|
5787
6220
|
await deps.run("git", ["tag", tag2]);
|
|
5788
6221
|
await deps.run("git", ["push", "origin", "rc"]);
|
|
5789
6222
|
await deps.run("git", ["push", "origin", tag2]);
|
|
5790
|
-
const
|
|
5791
|
-
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, dispatch:
|
|
6223
|
+
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch);
|
|
6224
|
+
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
|
|
5792
6225
|
}
|
|
5793
6226
|
await requireBranch(deps, "rc");
|
|
5794
6227
|
ensurePositiveCount(
|
|
@@ -5796,21 +6229,109 @@ async function runTrainApply(command, deps) {
|
|
|
5796
6229
|
"nothing to release: origin/rc is not ahead of origin/main"
|
|
5797
6230
|
);
|
|
5798
6231
|
const deployModel = await preflight(deps, ctx, "main");
|
|
6232
|
+
const predicted = await predictMergeConflicts(deps, "origin/main", "origin/rc");
|
|
6233
|
+
const predictedNonSpine = predicted.filter((f) => !isSpinePath(f));
|
|
6234
|
+
if (predictedNonSpine.length > 0) {
|
|
6235
|
+
throw new Error(
|
|
6236
|
+
`rc -> main merge would conflict on non-spine path(s): ${predictedNonSpine.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release.`
|
|
6237
|
+
);
|
|
6238
|
+
}
|
|
6239
|
+
const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
5799
6240
|
await deps.run("git", ["checkout", "main"]);
|
|
5800
6241
|
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
5801
|
-
|
|
6242
|
+
if (predicted.length === 0) {
|
|
6243
|
+
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
6244
|
+
} else {
|
|
6245
|
+
await mergeRcWithSpineResolution(deps);
|
|
6246
|
+
}
|
|
5802
6247
|
const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
5803
6248
|
await verifyHubDistributionVersion(deps, deployModel, tag);
|
|
5804
6249
|
await deps.run("git", ["tag", tag]);
|
|
5805
6250
|
await deps.run("git", ["push", "origin", "main"]);
|
|
5806
6251
|
await deps.run("git", ["push", "origin", tag]);
|
|
5807
6252
|
await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo]);
|
|
5808
|
-
const
|
|
6253
|
+
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch);
|
|
6254
|
+
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
5809
6255
|
await deps.run("git", ["checkout", "development"]);
|
|
5810
6256
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
5811
6257
|
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
5812
6258
|
await deps.run("git", ["push", "origin", "development"]);
|
|
5813
|
-
return {
|
|
6259
|
+
return {
|
|
6260
|
+
...ctx,
|
|
6261
|
+
command,
|
|
6262
|
+
stage: "main",
|
|
6263
|
+
ref: "main",
|
|
6264
|
+
tag,
|
|
6265
|
+
deployModel,
|
|
6266
|
+
promoted: true,
|
|
6267
|
+
dispatch: d.note,
|
|
6268
|
+
runId: d.runId,
|
|
6269
|
+
runUrl: d.runUrl,
|
|
6270
|
+
deployStatus: d.deployStatus,
|
|
6271
|
+
rcRetirement: retirement.status,
|
|
6272
|
+
rcRetirementNote: retirement.note
|
|
6273
|
+
};
|
|
6274
|
+
}
|
|
6275
|
+
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
6276
|
+
if (model !== "tenant-container") {
|
|
6277
|
+
return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
|
|
6278
|
+
}
|
|
6279
|
+
if (deployStatus === "failure") {
|
|
6280
|
+
return { status: "skipped", note: "prod deploy failed \u2014 rc runtime left untouched" };
|
|
6281
|
+
}
|
|
6282
|
+
if (deployStatus !== "success") {
|
|
6283
|
+
return {
|
|
6284
|
+
status: "skipped",
|
|
6285
|
+
note: `prod deploy outcome unconfirmed (run without --watch) \u2014 rc runtime left untouched; after verifying prod, retire with: mmi-cli tenant control ${ctx.repo} rc retire`
|
|
6286
|
+
};
|
|
6287
|
+
}
|
|
6288
|
+
try {
|
|
6289
|
+
await deps.run("git", ["fetch", "origin", "rc"]);
|
|
6290
|
+
const rcNow = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
6291
|
+
if (rcNow !== releasedRcSha) {
|
|
6292
|
+
return {
|
|
6293
|
+
status: "skipped",
|
|
6294
|
+
note: `origin/rc moved past the released candidate (${releasedRcSha.slice(0, 7)} -> ${rcNow.slice(0, 7)}) \u2014 a new candidate is in flight; rc runtime left untouched`
|
|
6295
|
+
};
|
|
6296
|
+
}
|
|
6297
|
+
const out = await deps.runSelf(["tenant", "control", ctx.repo, "rc", "retire"]);
|
|
6298
|
+
let commandId = "";
|
|
6299
|
+
try {
|
|
6300
|
+
commandId = String(JSON.parse(out).commandId ?? "");
|
|
6301
|
+
} catch {
|
|
6302
|
+
}
|
|
6303
|
+
return {
|
|
6304
|
+
status: "retired",
|
|
6305
|
+
note: `rc runtime retired (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
6306
|
+
};
|
|
6307
|
+
} catch (e) {
|
|
6308
|
+
return { status: "failed", note: `rc retirement failed (the release itself succeeded): ${e.message}` };
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
async function runTenantRedeploy(deps, options) {
|
|
6312
|
+
const { stage: stage2 } = options;
|
|
6313
|
+
const ref = options.ref ?? stage2;
|
|
6314
|
+
const watch = options.watch ?? false;
|
|
6315
|
+
const repo = options.repo;
|
|
6316
|
+
const [owner, name] = repo.split("/");
|
|
6317
|
+
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
6318
|
+
const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
6319
|
+
const verdict = await deps.trainAuthority(repo);
|
|
6320
|
+
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
6321
|
+
if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
|
|
6322
|
+
const ctx = { repo, owner, slug: name.toLowerCase(), login };
|
|
6323
|
+
let meta = null;
|
|
6324
|
+
try {
|
|
6325
|
+
meta = JSON.parse(await deps.runSelf(["project", "get", repo]));
|
|
6326
|
+
} catch {
|
|
6327
|
+
meta = null;
|
|
6328
|
+
}
|
|
6329
|
+
const deployModel = resolveDeployModel2(meta, repo);
|
|
6330
|
+
if (deployModel !== "tenant-container") {
|
|
6331
|
+
throw new Error(`${repo} is ${deployModel}, not tenant-container \u2014 there is no central tenant-deploy run to retry (its deploy fires from its own workflow)`);
|
|
6332
|
+
}
|
|
6333
|
+
const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
|
|
6334
|
+
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, deployStatus: d.deployStatus };
|
|
5814
6335
|
}
|
|
5815
6336
|
|
|
5816
6337
|
// src/port-registry.ts
|
|
@@ -6212,6 +6733,12 @@ async function rulesetDetails(deps, repo, list) {
|
|
|
6212
6733
|
}
|
|
6213
6734
|
return details;
|
|
6214
6735
|
}
|
|
6736
|
+
function repoRefsMatch(a, b) {
|
|
6737
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
6738
|
+
}
|
|
6739
|
+
function projectRegistryIncludesRepo(projects, repo) {
|
|
6740
|
+
return projects.some((p) => (p.repos ?? []).some((r) => repoRefsMatch(r, repo)));
|
|
6741
|
+
}
|
|
6215
6742
|
function localRegistryCheck(deps, path2, predicate) {
|
|
6216
6743
|
const text = deps.readLocalFile?.(path2);
|
|
6217
6744
|
if (text == null) return null;
|
|
@@ -6361,7 +6888,7 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6361
6888
|
}
|
|
6362
6889
|
const fanout = repo === "mutmutco/MMI-Hub" ? true : localRegistryCheck(deps, ".github/fanout-targets.json", (json) => Array.isArray(json?.repos) && json.repos.some((r) => r.repo === repo.split("/")[1] && r.branch === baseBranch));
|
|
6363
6890
|
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
6364
|
-
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && json.projects
|
|
6891
|
+
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
|
|
6365
6892
|
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
|
|
6366
6893
|
const rulesetList = await restJson2(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
6367
6894
|
const rulesets = await rulesetDetails(deps, repo, rulesetList);
|
|
@@ -6421,6 +6948,94 @@ function renderBootstrapVerifyReport(report) {
|
|
|
6421
6948
|
return lines.join("\n");
|
|
6422
6949
|
}
|
|
6423
6950
|
|
|
6951
|
+
// src/hub-auth.ts
|
|
6952
|
+
var import_node_crypto2 = require("node:crypto");
|
|
6953
|
+
var import_node_fs5 = require("node:fs");
|
|
6954
|
+
var import_node_path5 = require("node:path");
|
|
6955
|
+
var import_node_os2 = require("node:os");
|
|
6956
|
+
var REFRESH_WINDOW_MS = 10 * 60 * 1e3;
|
|
6957
|
+
var EXCHANGE_TIMEOUT_MS = 8e3;
|
|
6958
|
+
var EXCHANGE_ATTEMPTS = 2;
|
|
6959
|
+
function normalizeBaseUrl(baseUrl) {
|
|
6960
|
+
return baseUrl.replace(/\/$/, "");
|
|
6961
|
+
}
|
|
6962
|
+
function tokenFingerprint(token) {
|
|
6963
|
+
return (0, import_node_crypto2.createHash)("sha256").update(token).digest("hex");
|
|
6964
|
+
}
|
|
6965
|
+
function defaultHubSessionCachePath(env = process.env) {
|
|
6966
|
+
if (env.MMI_HUB_SESSION_CACHE) return env.MMI_HUB_SESSION_CACHE;
|
|
6967
|
+
if (process.platform === "win32") {
|
|
6968
|
+
const base2 = env.LOCALAPPDATA || (0, import_node_path5.join)((0, import_node_os2.homedir)(), "AppData", "Local");
|
|
6969
|
+
return (0, import_node_path5.join)(base2, "MMI Future", "mmi-cli", "hub-session.json");
|
|
6970
|
+
}
|
|
6971
|
+
const base = env.XDG_STATE_HOME || (0, import_node_path5.join)((0, import_node_os2.homedir)(), ".mmi");
|
|
6972
|
+
return (0, import_node_path5.join)(base, "mmi-cli", "hub-session.json");
|
|
6973
|
+
}
|
|
6974
|
+
function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
6975
|
+
try {
|
|
6976
|
+
const session = JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8"));
|
|
6977
|
+
if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return null;
|
|
6978
|
+
if (session.githubTokenFingerprint !== githubTokenFingerprint) return null;
|
|
6979
|
+
if (new Date(session.expiresAt).getTime() <= now.getTime() + REFRESH_WINDOW_MS) return null;
|
|
6980
|
+
return session;
|
|
6981
|
+
} catch {
|
|
6982
|
+
return null;
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
function writeCache(path2, session) {
|
|
6986
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(path2), { recursive: true });
|
|
6987
|
+
const tmp = `${path2}.${process.pid}.${Date.now()}.tmp`;
|
|
6988
|
+
(0, import_node_fs5.writeFileSync)(tmp, JSON.stringify(session, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
6989
|
+
try {
|
|
6990
|
+
(0, import_node_fs5.chmodSync)(tmp, 384);
|
|
6991
|
+
} catch {
|
|
6992
|
+
}
|
|
6993
|
+
(0, import_node_fs5.renameSync)(tmp, path2);
|
|
6994
|
+
try {
|
|
6995
|
+
(0, import_node_fs5.chmodSync)(path2, 384);
|
|
6996
|
+
} catch {
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6999
|
+
async function hubAuthSession(deps) {
|
|
7000
|
+
if (!deps.baseUrl) return void 0;
|
|
7001
|
+
const apiUrl = normalizeBaseUrl(deps.baseUrl);
|
|
7002
|
+
const now = deps.now?.() ?? /* @__PURE__ */ new Date();
|
|
7003
|
+
const cachePath = deps.cachePath ?? defaultHubSessionCachePath();
|
|
7004
|
+
const ghToken = await deps.githubToken();
|
|
7005
|
+
if (!ghToken) return void 0;
|
|
7006
|
+
const githubTokenFingerprint = tokenFingerprint(ghToken);
|
|
7007
|
+
const cached = readCache(cachePath, apiUrl, now, githubTokenFingerprint);
|
|
7008
|
+
if (cached) return cached;
|
|
7009
|
+
try {
|
|
7010
|
+
const res = await fetchWithRetry(
|
|
7011
|
+
deps.fetch ?? fetch,
|
|
7012
|
+
`${apiUrl}/auth/session`,
|
|
7013
|
+
{ method: "POST", headers: { Authorization: `Bearer ${ghToken}` } },
|
|
7014
|
+
{ attempts: EXCHANGE_ATTEMPTS, timeoutMs: EXCHANGE_TIMEOUT_MS }
|
|
7015
|
+
);
|
|
7016
|
+
if (!res.ok) return void 0;
|
|
7017
|
+
const body = await res.json();
|
|
7018
|
+
if (!body.token || !body.expiresAt) return void 0;
|
|
7019
|
+
const session = {
|
|
7020
|
+
token: body.token,
|
|
7021
|
+
expiresAt: body.expiresAt,
|
|
7022
|
+
login: typeof body.login === "string" ? body.login : void 0,
|
|
7023
|
+
apiUrl,
|
|
7024
|
+
githubTokenFingerprint
|
|
7025
|
+
};
|
|
7026
|
+
try {
|
|
7027
|
+
writeCache(cachePath, session);
|
|
7028
|
+
} catch {
|
|
7029
|
+
}
|
|
7030
|
+
return session;
|
|
7031
|
+
} catch {
|
|
7032
|
+
return void 0;
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
async function hubAuthToken(deps) {
|
|
7036
|
+
return (await hubAuthSession(deps))?.token;
|
|
7037
|
+
}
|
|
7038
|
+
|
|
6424
7039
|
// src/bootstrap-apply.ts
|
|
6425
7040
|
function parseOwnerRepo(repo) {
|
|
6426
7041
|
const trimmed = repo.trim();
|
|
@@ -6560,7 +7175,7 @@ function retriedFetch(deps, url, init) {
|
|
|
6560
7175
|
async function fetchTrainAuthority(repo, deps) {
|
|
6561
7176
|
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
6562
7177
|
const token = await deps.token();
|
|
6563
|
-
if (!token) return { ok: false, error: "no
|
|
7178
|
+
if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
|
|
6564
7179
|
try {
|
|
6565
7180
|
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
|
|
6566
7181
|
method: "GET",
|
|
@@ -6599,7 +7214,7 @@ async function fetchProjectBySlugChecked(slug, deps) {
|
|
|
6599
7214
|
if (!deps.baseUrl) return { ok: false, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
|
|
6600
7215
|
if (!slug) return { ok: false, error: "no slug" };
|
|
6601
7216
|
const token = await deps.token();
|
|
6602
|
-
if (!token) return { ok: false, error: "no
|
|
7217
|
+
if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
|
|
6603
7218
|
try {
|
|
6604
7219
|
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
|
|
6605
7220
|
method: "GET",
|
|
@@ -6651,7 +7266,7 @@ async function fetchOrgConfig(deps) {
|
|
|
6651
7266
|
async function postJson(pathSuffix, payload, deps, method = "POST") {
|
|
6652
7267
|
if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
|
|
6653
7268
|
const token = await deps.token();
|
|
6654
|
-
if (!token) return { ok: false, status: 0, body: null, error: "no
|
|
7269
|
+
if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
|
|
6655
7270
|
try {
|
|
6656
7271
|
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
|
|
6657
7272
|
method,
|
|
@@ -7087,7 +7702,7 @@ function parseKbTree(stdout, prefix) {
|
|
|
7087
7702
|
}
|
|
7088
7703
|
|
|
7089
7704
|
// src/plan.ts
|
|
7090
|
-
var
|
|
7705
|
+
var import_node_path6 = require("node:path");
|
|
7091
7706
|
|
|
7092
7707
|
// src/frontmatter.ts
|
|
7093
7708
|
function splitFrontmatter(content) {
|
|
@@ -7170,8 +7785,8 @@ function rankPlansByRelevance(plans, signals, opts = {}) {
|
|
|
7170
7785
|
|
|
7171
7786
|
// src/plan.ts
|
|
7172
7787
|
var PLANS_DIR = "plans";
|
|
7173
|
-
var META_FILE = (0,
|
|
7174
|
-
var planPath = (slug) => (0,
|
|
7788
|
+
var META_FILE = (0, import_node_path6.join)(PLANS_DIR, ".plan-meta.json");
|
|
7789
|
+
var planPath = (slug) => (0, import_node_path6.join)(PLANS_DIR, `${slug}.md`);
|
|
7175
7790
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
7176
7791
|
function parseMeta(raw) {
|
|
7177
7792
|
if (!raw) return {};
|
|
@@ -7603,14 +8218,16 @@ async function secretsRequest(deps, key, opts) {
|
|
|
7603
8218
|
deps.log(body.notified === false ? "admin notification not sent" : "admin notification sent");
|
|
7604
8219
|
return true;
|
|
7605
8220
|
}
|
|
7606
|
-
async function
|
|
7607
|
-
if (!isValidSecretKey(key))
|
|
7608
|
-
|
|
7609
|
-
|
|
8221
|
+
async function putSecret(deps, key, value, opts) {
|
|
8222
|
+
if (!isValidSecretKey(key)) {
|
|
8223
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
8224
|
+
return false;
|
|
8225
|
+
}
|
|
7610
8226
|
if (!value) {
|
|
7611
|
-
deps.err(
|
|
7612
|
-
return;
|
|
8227
|
+
deps.err(`secrets set: empty value for ${key} \u2014 aborted (nothing written)`);
|
|
8228
|
+
return false;
|
|
7613
8229
|
}
|
|
8230
|
+
const repo = await targetRepo(deps, opts);
|
|
7614
8231
|
const res = await deps.fetch(`${deps.apiUrl}/secrets/set`, {
|
|
7615
8232
|
method: "POST",
|
|
7616
8233
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
@@ -7621,9 +8238,19 @@ async function secretsSet(deps, key, opts) {
|
|
|
7621
8238
|
deps.err(
|
|
7622
8239
|
res.status === 403 ? `secrets set: not authorized to write ${key} (HTTP 403)${await readErr(res)}` : `secrets set failed: HTTP ${res.status}${await readErr(res)}`
|
|
7623
8240
|
);
|
|
7624
|
-
return;
|
|
8241
|
+
return false;
|
|
7625
8242
|
}
|
|
7626
8243
|
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
8244
|
+
return true;
|
|
8245
|
+
}
|
|
8246
|
+
async function secretsSet(deps, key, opts) {
|
|
8247
|
+
if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
8248
|
+
const value = await deps.readSecretValue(`value for ${key} (input hidden; will not be echoed): `);
|
|
8249
|
+
if (!value) {
|
|
8250
|
+
deps.err("secrets set: empty value \u2014 aborted (nothing written)");
|
|
8251
|
+
return;
|
|
8252
|
+
}
|
|
8253
|
+
await putSecret(deps, key, value, opts);
|
|
7627
8254
|
}
|
|
7628
8255
|
async function secretsEdit(deps, key, opts) {
|
|
7629
8256
|
return secretsSet(deps, key, opts);
|
|
@@ -7727,6 +8354,22 @@ function expectedRedirectUris(cfg) {
|
|
|
7727
8354
|
function oauthSsmKeys() {
|
|
7728
8355
|
return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
|
|
7729
8356
|
}
|
|
8357
|
+
function parseOauthClientJson(input) {
|
|
8358
|
+
let parsed;
|
|
8359
|
+
try {
|
|
8360
|
+
parsed = JSON.parse(input);
|
|
8361
|
+
} catch {
|
|
8362
|
+
throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
|
|
8363
|
+
}
|
|
8364
|
+
const root = parsed ?? {};
|
|
8365
|
+
const obj = root.web ?? root.installed ?? parsed;
|
|
8366
|
+
const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
|
|
8367
|
+
const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
|
|
8368
|
+
if (!clientId || !clientSecret) {
|
|
8369
|
+
throw new Error("missing client_id or client_secret in the JSON");
|
|
8370
|
+
}
|
|
8371
|
+
return { clientId, clientSecret };
|
|
8372
|
+
}
|
|
7730
8373
|
function parseOauthConfig(mmiConfig, slug) {
|
|
7731
8374
|
const rawUnknown = mmiConfig?.oauth;
|
|
7732
8375
|
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
@@ -7787,8 +8430,9 @@ async function awsCallerArn() {
|
|
|
7787
8430
|
return void 0;
|
|
7788
8431
|
}
|
|
7789
8432
|
}
|
|
7790
|
-
async function
|
|
7791
|
-
const
|
|
8433
|
+
async function hubHeaders(extra = {}) {
|
|
8434
|
+
const cfg = await loadConfig();
|
|
8435
|
+
const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
|
|
7792
8436
|
return t ? { ...extra, Authorization: `Bearer ${t}` } : extra;
|
|
7793
8437
|
}
|
|
7794
8438
|
async function loadConfig() {
|
|
@@ -7852,21 +8496,21 @@ function sessionDeps() {
|
|
|
7852
8496
|
env: process.env,
|
|
7853
8497
|
readPersisted: () => {
|
|
7854
8498
|
try {
|
|
7855
|
-
return (0,
|
|
8499
|
+
return (0, import_node_fs6.readFileSync)(SESSION_FILE, "utf8");
|
|
7856
8500
|
} catch {
|
|
7857
8501
|
return null;
|
|
7858
8502
|
}
|
|
7859
8503
|
},
|
|
7860
8504
|
writePersisted: (id) => persistSession(id),
|
|
7861
8505
|
now: () => /* @__PURE__ */ new Date(),
|
|
7862
|
-
rand: () => (0,
|
|
8506
|
+
rand: () => (0, import_node_crypto3.randomBytes)(4).toString("hex")
|
|
7863
8507
|
};
|
|
7864
8508
|
}
|
|
7865
8509
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
7866
8510
|
function persistSession(id) {
|
|
7867
8511
|
try {
|
|
7868
|
-
(0,
|
|
7869
|
-
(0,
|
|
8512
|
+
(0, import_node_fs6.mkdirSync)(".mmi", { recursive: true });
|
|
8513
|
+
(0, import_node_fs6.writeFileSync)(SESSION_FILE, id, "utf8");
|
|
7870
8514
|
} catch {
|
|
7871
8515
|
}
|
|
7872
8516
|
}
|
|
@@ -7884,7 +8528,7 @@ async function postCapture(capture, quiet = false) {
|
|
|
7884
8528
|
}
|
|
7885
8529
|
const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/capture`, {
|
|
7886
8530
|
method: "POST",
|
|
7887
|
-
headers: await
|
|
8531
|
+
headers: await hubHeaders({ "content-type": "application/json" }),
|
|
7888
8532
|
body: JSON.stringify({ ...capture, ...await sagaKey(cfg) })
|
|
7889
8533
|
}, { attempts: 2, timeoutMs: 2e4, retryOn: () => false });
|
|
7890
8534
|
let message = "";
|
|
@@ -7987,12 +8631,12 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7987
8631
|
}
|
|
7988
8632
|
function resolveVersion() {
|
|
7989
8633
|
try {
|
|
7990
|
-
const manifest = (0,
|
|
7991
|
-
return JSON.parse((0,
|
|
8634
|
+
const manifest = (0, import_node_path7.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
8635
|
+
return JSON.parse((0, import_node_fs6.readFileSync)(manifest, "utf8")).version || "0.0.0";
|
|
7992
8636
|
} catch {
|
|
7993
8637
|
try {
|
|
7994
|
-
const pkg = (0,
|
|
7995
|
-
return JSON.parse((0,
|
|
8638
|
+
const pkg = (0, import_node_path7.join)(__dirname, "..", "package.json");
|
|
8639
|
+
return JSON.parse((0, import_node_fs6.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
7996
8640
|
} catch {
|
|
7997
8641
|
return "0.0.0";
|
|
7998
8642
|
}
|
|
@@ -8000,7 +8644,7 @@ function resolveVersion() {
|
|
|
8000
8644
|
}
|
|
8001
8645
|
function readRepoVersion() {
|
|
8002
8646
|
try {
|
|
8003
|
-
return JSON.parse((0,
|
|
8647
|
+
return JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
8004
8648
|
} catch {
|
|
8005
8649
|
return void 0;
|
|
8006
8650
|
}
|
|
@@ -8097,10 +8741,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
8097
8741
|
for (const entry of fetched) {
|
|
8098
8742
|
if ("error" in entry) continue;
|
|
8099
8743
|
const { file, source } = entry;
|
|
8100
|
-
const current = (0,
|
|
8744
|
+
const current = (0, import_node_fs6.existsSync)(file) ? await (0, import_promises2.readFile)(file, "utf8") : null;
|
|
8101
8745
|
if (needsUpdate(source, current)) {
|
|
8102
8746
|
const slash = file.lastIndexOf("/");
|
|
8103
|
-
if (slash > 0) (0,
|
|
8747
|
+
if (slash > 0) (0, import_node_fs6.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
8104
8748
|
await (0, import_promises2.writeFile)(file, normalizeEol(source), "utf8");
|
|
8105
8749
|
changed++;
|
|
8106
8750
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -8126,7 +8770,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
8126
8770
|
return null;
|
|
8127
8771
|
}
|
|
8128
8772
|
},
|
|
8129
|
-
localContent: async (f) => (0,
|
|
8773
|
+
localContent: async (f) => (0, import_node_fs6.existsSync)(f) ? await (0, import_promises2.readFile)(f, "utf8") : null,
|
|
8130
8774
|
writeDoc: async (f, c) => {
|
|
8131
8775
|
await (0, import_promises2.writeFile)(f, c, "utf8");
|
|
8132
8776
|
}
|
|
@@ -8140,7 +8784,7 @@ docs.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
8140
8784
|
var saga = program2.command("saga").description("per-session continuity");
|
|
8141
8785
|
async function runNote(summary, o) {
|
|
8142
8786
|
const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
|
|
8143
|
-
const capture = buildNoteCapture(summary, o, (0,
|
|
8787
|
+
const capture = buildNoteCapture(summary, o, (0, import_node_crypto3.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
8144
8788
|
await postCapture(capture);
|
|
8145
8789
|
}
|
|
8146
8790
|
saga.command("note <summary>").description("record a one-line structured note into your saga (the per-turn capture)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--verified", "mark this claim as checked against source (state: verified, else asserted)").option("--diagnostic", "isolate a probe write (state: diagnostic, source: probe) \u2014 never resume/LAST 5").option("--supersedes <key>", "retire prior decisions matching an evidence key (pr:N | file:path)").option("--anchor <intent>", "set the sprint North-Star (write-protected; needs --anchor-force to change)").option("--anchor-force", "overwrite an existing anchor").action((summary, o) => runNote(summary, o));
|
|
@@ -8154,7 +8798,7 @@ async function runSagaShow(opts, io = consoleIo) {
|
|
|
8154
8798
|
try {
|
|
8155
8799
|
const key = await sagaKey(cfg);
|
|
8156
8800
|
const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
|
|
8157
|
-
const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await
|
|
8801
|
+
const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await hubHeaders() }, { attempts: 2, timeoutMs: 3e3 });
|
|
8158
8802
|
if (res.ok) {
|
|
8159
8803
|
io.log(resumeCue());
|
|
8160
8804
|
return io.log(await res.text());
|
|
@@ -8171,7 +8815,7 @@ saga.command("show").option("--quiet", "no-op silently when unconfigured/unreach
|
|
|
8171
8815
|
saga.command("capture").option("--quiet", "capture silently (for the Stop hook)").description("per-turn deterministic capture (Stop hook): turn boundary + current sha").action(async (opts) => {
|
|
8172
8816
|
const hook = parseHookInput(await readStdin());
|
|
8173
8817
|
if (hook.session_id) persistSession(hook.session_id);
|
|
8174
|
-
await postCapture({ event: "stop", id: (0,
|
|
8818
|
+
await postCapture({ event: "stop", id: (0, import_node_crypto3.randomUUID)(), source: "hook", sha: await gitOut(["rev-parse", "--short", "HEAD"]), surface: agentSurface() }, opts.quiet ?? false);
|
|
8175
8819
|
});
|
|
8176
8820
|
saga.command("session").option("--quiet", "silent (for the SessionStart hook)").description("persist the harness session id for this repo (SessionStart hook)").action(async () => {
|
|
8177
8821
|
const hook = parseHookInput(await readStdin());
|
|
@@ -8198,7 +8842,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
8198
8842
|
if (!cfg.sagaApiUrl) return;
|
|
8199
8843
|
const key = await sagaKey(cfg);
|
|
8200
8844
|
const qs = new URLSearchParams(key).toString();
|
|
8201
|
-
const res = await fetch(`${cfg.sagaApiUrl}/saga/state?${qs}`, { headers: await
|
|
8845
|
+
const res = await fetch(`${cfg.sagaApiUrl}/saga/state?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
|
|
8202
8846
|
if (!res.ok) return;
|
|
8203
8847
|
const state = await res.json();
|
|
8204
8848
|
if (!state.actionLog?.length) return;
|
|
@@ -8206,7 +8850,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
8206
8850
|
if (!update) return;
|
|
8207
8851
|
await fetch(`${cfg.sagaApiUrl}/saga/head`, {
|
|
8208
8852
|
method: "POST",
|
|
8209
|
-
headers: await
|
|
8853
|
+
headers: await hubHeaders({ "content-type": "application/json" }),
|
|
8210
8854
|
body: JSON.stringify({ ...update, ...key }),
|
|
8211
8855
|
signal: AbortSignal.timeout(2e4)
|
|
8212
8856
|
});
|
|
@@ -8223,7 +8867,7 @@ saga.command("key").option("--json", "machine-readable output").description("pri
|
|
|
8223
8867
|
});
|
|
8224
8868
|
async function probeBackend(url) {
|
|
8225
8869
|
try {
|
|
8226
|
-
const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await
|
|
8870
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await hubHeaders() }, { attempts: 3, timeoutMs: 4e3 });
|
|
8227
8871
|
let message = "";
|
|
8228
8872
|
try {
|
|
8229
8873
|
const body = await res.clone().json();
|
|
@@ -8238,7 +8882,7 @@ async function probeBackend(url) {
|
|
|
8238
8882
|
async function probeSagaAccess(url, key) {
|
|
8239
8883
|
try {
|
|
8240
8884
|
const qs = new URLSearchParams(key).toString();
|
|
8241
|
-
const res = await fetch(`${url}/saga/state?${qs}`, { headers: await
|
|
8885
|
+
const res = await fetch(`${url}/saga/state?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
|
|
8242
8886
|
return res.ok;
|
|
8243
8887
|
} catch {
|
|
8244
8888
|
return false;
|
|
@@ -8250,7 +8894,7 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
8250
8894
|
const key = await sagaKey(cfg, session);
|
|
8251
8895
|
const source = session.source;
|
|
8252
8896
|
const [identity, liveness] = await Promise.all([
|
|
8253
|
-
|
|
8897
|
+
hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }).then((s) => s?.login),
|
|
8254
8898
|
cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl) : Promise.resolve({ reachable: false })
|
|
8255
8899
|
]);
|
|
8256
8900
|
const authorized = cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
|
|
@@ -8387,6 +9031,7 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
8387
9031
|
return void 0;
|
|
8388
9032
|
}
|
|
8389
9033
|
}
|
|
9034
|
+
var ghRunner = async (args, timeoutMs) => (await execFileP4("gh", args, { timeout: timeoutMs })).stdout;
|
|
8390
9035
|
function scheduleRelatedDiscovery(o) {
|
|
8391
9036
|
try {
|
|
8392
9037
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
@@ -8401,39 +9046,39 @@ function scheduleRelatedDiscovery(o) {
|
|
|
8401
9046
|
}
|
|
8402
9047
|
}
|
|
8403
9048
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
8404
|
-
const ensureDir = () => (0,
|
|
9049
|
+
const ensureDir = () => (0, import_node_fs6.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
8405
9050
|
return {
|
|
8406
9051
|
apiUrl: cfg.sagaApiUrl,
|
|
8407
9052
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
8408
|
-
headers: (extra) =>
|
|
9053
|
+
headers: (extra) => hubHeaders(extra),
|
|
8409
9054
|
project: async () => (await sagaKey(cfg)).project,
|
|
8410
9055
|
readLocal: (slug) => {
|
|
8411
9056
|
try {
|
|
8412
|
-
return (0,
|
|
9057
|
+
return (0, import_node_fs6.readFileSync)(planPath(slug), "utf8");
|
|
8413
9058
|
} catch {
|
|
8414
9059
|
return null;
|
|
8415
9060
|
}
|
|
8416
9061
|
},
|
|
8417
9062
|
writeLocal: (slug, content) => {
|
|
8418
9063
|
ensureDir();
|
|
8419
|
-
(0,
|
|
9064
|
+
(0, import_node_fs6.writeFileSync)(planPath(slug), content, "utf8");
|
|
8420
9065
|
},
|
|
8421
9066
|
removeLocal: (slug) => {
|
|
8422
9067
|
try {
|
|
8423
|
-
(0,
|
|
9068
|
+
(0, import_node_fs6.rmSync)(planPath(slug));
|
|
8424
9069
|
} catch {
|
|
8425
9070
|
}
|
|
8426
9071
|
},
|
|
8427
9072
|
readMetaRaw: () => {
|
|
8428
9073
|
try {
|
|
8429
|
-
return (0,
|
|
9074
|
+
return (0, import_node_fs6.readFileSync)(META_FILE, "utf8");
|
|
8430
9075
|
} catch {
|
|
8431
9076
|
return null;
|
|
8432
9077
|
}
|
|
8433
9078
|
},
|
|
8434
9079
|
writeMetaRaw: (raw) => {
|
|
8435
9080
|
ensureDir();
|
|
8436
|
-
(0,
|
|
9081
|
+
(0, import_node_fs6.writeFileSync)(META_FILE, raw, "utf8");
|
|
8437
9082
|
},
|
|
8438
9083
|
log: (m) => io.log(m),
|
|
8439
9084
|
err: (m) => io.err(m),
|
|
@@ -8528,7 +9173,7 @@ function makeSecretsDeps(cfg) {
|
|
|
8528
9173
|
return {
|
|
8529
9174
|
apiUrl: cfg.sagaApiUrl,
|
|
8530
9175
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
8531
|
-
headers: (extra) =>
|
|
9176
|
+
headers: (extra) => hubHeaders(extra),
|
|
8532
9177
|
// Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
|
|
8533
9178
|
// casing, which leaked mixed-case into `secrets where` output (#681).
|
|
8534
9179
|
slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
|
|
@@ -8581,7 +9226,7 @@ secrets.command("use <key>").description("print guidance on consuming a secret w
|
|
|
8581
9226
|
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
8582
9227
|
secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-tier secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
|
|
8583
9228
|
function registryClientDeps(cfg) {
|
|
8584
|
-
return { baseUrl: cfg.sagaApiUrl, token: githubToken };
|
|
9229
|
+
return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
8585
9230
|
}
|
|
8586
9231
|
function slugOf(repoOrSlug) {
|
|
8587
9232
|
return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
|
|
@@ -8596,11 +9241,20 @@ function reportWrite(label, res) {
|
|
|
8596
9241
|
fail(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
8597
9242
|
}
|
|
8598
9243
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
8599
|
-
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action) => {
|
|
9244
|
+
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, plus rc-only retire) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action) => {
|
|
8600
9245
|
const cfg = await loadConfig();
|
|
8601
9246
|
const res = await tenantControl({ repo, stage: stage2, action }, registryClientDeps(cfg));
|
|
8602
9247
|
reportWrite("tenant control", res);
|
|
8603
9248
|
});
|
|
9249
|
+
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
|
|
9250
|
+
if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
|
|
9251
|
+
try {
|
|
9252
|
+
const result = await runTenantRedeploy(trainApplyDeps(), { repo, stage: stage2, ref: o.ref, watch: o.watch });
|
|
9253
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantRedeploy(result));
|
|
9254
|
+
} catch (e) {
|
|
9255
|
+
return fail(`tenant redeploy: ${e.message}`);
|
|
9256
|
+
}
|
|
9257
|
+
});
|
|
8604
9258
|
async function v2ReadinessDeps(cfg) {
|
|
8605
9259
|
const reg = registryClientDeps(cfg);
|
|
8606
9260
|
return {
|
|
@@ -8620,7 +9274,7 @@ async function v2ReadinessDeps(cfg) {
|
|
|
8620
9274
|
const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
|
|
8621
9275
|
const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
|
|
8622
9276
|
method: "GET",
|
|
8623
|
-
headers: await
|
|
9277
|
+
headers: await hubHeaders()
|
|
8624
9278
|
}, { attempts: 2, timeoutMs: 5e3 });
|
|
8625
9279
|
if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
|
|
8626
9280
|
const body = await res.json();
|
|
@@ -8680,9 +9334,14 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
8680
9334
|
} catch (e) {
|
|
8681
9335
|
return fail(e.message);
|
|
8682
9336
|
}
|
|
8683
|
-
const
|
|
8684
|
-
if (!
|
|
8685
|
-
|
|
9337
|
+
const read = await fetchProjectBySlugChecked(slugOf(target), registryClientDeps(cfg));
|
|
9338
|
+
if (!read.ok) {
|
|
9339
|
+
return fail(`project get: Hub registry read failed (${read.error}) \u2014 likely transient (cold start, network, or auth blip); retry shortly`);
|
|
9340
|
+
}
|
|
9341
|
+
if (!read.project) {
|
|
9342
|
+
return fail(`project get: no registry META for ${target} (unknown or unbootstrapped)`);
|
|
9343
|
+
}
|
|
9344
|
+
console.log(JSON.stringify(read.project));
|
|
8686
9345
|
});
|
|
8687
9346
|
project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
|
|
8688
9347
|
const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect the DEPLOY# item via the AWS console / a master DDB read instead.";
|
|
@@ -8814,7 +9473,29 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
|
|
|
8814
9473
|
console.log(`
|
|
8815
9474
|
SSM cred params (under /mmi-future/${slug}/):`);
|
|
8816
9475
|
ssm.forEach((k) => console.log(` ${k}`));
|
|
8817
|
-
console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; creds
|
|
9476
|
+
console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli oauth set-creds`.");
|
|
9477
|
+
});
|
|
9478
|
+
oauth.command("set-creds").description('store the OAuth client into the canonical {dev,rc,main}/GOOGLE_CLIENT_* SSM keys (pipe the Console "Download JSON" on stdin)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
9479
|
+
const raw = await readStdin();
|
|
9480
|
+
if (!raw.trim()) {
|
|
9481
|
+
return fail("oauth set-creds: pipe the Google client JSON on stdin \u2014 e.g.\n mmi-cli oauth set-creds --repo <owner/repo> < client.json");
|
|
9482
|
+
}
|
|
9483
|
+
let creds;
|
|
9484
|
+
try {
|
|
9485
|
+
creds = parseOauthClientJson(raw);
|
|
9486
|
+
} catch (e) {
|
|
9487
|
+
return fail(`oauth set-creds: ${e.message}`);
|
|
9488
|
+
}
|
|
9489
|
+
await withSecrets(async (d) => {
|
|
9490
|
+
for (const key of oauthSsmKeys()) {
|
|
9491
|
+
const value = key.endsWith("GOOGLE_CLIENT_ID") ? creds.clientId : creds.clientSecret;
|
|
9492
|
+
if (!await putSecret(d, key, value, { repo: o.repo })) {
|
|
9493
|
+
process.exitCode = 1;
|
|
9494
|
+
return;
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
console.log(`OAuth client stored in all ${oauthSsmKeys().length} canonical keys. Run \`mmi-cli oauth verify\` to confirm the client is port-agnostic.`);
|
|
9498
|
+
});
|
|
8818
9499
|
});
|
|
8819
9500
|
oauth.command("verify").description("probe Google authorize with an arbitrary port (:9123) to confirm the client is port-agnostic").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--client-id <id>", "OAuth client_id (else read dev/GOOGLE_CLIENT_ID from SSM)").option("--json", "machine-readable output").action(async (o) => {
|
|
8820
9501
|
const cfg = await loadConfig();
|
|
@@ -8854,7 +9535,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
8854
9535
|
if (mismatch) process.exitCode = 1;
|
|
8855
9536
|
});
|
|
8856
9537
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
8857
|
-
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)").requiredOption("--title <title>", "issue title").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").requiredOption("--priority <priority>", "urgent | high | medium | low (label + board Priority field when configured)").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("--no-related", "skip the auto related-issues comment").action(async (o) => {
|
|
9538
|
+
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)").requiredOption("--title <title>", "issue title").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").requiredOption("--priority <priority>", "urgent | high | medium | low (label + board Priority field when configured)").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) => {
|
|
8858
9539
|
let args;
|
|
8859
9540
|
let priority;
|
|
8860
9541
|
let body;
|
|
@@ -8862,6 +9543,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8862
9543
|
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
|
|
8863
9544
|
priority = normalizePriority(o.priority);
|
|
8864
9545
|
args = buildIssueArgs({ type: o.type, title: o.title, body, priority, repo: o.repo, labels: o.label });
|
|
9546
|
+
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
8865
9547
|
} catch (e) {
|
|
8866
9548
|
return fail(`issue create: ${e.message}`);
|
|
8867
9549
|
}
|
|
@@ -8875,8 +9557,20 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8875
9557
|
}
|
|
8876
9558
|
const created = await ghCreate(args);
|
|
8877
9559
|
const projectItemId = await attachToProject(created.number, o.repo, priority);
|
|
9560
|
+
let parent;
|
|
9561
|
+
let parentLinkError;
|
|
9562
|
+
if (o.parent !== void 0) {
|
|
9563
|
+
try {
|
|
9564
|
+
parent = await linkSubIssue(ghRunner, o.parent, created.url, o.repo);
|
|
9565
|
+
} catch (e) {
|
|
9566
|
+
const err = e;
|
|
9567
|
+
parentLinkError = (err.stderr || err.message || String(e)).trim();
|
|
9568
|
+
process.stderr.write(`warning: issue #${created.number} created but NOT linked under ${o.parent}: ${parentLinkError}
|
|
9569
|
+
`);
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
8878
9572
|
if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title: o.title, body });
|
|
8879
|
-
console.log(JSON.stringify({ ...created, label: o.type, priority, projectItemId }));
|
|
9573
|
+
console.log(JSON.stringify({ ...created, label: o.type, priority, projectItemId, ...parentLinkFields(parent, parentLinkError) }));
|
|
8880
9574
|
});
|
|
8881
9575
|
issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
|
|
8882
9576
|
const number = Number(o.number);
|
|
@@ -8913,6 +9607,17 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8913
9607
|
} catch {
|
|
8914
9608
|
}
|
|
8915
9609
|
});
|
|
9610
|
+
issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)").action(async (parentRef, childRef, o) => {
|
|
9611
|
+
const defaultRepo = await resolveRepo(o.repo);
|
|
9612
|
+
try {
|
|
9613
|
+
const result = await linkSubIssue(ghRunner, parentRef, childRef, defaultRepo);
|
|
9614
|
+
console.log(JSON.stringify(result));
|
|
9615
|
+
} catch (e) {
|
|
9616
|
+
const err = e;
|
|
9617
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
9618
|
+
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
9619
|
+
}
|
|
9620
|
+
});
|
|
8916
9621
|
program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").requiredOption("--title <title>", "one-line friction summary").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
|
|
8917
9622
|
let body;
|
|
8918
9623
|
let priority;
|
|
@@ -8991,6 +9696,20 @@ async function remoteBranchExists(branch) {
|
|
|
8991
9696
|
return void 0;
|
|
8992
9697
|
}
|
|
8993
9698
|
}
|
|
9699
|
+
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
9700
|
+
function teardownWorktreeStage(worktreePath) {
|
|
9701
|
+
return runWorktreeStageTeardown(worktreePath, {
|
|
9702
|
+
hasStageState: (wt) => (0, import_node_fs6.existsSync)(stageStatePath(wt)),
|
|
9703
|
+
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
9704
|
+
listComposeProjects: async () => {
|
|
9705
|
+
const { stdout } = await execFileP4("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
9706
|
+
return parseComposeLs(stdout);
|
|
9707
|
+
},
|
|
9708
|
+
composeDown: async (project2) => {
|
|
9709
|
+
await execFileP4("docker", ["compose", "-p", project2, "down", "-v"], { timeout: COMPOSE_TIMEOUT_MS });
|
|
9710
|
+
}
|
|
9711
|
+
});
|
|
9712
|
+
}
|
|
8994
9713
|
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
8995
9714
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
8996
9715
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
@@ -9027,7 +9746,8 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
9027
9746
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
9028
9747
|
beforeWorktrees,
|
|
9029
9748
|
startingPath,
|
|
9030
|
-
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
9749
|
+
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
9750
|
+
teardownWorktreeStage
|
|
9031
9751
|
});
|
|
9032
9752
|
console.log(JSON.stringify({
|
|
9033
9753
|
merged: number,
|
|
@@ -9143,16 +9863,50 @@ function rawValues(flag) {
|
|
|
9143
9863
|
return out;
|
|
9144
9864
|
}
|
|
9145
9865
|
function printLine(value) {
|
|
9146
|
-
(0,
|
|
9866
|
+
(0, import_node_fs6.writeSync)(1, `${value}
|
|
9147
9867
|
`);
|
|
9148
9868
|
}
|
|
9149
9869
|
function stageKeepAlive() {
|
|
9150
9870
|
return setTimeout(() => void 0, 5 * 60 * 1e3);
|
|
9151
9871
|
}
|
|
9872
|
+
async function resolveStage() {
|
|
9873
|
+
const cfg = await loadConfig();
|
|
9874
|
+
const local = cfg.stage;
|
|
9875
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch((e) => ({ ok: false, error: e.message }));
|
|
9876
|
+
const project2 = read.ok ? read.project : null;
|
|
9877
|
+
const portRangeMeta = project2?.portRange ?? void 0;
|
|
9878
|
+
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
9879
|
+
return decideStage({
|
|
9880
|
+
local,
|
|
9881
|
+
shell: shellFor(),
|
|
9882
|
+
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
9883
|
+
hasCompose: (0, import_node_fs6.existsSync)((0, import_node_path7.join)(process.cwd(), "docker-compose.yml")),
|
|
9884
|
+
hasEnvExample: (0, import_node_fs6.existsSync)((0, import_node_path7.join)(process.cwd(), ".env.example"))
|
|
9885
|
+
});
|
|
9886
|
+
}
|
|
9887
|
+
function stageStepsFor(res, stops = true) {
|
|
9888
|
+
if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
|
|
9889
|
+
if (res.source === "local") return stagePlan(res.config ?? {}, stops);
|
|
9890
|
+
return [{ label: `no local stage to run \u2014 ${res.gap ?? "stage config gap"}` }];
|
|
9891
|
+
}
|
|
9892
|
+
function staleStageNote(res) {
|
|
9893
|
+
if (!res.staleIgnored) return null;
|
|
9894
|
+
const fields = res.staleFields ?? [];
|
|
9895
|
+
const list = fields.join(", ");
|
|
9896
|
+
const label = fields.length > 1 ? `fields ${list}` : `field ${list || "build/up"}`;
|
|
9897
|
+
if (res.source === "local") {
|
|
9898
|
+
return `note: POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 kept the rest of the local recipe`;
|
|
9899
|
+
}
|
|
9900
|
+
return `note: stale POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 using the registry-derived default`;
|
|
9901
|
+
}
|
|
9902
|
+
function reportedStageUrl(res, result) {
|
|
9903
|
+
if (!res.derived) return void 0;
|
|
9904
|
+
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
9905
|
+
}
|
|
9152
9906
|
program2.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (committed-file fallback)").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
9153
|
-
const path2 = (0,
|
|
9907
|
+
const path2 = (0, import_node_path7.join)(process.cwd(), "infra", "port-ranges.json");
|
|
9154
9908
|
const allocate = async (seed) => {
|
|
9155
|
-
const { stdout } = await execFileP4("node", [(0,
|
|
9909
|
+
const { stdout } = await execFileP4("node", [(0, import_node_path7.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
9156
9910
|
const parsed = JSON.parse(stdout);
|
|
9157
9911
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
9158
9912
|
return parsed.range;
|
|
@@ -9161,20 +9915,27 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
9161
9915
|
printLine(o.json ? JSON.stringify({ repo, portRange: [start, end] }) : `${repo}: stage.portRange [${start}, ${end}]`);
|
|
9162
9916
|
});
|
|
9163
9917
|
var stage = program2.command("stage").description("plan or run the repo local stage environment").option("--json", "machine-readable output").option("--apply", "run the full local stage: stop previous, build, start, health-check").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async (o) => {
|
|
9164
|
-
const
|
|
9918
|
+
const res = await resolveStage();
|
|
9165
9919
|
if (o.apply) {
|
|
9920
|
+
if (res.source === "none") return fail(`stage: ${res.gap}`);
|
|
9921
|
+
const cfg = res.config ?? res.derived.config;
|
|
9166
9922
|
const hold = stageKeepAlive();
|
|
9167
9923
|
try {
|
|
9168
9924
|
const result = await runStage(cfg, { timeoutMs: Number(o.timeoutMs || 6e4) });
|
|
9169
|
-
|
|
9925
|
+
const reportUrl = reportedStageUrl(res, result);
|
|
9926
|
+
const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
|
|
9927
|
+
return printLine(o.json ? JSON.stringify({ ...result, source: res.source, url: reportUrl }) : `mmi-cli stage: ${result.message}${url}`);
|
|
9170
9928
|
} catch (e) {
|
|
9171
9929
|
return fail(`stage: ${e.message}`);
|
|
9172
9930
|
} finally {
|
|
9173
9931
|
clearTimeout(hold);
|
|
9174
9932
|
}
|
|
9175
9933
|
}
|
|
9176
|
-
const steps =
|
|
9177
|
-
|
|
9934
|
+
const steps = stageStepsFor(res);
|
|
9935
|
+
if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url, staleIgnored: res.staleIgnored, staleFields: res.staleFields, registryError: res.registryError, steps }, null, 2));
|
|
9936
|
+
const note = staleStageNote(res);
|
|
9937
|
+
if (note) printLine(note);
|
|
9938
|
+
console.log(renderSteps("mmi-cli stage: dry-run plan", steps));
|
|
9178
9939
|
});
|
|
9179
9940
|
stage.command("stop").description("stop the previous local stage process recorded in tmp/stage/state.json").option("--json", "machine-readable output").option("--apply", "kill the recorded process tree and remove the state file").action(async () => {
|
|
9180
9941
|
const o = { json: rawFlag("--json"), apply: rawFlag("--apply") };
|
|
@@ -9191,11 +9952,16 @@ stage.command("stop").description("stop the previous local stage process recorde
|
|
|
9191
9952
|
});
|
|
9192
9953
|
stage.command("start").description("start the configured local stage process and optionally wait for health").option("--json", "machine-readable output").option("--apply", "start the configured stage.up process").option("--timeout-ms <ms>", "bounded health timeout", "60000").action(async () => {
|
|
9193
9954
|
const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000") };
|
|
9194
|
-
const
|
|
9955
|
+
const res = await resolveStage();
|
|
9195
9956
|
if (!o.apply) {
|
|
9196
|
-
const steps =
|
|
9197
|
-
|
|
9198
|
-
|
|
9957
|
+
const steps = stageStepsFor(res, false);
|
|
9958
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url, staleIgnored: res.staleIgnored, staleFields: res.staleFields, registryError: res.registryError, steps }, null, 2));
|
|
9959
|
+
const note = staleStageNote(res);
|
|
9960
|
+
if (note) printLine(note);
|
|
9961
|
+
return printLine(renderSteps("mmi-cli stage start: dry-run plan", steps));
|
|
9962
|
+
}
|
|
9963
|
+
if (res.source === "none") return fail(`stage start: ${res.gap}`);
|
|
9964
|
+
const cfg = res.config ?? res.derived.config;
|
|
9199
9965
|
try {
|
|
9200
9966
|
const hold = stageKeepAlive();
|
|
9201
9967
|
let printed = false;
|
|
@@ -9204,10 +9970,12 @@ stage.command("start").description("start the configured local stage process and
|
|
|
9204
9970
|
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
9205
9971
|
onReady: (ready) => {
|
|
9206
9972
|
printed = true;
|
|
9207
|
-
|
|
9973
|
+
const reportUrl = reportedStageUrl(res, ready);
|
|
9974
|
+
const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
|
|
9975
|
+
printLine(o.json ? JSON.stringify({ ...ready, source: res.source, url: reportUrl }) : `mmi-cli stage start: ${ready.message}${url}`);
|
|
9208
9976
|
}
|
|
9209
9977
|
});
|
|
9210
|
-
if (!printed) printLine(o.json ? JSON.stringify(result) : `mmi-cli stage start: ${result.message}`);
|
|
9978
|
+
if (!printed) printLine(o.json ? JSON.stringify({ ...result, source: res.source, url: reportedStageUrl(res, result) }) : `mmi-cli stage start: ${result.message}`);
|
|
9211
9979
|
} finally {
|
|
9212
9980
|
clearTimeout(hold);
|
|
9213
9981
|
}
|
|
@@ -9217,11 +9985,16 @@ stage.command("start").description("start the configured local stage process and
|
|
|
9217
9985
|
});
|
|
9218
9986
|
stage.command("run").description("force-stop previous stage, build, start, and health-check").option("--json", "machine-readable output").option("--apply", "run the configured stage sequence").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async () => {
|
|
9219
9987
|
const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000") };
|
|
9220
|
-
const
|
|
9988
|
+
const res = await resolveStage();
|
|
9221
9989
|
if (!o.apply) {
|
|
9222
|
-
const steps =
|
|
9223
|
-
|
|
9224
|
-
|
|
9990
|
+
const steps = stageStepsFor(res);
|
|
9991
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url, staleIgnored: res.staleIgnored, staleFields: res.staleFields, registryError: res.registryError, steps }, null, 2));
|
|
9992
|
+
const note = staleStageNote(res);
|
|
9993
|
+
if (note) printLine(note);
|
|
9994
|
+
return printLine(renderSteps("mmi-cli stage run: dry-run plan", steps));
|
|
9995
|
+
}
|
|
9996
|
+
if (res.source === "none") return fail(`stage run: ${res.gap}`);
|
|
9997
|
+
const cfg = res.config ?? res.derived.config;
|
|
9225
9998
|
try {
|
|
9226
9999
|
const hold = stageKeepAlive();
|
|
9227
10000
|
let printed = false;
|
|
@@ -9229,12 +10002,14 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
9229
10002
|
const result = await runStage(cfg, {
|
|
9230
10003
|
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
9231
10004
|
onReady: (ready) => {
|
|
9232
|
-
const
|
|
10005
|
+
const reportUrl = reportedStageUrl(res, ready);
|
|
10006
|
+
const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
|
|
10007
|
+
const runReady = { ...ready, action: "run", source: res.source, url: reportUrl, message: `built and ${ready.message}${url}` };
|
|
9233
10008
|
printed = true;
|
|
9234
10009
|
printLine(o.json ? JSON.stringify(runReady) : `mmi-cli stage run: ${runReady.message}`);
|
|
9235
10010
|
}
|
|
9236
10011
|
});
|
|
9237
|
-
if (!printed) printLine(o.json ? JSON.stringify(result) : `mmi-cli stage run: ${result.message}`);
|
|
10012
|
+
if (!printed) printLine(o.json ? JSON.stringify({ ...result, source: res.source, url: reportedStageUrl(res, result) }) : `mmi-cli stage run: ${result.message}`);
|
|
9238
10013
|
} finally {
|
|
9239
10014
|
clearTimeout(hold);
|
|
9240
10015
|
}
|
|
@@ -9247,8 +10022,38 @@ program2.command("stage-live").description("explain that remote rc/live environm
|
|
|
9247
10022
|
const steps = stageLivePlan();
|
|
9248
10023
|
console.log(o.json ? JSON.stringify({ command: "stage-live", steps }, null, 2) : renderSteps("mmi-cli stage-live: not an org command", steps));
|
|
9249
10024
|
});
|
|
10025
|
+
var GH_TRAIN_TIMEOUT_MS = 3e4;
|
|
10026
|
+
var GH_RUN_WATCH_TIMEOUT_MS = 20 * 6e4;
|
|
10027
|
+
function trainApplyDeps() {
|
|
10028
|
+
return {
|
|
10029
|
+
run: async (file, args) => {
|
|
10030
|
+
const timeout = file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
|
|
10031
|
+
return (await execFileP4(file, args, { timeout })).stdout;
|
|
10032
|
+
},
|
|
10033
|
+
runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
|
|
10034
|
+
trainAuthority: async (repo) => {
|
|
10035
|
+
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
10036
|
+
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
10037
|
+
}
|
|
10038
|
+
};
|
|
10039
|
+
}
|
|
10040
|
+
function renderDeployLine(d) {
|
|
10041
|
+
const parts = [d.dispatch];
|
|
10042
|
+
if (d.runUrl) parts.push(`run ${d.runUrl}`);
|
|
10043
|
+
if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
|
|
10044
|
+
else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
|
|
10045
|
+
else if (d.runId != null) parts.push(`deploy: dispatched (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
10046
|
+
return parts.join("; ");
|
|
10047
|
+
}
|
|
10048
|
+
function renderTrainApply(commandName, r) {
|
|
10049
|
+
const base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
10050
|
+
return r.rcRetirement ? `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})` : base;
|
|
10051
|
+
}
|
|
10052
|
+
function renderTenantRedeploy(r) {
|
|
10053
|
+
return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
10054
|
+
}
|
|
9250
10055
|
for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
9251
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--apply", commandName === "hotfix" ? "reserved; hotfix uses the /hotfix skill PR path" : "execute the guarded master-only train after explicit approval").action(async (o) => {
|
|
10056
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the dispatched tenant-deploy.yml run and report its outcome (tenant-container)").option("--apply", commandName === "hotfix" ? "reserved; hotfix uses the /hotfix skill PR path" : "execute the guarded master-only train after explicit approval").action(async (o) => {
|
|
9252
10057
|
try {
|
|
9253
10058
|
await requireFreshTrainCli(commandName);
|
|
9254
10059
|
} catch (e) {
|
|
@@ -9257,16 +10062,8 @@ for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
|
9257
10062
|
if (o.apply) {
|
|
9258
10063
|
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
9259
10064
|
try {
|
|
9260
|
-
const result = await runTrainApply(commandName, {
|
|
9261
|
-
|
|
9262
|
-
runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
|
|
9263
|
-
trainAuthority: async (repo) => {
|
|
9264
|
-
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
9265
|
-
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
9266
|
-
}
|
|
9267
|
-
});
|
|
9268
|
-
const message = `mmi-cli ${commandName}: applied ${result.repo} ${result.stage} train at ${result.tag} [${result.deployModel}]; ${result.dispatch}`;
|
|
9269
|
-
return printLine(o.json ? JSON.stringify(result, null, 2) : message);
|
|
10065
|
+
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch });
|
|
10066
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
|
|
9270
10067
|
} catch (e) {
|
|
9271
10068
|
return fail(`${commandName}: ${e.message}`);
|
|
9272
10069
|
}
|
|
@@ -9286,13 +10083,14 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
9286
10083
|
const o = { class: rawValue("--class", "deployable"), json: rawFlag("--json") };
|
|
9287
10084
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap verify: --class must be deployable or content");
|
|
9288
10085
|
const cfg = await loadConfig();
|
|
9289
|
-
const
|
|
10086
|
+
const reg = registryClientDeps(cfg);
|
|
10087
|
+
const apiProjects = await fetchProjectsJson(reg);
|
|
9290
10088
|
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
9291
|
-
const meta = await fetchProjectBySlug(slug,
|
|
10089
|
+
const meta = await fetchProjectBySlug(slug, reg);
|
|
9292
10090
|
const report = await verifyBootstrap(repo, o.class, {
|
|
9293
10091
|
client: defaultGitHubClient(),
|
|
9294
10092
|
projectMeta: meta,
|
|
9295
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
10093
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs6.existsSync)(path2) ? (0, import_node_fs6.readFileSync)(path2, "utf8") : null,
|
|
9296
10094
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
9297
10095
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
9298
10096
|
requiredGcpApis: (() => {
|
|
@@ -9333,12 +10131,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
9333
10131
|
return fail(`bootstrap apply: ${e.message}`);
|
|
9334
10132
|
}
|
|
9335
10133
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
9336
|
-
if (!(0,
|
|
9337
|
-
const manifest = loadBootstrapSeeds((0,
|
|
10134
|
+
if (!(0, import_node_fs6.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
10135
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs6.readFileSync)(manifestPath, "utf8"));
|
|
9338
10136
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
9339
10137
|
const slug = parsedRepo.slug;
|
|
9340
10138
|
const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
|
|
9341
|
-
const readFile2 = (p) => (0,
|
|
10139
|
+
const readFile2 = (p) => (0, import_node_fs6.existsSync)(p) ? (0, import_node_fs6.readFileSync)(p, "utf8") : null;
|
|
9342
10140
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
9343
10141
|
const vars = {};
|
|
9344
10142
|
for (const value of rawValues("--var")) {
|
|
@@ -9411,7 +10209,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
9411
10209
|
}
|
|
9412
10210
|
if (o.execute) {
|
|
9413
10211
|
const cfg = await loadConfig();
|
|
9414
|
-
const res = await registerProject(registerPayload,
|
|
10212
|
+
const res = await registerProject(registerPayload, registryClientDeps(cfg));
|
|
9415
10213
|
if (res.ok) {
|
|
9416
10214
|
ddbWrites.push({ slug: registerPayload.slug, action: "register", record: registerPayload });
|
|
9417
10215
|
applied.push(`ddb register ${registerPayload.slug}`);
|
|
@@ -9462,16 +10260,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
9462
10260
|
if (o.class !== "deployable" && o.class !== "content") return fail("access audit: --class must be deployable or content");
|
|
9463
10261
|
targets = [{ repo: o.repo, class: o.class }];
|
|
9464
10262
|
} else {
|
|
9465
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
10263
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs6.existsSync)("projects.json") ? (0, import_node_fs6.readFileSync)("projects.json", "utf8") : null;
|
|
9466
10264
|
if (!projectsJson) return fail("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>");
|
|
9467
|
-
const fanoutJson = (0,
|
|
10265
|
+
const fanoutJson = (0, import_node_fs6.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs6.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
9468
10266
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
9469
10267
|
}
|
|
9470
10268
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
9471
|
-
const fileMatrix = (0,
|
|
10269
|
+
const fileMatrix = (0, import_node_fs6.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs6.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
9472
10270
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
9473
10271
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
9474
|
-
const fileContracts = (0,
|
|
10272
|
+
const fileContracts = (0, import_node_fs6.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs6.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
9475
10273
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
9476
10274
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
9477
10275
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -9480,18 +10278,18 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
9480
10278
|
var isWin = process.platform === "win32";
|
|
9481
10279
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
9482
10280
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
9483
|
-
return (0,
|
|
10281
|
+
return (0, import_node_path7.join)((0, import_node_os3.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
9484
10282
|
};
|
|
9485
10283
|
function readInstalledPlugins() {
|
|
9486
10284
|
try {
|
|
9487
|
-
return JSON.parse((0,
|
|
10285
|
+
return JSON.parse((0, import_node_fs6.readFileSync)(installedPluginsPath(), "utf8"));
|
|
9488
10286
|
} catch {
|
|
9489
10287
|
return null;
|
|
9490
10288
|
}
|
|
9491
10289
|
}
|
|
9492
10290
|
function readClaudeSettings() {
|
|
9493
10291
|
try {
|
|
9494
|
-
return JSON.parse((0,
|
|
10292
|
+
return JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
9495
10293
|
} catch {
|
|
9496
10294
|
return null;
|
|
9497
10295
|
}
|
|
@@ -9513,7 +10311,7 @@ function writeProjectInstallRecord(record) {
|
|
|
9513
10311
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
9514
10312
|
list.push(record);
|
|
9515
10313
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
9516
|
-
(0,
|
|
10314
|
+
(0, import_node_fs6.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
9517
10315
|
`, "utf8");
|
|
9518
10316
|
return true;
|
|
9519
10317
|
} catch {
|
|
@@ -9526,9 +10324,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
9526
10324
|
if (!file) return false;
|
|
9527
10325
|
if (!file.plugins) file.plugins = {};
|
|
9528
10326
|
const path2 = installedPluginsPath();
|
|
9529
|
-
(0,
|
|
10327
|
+
(0, import_node_fs6.copyFileSync)(path2, `${path2}.bak`);
|
|
9530
10328
|
file.plugins[pluginId] = records;
|
|
9531
|
-
(0,
|
|
10329
|
+
(0, import_node_fs6.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
9532
10330
|
`, "utf8");
|
|
9533
10331
|
return true;
|
|
9534
10332
|
} catch {
|
|
@@ -9537,14 +10335,14 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
9537
10335
|
}
|
|
9538
10336
|
function mmiPluginCacheRootSnapshots() {
|
|
9539
10337
|
const roots = [
|
|
9540
|
-
{ surface: "claude", root: (0,
|
|
9541
|
-
{ surface: "codex", root: (0,
|
|
10338
|
+
{ surface: "claude", root: (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
10339
|
+
{ surface: "codex", root: (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
9542
10340
|
];
|
|
9543
10341
|
return roots.flatMap(({ surface, root }) => {
|
|
9544
10342
|
try {
|
|
9545
|
-
const entries = (0,
|
|
10343
|
+
const entries = (0, import_node_fs6.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
9546
10344
|
name: entry.name,
|
|
9547
|
-
path: (0,
|
|
10345
|
+
path: (0, import_node_path7.join)(root, entry.name),
|
|
9548
10346
|
isDirectory: entry.isDirectory()
|
|
9549
10347
|
}));
|
|
9550
10348
|
return [{ surface, root, entries }];
|
|
@@ -9554,10 +10352,10 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
9554
10352
|
});
|
|
9555
10353
|
}
|
|
9556
10354
|
function uniqueQuarantineTarget(path2) {
|
|
9557
|
-
if (!(0,
|
|
10355
|
+
if (!(0, import_node_fs6.existsSync)(path2)) return path2;
|
|
9558
10356
|
for (let i = 1; i < 100; i += 1) {
|
|
9559
10357
|
const candidate = `${path2}-${i}`;
|
|
9560
|
-
if (!(0,
|
|
10358
|
+
if (!(0, import_node_fs6.existsSync)(candidate)) return candidate;
|
|
9561
10359
|
}
|
|
9562
10360
|
return `${path2}-${Date.now()}`;
|
|
9563
10361
|
}
|
|
@@ -9565,27 +10363,27 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
9565
10363
|
let moved = 0;
|
|
9566
10364
|
for (const move of plan2) {
|
|
9567
10365
|
try {
|
|
9568
|
-
if (!(0,
|
|
10366
|
+
if (!(0, import_node_fs6.existsSync)(move.from)) continue;
|
|
9569
10367
|
const target = uniqueQuarantineTarget(move.to);
|
|
9570
|
-
(0,
|
|
9571
|
-
(0,
|
|
10368
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path7.dirname)(target), { recursive: true });
|
|
10369
|
+
(0, import_node_fs6.renameSync)(move.from, target);
|
|
9572
10370
|
moved += 1;
|
|
9573
10371
|
} catch {
|
|
9574
10372
|
}
|
|
9575
10373
|
}
|
|
9576
10374
|
return moved;
|
|
9577
10375
|
}
|
|
9578
|
-
var gitignorePath = () => (0,
|
|
10376
|
+
var gitignorePath = () => (0, import_node_path7.join)(process.cwd(), ".gitignore");
|
|
9579
10377
|
function readGitignore() {
|
|
9580
10378
|
try {
|
|
9581
|
-
return (0,
|
|
10379
|
+
return (0, import_node_fs6.readFileSync)(gitignorePath(), "utf8");
|
|
9582
10380
|
} catch {
|
|
9583
10381
|
return null;
|
|
9584
10382
|
}
|
|
9585
10383
|
}
|
|
9586
10384
|
function writeGitignore(content) {
|
|
9587
10385
|
try {
|
|
9588
|
-
(0,
|
|
10386
|
+
(0, import_node_fs6.writeFileSync)(gitignorePath(), content, "utf8");
|
|
9589
10387
|
return true;
|
|
9590
10388
|
} catch {
|
|
9591
10389
|
return false;
|
|
@@ -9621,7 +10419,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9621
10419
|
let onPath = pathProbe;
|
|
9622
10420
|
if (!onPath) {
|
|
9623
10421
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
9624
|
-
if (root && (0,
|
|
10422
|
+
if (root && (0, import_node_fs6.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
9625
10423
|
}
|
|
9626
10424
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
9627
10425
|
const surface = detectSurface(process.env);
|
|
@@ -9666,7 +10464,12 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9666
10464
|
if (!gitignoreCheck.ok && gitignoreCheck.contentToWrite && !opts.json && !opts.banner) {
|
|
9667
10465
|
if (writeGitignore(gitignoreCheck.contentToWrite)) {
|
|
9668
10466
|
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
9669
|
-
|
|
10467
|
+
const drift = gitignoreCheck.seeded ? "inserted the org-managed block" : [
|
|
10468
|
+
gitignoreCheck.added?.length ? `added ${gitignoreCheck.added.join(", ")}` : "",
|
|
10469
|
+
gitignoreCheck.removed?.length ? `removed ${gitignoreCheck.removed.join(", ")}` : ""
|
|
10470
|
+
].filter(Boolean).join("; ") || "normalized the block";
|
|
10471
|
+
io.err(` \u21BB repaired: org-managed .gitignore block \u2014 ${drift}`);
|
|
10472
|
+
io.err(" this is an org-managed update (not unrelated churn) \u2014 stage & commit .gitignore so it stops recurring");
|
|
9670
10473
|
}
|
|
9671
10474
|
}
|
|
9672
10475
|
checks.push(gitignoreCheck);
|