@mutmutco/cli 2.10.1 → 2.12.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 +834 -453
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3392,9 +3392,9 @@ function useColor() {
|
|
|
3392
3392
|
var program = new Command();
|
|
3393
3393
|
|
|
3394
3394
|
// src/index.ts
|
|
3395
|
-
var
|
|
3395
|
+
var import_promises2 = require("node:fs/promises");
|
|
3396
3396
|
var import_node_fs5 = require("node:fs");
|
|
3397
|
-
var
|
|
3397
|
+
var import_node_crypto2 = require("node:crypto");
|
|
3398
3398
|
|
|
3399
3399
|
// src/rules-sync.ts
|
|
3400
3400
|
function normalizeEol(s) {
|
|
@@ -3468,6 +3468,25 @@ async function runSessionStart(parallel, sequential, io) {
|
|
|
3468
3468
|
for (const lines of buffered) flush(lines, io);
|
|
3469
3469
|
for (const step of sequential) flush(await runBufferedStep(step), io);
|
|
3470
3470
|
}
|
|
3471
|
+
function buildSessionStartPlan(verbs) {
|
|
3472
|
+
return {
|
|
3473
|
+
parallel: [
|
|
3474
|
+
{ name: "rules sync", run: verbs.rulesSync },
|
|
3475
|
+
{ name: "saga show", run: verbs.sagaShow },
|
|
3476
|
+
{ name: "saga health", run: verbs.sagaHealth }
|
|
3477
|
+
],
|
|
3478
|
+
sequential: [{ name: "doctor", run: verbs.doctor }]
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
function spawnDetachedSelf(args, deps) {
|
|
3482
|
+
try {
|
|
3483
|
+
deps.spawn(deps.execPath, [deps.scriptPath, ...args], { detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
3484
|
+
} catch {
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
function northstarPointer() {
|
|
3488
|
+
return "North Stars: run `mmi-cli northstar relevant` to load plans relevant to your task (`northstar list` for all).";
|
|
3489
|
+
}
|
|
3471
3490
|
|
|
3472
3491
|
// src/saga-capture.ts
|
|
3473
3492
|
function parseHookInput(stdin) {
|
|
@@ -3480,10 +3499,10 @@ function parseHookInput(stdin) {
|
|
|
3480
3499
|
}
|
|
3481
3500
|
|
|
3482
3501
|
// src/index.ts
|
|
3483
|
-
var
|
|
3484
|
-
var
|
|
3485
|
-
var
|
|
3486
|
-
var
|
|
3502
|
+
var import_node_child_process6 = require("node:child_process");
|
|
3503
|
+
var import_node_util6 = require("node:util");
|
|
3504
|
+
var import_node_path6 = require("node:path");
|
|
3505
|
+
var import_node_os2 = require("node:os");
|
|
3487
3506
|
|
|
3488
3507
|
// src/saga-head-maintainer.ts
|
|
3489
3508
|
var import_node_child_process2 = require("node:child_process");
|
|
@@ -3495,7 +3514,7 @@ var HEAD_PROMPT_ACTION_LIMIT = 50;
|
|
|
3495
3514
|
var HEAD_PROMPT_DECISION_LIMIT = 80;
|
|
3496
3515
|
function resolveEngine(platform, custom) {
|
|
3497
3516
|
if (custom) return { cmd: custom, args: [], shell: true };
|
|
3498
|
-
return { cmd: "claude", args: ["-p"], shell: platform === "win32" };
|
|
3517
|
+
return { cmd: "claude", args: ["-p", "--no-session-persistence"], shell: platform === "win32" };
|
|
3499
3518
|
}
|
|
3500
3519
|
function headTsPath(key) {
|
|
3501
3520
|
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -3608,6 +3627,12 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
|
3608
3627
|
});
|
|
3609
3628
|
}
|
|
3610
3629
|
|
|
3630
|
+
// src/gh-create.ts
|
|
3631
|
+
var import_promises = require("node:fs/promises");
|
|
3632
|
+
var import_node_os = require("node:os");
|
|
3633
|
+
var import_node_path3 = require("node:path");
|
|
3634
|
+
var import_node_crypto = require("node:crypto");
|
|
3635
|
+
|
|
3611
3636
|
// src/board-priority.ts
|
|
3612
3637
|
var BOARD_PRIORITY_NAMES = ["Urgent", "High", "Medium", "Low"];
|
|
3613
3638
|
var CLI_PRIORITIES = ["urgent", "high", "medium", "low"];
|
|
@@ -3646,6 +3671,11 @@ function recoverPriorityFromEvents(events) {
|
|
|
3646
3671
|
|
|
3647
3672
|
// src/gh-create.ts
|
|
3648
3673
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
3674
|
+
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
3675
|
+
function timeoutKillNote(err, timeoutMs) {
|
|
3676
|
+
if (typeof err !== "object" || err === null || !err.killed) return void 0;
|
|
3677
|
+
return `killed at the ${timeoutMs}ms timeout \u2014 the write may have completed server-side; verify before retrying`;
|
|
3678
|
+
}
|
|
3649
3679
|
function normalizePriority(priority) {
|
|
3650
3680
|
const p = priority.trim().toLowerCase();
|
|
3651
3681
|
if (!CLI_PRIORITIES.includes(p)) {
|
|
@@ -3673,6 +3703,24 @@ function buildIssueArgs({ type, title, body, priority, repo, labels }) {
|
|
|
3673
3703
|
for (const label of labels ?? []) args.push("--label", label);
|
|
3674
3704
|
return args;
|
|
3675
3705
|
}
|
|
3706
|
+
async function bodyArgsViaFile(args, deps = {}) {
|
|
3707
|
+
const i = args.indexOf("--body");
|
|
3708
|
+
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
3709
|
+
} };
|
|
3710
|
+
const write = deps.write ?? import_promises.writeFile;
|
|
3711
|
+
const remove = deps.remove ?? import_promises.unlink;
|
|
3712
|
+
const file = (0, import_node_path3.join)(deps.dir ?? (0, import_node_os.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto.randomBytes)(4).toString("hex")}.md`);
|
|
3713
|
+
await write(file, args[i + 1], "utf8");
|
|
3714
|
+
return {
|
|
3715
|
+
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
3716
|
+
cleanup: async () => {
|
|
3717
|
+
try {
|
|
3718
|
+
await remove(file);
|
|
3719
|
+
} catch {
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3676
3724
|
function buildAddToProjectArgs(projectId, contentId) {
|
|
3677
3725
|
if (!projectId) throw new Error("addToProject: projectId is required");
|
|
3678
3726
|
if (!contentId) throw new Error("addToProject: contentId is required");
|
|
@@ -3811,6 +3859,32 @@ function resumeCue() {
|
|
|
3811
3859
|
return '> STATUS/RESUME CUE \u2014 For any status, resume, or "where do I stand" report: read THIS saga HEAD first (`mmi-cli saga show`), then reconcile its NEXT / LAST 5 / DECISIONS against the live board + git/gh before reporting. Do not rebuild the picture from board/issues/memory while skipping the HEAD. PRECEDENCE: the HEAD is prior-session belief and MAY BE SUPERSEDED \u2014 the current live user/master instruction WINS over any conflicting HEAD anchor, NEXT, or checklist; follow the live instruction and treat the stale HEAD item as superseded.';
|
|
3812
3860
|
}
|
|
3813
3861
|
|
|
3862
|
+
// src/fetch-retry.ts
|
|
3863
|
+
async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
3864
|
+
const attempts = opts.attempts ?? 3;
|
|
3865
|
+
const baseDelayMs = opts.baseDelayMs ?? 250;
|
|
3866
|
+
const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
|
|
3867
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
3868
|
+
let lastErr;
|
|
3869
|
+
for (let i = 0; i < attempts; i++) {
|
|
3870
|
+
const isLast = i === attempts - 1;
|
|
3871
|
+
const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
|
|
3872
|
+
try {
|
|
3873
|
+
const res = await fetchImpl(url, attemptInit);
|
|
3874
|
+
if (!isLast && retryOn(res)) {
|
|
3875
|
+
await sleep(baseDelayMs * 2 ** i);
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
return res;
|
|
3879
|
+
} catch (e) {
|
|
3880
|
+
lastErr = e;
|
|
3881
|
+
if (isLast) throw e;
|
|
3882
|
+
await sleep(baseDelayMs * 2 ** i);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
throw lastErr;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3814
3888
|
// src/saga-note.ts
|
|
3815
3889
|
var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
|
|
3816
3890
|
var ROUTE_LEVEL_403 = "saga API route-level 403 from GitHubAuthorizer cache/policy";
|
|
@@ -4025,32 +4099,153 @@ ${buildReportBody(body, sourceRepo)}`;
|
|
|
4025
4099
|
}
|
|
4026
4100
|
|
|
4027
4101
|
// src/board.ts
|
|
4102
|
+
var import_node_child_process4 = require("node:child_process");
|
|
4103
|
+
var import_node_util4 = require("node:util");
|
|
4104
|
+
|
|
4105
|
+
// src/github-client.ts
|
|
4028
4106
|
var import_node_child_process3 = require("node:child_process");
|
|
4029
4107
|
var import_node_util3 = require("node:util");
|
|
4108
|
+
var rawExecFileP = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
4109
|
+
var execFileP = (file, args, options = {}) => rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: 1e4, killSignal: "SIGTERM", ...options });
|
|
4110
|
+
var cachedGhCliToken;
|
|
4111
|
+
async function githubToken() {
|
|
4112
|
+
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
4113
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
4114
|
+
cachedGhCliToken ??= execFileP("gh", ["auth", "token"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
4115
|
+
return cachedGhCliToken;
|
|
4116
|
+
}
|
|
4117
|
+
var GitHubApiError = class extends Error {
|
|
4118
|
+
status;
|
|
4119
|
+
stderr;
|
|
4120
|
+
graphqlErrors;
|
|
4121
|
+
constructor(message, opts = {}) {
|
|
4122
|
+
super(message);
|
|
4123
|
+
this.name = "GitHubApiError";
|
|
4124
|
+
this.status = opts.status ?? 0;
|
|
4125
|
+
this.stderr = message;
|
|
4126
|
+
this.graphqlErrors = opts.graphqlErrors;
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4129
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
4130
|
+
function joinUrl(base, path2) {
|
|
4131
|
+
if (path2.startsWith("http://") || path2.startsWith("https://")) return path2;
|
|
4132
|
+
return `${base.replace(/\/+$/, "")}/${path2.replace(/^\/+/, "")}`;
|
|
4133
|
+
}
|
|
4134
|
+
function withPerPage(url) {
|
|
4135
|
+
if (/[?&]per_page=/.test(url)) return url;
|
|
4136
|
+
return url.includes("?") ? `${url}&per_page=100` : `${url}?per_page=100`;
|
|
4137
|
+
}
|
|
4138
|
+
function nextLink(linkHeader) {
|
|
4139
|
+
if (!linkHeader) return void 0;
|
|
4140
|
+
for (const part of linkHeader.split(",")) {
|
|
4141
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
|
|
4142
|
+
if (match) return match[1];
|
|
4143
|
+
}
|
|
4144
|
+
return void 0;
|
|
4145
|
+
}
|
|
4146
|
+
async function errorFromResponse(res) {
|
|
4147
|
+
let detail = "";
|
|
4148
|
+
try {
|
|
4149
|
+
const text = await res.text();
|
|
4150
|
+
try {
|
|
4151
|
+
const parsed = JSON.parse(text);
|
|
4152
|
+
detail = parsed.message ?? text;
|
|
4153
|
+
} catch {
|
|
4154
|
+
detail = text;
|
|
4155
|
+
}
|
|
4156
|
+
} catch {
|
|
4157
|
+
detail = "";
|
|
4158
|
+
}
|
|
4159
|
+
const suffix = detail ? `: ${detail.trim()}` : "";
|
|
4160
|
+
return new GitHubApiError(`HTTP ${res.status}${suffix} (${res.url})`, { status: res.status });
|
|
4161
|
+
}
|
|
4162
|
+
function createGitHubClient(options = {}) {
|
|
4163
|
+
const baseUrl = options.baseUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
4164
|
+
const token = options.token ?? githubToken;
|
|
4165
|
+
const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
4166
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
4167
|
+
async function request(method, url, init = {}) {
|
|
4168
|
+
const t = await token();
|
|
4169
|
+
const headers = {
|
|
4170
|
+
Accept: "application/vnd.github+json",
|
|
4171
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
4172
|
+
"User-Agent": "mmi-cli",
|
|
4173
|
+
...t ? { Authorization: `Bearer ${t}` } : {},
|
|
4174
|
+
...init.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
4175
|
+
...init.headers
|
|
4176
|
+
};
|
|
4177
|
+
const res = await fetchImpl(url, {
|
|
4178
|
+
method,
|
|
4179
|
+
headers,
|
|
4180
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
4181
|
+
signal: AbortSignal.timeout(init.timeoutMs ?? defaultTimeoutMs)
|
|
4182
|
+
});
|
|
4183
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
4184
|
+
return res;
|
|
4185
|
+
}
|
|
4186
|
+
async function parseJson(res) {
|
|
4187
|
+
if (res.status === 204) return void 0;
|
|
4188
|
+
const text = await res.text();
|
|
4189
|
+
if (!text) return void 0;
|
|
4190
|
+
return JSON.parse(text);
|
|
4191
|
+
}
|
|
4192
|
+
return {
|
|
4193
|
+
async rest(method, path2, init) {
|
|
4194
|
+
const res = await request(method, joinUrl(baseUrl, path2), init);
|
|
4195
|
+
return parseJson(res);
|
|
4196
|
+
},
|
|
4197
|
+
async restPaginate(path2, init) {
|
|
4198
|
+
const items = [];
|
|
4199
|
+
let url = withPerPage(joinUrl(baseUrl, path2));
|
|
4200
|
+
while (url) {
|
|
4201
|
+
const res = await request("GET", url, init);
|
|
4202
|
+
const page = await parseJson(res);
|
|
4203
|
+
if (Array.isArray(page)) items.push(...page);
|
|
4204
|
+
url = nextLink(res.headers.get("link"));
|
|
4205
|
+
}
|
|
4206
|
+
return items;
|
|
4207
|
+
},
|
|
4208
|
+
async graphql(query, variables, init) {
|
|
4209
|
+
const res = await request("POST", joinUrl(baseUrl, "graphql"), {
|
|
4210
|
+
...init,
|
|
4211
|
+
body: { query, ...variables ? { variables } : {} }
|
|
4212
|
+
});
|
|
4213
|
+
const parsed = await parseJson(res);
|
|
4214
|
+
if (parsed?.errors?.length) {
|
|
4215
|
+
const message = parsed.errors.map((e) => e.message ?? e.type ?? "unknown GraphQL error").join("; ");
|
|
4216
|
+
throw new GitHubApiError(`GraphQL: ${message}`, { status: 200, graphqlErrors: parsed.errors });
|
|
4217
|
+
}
|
|
4218
|
+
if (!parsed || parsed.data === void 0 || parsed.data === null) {
|
|
4219
|
+
throw new GitHubApiError("GraphQL response did not include data", { status: 200 });
|
|
4220
|
+
}
|
|
4221
|
+
return parsed.data;
|
|
4222
|
+
}
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
var cachedDefaultClient;
|
|
4226
|
+
function defaultGitHubClient() {
|
|
4227
|
+
cachedDefaultClient ??= createGitHubClient();
|
|
4228
|
+
return cachedDefaultClient;
|
|
4229
|
+
}
|
|
4030
4230
|
|
|
4031
4231
|
// ../infra/board-vocab.mjs
|
|
4032
4232
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
4033
4233
|
|
|
4034
4234
|
// src/board.ts
|
|
4035
|
-
var
|
|
4036
|
-
var BOARD_GH_TIMEOUT_MS = 2e4;
|
|
4235
|
+
var rawExecFileP2 = (0, import_node_util4.promisify)(import_node_child_process4.execFile);
|
|
4037
4236
|
var BOARD_GIT_TIMEOUT_MS = 1e4;
|
|
4038
4237
|
var WRITE_PROBE_CONCURRENCY = 8;
|
|
4039
|
-
var
|
|
4238
|
+
var execFileP2 = (file, args, options = {}) => (
|
|
4040
4239
|
// encoding 'utf8' guarantees string output at runtime; the cast pins the type (promisify's
|
|
4041
4240
|
// overloads widen to string|Buffer when options is spread in).
|
|
4042
|
-
|
|
4241
|
+
rawExecFileP2(file, args, { encoding: "utf8", windowsHide: true, timeout: BOARD_GIT_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
|
|
4043
4242
|
);
|
|
4044
4243
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["Todo", "In Progress", "In Review"]);
|
|
4045
4244
|
var STATUS_ORDER = new Map(BOARD_STATUSES.map((s, i) => [s, i]));
|
|
4046
4245
|
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
4246
|
var defaultGit = async (args) => {
|
|
4052
4247
|
try {
|
|
4053
|
-
const { stdout } = await
|
|
4248
|
+
const { stdout } = await execFileP2("git", args, { timeout: BOARD_GIT_TIMEOUT_MS });
|
|
4054
4249
|
return stdout.trim();
|
|
4055
4250
|
} catch {
|
|
4056
4251
|
return "";
|
|
@@ -4104,36 +4299,52 @@ query($owner: String!, $number: Int!, $after: String) {
|
|
|
4104
4299
|
}
|
|
4105
4300
|
}
|
|
4106
4301
|
}`;
|
|
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 } }
|
|
4302
|
+
var SINGLE_ITEM_CONTENT_FRAGMENT = `
|
|
4303
|
+
id
|
|
4304
|
+
number
|
|
4305
|
+
title
|
|
4306
|
+
url
|
|
4307
|
+
state
|
|
4308
|
+
repository { nameWithOwner }
|
|
4309
|
+
labels(first: 10) { nodes { name } }
|
|
4310
|
+
assignees(first: 10) { nodes { login } }
|
|
4311
|
+
projectItems(first: 20) {
|
|
4312
|
+
nodes {
|
|
4313
|
+
id
|
|
4314
|
+
project { id title }
|
|
4315
|
+
fieldValues(first: 8) {
|
|
4316
|
+
nodes {
|
|
4317
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
4318
|
+
name
|
|
4319
|
+
optionId
|
|
4320
|
+
field { ... on ProjectV2SingleSelectField { id name } }
|
|
4321
|
+
}
|
|
4129
4322
|
}
|
|
4130
4323
|
}
|
|
4131
4324
|
}
|
|
4132
|
-
}
|
|
4325
|
+
}`;
|
|
4326
|
+
var ISSUE_PROJECT_ITEM_QUERY = `
|
|
4327
|
+
query($repoOwner: String!, $repoName: String!, $number: Int!) {
|
|
4328
|
+
viewer { login }
|
|
4329
|
+
repository(owner: $repoOwner, name: $repoName) {
|
|
4330
|
+
issueOrPullRequest(number: $number) {
|
|
4331
|
+
__typename
|
|
4332
|
+
... on Issue {${SINGLE_ITEM_CONTENT_FRAGMENT}
|
|
4333
|
+
}
|
|
4334
|
+
... on PullRequest {${SINGLE_ITEM_CONTENT_FRAGMENT}
|
|
4133
4335
|
}
|
|
4134
4336
|
}
|
|
4135
4337
|
}
|
|
4136
4338
|
}`;
|
|
4339
|
+
var UPDATE_ITEM_FIELD_MUTATION = `
|
|
4340
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
4341
|
+
updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) {
|
|
4342
|
+
projectV2Item { id }
|
|
4343
|
+
}
|
|
4344
|
+
}`;
|
|
4345
|
+
async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
|
|
4346
|
+
await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
|
|
4347
|
+
}
|
|
4137
4348
|
function resolveBoardConfig(cfg) {
|
|
4138
4349
|
const missing = [];
|
|
4139
4350
|
if (!cfg.projectOwner) missing.push("projectOwner");
|
|
@@ -4256,7 +4467,7 @@ function renderBoardReport(report) {
|
|
|
4256
4467
|
return lines.join("\n");
|
|
4257
4468
|
}
|
|
4258
4469
|
async function collectBoardItems(cfg, options, deps) {
|
|
4259
|
-
const
|
|
4470
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4260
4471
|
const git = deps.git ?? defaultGit;
|
|
4261
4472
|
const currentRepo = options.repo ?? repoFromGitRemote(await git(["remote", "get-url", "origin"])) ?? "";
|
|
4262
4473
|
if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
|
|
@@ -4269,7 +4480,7 @@ async function collectBoardItems(cfg, options, deps) {
|
|
|
4269
4480
|
let partial = false;
|
|
4270
4481
|
do {
|
|
4271
4482
|
try {
|
|
4272
|
-
const page = await fetchProjectPage(
|
|
4483
|
+
const page = await fetchProjectPage(client, cfg, after);
|
|
4273
4484
|
viewer ||= page.viewer.login;
|
|
4274
4485
|
const project2 = page.organization?.projectV2;
|
|
4275
4486
|
if (!project2) throw new Error(`project ${cfg.projectOwner}#${cfg.projectNumber} not found`);
|
|
@@ -4287,19 +4498,15 @@ async function collectBoardItems(cfg, options, deps) {
|
|
|
4287
4498
|
} while (after);
|
|
4288
4499
|
return { items: nodesToItems(nodes, warnings, cfg), viewer, repo: currentRepo, projectId, projectTitle, warnings, partial };
|
|
4289
4500
|
}
|
|
4290
|
-
async function repoCanPush(repo,
|
|
4501
|
+
async function repoCanPush(repo, client) {
|
|
4291
4502
|
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;
|
|
4503
|
+
const parsed = await client.rest("GET", `repos/${repo}`);
|
|
4504
|
+
return typeof parsed?.permissions?.push === "boolean" ? parsed.permissions.push : void 0;
|
|
4298
4505
|
} catch {
|
|
4299
4506
|
return void 0;
|
|
4300
4507
|
}
|
|
4301
4508
|
}
|
|
4302
|
-
async function resolveWritableReposForClaimables(items,
|
|
4509
|
+
async function resolveWritableReposForClaimables(items, client, allowPartial) {
|
|
4303
4510
|
const candidateRepos = [...new Set(items.filter((item) => item.status === "Todo" && item.assignees.length === 0).map((item) => item.repository))];
|
|
4304
4511
|
const repos = /* @__PURE__ */ new Set();
|
|
4305
4512
|
const warnings = [];
|
|
@@ -4308,7 +4515,7 @@ async function resolveWritableReposForClaimables(items, gh, allowPartial) {
|
|
|
4308
4515
|
const worker = async () => {
|
|
4309
4516
|
while (next < candidateRepos.length) {
|
|
4310
4517
|
const repo = candidateRepos[next++];
|
|
4311
|
-
const canPush = await repoCanPush(repo,
|
|
4518
|
+
const canPush = await repoCanPush(repo, client);
|
|
4312
4519
|
if (canPush === true) {
|
|
4313
4520
|
repos.add(repo.toLowerCase());
|
|
4314
4521
|
continue;
|
|
@@ -4326,9 +4533,9 @@ async function resolveWritableReposForClaimables(items, gh, allowPartial) {
|
|
|
4326
4533
|
}
|
|
4327
4534
|
async function readBoard(options, deps = {}) {
|
|
4328
4535
|
const cfg = resolveBoardConfig(options.config);
|
|
4329
|
-
const
|
|
4536
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4330
4537
|
const collected = await collectBoardItems(cfg, options, deps);
|
|
4331
|
-
const writable = await resolveWritableReposForClaimables(collected.items,
|
|
4538
|
+
const writable = await resolveWritableReposForClaimables(collected.items, client, options.allowPartial ?? false);
|
|
4332
4539
|
collected.warnings.push(...writable.warnings);
|
|
4333
4540
|
collected.partial = collected.partial || writable.partial;
|
|
4334
4541
|
const groups = partitionBoardItems(collected.items, collected.viewer, collected.repo, writable.repos);
|
|
@@ -4341,7 +4548,7 @@ async function readBoard(options, deps = {}) {
|
|
|
4341
4548
|
partial: collected.partial
|
|
4342
4549
|
};
|
|
4343
4550
|
if (options.includeBundleDetails) {
|
|
4344
|
-
await attachBundleDetails(report,
|
|
4551
|
+
await attachBundleDetails(report, client, options.allowPartial ?? false);
|
|
4345
4552
|
}
|
|
4346
4553
|
return report;
|
|
4347
4554
|
}
|
|
@@ -4352,67 +4559,50 @@ function findBoardItem(items, selector) {
|
|
|
4352
4559
|
if (!found) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
4353
4560
|
return found;
|
|
4354
4561
|
}
|
|
4562
|
+
async function resolveCurrentRepo(options, deps) {
|
|
4563
|
+
const git = deps.git ?? defaultGit;
|
|
4564
|
+
const currentRepo = options.repo ?? repoFromGitRemote(await git(["remote", "get-url", "origin"])) ?? "";
|
|
4565
|
+
if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
|
|
4566
|
+
return currentRepo;
|
|
4567
|
+
}
|
|
4355
4568
|
async function moveBoardItem(options, deps = {}) {
|
|
4356
4569
|
if (!BOARD_STATUSES.includes(options.status)) {
|
|
4357
4570
|
throw new Error(`unknown status '${options.status}'; expected one of ${BOARD_STATUSES.join(", ")}`);
|
|
4358
4571
|
}
|
|
4359
4572
|
const cfg = resolveBoardConfig(options.config);
|
|
4360
|
-
const
|
|
4361
|
-
const
|
|
4362
|
-
const selector = parseIssueSelector(options.selector,
|
|
4363
|
-
const
|
|
4573
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4574
|
+
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
4575
|
+
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
4576
|
+
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
4577
|
+
const item = lookup.item;
|
|
4578
|
+
if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
4364
4579
|
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
4365
4580
|
const optionId = cfg.statusOptions[options.status];
|
|
4366
4581
|
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
|
-
]);
|
|
4582
|
+
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
|
|
4379
4583
|
} catch (e) {
|
|
4380
4584
|
const warning = `partial move: ${item.ref} status was not changed to ${options.status} (${ghError(e)})`;
|
|
4381
4585
|
if (!options.allowPartial) throw new Error(warning);
|
|
4382
|
-
return { item, viewer:
|
|
4586
|
+
return { item, viewer: lookup.viewer, repo: currentRepo, status: item.status, partial: true, warning };
|
|
4383
4587
|
}
|
|
4384
4588
|
return {
|
|
4385
4589
|
item: { ...item, status: options.status, statusOptionId: optionId },
|
|
4386
|
-
viewer:
|
|
4387
|
-
repo:
|
|
4590
|
+
viewer: lookup.viewer,
|
|
4591
|
+
repo: currentRepo,
|
|
4388
4592
|
status: options.status,
|
|
4389
4593
|
partial: false
|
|
4390
4594
|
};
|
|
4391
4595
|
}
|
|
4392
4596
|
async function showBoardItem(options, deps = {}) {
|
|
4393
4597
|
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
|
-
}
|
|
4598
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4599
|
+
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
4600
|
+
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
4601
|
+
const { item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
4602
|
+
if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
4405
4603
|
if (item.contentType === "Issue") {
|
|
4406
4604
|
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
|
-
};
|
|
4605
|
+
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
4416
4606
|
} catch (e) {
|
|
4417
4607
|
if (!options.allowPartial) throw new Error(`detail read failed: ${item.ref}: ${ghError(e)}`);
|
|
4418
4608
|
}
|
|
@@ -4421,17 +4611,17 @@ async function showBoardItem(options, deps = {}) {
|
|
|
4421
4611
|
}
|
|
4422
4612
|
async function claimBoardIssue(options, deps = {}) {
|
|
4423
4613
|
const cfg = resolveBoardConfig(options.config);
|
|
4424
|
-
const
|
|
4614
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4425
4615
|
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial }, deps);
|
|
4426
4616
|
const selector = parseIssueSelector(options.selector, collected.repo);
|
|
4427
4617
|
try {
|
|
4428
4618
|
findBoardItem(collected.items, selector);
|
|
4429
4619
|
} catch (e) {
|
|
4430
|
-
const fallback = await fetchIssueProjectItem(
|
|
4620
|
+
const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
|
|
4431
4621
|
if (!fallback) throw e;
|
|
4432
4622
|
collected.items.push(fallback);
|
|
4433
4623
|
}
|
|
4434
|
-
const writable = await resolveWritableReposForClaimables(collected.items,
|
|
4624
|
+
const writable = await resolveWritableReposForClaimables(collected.items, client, options.allowPartial ?? false);
|
|
4435
4625
|
collected.warnings.push(...writable.warnings);
|
|
4436
4626
|
collected.partial = collected.partial || writable.partial;
|
|
4437
4627
|
const report = {
|
|
@@ -4448,7 +4638,7 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4448
4638
|
}
|
|
4449
4639
|
let item = findClaimableItem(report, selector);
|
|
4450
4640
|
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
4451
|
-
const fresh = await fetchIssueProjectItem(
|
|
4641
|
+
const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
|
|
4452
4642
|
if (!fresh) throw new Error(`${item.ref} is not on this project board`);
|
|
4453
4643
|
if (fresh.status !== "Todo") throw new Error(`${item.ref} is not claimable: Status is ${fresh.status}`);
|
|
4454
4644
|
if (fresh.assignees.length) throw new Error(`${item.ref} is already assigned to @${fresh.assignees.join(", @")}`);
|
|
@@ -4456,23 +4646,12 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4456
4646
|
const assignee = options.assignee ?? "@me";
|
|
4457
4647
|
const assignedLogin = assignee === "@me" ? report.viewer : assignee.replace(/^@/, "");
|
|
4458
4648
|
try {
|
|
4459
|
-
await
|
|
4649
|
+
await client.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
|
|
4460
4650
|
} catch (e) {
|
|
4461
4651
|
throw new Error(`claim failed before board status changed: ${ghError(e)}`);
|
|
4462
4652
|
}
|
|
4463
4653
|
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
|
-
]);
|
|
4654
|
+
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]);
|
|
4476
4655
|
} catch (e) {
|
|
4477
4656
|
const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
|
|
4478
4657
|
if (!options.allowPartial) throw new Error(warning);
|
|
@@ -4491,22 +4670,11 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4491
4670
|
partial: false
|
|
4492
4671
|
};
|
|
4493
4672
|
}
|
|
4494
|
-
async function setBoardItemPriority(
|
|
4673
|
+
async function setBoardItemPriority(client, cfg, itemId, priority) {
|
|
4495
4674
|
if (!isPriorityFieldConfigured(cfg)) return void 0;
|
|
4496
4675
|
const optionId = resolvePriorityOptionId(cfg, priority);
|
|
4497
4676
|
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
|
-
]);
|
|
4677
|
+
await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
|
|
4510
4678
|
return cliPriorityToFieldName(priority);
|
|
4511
4679
|
}
|
|
4512
4680
|
async function backfillBoardPriorities(options, deps = {}) {
|
|
@@ -4514,7 +4682,7 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4514
4682
|
if (!isPriorityFieldConfigured(cfg)) {
|
|
4515
4683
|
throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
|
|
4516
4684
|
}
|
|
4517
|
-
const
|
|
4685
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
4518
4686
|
const collected = await collectBoardItems(cfg, { repo: options.repo }, deps);
|
|
4519
4687
|
const issues = collected.items.filter((item) => item.contentType === "Issue");
|
|
4520
4688
|
const concurrency = Math.max(1, options.concurrency ?? 8);
|
|
@@ -4525,7 +4693,7 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4525
4693
|
return;
|
|
4526
4694
|
}
|
|
4527
4695
|
try {
|
|
4528
|
-
const priority = await recoverIssuePriority(
|
|
4696
|
+
const priority = await recoverIssuePriority(client, item);
|
|
4529
4697
|
if (!priority) {
|
|
4530
4698
|
result.skipped += 1;
|
|
4531
4699
|
return;
|
|
@@ -4537,18 +4705,7 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4537
4705
|
}
|
|
4538
4706
|
const optionId = cfg.priorityOptions?.[priority];
|
|
4539
4707
|
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
|
-
]);
|
|
4708
|
+
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.priorityFieldId, optionId);
|
|
4552
4709
|
result.set += 1;
|
|
4553
4710
|
result.details.push(`${item.ref} \u2192 ${priority}`);
|
|
4554
4711
|
} catch (e) {
|
|
@@ -4561,88 +4718,65 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
4561
4718
|
}
|
|
4562
4719
|
return result;
|
|
4563
4720
|
}
|
|
4564
|
-
async function recoverIssuePriority(
|
|
4721
|
+
async function recoverIssuePriority(client, item) {
|
|
4565
4722
|
for (const label of item.labels) {
|
|
4566
4723
|
const fromLabel = labelToFieldPriority(label);
|
|
4567
4724
|
if (fromLabel) return fromLabel;
|
|
4568
4725
|
}
|
|
4569
|
-
const
|
|
4570
|
-
|
|
4726
|
+
const events = await client.restPaginate(
|
|
4727
|
+
`repos/${item.repository}/issues/${item.number}/events`
|
|
4728
|
+
);
|
|
4729
|
+
return recoverPriorityFromEvents(events);
|
|
4571
4730
|
}
|
|
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
|
-
}
|
|
4731
|
+
async function fetchProjectPage(client, cfg, after) {
|
|
4732
|
+
return client.graphql(PROJECT_ITEMS_QUERY, {
|
|
4733
|
+
owner: cfg.projectOwner,
|
|
4734
|
+
number: cfg.projectNumber,
|
|
4735
|
+
...after ? { after } : {}
|
|
4587
4736
|
});
|
|
4588
4737
|
}
|
|
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;
|
|
4738
|
+
function isGraphQLNotFound(e) {
|
|
4739
|
+
const errors = e.graphqlErrors;
|
|
4740
|
+
return Boolean(e instanceof GitHubApiError && errors?.length && errors.every((entry) => entry.type === "NOT_FOUND"));
|
|
4605
4741
|
}
|
|
4606
|
-
async function fetchIssueProjectItem(
|
|
4742
|
+
async function fetchIssueProjectItem(client, cfg, selector) {
|
|
4607
4743
|
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
|
-
);
|
|
4744
|
+
if (!repoOwner || !repoName) return { viewer: "" };
|
|
4745
|
+
let data;
|
|
4746
|
+
try {
|
|
4747
|
+
data = await client.graphql(ISSUE_PROJECT_ITEM_QUERY, {
|
|
4748
|
+
repoOwner,
|
|
4749
|
+
repoName,
|
|
4750
|
+
number: selector.number
|
|
4751
|
+
});
|
|
4752
|
+
} catch (e) {
|
|
4753
|
+
if (isGraphQLNotFound(e)) return { viewer: "" };
|
|
4754
|
+
throw e;
|
|
4628
4755
|
}
|
|
4629
|
-
const
|
|
4630
|
-
|
|
4631
|
-
return
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4756
|
+
const viewer = data.viewer?.login ?? "";
|
|
4757
|
+
const content = data.repository?.issueOrPullRequest;
|
|
4758
|
+
if (!content) return { viewer };
|
|
4759
|
+
const projectItem = (content.projectItems?.nodes ?? []).find((item) => item.project?.id === cfg.projectId);
|
|
4760
|
+
if (!projectItem) return { viewer };
|
|
4761
|
+
const { projectItems: _projectItems, ...contentFields } = content;
|
|
4762
|
+
return {
|
|
4763
|
+
viewer,
|
|
4764
|
+
item: nodeToItem({
|
|
4765
|
+
id: projectItem.id,
|
|
4766
|
+
fieldValues: projectItem.fieldValues,
|
|
4767
|
+
content: contentFields
|
|
4768
|
+
}, cfg)
|
|
4769
|
+
};
|
|
4770
|
+
}
|
|
4771
|
+
async function fetchIssueDetails(client, repo, number) {
|
|
4772
|
+
const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
|
|
4773
|
+
const comments = await client.restPaginate(
|
|
4774
|
+
`repos/${repo}/issues/${number}/comments`
|
|
4775
|
+
);
|
|
4776
|
+
return {
|
|
4777
|
+
body: issue2?.body ?? "",
|
|
4778
|
+
comments: comments.map((comment) => ({ author: comment.user?.login ?? "", body: comment.body ?? "" }))
|
|
4779
|
+
};
|
|
4646
4780
|
}
|
|
4647
4781
|
function nodesToItems(nodes, warnings, cfg) {
|
|
4648
4782
|
const items = [];
|
|
@@ -4701,20 +4835,12 @@ function nodeToItem(node, cfg) {
|
|
|
4701
4835
|
function isSupportedContent(content) {
|
|
4702
4836
|
return Boolean(content && (content.__typename === "Issue" || content.__typename === "PullRequest"));
|
|
4703
4837
|
}
|
|
4704
|
-
async function attachBundleDetails(report,
|
|
4838
|
+
async function attachBundleDetails(report, client, allowPartial) {
|
|
4705
4839
|
const candidates = detailCandidates(report);
|
|
4706
4840
|
if (candidates.length <= 1) return;
|
|
4707
4841
|
for (const item of candidates) {
|
|
4708
4842
|
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
|
-
};
|
|
4843
|
+
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
4718
4844
|
} catch (e) {
|
|
4719
4845
|
const warning = `partial detail read: ${item.ref}: ${ghError(e)}`;
|
|
4720
4846
|
if (!allowPartial) throw new Error(warning);
|
|
@@ -5347,14 +5473,14 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
5347
5473
|
}
|
|
5348
5474
|
|
|
5349
5475
|
// src/stage-runner.ts
|
|
5350
|
-
var
|
|
5476
|
+
var import_node_child_process5 = require("node:child_process");
|
|
5351
5477
|
var import_node_fs3 = require("node:fs");
|
|
5352
|
-
var
|
|
5478
|
+
var import_node_path4 = require("node:path");
|
|
5353
5479
|
var import_node_net = require("node:net");
|
|
5354
|
-
var
|
|
5355
|
-
var
|
|
5480
|
+
var import_node_util5 = require("node:util");
|
|
5481
|
+
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
|
|
5356
5482
|
function stageStatePath(cwd = process.cwd()) {
|
|
5357
|
-
return (0,
|
|
5483
|
+
return (0, import_node_path4.join)(cwd, "tmp", "stage", "state.json");
|
|
5358
5484
|
}
|
|
5359
5485
|
function validateStageConfig(config = {}, action) {
|
|
5360
5486
|
const problems = [];
|
|
@@ -5387,7 +5513,7 @@ function isPortFree(port) {
|
|
|
5387
5513
|
});
|
|
5388
5514
|
}
|
|
5389
5515
|
async function shell(command, cwd, timeoutMs) {
|
|
5390
|
-
await
|
|
5516
|
+
await execFileP3(command, [], {
|
|
5391
5517
|
cwd,
|
|
5392
5518
|
shell: true,
|
|
5393
5519
|
timeout: timeoutMs,
|
|
@@ -5406,7 +5532,7 @@ function readState(path2) {
|
|
|
5406
5532
|
async function killTree(pid) {
|
|
5407
5533
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
5408
5534
|
if (process.platform === "win32") {
|
|
5409
|
-
await
|
|
5535
|
+
await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
|
|
5410
5536
|
return;
|
|
5411
5537
|
}
|
|
5412
5538
|
try {
|
|
@@ -5469,7 +5595,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
5469
5595
|
}
|
|
5470
5596
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
5471
5597
|
const up = sub(config.up.trim());
|
|
5472
|
-
const child = (0,
|
|
5598
|
+
const child = (0, import_node_child_process5.spawn)(up, {
|
|
5473
5599
|
cwd,
|
|
5474
5600
|
shell: true,
|
|
5475
5601
|
detached: true,
|
|
@@ -5759,22 +5885,29 @@ function safeJson(text, fallback) {
|
|
|
5759
5885
|
return fallback;
|
|
5760
5886
|
}
|
|
5761
5887
|
}
|
|
5762
|
-
async function
|
|
5888
|
+
async function restJson(deps, path2, fallback) {
|
|
5889
|
+
try {
|
|
5890
|
+
return await deps.client.rest("GET", path2) ?? fallback;
|
|
5891
|
+
} catch {
|
|
5892
|
+
return fallback;
|
|
5893
|
+
}
|
|
5894
|
+
}
|
|
5895
|
+
async function restPagedJson(deps, path2, fallback) {
|
|
5763
5896
|
try {
|
|
5764
|
-
return
|
|
5897
|
+
return await deps.client.restPaginate(path2);
|
|
5765
5898
|
} catch {
|
|
5766
5899
|
return fallback;
|
|
5767
5900
|
}
|
|
5768
5901
|
}
|
|
5769
5902
|
async function resolveOwners(deps) {
|
|
5770
|
-
const members = await
|
|
5903
|
+
const members = await restPagedJson(deps, `orgs/${OWNER}/members?role=admin`, []);
|
|
5771
5904
|
return members.map((m) => m.login);
|
|
5772
5905
|
}
|
|
5773
5906
|
function collaboratorRole(c) {
|
|
5774
5907
|
return c.role_name ?? (c.permissions?.admin ? "admin" : c.permissions?.maintain ? "maintain" : "write");
|
|
5775
5908
|
}
|
|
5776
5909
|
async function auditRepoCollaborators(repo, owners, deps) {
|
|
5777
|
-
const collabs = await
|
|
5910
|
+
const collabs = await restPagedJson(deps, `repos/${repo}/collaborators?affiliation=direct`, []);
|
|
5778
5911
|
const findings = [];
|
|
5779
5912
|
for (const c of collabs) {
|
|
5780
5913
|
if (owners.has(c.login)) continue;
|
|
@@ -5795,7 +5928,7 @@ async function auditRepoCollaborators(repo, owners, deps) {
|
|
|
5795
5928
|
async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @__PURE__ */ new Set()) {
|
|
5796
5929
|
let restrictions = null;
|
|
5797
5930
|
try {
|
|
5798
|
-
restrictions =
|
|
5931
|
+
restrictions = await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection/restrictions`) ?? null;
|
|
5799
5932
|
} catch {
|
|
5800
5933
|
restrictions = null;
|
|
5801
5934
|
}
|
|
@@ -5879,7 +6012,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
|
|
|
5879
6012
|
return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
5880
6013
|
}
|
|
5881
6014
|
async function auditOrgBasePermission(deps) {
|
|
5882
|
-
const org = await
|
|
6015
|
+
const org = await restJson(deps, `orgs/${OWNER}`, {});
|
|
5883
6016
|
const perm = org.default_repository_permission;
|
|
5884
6017
|
if (perm && perm !== "read" && perm !== "none") {
|
|
5885
6018
|
return [{
|
|
@@ -6022,9 +6155,16 @@ function safeJson2(text, fallback) {
|
|
|
6022
6155
|
return fallback;
|
|
6023
6156
|
}
|
|
6024
6157
|
}
|
|
6025
|
-
async function
|
|
6158
|
+
async function restJson2(deps, path2, fallback) {
|
|
6159
|
+
try {
|
|
6160
|
+
return await deps.client.rest("GET", path2) ?? fallback;
|
|
6161
|
+
} catch {
|
|
6162
|
+
return fallback;
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
async function restPagedJson2(deps, path2, fallback) {
|
|
6026
6166
|
try {
|
|
6027
|
-
return
|
|
6167
|
+
return await deps.client.restPaginate(path2);
|
|
6028
6168
|
} catch {
|
|
6029
6169
|
return fallback;
|
|
6030
6170
|
}
|
|
@@ -6032,7 +6172,7 @@ async function ghJson2(deps, args, fallback) {
|
|
|
6032
6172
|
async function contentExists(deps, repo, branch, path2) {
|
|
6033
6173
|
try {
|
|
6034
6174
|
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
6035
|
-
await deps.
|
|
6175
|
+
await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
6036
6176
|
return true;
|
|
6037
6177
|
} catch {
|
|
6038
6178
|
return false;
|
|
@@ -6040,7 +6180,7 @@ async function contentExists(deps, repo, branch, path2) {
|
|
|
6040
6180
|
}
|
|
6041
6181
|
async function getProtection(deps, repo, branch) {
|
|
6042
6182
|
try {
|
|
6043
|
-
return
|
|
6183
|
+
return await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection`) ?? {};
|
|
6044
6184
|
} catch {
|
|
6045
6185
|
return null;
|
|
6046
6186
|
}
|
|
@@ -6068,7 +6208,7 @@ async function rulesetDetails(deps, repo, list) {
|
|
|
6068
6208
|
details.push(ruleset);
|
|
6069
6209
|
continue;
|
|
6070
6210
|
}
|
|
6071
|
-
details.push(await
|
|
6211
|
+
details.push(await restJson2(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
|
|
6072
6212
|
}
|
|
6073
6213
|
return details;
|
|
6074
6214
|
}
|
|
@@ -6081,11 +6221,11 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6081
6221
|
const branchesWanted = expectedBranches(repoClass);
|
|
6082
6222
|
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
6083
6223
|
const checks = [];
|
|
6084
|
-
const repoInfo = await
|
|
6224
|
+
const repoInfo = await restJson2(deps, `repos/${repo}`, {});
|
|
6085
6225
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
6086
6226
|
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
6087
6227
|
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
|
|
6228
|
+
const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
|
|
6089
6229
|
const branchNames = new Set(branchList.map((b) => b.name));
|
|
6090
6230
|
for (const branch of branchesWanted) {
|
|
6091
6231
|
checks.push({ ok: branchNames.has(branch), label: `branch exists: ${branch}` });
|
|
@@ -6118,14 +6258,14 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6118
6258
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
|
|
6119
6259
|
}
|
|
6120
6260
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
|
|
6121
|
-
const labels = await
|
|
6261
|
+
const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
|
|
6122
6262
|
const labelNames = new Set(labels.map((l) => l.name));
|
|
6123
6263
|
for (const label of requiredLabels) {
|
|
6124
6264
|
checks.push({ ok: labelNames.has(label), label: `label exists: ${label}` });
|
|
6125
6265
|
}
|
|
6126
6266
|
const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
|
|
6127
6267
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
6128
|
-
const actions = await
|
|
6268
|
+
const actions = await restJson2(deps, `repos/${repo}/actions/permissions`, {});
|
|
6129
6269
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
6130
6270
|
const config = deps.projectMeta ?? null;
|
|
6131
6271
|
checks.push({
|
|
@@ -6133,12 +6273,18 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6133
6273
|
label: "registry project board META exists"
|
|
6134
6274
|
});
|
|
6135
6275
|
if (config?.projectOwner && config.projectNumber != null) {
|
|
6136
|
-
const
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6276
|
+
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 } } } } } } }`;
|
|
6277
|
+
const fields = await (async () => {
|
|
6278
|
+
try {
|
|
6279
|
+
const data = await deps.client.graphql(fieldsQuery, {
|
|
6280
|
+
login: config.projectOwner,
|
|
6281
|
+
number: config.projectNumber
|
|
6282
|
+
});
|
|
6283
|
+
return (data.organization?.projectV2?.fields?.nodes ?? []).filter((f) => Boolean(f?.id && f?.name));
|
|
6284
|
+
} catch {
|
|
6285
|
+
return [];
|
|
6286
|
+
}
|
|
6287
|
+
})();
|
|
6142
6288
|
const statusField = fields.find((field) => field.name === "Status");
|
|
6143
6289
|
const labelField = fields.find((field) => field.name === "Labels");
|
|
6144
6290
|
checks.push({
|
|
@@ -6195,12 +6341,17 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6195
6341
|
}
|
|
6196
6342
|
}
|
|
6197
6343
|
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
|
-
|
|
6344
|
+
const workflowResponse = await (async () => {
|
|
6345
|
+
try {
|
|
6346
|
+
return await deps.client.graphql(workflowQuery, {
|
|
6347
|
+
login: config.projectOwner,
|
|
6348
|
+
number: config.projectNumber
|
|
6349
|
+
});
|
|
6350
|
+
} catch {
|
|
6351
|
+
return {};
|
|
6352
|
+
}
|
|
6353
|
+
})();
|
|
6354
|
+
const workflows = workflowResponse.organization?.projectV2?.workflows?.nodes || [];
|
|
6204
6355
|
for (const workflowName of requiredProjectWorkflows) {
|
|
6205
6356
|
checks.push({
|
|
6206
6357
|
ok: workflows.some((workflow) => workflow.name === workflowName && workflow.enabled === true),
|
|
@@ -6212,11 +6363,7 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
6212
6363
|
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
6213
6364
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && json.projects.some((p) => (p.repos || []).includes(repo)));
|
|
6214
6365
|
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
|
-
);
|
|
6366
|
+
const rulesetList = await restJson2(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
6220
6367
|
const rulesets = await rulesetDetails(deps, repo, rulesetList);
|
|
6221
6368
|
const activeOrgRulesets = rulesets.filter(
|
|
6222
6369
|
(r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
|
|
@@ -6402,17 +6549,22 @@ var ORG_CONFIG_PATH = "/org/config";
|
|
|
6402
6549
|
var PROJECTS_ENVELOPE_KEY = "projects";
|
|
6403
6550
|
|
|
6404
6551
|
// src/registry-client.ts
|
|
6405
|
-
var
|
|
6552
|
+
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
6553
|
+
var RETRY_ATTEMPTS = 3;
|
|
6554
|
+
function retriedFetch(deps, url, init) {
|
|
6555
|
+
return fetchWithRetry(deps.fetch ?? fetch, url, init, {
|
|
6556
|
+
attempts: RETRY_ATTEMPTS,
|
|
6557
|
+
timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
6558
|
+
});
|
|
6559
|
+
}
|
|
6406
6560
|
async function fetchTrainAuthority(repo, deps) {
|
|
6407
6561
|
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
6408
6562
|
const token = await deps.token();
|
|
6409
6563
|
if (!token) return { ok: false, error: "no GitHub token (gh auth login)" };
|
|
6410
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6411
6564
|
try {
|
|
6412
|
-
const res = await
|
|
6565
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
|
|
6413
6566
|
method: "GET",
|
|
6414
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6415
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
6567
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6416
6568
|
});
|
|
6417
6569
|
if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
|
|
6418
6570
|
const body = await res.json();
|
|
@@ -6426,12 +6578,10 @@ async function fetchProjectsList(deps) {
|
|
|
6426
6578
|
if (!deps.baseUrl) return null;
|
|
6427
6579
|
const token = await deps.token();
|
|
6428
6580
|
if (!token) return null;
|
|
6429
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6430
6581
|
try {
|
|
6431
|
-
const res = await
|
|
6582
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${PROJECTS_LIST_PATH}`, {
|
|
6432
6583
|
method: "GET",
|
|
6433
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6434
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
6584
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6435
6585
|
});
|
|
6436
6586
|
if (!res.ok) return null;
|
|
6437
6587
|
const body = await res.json();
|
|
@@ -6450,12 +6600,10 @@ async function fetchProjectBySlugChecked(slug, deps) {
|
|
|
6450
6600
|
if (!slug) return { ok: false, error: "no slug" };
|
|
6451
6601
|
const token = await deps.token();
|
|
6452
6602
|
if (!token) return { ok: false, error: "no GitHub token (run `gh auth login`)" };
|
|
6453
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6454
6603
|
try {
|
|
6455
|
-
const res = await
|
|
6604
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
|
|
6456
6605
|
method: "GET",
|
|
6457
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6458
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
6606
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6459
6607
|
});
|
|
6460
6608
|
if (res.status === 404) return { ok: true, project: null };
|
|
6461
6609
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
@@ -6472,12 +6620,10 @@ async function fetchDeployStatusBySlug(slug, deps) {
|
|
|
6472
6620
|
if (!deps.baseUrl || !slug) return null;
|
|
6473
6621
|
const token = await deps.token();
|
|
6474
6622
|
if (!token) return null;
|
|
6475
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6476
6623
|
try {
|
|
6477
|
-
const res = await
|
|
6624
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
|
|
6478
6625
|
method: "GET",
|
|
6479
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6480
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
6626
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6481
6627
|
});
|
|
6482
6628
|
if (!res.ok) return null;
|
|
6483
6629
|
const body = await res.json();
|
|
@@ -6491,12 +6637,10 @@ async function fetchOrgConfig(deps) {
|
|
|
6491
6637
|
if (!deps.baseUrl) return null;
|
|
6492
6638
|
const token = await deps.token();
|
|
6493
6639
|
if (!token) return null;
|
|
6494
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6495
6640
|
try {
|
|
6496
|
-
const res = await
|
|
6641
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${ORG_CONFIG_PATH}`, {
|
|
6497
6642
|
method: "GET",
|
|
6498
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6499
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
6643
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6500
6644
|
});
|
|
6501
6645
|
if (!res.ok) return null;
|
|
6502
6646
|
return await res.json();
|
|
@@ -6508,13 +6652,11 @@ async function postJson(pathSuffix, payload, deps, method = "POST") {
|
|
|
6508
6652
|
if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
|
|
6509
6653
|
const token = await deps.token();
|
|
6510
6654
|
if (!token) return { ok: false, status: 0, body: null, error: "no GitHub token (run `gh auth login`)" };
|
|
6511
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6512
6655
|
try {
|
|
6513
|
-
const res = await
|
|
6656
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
|
|
6514
6657
|
method,
|
|
6515
6658
|
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6516
|
-
body: JSON.stringify(payload)
|
|
6517
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
6659
|
+
body: JSON.stringify(payload)
|
|
6518
6660
|
});
|
|
6519
6661
|
let body = null;
|
|
6520
6662
|
try {
|
|
@@ -6532,6 +6674,9 @@ async function registerProject(payload, deps) {
|
|
|
6532
6674
|
async function upsertProject(slug, patch, deps) {
|
|
6533
6675
|
return postJson(`/projects/${encodeURIComponent(slug)}`, patch, deps);
|
|
6534
6676
|
}
|
|
6677
|
+
async function attestAppGaps(slug, repo, deps) {
|
|
6678
|
+
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
6679
|
+
}
|
|
6535
6680
|
async function tenantControl(payload, deps) {
|
|
6536
6681
|
return postJson("/tenant-control", payload, deps);
|
|
6537
6682
|
}
|
|
@@ -6570,7 +6715,24 @@ function stageRequiredSecrets(stage2, meta) {
|
|
|
6570
6715
|
function stageKey(stage2, key) {
|
|
6571
6716
|
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
6572
6717
|
}
|
|
6718
|
+
function hasRuntimeSecretContract(contract) {
|
|
6719
|
+
return Boolean(contract) && !Array.isArray(contract);
|
|
6720
|
+
}
|
|
6721
|
+
function appAttestationOf(meta) {
|
|
6722
|
+
const a = meta?.appAttested;
|
|
6723
|
+
if (!a || typeof a !== "object" || Array.isArray(a)) return null;
|
|
6724
|
+
const { at, by } = a;
|
|
6725
|
+
return typeof at === "string" && at.length > 0 && typeof by === "string" && by.length > 0 ? { at, by } : null;
|
|
6726
|
+
}
|
|
6727
|
+
function attestedLine(att) {
|
|
6728
|
+
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.`;
|
|
6729
|
+
}
|
|
6730
|
+
var CONTRACT_UNDECLARED_LINE = "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: [] }).";
|
|
6573
6731
|
function appGapsFor(meta, model, slug, projectType) {
|
|
6732
|
+
const attested = appAttestationOf(meta);
|
|
6733
|
+
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless";
|
|
6734
|
+
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
6735
|
+
if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
|
|
6574
6736
|
if (projectType === "content" || model === "content") return ["Content/KB repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
6575
6737
|
if (projectType === "desktop-game") {
|
|
6576
6738
|
return [
|
|
@@ -6602,6 +6764,9 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
6602
6764
|
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
6603
6765
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
6604
6766
|
];
|
|
6767
|
+
if (contractUndeclared) {
|
|
6768
|
+
gaps.unshift(CONTRACT_UNDECLARED_LINE);
|
|
6769
|
+
}
|
|
6605
6770
|
if (slug === "mmi-katip") {
|
|
6606
6771
|
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
6772
|
}
|
|
@@ -6680,7 +6845,29 @@ async function runV2Heal(repoOrSlug, opts, deps) {
|
|
|
6680
6845
|
async function buildV2Doctor(repoOrSlug, deps) {
|
|
6681
6846
|
const slug = slugOfRepo(repoOrSlug);
|
|
6682
6847
|
const repo = repoFrom(repoOrSlug, slug);
|
|
6683
|
-
const
|
|
6848
|
+
const read = await deps.getProject(slug);
|
|
6849
|
+
if (!read.ok) {
|
|
6850
|
+
const degradedSecrets = {
|
|
6851
|
+
dev: { required: [], present: [], missing: [] },
|
|
6852
|
+
rc: { required: [], present: [], missing: [] },
|
|
6853
|
+
main: { required: [], present: [], missing: [] }
|
|
6854
|
+
};
|
|
6855
|
+
const degradedStage = {
|
|
6856
|
+
dev: { ok: false, required: false },
|
|
6857
|
+
rc: { ok: false, required: false },
|
|
6858
|
+
main: { ok: false, required: false }
|
|
6859
|
+
};
|
|
6860
|
+
return {
|
|
6861
|
+
ok: false,
|
|
6862
|
+
repo,
|
|
6863
|
+
slug,
|
|
6864
|
+
registryError: read.error,
|
|
6865
|
+
hubOwned: { meta: { ok: false, missing: [] }, deployCoords: degradedStage, deployState: degradedStage, secrets: degradedSecrets },
|
|
6866
|
+
autoHealAvailable: [],
|
|
6867
|
+
appOwnedGaps: [`Hub registry read failed (${read.error}) \u2014 diagnosis degraded; nothing is known about META, coords, or gaps. Likely transient (cold start, network, or auth blip): retry \`mmi-cli project doctor\` shortly.`]
|
|
6868
|
+
};
|
|
6869
|
+
}
|
|
6870
|
+
const meta = read.project;
|
|
6684
6871
|
const projectType = resolveProjectType(meta, repo);
|
|
6685
6872
|
const model = resolveDeployModel(meta, repo);
|
|
6686
6873
|
const autoHeal = buildV2HealPatch(repo, meta);
|
|
@@ -6741,7 +6928,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
6741
6928
|
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets: secrets2 },
|
|
6742
6929
|
secretsError,
|
|
6743
6930
|
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
6744
|
-
appOwnedGaps: autoHeal.appOwnedGaps
|
|
6931
|
+
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
6932
|
+
appAttested: appAttestationOf(meta) ?? void 0
|
|
6745
6933
|
};
|
|
6746
6934
|
}
|
|
6747
6935
|
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
@@ -6785,6 +6973,30 @@ ${section}`.trim();
|
|
|
6785
6973
|
// src/project-set.ts
|
|
6786
6974
|
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired"];
|
|
6787
6975
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
6976
|
+
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
6977
|
+
function parseRuntimeSecretsVar(raw) {
|
|
6978
|
+
let parsed;
|
|
6979
|
+
try {
|
|
6980
|
+
parsed = JSON.parse(raw);
|
|
6981
|
+
} catch {
|
|
6982
|
+
throw new Error('project set: requiredRuntimeSecrets must be JSON, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}');
|
|
6983
|
+
}
|
|
6984
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6985
|
+
throw new Error("project set: requiredRuntimeSecrets must be a stage map (a flat array is not box-loadable)");
|
|
6986
|
+
}
|
|
6987
|
+
const map = parsed;
|
|
6988
|
+
const out = {};
|
|
6989
|
+
for (const [stage2, names] of Object.entries(map)) {
|
|
6990
|
+
if (!RUNTIME_SECRET_STAGES.includes(stage2)) {
|
|
6991
|
+
throw new Error(`project set: requiredRuntimeSecrets stage "${stage2}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
6992
|
+
}
|
|
6993
|
+
if (!Array.isArray(names) || names.some((n) => typeof n !== "string" || !n.trim())) {
|
|
6994
|
+
throw new Error(`project set: requiredRuntimeSecrets.${stage2} must be an array of non-empty secret names`);
|
|
6995
|
+
}
|
|
6996
|
+
out[stage2] = names;
|
|
6997
|
+
}
|
|
6998
|
+
return out;
|
|
6999
|
+
}
|
|
6788
7000
|
function buildProjectSetPatch(input) {
|
|
6789
7001
|
const patch = {};
|
|
6790
7002
|
if (input.class) {
|
|
@@ -6814,6 +7026,8 @@ function buildProjectSetPatch(input) {
|
|
|
6814
7026
|
const n = Number(raw);
|
|
6815
7027
|
if (!Number.isFinite(n)) throw new Error("project set: projectNumber must be numeric");
|
|
6816
7028
|
patch[key] = n;
|
|
7029
|
+
} else if (key === "requiredRuntimeSecrets") {
|
|
7030
|
+
patch[key] = parseRuntimeSecretsVar(raw);
|
|
6817
7031
|
} else {
|
|
6818
7032
|
patch[key] = raw;
|
|
6819
7033
|
}
|
|
@@ -6834,6 +7048,10 @@ function buildProjectSetPatch(input) {
|
|
|
6834
7048
|
}
|
|
6835
7049
|
return patch;
|
|
6836
7050
|
}
|
|
7051
|
+
function repoFromRemoteUrl(remoteUrl) {
|
|
7052
|
+
const m = remoteUrl.trim().match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@\s/]+@)?github\.com[:/]([^/\s:]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
|
|
7053
|
+
return m ? `${m[1]}/${m[2]}` : void 0;
|
|
7054
|
+
}
|
|
6837
7055
|
function requireProjectTarget(commandName, explicitTarget, currentRepo) {
|
|
6838
7056
|
const target = explicitTarget?.trim() || currentRepo?.trim();
|
|
6839
7057
|
if (!target) {
|
|
@@ -6869,10 +7087,91 @@ function parseKbTree(stdout, prefix) {
|
|
|
6869
7087
|
}
|
|
6870
7088
|
|
|
6871
7089
|
// src/plan.ts
|
|
6872
|
-
var
|
|
7090
|
+
var import_node_path5 = require("node:path");
|
|
7091
|
+
|
|
7092
|
+
// src/frontmatter.ts
|
|
7093
|
+
function splitFrontmatter(content) {
|
|
7094
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
7095
|
+
if (!match) return { entries: [], body: content };
|
|
7096
|
+
return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
|
|
7097
|
+
}
|
|
7098
|
+
function entryKeyValue(line) {
|
|
7099
|
+
const i = line.indexOf(":");
|
|
7100
|
+
if (i < 0) return null;
|
|
7101
|
+
return { key: line.slice(0, i).trim().toLowerCase(), value: line.slice(i + 1).trim() };
|
|
7102
|
+
}
|
|
7103
|
+
function frontmatterValue(content, key) {
|
|
7104
|
+
const want = key.trim().toLowerCase();
|
|
7105
|
+
for (const line of splitFrontmatter(content).entries) {
|
|
7106
|
+
const kv = entryKeyValue(line);
|
|
7107
|
+
if (kv && kv.key === want) return kv.value || void 0;
|
|
7108
|
+
}
|
|
7109
|
+
return void 0;
|
|
7110
|
+
}
|
|
7111
|
+
function frontmatterList(content, key) {
|
|
7112
|
+
const raw = frontmatterValue(content, key);
|
|
7113
|
+
if (!raw) return [];
|
|
7114
|
+
return raw.replace(/^\[/, "").replace(/\]$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7115
|
+
}
|
|
7116
|
+
function extractPlanMeta(content) {
|
|
7117
|
+
const meta = {};
|
|
7118
|
+
const status = frontmatterValue(content, "status");
|
|
7119
|
+
if (status) meta.status = status;
|
|
7120
|
+
const tags = frontmatterList(content, "topic-tags");
|
|
7121
|
+
if (tags.length) meta.topicTags = tags;
|
|
7122
|
+
const title = frontmatterValue(content, "title");
|
|
7123
|
+
if (title) meta.title = title;
|
|
7124
|
+
const supersedes = frontmatterValue(content, "supersedes");
|
|
7125
|
+
if (supersedes) meta.supersedes = supersedes;
|
|
7126
|
+
return meta;
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
// src/plan-relevance.ts
|
|
7130
|
+
var STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "plan", "issue", "fix", "feat", "add", "wip", "src", "tsx", "mjs"]);
|
|
7131
|
+
function tokenize(s) {
|
|
7132
|
+
return (s ?? "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !STOP.has(t));
|
|
7133
|
+
}
|
|
7134
|
+
var SUPPRESSED = /* @__PURE__ */ new Set(["superseded", "graduated"]);
|
|
7135
|
+
function signalTokens(s) {
|
|
7136
|
+
const all = [
|
|
7137
|
+
...tokenize(s.branch),
|
|
7138
|
+
...tokenize(s.issueTitle),
|
|
7139
|
+
...(s.issueLabels ?? []).flatMap(tokenize),
|
|
7140
|
+
...(s.changedFiles ?? []).flatMap(tokenize)
|
|
7141
|
+
];
|
|
7142
|
+
return new Set(all);
|
|
7143
|
+
}
|
|
7144
|
+
function rankPlansByRelevance(plans, signals, opts = {}) {
|
|
7145
|
+
const wanted = signalTokens(signals);
|
|
7146
|
+
const eligible = opts.includeAll ? plans : plans.filter((p) => !p.status || !SUPPRESSED.has(p.status.toLowerCase()));
|
|
7147
|
+
const ranked = eligible.map((plan2) => {
|
|
7148
|
+
const tagTokens = new Set((plan2.topicTags ?? []).flatMap(tokenize));
|
|
7149
|
+
const titleTokens = new Set(tokenize(plan2.title));
|
|
7150
|
+
const slugTokens = new Set(tokenize(plan2.slug));
|
|
7151
|
+
const matched = /* @__PURE__ */ new Set();
|
|
7152
|
+
let score = 0;
|
|
7153
|
+
for (const t of wanted) {
|
|
7154
|
+
if (tagTokens.has(t)) {
|
|
7155
|
+
score += 3;
|
|
7156
|
+
matched.add(t);
|
|
7157
|
+
} else if (titleTokens.has(t)) {
|
|
7158
|
+
score += 2;
|
|
7159
|
+
matched.add(t);
|
|
7160
|
+
} else if (slugTokens.has(t)) {
|
|
7161
|
+
score += 1;
|
|
7162
|
+
matched.add(t);
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7165
|
+
return { plan: plan2, score, matched: [...matched] };
|
|
7166
|
+
});
|
|
7167
|
+
ranked.sort((a, b) => b.score - a.score || (b.plan.updatedAt ?? "").localeCompare(a.plan.updatedAt ?? ""));
|
|
7168
|
+
return ranked;
|
|
7169
|
+
}
|
|
7170
|
+
|
|
7171
|
+
// src/plan.ts
|
|
6873
7172
|
var PLANS_DIR = "plans";
|
|
6874
|
-
var META_FILE = (0,
|
|
6875
|
-
var planPath = (slug) => (0,
|
|
7173
|
+
var META_FILE = (0, import_node_path5.join)(PLANS_DIR, ".plan-meta.json");
|
|
7174
|
+
var planPath = (slug) => (0, import_node_path5.join)(PLANS_DIR, `${slug}.md`);
|
|
6876
7175
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
6877
7176
|
function parseMeta(raw) {
|
|
6878
7177
|
if (!raw) return {};
|
|
@@ -6901,12 +7200,7 @@ function formatPlanList(plans) {
|
|
|
6901
7200
|
return plans.map((p) => `${p.slug} \xB7 ${p.updatedAt ?? "-"} \xB7 ${p.project}`).join("\n");
|
|
6902
7201
|
}
|
|
6903
7202
|
var TIMEOUT_MS = 8e3;
|
|
6904
|
-
var GRADUATION_KEYS = /* @__PURE__ */ new Set(["northstar-graduation", "privacy", "merged-pr"]);
|
|
6905
|
-
function splitFrontmatter(content) {
|
|
6906
|
-
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
6907
|
-
if (!match) return { entries: [], body: content };
|
|
6908
|
-
return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
|
|
6909
|
-
}
|
|
7203
|
+
var GRADUATION_KEYS = /* @__PURE__ */ new Set(["northstar-graduation", "privacy", "merged-pr", "status"]);
|
|
6910
7204
|
function markPlanGraduated(content, opts) {
|
|
6911
7205
|
const { entries, body } = splitFrontmatter(normalizeEol(content));
|
|
6912
7206
|
const preserved = entries.filter((line) => {
|
|
@@ -6916,6 +7210,7 @@ function markPlanGraduated(content, opts) {
|
|
|
6916
7210
|
const next = [
|
|
6917
7211
|
...preserved,
|
|
6918
7212
|
"northstar-graduation: built-and-merged",
|
|
7213
|
+
"status: graduated",
|
|
6919
7214
|
"privacy: org",
|
|
6920
7215
|
`merged-pr: ${opts.mergedPr}`
|
|
6921
7216
|
];
|
|
@@ -6935,6 +7230,8 @@ async function planPush(deps, slug, opts = {}) {
|
|
|
6935
7230
|
const meta = parseMeta(deps.readMetaRaw());
|
|
6936
7231
|
const entry = meta[metaKey(project2, slug)];
|
|
6937
7232
|
const body = { project: project2, slug, content };
|
|
7233
|
+
const frontmatterMeta = extractPlanMeta(content);
|
|
7234
|
+
if (Object.keys(frontmatterMeta).length) body.meta = frontmatterMeta;
|
|
6938
7235
|
if (opts.force) body.force = true;
|
|
6939
7236
|
else if (entry?.etag) body.baseEtag = entry.etag;
|
|
6940
7237
|
const res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
|
|
@@ -6988,35 +7285,59 @@ async function planPull(deps, slug, opts = {}) {
|
|
|
6988
7285
|
deps.log(`pulled ${slug} \u2192 ${planPath(slug)}`);
|
|
6989
7286
|
return true;
|
|
6990
7287
|
}
|
|
7288
|
+
async function fetchPlanList(deps, project2) {
|
|
7289
|
+
const qs = project2 ? `?${new URLSearchParams({ project: project2 }).toString()}` : "";
|
|
7290
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/list${qs}`, {
|
|
7291
|
+
method: "GET",
|
|
7292
|
+
headers: await deps.headers(),
|
|
7293
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
7294
|
+
});
|
|
7295
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
7296
|
+
const { plans } = await res.json();
|
|
7297
|
+
return plans ?? [];
|
|
7298
|
+
}
|
|
6991
7299
|
async function planList(deps, opts = {}) {
|
|
6992
7300
|
const project2 = opts.project ?? (opts.quiet ? await deps.project() : void 0);
|
|
6993
|
-
|
|
6994
|
-
let res;
|
|
7301
|
+
let plans;
|
|
6995
7302
|
try {
|
|
6996
|
-
|
|
6997
|
-
method: "GET",
|
|
6998
|
-
headers: await deps.headers(),
|
|
6999
|
-
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
7000
|
-
});
|
|
7303
|
+
plans = await fetchPlanList(deps, project2);
|
|
7001
7304
|
} catch (e) {
|
|
7002
7305
|
if (!opts.quiet) deps.err(`plan list: ${e.message}`);
|
|
7003
7306
|
return;
|
|
7004
7307
|
}
|
|
7005
|
-
if (
|
|
7006
|
-
if (!opts.quiet) deps.err(`plan list failed: HTTP ${res.status}`);
|
|
7007
|
-
return;
|
|
7008
|
-
}
|
|
7009
|
-
const { plans } = await res.json();
|
|
7010
|
-
if (opts.json) {
|
|
7011
|
-
deps.log(JSON.stringify(plans));
|
|
7012
|
-
return;
|
|
7013
|
-
}
|
|
7308
|
+
if (opts.json) return deps.log(JSON.stringify(plans));
|
|
7014
7309
|
if (!plans.length) {
|
|
7015
7310
|
if (!opts.quiet) deps.log("no plans");
|
|
7016
7311
|
return;
|
|
7017
7312
|
}
|
|
7018
7313
|
deps.log(formatPlanList(plans));
|
|
7019
7314
|
}
|
|
7315
|
+
function formatRelevant(ranked) {
|
|
7316
|
+
return ranked.map((r) => {
|
|
7317
|
+
const why = r.matched.length ? `matches ${r.matched.join(", ")}` : "recent";
|
|
7318
|
+
return `${r.plan.slug} \xB7 ${why} \xB7 mmi-cli northstar pull ${r.plan.slug}`;
|
|
7319
|
+
}).join("\n");
|
|
7320
|
+
}
|
|
7321
|
+
async function relevantPlans(deps, signals, opts = {}) {
|
|
7322
|
+
const project2 = opts.project ?? await deps.project();
|
|
7323
|
+
let plans;
|
|
7324
|
+
try {
|
|
7325
|
+
const scoped = await fetchPlanList(deps, project2);
|
|
7326
|
+
const unprojected = project2 === "-" ? [] : await fetchPlanList(deps, "-").catch(() => []);
|
|
7327
|
+
const seen = new Set(scoped.map((p) => `${p.project}/${p.slug}`));
|
|
7328
|
+
plans = [...scoped, ...unprojected.filter((p) => !seen.has(`${p.project}/${p.slug}`))];
|
|
7329
|
+
} catch (e) {
|
|
7330
|
+
deps.err(`northstar relevant: ${e.message}`);
|
|
7331
|
+
return;
|
|
7332
|
+
}
|
|
7333
|
+
if (!plans.length) return deps.log("no North Stars for this repo yet");
|
|
7334
|
+
const ranked = rankPlansByRelevance(plans, signals, { includeAll: opts.includeAll });
|
|
7335
|
+
const top = (opts.includeAll ? ranked : ranked.filter((r) => r.score > 0)).slice(0, opts.limit ?? 5);
|
|
7336
|
+
if (!top.length) {
|
|
7337
|
+
return deps.log(`no task-relevant North Stars among ${plans.length} for this repo \u2014 \`mmi-cli northstar relevant --all\` lists recent ones`);
|
|
7338
|
+
}
|
|
7339
|
+
deps.log(formatRelevant(top));
|
|
7340
|
+
}
|
|
7020
7341
|
async function planDelete(deps, slug, opts = {}) {
|
|
7021
7342
|
const project2 = opts.project ?? await deps.project();
|
|
7022
7343
|
const res = await deps.fetch(`${deps.apiUrl}/plan/delete`, {
|
|
@@ -7113,12 +7434,14 @@ function formatVaultPointer(p) {
|
|
|
7113
7434
|
}
|
|
7114
7435
|
var TIMEOUT_MS2 = 8e3;
|
|
7115
7436
|
var repoOf = (slug) => `${OWNER2}/${slug}`;
|
|
7437
|
+
async function vaultSlug(deps, opts) {
|
|
7438
|
+
return (opts.repo ? opts.repo.split("/").pop() : await deps.slug()).toLowerCase();
|
|
7439
|
+
}
|
|
7116
7440
|
async function targetRepo(deps, opts) {
|
|
7117
|
-
return opts.repo ?? repoOf(await deps
|
|
7441
|
+
return opts.repo ?? repoOf(await vaultSlug(deps, opts));
|
|
7118
7442
|
}
|
|
7119
7443
|
async function secretsWhere(deps, opts) {
|
|
7120
|
-
|
|
7121
|
-
deps.log(formatVaultPointer(vaultPointer(slug)));
|
|
7444
|
+
deps.log(formatVaultPointer(vaultPointer(await vaultSlug(deps, opts))));
|
|
7122
7445
|
}
|
|
7123
7446
|
async function readErr(res) {
|
|
7124
7447
|
try {
|
|
@@ -7300,7 +7623,7 @@ async function secretsSet(deps, key, opts) {
|
|
|
7300
7623
|
);
|
|
7301
7624
|
return;
|
|
7302
7625
|
}
|
|
7303
|
-
deps.log(`set ${key} (${classifyTier(await deps
|
|
7626
|
+
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
7304
7627
|
}
|
|
7305
7628
|
async function secretsEdit(deps, key, opts) {
|
|
7306
7629
|
return secretsSet(deps, key, opts);
|
|
@@ -7352,8 +7675,8 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
|
|
|
7352
7675
|
}
|
|
7353
7676
|
deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
|
|
7354
7677
|
}
|
|
7355
|
-
async function secretsUse(deps, key,
|
|
7356
|
-
const slug = await deps
|
|
7678
|
+
async function secretsUse(deps, key, opts) {
|
|
7679
|
+
const slug = await vaultSlug(deps, opts);
|
|
7357
7680
|
const tier = classifyTier(slug, key);
|
|
7358
7681
|
const path2 = secretParamName(slug, key);
|
|
7359
7682
|
deps.log(
|
|
@@ -7438,30 +7761,23 @@ function authorizeBodyHasMismatch(body) {
|
|
|
7438
7761
|
}
|
|
7439
7762
|
|
|
7440
7763
|
// src/index.ts
|
|
7441
|
-
var
|
|
7764
|
+
var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
|
|
7442
7765
|
var DEFAULT_EXEC_TIMEOUT_MS = 1e4;
|
|
7443
|
-
var
|
|
7766
|
+
var execFileP4 = (file, args, options = {}) => (
|
|
7444
7767
|
// encoding 'utf8' guarantees string stdout/stderr at runtime; the cast pins the type because
|
|
7445
7768
|
// promisify(execFile)'s overloads widen to string|Buffer when options is spread in.
|
|
7446
|
-
|
|
7769
|
+
rawExecFileP3(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
|
|
7447
7770
|
);
|
|
7448
7771
|
var GIT_TIMEOUT_MS = DEFAULT_EXEC_TIMEOUT_MS;
|
|
7449
7772
|
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
7773
|
var cachedGithubLogin;
|
|
7458
7774
|
async function githubLogin() {
|
|
7459
|
-
cachedGithubLogin ??=
|
|
7775
|
+
cachedGithubLogin ??= execFileP4("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
7460
7776
|
return cachedGithubLogin;
|
|
7461
7777
|
}
|
|
7462
7778
|
async function awsCallerArn() {
|
|
7463
7779
|
try {
|
|
7464
|
-
const { stdout } = await
|
|
7780
|
+
const { stdout } = await execFileP4(
|
|
7465
7781
|
"aws",
|
|
7466
7782
|
["sts", "get-caller-identity", "--query", "Arn", "--output", "text"],
|
|
7467
7783
|
{ timeout: GIT_TIMEOUT_MS }
|
|
@@ -7478,7 +7794,7 @@ async function sagaHeaders(extra = {}) {
|
|
|
7478
7794
|
async function loadConfig() {
|
|
7479
7795
|
let file = {};
|
|
7480
7796
|
try {
|
|
7481
|
-
file = JSON.parse(await (0,
|
|
7797
|
+
file = JSON.parse(await (0, import_promises2.readFile)(".mmi/config.json", "utf8"));
|
|
7482
7798
|
} catch {
|
|
7483
7799
|
file = {};
|
|
7484
7800
|
}
|
|
@@ -7486,12 +7802,16 @@ async function loadConfig() {
|
|
|
7486
7802
|
return file;
|
|
7487
7803
|
}
|
|
7488
7804
|
var discoveredConfig = null;
|
|
7805
|
+
function registryDegradeError(error) {
|
|
7806
|
+
return new Error(`Hub registry read failed (${error}) \u2014 board coords could not be discovered; likely transient (cold start, network, or auth blip) \u2014 retry shortly`);
|
|
7807
|
+
}
|
|
7489
7808
|
async function loadConfigOrDiscover() {
|
|
7490
7809
|
if (discoveredConfig) return discoveredConfig;
|
|
7491
7810
|
const floor = await loadConfig();
|
|
7492
7811
|
if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
|
|
7493
|
-
const
|
|
7494
|
-
|
|
7812
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(floor));
|
|
7813
|
+
if (!read.ok) throw registryDegradeError(read.error);
|
|
7814
|
+
discoveredConfig = read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
|
|
7495
7815
|
return discoveredConfig;
|
|
7496
7816
|
}
|
|
7497
7817
|
async function loadConfigForRepo(targetRepo2) {
|
|
@@ -7499,9 +7819,10 @@ async function loadConfigForRepo(targetRepo2) {
|
|
|
7499
7819
|
const cwdRepo = await resolveRepo();
|
|
7500
7820
|
if (cwdRepo && targetRepo2.toLowerCase() === cwdRepo.toLowerCase()) return loadConfigOrDiscover();
|
|
7501
7821
|
const floor = await loadConfig();
|
|
7502
|
-
const
|
|
7503
|
-
if (!
|
|
7504
|
-
return
|
|
7822
|
+
const read = await fetchProjectBySlugChecked(slugOf(targetRepo2), registryClientDeps(floor));
|
|
7823
|
+
if (!read.ok) throw registryDegradeError(read.error);
|
|
7824
|
+
if (!read.project) return stripMutableBoardConfig(floor);
|
|
7825
|
+
return boardConfigFromProject(read.project, floor);
|
|
7505
7826
|
}
|
|
7506
7827
|
function repoFromSelector(selector) {
|
|
7507
7828
|
const trimmed = selector.trim();
|
|
@@ -7521,7 +7842,7 @@ var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/d
|
|
|
7521
7842
|
var SESSION_FILE = ".mmi/.session";
|
|
7522
7843
|
var gitOut = async (args) => {
|
|
7523
7844
|
try {
|
|
7524
|
-
return (await
|
|
7845
|
+
return (await execFileP4("git", [...args])).stdout.trim();
|
|
7525
7846
|
} catch {
|
|
7526
7847
|
return "";
|
|
7527
7848
|
}
|
|
@@ -7538,7 +7859,7 @@ function sessionDeps() {
|
|
|
7538
7859
|
},
|
|
7539
7860
|
writePersisted: (id) => persistSession(id),
|
|
7540
7861
|
now: () => /* @__PURE__ */ new Date(),
|
|
7541
|
-
rand: () => (0,
|
|
7862
|
+
rand: () => (0, import_node_crypto2.randomBytes)(4).toString("hex")
|
|
7542
7863
|
};
|
|
7543
7864
|
}
|
|
7544
7865
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
@@ -7561,16 +7882,11 @@ async function postCapture(capture, quiet = false) {
|
|
|
7561
7882
|
if (!quiet) console.error("mmi-cli saga: Hub API URL not configured");
|
|
7562
7883
|
return;
|
|
7563
7884
|
}
|
|
7564
|
-
const res = await fetch
|
|
7885
|
+
const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/capture`, {
|
|
7565
7886
|
method: "POST",
|
|
7566
7887
|
headers: await sagaHeaders({ "content-type": "application/json" }),
|
|
7567
|
-
body: JSON.stringify({ ...capture, ...await sagaKey(cfg) })
|
|
7568
|
-
|
|
7569
|
-
// head-write timeout (20s) so a continuity note isn't lost to a slow/cold backend. No client retry:
|
|
7570
|
-
// the capture isn't guaranteed idempotent, so a retry after a server-side-completed write could
|
|
7571
|
-
// duplicate the note. Backend capture-latency root cause tracked in #255.
|
|
7572
|
-
signal: AbortSignal.timeout(2e4)
|
|
7573
|
-
});
|
|
7888
|
+
body: JSON.stringify({ ...capture, ...await sagaKey(cfg) })
|
|
7889
|
+
}, { attempts: 2, timeoutMs: 2e4, retryOn: () => false });
|
|
7574
7890
|
let message = "";
|
|
7575
7891
|
if (!res.ok) {
|
|
7576
7892
|
try {
|
|
@@ -7593,18 +7909,18 @@ async function readStdin() {
|
|
|
7593
7909
|
async function ghPrs(limit) {
|
|
7594
7910
|
const args = (state) => ["pr", "list", "--state", state, "--limit", String(limit), "--json", "number,headRefName,state"];
|
|
7595
7911
|
const [open, closed] = await Promise.all([
|
|
7596
|
-
|
|
7597
|
-
|
|
7912
|
+
execFileP4("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
|
|
7913
|
+
execFileP4("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
|
|
7598
7914
|
]);
|
|
7599
7915
|
return [...JSON.parse(open.stdout || "[]"), ...JSON.parse(closed.stdout || "[]")];
|
|
7600
7916
|
}
|
|
7601
7917
|
async function worktreeBranches() {
|
|
7602
|
-
const { stdout } = await
|
|
7918
|
+
const { stdout } = await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7603
7919
|
const parsed = parseWorktreePorcelain(stdout);
|
|
7604
7920
|
return await Promise.all(parsed.map(async (w) => {
|
|
7605
7921
|
let dirty = true;
|
|
7606
7922
|
try {
|
|
7607
|
-
const { stdout: status } = await
|
|
7923
|
+
const { stdout: status } = await execFileP4("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
7608
7924
|
dirty = status.trim().length > 0;
|
|
7609
7925
|
} catch {
|
|
7610
7926
|
dirty = true;
|
|
@@ -7616,7 +7932,7 @@ async function gcPlan(remote, limit) {
|
|
|
7616
7932
|
const [branches, current, stale, prs, worktrees] = await Promise.all([
|
|
7617
7933
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
7618
7934
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
7619
|
-
|
|
7935
|
+
execFileP4("git", ["remote", "prune", remote, "--dry-run"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseRemotePruneDryRun(`${r.stdout}${r.stderr}`)).catch(() => []),
|
|
7620
7936
|
ghPrs(limit),
|
|
7621
7937
|
worktreeBranches()
|
|
7622
7938
|
]);
|
|
@@ -7644,8 +7960,8 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7644
7960
|
const result = { removedBranches: [], removedTrackingRefs: [], failed: [], pruned: false };
|
|
7645
7961
|
for (const branch of plan2.branches) {
|
|
7646
7962
|
try {
|
|
7647
|
-
if (branch.worktreePath) await
|
|
7648
|
-
await
|
|
7963
|
+
if (branch.worktreePath) await execFileP4("git", ["worktree", "remove", "--force", branch.worktreePath], { timeout: GIT_TIMEOUT_MS });
|
|
7964
|
+
await execFileP4("git", ["branch", "-D", branch.branch], { timeout: GIT_TIMEOUT_MS });
|
|
7649
7965
|
result.removedBranches.push(branch.branch);
|
|
7650
7966
|
} catch (e) {
|
|
7651
7967
|
result.failed.push(`${branch.branch}: ${e.message.split("\n")[0]}`);
|
|
@@ -7653,7 +7969,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7653
7969
|
}
|
|
7654
7970
|
for (const ref of plan2.trackingRefs) {
|
|
7655
7971
|
try {
|
|
7656
|
-
await
|
|
7972
|
+
await execFileP4("git", ["update-ref", "-d", `refs/remotes/${remote}/${ref.branch}`], { timeout: GIT_TIMEOUT_MS });
|
|
7657
7973
|
result.removedTrackingRefs.push(ref.branch);
|
|
7658
7974
|
} catch (e) {
|
|
7659
7975
|
result.failed.push(`${remote}/${ref.branch}: ${e.message.split("\n")[0]}`);
|
|
@@ -7661,7 +7977,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7661
7977
|
}
|
|
7662
7978
|
if (plan2.branches.some((b) => b.worktreePath)) {
|
|
7663
7979
|
try {
|
|
7664
|
-
await
|
|
7980
|
+
await execFileP4("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
|
|
7665
7981
|
result.pruned = true;
|
|
7666
7982
|
} catch (e) {
|
|
7667
7983
|
result.failed.push(`worktree prune: ${e.message.split("\n")[0]}`);
|
|
@@ -7671,11 +7987,11 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7671
7987
|
}
|
|
7672
7988
|
function resolveVersion() {
|
|
7673
7989
|
try {
|
|
7674
|
-
const manifest = (0,
|
|
7990
|
+
const manifest = (0, import_node_path6.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
7675
7991
|
return JSON.parse((0, import_node_fs5.readFileSync)(manifest, "utf8")).version || "0.0.0";
|
|
7676
7992
|
} catch {
|
|
7677
7993
|
try {
|
|
7678
|
-
const pkg = (0,
|
|
7994
|
+
const pkg = (0, import_node_path6.join)(__dirname, "..", "package.json");
|
|
7679
7995
|
return JSON.parse((0, import_node_fs5.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
7680
7996
|
} catch {
|
|
7681
7997
|
return "0.0.0";
|
|
@@ -7684,14 +8000,14 @@ function resolveVersion() {
|
|
|
7684
8000
|
}
|
|
7685
8001
|
function readRepoVersion() {
|
|
7686
8002
|
try {
|
|
7687
|
-
return JSON.parse((0, import_node_fs5.readFileSync)((0,
|
|
8003
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path6.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
7688
8004
|
} catch {
|
|
7689
8005
|
return void 0;
|
|
7690
8006
|
}
|
|
7691
8007
|
}
|
|
7692
8008
|
async function fetchReleasedVersion() {
|
|
7693
8009
|
try {
|
|
7694
|
-
const { stdout } = await
|
|
8010
|
+
const { stdout } = await execFileP4("gh", pluginManifestVersionArgs(), { timeout: 5e3 });
|
|
7695
8011
|
return parseManifestVersion(stdout);
|
|
7696
8012
|
} catch {
|
|
7697
8013
|
return void 0;
|
|
@@ -7705,9 +8021,9 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
7705
8021
|
const target = report.releasedVersion ?? "latest";
|
|
7706
8022
|
if (action === "plugin-pull") {
|
|
7707
8023
|
try {
|
|
7708
|
-
const root = (await
|
|
8024
|
+
const root = (await execFileP4("git", ["-C", process.env.CLAUDE_PLUGIN_ROOT, "rev-parse", "--show-toplevel"], { timeout: PLUGIN_PULL_TIMEOUT_MS })).stdout.trim();
|
|
7709
8025
|
log(` \u21BB refreshing MMI plugin ${report.currentVersion} \u2192 ${target} (effective next session)\u2026`);
|
|
7710
|
-
await
|
|
8026
|
+
await execFileP4("git", ["-C", root, "pull", "--ff-only"], { timeout: PLUGIN_PULL_TIMEOUT_MS });
|
|
7711
8027
|
return { ...report, ok: true };
|
|
7712
8028
|
} catch {
|
|
7713
8029
|
return report;
|
|
@@ -7734,7 +8050,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
7734
8050
|
var consoleIo = { log: (m) => console.log(m), err: (m) => console.error(m) };
|
|
7735
8051
|
var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
7736
8052
|
function runHostBin(bin, args, opts) {
|
|
7737
|
-
return isWin ?
|
|
8053
|
+
return isWin ? execFileP4("cmd.exe", ["/c", bin, ...args], opts) : execFileP4(bin, args, opts);
|
|
7738
8054
|
}
|
|
7739
8055
|
async function runClaudePlugin(args) {
|
|
7740
8056
|
try {
|
|
@@ -7781,11 +8097,11 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
7781
8097
|
for (const entry of fetched) {
|
|
7782
8098
|
if ("error" in entry) continue;
|
|
7783
8099
|
const { file, source } = entry;
|
|
7784
|
-
const current = (0, import_node_fs5.existsSync)(file) ? await (0,
|
|
8100
|
+
const current = (0, import_node_fs5.existsSync)(file) ? await (0, import_promises2.readFile)(file, "utf8") : null;
|
|
7785
8101
|
if (needsUpdate(source, current)) {
|
|
7786
8102
|
const slash = file.lastIndexOf("/");
|
|
7787
8103
|
if (slash > 0) (0, import_node_fs5.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
7788
|
-
await (0,
|
|
8104
|
+
await (0, import_promises2.writeFile)(file, normalizeEol(source), "utf8");
|
|
7789
8105
|
changed++;
|
|
7790
8106
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
7791
8107
|
}
|
|
@@ -7805,14 +8121,14 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
7805
8121
|
isDirty: async (f) => await gitOut(["status", "--porcelain", "--", f]) !== "",
|
|
7806
8122
|
originContent: async (f) => {
|
|
7807
8123
|
try {
|
|
7808
|
-
return (await
|
|
8124
|
+
return (await execFileP4("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
|
|
7809
8125
|
} catch {
|
|
7810
8126
|
return null;
|
|
7811
8127
|
}
|
|
7812
8128
|
},
|
|
7813
|
-
localContent: async (f) => (0, import_node_fs5.existsSync)(f) ? await (0,
|
|
8129
|
+
localContent: async (f) => (0, import_node_fs5.existsSync)(f) ? await (0, import_promises2.readFile)(f, "utf8") : null,
|
|
7814
8130
|
writeDoc: async (f, c) => {
|
|
7815
|
-
await (0,
|
|
8131
|
+
await (0, import_promises2.writeFile)(f, c, "utf8");
|
|
7816
8132
|
}
|
|
7817
8133
|
});
|
|
7818
8134
|
for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
|
|
@@ -7824,7 +8140,7 @@ docs.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
7824
8140
|
var saga = program2.command("saga").description("per-session continuity");
|
|
7825
8141
|
async function runNote(summary, o) {
|
|
7826
8142
|
const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
|
|
7827
|
-
const capture = buildNoteCapture(summary, o, (0,
|
|
8143
|
+
const capture = buildNoteCapture(summary, o, (0, import_node_crypto2.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
7828
8144
|
await postCapture(capture);
|
|
7829
8145
|
}
|
|
7830
8146
|
saga.command("note <summary>").description("record a one-line structured note into your saga (the per-turn capture)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--verified", "mark this claim as checked against source (state: verified, else asserted)").option("--diagnostic", "isolate a probe write (state: diagnostic, source: probe) \u2014 never resume/LAST 5").option("--supersedes <key>", "retire prior decisions matching an evidence key (pr:N | file:path)").option("--anchor <intent>", "set the sprint North-Star (write-protected; needs --anchor-force to change)").option("--anchor-force", "overwrite an existing anchor").action((summary, o) => runNote(summary, o));
|
|
@@ -7838,21 +8154,24 @@ async function runSagaShow(opts, io = consoleIo) {
|
|
|
7838
8154
|
try {
|
|
7839
8155
|
const key = await sagaKey(cfg);
|
|
7840
8156
|
const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
|
|
7841
|
-
const res = await fetch
|
|
8157
|
+
const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders() }, { attempts: 2, timeoutMs: 3e3 });
|
|
7842
8158
|
if (res.ok) {
|
|
7843
8159
|
io.log(resumeCue());
|
|
7844
8160
|
return io.log(await res.text());
|
|
7845
8161
|
}
|
|
7846
8162
|
if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
|
|
7847
8163
|
} catch (e) {
|
|
7848
|
-
if (!opts.quiet)
|
|
8164
|
+
if (!opts.quiet) {
|
|
8165
|
+
const reason = e.name === "TimeoutError" ? "backend unreachable (timed out after 2 attempts)" : e.message;
|
|
8166
|
+
io.err(`saga show: ${reason} \u2014 continuing without saga; diagnose with \`mmi-cli saga health --json\``);
|
|
8167
|
+
}
|
|
7849
8168
|
}
|
|
7850
8169
|
}
|
|
7851
8170
|
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));
|
|
7852
8171
|
saga.command("capture").option("--quiet", "capture silently (for the Stop hook)").description("per-turn deterministic capture (Stop hook): turn boundary + current sha").action(async (opts) => {
|
|
7853
8172
|
const hook = parseHookInput(await readStdin());
|
|
7854
8173
|
if (hook.session_id) persistSession(hook.session_id);
|
|
7855
|
-
await postCapture({ event: "stop", id: (0,
|
|
8174
|
+
await postCapture({ event: "stop", id: (0, import_node_crypto2.randomUUID)(), source: "hook", sha: await gitOut(["rev-parse", "--short", "HEAD"]), surface: agentSurface() }, opts.quiet ?? false);
|
|
7856
8175
|
});
|
|
7857
8176
|
saga.command("session").option("--quiet", "silent (for the SessionStart hook)").description("persist the harness session id for this repo (SessionStart hook)").action(async () => {
|
|
7858
8177
|
const hook = parseHookInput(await readStdin());
|
|
@@ -7865,7 +8184,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
7865
8184
|
if (!headGateDue(tsPath)) return;
|
|
7866
8185
|
markHeadRun(tsPath);
|
|
7867
8186
|
try {
|
|
7868
|
-
(0,
|
|
8187
|
+
(0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], "saga", "head-update", "--run"], {
|
|
7869
8188
|
detached: true,
|
|
7870
8189
|
stdio: "ignore",
|
|
7871
8190
|
windowsHide: true
|
|
@@ -7904,7 +8223,7 @@ saga.command("key").option("--json", "machine-readable output").description("pri
|
|
|
7904
8223
|
});
|
|
7905
8224
|
async function probeBackend(url) {
|
|
7906
8225
|
try {
|
|
7907
|
-
const res = await fetch
|
|
8226
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await sagaHeaders() }, { attempts: 3, timeoutMs: 4e3 });
|
|
7908
8227
|
let message = "";
|
|
7909
8228
|
try {
|
|
7910
8229
|
const body = await res.clone().json();
|
|
@@ -7982,7 +8301,7 @@ var kb = program2.command("kb").description("org knowledgebase (read-only)");
|
|
|
7982
8301
|
kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
|
|
7983
8302
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
7984
8303
|
try {
|
|
7985
|
-
const { stdout } = await
|
|
8304
|
+
const { stdout } = await execFileP4("gh", buildKbGetArgs(src, path2), { timeout: 1e4 });
|
|
7986
8305
|
process.stdout.write(stdout);
|
|
7987
8306
|
} catch (e) {
|
|
7988
8307
|
const err = e;
|
|
@@ -7992,7 +8311,7 @@ kb.command("get <path>").description("print a KB document by path").action(async
|
|
|
7992
8311
|
kb.command("list [prefix]").description("list KB document paths (optionally under a prefix)").action(async (prefix) => {
|
|
7993
8312
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
7994
8313
|
try {
|
|
7995
|
-
const { stdout } = await
|
|
8314
|
+
const { stdout } = await execFileP4("gh", buildKbTreeArgs(src), { timeout: 1e4 });
|
|
7996
8315
|
const paths = parseKbTree(stdout, prefix);
|
|
7997
8316
|
if (!paths.length) return fail(`kb list: no documents${prefix ? ` under ${prefix}` : ""}`);
|
|
7998
8317
|
console.log(paths.join("\n"));
|
|
@@ -8002,22 +8321,29 @@ kb.command("list [prefix]").description("list KB document paths (optionally unde
|
|
|
8002
8321
|
}
|
|
8003
8322
|
});
|
|
8004
8323
|
async function ghCreate(args) {
|
|
8324
|
+
const swapped = await bodyArgsViaFile(args);
|
|
8005
8325
|
try {
|
|
8006
|
-
const { stdout } = await
|
|
8326
|
+
const { stdout } = await execFileP4("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8007
8327
|
return parseCreatedUrl(stdout);
|
|
8008
8328
|
} catch (e) {
|
|
8329
|
+
await swapped.cleanup();
|
|
8009
8330
|
const err = e;
|
|
8010
|
-
|
|
8331
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
8332
|
+
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
8333
|
+
} finally {
|
|
8334
|
+
await swapped.cleanup();
|
|
8011
8335
|
}
|
|
8012
8336
|
}
|
|
8013
|
-
async function
|
|
8014
|
-
const { stdout } = await
|
|
8337
|
+
async function ghJson(args, timeout = 1e4) {
|
|
8338
|
+
const { stdout } = await execFileP4("gh", args, { timeout });
|
|
8015
8339
|
return JSON.parse(stdout);
|
|
8016
8340
|
}
|
|
8017
8341
|
async function resolveRepo(repo) {
|
|
8018
8342
|
if (repo) return repo;
|
|
8343
|
+
const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
|
|
8344
|
+
if (fromOrigin) return fromOrigin;
|
|
8019
8345
|
try {
|
|
8020
|
-
const { stdout } = await
|
|
8346
|
+
const { stdout } = await execFileP4("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
|
|
8021
8347
|
return stdout.trim() || void 0;
|
|
8022
8348
|
} catch {
|
|
8023
8349
|
return void 0;
|
|
@@ -8025,7 +8351,13 @@ async function resolveRepo(repo) {
|
|
|
8025
8351
|
}
|
|
8026
8352
|
async function attachToProject(issueNumber, repo, priority) {
|
|
8027
8353
|
const targetRepo2 = await resolveRepo(repo);
|
|
8028
|
-
|
|
8354
|
+
let cfg;
|
|
8355
|
+
try {
|
|
8356
|
+
cfg = await loadConfigForRepo(targetRepo2);
|
|
8357
|
+
} catch (e) {
|
|
8358
|
+
console.error(`issue create: board attach skipped \u2014 ${e.message}`);
|
|
8359
|
+
return void 0;
|
|
8360
|
+
}
|
|
8029
8361
|
if (!cfg.projectId) {
|
|
8030
8362
|
console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
8031
8363
|
return void 0;
|
|
@@ -8033,19 +8365,14 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
8033
8365
|
try {
|
|
8034
8366
|
const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
|
|
8035
8367
|
if (targetRepo2) viewArgs.push("--repo", targetRepo2);
|
|
8036
|
-
const { stdout: idOut } = await
|
|
8368
|
+
const { stdout: idOut } = await execFileP4("gh", viewArgs, { timeout: 1e4 });
|
|
8037
8369
|
const contentId = idOut.trim();
|
|
8038
8370
|
if (!contentId) throw new Error("could not resolve issue node id");
|
|
8039
|
-
const { stdout } = await
|
|
8371
|
+
const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8040
8372
|
const projectItemId = parseAddedItemId(stdout);
|
|
8041
8373
|
if (projectItemId && priority) {
|
|
8042
8374
|
try {
|
|
8043
|
-
await setBoardItemPriority(
|
|
8044
|
-
async (args) => execFileP3("gh", args, { timeout: 1e4 }),
|
|
8045
|
-
cfg,
|
|
8046
|
-
projectItemId,
|
|
8047
|
-
priority
|
|
8048
|
-
);
|
|
8375
|
+
await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
|
|
8049
8376
|
} catch (e) {
|
|
8050
8377
|
const err = e;
|
|
8051
8378
|
process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
|
|
@@ -8064,7 +8391,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
8064
8391
|
try {
|
|
8065
8392
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
8066
8393
|
if (o.repo) args.push("--repo", o.repo);
|
|
8067
|
-
(0,
|
|
8394
|
+
(0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
8068
8395
|
detached: true,
|
|
8069
8396
|
stdio: "ignore",
|
|
8070
8397
|
windowsHide: true,
|
|
@@ -8120,7 +8447,7 @@ function openInEditor(path2) {
|
|
|
8120
8447
|
return;
|
|
8121
8448
|
}
|
|
8122
8449
|
try {
|
|
8123
|
-
(0,
|
|
8450
|
+
(0, import_node_child_process6.spawn)(editor, [path2], { stdio: "inherit" });
|
|
8124
8451
|
} catch {
|
|
8125
8452
|
console.log(`open ${path2} manually`);
|
|
8126
8453
|
}
|
|
@@ -8133,6 +8460,26 @@ async function withPlan(quiet, run, io = consoleIo) {
|
|
|
8133
8460
|
}
|
|
8134
8461
|
await run(makePlanDeps(cfg, io));
|
|
8135
8462
|
}
|
|
8463
|
+
async function gatherRelevanceSignals() {
|
|
8464
|
+
const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
|
|
8465
|
+
const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
8466
|
+
const signals = { branch, changedFiles: changed.length ? changed : void 0 };
|
|
8467
|
+
const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
|
|
8468
|
+
if (issueNum) {
|
|
8469
|
+
try {
|
|
8470
|
+
const { stdout } = await execFileP4(
|
|
8471
|
+
"gh",
|
|
8472
|
+
["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
|
|
8473
|
+
{ timeout: 1e4 }
|
|
8474
|
+
);
|
|
8475
|
+
const j = JSON.parse(stdout);
|
|
8476
|
+
if (j.title) signals.issueTitle = j.title;
|
|
8477
|
+
if (j.labels?.length) signals.issueLabels = j.labels;
|
|
8478
|
+
} catch {
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
return signals;
|
|
8482
|
+
}
|
|
8136
8483
|
function registerNorthStarCommands(cmd) {
|
|
8137
8484
|
cmd.command("push <slug>").description("push a local North Star plan (plans/<slug>.md) to the server").option("--project <name>", "override the project key").option("--force", "overwrite the remote even if it changed since your last sync").action((slug, o) => withPlan(false, async (d) => {
|
|
8138
8485
|
const ok = await planPush(d, slug, o);
|
|
@@ -8143,6 +8490,10 @@ function registerNorthStarCommands(cmd) {
|
|
|
8143
8490
|
if (!ok) process.exitCode = 1;
|
|
8144
8491
|
}));
|
|
8145
8492
|
cmd.command("list").description("list your North Star plans (cross-device)").option("--project <name>", "filter by project").option("--json", "machine-readable output").option("--quiet", "silent when unconfigured/empty/unreachable (SessionStart hook)").action((o) => withPlan(o.quiet ?? false, (d) => planList(d, o)));
|
|
8493
|
+
cmd.command("relevant").description("list North Stars relevant to your current task (branch + claimed issue + changed files)").option("--project <name>", "override the project key").option("--all", "include superseded/graduated and recent non-matching plans").option("--limit <n>", "max results (default 5)").action((o) => withPlan(false, async (d) => {
|
|
8494
|
+
const signals = await gatherRelevanceSignals();
|
|
8495
|
+
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0 });
|
|
8496
|
+
}));
|
|
8146
8497
|
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
8147
8498
|
(slug, o) => withPlan(false, async (d) => {
|
|
8148
8499
|
const ok = await planPull(d, slug, { project: o.project });
|
|
@@ -8178,7 +8529,9 @@ function makeSecretsDeps(cfg) {
|
|
|
8178
8529
|
apiUrl: cfg.sagaApiUrl,
|
|
8179
8530
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
8180
8531
|
headers: (extra) => sagaHeaders(extra),
|
|
8181
|
-
|
|
8532
|
+
// Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
|
|
8533
|
+
// casing, which leaked mixed-case into `secrets where` output (#681).
|
|
8534
|
+
slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
|
|
8182
8535
|
readSecretValue: () => readSecretStdin(),
|
|
8183
8536
|
log: (m) => console.log(m),
|
|
8184
8537
|
err: (m) => console.error(m)
|
|
@@ -8251,7 +8604,8 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
8251
8604
|
async function v2ReadinessDeps(cfg) {
|
|
8252
8605
|
const reg = registryClientDeps(cfg);
|
|
8253
8606
|
return {
|
|
8254
|
-
|
|
8607
|
+
// Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
|
|
8608
|
+
getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
|
|
8255
8609
|
hasDeployCoords: async (slug, stage2) => {
|
|
8256
8610
|
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
8257
8611
|
return Boolean(status?.stages[stage2]);
|
|
@@ -8264,11 +8618,10 @@ async function v2ReadinessDeps(cfg) {
|
|
|
8264
8618
|
const apiUrl = cfg.sagaApiUrl;
|
|
8265
8619
|
if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
|
|
8266
8620
|
const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
|
|
8267
|
-
const res = await fetch
|
|
8621
|
+
const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
|
|
8268
8622
|
method: "GET",
|
|
8269
|
-
headers: await sagaHeaders()
|
|
8270
|
-
|
|
8271
|
-
});
|
|
8623
|
+
headers: await sagaHeaders()
|
|
8624
|
+
}, { attempts: 2, timeoutMs: 5e3 });
|
|
8272
8625
|
if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
|
|
8273
8626
|
const body = await res.json();
|
|
8274
8627
|
return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
|
|
@@ -8278,22 +8631,32 @@ async function v2ReadinessDeps(cfg) {
|
|
|
8278
8631
|
async function updateV2ReadinessIssue(repo, report, healed) {
|
|
8279
8632
|
const title = "v2 readiness: central deploy + secrets alignment";
|
|
8280
8633
|
const freshBody = renderReadinessIssueBody("", report, { healed });
|
|
8281
|
-
const list = await
|
|
8634
|
+
const list = await execFileP4("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
|
|
8282
8635
|
const issues = JSON.parse(list.stdout || "[]");
|
|
8283
8636
|
const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
|
|
8284
8637
|
if (!existing) {
|
|
8285
|
-
const
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8638
|
+
const create = await bodyArgsViaFile(["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"]);
|
|
8639
|
+
try {
|
|
8640
|
+
const created = await execFileP4("gh", create.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8641
|
+
const url = created.stdout.trim();
|
|
8642
|
+
const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
|
|
8643
|
+
return { number, url, action: "created" };
|
|
8644
|
+
} finally {
|
|
8645
|
+
await create.cleanup();
|
|
8646
|
+
}
|
|
8289
8647
|
}
|
|
8290
|
-
const view = await
|
|
8648
|
+
const view = await execFileP4("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
|
|
8291
8649
|
const current = JSON.parse(view.stdout || "{}");
|
|
8292
8650
|
const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
|
|
8293
|
-
await
|
|
8651
|
+
const edit = await bodyArgsViaFile(["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody]);
|
|
8652
|
+
try {
|
|
8653
|
+
await execFileP4("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8654
|
+
} finally {
|
|
8655
|
+
await edit.cleanup();
|
|
8656
|
+
}
|
|
8294
8657
|
return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
|
|
8295
8658
|
}
|
|
8296
|
-
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
|
|
8659
|
+
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
8660
|
async function projectTarget(commandName, explicitTarget) {
|
|
8298
8661
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
8299
8662
|
}
|
|
@@ -8330,7 +8693,7 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
8330
8693
|
}
|
|
8331
8694
|
fail(msg);
|
|
8332
8695
|
});
|
|
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) => {
|
|
8696
|
+
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
8697
|
const cfg = await loadConfig();
|
|
8335
8698
|
let target;
|
|
8336
8699
|
try {
|
|
@@ -8380,7 +8743,19 @@ project.command("readiness [owner/repo]").description("update the repo v2 readin
|
|
|
8380
8743
|
const issue2 = await updateV2ReadinessIssue(target, report, []);
|
|
8381
8744
|
console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
|
|
8382
8745
|
});
|
|
8383
|
-
project.command("
|
|
8746
|
+
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) => {
|
|
8747
|
+
const cfg = await loadConfig();
|
|
8748
|
+
let target;
|
|
8749
|
+
try {
|
|
8750
|
+
target = await projectTarget("project attest", repoOrSlug);
|
|
8751
|
+
} catch (e) {
|
|
8752
|
+
return fail(e.message);
|
|
8753
|
+
}
|
|
8754
|
+
const repo = target.includes("/") ? target : `mutmutco/${slugOf(target)}`;
|
|
8755
|
+
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
8756
|
+
reportWrite("project attest", res);
|
|
8757
|
+
});
|
|
8758
|
+
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, requiredRuntimeSecrets (JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]})').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
8759
|
const cfg = await loadConfig();
|
|
8385
8760
|
let target;
|
|
8386
8761
|
try {
|
|
@@ -8484,7 +8859,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8484
8859
|
let priority;
|
|
8485
8860
|
let body;
|
|
8486
8861
|
try {
|
|
8487
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
8862
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
|
|
8488
8863
|
priority = normalizePriority(o.priority);
|
|
8489
8864
|
args = buildIssueArgs({ type: o.type, title: o.title, body, priority, repo: o.repo, labels: o.label });
|
|
8490
8865
|
} catch (e) {
|
|
@@ -8494,7 +8869,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8494
8869
|
const la = ["label", "create", label, "--color", "ededed"];
|
|
8495
8870
|
if (o.repo) la.push("--repo", o.repo);
|
|
8496
8871
|
try {
|
|
8497
|
-
await
|
|
8872
|
+
await execFileP4("gh", la, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8498
8873
|
} catch {
|
|
8499
8874
|
}
|
|
8500
8875
|
}
|
|
@@ -8509,7 +8884,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8509
8884
|
const repo = await resolveRepo(o.repo);
|
|
8510
8885
|
if (!repo) return fail("issue discover-related: could not resolve repo");
|
|
8511
8886
|
try {
|
|
8512
|
-
const issues = await
|
|
8887
|
+
const issues = await ghJson([
|
|
8513
8888
|
"issue",
|
|
8514
8889
|
"list",
|
|
8515
8890
|
"--repo",
|
|
@@ -8524,7 +8899,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8524
8899
|
const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
|
|
8525
8900
|
if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
|
|
8526
8901
|
if (!candidates.length) return;
|
|
8527
|
-
const viewed = await
|
|
8902
|
+
const viewed = await ghJson([
|
|
8528
8903
|
"issue",
|
|
8529
8904
|
"view",
|
|
8530
8905
|
String(number),
|
|
@@ -8534,18 +8909,18 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8534
8909
|
"comments"
|
|
8535
8910
|
]);
|
|
8536
8911
|
if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
|
|
8537
|
-
await
|
|
8912
|
+
await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8538
8913
|
} catch {
|
|
8539
8914
|
}
|
|
8540
8915
|
});
|
|
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) => {
|
|
8916
|
+
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
8917
|
let body;
|
|
8543
8918
|
let priority;
|
|
8544
8919
|
let args;
|
|
8545
8920
|
const targetRepo2 = o.repo ?? HUB_REPO;
|
|
8546
8921
|
const sourceRepo = await resolveRepo(void 0);
|
|
8547
8922
|
try {
|
|
8548
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
8923
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
|
|
8549
8924
|
priority = normalizePriority(o.priority);
|
|
8550
8925
|
args = buildIssueArgs({
|
|
8551
8926
|
type: o.type,
|
|
@@ -8561,7 +8936,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8561
8936
|
if (!o.force) {
|
|
8562
8937
|
let openReports = [];
|
|
8563
8938
|
try {
|
|
8564
|
-
openReports = await
|
|
8939
|
+
openReports = await ghJson([
|
|
8565
8940
|
"issue",
|
|
8566
8941
|
"list",
|
|
8567
8942
|
"--repo",
|
|
@@ -8580,7 +8955,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8580
8955
|
const dup = findDuplicateReport({ title: o.title, body }, openReports);
|
|
8581
8956
|
if (dup) {
|
|
8582
8957
|
try {
|
|
8583
|
-
await
|
|
8958
|
+
await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8584
8959
|
} catch (e) {
|
|
8585
8960
|
const err = e;
|
|
8586
8961
|
return fail(`report: duplicate of #${dup.number} (${dup.url}) but the +1 comment failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
@@ -8589,7 +8964,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8589
8964
|
}
|
|
8590
8965
|
}
|
|
8591
8966
|
try {
|
|
8592
|
-
await
|
|
8967
|
+
await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8593
8968
|
} catch {
|
|
8594
8969
|
}
|
|
8595
8970
|
const created = await ghCreate(args);
|
|
@@ -8597,14 +8972,20 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8597
8972
|
console.log(JSON.stringify({ ...created, deduped: false, label: REPORT_LABEL, priority, projectItemId }));
|
|
8598
8973
|
});
|
|
8599
8974
|
var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
|
|
8600
|
-
pr.command("create").description("create a PR and print {number,url} JSON").requiredOption("--title <title>", "PR title").
|
|
8601
|
-
|
|
8975
|
+
pr.command("create").description("create a PR and print {number,url} JSON").requiredOption("--title <title>", "PR title").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
8976
|
+
let body;
|
|
8977
|
+
try {
|
|
8978
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
|
|
8979
|
+
} catch (e) {
|
|
8980
|
+
return fail(`pr create: ${e.message}`);
|
|
8981
|
+
}
|
|
8982
|
+
const created = await ghCreate(buildPrArgs({ title: o.title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
8602
8983
|
console.log(JSON.stringify(created));
|
|
8603
8984
|
});
|
|
8604
8985
|
async function remoteBranchExists(branch) {
|
|
8605
8986
|
if (!branch) return void 0;
|
|
8606
8987
|
try {
|
|
8607
|
-
const { stdout } = await
|
|
8988
|
+
const { stdout } = await execFileP4("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS });
|
|
8608
8989
|
return stdout.trim().length > 0;
|
|
8609
8990
|
} catch {
|
|
8610
8991
|
return void 0;
|
|
@@ -8613,20 +8994,22 @@ async function remoteBranchExists(branch) {
|
|
|
8613
8994
|
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
8995
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
8615
8996
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
8616
|
-
const headRef = (await
|
|
8617
|
-
const startingPath = (await
|
|
8997
|
+
const headRef = (await execFileP4("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
8998
|
+
const startingPath = (await execFileP4("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
8618
8999
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
8619
|
-
(await
|
|
9000
|
+
(await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
8620
9001
|
);
|
|
8621
9002
|
const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
|
|
8622
9003
|
let remoteDeleteAttempted = false;
|
|
8623
9004
|
let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
|
|
8624
|
-
await
|
|
9005
|
+
await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
|
|
8625
9006
|
const message = String(e.message || "");
|
|
8626
9007
|
if (/already been merged/i.test(message)) {
|
|
8627
9008
|
remoteNotAttemptedReason = "pr-already-merged";
|
|
8628
9009
|
return;
|
|
8629
9010
|
}
|
|
9011
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
9012
|
+
if (note) throw new Error(`gh pr merge ${number}: ${note}`);
|
|
8630
9013
|
if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
|
|
8631
9014
|
});
|
|
8632
9015
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
@@ -8644,7 +9027,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
8644
9027
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
8645
9028
|
beforeWorktrees,
|
|
8646
9029
|
startingPath,
|
|
8647
|
-
execGit: async (args) => (await
|
|
9030
|
+
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
8648
9031
|
});
|
|
8649
9032
|
console.log(JSON.stringify({
|
|
8650
9033
|
merged: number,
|
|
@@ -8767,9 +9150,9 @@ function stageKeepAlive() {
|
|
|
8767
9150
|
return setTimeout(() => void 0, 5 * 60 * 1e3);
|
|
8768
9151
|
}
|
|
8769
9152
|
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
|
-
const path2 = (0,
|
|
9153
|
+
const path2 = (0, import_node_path6.join)(process.cwd(), "infra", "port-ranges.json");
|
|
8771
9154
|
const allocate = async (seed) => {
|
|
8772
|
-
const { stdout } = await
|
|
9155
|
+
const { stdout } = await execFileP4("node", [(0, import_node_path6.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
8773
9156
|
const parsed = JSON.parse(stdout);
|
|
8774
9157
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
8775
9158
|
return parsed.range;
|
|
@@ -8875,8 +9258,8 @@ for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
|
8875
9258
|
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
8876
9259
|
try {
|
|
8877
9260
|
const result = await runTrainApply(commandName, {
|
|
8878
|
-
run: async (file, args) => (await
|
|
8879
|
-
runSelf: async (args) => (await
|
|
9261
|
+
run: async (file, args) => (await execFileP4(file, args, { timeout: file === "gh" ? 3e4 : GIT_TIMEOUT_MS })).stdout,
|
|
9262
|
+
runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
|
|
8880
9263
|
trainAuthority: async (repo) => {
|
|
8881
9264
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
8882
9265
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
@@ -8907,7 +9290,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
8907
9290
|
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
8908
9291
|
const meta = await fetchProjectBySlug(slug, { baseUrl: cfg.sagaApiUrl, token: githubToken });
|
|
8909
9292
|
const report = await verifyBootstrap(repo, o.class, {
|
|
8910
|
-
|
|
9293
|
+
client: defaultGitHubClient(),
|
|
8911
9294
|
projectMeta: meta,
|
|
8912
9295
|
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs5.existsSync)(path2) ? (0, import_node_fs5.readFileSync)(path2, "utf8") : null,
|
|
8913
9296
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
@@ -8920,7 +9303,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
8920
9303
|
})(),
|
|
8921
9304
|
listEnabledGcpApis: async (gcpProject) => {
|
|
8922
9305
|
try {
|
|
8923
|
-
const { stdout } = await
|
|
9306
|
+
const { stdout } = await execFileP4(
|
|
8924
9307
|
"gcloud",
|
|
8925
9308
|
["services", "list", "--enabled", "--project", gcpProject, "--format", "value(config.name)"],
|
|
8926
9309
|
{ timeout: 3e4 }
|
|
@@ -8954,7 +9337,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
8954
9337
|
const manifest = loadBootstrapSeeds((0, import_node_fs5.readFileSync)(manifestPath, "utf8"));
|
|
8955
9338
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
8956
9339
|
const slug = parsedRepo.slug;
|
|
8957
|
-
const gh = async (args) =>
|
|
9340
|
+
const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
|
|
8958
9341
|
const readFile2 = (p) => (0, import_node_fs5.existsSync)(p) ? (0, import_node_fs5.readFileSync)(p, "utf8") : null;
|
|
8959
9342
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
8960
9343
|
const vars = {};
|
|
@@ -9071,7 +9454,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
9071
9454
|
});
|
|
9072
9455
|
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
9456
|
const o = { json: rawFlag("--json"), repo: rawValue("--repo", ""), class: rawValue("--class", "deployable") };
|
|
9074
|
-
const deps = {
|
|
9457
|
+
const deps = { client: defaultGitHubClient() };
|
|
9075
9458
|
let targets;
|
|
9076
9459
|
const cfg = await loadConfig();
|
|
9077
9460
|
const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -9097,7 +9480,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
9097
9480
|
var isWin = process.platform === "win32";
|
|
9098
9481
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
9099
9482
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
9100
|
-
return (0,
|
|
9483
|
+
return (0, import_node_path6.join)((0, import_node_os2.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
9101
9484
|
};
|
|
9102
9485
|
function readInstalledPlugins() {
|
|
9103
9486
|
try {
|
|
@@ -9108,7 +9491,7 @@ function readInstalledPlugins() {
|
|
|
9108
9491
|
}
|
|
9109
9492
|
function readClaudeSettings() {
|
|
9110
9493
|
try {
|
|
9111
|
-
return JSON.parse((0, import_node_fs5.readFileSync)((0,
|
|
9494
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path6.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
9112
9495
|
} catch {
|
|
9113
9496
|
return null;
|
|
9114
9497
|
}
|
|
@@ -9154,14 +9537,14 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
9154
9537
|
}
|
|
9155
9538
|
function mmiPluginCacheRootSnapshots() {
|
|
9156
9539
|
const roots = [
|
|
9157
|
-
{ surface: "claude", root: (0,
|
|
9158
|
-
{ surface: "codex", root: (0,
|
|
9540
|
+
{ surface: "claude", root: (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
9541
|
+
{ surface: "codex", root: (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
9159
9542
|
];
|
|
9160
9543
|
return roots.flatMap(({ surface, root }) => {
|
|
9161
9544
|
try {
|
|
9162
9545
|
const entries = (0, import_node_fs5.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
9163
9546
|
name: entry.name,
|
|
9164
|
-
path: (0,
|
|
9547
|
+
path: (0, import_node_path6.join)(root, entry.name),
|
|
9165
9548
|
isDirectory: entry.isDirectory()
|
|
9166
9549
|
}));
|
|
9167
9550
|
return [{ surface, root, entries }];
|
|
@@ -9184,7 +9567,7 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
9184
9567
|
try {
|
|
9185
9568
|
if (!(0, import_node_fs5.existsSync)(move.from)) continue;
|
|
9186
9569
|
const target = uniqueQuarantineTarget(move.to);
|
|
9187
|
-
(0, import_node_fs5.mkdirSync)((0,
|
|
9570
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path6.dirname)(target), { recursive: true });
|
|
9188
9571
|
(0, import_node_fs5.renameSync)(move.from, target);
|
|
9189
9572
|
moved += 1;
|
|
9190
9573
|
} catch {
|
|
@@ -9192,7 +9575,7 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
9192
9575
|
}
|
|
9193
9576
|
return moved;
|
|
9194
9577
|
}
|
|
9195
|
-
var gitignorePath = () => (0,
|
|
9578
|
+
var gitignorePath = () => (0, import_node_path6.join)(process.cwd(), ".gitignore");
|
|
9196
9579
|
function readGitignore() {
|
|
9197
9580
|
try {
|
|
9198
9581
|
return (0, import_node_fs5.readFileSync)(gitignorePath(), "utf8");
|
|
@@ -9219,17 +9602,17 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9219
9602
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
9220
9603
|
const [login, pathProbe, releasedVersion, cfg, callerArn, cloneProbe] = await Promise.all([
|
|
9221
9604
|
githubLogin(),
|
|
9222
|
-
|
|
9605
|
+
execFileP4(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
|
|
9223
9606
|
fetchReleasedVersion(),
|
|
9224
9607
|
loadConfig(),
|
|
9225
9608
|
awsCallerArn(),
|
|
9226
|
-
|
|
9609
|
+
execFileP4("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false)
|
|
9227
9610
|
// unset → repair below
|
|
9228
9611
|
]);
|
|
9229
9612
|
let ghInstalled = true;
|
|
9230
9613
|
if (!login) {
|
|
9231
9614
|
try {
|
|
9232
|
-
await
|
|
9615
|
+
await execFileP4("gh", ["--version"]);
|
|
9233
9616
|
} catch {
|
|
9234
9617
|
ghInstalled = false;
|
|
9235
9618
|
}
|
|
@@ -9256,7 +9639,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9256
9639
|
let cloneOk = cloneProbe;
|
|
9257
9640
|
if (!cloneOk && !opts.banner && !opts.json) {
|
|
9258
9641
|
try {
|
|
9259
|
-
await
|
|
9642
|
+
await execFileP4("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
9260
9643
|
cloneOk = true;
|
|
9261
9644
|
io.err(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
|
|
9262
9645
|
} catch {
|
|
@@ -9355,24 +9738,22 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9355
9738
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
9356
9739
|
}
|
|
9357
9740
|
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output").action((opts) => runDoctor(opts));
|
|
9358
|
-
program2.command("session-start").description("run the SessionStart verbs (rules
|
|
9741
|
+
program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, doctor) in one process; docs sync runs detached").action(async () => {
|
|
9359
9742
|
try {
|
|
9360
9743
|
const hook = parseHookInput(await readStdin());
|
|
9361
9744
|
if (hook.session_id) persistSession(hook.session_id);
|
|
9362
9745
|
} catch (e) {
|
|
9363
9746
|
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
9364
9747
|
}
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
const sequential = [
|
|
9373
|
-
{ name: "doctor", run: (io) => runDoctor({ banner: true }, io) }
|
|
9374
|
-
];
|
|
9748
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process6.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
9749
|
+
const { parallel, sequential } = buildSessionStartPlan({
|
|
9750
|
+
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
9751
|
+
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
9752
|
+
sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
|
|
9753
|
+
doctor: (io) => runDoctor({ banner: true }, io)
|
|
9754
|
+
});
|
|
9375
9755
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
9756
|
+
consoleIo.log(northstarPointer());
|
|
9376
9757
|
});
|
|
9377
9758
|
function fail(msg) {
|
|
9378
9759
|
console.error(`mmi-cli ${msg}`);
|