@mutmutco/cli 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.cjs +768 -98
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -10,6 +10,12 @@ This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MM
|
|
|
10
10
|
npm install -g @mutmutco/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Authenticate GitHub once for saga and Project board operations:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
gh auth login --hostname github.com --git-protocol https --web --scopes "project"
|
|
17
|
+
```
|
|
18
|
+
|
|
13
19
|
Then verify the installed command:
|
|
14
20
|
|
|
15
21
|
```powershell
|
package/dist/index.cjs
CHANGED
|
@@ -3038,7 +3038,7 @@ var {
|
|
|
3038
3038
|
|
|
3039
3039
|
// src/index.ts
|
|
3040
3040
|
var import_promises = require("node:fs/promises");
|
|
3041
|
-
var
|
|
3041
|
+
var import_node_fs3 = require("node:fs");
|
|
3042
3042
|
var import_node_crypto = require("node:crypto");
|
|
3043
3043
|
|
|
3044
3044
|
// src/rules-sync.ts
|
|
@@ -3052,6 +3052,18 @@ function needsUpdate(source, current) {
|
|
|
3052
3052
|
function isRulesSource(orgRulesSource) {
|
|
3053
3053
|
return orgRulesSource === "self";
|
|
3054
3054
|
}
|
|
3055
|
+
function rulesSourceAuthHeaders(sourceUrl, token) {
|
|
3056
|
+
if (!token) return void 0;
|
|
3057
|
+
try {
|
|
3058
|
+
const host = new URL(sourceUrl).hostname.toLowerCase();
|
|
3059
|
+
if (host === "raw.githubusercontent.com" || host === "api.github.com") {
|
|
3060
|
+
return { Authorization: `Bearer ${token}` };
|
|
3061
|
+
}
|
|
3062
|
+
} catch {
|
|
3063
|
+
return void 0;
|
|
3064
|
+
}
|
|
3065
|
+
return void 0;
|
|
3066
|
+
}
|
|
3055
3067
|
|
|
3056
3068
|
// src/saga-capture.ts
|
|
3057
3069
|
function parseHookInput(stdin) {
|
|
@@ -3064,9 +3076,9 @@ function parseHookInput(stdin) {
|
|
|
3064
3076
|
}
|
|
3065
3077
|
|
|
3066
3078
|
// src/index.ts
|
|
3067
|
-
var
|
|
3068
|
-
var
|
|
3069
|
-
var
|
|
3079
|
+
var import_node_child_process4 = require("node:child_process");
|
|
3080
|
+
var import_node_util3 = require("node:util");
|
|
3081
|
+
var import_node_path4 = require("node:path");
|
|
3070
3082
|
|
|
3071
3083
|
// src/saga-head-maintainer.ts
|
|
3072
3084
|
var import_node_child_process = require("node:child_process");
|
|
@@ -3099,13 +3111,18 @@ function markHeadRun(path, now = Date.now()) {
|
|
|
3099
3111
|
}
|
|
3100
3112
|
function headPrompt(state) {
|
|
3101
3113
|
return [
|
|
3102
|
-
"You maintain
|
|
3103
|
-
"
|
|
3104
|
-
"
|
|
3105
|
-
"
|
|
3106
|
-
"
|
|
3114
|
+
"You maintain ONE durable slot of a work-session: PINNED (things worth remembering). Given the CURRENT",
|
|
3115
|
+
"HEAD and the recent TRANSCRIPT + DECISIONS, return an updated PINNED only. Keep it tight and concrete;",
|
|
3116
|
+
"keep anything the user pinned; never invent; preserve Turkish characters (\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do",
|
|
3117
|
+
"NOT manage next or the checklist \u2014 the note path owns those. The ANCHOR is the read-only North-Star \u2014",
|
|
3118
|
+
"NEVER change it. Never restate an unverified artifact-claim (a named file, PR, flag, or board state)",
|
|
3107
3119
|
"as settled fact \u2014 keep it as the belief it was recorded as.",
|
|
3108
|
-
|
|
3120
|
+
"You MAY also propose supersessions: each DECISION is shown with a stable 0-based index. Propose a",
|
|
3121
|
+
"supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
|
|
3122
|
+
"already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
|
|
3123
|
+
"is now false or obsolete; the newer decision's timestamp MUST be later than the older's (newer-supersedes-",
|
|
3124
|
+
"older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor, next, or checklist.",
|
|
3125
|
+
'Output ONLY a JSON object: {"pinned":[string],"supersede":[{"older":int,"newer":int,"reason":string}]}.',
|
|
3109
3126
|
"",
|
|
3110
3127
|
"CURRENT HEAD:",
|
|
3111
3128
|
JSON.stringify(state.head ?? {}, null, 2),
|
|
@@ -3114,7 +3131,7 @@ function headPrompt(state) {
|
|
|
3114
3131
|
JSON.stringify(state.actionLog ?? []),
|
|
3115
3132
|
"",
|
|
3116
3133
|
"DECISIONS:",
|
|
3117
|
-
(state.decisions ?? []).map((d) =>
|
|
3134
|
+
(state.decisions ?? []).map((d, i) => `[${i}] ${d}`).join("\n") || "(none)"
|
|
3118
3135
|
].join("\n");
|
|
3119
3136
|
}
|
|
3120
3137
|
function parseHeadUpdate(raw) {
|
|
@@ -3128,16 +3145,23 @@ function parseHeadUpdate(raw) {
|
|
|
3128
3145
|
}
|
|
3129
3146
|
if (!obj || typeof obj !== "object") return null;
|
|
3130
3147
|
const u = {};
|
|
3131
|
-
if (typeof obj.goal === "string") u.goal = obj.goal;
|
|
3132
3148
|
if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
|
|
3149
|
+
if (Array.isArray(obj.supersede)) {
|
|
3150
|
+
const supersede = obj.supersede.filter((e) => {
|
|
3151
|
+
if (!e || typeof e !== "object") return false;
|
|
3152
|
+
const { older, newer, reason } = e;
|
|
3153
|
+
return Number.isInteger(older) && older >= 0 && Number.isInteger(newer) && newer >= 0 && older !== newer && typeof reason === "string" && reason.length > 0;
|
|
3154
|
+
}).map((e) => ({ older: e.older, newer: e.newer, reason: e.reason }));
|
|
3155
|
+
if (supersede.length) u.supersede = supersede;
|
|
3156
|
+
}
|
|
3133
3157
|
return Object.keys(u).length ? u : null;
|
|
3134
3158
|
}
|
|
3135
3159
|
async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
3136
|
-
const { cmd, args, shell } = resolveEngine(process.platform, process.env.SAGA_HEAD_ENGINE);
|
|
3160
|
+
const { cmd, args, shell: shell2 } = resolveEngine(process.platform, process.env.SAGA_HEAD_ENGINE);
|
|
3137
3161
|
return await new Promise((resolve) => {
|
|
3138
3162
|
let child;
|
|
3139
3163
|
try {
|
|
3140
|
-
child = (0, import_node_child_process.spawn)(cmd, args, { shell, windowsHide: true });
|
|
3164
|
+
child = (0, import_node_child_process.spawn)(cmd, args, { shell: shell2, windowsHide: true });
|
|
3141
3165
|
} catch {
|
|
3142
3166
|
return resolve("");
|
|
3143
3167
|
}
|
|
@@ -3246,11 +3270,12 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
3246
3270
|
const queueOp = o.queueAdd ? { op: "add", text: o.queueAdd } : o.queueDone != null ? { op: "done", index: Number(o.queueDone) } : void 0;
|
|
3247
3271
|
const state = o.diagnostic ? "diagnostic" : o.verified ? "verified" : "asserted";
|
|
3248
3272
|
const source = o.diagnostic ? "probe" : "note";
|
|
3249
|
-
const
|
|
3250
|
-
if (evidence.sha)
|
|
3251
|
-
if (evidence.branch)
|
|
3252
|
-
if (evidence.pr)
|
|
3253
|
-
if (evidence.file)
|
|
3273
|
+
const ev = {};
|
|
3274
|
+
if (evidence.sha) ev.sha = evidence.sha;
|
|
3275
|
+
if (evidence.branch) ev.branch = evidence.branch;
|
|
3276
|
+
if (evidence.pr) ev.pr = evidence.pr;
|
|
3277
|
+
if (evidence.file) ev.file = evidence.file;
|
|
3278
|
+
const anchor = o.anchor ? { intent: o.anchor, setAt: (/* @__PURE__ */ new Date()).toISOString() } : void 0;
|
|
3254
3279
|
return {
|
|
3255
3280
|
event: "note",
|
|
3256
3281
|
id,
|
|
@@ -3260,14 +3285,17 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
3260
3285
|
queueOp,
|
|
3261
3286
|
state,
|
|
3262
3287
|
source,
|
|
3263
|
-
evidence: Object.keys(
|
|
3264
|
-
surface: process.env.MMI_AGENT_SURFACE || "claude"
|
|
3288
|
+
evidence: Object.keys(ev).length ? ev : void 0,
|
|
3289
|
+
surface: process.env.MMI_AGENT_SURFACE || "claude",
|
|
3290
|
+
supersedes: o.supersedes,
|
|
3291
|
+
anchor,
|
|
3292
|
+
anchorForce: o.anchorForce || void 0
|
|
3265
3293
|
};
|
|
3266
3294
|
}
|
|
3267
3295
|
|
|
3268
3296
|
// src/version-lag.ts
|
|
3269
3297
|
var VERSION_LABEL = "installed plugin/adapter cache freshness";
|
|
3270
|
-
var VERSION_FIX = "
|
|
3298
|
+
var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
|
|
3271
3299
|
function parseVersion(v) {
|
|
3272
3300
|
return v.replace(/^v/, "").split(/[.-]/).slice(0, 3).map((part) => {
|
|
3273
3301
|
const n = Number.parseInt(part, 10);
|
|
@@ -3824,41 +3852,41 @@ function parseWorktreePorcelain(stdout) {
|
|
|
3824
3852
|
}
|
|
3825
3853
|
return out;
|
|
3826
3854
|
}
|
|
3827
|
-
function formatGcPlan(
|
|
3855
|
+
function formatGcPlan(plan2, apply) {
|
|
3828
3856
|
const lines = [`mmi-cli gc: ${apply ? "apply" : "dry-run"}`];
|
|
3829
|
-
if (!
|
|
3830
|
-
if (
|
|
3857
|
+
if (!plan2.branches.length && !plan2.trackingRefs.length) lines.push("nothing to clean");
|
|
3858
|
+
if (plan2.branches.length) {
|
|
3831
3859
|
lines.push("local branches:");
|
|
3832
|
-
for (const b of
|
|
3860
|
+
for (const b of plan2.branches) {
|
|
3833
3861
|
const prs = b.prNumbers.length ? ` #${b.prNumbers.join(",#")}` : "";
|
|
3834
3862
|
const wt = b.worktreePath ? ` (worktree: ${b.worktreePath})` : "";
|
|
3835
3863
|
lines.push(` - ${b.branch} (${b.prState}${prs})${wt}`);
|
|
3836
3864
|
}
|
|
3837
3865
|
}
|
|
3838
|
-
if (
|
|
3866
|
+
if (plan2.trackingRefs.length) {
|
|
3839
3867
|
lines.push("stale tracking refs:");
|
|
3840
|
-
for (const r of
|
|
3868
|
+
for (const r of plan2.trackingRefs) {
|
|
3841
3869
|
const prs = r.prNumbers.length ? ` #${r.prNumbers.join(",#")}` : "";
|
|
3842
3870
|
lines.push(` - ${r.ref} (${r.prState}${prs})`);
|
|
3843
3871
|
}
|
|
3844
3872
|
}
|
|
3845
|
-
if (
|
|
3873
|
+
if (plan2.skipped.length) {
|
|
3846
3874
|
lines.push("skipped:");
|
|
3847
|
-
for (const s of
|
|
3875
|
+
for (const s of plan2.skipped) {
|
|
3848
3876
|
lines.push(` - ${s.branch}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
|
|
3849
3877
|
}
|
|
3850
3878
|
}
|
|
3851
|
-
if (!apply && (
|
|
3879
|
+
if (!apply && (plan2.branches.length || plan2.trackingRefs.length)) lines.push("rerun with --apply to delete only the listed items");
|
|
3852
3880
|
return lines.join("\n");
|
|
3853
3881
|
}
|
|
3854
3882
|
|
|
3855
3883
|
// src/command-plans.ts
|
|
3856
|
-
function stagePlan(
|
|
3884
|
+
function stagePlan(stage2 = {}) {
|
|
3857
3885
|
return [
|
|
3858
3886
|
{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" },
|
|
3859
|
-
{ label: "run local build", command:
|
|
3860
|
-
{ label: "start local stage", command:
|
|
3861
|
-
{ label: "check health", command:
|
|
3887
|
+
{ label: "run local build", command: stage2.build || "(no stage.build configured)" },
|
|
3888
|
+
{ label: "start local stage", command: stage2.up || "(no stage.up configured)" },
|
|
3889
|
+
{ label: "check health", command: stage2.healthUrl ? `curl --fail ${stage2.healthUrl}` : "(no stage.healthUrl configured)" }
|
|
3862
3890
|
];
|
|
3863
3891
|
}
|
|
3864
3892
|
function trainPlan(command) {
|
|
@@ -3886,26 +3914,492 @@ function trainPlan(command) {
|
|
|
3886
3914
|
];
|
|
3887
3915
|
}
|
|
3888
3916
|
function bootstrapPlan(repo, repoClass) {
|
|
3889
|
-
const
|
|
3917
|
+
const branchModel = repoClass === "content" ? "content repo: main only" : "deployable repo: development, rc, main";
|
|
3918
|
+
const protectedBranches = repoClass === "content" ? "main" : "development, rc, main";
|
|
3890
3919
|
return [
|
|
3891
3920
|
{ label: `create or inspect ${repo}` },
|
|
3892
|
-
{ label: `provision
|
|
3893
|
-
{ label:
|
|
3894
|
-
{ label: "attach GitHub Project v2 and write .mmi/config.json", gated: true },
|
|
3895
|
-
{ label: "seed README.md and
|
|
3896
|
-
{ label: "
|
|
3921
|
+
{ label: `provision branch model: ${branchModel}`, gated: true },
|
|
3922
|
+
{ label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
|
|
3923
|
+
{ label: "attach GitHub Project v2 and write saga-ready .mmi/config.json", gated: true },
|
|
3924
|
+
{ label: "seed README.md, architecture.md, AGENTS.md, and CLAUDE.md", gated: true },
|
|
3925
|
+
{ label: "commit .claude/settings.json and .cursor/rules/<repo>.mdc", gated: true },
|
|
3926
|
+
{ label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
|
|
3897
3927
|
];
|
|
3898
3928
|
}
|
|
3899
3929
|
|
|
3900
|
-
// src/
|
|
3930
|
+
// src/doctor.ts
|
|
3931
|
+
var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
|
|
3932
|
+
function buildGithubAuthCheck(input) {
|
|
3933
|
+
const ok = Boolean(input.login?.trim());
|
|
3934
|
+
return {
|
|
3935
|
+
ok,
|
|
3936
|
+
label: "GitHub auth identity (saga + gh ops)",
|
|
3937
|
+
fix: input.ghInstalled ? GH_PROJECT_LOGIN_FIX : `install GitHub CLI (https://cli.github.com), then: ${GH_PROJECT_LOGIN_FIX.replace(/^run: /, "")}`
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
// src/stage-runner.ts
|
|
3942
|
+
var import_node_child_process3 = require("node:child_process");
|
|
3943
|
+
var import_node_fs2 = require("node:fs");
|
|
3944
|
+
var import_node_path2 = require("node:path");
|
|
3945
|
+
var import_node_util2 = require("node:util");
|
|
3901
3946
|
var execFileP2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
3947
|
+
function stageStatePath(cwd = process.cwd()) {
|
|
3948
|
+
return (0, import_node_path2.join)(cwd, "tmp", "stage", "state.json");
|
|
3949
|
+
}
|
|
3950
|
+
function validateStageConfig(config = {}, action) {
|
|
3951
|
+
const problems = [];
|
|
3952
|
+
if (action === "run" && !config.build?.trim()) problems.push("stage.build is required for stage run");
|
|
3953
|
+
if (!config.up?.trim()) problems.push("stage.up is required to start the local stage");
|
|
3954
|
+
if (config.healthUrl != null && config.healthUrl.trim() && !/^https?:\/\//.test(config.healthUrl.trim())) {
|
|
3955
|
+
problems.push("stage.healthUrl must be an http(s) URL");
|
|
3956
|
+
}
|
|
3957
|
+
return problems;
|
|
3958
|
+
}
|
|
3959
|
+
async function shell(command, cwd, timeoutMs) {
|
|
3960
|
+
await execFileP2(command, [], {
|
|
3961
|
+
cwd,
|
|
3962
|
+
shell: true,
|
|
3963
|
+
timeout: timeoutMs,
|
|
3964
|
+
windowsHide: true,
|
|
3965
|
+
maxBuffer: 1024 * 1024 * 4
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
function readState(path) {
|
|
3969
|
+
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
3970
|
+
try {
|
|
3971
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
3972
|
+
} catch {
|
|
3973
|
+
return null;
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
async function killTree(pid) {
|
|
3977
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
3978
|
+
if (process.platform === "win32") {
|
|
3979
|
+
await execFileP2("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
try {
|
|
3983
|
+
process.kill(-pid, "SIGTERM");
|
|
3984
|
+
} catch {
|
|
3985
|
+
try {
|
|
3986
|
+
process.kill(pid, "SIGTERM");
|
|
3987
|
+
} catch {
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3991
|
+
try {
|
|
3992
|
+
process.kill(-pid, "SIGKILL");
|
|
3993
|
+
} catch {
|
|
3994
|
+
try {
|
|
3995
|
+
process.kill(pid, "SIGKILL");
|
|
3996
|
+
} catch {
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
async function waitForHealth(url, timeoutMs) {
|
|
4001
|
+
const deadline = Date.now() + timeoutMs;
|
|
4002
|
+
let last = "";
|
|
4003
|
+
while (Date.now() < deadline) {
|
|
4004
|
+
try {
|
|
4005
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(Math.min(5e3, timeoutMs)) });
|
|
4006
|
+
if (res.ok) return;
|
|
4007
|
+
last = `HTTP ${res.status}`;
|
|
4008
|
+
} catch (e) {
|
|
4009
|
+
last = e.message;
|
|
4010
|
+
}
|
|
4011
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
4012
|
+
}
|
|
4013
|
+
throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
|
|
4014
|
+
}
|
|
4015
|
+
async function stopStage(opts = {}) {
|
|
4016
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
4017
|
+
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
4018
|
+
const state = readState(statePath);
|
|
4019
|
+
if (!state) {
|
|
4020
|
+
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
4021
|
+
}
|
|
4022
|
+
await killTree(state.pid);
|
|
4023
|
+
(0, import_node_fs2.rmSync)(statePath, { force: true });
|
|
4024
|
+
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
4025
|
+
}
|
|
4026
|
+
async function startStage(config = {}, opts = {}) {
|
|
4027
|
+
const problems = validateStageConfig(config, "start");
|
|
4028
|
+
if (problems.length) throw new Error(problems.join("; "));
|
|
4029
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
4030
|
+
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
4031
|
+
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
4032
|
+
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
4033
|
+
const up = config.up.trim();
|
|
4034
|
+
const child = (0, import_node_child_process3.spawn)(up, {
|
|
4035
|
+
cwd,
|
|
4036
|
+
shell: true,
|
|
4037
|
+
detached: true,
|
|
4038
|
+
windowsHide: true,
|
|
4039
|
+
stdio: "ignore"
|
|
4040
|
+
});
|
|
4041
|
+
const state = {
|
|
4042
|
+
pid: child.pid ?? 0,
|
|
4043
|
+
command: up,
|
|
4044
|
+
cwd,
|
|
4045
|
+
startedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
4046
|
+
healthUrl: config.healthUrl?.trim() || void 0
|
|
4047
|
+
};
|
|
4048
|
+
(0, import_node_fs2.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
4049
|
+
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4);
|
|
4050
|
+
const result = { ok: true, action: "start", statePath, pid: state.pid, message: `started stage pid ${state.pid}` };
|
|
4051
|
+
opts.onReady?.(result);
|
|
4052
|
+
child.unref();
|
|
4053
|
+
return result;
|
|
4054
|
+
}
|
|
4055
|
+
async function runStage(config = {}, opts = {}) {
|
|
4056
|
+
const problems = validateStageConfig(config, "run");
|
|
4057
|
+
if (problems.length) throw new Error(problems.join("; "));
|
|
4058
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
4059
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
4060
|
+
await stopStage({ ...opts, cwd });
|
|
4061
|
+
await shell(config.build.trim(), cwd, timeoutMs);
|
|
4062
|
+
const started = await startStage(config, { ...opts, cwd, timeoutMs });
|
|
4063
|
+
return { ...started, action: "run", message: `built and ${started.message}` };
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
// src/bootstrap-verify.ts
|
|
4067
|
+
var requiredDocs = ["README.md", "architecture.md", "AGENTS.md", "CLAUDE.md", ".claude/settings.json", ".mmi/config.json"];
|
|
4068
|
+
var requiredIssueTemplates = [
|
|
4069
|
+
".github/ISSUE_TEMPLATE/bug.yml",
|
|
4070
|
+
".github/ISSUE_TEMPLATE/feature.yml",
|
|
4071
|
+
".github/ISSUE_TEMPLATE/task.yml",
|
|
4072
|
+
".github/ISSUE_TEMPLATE/config.yml"
|
|
4073
|
+
];
|
|
4074
|
+
var requiredWorkflows = [".github/workflows/pr-to-board.yml"];
|
|
4075
|
+
var requiredLabels = ["bug", "feature", "task", "priority:high", "priority:medium", "priority:low"];
|
|
4076
|
+
var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
|
|
4077
|
+
var requiredProjectWorkflows = [
|
|
4078
|
+
"Auto-add sub-issues to project",
|
|
4079
|
+
"Auto-close issue",
|
|
4080
|
+
"Item added to project",
|
|
4081
|
+
"Item closed",
|
|
4082
|
+
"Pull request linked to issue",
|
|
4083
|
+
"Pull request merged"
|
|
4084
|
+
];
|
|
4085
|
+
var requiredActionsVariables = ["MMI_APP_ID"];
|
|
4086
|
+
var requiredActionsSecrets = ["MMI_APP_PRIVATE_KEY"];
|
|
4087
|
+
function expectedBranches(repoClass) {
|
|
4088
|
+
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
4089
|
+
}
|
|
4090
|
+
function safeJson(text, fallback) {
|
|
4091
|
+
try {
|
|
4092
|
+
return JSON.parse(text);
|
|
4093
|
+
} catch {
|
|
4094
|
+
return fallback;
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
async function ghJson(deps, args, fallback) {
|
|
4098
|
+
try {
|
|
4099
|
+
return safeJson((await deps.gh(args)).stdout, fallback);
|
|
4100
|
+
} catch {
|
|
4101
|
+
return fallback;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
async function contentExists(deps, repo, branch, path) {
|
|
4105
|
+
try {
|
|
4106
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
4107
|
+
await deps.gh(["api", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`]);
|
|
4108
|
+
return true;
|
|
4109
|
+
} catch {
|
|
4110
|
+
return false;
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
async function contentText(deps, repo, branch, path) {
|
|
4114
|
+
try {
|
|
4115
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
4116
|
+
const response = safeJson(
|
|
4117
|
+
(await deps.gh(["api", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`])).stdout,
|
|
4118
|
+
{}
|
|
4119
|
+
);
|
|
4120
|
+
if (response.content == null) return null;
|
|
4121
|
+
if (response.encoding != null && response.encoding !== "base64") return null;
|
|
4122
|
+
return Buffer.from(response.content.replace(/\s/g, ""), "base64").toString("utf8");
|
|
4123
|
+
} catch {
|
|
4124
|
+
return null;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
async function protectedBranch(deps, repo, branch) {
|
|
4128
|
+
try {
|
|
4129
|
+
await deps.gh(["api", `repos/${repo}/branches/${branch}/protection`]);
|
|
4130
|
+
return true;
|
|
4131
|
+
} catch {
|
|
4132
|
+
return false;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
function optionDetail(missing) {
|
|
4136
|
+
return missing.length === 0 ? void 0 : `missing: ${missing.join(", ")}`;
|
|
4137
|
+
}
|
|
4138
|
+
function localRegistryCheck(deps, path, predicate) {
|
|
4139
|
+
const text = deps.readLocalFile?.(path);
|
|
4140
|
+
if (text == null) return null;
|
|
4141
|
+
return predicate(safeJson(text, null));
|
|
4142
|
+
}
|
|
4143
|
+
async function verifyBootstrap(repo, repoClass, deps) {
|
|
4144
|
+
const branchesWanted = expectedBranches(repoClass);
|
|
4145
|
+
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
4146
|
+
const checks = [];
|
|
4147
|
+
const repoInfo = await ghJson(deps, ["api", `repos/${repo}`], {});
|
|
4148
|
+
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
4149
|
+
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
4150
|
+
checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
|
|
4151
|
+
const branchList = await ghJson(deps, ["api", `repos/${repo}/branches`, "--paginate"], []);
|
|
4152
|
+
const branchNames = new Set(branchList.map((b) => b.name));
|
|
4153
|
+
for (const branch of branchesWanted) {
|
|
4154
|
+
checks.push({ ok: branchNames.has(branch), label: `branch exists: ${branch}` });
|
|
4155
|
+
checks.push({ ok: await protectedBranch(deps, repo, branch), label: `branch protection exists: ${branch}` });
|
|
4156
|
+
}
|
|
4157
|
+
for (const path of requiredDocs) {
|
|
4158
|
+
checks.push({ ok: await contentExists(deps, repo, baseBranch, path), label: `bootstrap artifact exists: ${path}` });
|
|
4159
|
+
}
|
|
4160
|
+
for (const path of requiredIssueTemplates) {
|
|
4161
|
+
checks.push({ ok: await contentExists(deps, repo, baseBranch, path), label: `issue template exists: ${path}` });
|
|
4162
|
+
}
|
|
4163
|
+
for (const path of requiredWorkflows) {
|
|
4164
|
+
checks.push({ ok: await contentExists(deps, repo, baseBranch, path), label: `automation workflow exists: ${path}` });
|
|
4165
|
+
}
|
|
4166
|
+
checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
|
|
4167
|
+
const labels = await ghJson(deps, ["label", "list", "--repo", repo, "--limit", "200", "--json", "name"], []);
|
|
4168
|
+
const labelNames = new Set(labels.map((l) => l.name));
|
|
4169
|
+
for (const label of requiredLabels) {
|
|
4170
|
+
checks.push({ ok: labelNames.has(label), label: `label exists: ${label}` });
|
|
4171
|
+
}
|
|
4172
|
+
const actions = await ghJson(deps, ["api", `repos/${repo}/actions/permissions`], {});
|
|
4173
|
+
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
4174
|
+
const variables = await ghJson(deps, ["variable", "list", "--repo", repo, "--json", "name"], []);
|
|
4175
|
+
const variableNames = new Set(variables.map((v) => v.name));
|
|
4176
|
+
for (const variable of requiredActionsVariables) {
|
|
4177
|
+
checks.push({ ok: variableNames.has(variable), label: `Actions variable exists: ${variable}` });
|
|
4178
|
+
}
|
|
4179
|
+
const secrets = await ghJson(deps, ["secret", "list", "--repo", repo, "--json", "name"], []);
|
|
4180
|
+
const secretNames = new Set(secrets.map((s) => s.name));
|
|
4181
|
+
for (const secret of requiredActionsSecrets) {
|
|
4182
|
+
checks.push({ ok: secretNames.has(secret), label: `Actions secret exists: ${secret}` });
|
|
4183
|
+
}
|
|
4184
|
+
const config = safeJson(await contentText(deps, repo, baseBranch, ".mmi/config.json") || "", null);
|
|
4185
|
+
checks.push({
|
|
4186
|
+
ok: Boolean(config?.projectOwner && config?.projectNumber),
|
|
4187
|
+
label: ".mmi project board config exists"
|
|
4188
|
+
});
|
|
4189
|
+
if (config?.projectOwner && config.projectNumber != null) {
|
|
4190
|
+
const project = await ghJson(
|
|
4191
|
+
deps,
|
|
4192
|
+
["project", "field-list", String(config.projectNumber), "--owner", config.projectOwner, "--format", "json"],
|
|
4193
|
+
{}
|
|
4194
|
+
);
|
|
4195
|
+
const fields = project.fields || [];
|
|
4196
|
+
const statusField = fields.find((field) => field.name === "Status");
|
|
4197
|
+
const labelField = fields.find((field) => field.name === "Labels");
|
|
4198
|
+
checks.push({
|
|
4199
|
+
ok: Boolean(statusField),
|
|
4200
|
+
label: `Project Status field exists: ${config.projectOwner}#${config.projectNumber}`
|
|
4201
|
+
});
|
|
4202
|
+
checks.push({
|
|
4203
|
+
ok: Boolean(labelField),
|
|
4204
|
+
label: `Project Labels field exists: ${config.projectOwner}#${config.projectNumber}`
|
|
4205
|
+
});
|
|
4206
|
+
if (statusField != null) {
|
|
4207
|
+
const optionNames = new Set((statusField.options || []).map((option) => option.name));
|
|
4208
|
+
const missingOptions = requiredStatusOptions.filter((option) => !optionNames.has(option));
|
|
4209
|
+
checks.push({
|
|
4210
|
+
ok: missingOptions.length === 0,
|
|
4211
|
+
label: "Project Status lanes configured",
|
|
4212
|
+
detail: optionDetail(missingOptions)
|
|
4213
|
+
});
|
|
4214
|
+
checks.push({
|
|
4215
|
+
ok: config.statusFieldId === statusField.id,
|
|
4216
|
+
label: ".mmi statusFieldId matches project"
|
|
4217
|
+
});
|
|
4218
|
+
for (const optionName of requiredStatusOptions) {
|
|
4219
|
+
const projectOption = statusField.options?.find((option) => option.name === optionName);
|
|
4220
|
+
checks.push({
|
|
4221
|
+
ok: Boolean(projectOption?.id && config.statusOptions?.[optionName] === projectOption.id),
|
|
4222
|
+
label: `.mmi status option matches project: ${optionName}`
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
const workflowQuery = "query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { workflows(first: 30) { nodes { name enabled } } } } }";
|
|
4227
|
+
const workflowResponse = await ghJson(
|
|
4228
|
+
deps,
|
|
4229
|
+
["api", "graphql", "-f", `query=${workflowQuery}`, "-f", `login=${config.projectOwner}`, "-F", `number=${config.projectNumber}`],
|
|
4230
|
+
{}
|
|
4231
|
+
);
|
|
4232
|
+
const workflows = workflowResponse.data?.organization?.projectV2?.workflows?.nodes || [];
|
|
4233
|
+
for (const workflowName of requiredProjectWorkflows) {
|
|
4234
|
+
checks.push({
|
|
4235
|
+
ok: workflows.some((workflow) => workflow.name === workflowName && workflow.enabled === true),
|
|
4236
|
+
label: `Project workflow enabled: ${workflowName}`
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
const fanout = repo === "mutmutco/MMI-Hub" ? true : localRegistryCheck(deps, ".github/fanout-targets.json", (json) => Array.isArray(json?.repos) && json.repos.some((r) => r.repo === repo.split("/")[1] && r.branch === baseBranch));
|
|
4241
|
+
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
4242
|
+
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && json.projects.some((p) => (p.repos || []).includes(repo)));
|
|
4243
|
+
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
|
|
4244
|
+
return { ok: checks.every((c) => c.ok), repo, class: repoClass, baseBranch, checks };
|
|
4245
|
+
}
|
|
4246
|
+
function renderBootstrapVerifyReport(report) {
|
|
4247
|
+
const lines = [`mmi-cli bootstrap verify: ${report.ok ? "OK" : "CHECK"} ${report.repo} (${report.class}, ${report.baseBranch})`];
|
|
4248
|
+
for (const check of report.checks) {
|
|
4249
|
+
lines.push(`${check.ok ? "OK" : "FAIL"} ${check.label}${check.detail ? ` - ${check.detail}` : ""}`);
|
|
4250
|
+
}
|
|
4251
|
+
return lines.join("\n");
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
// src/plan.ts
|
|
4255
|
+
var import_node_path3 = require("node:path");
|
|
4256
|
+
var PLANS_DIR = "plans";
|
|
4257
|
+
var META_FILE = (0, import_node_path3.join)(PLANS_DIR, ".plan-meta.json");
|
|
4258
|
+
var planPath = (slug) => (0, import_node_path3.join)(PLANS_DIR, `${slug}.md`);
|
|
4259
|
+
var metaKey = (project, slug) => `${project}/${slug}`;
|
|
4260
|
+
function parseMeta(raw) {
|
|
4261
|
+
if (!raw) return {};
|
|
4262
|
+
try {
|
|
4263
|
+
const o = JSON.parse(raw);
|
|
4264
|
+
return o && typeof o === "object" ? o : {};
|
|
4265
|
+
} catch {
|
|
4266
|
+
return {};
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
function serializeMeta(meta) {
|
|
4270
|
+
return JSON.stringify(meta, null, 2) + "\n";
|
|
4271
|
+
}
|
|
4272
|
+
function hashContent(s) {
|
|
4273
|
+
let h = 2166136261;
|
|
4274
|
+
for (let i = 0; i < s.length; i++) {
|
|
4275
|
+
h ^= s.charCodeAt(i);
|
|
4276
|
+
h = Math.imul(h, 16777619);
|
|
4277
|
+
}
|
|
4278
|
+
return (h >>> 0).toString(16);
|
|
4279
|
+
}
|
|
4280
|
+
function staleHint(slug) {
|
|
4281
|
+
return `remote "${slug}" is newer \u2014 run \`mmi-cli plan pull ${slug}\` first (your local is based on an older version), or re-push with \`--force\` to overwrite`;
|
|
4282
|
+
}
|
|
4283
|
+
function formatPlanList(plans) {
|
|
4284
|
+
return plans.map((p) => `${p.slug} \xB7 ${p.updatedAt ?? "-"} \xB7 ${p.project}`).join("\n");
|
|
4285
|
+
}
|
|
4286
|
+
var TIMEOUT_MS = 8e3;
|
|
4287
|
+
async function planPush(deps, slug, opts = {}) {
|
|
4288
|
+
const raw = deps.readLocal(slug);
|
|
4289
|
+
if (raw == null) {
|
|
4290
|
+
deps.err(`no local ${planPath(slug)} to push`);
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
const content = normalizeEol(raw);
|
|
4294
|
+
const project = opts.project ?? await deps.project();
|
|
4295
|
+
const meta = parseMeta(deps.readMetaRaw());
|
|
4296
|
+
const entry = meta[metaKey(project, slug)];
|
|
4297
|
+
const body = { project, slug, content };
|
|
4298
|
+
if (opts.force) body.force = true;
|
|
4299
|
+
else if (entry?.etag) body.baseEtag = entry.etag;
|
|
4300
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
|
|
4301
|
+
method: "POST",
|
|
4302
|
+
headers: await deps.headers({ "content-type": "application/json" }),
|
|
4303
|
+
body: JSON.stringify(body),
|
|
4304
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4305
|
+
});
|
|
4306
|
+
if (res.ok) {
|
|
4307
|
+
const out = await res.json();
|
|
4308
|
+
meta[metaKey(project, slug)] = { etag: out.etag, hash: hashContent(content), syncedAt: deps.now() };
|
|
4309
|
+
deps.writeMetaRaw(serializeMeta(meta));
|
|
4310
|
+
deps.log(`pushed ${slug}`);
|
|
4311
|
+
} else if (res.status === 409) {
|
|
4312
|
+
deps.err(staleHint(slug));
|
|
4313
|
+
} else {
|
|
4314
|
+
deps.err(`plan push failed: HTTP ${res.status}`);
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
async function planPull(deps, slug, opts = {}) {
|
|
4318
|
+
const project = opts.project ?? await deps.project();
|
|
4319
|
+
const meta = parseMeta(deps.readMetaRaw());
|
|
4320
|
+
const entry = meta[metaKey(project, slug)];
|
|
4321
|
+
const local = deps.readLocal(slug);
|
|
4322
|
+
if (local != null && entry && !opts.force && hashContent(normalizeEol(local)) !== entry.hash) {
|
|
4323
|
+
deps.err(`local ${planPath(slug)} has unpushed edits \u2014 push it, or pull with --force to overwrite`);
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
const qs = new URLSearchParams({ project, slug }).toString();
|
|
4327
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/get?${qs}`, {
|
|
4328
|
+
method: "GET",
|
|
4329
|
+
headers: await deps.headers(),
|
|
4330
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4331
|
+
});
|
|
4332
|
+
if (res.status === 404) {
|
|
4333
|
+
deps.err(`no plan "${slug}" found on the server`);
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
if (!res.ok) {
|
|
4337
|
+
deps.err(`plan pull failed: HTTP ${res.status}`);
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4340
|
+
const doc = await res.json();
|
|
4341
|
+
const content = normalizeEol(doc.content ?? "");
|
|
4342
|
+
deps.writeLocal(slug, content);
|
|
4343
|
+
meta[metaKey(project, slug)] = { etag: doc.etag, hash: hashContent(content), syncedAt: deps.now() };
|
|
4344
|
+
deps.writeMetaRaw(serializeMeta(meta));
|
|
4345
|
+
deps.log(`pulled ${slug} \u2192 ${planPath(slug)}`);
|
|
4346
|
+
}
|
|
4347
|
+
async function planList(deps, opts = {}) {
|
|
4348
|
+
const qs = opts.project ? `?${new URLSearchParams({ project: opts.project }).toString()}` : "";
|
|
4349
|
+
let res;
|
|
4350
|
+
try {
|
|
4351
|
+
res = await deps.fetch(`${deps.apiUrl}/plan/list${qs}`, {
|
|
4352
|
+
method: "GET",
|
|
4353
|
+
headers: await deps.headers(),
|
|
4354
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4355
|
+
});
|
|
4356
|
+
} catch (e) {
|
|
4357
|
+
if (!opts.quiet) deps.err(`plan list: ${e.message}`);
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
4360
|
+
if (!res.ok) {
|
|
4361
|
+
if (!opts.quiet) deps.err(`plan list failed: HTTP ${res.status}`);
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
const { plans } = await res.json();
|
|
4365
|
+
if (opts.json) {
|
|
4366
|
+
deps.log(JSON.stringify(plans));
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
if (!plans.length) {
|
|
4370
|
+
if (!opts.quiet) deps.log("no plans");
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
deps.log(formatPlanList(plans));
|
|
4374
|
+
}
|
|
4375
|
+
async function planDelete(deps, slug, opts = {}) {
|
|
4376
|
+
const project = opts.project ?? await deps.project();
|
|
4377
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/delete`, {
|
|
4378
|
+
method: "POST",
|
|
4379
|
+
headers: await deps.headers({ "content-type": "application/json" }),
|
|
4380
|
+
body: JSON.stringify({ project, slug }),
|
|
4381
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4382
|
+
});
|
|
4383
|
+
if (!res.ok) {
|
|
4384
|
+
deps.err(`plan delete failed: HTTP ${res.status}`);
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
deps.removeLocal(slug);
|
|
4388
|
+
const meta = parseMeta(deps.readMetaRaw());
|
|
4389
|
+
delete meta[metaKey(project, slug)];
|
|
4390
|
+
deps.writeMetaRaw(serializeMeta(meta));
|
|
4391
|
+
deps.log(`deleted ${slug}`);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
// src/index.ts
|
|
4395
|
+
var execFileP3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
3902
4396
|
var GIT_TIMEOUT_MS = 1e4;
|
|
3903
4397
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
3904
4398
|
async function githubToken() {
|
|
3905
4399
|
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
3906
4400
|
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
3907
4401
|
try {
|
|
3908
|
-
const { stdout } = await
|
|
4402
|
+
const { stdout } = await execFileP3("gh", ["auth", "token"]);
|
|
3909
4403
|
return stdout.trim() || void 0;
|
|
3910
4404
|
} catch {
|
|
3911
4405
|
return void 0;
|
|
@@ -3913,7 +4407,7 @@ async function githubToken() {
|
|
|
3913
4407
|
}
|
|
3914
4408
|
async function githubLogin() {
|
|
3915
4409
|
try {
|
|
3916
|
-
const { stdout } = await
|
|
4410
|
+
const { stdout } = await execFileP3("gh", ["api", "user", "--jq", ".login"]);
|
|
3917
4411
|
return stdout.trim() || void 0;
|
|
3918
4412
|
} catch {
|
|
3919
4413
|
return void 0;
|
|
@@ -3935,7 +4429,7 @@ var DEFAULT_KB_SOURCE = "https://raw.githubusercontent.com/mutmutco/MM-KB/main";
|
|
|
3935
4429
|
var SESSION_FILE = ".mmi/.session";
|
|
3936
4430
|
var gitOut = async (args) => {
|
|
3937
4431
|
try {
|
|
3938
|
-
return (await
|
|
4432
|
+
return (await execFileP3("git", [...args])).stdout.trim();
|
|
3939
4433
|
} catch {
|
|
3940
4434
|
return "";
|
|
3941
4435
|
}
|
|
@@ -3945,7 +4439,7 @@ function sessionDeps() {
|
|
|
3945
4439
|
env: process.env,
|
|
3946
4440
|
readPersisted: () => {
|
|
3947
4441
|
try {
|
|
3948
|
-
return (0,
|
|
4442
|
+
return (0, import_node_fs3.readFileSync)(SESSION_FILE, "utf8");
|
|
3949
4443
|
} catch {
|
|
3950
4444
|
return null;
|
|
3951
4445
|
}
|
|
@@ -3958,8 +4452,8 @@ function sessionDeps() {
|
|
|
3958
4452
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
3959
4453
|
function persistSession(id) {
|
|
3960
4454
|
try {
|
|
3961
|
-
(0,
|
|
3962
|
-
(0,
|
|
4455
|
+
(0, import_node_fs3.mkdirSync)(".mmi", { recursive: true });
|
|
4456
|
+
(0, import_node_fs3.writeFileSync)(SESSION_FILE, id, "utf8");
|
|
3963
4457
|
} catch {
|
|
3964
4458
|
}
|
|
3965
4459
|
}
|
|
@@ -3995,18 +4489,18 @@ async function readStdin() {
|
|
|
3995
4489
|
async function ghPrs(limit) {
|
|
3996
4490
|
const args = (state) => ["pr", "list", "--state", state, "--limit", String(limit), "--json", "number,headRefName,state"];
|
|
3997
4491
|
const [open, closed] = await Promise.all([
|
|
3998
|
-
|
|
3999
|
-
|
|
4492
|
+
execFileP3("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
|
|
4493
|
+
execFileP3("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
|
|
4000
4494
|
]);
|
|
4001
4495
|
return [...JSON.parse(open.stdout || "[]"), ...JSON.parse(closed.stdout || "[]")];
|
|
4002
4496
|
}
|
|
4003
4497
|
async function worktreeBranches() {
|
|
4004
|
-
const { stdout } = await
|
|
4498
|
+
const { stdout } = await execFileP3("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
4005
4499
|
const parsed = parseWorktreePorcelain(stdout);
|
|
4006
4500
|
return await Promise.all(parsed.map(async (w) => {
|
|
4007
4501
|
let dirty = true;
|
|
4008
4502
|
try {
|
|
4009
|
-
const { stdout: status } = await
|
|
4503
|
+
const { stdout: status } = await execFileP3("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
4010
4504
|
dirty = status.trim().length > 0;
|
|
4011
4505
|
} catch {
|
|
4012
4506
|
dirty = true;
|
|
@@ -4018,7 +4512,7 @@ async function gcPlan(remote, limit) {
|
|
|
4018
4512
|
const [branches, current, stale, prs, worktrees] = await Promise.all([
|
|
4019
4513
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
4020
4514
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
4021
|
-
|
|
4515
|
+
execFileP3("git", ["remote", "prune", remote, "--dry-run"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseRemotePruneDryRun(`${r.stdout}${r.stderr}`)).catch(() => []),
|
|
4022
4516
|
ghPrs(limit),
|
|
4023
4517
|
worktreeBranches()
|
|
4024
4518
|
]);
|
|
@@ -4031,26 +4525,26 @@ async function gcPlan(remote, limit) {
|
|
|
4031
4525
|
remote
|
|
4032
4526
|
});
|
|
4033
4527
|
}
|
|
4034
|
-
async function applyGcPlan(
|
|
4035
|
-
for (const branch of
|
|
4036
|
-
if (branch.worktreePath) await
|
|
4037
|
-
await
|
|
4528
|
+
async function applyGcPlan(plan2, remote) {
|
|
4529
|
+
for (const branch of plan2.branches) {
|
|
4530
|
+
if (branch.worktreePath) await execFileP3("git", ["worktree", "remove", branch.worktreePath], { timeout: GIT_TIMEOUT_MS });
|
|
4531
|
+
await execFileP3("git", ["branch", "-D", branch.branch], { timeout: GIT_TIMEOUT_MS });
|
|
4038
4532
|
}
|
|
4039
|
-
for (const ref of
|
|
4040
|
-
await
|
|
4533
|
+
for (const ref of plan2.trackingRefs) {
|
|
4534
|
+
await execFileP3("git", ["update-ref", "-d", `refs/remotes/${remote}/${ref.branch}`], { timeout: GIT_TIMEOUT_MS });
|
|
4041
4535
|
}
|
|
4042
|
-
if (
|
|
4043
|
-
await
|
|
4536
|
+
if (plan2.branches.some((b) => b.worktreePath)) {
|
|
4537
|
+
await execFileP3("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
|
|
4044
4538
|
}
|
|
4045
4539
|
}
|
|
4046
4540
|
function resolveVersion() {
|
|
4047
4541
|
try {
|
|
4048
|
-
const manifest = (0,
|
|
4049
|
-
return JSON.parse((0,
|
|
4542
|
+
const manifest = (0, import_node_path4.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
4543
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(manifest, "utf8")).version || "0.0.0";
|
|
4050
4544
|
} catch {
|
|
4051
4545
|
try {
|
|
4052
|
-
const pkg = (0,
|
|
4053
|
-
return JSON.parse((0,
|
|
4546
|
+
const pkg = (0, import_node_path4.join)(__dirname, "..", "package.json");
|
|
4547
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
4054
4548
|
} catch {
|
|
4055
4549
|
return "0.0.0";
|
|
4056
4550
|
}
|
|
@@ -4058,7 +4552,7 @@ function resolveVersion() {
|
|
|
4058
4552
|
}
|
|
4059
4553
|
function readRepoVersion() {
|
|
4060
4554
|
try {
|
|
4061
|
-
return JSON.parse((0,
|
|
4555
|
+
return JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path4.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
4062
4556
|
} catch {
|
|
4063
4557
|
return void 0;
|
|
4064
4558
|
}
|
|
@@ -4084,21 +4578,23 @@ rules.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
4084
4578
|
return;
|
|
4085
4579
|
}
|
|
4086
4580
|
const base = (cfg.orgRulesSource ?? DEFAULT_RULES_SOURCE).replace(/\/$/, "");
|
|
4581
|
+
const token = await githubToken();
|
|
4087
4582
|
let changed = 0;
|
|
4088
4583
|
for (const file of ["AGENTS.md", "CLAUDE.md", ".claude/settings.json"]) {
|
|
4089
4584
|
let source;
|
|
4090
4585
|
try {
|
|
4091
|
-
const
|
|
4586
|
+
const url = `${base}/${file}`;
|
|
4587
|
+
const res = await fetch(url, { headers: rulesSourceAuthHeaders(url, token) });
|
|
4092
4588
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
4093
4589
|
source = await res.text();
|
|
4094
4590
|
} catch (e) {
|
|
4095
4591
|
if (!opts.quiet) console.error(`mmi-cli rules: could not fetch ${file} (${e.message}); left it untouched`);
|
|
4096
4592
|
continue;
|
|
4097
4593
|
}
|
|
4098
|
-
const current = (0,
|
|
4594
|
+
const current = (0, import_node_fs3.existsSync)(file) ? await (0, import_promises.readFile)(file, "utf8") : null;
|
|
4099
4595
|
if (needsUpdate(source, current)) {
|
|
4100
4596
|
const slash = file.lastIndexOf("/");
|
|
4101
|
-
if (slash > 0) (0,
|
|
4597
|
+
if (slash > 0) (0, import_node_fs3.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
4102
4598
|
await (0, import_promises.writeFile)(file, normalizeEol(source), "utf8");
|
|
4103
4599
|
changed++;
|
|
4104
4600
|
if (!opts.quiet) console.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -4112,7 +4608,7 @@ async function runNote(summary, o) {
|
|
|
4112
4608
|
const capture = buildNoteCapture(summary, o, (0, import_node_crypto.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
4113
4609
|
await postCapture(capture);
|
|
4114
4610
|
}
|
|
4115
|
-
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").action((summary, o) => runNote(summary, o));
|
|
4611
|
+
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));
|
|
4116
4612
|
saga.command("probe <summary>").description("record a diagnostic probe note (alias for `saga note --diagnostic`)").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").action((summary, o) => runNote(summary, { ...o, diagnostic: true }));
|
|
4117
4613
|
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(async (opts) => {
|
|
4118
4614
|
const cfg = await loadConfig();
|
|
@@ -4146,7 +4642,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
4146
4642
|
if (!headGateDue(tsPath)) return;
|
|
4147
4643
|
markHeadRun(tsPath);
|
|
4148
4644
|
try {
|
|
4149
|
-
(0,
|
|
4645
|
+
(0, import_node_child_process4.spawn)(process.execPath, [process.argv[1], "saga", "head-update", "--run"], {
|
|
4150
4646
|
detached: true,
|
|
4151
4647
|
stdio: "ignore",
|
|
4152
4648
|
windowsHide: true
|
|
@@ -4164,7 +4660,8 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
4164
4660
|
if (!res.ok) return;
|
|
4165
4661
|
const state = await res.json();
|
|
4166
4662
|
if (!state.actionLog?.length) return;
|
|
4167
|
-
const
|
|
4663
|
+
const decisionTexts = (state.decisions ?? []).map((d) => typeof d === "string" ? d : d.text);
|
|
4664
|
+
const update = parseHeadUpdate(await runHeadEngine(headPrompt({ ...state, decisions: decisionTexts })));
|
|
4168
4665
|
if (!update) return;
|
|
4169
4666
|
await fetch(`${cfg.sagaApiUrl}/saga/head`, {
|
|
4170
4667
|
method: "POST",
|
|
@@ -4214,10 +4711,10 @@ program2.command("gc").description("dry-run cleanup for merged/closed PR branche
|
|
|
4214
4711
|
const limit = Number.parseInt(o.limit, 10);
|
|
4215
4712
|
if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
|
|
4216
4713
|
try {
|
|
4217
|
-
const
|
|
4218
|
-
if (o.apply) await applyGcPlan(
|
|
4219
|
-
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan }, null, 2));
|
|
4220
|
-
console.log(formatGcPlan(
|
|
4714
|
+
const plan2 = await gcPlan(o.remote, limit);
|
|
4715
|
+
if (o.apply) await applyGcPlan(plan2, o.remote);
|
|
4716
|
+
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan: plan2 }, null, 2));
|
|
4717
|
+
console.log(formatGcPlan(plan2, Boolean(o.apply)));
|
|
4221
4718
|
} catch (e) {
|
|
4222
4719
|
fail(`gc: ${e.message}`);
|
|
4223
4720
|
}
|
|
@@ -4230,21 +4727,21 @@ program2.command("kb").description("org knowledgebase (read-only)").command("get
|
|
|
4230
4727
|
});
|
|
4231
4728
|
async function ghCreate(args) {
|
|
4232
4729
|
try {
|
|
4233
|
-
const { stdout } = await
|
|
4730
|
+
const { stdout } = await execFileP3("gh", args);
|
|
4234
4731
|
return parseCreatedUrl(stdout);
|
|
4235
4732
|
} catch (e) {
|
|
4236
4733
|
const err = e;
|
|
4237
4734
|
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
4238
4735
|
}
|
|
4239
4736
|
}
|
|
4240
|
-
async function
|
|
4241
|
-
const { stdout } = await
|
|
4737
|
+
async function ghJson2(args, timeout = 1e4) {
|
|
4738
|
+
const { stdout } = await execFileP3("gh", args, { timeout });
|
|
4242
4739
|
return JSON.parse(stdout);
|
|
4243
4740
|
}
|
|
4244
4741
|
async function resolveRepo(repo) {
|
|
4245
4742
|
if (repo) return repo;
|
|
4246
4743
|
try {
|
|
4247
|
-
const { stdout } = await
|
|
4744
|
+
const { stdout } = await execFileP3("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
|
|
4248
4745
|
return stdout.trim() || void 0;
|
|
4249
4746
|
} catch {
|
|
4250
4747
|
return void 0;
|
|
@@ -4254,7 +4751,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
4254
4751
|
try {
|
|
4255
4752
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
4256
4753
|
if (o.repo) args.push("--repo", o.repo);
|
|
4257
|
-
(0,
|
|
4754
|
+
(0, import_node_child_process4.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
4258
4755
|
detached: true,
|
|
4259
4756
|
stdio: "ignore",
|
|
4260
4757
|
windowsHide: true,
|
|
@@ -4263,6 +4760,77 @@ function scheduleRelatedDiscovery(o) {
|
|
|
4263
4760
|
} catch {
|
|
4264
4761
|
}
|
|
4265
4762
|
}
|
|
4763
|
+
function makePlanDeps(cfg) {
|
|
4764
|
+
const ensureDir = () => (0, import_node_fs3.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
4765
|
+
return {
|
|
4766
|
+
apiUrl: cfg.sagaApiUrl,
|
|
4767
|
+
fetch: (url, init) => fetch(url, init),
|
|
4768
|
+
headers: (extra) => sagaHeaders(extra),
|
|
4769
|
+
project: async () => (await sagaKey(cfg)).project,
|
|
4770
|
+
readLocal: (slug) => {
|
|
4771
|
+
try {
|
|
4772
|
+
return (0, import_node_fs3.readFileSync)(planPath(slug), "utf8");
|
|
4773
|
+
} catch {
|
|
4774
|
+
return null;
|
|
4775
|
+
}
|
|
4776
|
+
},
|
|
4777
|
+
writeLocal: (slug, content) => {
|
|
4778
|
+
ensureDir();
|
|
4779
|
+
(0, import_node_fs3.writeFileSync)(planPath(slug), content, "utf8");
|
|
4780
|
+
},
|
|
4781
|
+
removeLocal: (slug) => {
|
|
4782
|
+
try {
|
|
4783
|
+
(0, import_node_fs3.rmSync)(planPath(slug));
|
|
4784
|
+
} catch {
|
|
4785
|
+
}
|
|
4786
|
+
},
|
|
4787
|
+
readMetaRaw: () => {
|
|
4788
|
+
try {
|
|
4789
|
+
return (0, import_node_fs3.readFileSync)(META_FILE, "utf8");
|
|
4790
|
+
} catch {
|
|
4791
|
+
return null;
|
|
4792
|
+
}
|
|
4793
|
+
},
|
|
4794
|
+
writeMetaRaw: (raw) => {
|
|
4795
|
+
ensureDir();
|
|
4796
|
+
(0, import_node_fs3.writeFileSync)(META_FILE, raw, "utf8");
|
|
4797
|
+
},
|
|
4798
|
+
log: (m) => console.log(m),
|
|
4799
|
+
err: (m) => console.error(m),
|
|
4800
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function openInEditor(path) {
|
|
4804
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
4805
|
+
if (!editor) {
|
|
4806
|
+
console.log(`plan at ${path} (set $EDITOR to open it automatically)`);
|
|
4807
|
+
return;
|
|
4808
|
+
}
|
|
4809
|
+
try {
|
|
4810
|
+
(0, import_node_child_process4.spawn)(editor, [path], { stdio: "inherit" });
|
|
4811
|
+
} catch {
|
|
4812
|
+
console.log(`open ${path} manually`);
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
async function withPlan(quiet, run) {
|
|
4816
|
+
const cfg = await loadConfig();
|
|
4817
|
+
if (!cfg.sagaApiUrl) {
|
|
4818
|
+
if (!quiet) fail("plan: sagaApiUrl not configured in .mmi/config.json");
|
|
4819
|
+
return;
|
|
4820
|
+
}
|
|
4821
|
+
await run(makePlanDeps(cfg));
|
|
4822
|
+
}
|
|
4823
|
+
var plan = program2.command("plan").description("your cross-device plans/SSOTs (S3-backed, git-clean)");
|
|
4824
|
+
plan.command("push <slug>").description("push a local 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, (d) => planPush(d, slug, o)));
|
|
4825
|
+
plan.command("pull <slug>").description("pull a plan from the server into plans/<slug>.md").option("--project <name>", "override the project key").option("--force", "overwrite local even if it has unpushed edits").action((slug, o) => withPlan(false, (d) => planPull(d, slug, o)));
|
|
4826
|
+
plan.command("list").description("list your 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)));
|
|
4827
|
+
plan.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
4828
|
+
(slug, o) => withPlan(false, async (d) => {
|
|
4829
|
+
await planPull(d, slug, { project: o.project });
|
|
4830
|
+
openInEditor(planPath(slug));
|
|
4831
|
+
})
|
|
4832
|
+
);
|
|
4833
|
+
plan.command("delete <slug>").description("delete a plan from the server and the local copy").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, (d) => planDelete(d, slug, o)));
|
|
4266
4834
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
4267
4835
|
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").requiredOption("--title <title>", "issue title").requiredOption("--body <body>", "issue body (markdown)").requiredOption("--priority <priority>", "high | medium | low (adds a priority:<p> label)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
4268
4836
|
let args;
|
|
@@ -4281,7 +4849,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
4281
4849
|
const repo = await resolveRepo(o.repo);
|
|
4282
4850
|
if (!repo) return fail("issue discover-related: could not resolve repo");
|
|
4283
4851
|
try {
|
|
4284
|
-
const issues = await
|
|
4852
|
+
const issues = await ghJson2([
|
|
4285
4853
|
"issue",
|
|
4286
4854
|
"list",
|
|
4287
4855
|
"--repo",
|
|
@@ -4296,7 +4864,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
4296
4864
|
const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
|
|
4297
4865
|
if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
|
|
4298
4866
|
if (!candidates.length) return;
|
|
4299
|
-
const viewed = await
|
|
4867
|
+
const viewed = await ghJson2([
|
|
4300
4868
|
"issue",
|
|
4301
4869
|
"view",
|
|
4302
4870
|
String(number),
|
|
@@ -4306,7 +4874,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
4306
4874
|
"comments"
|
|
4307
4875
|
]);
|
|
4308
4876
|
if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
|
|
4309
|
-
await
|
|
4877
|
+
await execFileP3("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: 1e4 });
|
|
4310
4878
|
} catch {
|
|
4311
4879
|
}
|
|
4312
4880
|
});
|
|
@@ -4345,11 +4913,102 @@ function renderSteps(title, steps) {
|
|
|
4345
4913
|
...steps.map((step, i) => `${i + 1}. ${step.gated ? "[gated] " : ""}${step.label}${step.command ? ` - ${step.command}` : ""}`)
|
|
4346
4914
|
].join("\n");
|
|
4347
4915
|
}
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4916
|
+
function rawFlag(flag) {
|
|
4917
|
+
return process.argv.includes(flag);
|
|
4918
|
+
}
|
|
4919
|
+
function rawValue(flag, fallback) {
|
|
4920
|
+
const index = process.argv.indexOf(flag);
|
|
4921
|
+
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
|
|
4922
|
+
}
|
|
4923
|
+
function printLine(value) {
|
|
4924
|
+
(0, import_node_fs3.writeSync)(1, `${value}
|
|
4925
|
+
`);
|
|
4926
|
+
}
|
|
4927
|
+
function stageKeepAlive() {
|
|
4928
|
+
return setTimeout(() => void 0, 5 * 60 * 1e3);
|
|
4929
|
+
}
|
|
4930
|
+
var stage = program2.command("stage").description("plan or run the repo local stage environment").option("--json", "machine-readable output").option("--apply", "run the full local stage: stop previous, build, start, health-check").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async (o) => {
|
|
4931
|
+
const cfg = (await loadConfig()).stage;
|
|
4932
|
+
if (o.apply) {
|
|
4933
|
+
const hold = stageKeepAlive();
|
|
4934
|
+
try {
|
|
4935
|
+
const result = await runStage(cfg, { timeoutMs: Number(o.timeoutMs || 6e4) });
|
|
4936
|
+
return printLine(o.json ? JSON.stringify(result) : `mmi-cli stage: ${result.message}`);
|
|
4937
|
+
} catch (e) {
|
|
4938
|
+
return fail(`stage: ${e.message}`);
|
|
4939
|
+
} finally {
|
|
4940
|
+
clearTimeout(hold);
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
const steps = stagePlan(cfg);
|
|
4351
4944
|
console.log(o.json ? JSON.stringify({ command: "stage", steps }, null, 2) : renderSteps("mmi-cli stage: dry-run plan", steps));
|
|
4352
4945
|
});
|
|
4946
|
+
stage.command("stop").description("stop the previous local stage process recorded in tmp/stage/state.json").option("--json", "machine-readable output").option("--apply", "kill the recorded process tree and remove the state file").action(async () => {
|
|
4947
|
+
const o = { json: rawFlag("--json"), apply: rawFlag("--apply") };
|
|
4948
|
+
if (!o.apply) {
|
|
4949
|
+
const steps = [{ label: "stop recorded local stage", command: "mmi-cli stage stop --apply" }];
|
|
4950
|
+
return printLine(o.json ? JSON.stringify({ command: "stage stop", steps }, null, 2) : renderSteps("mmi-cli stage stop: dry-run plan", steps));
|
|
4951
|
+
}
|
|
4952
|
+
try {
|
|
4953
|
+
const result = await stopStage();
|
|
4954
|
+
printLine(o.json ? JSON.stringify(result) : `mmi-cli stage stop: ${result.message}`);
|
|
4955
|
+
} catch (e) {
|
|
4956
|
+
fail(`stage stop: ${e.message}`);
|
|
4957
|
+
}
|
|
4958
|
+
});
|
|
4959
|
+
stage.command("start").description("start the configured local stage process and optionally wait for health").option("--json", "machine-readable output").option("--apply", "start the configured stage.up process").option("--timeout-ms <ms>", "bounded health timeout", "60000").action(async () => {
|
|
4960
|
+
const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000") };
|
|
4961
|
+
const cfg = (await loadConfig()).stage;
|
|
4962
|
+
if (!o.apply) {
|
|
4963
|
+
const steps = [{ label: "start local stage", command: cfg?.up || "(no stage.up configured)" }];
|
|
4964
|
+
return printLine(o.json ? JSON.stringify({ command: "stage start", steps }, null, 2) : renderSteps("mmi-cli stage start: dry-run plan", steps));
|
|
4965
|
+
}
|
|
4966
|
+
try {
|
|
4967
|
+
const hold = stageKeepAlive();
|
|
4968
|
+
let printed = false;
|
|
4969
|
+
try {
|
|
4970
|
+
const result = await startStage(cfg, {
|
|
4971
|
+
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
4972
|
+
onReady: (ready) => {
|
|
4973
|
+
printed = true;
|
|
4974
|
+
printLine(o.json ? JSON.stringify(ready) : `mmi-cli stage start: ${ready.message}`);
|
|
4975
|
+
}
|
|
4976
|
+
});
|
|
4977
|
+
if (!printed) printLine(o.json ? JSON.stringify(result) : `mmi-cli stage start: ${result.message}`);
|
|
4978
|
+
} finally {
|
|
4979
|
+
clearTimeout(hold);
|
|
4980
|
+
}
|
|
4981
|
+
} catch (e) {
|
|
4982
|
+
fail(`stage start: ${e.message}`);
|
|
4983
|
+
}
|
|
4984
|
+
});
|
|
4985
|
+
stage.command("run").description("force-stop previous stage, build, start, and health-check").option("--json", "machine-readable output").option("--apply", "run the configured stage sequence").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async () => {
|
|
4986
|
+
const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000") };
|
|
4987
|
+
const cfg = (await loadConfig()).stage;
|
|
4988
|
+
if (!o.apply) {
|
|
4989
|
+
const steps = stagePlan(cfg);
|
|
4990
|
+
return printLine(o.json ? JSON.stringify({ command: "stage run", steps }, null, 2) : renderSteps("mmi-cli stage run: dry-run plan", steps));
|
|
4991
|
+
}
|
|
4992
|
+
try {
|
|
4993
|
+
const hold = stageKeepAlive();
|
|
4994
|
+
let printed = false;
|
|
4995
|
+
try {
|
|
4996
|
+
const result = await runStage(cfg, {
|
|
4997
|
+
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
4998
|
+
onReady: (ready) => {
|
|
4999
|
+
const runReady = { ...ready, action: "run", message: `built and ${ready.message}` };
|
|
5000
|
+
printed = true;
|
|
5001
|
+
printLine(o.json ? JSON.stringify(runReady) : `mmi-cli stage run: ${runReady.message}`);
|
|
5002
|
+
}
|
|
5003
|
+
});
|
|
5004
|
+
if (!printed) printLine(o.json ? JSON.stringify(result) : `mmi-cli stage run: ${result.message}`);
|
|
5005
|
+
} finally {
|
|
5006
|
+
clearTimeout(hold);
|
|
5007
|
+
}
|
|
5008
|
+
} catch (e) {
|
|
5009
|
+
fail(`stage run: ${e.message}`);
|
|
5010
|
+
}
|
|
5011
|
+
});
|
|
4353
5012
|
for (const commandName of ["rc", "release", "hotfix"]) {
|
|
4354
5013
|
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit approval`).option("--json", "machine-readable output").option("--apply", "reserved for future train execution after explicit admin approval").action((o) => {
|
|
4355
5014
|
if (o.apply) return fail(`${commandName}: execution is not implemented yet; use the dry-run plan and the existing /${commandName} skill`);
|
|
@@ -4357,34 +5016,45 @@ for (const commandName of ["rc", "release", "hotfix"]) {
|
|
|
4357
5016
|
console.log(o.json ? JSON.stringify({ command: commandName, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps));
|
|
4358
5017
|
});
|
|
4359
5018
|
}
|
|
4360
|
-
program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").
|
|
5019
|
+
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
|
|
5020
|
+
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
4361
5021
|
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|
|
4362
5022
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
|
|
4363
5023
|
const steps = bootstrapPlan(o.repo, o.class);
|
|
4364
5024
|
console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
|
|
4365
5025
|
});
|
|
5026
|
+
bootstrap.command("verify <repo>").description("audit whether an existing repo is bootstrapped correctly; no mutations").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").action(async (repo) => {
|
|
5027
|
+
const o = { class: rawValue("--class", "deployable"), json: rawFlag("--json") };
|
|
5028
|
+
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap verify: --class must be deployable or content");
|
|
5029
|
+
const report = await verifyBootstrap(repo, o.class, {
|
|
5030
|
+
gh: async (args) => execFileP3("gh", args, { timeout: 2e4 }),
|
|
5031
|
+
readLocalFile: (path) => (0, import_node_fs3.existsSync)(path) ? (0, import_node_fs3.readFileSync)(path, "utf8") : null
|
|
5032
|
+
});
|
|
5033
|
+
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
5034
|
+
if (!report.ok) process.exitCode = 1;
|
|
5035
|
+
});
|
|
4366
5036
|
var isWin = process.platform === "win32";
|
|
4367
5037
|
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, repo config, plugin git clone) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--json", "machine-readable output").action(async (opts) => {
|
|
4368
5038
|
const checks = [];
|
|
4369
|
-
const
|
|
4370
|
-
let
|
|
4371
|
-
if (!
|
|
5039
|
+
const login = await githubLogin();
|
|
5040
|
+
let ghInstalled = true;
|
|
5041
|
+
if (!login) {
|
|
4372
5042
|
try {
|
|
4373
|
-
await
|
|
5043
|
+
await execFileP3("gh", ["--version"]);
|
|
4374
5044
|
} catch {
|
|
4375
|
-
|
|
5045
|
+
ghInstalled = false;
|
|
4376
5046
|
}
|
|
4377
5047
|
}
|
|
4378
|
-
checks.push({
|
|
5048
|
+
checks.push(buildGithubAuthCheck({ login, ghInstalled }));
|
|
4379
5049
|
let onPath = false;
|
|
4380
5050
|
try {
|
|
4381
|
-
await
|
|
5051
|
+
await execFileP3(isWin ? "where" : "which", ["mmi-cli"]);
|
|
4382
5052
|
onPath = true;
|
|
4383
5053
|
} catch {
|
|
4384
5054
|
}
|
|
4385
5055
|
if (!onPath) {
|
|
4386
5056
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
4387
|
-
if (root && (0,
|
|
5057
|
+
if (root && (0, import_node_fs3.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
4388
5058
|
}
|
|
4389
5059
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
4390
5060
|
checks.push(buildVersionLagReport({
|
|
@@ -4398,13 +5068,13 @@ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi
|
|
|
4398
5068
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
4399
5069
|
let cloneOk = false;
|
|
4400
5070
|
try {
|
|
4401
|
-
const { stdout } = await
|
|
5071
|
+
const { stdout } = await execFileP3("git", ["config", "--global", "--get-all", REWRITE_KEY]);
|
|
4402
5072
|
cloneOk = stdout.split("\n").some((l) => l.trim() === "git@github.com:");
|
|
4403
5073
|
} catch {
|
|
4404
5074
|
}
|
|
4405
5075
|
if (!cloneOk) {
|
|
4406
5076
|
try {
|
|
4407
|
-
await
|
|
5077
|
+
await execFileP3("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
4408
5078
|
cloneOk = true;
|
|
4409
5079
|
if (!opts.banner && !opts.json) console.error(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
|
|
4410
5080
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"test": "vitest run",
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
|
-
"dependencies": {
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^12.1.0"
|
|
36
|
+
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"@types/node": "^22.0.0",
|
|
37
39
|
"esbuild": "^0.24.0",
|