@mutmutco/cli 2.10.1 → 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 +451 -344
- 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);
|
|
@@ -5347,12 +5399,12 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
5347
5399
|
}
|
|
5348
5400
|
|
|
5349
5401
|
// src/stage-runner.ts
|
|
5350
|
-
var
|
|
5402
|
+
var import_node_child_process5 = require("node:child_process");
|
|
5351
5403
|
var import_node_fs3 = require("node:fs");
|
|
5352
5404
|
var import_node_path3 = require("node:path");
|
|
5353
5405
|
var import_node_net = require("node:net");
|
|
5354
|
-
var
|
|
5355
|
-
var
|
|
5406
|
+
var import_node_util5 = require("node:util");
|
|
5407
|
+
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
|
|
5356
5408
|
function stageStatePath(cwd = process.cwd()) {
|
|
5357
5409
|
return (0, import_node_path3.join)(cwd, "tmp", "stage", "state.json");
|
|
5358
5410
|
}
|
|
@@ -5387,7 +5439,7 @@ function isPortFree(port) {
|
|
|
5387
5439
|
});
|
|
5388
5440
|
}
|
|
5389
5441
|
async function shell(command, cwd, timeoutMs) {
|
|
5390
|
-
await
|
|
5442
|
+
await execFileP3(command, [], {
|
|
5391
5443
|
cwd,
|
|
5392
5444
|
shell: true,
|
|
5393
5445
|
timeout: timeoutMs,
|
|
@@ -5406,7 +5458,7 @@ function readState(path2) {
|
|
|
5406
5458
|
async function killTree(pid) {
|
|
5407
5459
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
5408
5460
|
if (process.platform === "win32") {
|
|
5409
|
-
await
|
|
5461
|
+
await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
|
|
5410
5462
|
return;
|
|
5411
5463
|
}
|
|
5412
5464
|
try {
|
|
@@ -5469,7 +5521,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
5469
5521
|
}
|
|
5470
5522
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
5471
5523
|
const up = sub(config.up.trim());
|
|
5472
|
-
const child = (0,
|
|
5524
|
+
const child = (0, import_node_child_process5.spawn)(up, {
|
|
5473
5525
|
cwd,
|
|
5474
5526
|
shell: true,
|
|
5475
5527
|
detached: true,
|
|
@@ -5759,22 +5811,29 @@ function safeJson(text, fallback) {
|
|
|
5759
5811
|
return fallback;
|
|
5760
5812
|
}
|
|
5761
5813
|
}
|
|
5762
|
-
async function
|
|
5814
|
+
async function restJson(deps, path2, fallback) {
|
|
5763
5815
|
try {
|
|
5764
|
-
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);
|
|
5765
5824
|
} catch {
|
|
5766
5825
|
return fallback;
|
|
5767
5826
|
}
|
|
5768
5827
|
}
|
|
5769
5828
|
async function resolveOwners(deps) {
|
|
5770
|
-
const members = await
|
|
5829
|
+
const members = await restPagedJson(deps, `orgs/${OWNER}/members?role=admin`, []);
|
|
5771
5830
|
return members.map((m) => m.login);
|
|
5772
5831
|
}
|
|
5773
5832
|
function collaboratorRole(c) {
|
|
5774
5833
|
return c.role_name ?? (c.permissions?.admin ? "admin" : c.permissions?.maintain ? "maintain" : "write");
|
|
5775
5834
|
}
|
|
5776
5835
|
async function auditRepoCollaborators(repo, owners, deps) {
|
|
5777
|
-
const collabs = await
|
|
5836
|
+
const collabs = await restPagedJson(deps, `repos/${repo}/collaborators?affiliation=direct`, []);
|
|
5778
5837
|
const findings = [];
|
|
5779
5838
|
for (const c of collabs) {
|
|
5780
5839
|
if (owners.has(c.login)) continue;
|
|
@@ -5795,7 +5854,7 @@ async function auditRepoCollaborators(repo, owners, deps) {
|
|
|
5795
5854
|
async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @__PURE__ */ new Set()) {
|
|
5796
5855
|
let restrictions = null;
|
|
5797
5856
|
try {
|
|
5798
|
-
restrictions =
|
|
5857
|
+
restrictions = await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection/restrictions`) ?? null;
|
|
5799
5858
|
} catch {
|
|
5800
5859
|
restrictions = null;
|
|
5801
5860
|
}
|
|
@@ -5879,7 +5938,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
|
|
|
5879
5938
|
return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
5880
5939
|
}
|
|
5881
5940
|
async function auditOrgBasePermission(deps) {
|
|
5882
|
-
const org = await
|
|
5941
|
+
const org = await restJson(deps, `orgs/${OWNER}`, {});
|
|
5883
5942
|
const perm = org.default_repository_permission;
|
|
5884
5943
|
if (perm && perm !== "read" && perm !== "none") {
|
|
5885
5944
|
return [{
|
|
@@ -6022,9 +6081,16 @@ function safeJson2(text, fallback) {
|
|
|
6022
6081
|
return fallback;
|
|
6023
6082
|
}
|
|
6024
6083
|
}
|
|
6025
|
-
async function
|
|
6084
|
+
async function restJson2(deps, path2, fallback) {
|
|
6085
|
+
try {
|
|
6086
|
+
return await deps.client.rest("GET", path2) ?? fallback;
|
|
6087
|
+
} catch {
|
|
6088
|
+
return fallback;
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
async function restPagedJson2(deps, path2, fallback) {
|
|
6026
6092
|
try {
|
|
6027
|
-
return
|
|
6093
|
+
return await deps.client.restPaginate(path2);
|
|
6028
6094
|
} catch {
|
|
6029
6095
|
return fallback;
|
|
6030
6096
|
}
|
|
@@ -6032,7 +6098,7 @@ async function ghJson2(deps, args, fallback) {
|
|
|
6032
6098
|
async function contentExists(deps, repo, branch, path2) {
|
|
6033
6099
|
try {
|
|
6034
6100
|
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
6035
|
-
await deps.
|
|
6101
|
+
await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
6036
6102
|
return true;
|
|
6037
6103
|
} catch {
|
|
6038
6104
|
return false;
|
|
@@ -6040,7 +6106,7 @@ async function contentExists(deps, repo, branch, path2) {
|
|
|
6040
6106
|
}
|
|
6041
6107
|
async function getProtection(deps, repo, branch) {
|
|
6042
6108
|
try {
|
|
6043
|
-
return
|
|
6109
|
+
return await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection`) ?? {};
|
|
6044
6110
|
} catch {
|
|
6045
6111
|
return null;
|
|
6046
6112
|
}
|
|
@@ -6068,7 +6134,7 @@ async function rulesetDetails(deps, repo, list) {
|
|
|
6068
6134
|
details.push(ruleset);
|
|
6069
6135
|
continue;
|
|
6070
6136
|
}
|
|
6071
|
-
details.push(await
|
|
6137
|
+
details.push(await restJson2(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
|
|
6072
6138
|
}
|
|
6073
6139
|
return details;
|
|
6074
6140
|
}
|
|
@@ -6081,11 +6147,11 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6081
6147
|
const branchesWanted = expectedBranches(repoClass);
|
|
6082
6148
|
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
6083
6149
|
const checks = [];
|
|
6084
|
-
const repoInfo = await
|
|
6150
|
+
const repoInfo = await restJson2(deps, `repos/${repo}`, {});
|
|
6085
6151
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
6086
6152
|
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
6087
6153
|
checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
|
|
6088
|
-
const branchList = await
|
|
6154
|
+
const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
|
|
6089
6155
|
const branchNames = new Set(branchList.map((b) => b.name));
|
|
6090
6156
|
for (const branch of branchesWanted) {
|
|
6091
6157
|
checks.push({ ok: branchNames.has(branch), label: `branch exists: ${branch}` });
|
|
@@ -6118,14 +6184,14 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6118
6184
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
|
|
6119
6185
|
}
|
|
6120
6186
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
|
|
6121
|
-
const labels = await
|
|
6187
|
+
const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
|
|
6122
6188
|
const labelNames = new Set(labels.map((l) => l.name));
|
|
6123
6189
|
for (const label of requiredLabels) {
|
|
6124
6190
|
checks.push({ ok: labelNames.has(label), label: `label exists: ${label}` });
|
|
6125
6191
|
}
|
|
6126
6192
|
const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
|
|
6127
6193
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
6128
|
-
const actions = await
|
|
6194
|
+
const actions = await restJson2(deps, `repos/${repo}/actions/permissions`, {});
|
|
6129
6195
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
6130
6196
|
const config = deps.projectMeta ?? null;
|
|
6131
6197
|
checks.push({
|
|
@@ -6133,12 +6199,18 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6133
6199
|
label: "registry project board META exists"
|
|
6134
6200
|
});
|
|
6135
6201
|
if (config?.projectOwner && config.projectNumber != null) {
|
|
6136
|
-
const
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
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
|
+
})();
|
|
6142
6214
|
const statusField = fields.find((field) => field.name === "Status");
|
|
6143
6215
|
const labelField = fields.find((field) => field.name === "Labels");
|
|
6144
6216
|
checks.push({
|
|
@@ -6195,12 +6267,17 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6195
6267
|
}
|
|
6196
6268
|
}
|
|
6197
6269
|
const workflowQuery = "query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { workflows(first: 30) { nodes { name enabled } } } } }";
|
|
6198
|
-
const workflowResponse = await
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
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 || [];
|
|
6204
6281
|
for (const workflowName of requiredProjectWorkflows) {
|
|
6205
6282
|
checks.push({
|
|
6206
6283
|
ok: workflows.some((workflow) => workflow.name === workflowName && workflow.enabled === true),
|
|
@@ -6212,11 +6289,7 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6212
6289
|
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
6213
6290
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && json.projects.some((p) => (p.repos || []).includes(repo)));
|
|
6214
6291
|
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
|
|
6215
|
-
const rulesetList = await
|
|
6216
|
-
deps,
|
|
6217
|
-
["api", `repos/${repo}/rulesets?includes_parents=true`],
|
|
6218
|
-
[]
|
|
6219
|
-
);
|
|
6292
|
+
const rulesetList = await restJson2(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
6220
6293
|
const rulesets = await rulesetDetails(deps, repo, rulesetList);
|
|
6221
6294
|
const activeOrgRulesets = rulesets.filter(
|
|
6222
6295
|
(r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
|
|
@@ -6402,7 +6475,7 @@ var ORG_CONFIG_PATH = "/org/config";
|
|
|
6402
6475
|
var PROJECTS_ENVELOPE_KEY = "projects";
|
|
6403
6476
|
|
|
6404
6477
|
// src/registry-client.ts
|
|
6405
|
-
var
|
|
6478
|
+
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
6406
6479
|
async function fetchTrainAuthority(repo, deps) {
|
|
6407
6480
|
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
6408
6481
|
const token = await deps.token();
|
|
@@ -6412,7 +6485,7 @@ async function fetchTrainAuthority(repo, deps) {
|
|
|
6412
6485
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
|
|
6413
6486
|
method: "GET",
|
|
6414
6487
|
headers: { Authorization: `Bearer ${token}` },
|
|
6415
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6488
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6416
6489
|
});
|
|
6417
6490
|
if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
|
|
6418
6491
|
const body = await res.json();
|
|
@@ -6431,7 +6504,7 @@ async function fetchProjectsList(deps) {
|
|
|
6431
6504
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${PROJECTS_LIST_PATH}`, {
|
|
6432
6505
|
method: "GET",
|
|
6433
6506
|
headers: { Authorization: `Bearer ${token}` },
|
|
6434
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6507
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6435
6508
|
});
|
|
6436
6509
|
if (!res.ok) return null;
|
|
6437
6510
|
const body = await res.json();
|
|
@@ -6455,7 +6528,7 @@ async function fetchProjectBySlugChecked(slug, deps) {
|
|
|
6455
6528
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
|
|
6456
6529
|
method: "GET",
|
|
6457
6530
|
headers: { Authorization: `Bearer ${token}` },
|
|
6458
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6531
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6459
6532
|
});
|
|
6460
6533
|
if (res.status === 404) return { ok: true, project: null };
|
|
6461
6534
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
@@ -6477,7 +6550,7 @@ async function fetchDeployStatusBySlug(slug, deps) {
|
|
|
6477
6550
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
|
|
6478
6551
|
method: "GET",
|
|
6479
6552
|
headers: { Authorization: `Bearer ${token}` },
|
|
6480
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6553
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6481
6554
|
});
|
|
6482
6555
|
if (!res.ok) return null;
|
|
6483
6556
|
const body = await res.json();
|
|
@@ -6496,7 +6569,7 @@ async function fetchOrgConfig(deps) {
|
|
|
6496
6569
|
const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${ORG_CONFIG_PATH}`, {
|
|
6497
6570
|
method: "GET",
|
|
6498
6571
|
headers: { Authorization: `Bearer ${token}` },
|
|
6499
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6572
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6500
6573
|
});
|
|
6501
6574
|
if (!res.ok) return null;
|
|
6502
6575
|
return await res.json();
|
|
@@ -6514,7 +6587,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST") {
|
|
|
6514
6587
|
method,
|
|
6515
6588
|
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6516
6589
|
body: JSON.stringify(payload),
|
|
6517
|
-
signal: AbortSignal.timeout(deps.timeoutMs ??
|
|
6590
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6518
6591
|
});
|
|
6519
6592
|
let body = null;
|
|
6520
6593
|
try {
|
|
@@ -6532,6 +6605,9 @@ async function registerProject(payload, deps) {
|
|
|
6532
6605
|
async function upsertProject(slug, patch, deps) {
|
|
6533
6606
|
return postJson(`/projects/${encodeURIComponent(slug)}`, patch, deps);
|
|
6534
6607
|
}
|
|
6608
|
+
async function attestAppGaps(slug, repo, deps) {
|
|
6609
|
+
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
6610
|
+
}
|
|
6535
6611
|
async function tenantControl(payload, deps) {
|
|
6536
6612
|
return postJson("/tenant-control", payload, deps);
|
|
6537
6613
|
}
|
|
@@ -6570,7 +6646,21 @@ function stageRequiredSecrets(stage2, meta) {
|
|
|
6570
6646
|
function stageKey(stage2, key) {
|
|
6571
6647
|
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
6572
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
|
+
}
|
|
6573
6661
|
function appGapsFor(meta, model, slug, projectType) {
|
|
6662
|
+
const attested = appAttestationOf(meta);
|
|
6663
|
+
if (attested) return [attestedLine(attested)];
|
|
6574
6664
|
if (projectType === "content" || model === "content") return ["Content/KB repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
6575
6665
|
if (projectType === "desktop-game") {
|
|
6576
6666
|
return [
|
|
@@ -6602,6 +6692,9 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
6602
6692
|
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
6603
6693
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
6604
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
|
+
}
|
|
6605
6698
|
if (slug === "mmi-katip") {
|
|
6606
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.");
|
|
6607
6700
|
}
|
|
@@ -6741,7 +6834,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
6741
6834
|
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets: secrets2 },
|
|
6742
6835
|
secretsError,
|
|
6743
6836
|
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
6744
|
-
appOwnedGaps: autoHeal.appOwnedGaps
|
|
6837
|
+
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
6838
|
+
appAttested: appAttestationOf(meta) ?? void 0
|
|
6745
6839
|
};
|
|
6746
6840
|
}
|
|
6747
6841
|
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
@@ -6834,6 +6928,10 @@ function buildProjectSetPatch(input) {
|
|
|
6834
6928
|
}
|
|
6835
6929
|
return patch;
|
|
6836
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
|
+
}
|
|
6837
6935
|
function requireProjectTarget(commandName, explicitTarget, currentRepo) {
|
|
6838
6936
|
const target = explicitTarget?.trim() || currentRepo?.trim();
|
|
6839
6937
|
if (!target) {
|
|
@@ -7113,12 +7211,14 @@ function formatVaultPointer(p) {
|
|
|
7113
7211
|
}
|
|
7114
7212
|
var TIMEOUT_MS2 = 8e3;
|
|
7115
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
|
+
}
|
|
7116
7217
|
async function targetRepo(deps, opts) {
|
|
7117
|
-
return opts.repo ?? repoOf(await deps
|
|
7218
|
+
return opts.repo ?? repoOf(await vaultSlug(deps, opts));
|
|
7118
7219
|
}
|
|
7119
7220
|
async function secretsWhere(deps, opts) {
|
|
7120
|
-
|
|
7121
|
-
deps.log(formatVaultPointer(vaultPointer(slug)));
|
|
7221
|
+
deps.log(formatVaultPointer(vaultPointer(await vaultSlug(deps, opts))));
|
|
7122
7222
|
}
|
|
7123
7223
|
async function readErr(res) {
|
|
7124
7224
|
try {
|
|
@@ -7300,7 +7400,7 @@ async function secretsSet(deps, key, opts) {
|
|
|
7300
7400
|
);
|
|
7301
7401
|
return;
|
|
7302
7402
|
}
|
|
7303
|
-
deps.log(`set ${key} (${classifyTier(await deps
|
|
7403
|
+
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
7304
7404
|
}
|
|
7305
7405
|
async function secretsEdit(deps, key, opts) {
|
|
7306
7406
|
return secretsSet(deps, key, opts);
|
|
@@ -7352,8 +7452,8 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
|
|
|
7352
7452
|
}
|
|
7353
7453
|
deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
|
|
7354
7454
|
}
|
|
7355
|
-
async function secretsUse(deps, key,
|
|
7356
|
-
const slug = await deps
|
|
7455
|
+
async function secretsUse(deps, key, opts) {
|
|
7456
|
+
const slug = await vaultSlug(deps, opts);
|
|
7357
7457
|
const tier = classifyTier(slug, key);
|
|
7358
7458
|
const path2 = secretParamName(slug, key);
|
|
7359
7459
|
deps.log(
|
|
@@ -7438,30 +7538,23 @@ function authorizeBodyHasMismatch(body) {
|
|
|
7438
7538
|
}
|
|
7439
7539
|
|
|
7440
7540
|
// src/index.ts
|
|
7441
|
-
var
|
|
7541
|
+
var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
|
|
7442
7542
|
var DEFAULT_EXEC_TIMEOUT_MS = 1e4;
|
|
7443
|
-
var
|
|
7543
|
+
var execFileP4 = (file, args, options = {}) => (
|
|
7444
7544
|
// encoding 'utf8' guarantees string stdout/stderr at runtime; the cast pins the type because
|
|
7445
7545
|
// promisify(execFile)'s overloads widen to string|Buffer when options is spread in.
|
|
7446
|
-
|
|
7546
|
+
rawExecFileP3(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
|
|
7447
7547
|
);
|
|
7448
7548
|
var GIT_TIMEOUT_MS = DEFAULT_EXEC_TIMEOUT_MS;
|
|
7449
7549
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
7450
|
-
var cachedGhCliToken;
|
|
7451
|
-
async function githubToken() {
|
|
7452
|
-
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
7453
|
-
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
7454
|
-
cachedGhCliToken ??= execFileP3("gh", ["auth", "token"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
7455
|
-
return cachedGhCliToken;
|
|
7456
|
-
}
|
|
7457
7550
|
var cachedGithubLogin;
|
|
7458
7551
|
async function githubLogin() {
|
|
7459
|
-
cachedGithubLogin ??=
|
|
7552
|
+
cachedGithubLogin ??= execFileP4("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
7460
7553
|
return cachedGithubLogin;
|
|
7461
7554
|
}
|
|
7462
7555
|
async function awsCallerArn() {
|
|
7463
7556
|
try {
|
|
7464
|
-
const { stdout } = await
|
|
7557
|
+
const { stdout } = await execFileP4(
|
|
7465
7558
|
"aws",
|
|
7466
7559
|
["sts", "get-caller-identity", "--query", "Arn", "--output", "text"],
|
|
7467
7560
|
{ timeout: GIT_TIMEOUT_MS }
|
|
@@ -7521,7 +7614,7 @@ var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/d
|
|
|
7521
7614
|
var SESSION_FILE = ".mmi/.session";
|
|
7522
7615
|
var gitOut = async (args) => {
|
|
7523
7616
|
try {
|
|
7524
|
-
return (await
|
|
7617
|
+
return (await execFileP4("git", [...args])).stdout.trim();
|
|
7525
7618
|
} catch {
|
|
7526
7619
|
return "";
|
|
7527
7620
|
}
|
|
@@ -7593,18 +7686,18 @@ async function readStdin() {
|
|
|
7593
7686
|
async function ghPrs(limit) {
|
|
7594
7687
|
const args = (state) => ["pr", "list", "--state", state, "--limit", String(limit), "--json", "number,headRefName,state"];
|
|
7595
7688
|
const [open, closed] = await Promise.all([
|
|
7596
|
-
|
|
7597
|
-
|
|
7689
|
+
execFileP4("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
|
|
7690
|
+
execFileP4("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
|
|
7598
7691
|
]);
|
|
7599
7692
|
return [...JSON.parse(open.stdout || "[]"), ...JSON.parse(closed.stdout || "[]")];
|
|
7600
7693
|
}
|
|
7601
7694
|
async function worktreeBranches() {
|
|
7602
|
-
const { stdout } = await
|
|
7695
|
+
const { stdout } = await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7603
7696
|
const parsed = parseWorktreePorcelain(stdout);
|
|
7604
7697
|
return await Promise.all(parsed.map(async (w) => {
|
|
7605
7698
|
let dirty = true;
|
|
7606
7699
|
try {
|
|
7607
|
-
const { stdout: status } = await
|
|
7700
|
+
const { stdout: status } = await execFileP4("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7608
7701
|
dirty = status.trim().length > 0;
|
|
7609
7702
|
} catch {
|
|
7610
7703
|
dirty = true;
|
|
@@ -7616,7 +7709,7 @@ async function gcPlan(remote, limit) {
|
|
|
7616
7709
|
const [branches, current, stale, prs, worktrees] = await Promise.all([
|
|
7617
7710
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
7618
7711
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
7619
|
-
|
|
7712
|
+
execFileP4("git", ["remote", "prune", remote, "--dry-run"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseRemotePruneDryRun(`${r.stdout}${r.stderr}`)).catch(() => []),
|
|
7620
7713
|
ghPrs(limit),
|
|
7621
7714
|
worktreeBranches()
|
|
7622
7715
|
]);
|
|
@@ -7644,8 +7737,8 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7644
7737
|
const result = { removedBranches: [], removedTrackingRefs: [], failed: [], pruned: false };
|
|
7645
7738
|
for (const branch of plan2.branches) {
|
|
7646
7739
|
try {
|
|
7647
|
-
if (branch.worktreePath) await
|
|
7648
|
-
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 });
|
|
7649
7742
|
result.removedBranches.push(branch.branch);
|
|
7650
7743
|
} catch (e) {
|
|
7651
7744
|
result.failed.push(`${branch.branch}: ${e.message.split("\n")[0]}`);
|
|
@@ -7653,7 +7746,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7653
7746
|
}
|
|
7654
7747
|
for (const ref of plan2.trackingRefs) {
|
|
7655
7748
|
try {
|
|
7656
|
-
await
|
|
7749
|
+
await execFileP4("git", ["update-ref", "-d", `refs/remotes/${remote}/${ref.branch}`], { timeout: GIT_TIMEOUT_MS });
|
|
7657
7750
|
result.removedTrackingRefs.push(ref.branch);
|
|
7658
7751
|
} catch (e) {
|
|
7659
7752
|
result.failed.push(`${remote}/${ref.branch}: ${e.message.split("\n")[0]}`);
|
|
@@ -7661,7 +7754,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7661
7754
|
}
|
|
7662
7755
|
if (plan2.branches.some((b) => b.worktreePath)) {
|
|
7663
7756
|
try {
|
|
7664
|
-
await
|
|
7757
|
+
await execFileP4("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
|
|
7665
7758
|
result.pruned = true;
|
|
7666
7759
|
} catch (e) {
|
|
7667
7760
|
result.failed.push(`worktree prune: ${e.message.split("\n")[0]}`);
|
|
@@ -7691,7 +7784,7 @@ function readRepoVersion() {
|
|
|
7691
7784
|
}
|
|
7692
7785
|
async function fetchReleasedVersion() {
|
|
7693
7786
|
try {
|
|
7694
|
-
const { stdout } = await
|
|
7787
|
+
const { stdout } = await execFileP4("gh", pluginManifestVersionArgs(), { timeout: 5e3 });
|
|
7695
7788
|
return parseManifestVersion(stdout);
|
|
7696
7789
|
} catch {
|
|
7697
7790
|
return void 0;
|
|
@@ -7705,9 +7798,9 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
7705
7798
|
const target = report.releasedVersion ?? "latest";
|
|
7706
7799
|
if (action === "plugin-pull") {
|
|
7707
7800
|
try {
|
|
7708
|
-
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();
|
|
7709
7802
|
log(` \u21BB refreshing MMI plugin ${report.currentVersion} \u2192 ${target} (effective next session)\u2026`);
|
|
7710
|
-
await
|
|
7803
|
+
await execFileP4("git", ["-C", root, "pull", "--ff-only"], { timeout: PLUGIN_PULL_TIMEOUT_MS });
|
|
7711
7804
|
return { ...report, ok: true };
|
|
7712
7805
|
} catch {
|
|
7713
7806
|
return report;
|
|
@@ -7734,7 +7827,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
7734
7827
|
var consoleIo = { log: (m) => console.log(m), err: (m) => console.error(m) };
|
|
7735
7828
|
var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
7736
7829
|
function runHostBin(bin, args, opts) {
|
|
7737
|
-
return isWin ?
|
|
7830
|
+
return isWin ? execFileP4("cmd.exe", ["/c", bin, ...args], opts) : execFileP4(bin, args, opts);
|
|
7738
7831
|
}
|
|
7739
7832
|
async function runClaudePlugin(args) {
|
|
7740
7833
|
try {
|
|
@@ -7805,7 +7898,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
7805
7898
|
isDirty: async (f) => await gitOut(["status", "--porcelain", "--", f]) !== "",
|
|
7806
7899
|
originContent: async (f) => {
|
|
7807
7900
|
try {
|
|
7808
|
-
return (await
|
|
7901
|
+
return (await execFileP4("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
|
|
7809
7902
|
} catch {
|
|
7810
7903
|
return null;
|
|
7811
7904
|
}
|
|
@@ -7838,14 +7931,17 @@ async function runSagaShow(opts, io = consoleIo) {
|
|
|
7838
7931
|
try {
|
|
7839
7932
|
const key = await sagaKey(cfg);
|
|
7840
7933
|
const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
|
|
7841
|
-
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) });
|
|
7842
7935
|
if (res.ok) {
|
|
7843
7936
|
io.log(resumeCue());
|
|
7844
7937
|
return io.log(await res.text());
|
|
7845
7938
|
}
|
|
7846
7939
|
if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
|
|
7847
7940
|
} catch (e) {
|
|
7848
|
-
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
|
+
}
|
|
7849
7945
|
}
|
|
7850
7946
|
}
|
|
7851
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));
|
|
@@ -7865,7 +7961,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
7865
7961
|
if (!headGateDue(tsPath)) return;
|
|
7866
7962
|
markHeadRun(tsPath);
|
|
7867
7963
|
try {
|
|
7868
|
-
(0,
|
|
7964
|
+
(0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], "saga", "head-update", "--run"], {
|
|
7869
7965
|
detached: true,
|
|
7870
7966
|
stdio: "ignore",
|
|
7871
7967
|
windowsHide: true
|
|
@@ -7982,7 +8078,7 @@ var kb = program2.command("kb").description("org knowledgebase (read-only)");
|
|
|
7982
8078
|
kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
|
|
7983
8079
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
7984
8080
|
try {
|
|
7985
|
-
const { stdout } = await
|
|
8081
|
+
const { stdout } = await execFileP4("gh", buildKbGetArgs(src, path2), { timeout: 1e4 });
|
|
7986
8082
|
process.stdout.write(stdout);
|
|
7987
8083
|
} catch (e) {
|
|
7988
8084
|
const err = e;
|
|
@@ -7992,7 +8088,7 @@ kb.command("get <path>").description("print a KB document by path").action(async
|
|
|
7992
8088
|
kb.command("list [prefix]").description("list KB document paths (optionally under a prefix)").action(async (prefix) => {
|
|
7993
8089
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
7994
8090
|
try {
|
|
7995
|
-
const { stdout } = await
|
|
8091
|
+
const { stdout } = await execFileP4("gh", buildKbTreeArgs(src), { timeout: 1e4 });
|
|
7996
8092
|
const paths = parseKbTree(stdout, prefix);
|
|
7997
8093
|
if (!paths.length) return fail(`kb list: no documents${prefix ? ` under ${prefix}` : ""}`);
|
|
7998
8094
|
console.log(paths.join("\n"));
|
|
@@ -8003,21 +8099,23 @@ kb.command("list [prefix]").description("list KB document paths (optionally unde
|
|
|
8003
8099
|
});
|
|
8004
8100
|
async function ghCreate(args) {
|
|
8005
8101
|
try {
|
|
8006
|
-
const { stdout } = await
|
|
8102
|
+
const { stdout } = await execFileP4("gh", args);
|
|
8007
8103
|
return parseCreatedUrl(stdout);
|
|
8008
8104
|
} catch (e) {
|
|
8009
8105
|
const err = e;
|
|
8010
8106
|
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
8011
8107
|
}
|
|
8012
8108
|
}
|
|
8013
|
-
async function
|
|
8014
|
-
const { stdout } = await
|
|
8109
|
+
async function ghJson(args, timeout = 1e4) {
|
|
8110
|
+
const { stdout } = await execFileP4("gh", args, { timeout });
|
|
8015
8111
|
return JSON.parse(stdout);
|
|
8016
8112
|
}
|
|
8017
8113
|
async function resolveRepo(repo) {
|
|
8018
8114
|
if (repo) return repo;
|
|
8115
|
+
const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
|
|
8116
|
+
if (fromOrigin) return fromOrigin;
|
|
8019
8117
|
try {
|
|
8020
|
-
const { stdout } = await
|
|
8118
|
+
const { stdout } = await execFileP4("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
|
|
8021
8119
|
return stdout.trim() || void 0;
|
|
8022
8120
|
} catch {
|
|
8023
8121
|
return void 0;
|
|
@@ -8033,19 +8131,14 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
8033
8131
|
try {
|
|
8034
8132
|
const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
|
|
8035
8133
|
if (targetRepo2) viewArgs.push("--repo", targetRepo2);
|
|
8036
|
-
const { stdout: idOut } = await
|
|
8134
|
+
const { stdout: idOut } = await execFileP4("gh", viewArgs, { timeout: 1e4 });
|
|
8037
8135
|
const contentId = idOut.trim();
|
|
8038
8136
|
if (!contentId) throw new Error("could not resolve issue node id");
|
|
8039
|
-
const { stdout } = await
|
|
8137
|
+
const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: 1e4 });
|
|
8040
8138
|
const projectItemId = parseAddedItemId(stdout);
|
|
8041
8139
|
if (projectItemId && priority) {
|
|
8042
8140
|
try {
|
|
8043
|
-
await setBoardItemPriority(
|
|
8044
|
-
async (args) => execFileP3("gh", args, { timeout: 1e4 }),
|
|
8045
|
-
cfg,
|
|
8046
|
-
projectItemId,
|
|
8047
|
-
priority
|
|
8048
|
-
);
|
|
8141
|
+
await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
|
|
8049
8142
|
} catch (e) {
|
|
8050
8143
|
const err = e;
|
|
8051
8144
|
process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
|
|
@@ -8064,7 +8157,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
8064
8157
|
try {
|
|
8065
8158
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
8066
8159
|
if (o.repo) args.push("--repo", o.repo);
|
|
8067
|
-
(0,
|
|
8160
|
+
(0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
8068
8161
|
detached: true,
|
|
8069
8162
|
stdio: "ignore",
|
|
8070
8163
|
windowsHide: true,
|
|
@@ -8120,7 +8213,7 @@ function openInEditor(path2) {
|
|
|
8120
8213
|
return;
|
|
8121
8214
|
}
|
|
8122
8215
|
try {
|
|
8123
|
-
(0,
|
|
8216
|
+
(0, import_node_child_process6.spawn)(editor, [path2], { stdio: "inherit" });
|
|
8124
8217
|
} catch {
|
|
8125
8218
|
console.log(`open ${path2} manually`);
|
|
8126
8219
|
}
|
|
@@ -8178,7 +8271,9 @@ function makeSecretsDeps(cfg) {
|
|
|
8178
8271
|
apiUrl: cfg.sagaApiUrl,
|
|
8179
8272
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
8180
8273
|
headers: (extra) => sagaHeaders(extra),
|
|
8181
|
-
|
|
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(),
|
|
8182
8277
|
readSecretValue: () => readSecretStdin(),
|
|
8183
8278
|
log: (m) => console.log(m),
|
|
8184
8279
|
err: (m) => console.error(m)
|
|
@@ -8278,22 +8373,22 @@ async function v2ReadinessDeps(cfg) {
|
|
|
8278
8373
|
async function updateV2ReadinessIssue(repo, report, healed) {
|
|
8279
8374
|
const title = "v2 readiness: central deploy + secrets alignment";
|
|
8280
8375
|
const freshBody = renderReadinessIssueBody("", report, { healed });
|
|
8281
|
-
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 });
|
|
8282
8377
|
const issues = JSON.parse(list.stdout || "[]");
|
|
8283
8378
|
const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
|
|
8284
8379
|
if (!existing) {
|
|
8285
|
-
const created = await
|
|
8380
|
+
const created = await execFileP4("gh", ["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"], { timeout: 2e4 });
|
|
8286
8381
|
const url = created.stdout.trim();
|
|
8287
8382
|
const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
|
|
8288
8383
|
return { number, url, action: "created" };
|
|
8289
8384
|
}
|
|
8290
|
-
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 });
|
|
8291
8386
|
const current = JSON.parse(view.stdout || "{}");
|
|
8292
8387
|
const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
|
|
8293
|
-
await
|
|
8388
|
+
await execFileP4("gh", ["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody], { timeout: 2e4 });
|
|
8294
8389
|
return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
|
|
8295
8390
|
}
|
|
8296
|
-
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");
|
|
8297
8392
|
async function projectTarget(commandName, explicitTarget) {
|
|
8298
8393
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
8299
8394
|
}
|
|
@@ -8330,7 +8425,7 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
8330
8425
|
}
|
|
8331
8426
|
fail(msg);
|
|
8332
8427
|
});
|
|
8333
|
-
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) => {
|
|
8334
8429
|
const cfg = await loadConfig();
|
|
8335
8430
|
let target;
|
|
8336
8431
|
try {
|
|
@@ -8380,6 +8475,18 @@ project.command("readiness [owner/repo]").description("update the repo v2 readin
|
|
|
8380
8475
|
const issue2 = await updateV2ReadinessIssue(target, report, []);
|
|
8381
8476
|
console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
|
|
8382
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
|
+
});
|
|
8383
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) => {
|
|
8384
8491
|
const cfg = await loadConfig();
|
|
8385
8492
|
let target;
|
|
@@ -8494,7 +8601,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8494
8601
|
const la = ["label", "create", label, "--color", "ededed"];
|
|
8495
8602
|
if (o.repo) la.push("--repo", o.repo);
|
|
8496
8603
|
try {
|
|
8497
|
-
await
|
|
8604
|
+
await execFileP4("gh", la, { timeout: 1e4 });
|
|
8498
8605
|
} catch {
|
|
8499
8606
|
}
|
|
8500
8607
|
}
|
|
@@ -8509,7 +8616,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8509
8616
|
const repo = await resolveRepo(o.repo);
|
|
8510
8617
|
if (!repo) return fail("issue discover-related: could not resolve repo");
|
|
8511
8618
|
try {
|
|
8512
|
-
const issues = await
|
|
8619
|
+
const issues = await ghJson([
|
|
8513
8620
|
"issue",
|
|
8514
8621
|
"list",
|
|
8515
8622
|
"--repo",
|
|
@@ -8524,7 +8631,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8524
8631
|
const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
|
|
8525
8632
|
if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
|
|
8526
8633
|
if (!candidates.length) return;
|
|
8527
|
-
const viewed = await
|
|
8634
|
+
const viewed = await ghJson([
|
|
8528
8635
|
"issue",
|
|
8529
8636
|
"view",
|
|
8530
8637
|
String(number),
|
|
@@ -8534,11 +8641,11 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8534
8641
|
"comments"
|
|
8535
8642
|
]);
|
|
8536
8643
|
if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
|
|
8537
|
-
await
|
|
8644
|
+
await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: 1e4 });
|
|
8538
8645
|
} catch {
|
|
8539
8646
|
}
|
|
8540
8647
|
});
|
|
8541
|
-
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) => {
|
|
8542
8649
|
let body;
|
|
8543
8650
|
let priority;
|
|
8544
8651
|
let args;
|
|
@@ -8561,7 +8668,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8561
8668
|
if (!o.force) {
|
|
8562
8669
|
let openReports = [];
|
|
8563
8670
|
try {
|
|
8564
|
-
openReports = await
|
|
8671
|
+
openReports = await ghJson([
|
|
8565
8672
|
"issue",
|
|
8566
8673
|
"list",
|
|
8567
8674
|
"--repo",
|
|
@@ -8580,7 +8687,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8580
8687
|
const dup = findDuplicateReport({ title: o.title, body }, openReports);
|
|
8581
8688
|
if (dup) {
|
|
8582
8689
|
try {
|
|
8583
|
-
await
|
|
8690
|
+
await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: 1e4 });
|
|
8584
8691
|
} catch (e) {
|
|
8585
8692
|
const err = e;
|
|
8586
8693
|
return fail(`report: duplicate of #${dup.number} (${dup.url}) but the +1 comment failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
@@ -8589,7 +8696,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8589
8696
|
}
|
|
8590
8697
|
}
|
|
8591
8698
|
try {
|
|
8592
|
-
await
|
|
8699
|
+
await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: 1e4 });
|
|
8593
8700
|
} catch {
|
|
8594
8701
|
}
|
|
8595
8702
|
const created = await ghCreate(args);
|
|
@@ -8604,7 +8711,7 @@ pr.command("create").description("create a PR and print {number,url} JSON").requ
|
|
|
8604
8711
|
async function remoteBranchExists(branch) {
|
|
8605
8712
|
if (!branch) return void 0;
|
|
8606
8713
|
try {
|
|
8607
|
-
const { stdout } = await
|
|
8714
|
+
const { stdout } = await execFileP4("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS });
|
|
8608
8715
|
return stdout.trim().length > 0;
|
|
8609
8716
|
} catch {
|
|
8610
8717
|
return void 0;
|
|
@@ -8613,15 +8720,15 @@ async function remoteBranchExists(branch) {
|
|
|
8613
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) => {
|
|
8614
8721
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
8615
8722
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
8616
|
-
const headRef = (await
|
|
8617
|
-
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();
|
|
8618
8725
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
8619
|
-
(await
|
|
8726
|
+
(await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
8620
8727
|
);
|
|
8621
8728
|
const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
|
|
8622
8729
|
let remoteDeleteAttempted = false;
|
|
8623
8730
|
let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
|
|
8624
|
-
await
|
|
8731
|
+
await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GC_GH_TIMEOUT_MS }).catch((e) => {
|
|
8625
8732
|
const message = String(e.message || "");
|
|
8626
8733
|
if (/already been merged/i.test(message)) {
|
|
8627
8734
|
remoteNotAttemptedReason = "pr-already-merged";
|
|
@@ -8644,7 +8751,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
8644
8751
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
8645
8752
|
beforeWorktrees,
|
|
8646
8753
|
startingPath,
|
|
8647
|
-
execGit: async (args) => (await
|
|
8754
|
+
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
8648
8755
|
});
|
|
8649
8756
|
console.log(JSON.stringify({
|
|
8650
8757
|
merged: number,
|
|
@@ -8769,7 +8876,7 @@ function stageKeepAlive() {
|
|
|
8769
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) => {
|
|
8770
8877
|
const path2 = (0, import_node_path5.join)(process.cwd(), "infra", "port-ranges.json");
|
|
8771
8878
|
const allocate = async (seed) => {
|
|
8772
|
-
const { stdout } = await
|
|
8879
|
+
const { stdout } = await execFileP4("node", [(0, import_node_path5.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
8773
8880
|
const parsed = JSON.parse(stdout);
|
|
8774
8881
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
8775
8882
|
return parsed.range;
|
|
@@ -8875,8 +8982,8 @@ for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
|
8875
8982
|
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
8876
8983
|
try {
|
|
8877
8984
|
const result = await runTrainApply(commandName, {
|
|
8878
|
-
run: async (file, args) => (await
|
|
8879
|
-
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,
|
|
8880
8987
|
trainAuthority: async (repo) => {
|
|
8881
8988
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
8882
8989
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
@@ -8907,7 +9014,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
8907
9014
|
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
8908
9015
|
const meta = await fetchProjectBySlug(slug, { baseUrl: cfg.sagaApiUrl, token: githubToken });
|
|
8909
9016
|
const report = await verifyBootstrap(repo, o.class, {
|
|
8910
|
-
|
|
9017
|
+
client: defaultGitHubClient(),
|
|
8911
9018
|
projectMeta: meta,
|
|
8912
9019
|
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs5.existsSync)(path2) ? (0, import_node_fs5.readFileSync)(path2, "utf8") : null,
|
|
8913
9020
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
@@ -8920,7 +9027,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
8920
9027
|
})(),
|
|
8921
9028
|
listEnabledGcpApis: async (gcpProject) => {
|
|
8922
9029
|
try {
|
|
8923
|
-
const { stdout } = await
|
|
9030
|
+
const { stdout } = await execFileP4(
|
|
8924
9031
|
"gcloud",
|
|
8925
9032
|
["services", "list", "--enabled", "--project", gcpProject, "--format", "value(config.name)"],
|
|
8926
9033
|
{ timeout: 3e4 }
|
|
@@ -8954,7 +9061,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
8954
9061
|
const manifest = loadBootstrapSeeds((0, import_node_fs5.readFileSync)(manifestPath, "utf8"));
|
|
8955
9062
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
8956
9063
|
const slug = parsedRepo.slug;
|
|
8957
|
-
const gh = async (args) =>
|
|
9064
|
+
const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
|
|
8958
9065
|
const readFile2 = (p) => (0, import_node_fs5.existsSync)(p) ? (0, import_node_fs5.readFileSync)(p, "utf8") : null;
|
|
8959
9066
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
8960
9067
|
const vars = {};
|
|
@@ -9071,7 +9178,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
9071
9178
|
});
|
|
9072
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 () => {
|
|
9073
9180
|
const o = { json: rawFlag("--json"), repo: rawValue("--repo", ""), class: rawValue("--class", "deployable") };
|
|
9074
|
-
const deps = {
|
|
9181
|
+
const deps = { client: defaultGitHubClient() };
|
|
9075
9182
|
let targets;
|
|
9076
9183
|
const cfg = await loadConfig();
|
|
9077
9184
|
const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -9219,17 +9326,17 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9219
9326
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
9220
9327
|
const [login, pathProbe, releasedVersion, cfg, callerArn, cloneProbe] = await Promise.all([
|
|
9221
9328
|
githubLogin(),
|
|
9222
|
-
|
|
9329
|
+
execFileP4(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
|
|
9223
9330
|
fetchReleasedVersion(),
|
|
9224
9331
|
loadConfig(),
|
|
9225
9332
|
awsCallerArn(),
|
|
9226
|
-
|
|
9333
|
+
execFileP4("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false)
|
|
9227
9334
|
// unset → repair below
|
|
9228
9335
|
]);
|
|
9229
9336
|
let ghInstalled = true;
|
|
9230
9337
|
if (!login) {
|
|
9231
9338
|
try {
|
|
9232
|
-
await
|
|
9339
|
+
await execFileP4("gh", ["--version"]);
|
|
9233
9340
|
} catch {
|
|
9234
9341
|
ghInstalled = false;
|
|
9235
9342
|
}
|
|
@@ -9256,7 +9363,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9256
9363
|
let cloneOk = cloneProbe;
|
|
9257
9364
|
if (!cloneOk && !opts.banner && !opts.json) {
|
|
9258
9365
|
try {
|
|
9259
|
-
await
|
|
9366
|
+
await execFileP4("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
9260
9367
|
cloneOk = true;
|
|
9261
9368
|
io.err(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
|
|
9262
9369
|
} catch {
|