@mutmutco/cli 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +480 -361
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3480,8 +3480,8 @@ function parseHookInput(stdin) {
|
|
|
3480
3480
|
}
|
|
3481
3481
|
|
|
3482
3482
|
// src/index.ts
|
|
3483
|
-
var
|
|
3484
|
-
var
|
|
3483
|
+
var import_node_child_process6 = require("node:child_process");
|
|
3484
|
+
var import_node_util6 = require("node:util");
|
|
3485
3485
|
var import_node_path5 = require("node:path");
|
|
3486
3486
|
var import_node_os = require("node:os");
|
|
3487
3487
|
|
|
@@ -3495,7 +3495,7 @@ var HEAD_PROMPT_ACTION_LIMIT = 50;
|
|
|
3495
3495
|
var HEAD_PROMPT_DECISION_LIMIT = 80;
|
|
3496
3496
|
function resolveEngine(platform, custom) {
|
|
3497
3497
|
if (custom) return { cmd: custom, args: [], shell: true };
|
|
3498
|
-
return { cmd: "claude", args: ["-p"], shell: platform === "win32" };
|
|
3498
|
+
return { cmd: "claude", args: ["-p", "--no-session-persistence"], shell: platform === "win32" };
|
|
3499
3499
|
}
|
|
3500
3500
|
function headTsPath(key) {
|
|
3501
3501
|
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -4025,32 +4025,153 @@ ${buildReportBody(body, sourceRepo)}`;
|
|
|
4025
4025
|
}
|
|
4026
4026
|
|
|
4027
4027
|
// src/board.ts
|
|
4028
|
+
var import_node_child_process4 = require("node:child_process");
|
|
4029
|
+
var import_node_util4 = require("node:util");
|
|
4030
|
+
|
|
4031
|
+
// src/github-client.ts
|
|
4028
4032
|
var import_node_child_process3 = require("node:child_process");
|
|
4029
4033
|
var import_node_util3 = require("node:util");
|
|
4034
|
+
var rawExecFileP = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
4035
|
+
var execFileP = (file, args, options = {}) => rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: 1e4, killSignal: "SIGTERM", ...options });
|
|
4036
|
+
var cachedGhCliToken;
|
|
4037
|
+
async function githubToken() {
|
|
4038
|
+
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
4039
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
4040
|
+
cachedGhCliToken ??= execFileP("gh", ["auth", "token"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
4041
|
+
return cachedGhCliToken;
|
|
4042
|
+
}
|
|
4043
|
+
var GitHubApiError = class extends Error {
|
|
4044
|
+
status;
|
|
4045
|
+
stderr;
|
|
4046
|
+
graphqlErrors;
|
|
4047
|
+
constructor(message, opts = {}) {
|
|
4048
|
+
super(message);
|
|
4049
|
+
this.name = "GitHubApiError";
|
|
4050
|
+
this.status = opts.status ?? 0;
|
|
4051
|
+
this.stderr = message;
|
|
4052
|
+
this.graphqlErrors = opts.graphqlErrors;
|
|
4053
|
+
}
|
|
4054
|
+
};
|
|
4055
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
4056
|
+
function joinUrl(base, path2) {
|
|
4057
|
+
if (path2.startsWith("http://") || path2.startsWith("https://")) return path2;
|
|
4058
|
+
return `${base.replace(/\/+$/, "")}/${path2.replace(/^\/+/, "")}`;
|
|
4059
|
+
}
|
|
4060
|
+
function withPerPage(url) {
|
|
4061
|
+
if (/[?&]per_page=/.test(url)) return url;
|
|
4062
|
+
return url.includes("?") ? `${url}&per_page=100` : `${url}?per_page=100`;
|
|
4063
|
+
}
|
|
4064
|
+
function nextLink(linkHeader) {
|
|
4065
|
+
if (!linkHeader) return void 0;
|
|
4066
|
+
for (const part of linkHeader.split(",")) {
|
|
4067
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
|
|
4068
|
+
if (match) return match[1];
|
|
4069
|
+
}
|
|
4070
|
+
return void 0;
|
|
4071
|
+
}
|
|
4072
|
+
async function errorFromResponse(res) {
|
|
4073
|
+
let detail = "";
|
|
4074
|
+
try {
|
|
4075
|
+
const text = await res.text();
|
|
4076
|
+
try {
|
|
4077
|
+
const parsed = JSON.parse(text);
|
|
4078
|
+
detail = parsed.message ?? text;
|
|
4079
|
+
} catch {
|
|
4080
|
+
detail = text;
|
|
4081
|
+
}
|
|
4082
|
+
} catch {
|
|
4083
|
+
detail = "";
|
|
4084
|
+
}
|
|
4085
|
+
const suffix = detail ? `: ${detail.trim()}` : "";
|
|
4086
|
+
return new GitHubApiError(`HTTP ${res.status}${suffix} (${res.url})`, { status: res.status });
|
|
4087
|
+
}
|
|
4088
|
+
function createGitHubClient(options = {}) {
|
|
4089
|
+
const baseUrl = options.baseUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
4090
|
+
const token = options.token ?? githubToken;
|
|
4091
|
+
const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
4092
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
4093
|
+
async function request(method, url, init = {}) {
|
|
4094
|
+
const t = await token();
|
|
4095
|
+
const headers = {
|
|
4096
|
+
Accept: "application/vnd.github+json",
|
|
4097
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
4098
|
+
"User-Agent": "mmi-cli",
|
|
4099
|
+
...t ? { Authorization: `Bearer ${t}` } : {},
|
|
4100
|
+
...init.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
4101
|
+
...init.headers
|
|
4102
|
+
};
|
|
4103
|
+
const res = await fetchImpl(url, {
|
|
4104
|
+
method,
|
|
4105
|
+
headers,
|
|
4106
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
4107
|
+
signal: AbortSignal.timeout(init.timeoutMs ?? defaultTimeoutMs)
|
|
4108
|
+
});
|
|
4109
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
4110
|
+
return res;
|
|
4111
|
+
}
|
|
4112
|
+
async function parseJson(res) {
|
|
4113
|
+
if (res.status === 204) return void 0;
|
|
4114
|
+
const text = await res.text();
|
|
4115
|
+
if (!text) return void 0;
|
|
4116
|
+
return JSON.parse(text);
|
|
4117
|
+
}
|
|
4118
|
+
return {
|
|
4119
|
+
async rest(method, path2, init) {
|
|
4120
|
+
const res = await request(method, joinUrl(baseUrl, path2), init);
|
|
4121
|
+
return parseJson(res);
|
|
4122
|
+
},
|
|
4123
|
+
async restPaginate(path2, init) {
|
|
4124
|
+
const items = [];
|
|
4125
|
+
let url = withPerPage(joinUrl(baseUrl, path2));
|
|
4126
|
+
while (url) {
|
|
4127
|
+
const res = await request("GET", url, init);
|
|
4128
|
+
const page = await parseJson(res);
|
|
4129
|
+
if (Array.isArray(page)) items.push(...page);
|
|
4130
|
+
url = nextLink(res.headers.get("link"));
|
|
4131
|
+
}
|
|
4132
|
+
return items;
|
|
4133
|
+
},
|
|
4134
|
+
async graphql(query, variables, init) {
|
|
4135
|
+
const res = await request("POST", joinUrl(baseUrl, "graphql"), {
|
|
4136
|
+
...init,
|
|
4137
|
+
body: { query, ...variables ? { variables } : {} }
|
|
4138
|
+
});
|
|
4139
|
+
const parsed = await parseJson(res);
|
|
4140
|
+
if (parsed?.errors?.length) {
|
|
4141
|
+
const message = parsed.errors.map((e) => e.message ?? e.type ?? "unknown GraphQL error").join("; ");
|
|
4142
|
+
throw new GitHubApiError(`GraphQL: ${message}`, { status: 200, graphqlErrors: parsed.errors });
|
|
4143
|
+
}
|
|
4144
|
+
if (!parsed || parsed.data === void 0 || parsed.data === null) {
|
|
4145
|
+
throw new GitHubApiError("GraphQL response did not include data", { status: 200 });
|
|
4146
|
+
}
|
|
4147
|
+
return parsed.data;
|
|
4148
|
+
}
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
var cachedDefaultClient;
|
|
4152
|
+
function defaultGitHubClient() {
|
|
4153
|
+
cachedDefaultClient ??= createGitHubClient();
|
|
4154
|
+
return cachedDefaultClient;
|
|
4155
|
+
}
|
|
4030
4156
|
|
|
4031
4157
|
// ../infra/board-vocab.mjs
|
|
4032
4158
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
4033
4159
|
|
|
4034
4160
|
// src/board.ts
|
|
4035
|
-
var
|
|
4036
|
-
var BOARD_GH_TIMEOUT_MS = 2e4;
|
|
4161
|
+
var rawExecFileP2 = (0, import_node_util4.promisify)(import_node_child_process4.execFile);
|
|
4037
4162
|
var BOARD_GIT_TIMEOUT_MS = 1e4;
|
|
4038
4163
|
var WRITE_PROBE_CONCURRENCY = 8;
|
|
4039
|
-
var
|
|
4164
|
+
var execFileP2 = (file, args, options = {}) => (
|
|
4040
4165
|
// encoding 'utf8' guarantees string output at runtime; the cast pins the type (promisify's
|
|
4041
4166
|
// overloads widen to string|Buffer when options is spread in).
|
|
4042
|
-
|
|
4167
|
+
rawExecFileP2(file, args, { encoding: "utf8", windowsHide: true, timeout: BOARD_GIT_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
|
|
4043
4168
|
);
|
|
4044
4169
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["Todo", "In Progress", "In Review"]);
|
|
4045
4170
|
var STATUS_ORDER = new Map(BOARD_STATUSES.map((s, i) => [s, i]));
|
|
4046
4171
|
var TYPE_LABELS = ["bug", "feature", "task"];
|
|
4047
|
-
var defaultGh = async (args) => {
|
|
4048
|
-
const { stdout, stderr } = await execFileP("gh", args, { timeout: BOARD_GH_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 });
|
|
4049
|
-
return { stdout: String(stdout), stderr: String(stderr) };
|
|
4050
|
-
};
|
|
4051
4172
|
var defaultGit = async (args) => {
|
|
4052
4173
|
try {
|
|
4053
|
-
const { stdout } = await
|
|
4174
|
+
const { stdout } = await execFileP2("git", args, { timeout: BOARD_GIT_TIMEOUT_MS });
|
|
4054
4175
|
return stdout.trim();
|
|
4055
4176
|
} catch {
|
|
4056
4177
|
return "";
|
|
@@ -4104,36 +4225,52 @@ query($owner: String!, $number: Int!, $after: String) {
|
|
|
4104
4225
|
}
|
|
4105
4226
|
}
|
|
4106
4227
|
}`;
|
|
4107
|
-
var
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
optionId
|
|
4128
|
-
field { ... on ProjectV2SingleSelectField { id name } }
|
|
4228
|
+
var SINGLE_ITEM_CONTENT_FRAGMENT = `
|
|
4229
|
+
id
|
|
4230
|
+
number
|
|
4231
|
+
title
|
|
4232
|
+
url
|
|
4233
|
+
state
|
|
4234
|
+
repository { nameWithOwner }
|
|
4235
|
+
labels(first: 10) { nodes { name } }
|
|
4236
|
+
assignees(first: 10) { nodes { login } }
|
|
4237
|
+
projectItems(first: 20) {
|
|
4238
|
+
nodes {
|
|
4239
|
+
id
|
|
4240
|
+
project { id title }
|
|
4241
|
+
fieldValues(first: 8) {
|
|
4242
|
+
nodes {
|
|
4243
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
4244
|
+
name
|
|
4245
|
+
optionId
|
|
4246
|
+
field { ... on ProjectV2SingleSelectField { id name } }
|
|
4247
|
+
}
|
|
4129
4248
|
}
|
|
4130
4249
|
}
|
|
4131
4250
|
}
|
|
4132
|
-
}
|
|
4251
|
+
}`;
|
|
4252
|
+
var ISSUE_PROJECT_ITEM_QUERY = `
|
|
4253
|
+
query($repoOwner: String!, $repoName: String!, $number: Int!) {
|
|
4254
|
+
viewer { login }
|
|
4255
|
+
repository(owner: $repoOwner, name: $repoName) {
|
|
4256
|
+
issueOrPullRequest(number: $number) {
|
|
4257
|
+
__typename
|
|
4258
|
+
... on Issue {${SINGLE_ITEM_CONTENT_FRAGMENT}
|
|
4259
|
+
}
|
|
4260
|
+
... on PullRequest {${SINGLE_ITEM_CONTENT_FRAGMENT}
|
|
4133
4261
|
}
|
|
4134
4262
|
}
|
|
4135
4263
|
}
|
|
4136
4264
|
}`;
|
|
4265
|
+
var UPDATE_ITEM_FIELD_MUTATION = `
|
|
4266
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
4267
|
+
updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) {
|
|
4268
|
+
projectV2Item { id }
|
|
4269
|
+
}
|
|
4270
|
+
}`;
|
|
4271
|
+
async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
|
|
4272
|
+
await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
|
|
4273
|
+
}
|
|
4137
4274
|
function resolveBoardConfig(cfg) {
|
|
4138
4275
|
const missing = [];
|
|
4139
4276
|
if (!cfg.projectOwner) missing.push("projectOwner");
|
|
@@ -4256,7 +4393,7 @@ function renderBoardReport(report) {
|
|
|
4256
4393
|
return lines.join("\n");
|
|
4257
4394
|
}
|
|
4258
4395
|
async function collectBoardItems(cfg, options, deps) {
|
|
4259
|
-
const
|
|
4396
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4260
4397
|
const git = deps.git ?? defaultGit;
|
|
4261
4398
|
const currentRepo = options.repo ?? repoFromGitRemote(await git(["remote", "get-url", "origin"])) ?? "";
|
|
4262
4399
|
if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
|
|
@@ -4269,7 +4406,7 @@ async function collectBoardItems(cfg, options, deps) {
|
|
|
4269
4406
|
let partial = false;
|
|
4270
4407
|
do {
|
|
4271
4408
|
try {
|
|
4272
|
-
const page = await fetchProjectPage(
|
|
4409
|
+
const page = await fetchProjectPage(client, cfg, after);
|
|
4273
4410
|
viewer ||= page.viewer.login;
|
|
4274
4411
|
const project2 = page.organization?.projectV2;
|
|
4275
4412
|
if (!project2) throw new Error(`project ${cfg.projectOwner}#${cfg.projectNumber} not found`);
|
|
@@ -4287,19 +4424,15 @@ async function collectBoardItems(cfg, options, deps) {
|
|
|
4287
4424
|
} while (after);
|
|
4288
4425
|
return { items: nodesToItems(nodes, warnings, cfg), viewer, repo: currentRepo, projectId, projectTitle, warnings, partial };
|
|
4289
4426
|
}
|
|
4290
|
-
async function repoCanPush(repo,
|
|
4427
|
+
async function repoCanPush(repo, client) {
|
|
4291
4428
|
try {
|
|
4292
|
-
const
|
|
4293
|
-
|
|
4294
|
-
if (value === "true") return true;
|
|
4295
|
-
if (value === "false") return false;
|
|
4296
|
-
const parsed = JSON.parse(value);
|
|
4297
|
-
return typeof parsed.permissions?.push === "boolean" ? parsed.permissions.push : void 0;
|
|
4429
|
+
const parsed = await client.rest("GET", `repos/${repo}`);
|
|
4430
|
+
return typeof parsed?.permissions?.push === "boolean" ? parsed.permissions.push : void 0;
|
|
4298
4431
|
} catch {
|
|
4299
4432
|
return void 0;
|
|
4300
4433
|
}
|
|
4301
4434
|
}
|
|
4302
|
-
async function resolveWritableReposForClaimables(items,
|
|
4435
|
+
async function resolveWritableReposForClaimables(items, client, allowPartial) {
|
|
4303
4436
|
const candidateRepos = [...new Set(items.filter((item) => item.status === "Todo" && item.assignees.length === 0).map((item) => item.repository))];
|
|
4304
4437
|
const repos = /* @__PURE__ */ new Set();
|
|
4305
4438
|
const warnings = [];
|
|
@@ -4308,7 +4441,7 @@ async function resolveWritableReposForClaimables(items, gh, allowPartial) {
|
|
|
4308
4441
|
const worker = async () => {
|
|
4309
4442
|
while (next < candidateRepos.length) {
|
|
4310
4443
|
const repo = candidateRepos[next++];
|
|
4311
|
-
const canPush = await repoCanPush(repo,
|
|
4444
|
+
const canPush = await repoCanPush(repo, client);
|
|
4312
4445
|
if (canPush === true) {
|
|
4313
4446
|
repos.add(repo.toLowerCase());
|
|
4314
4447
|
continue;
|
|
@@ -4326,9 +4459,9 @@ async function resolveWritableReposForClaimables(items, gh, allowPartial) {
|
|
|
4326
4459
|
}
|
|
4327
4460
|
async function readBoard(options, deps = {}) {
|
|
4328
4461
|
const cfg = resolveBoardConfig(options.config);
|
|
4329
|
-
const
|
|
4462
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4330
4463
|
const collected = await collectBoardItems(cfg, options, deps);
|
|
4331
|
-
const writable = await resolveWritableReposForClaimables(collected.items,
|
|
4464
|
+
const writable = await resolveWritableReposForClaimables(collected.items, client, options.allowPartial ?? false);
|
|
4332
4465
|
collected.warnings.push(...writable.warnings);
|
|
4333
4466
|
collected.partial = collected.partial || writable.partial;
|
|
4334
4467
|
const groups = partitionBoardItems(collected.items, collected.viewer, collected.repo, writable.repos);
|
|
@@ -4341,7 +4474,7 @@ async function readBoard(options, deps = {}) {
|
|
|
4341
4474
|
partial: collected.partial
|
|
4342
4475
|
};
|
|
4343
4476
|
if (options.includeBundleDetails) {
|
|
4344
|
-
await attachBundleDetails(report,
|
|
4477
|
+
await attachBundleDetails(report, client, options.allowPartial ?? false);
|
|
4345
4478
|
}
|
|
4346
4479
|
return report;
|
|
4347
4480
|
}
|
|
@@ -4352,67 +4485,50 @@ function findBoardItem(items, selector) {
|
|
|
4352
4485
|
if (!found) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
4353
4486
|
return found;
|
|
4354
4487
|
}
|
|
4488
|
+
async function resolveCurrentRepo(options, deps) {
|
|
4489
|
+
const git = deps.git ?? defaultGit;
|
|
4490
|
+
const currentRepo = options.repo ?? repoFromGitRemote(await git(["remote", "get-url", "origin"])) ?? "";
|
|
4491
|
+
if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
|
|
4492
|
+
return currentRepo;
|
|
4493
|
+
}
|
|
4355
4494
|
async function moveBoardItem(options, deps = {}) {
|
|
4356
4495
|
if (!BOARD_STATUSES.includes(options.status)) {
|
|
4357
4496
|
throw new Error(`unknown status '${options.status}'; expected one of ${BOARD_STATUSES.join(", ")}`);
|
|
4358
4497
|
}
|
|
4359
4498
|
const cfg = resolveBoardConfig(options.config);
|
|
4360
|
-
const
|
|
4361
|
-
const
|
|
4362
|
-
const selector = parseIssueSelector(options.selector,
|
|
4363
|
-
const
|
|
4499
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4500
|
+
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
4501
|
+
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
4502
|
+
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
4503
|
+
const item = lookup.item;
|
|
4504
|
+
if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
4364
4505
|
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
4365
4506
|
const optionId = cfg.statusOptions[options.status];
|
|
4366
4507
|
try {
|
|
4367
|
-
await
|
|
4368
|
-
"project",
|
|
4369
|
-
"item-edit",
|
|
4370
|
-
"--id",
|
|
4371
|
-
item.itemId,
|
|
4372
|
-
"--project-id",
|
|
4373
|
-
cfg.projectId,
|
|
4374
|
-
"--field-id",
|
|
4375
|
-
cfg.statusFieldId,
|
|
4376
|
-
"--single-select-option-id",
|
|
4377
|
-
optionId
|
|
4378
|
-
]);
|
|
4508
|
+
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
|
|
4379
4509
|
} catch (e) {
|
|
4380
4510
|
const warning = `partial move: ${item.ref} status was not changed to ${options.status} (${ghError(e)})`;
|
|
4381
4511
|
if (!options.allowPartial) throw new Error(warning);
|
|
4382
|
-
return { item, viewer:
|
|
4512
|
+
return { item, viewer: lookup.viewer, repo: currentRepo, status: item.status, partial: true, warning };
|
|
4383
4513
|
}
|
|
4384
4514
|
return {
|
|
4385
4515
|
item: { ...item, status: options.status, statusOptionId: optionId },
|
|
4386
|
-
viewer:
|
|
4387
|
-
repo:
|
|
4516
|
+
viewer: lookup.viewer,
|
|
4517
|
+
repo: currentRepo,
|
|
4388
4518
|
status: options.status,
|
|
4389
4519
|
partial: false
|
|
4390
4520
|
};
|
|
4391
4521
|
}
|
|
4392
4522
|
async function showBoardItem(options, deps = {}) {
|
|
4393
4523
|
const cfg = resolveBoardConfig(options.config);
|
|
4394
|
-
const
|
|
4395
|
-
const
|
|
4396
|
-
const selector = parseIssueSelector(options.selector,
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
item = findBoardItem(collected.items, selector);
|
|
4400
|
-
} catch (e) {
|
|
4401
|
-
const fallback = await fetchIssueProjectItem(gh, cfg, selector);
|
|
4402
|
-
if (!fallback) throw e;
|
|
4403
|
-
item = fallback;
|
|
4404
|
-
}
|
|
4524
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4525
|
+
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
4526
|
+
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
4527
|
+
const { item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
4528
|
+
if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
4405
4529
|
if (item.contentType === "Issue") {
|
|
4406
4530
|
try {
|
|
4407
|
-
|
|
4408
|
-
const detail = JSON.parse(stdout);
|
|
4409
|
-
item.details = {
|
|
4410
|
-
body: detail.body ?? "",
|
|
4411
|
-
comments: (detail.comments ?? []).map((comment) => ({
|
|
4412
|
-
author: comment.author?.login ?? "",
|
|
4413
|
-
body: comment.body ?? ""
|
|
4414
|
-
}))
|
|
4415
|
-
};
|
|
4531
|
+
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
4416
4532
|
} catch (e) {
|
|
4417
4533
|
if (!options.allowPartial) throw new Error(`detail read failed: ${item.ref}: ${ghError(e)}`);
|
|
4418
4534
|
}
|
|
@@ -4421,17 +4537,17 @@ async function showBoardItem(options, deps = {}) {
|
|
|
4421
4537
|
}
|
|
4422
4538
|
async function claimBoardIssue(options, deps = {}) {
|
|
4423
4539
|
const cfg = resolveBoardConfig(options.config);
|
|
4424
|
-
const
|
|
4540
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4425
4541
|
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial }, deps);
|
|
4426
4542
|
const selector = parseIssueSelector(options.selector, collected.repo);
|
|
4427
4543
|
try {
|
|
4428
4544
|
findBoardItem(collected.items, selector);
|
|
4429
4545
|
} catch (e) {
|
|
4430
|
-
const fallback = await fetchIssueProjectItem(
|
|
4546
|
+
const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
|
|
4431
4547
|
if (!fallback) throw e;
|
|
4432
4548
|
collected.items.push(fallback);
|
|
4433
4549
|
}
|
|
4434
|
-
const writable = await resolveWritableReposForClaimables(collected.items,
|
|
4550
|
+
const writable = await resolveWritableReposForClaimables(collected.items, client, options.allowPartial ?? false);
|
|
4435
4551
|
collected.warnings.push(...writable.warnings);
|
|
4436
4552
|
collected.partial = collected.partial || writable.partial;
|
|
4437
4553
|
const report = {
|
|
@@ -4448,7 +4564,7 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4448
4564
|
}
|
|
4449
4565
|
let item = findClaimableItem(report, selector);
|
|
4450
4566
|
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
4451
|
-
const fresh = await fetchIssueProjectItem(
|
|
4567
|
+
const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
|
|
4452
4568
|
if (!fresh) throw new Error(`${item.ref} is not on this project board`);
|
|
4453
4569
|
if (fresh.status !== "Todo") throw new Error(`${item.ref} is not claimable: Status is ${fresh.status}`);
|
|
4454
4570
|
if (fresh.assignees.length) throw new Error(`${item.ref} is already assigned to @${fresh.assignees.join(", @")}`);
|
|
@@ -4456,23 +4572,12 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4456
4572
|
const assignee = options.assignee ?? "@me";
|
|
4457
4573
|
const assignedLogin = assignee === "@me" ? report.viewer : assignee.replace(/^@/, "");
|
|
4458
4574
|
try {
|
|
4459
|
-
await
|
|
4575
|
+
await client.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
|
|
4460
4576
|
} catch (e) {
|
|
4461
4577
|
throw new Error(`claim failed before board status changed: ${ghError(e)}`);
|
|
4462
4578
|
}
|
|
4463
4579
|
try {
|
|
4464
|
-
await
|
|
4465
|
-
"project",
|
|
4466
|
-
"item-edit",
|
|
4467
|
-
"--id",
|
|
4468
|
-
item.itemId,
|
|
4469
|
-
"--project-id",
|
|
4470
|
-
cfg.projectId,
|
|
4471
|
-
"--field-id",
|
|
4472
|
-
cfg.statusFieldId,
|
|
4473
|
-
"--single-select-option-id",
|
|
4474
|
-
cfg.statusOptions["In Progress"]
|
|
4475
|
-
]);
|
|
4580
|
+
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]);
|
|
4476
4581
|
} catch (e) {
|
|
4477
4582
|
const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
|
|
4478
4583
|
if (!options.allowPartial) throw new Error(warning);
|
|
@@ -4491,22 +4596,11 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4491
4596
|
partial: false
|
|
4492
4597
|
};
|
|
4493
4598
|
}
|
|
4494
|
-
async function setBoardItemPriority(
|
|
4599
|
+
async function setBoardItemPriority(client, cfg, itemId, priority) {
|
|
4495
4600
|
if (!isPriorityFieldConfigured(cfg)) return void 0;
|
|
4496
4601
|
const optionId = resolvePriorityOptionId(cfg, priority);
|
|
4497
4602
|
if (!optionId || !cfg.priorityFieldId || !cfg.projectId) return void 0;
|
|
4498
|
-
await
|
|
4499
|
-
"project",
|
|
4500
|
-
"item-edit",
|
|
4501
|
-
"--id",
|
|
4502
|
-
itemId,
|
|
4503
|
-
"--project-id",
|
|
4504
|
-
cfg.projectId,
|
|
4505
|
-
"--field-id",
|
|
4506
|
-
cfg.priorityFieldId,
|
|
4507
|
-
"--single-select-option-id",
|
|
4508
|
-
optionId
|
|
4509
|
-
]);
|
|
4603
|
+
await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
|
|
4510
4604
|
return cliPriorityToFieldName(priority);
|
|
4511
4605
|
}
|
|
4512
4606
|
async function backfillBoardPriorities(options, deps = {}) {
|
|
@@ -4514,7 +4608,7 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4514
4608
|
if (!isPriorityFieldConfigured(cfg)) {
|
|
4515
4609
|
throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
|
|
4516
4610
|
}
|
|
4517
|
-
const
|
|
4611
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4518
4612
|
const collected = await collectBoardItems(cfg, { repo: options.repo }, deps);
|
|
4519
4613
|
const issues = collected.items.filter((item) => item.contentType === "Issue");
|
|
4520
4614
|
const concurrency = Math.max(1, options.concurrency ?? 8);
|
|
@@ -4525,7 +4619,7 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4525
4619
|
return;
|
|
4526
4620
|
}
|
|
4527
4621
|
try {
|
|
4528
|
-
const priority = await recoverIssuePriority(
|
|
4622
|
+
const priority = await recoverIssuePriority(client, item);
|
|
4529
4623
|
if (!priority) {
|
|
4530
4624
|
result.skipped += 1;
|
|
4531
4625
|
return;
|
|
@@ -4537,18 +4631,7 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4537
4631
|
}
|
|
4538
4632
|
const optionId = cfg.priorityOptions?.[priority];
|
|
4539
4633
|
if (!optionId) throw new Error(`no option id for ${priority}`);
|
|
4540
|
-
await
|
|
4541
|
-
"project",
|
|
4542
|
-
"item-edit",
|
|
4543
|
-
"--id",
|
|
4544
|
-
item.itemId,
|
|
4545
|
-
"--project-id",
|
|
4546
|
-
cfg.projectId,
|
|
4547
|
-
"--field-id",
|
|
4548
|
-
cfg.priorityFieldId,
|
|
4549
|
-
"--single-select-option-id",
|
|
4550
|
-
optionId
|
|
4551
|
-
]);
|
|
4634
|
+
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.priorityFieldId, optionId);
|
|
4552
4635
|
result.set += 1;
|
|
4553
4636
|
result.details.push(`${item.ref} \u2192 ${priority}`);
|
|
4554
4637
|
} catch (e) {
|
|
@@ -4561,88 +4644,65 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4561
4644
|
}
|
|
4562
4645
|
return result;
|
|
4563
4646
|
}
|
|
4564
|
-
async function recoverIssuePriority(
|
|
4647
|
+
async function recoverIssuePriority(client, item) {
|
|
4565
4648
|
for (const label of item.labels) {
|
|
4566
4649
|
const fromLabel = labelToFieldPriority(label);
|
|
4567
4650
|
if (fromLabel) return fromLabel;
|
|
4568
4651
|
}
|
|
4569
|
-
const
|
|
4570
|
-
|
|
4652
|
+
const events = await client.restPaginate(
|
|
4653
|
+
`repos/${item.repository}/issues/${item.number}/events`
|
|
4654
|
+
);
|
|
4655
|
+
return recoverPriorityFromEvents(events);
|
|
4571
4656
|
}
|
|
4572
|
-
function
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
if (Array.isArray(parsed)) return parsed;
|
|
4578
|
-
} catch {
|
|
4579
|
-
}
|
|
4580
|
-
return trimmed.split(/\r?\n/).filter(Boolean).flatMap((line) => {
|
|
4581
|
-
try {
|
|
4582
|
-
const parsed = JSON.parse(line);
|
|
4583
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
4584
|
-
} catch {
|
|
4585
|
-
return [];
|
|
4586
|
-
}
|
|
4657
|
+
async function fetchProjectPage(client, cfg, after) {
|
|
4658
|
+
return client.graphql(PROJECT_ITEMS_QUERY, {
|
|
4659
|
+
owner: cfg.projectOwner,
|
|
4660
|
+
number: cfg.projectNumber,
|
|
4661
|
+
...after ? { after } : {}
|
|
4587
4662
|
});
|
|
4588
4663
|
}
|
|
4589
|
-
|
|
4590
|
-
const
|
|
4591
|
-
|
|
4592
|
-
"graphql",
|
|
4593
|
-
"-f",
|
|
4594
|
-
`query=${PROJECT_ITEMS_QUERY}`,
|
|
4595
|
-
"-f",
|
|
4596
|
-
`owner=${cfg.projectOwner}`,
|
|
4597
|
-
"-F",
|
|
4598
|
-
`number=${cfg.projectNumber}`
|
|
4599
|
-
];
|
|
4600
|
-
if (after) args.push("-f", `after=${after}`);
|
|
4601
|
-
const { stdout } = await gh(args);
|
|
4602
|
-
const parsed = JSON.parse(stdout);
|
|
4603
|
-
if (!parsed.data) throw new Error("gh GraphQL response did not include data");
|
|
4604
|
-
return parsed.data;
|
|
4664
|
+
function isGraphQLNotFound(e) {
|
|
4665
|
+
const errors = e.graphqlErrors;
|
|
4666
|
+
return Boolean(e instanceof GitHubApiError && errors?.length && errors.every((entry) => entry.type === "NOT_FOUND"));
|
|
4605
4667
|
}
|
|
4606
|
-
async function fetchIssueProjectItem(
|
|
4668
|
+
async function fetchIssueProjectItem(client, cfg, selector) {
|
|
4607
4669
|
const [repoOwner, repoName] = selector.repo.split("/");
|
|
4608
|
-
if (!repoOwner || !repoName) return
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
`number=${selector.number}`
|
|
4620
|
-
]);
|
|
4621
|
-
const parsed = JSON.parse(stdout);
|
|
4622
|
-
const issue2 = parsed.data?.repository?.issue;
|
|
4623
|
-
if (!issue2) {
|
|
4624
|
-
const pageNodes = parsed.data?.organization?.projectV2?.items.nodes ?? [];
|
|
4625
|
-
return nodesToItems(pageNodes, [], cfg).find(
|
|
4626
|
-
(item) => item.repository.toLowerCase() === selector.repo.toLowerCase() && item.number === selector.number
|
|
4627
|
-
);
|
|
4670
|
+
if (!repoOwner || !repoName) return { viewer: "" };
|
|
4671
|
+
let data;
|
|
4672
|
+
try {
|
|
4673
|
+
data = await client.graphql(ISSUE_PROJECT_ITEM_QUERY, {
|
|
4674
|
+
repoOwner,
|
|
4675
|
+
repoName,
|
|
4676
|
+
number: selector.number
|
|
4677
|
+
});
|
|
4678
|
+
} catch (e) {
|
|
4679
|
+
if (isGraphQLNotFound(e)) return { viewer: "" };
|
|
4680
|
+
throw e;
|
|
4628
4681
|
}
|
|
4629
|
-
const
|
|
4630
|
-
|
|
4631
|
-
return
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4682
|
+
const viewer = data.viewer?.login ?? "";
|
|
4683
|
+
const content = data.repository?.issueOrPullRequest;
|
|
4684
|
+
if (!content) return { viewer };
|
|
4685
|
+
const projectItem = (content.projectItems?.nodes ?? []).find((item) => item.project?.id === cfg.projectId);
|
|
4686
|
+
if (!projectItem) return { viewer };
|
|
4687
|
+
const { projectItems: _projectItems, ...contentFields } = content;
|
|
4688
|
+
return {
|
|
4689
|
+
viewer,
|
|
4690
|
+
item: nodeToItem({
|
|
4691
|
+
id: projectItem.id,
|
|
4692
|
+
fieldValues: projectItem.fieldValues,
|
|
4693
|
+
content: contentFields
|
|
4694
|
+
}, cfg)
|
|
4695
|
+
};
|
|
4696
|
+
}
|
|
4697
|
+
async function fetchIssueDetails(client, repo, number) {
|
|
4698
|
+
const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
|
|
4699
|
+
const comments = await client.restPaginate(
|
|
4700
|
+
`repos/${repo}/issues/${number}/comments`
|
|
4701
|
+
);
|
|
4702
|
+
return {
|
|
4703
|
+
body: issue2?.body ?? "",
|
|
4704
|
+
comments: comments.map((comment) => ({ author: comment.user?.login ?? "", body: comment.body ?? "" }))
|
|
4705
|
+
};
|
|
4646
4706
|
}
|
|
4647
4707
|
function nodesToItems(nodes, warnings, cfg) {
|
|
4648
4708
|
const items = [];
|
|
@@ -4701,20 +4761,12 @@ function nodeToItem(node, cfg) {
|
|
|
4701
4761
|
function isSupportedContent(content) {
|
|
4702
4762
|
return Boolean(content && (content.__typename === "Issue" || content.__typename === "PullRequest"));
|
|
4703
4763
|
}
|
|
4704
|
-
async function attachBundleDetails(report,
|
|
4764
|
+
async function attachBundleDetails(report, client, allowPartial) {
|
|
4705
4765
|
const candidates = detailCandidates(report);
|
|
4706
4766
|
if (candidates.length <= 1) return;
|
|
4707
4767
|
for (const item of candidates) {
|
|
4708
4768
|
try {
|
|
4709
|
-
|
|
4710
|
-
const detail = JSON.parse(stdout);
|
|
4711
|
-
item.details = {
|
|
4712
|
-
body: detail.body ?? "",
|
|
4713
|
-
comments: (detail.comments ?? []).map((comment) => ({
|
|
4714
|
-
author: comment.author?.login ?? "",
|
|
4715
|
-
body: comment.body ?? ""
|
|
4716
|
-
}))
|
|
4717
|
-
};
|
|
4769
|
+
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
4718
4770
|
} catch (e) {
|
|
4719
4771
|
const warning = `partial detail read: ${item.ref}: ${ghError(e)}`;
|
|
4720
4772
|
if (!allowPartial) throw new Error(warning);
|
|
@@ -5140,8 +5192,9 @@ function buildAwsCrossAccountCheck(input) {
|
|
|
5140
5192
|
}
|
|
5141
5193
|
var MMI_PLUGIN_ID = "mmi@mmi";
|
|
5142
5194
|
var PLUGIN_LABEL = "plugin install record (mmi@mmi for this project)";
|
|
5143
|
-
function pluginInstallManualFix(projectPath) {
|
|
5144
|
-
|
|
5195
|
+
function pluginInstallManualFix(projectPath, surface = "claude-cli") {
|
|
5196
|
+
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}\``;
|
|
5197
|
+
return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
|
|
5145
5198
|
}
|
|
5146
5199
|
function isMmiPluginEnabled(settings) {
|
|
5147
5200
|
return Boolean(settings?.enabledPlugins?.[MMI_PLUGIN_ID]);
|
|
@@ -5161,7 +5214,7 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
5161
5214
|
const base = {
|
|
5162
5215
|
ok: true,
|
|
5163
5216
|
label: PLUGIN_LABEL,
|
|
5164
|
-
fix: pluginInstallManualFix(input.projectPath),
|
|
5217
|
+
fix: pluginInstallManualFix(input.projectPath, input.surface),
|
|
5165
5218
|
pluginId
|
|
5166
5219
|
};
|
|
5167
5220
|
if (!input.isOrgRepo || !isMmiPluginEnabled(input.settings)) return base;
|
|
@@ -5177,7 +5230,7 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
5177
5230
|
return {
|
|
5178
5231
|
ok: false,
|
|
5179
5232
|
label: PLUGIN_LABEL,
|
|
5180
|
-
fix: pluginInstallManualFix(input.projectPath),
|
|
5233
|
+
fix: pluginInstallManualFix(input.projectPath, input.surface),
|
|
5181
5234
|
pluginId,
|
|
5182
5235
|
recordToInsert
|
|
5183
5236
|
};
|
|
@@ -5196,8 +5249,7 @@ function bestRecord(records) {
|
|
|
5196
5249
|
}
|
|
5197
5250
|
function pluginConfigDriftFix(pluginId, surface = "claude-cli") {
|
|
5198
5251
|
const file = surface === "codex" ? "~/.codex/plugins/installed_plugins.json" : "~/.claude/plugins/installed_plugins.json";
|
|
5199
|
-
|
|
5200
|
-
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 ${reload}`;
|
|
5252
|
+
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)}`;
|
|
5201
5253
|
}
|
|
5202
5254
|
function buildPluginConfigDriftCheck(input) {
|
|
5203
5255
|
const pluginId = input.pluginId ?? MMI_PLUGIN_ID;
|
|
@@ -5293,13 +5345,24 @@ function detectSurface(env) {
|
|
|
5293
5345
|
if (isClaude) return "claude-cli";
|
|
5294
5346
|
return "shell";
|
|
5295
5347
|
}
|
|
5348
|
+
function reloadAction(surface) {
|
|
5349
|
+
switch (surface) {
|
|
5350
|
+
case "claude-vscode":
|
|
5351
|
+
return "restart VS Code";
|
|
5352
|
+
case "codex":
|
|
5353
|
+
return "restart Codex";
|
|
5354
|
+
case "claude-cli":
|
|
5355
|
+
case "shell":
|
|
5356
|
+
default:
|
|
5357
|
+
return "restart Claude Code (or run /reload-plugins)";
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5296
5360
|
function pluginRecoveryFix(surface) {
|
|
5297
5361
|
const claude = "claude plugin marketplace update mmi && claude plugin update mmi@mmi && claude plugin enable mmi@mmi";
|
|
5298
5362
|
switch (surface) {
|
|
5299
5363
|
case "claude-vscode":
|
|
5300
|
-
return `${claude} # then reopen the VS Code workspace to reload MMI commands`;
|
|
5301
5364
|
case "claude-cli":
|
|
5302
|
-
return `${claude} # then
|
|
5365
|
+
return `${claude} # then ${reloadAction(surface)} to reload MMI commands`;
|
|
5303
5366
|
case "codex":
|
|
5304
5367
|
return "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi # then restart Codex";
|
|
5305
5368
|
case "shell":
|
|
@@ -5336,12 +5399,12 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
5336
5399
|
}
|
|
5337
5400
|
|
|
5338
5401
|
// src/stage-runner.ts
|
|
5339
|
-
var
|
|
5402
|
+
var import_node_child_process5 = require("node:child_process");
|
|
5340
5403
|
var import_node_fs3 = require("node:fs");
|
|
5341
5404
|
var import_node_path3 = require("node:path");
|
|
5342
5405
|
var import_node_net = require("node:net");
|
|
5343
|
-
var
|
|
5344
|
-
var
|
|
5406
|
+
var import_node_util5 = require("node:util");
|
|
5407
|
+
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
|
|
5345
5408
|
function stageStatePath(cwd = process.cwd()) {
|
|
5346
5409
|
return (0, import_node_path3.join)(cwd, "tmp", "stage", "state.json");
|
|
5347
5410
|
}
|
|
@@ -5376,7 +5439,7 @@ function isPortFree(port) {
|
|
|
5376
5439
|
});
|
|
5377
5440
|
}
|
|
5378
5441
|
async function shell(command, cwd, timeoutMs) {
|
|
5379
|
-
await
|
|
5442
|
+
await execFileP3(command, [], {
|
|
5380
5443
|
cwd,
|
|
5381
5444
|
shell: true,
|
|
5382
5445
|
timeout: timeoutMs,
|
|
@@ -5395,7 +5458,7 @@ function readState(path2) {
|
|
|
5395
5458
|
async function killTree(pid) {
|
|
5396
5459
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
5397
5460
|
if (process.platform === "win32") {
|
|
5398
|
-
await
|
|
5461
|
+
await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
|
|
5399
5462
|
return;
|
|
5400
5463
|
}
|
|
5401
5464
|
try {
|
|
@@ -5458,7 +5521,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
5458
5521
|
}
|
|
5459
5522
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
5460
5523
|
const up = sub(config.up.trim());
|
|
5461
|
-
const child = (0,
|
|
5524
|
+
const child = (0, import_node_child_process5.spawn)(up, {
|
|
5462
5525
|
cwd,
|
|
5463
5526
|
shell: true,
|
|
5464
5527
|
detached: true,
|
|
@@ -5748,22 +5811,29 @@ function safeJson(text, fallback) {
|
|
|
5748
5811
|
return fallback;
|
|
5749
5812
|
}
|
|
5750
5813
|
}
|
|
5751
|
-
async function
|
|
5814
|
+
async function restJson(deps, path2, fallback) {
|
|
5752
5815
|
try {
|
|
5753
|
-
return
|
|
5816
|
+
return await deps.client.rest("GET", path2) ?? fallback;
|
|
5817
|
+
} catch {
|
|
5818
|
+
return fallback;
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
async function restPagedJson(deps, path2, fallback) {
|
|
5822
|
+
try {
|
|
5823
|
+
return await deps.client.restPaginate(path2);
|
|
5754
5824
|
} catch {
|
|
5755
5825
|
return fallback;
|
|
5756
5826
|
}
|
|
5757
5827
|
}
|
|
5758
5828
|
async function resolveOwners(deps) {
|
|
5759
|
-
const members = await
|
|
5829
|
+
const members = await restPagedJson(deps, `orgs/${OWNER}/members?role=admin`, []);
|
|
5760
5830
|
return members.map((m) => m.login);
|
|
5761
5831
|
}
|
|
5762
5832
|
function collaboratorRole(c) {
|
|
5763
5833
|
return c.role_name ?? (c.permissions?.admin ? "admin" : c.permissions?.maintain ? "maintain" : "write");
|
|
5764
5834
|
}
|
|
5765
5835
|
async function auditRepoCollaborators(repo, owners, deps) {
|
|
5766
|
-
const collabs = await
|
|
5836
|
+
const collabs = await restPagedJson(deps, `repos/${repo}/collaborators?affiliation=direct`, []);
|
|
5767
5837
|
const findings = [];
|
|
5768
5838
|
for (const c of collabs) {
|
|
5769
5839
|
if (owners.has(c.login)) continue;
|
|
@@ -5784,7 +5854,7 @@ async function auditRepoCollaborators(repo, owners, deps) {
|
|
|
5784
5854
|
async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @__PURE__ */ new Set()) {
|
|
5785
5855
|
let restrictions = null;
|
|
5786
5856
|
try {
|
|
5787
|
-
restrictions =
|
|
5857
|
+
restrictions = await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection/restrictions`) ?? null;
|
|
5788
5858
|
} catch {
|
|
5789
5859
|
restrictions = null;
|
|
5790
5860
|
}
|
|
@@ -5868,7 +5938,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
|
|
|
5868
5938
|
return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
5869
5939
|
}
|
|
5870
5940
|
async function auditOrgBasePermission(deps) {
|
|
5871
|
-
const org = await
|
|
5941
|
+
const org = await restJson(deps, `orgs/${OWNER}`, {});
|
|
5872
5942
|
const perm = org.default_repository_permission;
|
|
5873
5943
|
if (perm && perm !== "read" && perm !== "none") {
|
|
5874
5944
|
return [{
|
|
@@ -6011,9 +6081,16 @@ function safeJson2(text, fallback) {
|
|
|
6011
6081
|
return fallback;
|
|
6012
6082
|
}
|
|
6013
6083
|
}
|
|
6014
|
-
async function
|
|
6084
|
+
async function restJson2(deps, path2, fallback) {
|
|
6015
6085
|
try {
|
|
6016
|
-
return
|
|
6086
|
+
return await deps.client.rest("GET", path2) ?? fallback;
|
|
6087
|
+
} catch {
|
|
6088
|
+
return fallback;
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
async function restPagedJson2(deps, path2, fallback) {
|
|
6092
|
+
try {
|
|
6093
|
+
return await deps.client.restPaginate(path2);
|
|
6017
6094
|
} catch {
|
|
6018
6095
|
return fallback;
|
|
6019
6096
|
}
|
|
@@ -6021,7 +6098,7 @@ async function ghJson2(deps, args, fallback) {
|
|
|
6021
6098
|
async function contentExists(deps, repo, branch, path2) {
|
|
6022
6099
|
try {
|
|
6023
6100
|
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
6024
|
-
await deps.
|
|
6101
|
+
await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
6025
6102
|
return true;
|
|
6026
6103
|
} catch {
|
|
6027
6104
|
return false;
|
|
@@ -6029,7 +6106,7 @@ async function contentExists(deps, repo, branch, path2) {
|
|
|
6029
6106
|
}
|
|
6030
6107
|
async function getProtection(deps, repo, branch) {
|
|
6031
6108
|
try {
|
|
6032
|
-
return
|
|
6109
|
+
return await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection`) ?? {};
|
|
6033
6110
|
} catch {
|
|
6034
6111
|
return null;
|
|
6035
6112
|
}
|
|
@@ -6057,7 +6134,7 @@ async function rulesetDetails(deps, repo, list) {
|
|
|
6057
6134
|
details.push(ruleset);
|
|
6058
6135
|
continue;
|
|
6059
6136
|
}
|
|
6060
|
-
details.push(await
|
|
6137
|
+
details.push(await restJson2(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
|
|
6061
6138
|
}
|
|
6062
6139
|
return details;
|
|
6063
6140
|
}
|
|
@@ -6070,11 +6147,11 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6070
6147
|
const branchesWanted = expectedBranches(repoClass);
|
|
6071
6148
|
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
6072
6149
|
const checks = [];
|
|
6073
|
-
const repoInfo = await
|
|
6150
|
+
const repoInfo = await restJson2(deps, `repos/${repo}`, {});
|
|
6074
6151
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
6075
6152
|
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
6076
6153
|
checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
|
|
6077
|
-
const branchList = await
|
|
6154
|
+
const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
|
|
6078
6155
|
const branchNames = new Set(branchList.map((b) => b.name));
|
|
6079
6156
|
for (const branch of branchesWanted) {
|
|
6080
6157
|
checks.push({ ok: branchNames.has(branch), label: `branch exists: ${branch}` });
|
|
@@ -6107,14 +6184,14 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6107
6184
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
|
|
6108
6185
|
}
|
|
6109
6186
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
|
|
6110
|
-
const labels = await
|
|
6187
|
+
const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
|
|
6111
6188
|
const labelNames = new Set(labels.map((l) => l.name));
|
|
6112
6189
|
for (const label of requiredLabels) {
|
|
6113
6190
|
checks.push({ ok: labelNames.has(label), label: `label exists: ${label}` });
|
|
6114
6191
|
}
|
|
6115
6192
|
const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
|
|
6116
6193
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
6117
|
-
const actions = await
|
|
6194
|
+
const actions = await restJson2(deps, `repos/${repo}/actions/permissions`, {});
|
|
6118
6195
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
6119
6196
|
const config = deps.projectMeta ?? null;
|
|
6120
6197
|
checks.push({
|
|
@@ -6122,12 +6199,18 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6122
6199
|
label: "registry project board META exists"
|
|
6123
6200
|
});
|
|
6124
6201
|
if (config?.projectOwner && config.projectNumber != null) {
|
|
6125
|
-
const
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6202
|
+
const fieldsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name } ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }`;
|
|
6203
|
+
const fields = await (async () => {
|
|
6204
|
+
try {
|
|
6205
|
+
const data = await deps.client.graphql(fieldsQuery, {
|
|
6206
|
+
login: config.projectOwner,
|
|
6207
|
+
number: config.projectNumber
|
|
6208
|
+
});
|
|
6209
|
+
return (data.organization?.projectV2?.fields?.nodes ?? []).filter((f) => Boolean(f?.id && f?.name));
|
|
6210
|
+
} catch {
|
|
6211
|
+
return [];
|
|
6212
|
+
}
|
|
6213
|
+
})();
|
|
6131
6214
|
const statusField = fields.find((field) => field.name === "Status");
|
|
6132
6215
|
const labelField = fields.find((field) => field.name === "Labels");
|
|
6133
6216
|
checks.push({
|
|
@@ -6184,12 +6267,17 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6184
6267
|
}
|
|
6185
6268
|
}
|
|
6186
6269
|
const workflowQuery = "query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { workflows(first: 30) { nodes { name enabled } } } } }";
|
|
6187
|
-
const workflowResponse = await
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6270
|
+
const workflowResponse = await (async () => {
|
|
6271
|
+
try {
|
|
6272
|
+
return await deps.client.graphql(workflowQuery, {
|
|
6273
|
+
login: config.projectOwner,
|
|
6274
|
+
number: config.projectNumber
|
|
6275
|
+
});
|
|
6276
|
+
} catch {
|
|
6277
|
+
return {};
|
|
6278
|
+
}
|
|
6279
|
+
})();
|
|
6280
|
+
const workflows = workflowResponse.organization?.projectV2?.workflows?.nodes || [];
|
|
6193
6281
|
for (const workflowName of requiredProjectWorkflows) {
|
|
6194
6282
|
checks.push({
|
|
6195
6283
|
ok: workflows.some((workflow) => workflow.name === workflowName && workflow.enabled === true),
|
|
@@ -6201,11 +6289,7 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6201
6289
|
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
6202
6290
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && json.projects.some((p) => (p.repos || []).includes(repo)));
|
|
6203
6291
|
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
|
|
6204
|
-
const rulesetList = await
|
|
6205
|
-
deps,
|
|
6206
|
-
["api", `repos/${repo}/rulesets?includes_parents=true`],
|
|
6207
|
-
[]
|
|
6208
|
-
);
|
|
6292
|
+
const rulesetList = await restJson2(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
6209
6293
|
const rulesets = await rulesetDetails(deps, repo, rulesetList);
|
|
6210
6294
|
const activeOrgRulesets = rulesets.filter(
|
|
6211
6295
|
(r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
|
|
@@ -6391,7 +6475,7 @@ var ORG_CONFIG_PATH = "/org/config";
|
|
|
6391
6475
|
var PROJECTS_ENVELOPE_KEY = "projects";
|
|
6392
6476
|
|
|
6393
6477
|
// src/registry-client.ts
|
|
6394
|
-
var
|
|
6478
|
+
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
6395
6479
|
async function fetchTrainAuthority(repo, deps) {
|
|
6396
6480
|
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
6397
6481
|
const token = await deps.token();
|
|
@@ -6401,7 +6485,7 @@ async function fetchTrainAuthority(repo, deps) {
|
|
|
6401
6485
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
|
|
6402
6486
|
method: "GET",
|
|
6403
6487
|
headers: { Authorization: `Bearer ${token}` },
|
|
6404
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6488
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6405
6489
|
});
|
|
6406
6490
|
if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
|
|
6407
6491
|
const body = await res.json();
|
|
@@ -6420,7 +6504,7 @@ async function fetchProjectsList(deps) {
|
|
|
6420
6504
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${PROJECTS_LIST_PATH}`, {
|
|
6421
6505
|
method: "GET",
|
|
6422
6506
|
headers: { Authorization: `Bearer ${token}` },
|
|
6423
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6507
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6424
6508
|
});
|
|
6425
6509
|
if (!res.ok) return null;
|
|
6426
6510
|
const body = await res.json();
|
|
@@ -6444,7 +6528,7 @@ async function fetchProjectBySlugChecked(slug, deps) {
|
|
|
6444
6528
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
|
|
6445
6529
|
method: "GET",
|
|
6446
6530
|
headers: { Authorization: `Bearer ${token}` },
|
|
6447
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6531
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6448
6532
|
});
|
|
6449
6533
|
if (res.status === 404) return { ok: true, project: null };
|
|
6450
6534
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
@@ -6466,7 +6550,7 @@ async function fetchDeployStatusBySlug(slug, deps) {
|
|
|
6466
6550
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
|
|
6467
6551
|
method: "GET",
|
|
6468
6552
|
headers: { Authorization: `Bearer ${token}` },
|
|
6469
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6553
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6470
6554
|
});
|
|
6471
6555
|
if (!res.ok) return null;
|
|
6472
6556
|
const body = await res.json();
|
|
@@ -6485,7 +6569,7 @@ async function fetchOrgConfig(deps) {
|
|
|
6485
6569
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${ORG_CONFIG_PATH}`, {
|
|
6486
6570
|
method: "GET",
|
|
6487
6571
|
headers: { Authorization: `Bearer ${token}` },
|
|
6488
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6572
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6489
6573
|
});
|
|
6490
6574
|
if (!res.ok) return null;
|
|
6491
6575
|
return await res.json();
|
|
@@ -6503,7 +6587,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST") {
|
|
|
6503
6587
|
method,
|
|
6504
6588
|
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6505
6589
|
body: JSON.stringify(payload),
|
|
6506
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6590
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6507
6591
|
});
|
|
6508
6592
|
let body = null;
|
|
6509
6593
|
try {
|
|
@@ -6521,6 +6605,9 @@ async function registerProject(payload, deps) {
|
|
|
6521
6605
|
async function upsertProject(slug, patch, deps) {
|
|
6522
6606
|
return postJson(`/projects/${encodeURIComponent(slug)}`, patch, deps);
|
|
6523
6607
|
}
|
|
6608
|
+
async function attestAppGaps(slug, repo, deps) {
|
|
6609
|
+
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
6610
|
+
}
|
|
6524
6611
|
async function tenantControl(payload, deps) {
|
|
6525
6612
|
return postJson("/tenant-control", payload, deps);
|
|
6526
6613
|
}
|
|
@@ -6559,7 +6646,21 @@ function stageRequiredSecrets(stage2, meta) {
|
|
|
6559
6646
|
function stageKey(stage2, key) {
|
|
6560
6647
|
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
6561
6648
|
}
|
|
6649
|
+
function hasRuntimeSecretContract(contract) {
|
|
6650
|
+
return Boolean(contract) && !Array.isArray(contract);
|
|
6651
|
+
}
|
|
6652
|
+
function appAttestationOf(meta) {
|
|
6653
|
+
const a = meta?.appAttested;
|
|
6654
|
+
if (!a || typeof a !== "object" || Array.isArray(a)) return null;
|
|
6655
|
+
const { at, by } = a;
|
|
6656
|
+
return typeof at === "string" && at.length > 0 && typeof by === "string" && by.length > 0 ? { at, by } : null;
|
|
6657
|
+
}
|
|
6658
|
+
function attestedLine(att) {
|
|
6659
|
+
return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli project attest\` after app-owned structural changes.`;
|
|
6660
|
+
}
|
|
6562
6661
|
function appGapsFor(meta, model, slug, projectType) {
|
|
6662
|
+
const attested = appAttestationOf(meta);
|
|
6663
|
+
if (attested) return [attestedLine(attested)];
|
|
6563
6664
|
if (projectType === "content" || model === "content") return ["Content/KB repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
6564
6665
|
if (projectType === "desktop-game") {
|
|
6565
6666
|
return [
|
|
@@ -6591,6 +6692,9 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
6591
6692
|
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
6592
6693
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
6593
6694
|
];
|
|
6695
|
+
if (meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
|
|
6696
|
+
gaps.unshift("No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).");
|
|
6697
|
+
}
|
|
6594
6698
|
if (slug === "mmi-katip") {
|
|
6595
6699
|
gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
|
|
6596
6700
|
}
|
|
@@ -6730,7 +6834,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
6730
6834
|
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets: secrets2 },
|
|
6731
6835
|
secretsError,
|
|
6732
6836
|
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
6733
|
-
appOwnedGaps: autoHeal.appOwnedGaps
|
|
6837
|
+
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
6838
|
+
appAttested: appAttestationOf(meta) ?? void 0
|
|
6734
6839
|
};
|
|
6735
6840
|
}
|
|
6736
6841
|
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
@@ -6823,6 +6928,10 @@ function buildProjectSetPatch(input) {
|
|
|
6823
6928
|
}
|
|
6824
6929
|
return patch;
|
|
6825
6930
|
}
|
|
6931
|
+
function repoFromRemoteUrl(remoteUrl) {
|
|
6932
|
+
const m = remoteUrl.trim().match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@\s/]+@)?github\.com[:/]([^/\s:]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
|
|
6933
|
+
return m ? `${m[1]}/${m[2]}` : void 0;
|
|
6934
|
+
}
|
|
6826
6935
|
function requireProjectTarget(commandName, explicitTarget, currentRepo) {
|
|
6827
6936
|
const target = explicitTarget?.trim() || currentRepo?.trim();
|
|
6828
6937
|
if (!target) {
|
|
@@ -7102,12 +7211,14 @@ function formatVaultPointer(p) {
|
|
|
7102
7211
|
}
|
|
7103
7212
|
var TIMEOUT_MS2 = 8e3;
|
|
7104
7213
|
var repoOf = (slug) => `${OWNER2}/${slug}`;
|
|
7214
|
+
async function vaultSlug(deps, opts) {
|
|
7215
|
+
return (opts.repo ? opts.repo.split("/").pop() : await deps.slug()).toLowerCase();
|
|
7216
|
+
}
|
|
7105
7217
|
async function targetRepo(deps, opts) {
|
|
7106
|
-
return opts.repo ?? repoOf(await deps
|
|
7218
|
+
return opts.repo ?? repoOf(await vaultSlug(deps, opts));
|
|
7107
7219
|
}
|
|
7108
7220
|
async function secretsWhere(deps, opts) {
|
|
7109
|
-
|
|
7110
|
-
deps.log(formatVaultPointer(vaultPointer(slug)));
|
|
7221
|
+
deps.log(formatVaultPointer(vaultPointer(await vaultSlug(deps, opts))));
|
|
7111
7222
|
}
|
|
7112
7223
|
async function readErr(res) {
|
|
7113
7224
|
try {
|
|
@@ -7289,7 +7400,7 @@ async function secretsSet(deps, key, opts) {
|
|
|
7289
7400
|
);
|
|
7290
7401
|
return;
|
|
7291
7402
|
}
|
|
7292
|
-
deps.log(`set ${key} (${classifyTier(await deps
|
|
7403
|
+
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
7293
7404
|
}
|
|
7294
7405
|
async function secretsEdit(deps, key, opts) {
|
|
7295
7406
|
return secretsSet(deps, key, opts);
|
|
@@ -7341,8 +7452,8 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
|
|
|
7341
7452
|
}
|
|
7342
7453
|
deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
|
|
7343
7454
|
}
|
|
7344
|
-
async function secretsUse(deps, key,
|
|
7345
|
-
const slug = await deps
|
|
7455
|
+
async function secretsUse(deps, key, opts) {
|
|
7456
|
+
const slug = await vaultSlug(deps, opts);
|
|
7346
7457
|
const tier = classifyTier(slug, key);
|
|
7347
7458
|
const path2 = secretParamName(slug, key);
|
|
7348
7459
|
deps.log(
|
|
@@ -7427,30 +7538,23 @@ function authorizeBodyHasMismatch(body) {
|
|
|
7427
7538
|
}
|
|
7428
7539
|
|
|
7429
7540
|
// src/index.ts
|
|
7430
|
-
var
|
|
7541
|
+
var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
|
|
7431
7542
|
var DEFAULT_EXEC_TIMEOUT_MS = 1e4;
|
|
7432
|
-
var
|
|
7543
|
+
var execFileP4 = (file, args, options = {}) => (
|
|
7433
7544
|
// encoding 'utf8' guarantees string stdout/stderr at runtime; the cast pins the type because
|
|
7434
7545
|
// promisify(execFile)'s overloads widen to string|Buffer when options is spread in.
|
|
7435
|
-
|
|
7546
|
+
rawExecFileP3(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
|
|
7436
7547
|
);
|
|
7437
7548
|
var GIT_TIMEOUT_MS = DEFAULT_EXEC_TIMEOUT_MS;
|
|
7438
7549
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
7439
|
-
var cachedGhCliToken;
|
|
7440
|
-
async function githubToken() {
|
|
7441
|
-
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
7442
|
-
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
7443
|
-
cachedGhCliToken ??= execFileP3("gh", ["auth", "token"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
7444
|
-
return cachedGhCliToken;
|
|
7445
|
-
}
|
|
7446
7550
|
var cachedGithubLogin;
|
|
7447
7551
|
async function githubLogin() {
|
|
7448
|
-
cachedGithubLogin ??=
|
|
7552
|
+
cachedGithubLogin ??= execFileP4("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
7449
7553
|
return cachedGithubLogin;
|
|
7450
7554
|
}
|
|
7451
7555
|
async function awsCallerArn() {
|
|
7452
7556
|
try {
|
|
7453
|
-
const { stdout } = await
|
|
7557
|
+
const { stdout } = await execFileP4(
|
|
7454
7558
|
"aws",
|
|
7455
7559
|
["sts", "get-caller-identity", "--query", "Arn", "--output", "text"],
|
|
7456
7560
|
{ timeout: GIT_TIMEOUT_MS }
|
|
@@ -7510,7 +7614,7 @@ var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/d
|
|
|
7510
7614
|
var SESSION_FILE = ".mmi/.session";
|
|
7511
7615
|
var gitOut = async (args) => {
|
|
7512
7616
|
try {
|
|
7513
|
-
return (await
|
|
7617
|
+
return (await execFileP4("git", [...args])).stdout.trim();
|
|
7514
7618
|
} catch {
|
|
7515
7619
|
return "";
|
|
7516
7620
|
}
|
|
@@ -7582,18 +7686,18 @@ async function readStdin() {
|
|
|
7582
7686
|
async function ghPrs(limit) {
|
|
7583
7687
|
const args = (state) => ["pr", "list", "--state", state, "--limit", String(limit), "--json", "number,headRefName,state"];
|
|
7584
7688
|
const [open, closed] = await Promise.all([
|
|
7585
|
-
|
|
7586
|
-
|
|
7689
|
+
execFileP4("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
|
|
7690
|
+
execFileP4("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
|
|
7587
7691
|
]);
|
|
7588
7692
|
return [...JSON.parse(open.stdout || "[]"), ...JSON.parse(closed.stdout || "[]")];
|
|
7589
7693
|
}
|
|
7590
7694
|
async function worktreeBranches() {
|
|
7591
|
-
const { stdout } = await
|
|
7695
|
+
const { stdout } = await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7592
7696
|
const parsed = parseWorktreePorcelain(stdout);
|
|
7593
7697
|
return await Promise.all(parsed.map(async (w) => {
|
|
7594
7698
|
let dirty = true;
|
|
7595
7699
|
try {
|
|
7596
|
-
const { stdout: status } = await
|
|
7700
|
+
const { stdout: status } = await execFileP4("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7597
7701
|
dirty = status.trim().length > 0;
|
|
7598
7702
|
} catch {
|
|
7599
7703
|
dirty = true;
|
|
@@ -7605,7 +7709,7 @@ async function gcPlan(remote, limit) {
|
|
|
7605
7709
|
const [branches, current, stale, prs, worktrees] = await Promise.all([
|
|
7606
7710
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
7607
7711
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
7608
|
-
|
|
7712
|
+
execFileP4("git", ["remote", "prune", remote, "--dry-run"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseRemotePruneDryRun(`${r.stdout}${r.stderr}`)).catch(() => []),
|
|
7609
7713
|
ghPrs(limit),
|
|
7610
7714
|
worktreeBranches()
|
|
7611
7715
|
]);
|
|
@@ -7633,8 +7737,8 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7633
7737
|
const result = { removedBranches: [], removedTrackingRefs: [], failed: [], pruned: false };
|
|
7634
7738
|
for (const branch of plan2.branches) {
|
|
7635
7739
|
try {
|
|
7636
|
-
if (branch.worktreePath) await
|
|
7637
|
-
await
|
|
7740
|
+
if (branch.worktreePath) await execFileP4("git", ["worktree", "remove", "--force", branch.worktreePath], { timeout: GIT_TIMEOUT_MS });
|
|
7741
|
+
await execFileP4("git", ["branch", "-D", branch.branch], { timeout: GIT_TIMEOUT_MS });
|
|
7638
7742
|
result.removedBranches.push(branch.branch);
|
|
7639
7743
|
} catch (e) {
|
|
7640
7744
|
result.failed.push(`${branch.branch}: ${e.message.split("\n")[0]}`);
|
|
@@ -7642,7 +7746,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7642
7746
|
}
|
|
7643
7747
|
for (const ref of plan2.trackingRefs) {
|
|
7644
7748
|
try {
|
|
7645
|
-
await
|
|
7749
|
+
await execFileP4("git", ["update-ref", "-d", `refs/remotes/${remote}/${ref.branch}`], { timeout: GIT_TIMEOUT_MS });
|
|
7646
7750
|
result.removedTrackingRefs.push(ref.branch);
|
|
7647
7751
|
} catch (e) {
|
|
7648
7752
|
result.failed.push(`${remote}/${ref.branch}: ${e.message.split("\n")[0]}`);
|
|
@@ -7650,7 +7754,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7650
7754
|
}
|
|
7651
7755
|
if (plan2.branches.some((b) => b.worktreePath)) {
|
|
7652
7756
|
try {
|
|
7653
|
-
await
|
|
7757
|
+
await execFileP4("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
|
|
7654
7758
|
result.pruned = true;
|
|
7655
7759
|
} catch (e) {
|
|
7656
7760
|
result.failed.push(`worktree prune: ${e.message.split("\n")[0]}`);
|
|
@@ -7680,7 +7784,7 @@ function readRepoVersion() {
|
|
|
7680
7784
|
}
|
|
7681
7785
|
async function fetchReleasedVersion() {
|
|
7682
7786
|
try {
|
|
7683
|
-
const { stdout } = await
|
|
7787
|
+
const { stdout } = await execFileP4("gh", pluginManifestVersionArgs(), { timeout: 5e3 });
|
|
7684
7788
|
return parseManifestVersion(stdout);
|
|
7685
7789
|
} catch {
|
|
7686
7790
|
return void 0;
|
|
@@ -7694,9 +7798,9 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
7694
7798
|
const target = report.releasedVersion ?? "latest";
|
|
7695
7799
|
if (action === "plugin-pull") {
|
|
7696
7800
|
try {
|
|
7697
|
-
const root = (await
|
|
7801
|
+
const root = (await execFileP4("git", ["-C", process.env.CLAUDE_PLUGIN_ROOT, "rev-parse", "--show-toplevel"], { timeout: PLUGIN_PULL_TIMEOUT_MS })).stdout.trim();
|
|
7698
7802
|
log(` \u21BB refreshing MMI plugin ${report.currentVersion} \u2192 ${target} (effective next session)\u2026`);
|
|
7699
|
-
await
|
|
7803
|
+
await execFileP4("git", ["-C", root, "pull", "--ff-only"], { timeout: PLUGIN_PULL_TIMEOUT_MS });
|
|
7700
7804
|
return { ...report, ok: true };
|
|
7701
7805
|
} catch {
|
|
7702
7806
|
return report;
|
|
@@ -7723,7 +7827,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
7723
7827
|
var consoleIo = { log: (m) => console.log(m), err: (m) => console.error(m) };
|
|
7724
7828
|
var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
7725
7829
|
function runHostBin(bin, args, opts) {
|
|
7726
|
-
return isWin ?
|
|
7830
|
+
return isWin ? execFileP4("cmd.exe", ["/c", bin, ...args], opts) : execFileP4(bin, args, opts);
|
|
7727
7831
|
}
|
|
7728
7832
|
async function runClaudePlugin(args) {
|
|
7729
7833
|
try {
|
|
@@ -7794,7 +7898,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
7794
7898
|
isDirty: async (f) => await gitOut(["status", "--porcelain", "--", f]) !== "",
|
|
7795
7899
|
originContent: async (f) => {
|
|
7796
7900
|
try {
|
|
7797
|
-
return (await
|
|
7901
|
+
return (await execFileP4("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
|
|
7798
7902
|
} catch {
|
|
7799
7903
|
return null;
|
|
7800
7904
|
}
|
|
@@ -7827,14 +7931,17 @@ async function runSagaShow(opts, io = consoleIo) {
|
|
|
7827
7931
|
try {
|
|
7828
7932
|
const key = await sagaKey(cfg);
|
|
7829
7933
|
const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
|
|
7830
|
-
const res = await fetch(`${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(
|
|
7934
|
+
const res = await fetch(`${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(3e3) });
|
|
7831
7935
|
if (res.ok) {
|
|
7832
7936
|
io.log(resumeCue());
|
|
7833
7937
|
return io.log(await res.text());
|
|
7834
7938
|
}
|
|
7835
7939
|
if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
|
|
7836
7940
|
} catch (e) {
|
|
7837
|
-
if (!opts.quiet)
|
|
7941
|
+
if (!opts.quiet) {
|
|
7942
|
+
const reason = e.name === "TimeoutError" ? "backend unreachable (timed out after 3s)" : e.message;
|
|
7943
|
+
io.err(`saga show: ${reason} \u2014 continuing without saga; diagnose with \`mmi-cli saga health --json\``);
|
|
7944
|
+
}
|
|
7838
7945
|
}
|
|
7839
7946
|
}
|
|
7840
7947
|
saga.command("show").option("--quiet", "no-op silently when unconfigured/unreachable (SessionStart hook)").option("--latest-anywhere", "resume the newest saga across all repos (default: current repo)").description("print your resume block \u2014 current repo HEAD + project memory (where you left off)").action((opts) => runSagaShow(opts));
|
|
@@ -7854,7 +7961,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
7854
7961
|
if (!headGateDue(tsPath)) return;
|
|
7855
7962
|
markHeadRun(tsPath);
|
|
7856
7963
|
try {
|
|
7857
|
-
(0,
|
|
7964
|
+
(0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], "saga", "head-update", "--run"], {
|
|
7858
7965
|
detached: true,
|
|
7859
7966
|
stdio: "ignore",
|
|
7860
7967
|
windowsHide: true
|
|
@@ -7971,7 +8078,7 @@ var kb = program2.command("kb").description("org knowledgebase (read-only)");
|
|
|
7971
8078
|
kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
|
|
7972
8079
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
7973
8080
|
try {
|
|
7974
|
-
const { stdout } = await
|
|
8081
|
+
const { stdout } = await execFileP4("gh", buildKbGetArgs(src, path2), { timeout: 1e4 });
|
|
7975
8082
|
process.stdout.write(stdout);
|
|
7976
8083
|
} catch (e) {
|
|
7977
8084
|
const err = e;
|
|
@@ -7981,7 +8088,7 @@ kb.command("get <path>").description("print a KB document by path").action(async
|
|
|
7981
8088
|
kb.command("list [prefix]").description("list KB document paths (optionally under a prefix)").action(async (prefix) => {
|
|
7982
8089
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
7983
8090
|
try {
|
|
7984
|
-
const { stdout } = await
|
|
8091
|
+
const { stdout } = await execFileP4("gh", buildKbTreeArgs(src), { timeout: 1e4 });
|
|
7985
8092
|
const paths = parseKbTree(stdout, prefix);
|
|
7986
8093
|
if (!paths.length) return fail(`kb list: no documents${prefix ? ` under ${prefix}` : ""}`);
|
|
7987
8094
|
console.log(paths.join("\n"));
|
|
@@ -7992,21 +8099,23 @@ kb.command("list [prefix]").description("list KB document paths (optionally unde
|
|
|
7992
8099
|
});
|
|
7993
8100
|
async function ghCreate(args) {
|
|
7994
8101
|
try {
|
|
7995
|
-
const { stdout } = await
|
|
8102
|
+
const { stdout } = await execFileP4("gh", args);
|
|
7996
8103
|
return parseCreatedUrl(stdout);
|
|
7997
8104
|
} catch (e) {
|
|
7998
8105
|
const err = e;
|
|
7999
8106
|
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
8000
8107
|
}
|
|
8001
8108
|
}
|
|
8002
|
-
async function
|
|
8003
|
-
const { stdout } = await
|
|
8109
|
+
async function ghJson(args, timeout = 1e4) {
|
|
8110
|
+
const { stdout } = await execFileP4("gh", args, { timeout });
|
|
8004
8111
|
return JSON.parse(stdout);
|
|
8005
8112
|
}
|
|
8006
8113
|
async function resolveRepo(repo) {
|
|
8007
8114
|
if (repo) return repo;
|
|
8115
|
+
const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
|
|
8116
|
+
if (fromOrigin) return fromOrigin;
|
|
8008
8117
|
try {
|
|
8009
|
-
const { stdout } = await
|
|
8118
|
+
const { stdout } = await execFileP4("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
|
|
8010
8119
|
return stdout.trim() || void 0;
|
|
8011
8120
|
} catch {
|
|
8012
8121
|
return void 0;
|
|
@@ -8022,19 +8131,14 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
8022
8131
|
try {
|
|
8023
8132
|
const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
|
|
8024
8133
|
if (targetRepo2) viewArgs.push("--repo", targetRepo2);
|
|
8025
|
-
const { stdout: idOut } = await
|
|
8134
|
+
const { stdout: idOut } = await execFileP4("gh", viewArgs, { timeout: 1e4 });
|
|
8026
8135
|
const contentId = idOut.trim();
|
|
8027
8136
|
if (!contentId) throw new Error("could not resolve issue node id");
|
|
8028
|
-
const { stdout } = await
|
|
8137
|
+
const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: 1e4 });
|
|
8029
8138
|
const projectItemId = parseAddedItemId(stdout);
|
|
8030
8139
|
if (projectItemId && priority) {
|
|
8031
8140
|
try {
|
|
8032
|
-
await setBoardItemPriority(
|
|
8033
|
-
async (args) => execFileP3("gh", args, { timeout: 1e4 }),
|
|
8034
|
-
cfg,
|
|
8035
|
-
projectItemId,
|
|
8036
|
-
priority
|
|
8037
|
-
);
|
|
8141
|
+
await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
|
|
8038
8142
|
} catch (e) {
|
|
8039
8143
|
const err = e;
|
|
8040
8144
|
process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
|
|
@@ -8053,7 +8157,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
8053
8157
|
try {
|
|
8054
8158
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
8055
8159
|
if (o.repo) args.push("--repo", o.repo);
|
|
8056
|
-
(0,
|
|
8160
|
+
(0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
8057
8161
|
detached: true,
|
|
8058
8162
|
stdio: "ignore",
|
|
8059
8163
|
windowsHide: true,
|
|
@@ -8109,7 +8213,7 @@ function openInEditor(path2) {
|
|
|
8109
8213
|
return;
|
|
8110
8214
|
}
|
|
8111
8215
|
try {
|
|
8112
|
-
(0,
|
|
8216
|
+
(0, import_node_child_process6.spawn)(editor, [path2], { stdio: "inherit" });
|
|
8113
8217
|
} catch {
|
|
8114
8218
|
console.log(`open ${path2} manually`);
|
|
8115
8219
|
}
|
|
@@ -8167,7 +8271,9 @@ function makeSecretsDeps(cfg) {
|
|
|
8167
8271
|
apiUrl: cfg.sagaApiUrl,
|
|
8168
8272
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
8169
8273
|
headers: (extra) => sagaHeaders(extra),
|
|
8170
|
-
|
|
8274
|
+
// Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
|
|
8275
|
+
// casing, which leaked mixed-case into `secrets where` output (#681).
|
|
8276
|
+
slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
|
|
8171
8277
|
readSecretValue: () => readSecretStdin(),
|
|
8172
8278
|
log: (m) => console.log(m),
|
|
8173
8279
|
err: (m) => console.error(m)
|
|
@@ -8267,22 +8373,22 @@ async function v2ReadinessDeps(cfg) {
|
|
|
8267
8373
|
async function updateV2ReadinessIssue(repo, report, healed) {
|
|
8268
8374
|
const title = "v2 readiness: central deploy + secrets alignment";
|
|
8269
8375
|
const freshBody = renderReadinessIssueBody("", report, { healed });
|
|
8270
|
-
const list = await
|
|
8376
|
+
const list = await execFileP4("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
|
|
8271
8377
|
const issues = JSON.parse(list.stdout || "[]");
|
|
8272
8378
|
const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
|
|
8273
8379
|
if (!existing) {
|
|
8274
|
-
const created = await
|
|
8380
|
+
const created = await execFileP4("gh", ["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"], { timeout: 2e4 });
|
|
8275
8381
|
const url = created.stdout.trim();
|
|
8276
8382
|
const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
|
|
8277
8383
|
return { number, url, action: "created" };
|
|
8278
8384
|
}
|
|
8279
|
-
const view = await
|
|
8385
|
+
const view = await execFileP4("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
|
|
8280
8386
|
const current = JSON.parse(view.stdout || "{}");
|
|
8281
8387
|
const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
|
|
8282
|
-
await
|
|
8388
|
+
await execFileP4("gh", ["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody], { timeout: 2e4 });
|
|
8283
8389
|
return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
|
|
8284
8390
|
}
|
|
8285
|
-
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
|
|
8391
|
+
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); attest is project-admin; set is master-only");
|
|
8286
8392
|
async function projectTarget(commandName, explicitTarget) {
|
|
8287
8393
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
8288
8394
|
}
|
|
@@ -8319,7 +8425,7 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
8319
8425
|
}
|
|
8320
8426
|
fail(msg);
|
|
8321
8427
|
});
|
|
8322
|
-
project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
|
|
8428
|
+
project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files \u2014 appOwnedGaps are advisory templates; clear them with `project attest`; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
|
|
8323
8429
|
const cfg = await loadConfig();
|
|
8324
8430
|
let target;
|
|
8325
8431
|
try {
|
|
@@ -8369,6 +8475,18 @@ project.command("readiness [owner/repo]").description("update the repo v2 readin
|
|
|
8369
8475
|
const issue2 = await updateV2ReadinessIssue(target, report, []);
|
|
8370
8476
|
console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
|
|
8371
8477
|
});
|
|
8478
|
+
project.command("attest [owner/repo]").description("attest this repo's app-owned v2 readiness work is done (project-admin of the repo or master) \u2014 clears doctor's static appOwnedGaps; re-running overwrites; defaults to the current repo").option("--json", "machine-readable output").action(async (repoOrSlug) => {
|
|
8479
|
+
const cfg = await loadConfig();
|
|
8480
|
+
let target;
|
|
8481
|
+
try {
|
|
8482
|
+
target = await projectTarget("project attest", repoOrSlug);
|
|
8483
|
+
} catch (e) {
|
|
8484
|
+
return fail(e.message);
|
|
8485
|
+
}
|
|
8486
|
+
const repo = target.includes("/") ? target : `mutmutco/${slugOf(target)}`;
|
|
8487
|
+
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
8488
|
+
reportWrite("project attest", res);
|
|
8489
|
+
});
|
|
8372
8490
|
project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--var <KEY=VALUE...>", "META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
8373
8491
|
const cfg = await loadConfig();
|
|
8374
8492
|
let target;
|
|
@@ -8483,7 +8601,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8483
8601
|
const la = ["label", "create", label, "--color", "ededed"];
|
|
8484
8602
|
if (o.repo) la.push("--repo", o.repo);
|
|
8485
8603
|
try {
|
|
8486
|
-
await
|
|
8604
|
+
await execFileP4("gh", la, { timeout: 1e4 });
|
|
8487
8605
|
} catch {
|
|
8488
8606
|
}
|
|
8489
8607
|
}
|
|
@@ -8498,7 +8616,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8498
8616
|
const repo = await resolveRepo(o.repo);
|
|
8499
8617
|
if (!repo) return fail("issue discover-related: could not resolve repo");
|
|
8500
8618
|
try {
|
|
8501
|
-
const issues = await
|
|
8619
|
+
const issues = await ghJson([
|
|
8502
8620
|
"issue",
|
|
8503
8621
|
"list",
|
|
8504
8622
|
"--repo",
|
|
@@ -8513,7 +8631,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8513
8631
|
const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
|
|
8514
8632
|
if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
|
|
8515
8633
|
if (!candidates.length) return;
|
|
8516
|
-
const viewed = await
|
|
8634
|
+
const viewed = await ghJson([
|
|
8517
8635
|
"issue",
|
|
8518
8636
|
"view",
|
|
8519
8637
|
String(number),
|
|
@@ -8523,11 +8641,11 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8523
8641
|
"comments"
|
|
8524
8642
|
]);
|
|
8525
8643
|
if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
|
|
8526
|
-
await
|
|
8644
|
+
await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: 1e4 });
|
|
8527
8645
|
} catch {
|
|
8528
8646
|
}
|
|
8529
8647
|
});
|
|
8530
|
-
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").action(async (o) => {
|
|
8648
|
+
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) => {
|
|
8531
8649
|
let body;
|
|
8532
8650
|
let priority;
|
|
8533
8651
|
let args;
|
|
@@ -8550,7 +8668,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8550
8668
|
if (!o.force) {
|
|
8551
8669
|
let openReports = [];
|
|
8552
8670
|
try {
|
|
8553
|
-
openReports = await
|
|
8671
|
+
openReports = await ghJson([
|
|
8554
8672
|
"issue",
|
|
8555
8673
|
"list",
|
|
8556
8674
|
"--repo",
|
|
@@ -8569,7 +8687,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8569
8687
|
const dup = findDuplicateReport({ title: o.title, body }, openReports);
|
|
8570
8688
|
if (dup) {
|
|
8571
8689
|
try {
|
|
8572
|
-
await
|
|
8690
|
+
await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: 1e4 });
|
|
8573
8691
|
} catch (e) {
|
|
8574
8692
|
const err = e;
|
|
8575
8693
|
return fail(`report: duplicate of #${dup.number} (${dup.url}) but the +1 comment failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
@@ -8578,7 +8696,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8578
8696
|
}
|
|
8579
8697
|
}
|
|
8580
8698
|
try {
|
|
8581
|
-
await
|
|
8699
|
+
await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: 1e4 });
|
|
8582
8700
|
} catch {
|
|
8583
8701
|
}
|
|
8584
8702
|
const created = await ghCreate(args);
|
|
@@ -8593,7 +8711,7 @@ pr.command("create").description("create a PR and print {number,url} JSON").requ
|
|
|
8593
8711
|
async function remoteBranchExists(branch) {
|
|
8594
8712
|
if (!branch) return void 0;
|
|
8595
8713
|
try {
|
|
8596
|
-
const { stdout } = await
|
|
8714
|
+
const { stdout } = await execFileP4("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS });
|
|
8597
8715
|
return stdout.trim().length > 0;
|
|
8598
8716
|
} catch {
|
|
8599
8717
|
return void 0;
|
|
@@ -8602,15 +8720,15 @@ async function remoteBranchExists(branch) {
|
|
|
8602
8720
|
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) => {
|
|
8603
8721
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
8604
8722
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
8605
|
-
const headRef = (await
|
|
8606
|
-
const startingPath = (await
|
|
8723
|
+
const headRef = (await execFileP4("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
8724
|
+
const startingPath = (await execFileP4("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
8607
8725
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
8608
|
-
(await
|
|
8726
|
+
(await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
8609
8727
|
);
|
|
8610
8728
|
const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
|
|
8611
8729
|
let remoteDeleteAttempted = false;
|
|
8612
8730
|
let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
|
|
8613
|
-
await
|
|
8731
|
+
await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GC_GH_TIMEOUT_MS }).catch((e) => {
|
|
8614
8732
|
const message = String(e.message || "");
|
|
8615
8733
|
if (/already been merged/i.test(message)) {
|
|
8616
8734
|
remoteNotAttemptedReason = "pr-already-merged";
|
|
@@ -8633,7 +8751,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
8633
8751
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
8634
8752
|
beforeWorktrees,
|
|
8635
8753
|
startingPath,
|
|
8636
|
-
execGit: async (args) => (await
|
|
8754
|
+
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
8637
8755
|
});
|
|
8638
8756
|
console.log(JSON.stringify({
|
|
8639
8757
|
merged: number,
|
|
@@ -8758,7 +8876,7 @@ function stageKeepAlive() {
|
|
|
8758
8876
|
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) => {
|
|
8759
8877
|
const path2 = (0, import_node_path5.join)(process.cwd(), "infra", "port-ranges.json");
|
|
8760
8878
|
const allocate = async (seed) => {
|
|
8761
|
-
const { stdout } = await
|
|
8879
|
+
const { stdout } = await execFileP4("node", [(0, import_node_path5.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
8762
8880
|
const parsed = JSON.parse(stdout);
|
|
8763
8881
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
8764
8882
|
return parsed.range;
|
|
@@ -8864,8 +8982,8 @@ for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
|
8864
8982
|
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
8865
8983
|
try {
|
|
8866
8984
|
const result = await runTrainApply(commandName, {
|
|
8867
|
-
run: async (file, args) => (await
|
|
8868
|
-
runSelf: async (args) => (await
|
|
8985
|
+
run: async (file, args) => (await execFileP4(file, args, { timeout: file === "gh" ? 3e4 : GIT_TIMEOUT_MS })).stdout,
|
|
8986
|
+
runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
|
|
8869
8987
|
trainAuthority: async (repo) => {
|
|
8870
8988
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
8871
8989
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
@@ -8896,7 +9014,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
8896
9014
|
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
8897
9015
|
const meta = await fetchProjectBySlug(slug, { baseUrl: cfg.sagaApiUrl, token: githubToken });
|
|
8898
9016
|
const report = await verifyBootstrap(repo, o.class, {
|
|
8899
|
-
|
|
9017
|
+
client: defaultGitHubClient(),
|
|
8900
9018
|
projectMeta: meta,
|
|
8901
9019
|
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs5.existsSync)(path2) ? (0, import_node_fs5.readFileSync)(path2, "utf8") : null,
|
|
8902
9020
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
@@ -8909,7 +9027,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
8909
9027
|
})(),
|
|
8910
9028
|
listEnabledGcpApis: async (gcpProject) => {
|
|
8911
9029
|
try {
|
|
8912
|
-
const { stdout } = await
|
|
9030
|
+
const { stdout } = await execFileP4(
|
|
8913
9031
|
"gcloud",
|
|
8914
9032
|
["services", "list", "--enabled", "--project", gcpProject, "--format", "value(config.name)"],
|
|
8915
9033
|
{ timeout: 3e4 }
|
|
@@ -8943,7 +9061,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
8943
9061
|
const manifest = loadBootstrapSeeds((0, import_node_fs5.readFileSync)(manifestPath, "utf8"));
|
|
8944
9062
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
8945
9063
|
const slug = parsedRepo.slug;
|
|
8946
|
-
const gh = async (args) =>
|
|
9064
|
+
const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
|
|
8947
9065
|
const readFile2 = (p) => (0, import_node_fs5.existsSync)(p) ? (0, import_node_fs5.readFileSync)(p, "utf8") : null;
|
|
8948
9066
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
8949
9067
|
const vars = {};
|
|
@@ -9060,7 +9178,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
9060
9178
|
});
|
|
9061
9179
|
access.command("audit").description("audit collaborator roles + train-branch push allowlists vs the locked state; read-only, emits gh remediation, never applies").option("--json", "machine-readable output").option("--repo <owner/repo>", "audit a single repo instead of the whole org").option("--class <class>", "repo class for --repo (deployable | content)", "deployable").action(async () => {
|
|
9062
9180
|
const o = { json: rawFlag("--json"), repo: rawValue("--repo", ""), class: rawValue("--class", "deployable") };
|
|
9063
|
-
const deps = {
|
|
9181
|
+
const deps = { client: defaultGitHubClient() };
|
|
9064
9182
|
let targets;
|
|
9065
9183
|
const cfg = await loadConfig();
|
|
9066
9184
|
const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -9208,17 +9326,17 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9208
9326
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
9209
9327
|
const [login, pathProbe, releasedVersion, cfg, callerArn, cloneProbe] = await Promise.all([
|
|
9210
9328
|
githubLogin(),
|
|
9211
|
-
|
|
9329
|
+
execFileP4(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
|
|
9212
9330
|
fetchReleasedVersion(),
|
|
9213
9331
|
loadConfig(),
|
|
9214
9332
|
awsCallerArn(),
|
|
9215
|
-
|
|
9333
|
+
execFileP4("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false)
|
|
9216
9334
|
// unset → repair below
|
|
9217
9335
|
]);
|
|
9218
9336
|
let ghInstalled = true;
|
|
9219
9337
|
if (!login) {
|
|
9220
9338
|
try {
|
|
9221
|
-
await
|
|
9339
|
+
await execFileP4("gh", ["--version"]);
|
|
9222
9340
|
} catch {
|
|
9223
9341
|
ghInstalled = false;
|
|
9224
9342
|
}
|
|
@@ -9231,6 +9349,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9231
9349
|
}
|
|
9232
9350
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
9233
9351
|
const surface = detectSurface(process.env);
|
|
9352
|
+
const reloadHint = reloadAction(surface);
|
|
9234
9353
|
let versionReport = buildVersionLagReport({
|
|
9235
9354
|
currentVersion: resolveVersion(),
|
|
9236
9355
|
repoVersion: readRepoVersion(),
|
|
@@ -9244,7 +9363,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9244
9363
|
let cloneOk = cloneProbe;
|
|
9245
9364
|
if (!cloneOk && !opts.banner && !opts.json) {
|
|
9246
9365
|
try {
|
|
9247
|
-
await
|
|
9366
|
+
await execFileP4("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
9248
9367
|
cloneOk = true;
|
|
9249
9368
|
io.err(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
|
|
9250
9369
|
} catch {
|
|
@@ -9257,12 +9376,13 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9257
9376
|
settings: readClaudeSettings(),
|
|
9258
9377
|
installed,
|
|
9259
9378
|
projectPath: process.cwd(),
|
|
9260
|
-
mirrorFrom: existingMirrorRecord(installed)
|
|
9379
|
+
mirrorFrom: existingMirrorRecord(installed),
|
|
9380
|
+
surface
|
|
9261
9381
|
});
|
|
9262
|
-
if (!pluginCheck.ok && pluginCheck.recordToInsert && !opts.json
|
|
9382
|
+
if (!pluginCheck.ok && pluginCheck.recordToInsert && !opts.json) {
|
|
9263
9383
|
if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
|
|
9264
9384
|
pluginCheck = { ...pluginCheck, ok: true };
|
|
9265
|
-
io.err(
|
|
9385
|
+
io.err(` \u21BB repaired: registered mmi@mmi project install record \u2014 ${reloadHint} to load MMI commands`);
|
|
9266
9386
|
}
|
|
9267
9387
|
}
|
|
9268
9388
|
checks.push(pluginCheck);
|
|
@@ -9275,10 +9395,10 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9275
9395
|
}
|
|
9276
9396
|
checks.push(gitignoreCheck);
|
|
9277
9397
|
let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), installed, surface });
|
|
9278
|
-
if (!driftCheck.ok && driftCheck.recordsToWrite && !opts.json
|
|
9398
|
+
if (!driftCheck.ok && driftCheck.recordsToWrite && !opts.json) {
|
|
9279
9399
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
9280
9400
|
driftCheck = { ...driftCheck, ok: true };
|
|
9281
|
-
io.err(
|
|
9401
|
+
io.err(` \u21BB repaired: collapsed mmi@mmi to one user-scope entry (backup at installed_plugins.json.bak) \u2014 ${reloadHint} to load MMI commands`);
|
|
9282
9402
|
}
|
|
9283
9403
|
}
|
|
9284
9404
|
checks.push(driftCheck);
|
|
@@ -9298,8 +9418,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9298
9418
|
});
|
|
9299
9419
|
installedVersionCheck = healed;
|
|
9300
9420
|
if (healed.ok) {
|
|
9301
|
-
|
|
9302
|
-
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reload} to load the new commands`);
|
|
9421
|
+
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
9303
9422
|
}
|
|
9304
9423
|
}
|
|
9305
9424
|
}
|
|
@@ -9311,12 +9430,12 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9311
9430
|
releasedVersion,
|
|
9312
9431
|
installedVersions: installedPluginVersions(installed)
|
|
9313
9432
|
});
|
|
9314
|
-
if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && !opts.json
|
|
9433
|
+
if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && !opts.json) {
|
|
9315
9434
|
const moved = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
9316
9435
|
if (moved > 0) {
|
|
9317
9436
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
9318
9437
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
9319
|
-
io.err(` \u21BB quarantined ${moved} stale MMI plugin cache dir(s) for ${surfaces || "agent surfaces"}: ${names} \u2014
|
|
9438
|
+
io.err(` \u21BB quarantined ${moved} stale MMI plugin cache dir(s) for ${surfaces || "agent surfaces"}: ${names} \u2014 ${reloadHint} to load MMI commands`);
|
|
9320
9439
|
}
|
|
9321
9440
|
cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
9322
9441
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|