@mutmutco/cli 2.0.0 → 2.3.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 +1 -1
- package/dist/index.cjs +395 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ mmi-cli doctor --json
|
|
|
43
43
|
- `mmi-cli pr create` and `pr merge` create PRs and land them with branch/worktree cleanup; `mmi-cli gc` dry-runs cleanup of merged/closed PR branches + stale tracking refs.
|
|
44
44
|
- `mmi-cli board read|claim|show|move|done|backfill-priority` reads and moves GitHub Project work.
|
|
45
45
|
- `mmi-cli stage`, `stage start`, `stage stop`, `stage run`, and `port-range <repo>` manage the local gitignored stage and its port block; `stage-live` explains that remote rc/live move only via `/rcand` · `/release` · `/hotfix`.
|
|
46
|
-
- `mmi-cli
|
|
46
|
+
- `mmi-cli rcand`, `release`, and `hotfix` render guarded train plans; the train triggers the Hub's central tenant deployer, so product repos carry no deploy file.
|
|
47
47
|
- `mmi-cli bootstrap`, `bootstrap verify`, and `bootstrap apply` plan, audit, and seed repo onboarding.
|
|
48
48
|
- `mmi-cli access audit` checks collaborator roles and train-branch allowlists.
|
|
49
49
|
- `mmi-cli doctor` checks GitHub auth, repo config, CLI availability, the per-project plugin install record, and stale plugin/cache state, auto-repairing the safe gaps.
|
package/dist/index.cjs
CHANGED
|
@@ -3221,6 +3221,13 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
|
3221
3221
|
// src/gh-create.ts
|
|
3222
3222
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
3223
3223
|
var PRIORITIES = ["urgent", "high", "medium", "low"];
|
|
3224
|
+
function normalizePriority(priority) {
|
|
3225
|
+
const p = priority.trim().toLowerCase();
|
|
3226
|
+
if (!PRIORITIES.includes(p)) {
|
|
3227
|
+
throw new Error(`unknown priority "${priority}" \u2014 expected one of: ${PRIORITIES.join(", ")}`);
|
|
3228
|
+
}
|
|
3229
|
+
return p;
|
|
3230
|
+
}
|
|
3224
3231
|
function parseCreatedUrl(stdout) {
|
|
3225
3232
|
const re = /https:\/\/github\.com\/[^\s]+\/(?:issues|pull)\/(\d+)/g;
|
|
3226
3233
|
let match;
|
|
@@ -3234,9 +3241,7 @@ ${stdout.trim() || "(empty)"}`);
|
|
|
3234
3241
|
}
|
|
3235
3242
|
function buildIssueArgs({ type, title, body, priority, repo, labels }) {
|
|
3236
3243
|
if (!ISSUE_TYPES.includes(type)) throw new Error(`unknown issue type "${type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
3237
|
-
|
|
3238
|
-
throw new Error(`unknown priority "${priority}" \u2014 expected one of: ${PRIORITIES.join(", ")}`);
|
|
3239
|
-
}
|
|
3244
|
+
normalizePriority(priority);
|
|
3240
3245
|
const args = ["issue", "create"];
|
|
3241
3246
|
if (repo) args.push("--repo", repo);
|
|
3242
3247
|
args.push("--title", title, "--body", body, "--label", type);
|
|
@@ -3304,6 +3309,7 @@ function buildHealth(i) {
|
|
|
3304
3309
|
if (!i.sagaApiUrl) problems.push("sagaApiUrl not configured in .mmi/config.json");
|
|
3305
3310
|
if (!i.identity) problems.push("no GitHub identity (gh auth token / GH_TOKEN)");
|
|
3306
3311
|
if (!i.reachable) problems.push("saga backend unreachable");
|
|
3312
|
+
if (i.reachable && i.authorized === false) problems.push("saga backend rejected authenticated state access");
|
|
3307
3313
|
if (!i.key.sessionId || i.key.sessionId === "-") problems.push("unsafe session id");
|
|
3308
3314
|
const safeToWrite = problems.length === 0;
|
|
3309
3315
|
return {
|
|
@@ -3311,6 +3317,7 @@ function buildHealth(i) {
|
|
|
3311
3317
|
safeToWrite,
|
|
3312
3318
|
identity: i.identity,
|
|
3313
3319
|
reachable: i.reachable,
|
|
3320
|
+
authorized: i.authorized,
|
|
3314
3321
|
sagaApiUrl: i.sagaApiUrl,
|
|
3315
3322
|
key: i.key,
|
|
3316
3323
|
source: i.source,
|
|
@@ -3324,7 +3331,7 @@ function healthBanner(report) {
|
|
|
3324
3331
|
return `saga health: CHECK - ${summary}${suffix}`;
|
|
3325
3332
|
}
|
|
3326
3333
|
function resumeCue() {
|
|
3327
|
-
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.';
|
|
3334
|
+
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.';
|
|
3328
3335
|
}
|
|
3329
3336
|
|
|
3330
3337
|
// src/saga-note.ts
|
|
@@ -4288,23 +4295,29 @@ function stagePlan(stage2 = {}) {
|
|
|
4288
4295
|
function stageLivePlan() {
|
|
4289
4296
|
return [
|
|
4290
4297
|
{ label: "stage-live is not an org command; /stage is local only", command: "mmi-cli stage run --apply" },
|
|
4291
|
-
{ label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli
|
|
4298
|
+
{ label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli rcand && mmi-cli release && mmi-cli hotfix", gated: true }
|
|
4292
4299
|
];
|
|
4293
4300
|
}
|
|
4294
4301
|
function trainPlan(command) {
|
|
4295
|
-
if (command === "
|
|
4302
|
+
if (command === "rcand") {
|
|
4296
4303
|
return [
|
|
4297
|
-
{ label: "verify
|
|
4304
|
+
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
4305
|
+
{ label: "verify current branch is development", gated: true },
|
|
4306
|
+
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
4307
|
+
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
4298
4308
|
{ label: "merge development to rc", gated: true },
|
|
4299
|
-
{ label: "deploy rc", gated: true }
|
|
4309
|
+
{ label: "dispatch central tenant deploy for rc", command: "gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub -f slug=<slug> -f repo=<owner/repo> -f ref=rc -f stage=rc", gated: true }
|
|
4300
4310
|
];
|
|
4301
4311
|
}
|
|
4302
4312
|
if (command === "release") {
|
|
4303
4313
|
return [
|
|
4304
|
-
{ label: "verify
|
|
4314
|
+
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
4315
|
+
{ label: "verify current branch is rc", gated: true },
|
|
4316
|
+
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
4317
|
+
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
4305
4318
|
{ label: "merge rc to main", gated: true },
|
|
4306
4319
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
4307
|
-
{ label: "deploy
|
|
4320
|
+
{ label: "dispatch central tenant deploy for main", command: "gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub -f slug=<slug> -f repo=<owner/repo> -f ref=main -f stage=main", gated: true },
|
|
4308
4321
|
{ label: "roll development forward", gated: true }
|
|
4309
4322
|
];
|
|
4310
4323
|
}
|
|
@@ -4406,6 +4419,13 @@ ${block}
|
|
|
4406
4419
|
// src/doctor.ts
|
|
4407
4420
|
var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
|
|
4408
4421
|
var AWS_CROSS_ACCOUNT_FIX = "use a non-root IAM user/session profile for master-agent AWS checks; set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), then verify `aws sts get-caller-identity` does not end in :root";
|
|
4422
|
+
var MMI_AGENTIC_ONBOARDING_GUIDE = {
|
|
4423
|
+
label: "MMI Agentic Onboarding",
|
|
4424
|
+
url: "https://github.com/mutmutco/MMI-Hub/wiki/Agentic-Dev-Environment-Guide"
|
|
4425
|
+
};
|
|
4426
|
+
function doctorResourcesForGaps(gaps) {
|
|
4427
|
+
return gaps.length ? [MMI_AGENTIC_ONBOARDING_GUIDE] : [];
|
|
4428
|
+
}
|
|
4409
4429
|
function buildGithubAuthCheck(input) {
|
|
4410
4430
|
const ok = Boolean(input.login?.trim());
|
|
4411
4431
|
return {
|
|
@@ -4461,6 +4481,45 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
4461
4481
|
recordToInsert
|
|
4462
4482
|
};
|
|
4463
4483
|
}
|
|
4484
|
+
var PLUGIN_DRIFT_LABEL = "plugin config drift (mmi@mmi duplicate rows / stale gitCommitSha)";
|
|
4485
|
+
function recordFreshness(r) {
|
|
4486
|
+
return r.lastUpdated ?? r.installedAt ?? "";
|
|
4487
|
+
}
|
|
4488
|
+
function freshestRecord(records) {
|
|
4489
|
+
return records.reduce((best, r) => recordFreshness(r) > recordFreshness(best) ? r : best);
|
|
4490
|
+
}
|
|
4491
|
+
function pluginConfigDriftFix(pluginId) {
|
|
4492
|
+
return `\`${pluginId}\` registered as N duplicate project rows / a stale gitCommitSha in ~/.claude/plugins/installed_plugins.json \u2014 run \`mmi-cli doctor\` to collapse them to one \`scope: user\` entry at the highest version (a .bak backup is written first), or in \`/plugin\` uninstall the extra rows and reinstall once at user scope`;
|
|
4493
|
+
}
|
|
4494
|
+
function buildPluginConfigDriftCheck(input) {
|
|
4495
|
+
const pluginId = input.pluginId ?? MMI_PLUGIN_ID;
|
|
4496
|
+
const base = {
|
|
4497
|
+
ok: true,
|
|
4498
|
+
label: PLUGIN_DRIFT_LABEL,
|
|
4499
|
+
fix: pluginConfigDriftFix(pluginId),
|
|
4500
|
+
pluginId
|
|
4501
|
+
};
|
|
4502
|
+
if (!input.isOrgRepo) return base;
|
|
4503
|
+
const records = input.installed?.plugins?.[pluginId];
|
|
4504
|
+
if (!Array.isArray(records) || records.length === 0) return base;
|
|
4505
|
+
const projectRows = records.filter((r) => r.scope === "project");
|
|
4506
|
+
const freshest = freshestRecord(records);
|
|
4507
|
+
const freshSha = freshest.gitCommitSha;
|
|
4508
|
+
const staleShaRows = freshSha ? records.filter((r) => r.gitCommitSha !== void 0 && r.gitCommitSha !== freshSha).length : 0;
|
|
4509
|
+
const duplicateProjectRows = projectRows.length > 1 ? projectRows.length : 0;
|
|
4510
|
+
if (duplicateProjectRows === 0 && staleShaRows === 0) {
|
|
4511
|
+
return { ...base, duplicateProjectRows: 0, staleShaRows: 0 };
|
|
4512
|
+
}
|
|
4513
|
+
const { projectPath: _drop, ...rest } = freshest;
|
|
4514
|
+
const collapsed = { ...rest, scope: "user" };
|
|
4515
|
+
return {
|
|
4516
|
+
...base,
|
|
4517
|
+
ok: false,
|
|
4518
|
+
recordsToWrite: [collapsed],
|
|
4519
|
+
duplicateProjectRows,
|
|
4520
|
+
staleShaRows
|
|
4521
|
+
};
|
|
4522
|
+
}
|
|
4464
4523
|
var GITIGNORE_BLOCK_LABEL = "org .gitignore managed block (.playwright-mcp/, .claude/worktrees/, scratch *.png)";
|
|
4465
4524
|
var GITIGNORE_BLOCK_FIX = "run `mmi-cli doctor` to auto-insert the `# >>> mmi-managed >>>` block (or copy it from MMI-Hub's .gitignore)";
|
|
4466
4525
|
function buildGitignoreManagedBlockCheck(input) {
|
|
@@ -4628,6 +4687,144 @@ async function runStage(config = {}, opts = {}) {
|
|
|
4628
4687
|
return { ...started, action: "run", message: `built and ${started.message}` };
|
|
4629
4688
|
}
|
|
4630
4689
|
|
|
4690
|
+
// src/train-apply.ts
|
|
4691
|
+
function resolveDeployModel(meta, repo) {
|
|
4692
|
+
const m = meta?.deployModel;
|
|
4693
|
+
if (m === "hub-serverless" || m === "serverless" || m === "tenant-container" || m === "content") return m;
|
|
4694
|
+
if (meta?.class === "content") return "content";
|
|
4695
|
+
if (repo.toLowerCase().endsWith("/mmi-hub")) return "hub-serverless";
|
|
4696
|
+
return "tenant-container";
|
|
4697
|
+
}
|
|
4698
|
+
function clean(out) {
|
|
4699
|
+
return out.trim();
|
|
4700
|
+
}
|
|
4701
|
+
function requireValue(value, label) {
|
|
4702
|
+
if (!value) throw new Error(`${label} could not be resolved`);
|
|
4703
|
+
return value;
|
|
4704
|
+
}
|
|
4705
|
+
function releaseTagFromRcTag(tag) {
|
|
4706
|
+
return tag.replace(/-rc\.\d+$/, "");
|
|
4707
|
+
}
|
|
4708
|
+
async function verifyHubDistributionIfChanged(deps, model, releaseTag) {
|
|
4709
|
+
if (model !== "hub-serverless") return;
|
|
4710
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "verify-if-changed", releaseTag, "--skip-npm-view"]);
|
|
4711
|
+
}
|
|
4712
|
+
function ensurePositiveCount(out, emptyMessage) {
|
|
4713
|
+
const count = Number.parseInt(out.trim(), 10);
|
|
4714
|
+
if (!Number.isFinite(count)) throw new Error(`could not parse ahead count: ${out.trim() || "(empty)"}`);
|
|
4715
|
+
if (count <= 0) throw new Error(emptyMessage);
|
|
4716
|
+
}
|
|
4717
|
+
async function buildTrainApplyContext(deps) {
|
|
4718
|
+
const repo = requireValue(clean(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
4719
|
+
const [owner, name] = repo.split("/");
|
|
4720
|
+
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
4721
|
+
const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
4722
|
+
const role = clean(await deps.run("gh", ["api", `orgs/${owner}/memberships/${login}`, "--jq", ".role"]));
|
|
4723
|
+
if (role !== "admin") {
|
|
4724
|
+
throw new Error(`${commandAuthorityLabel(owner)} is master-admin only; @${login} is ${role || "not an org admin"}`);
|
|
4725
|
+
}
|
|
4726
|
+
return {
|
|
4727
|
+
repo,
|
|
4728
|
+
owner,
|
|
4729
|
+
slug: name.toLowerCase(),
|
|
4730
|
+
login
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
function commandAuthorityLabel(owner) {
|
|
4734
|
+
return `${owner} release train`;
|
|
4735
|
+
}
|
|
4736
|
+
async function requireCleanTree(deps) {
|
|
4737
|
+
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
4738
|
+
if (status.trim()) throw new Error("working tree must be clean before train apply");
|
|
4739
|
+
}
|
|
4740
|
+
async function requireBranch(deps, branch) {
|
|
4741
|
+
const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
4742
|
+
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
4743
|
+
}
|
|
4744
|
+
async function dispatchDeploy(deps, ctx, stage2, ref, model) {
|
|
4745
|
+
if (model === "tenant-container") {
|
|
4746
|
+
await deps.run("gh", [
|
|
4747
|
+
"workflow",
|
|
4748
|
+
"run",
|
|
4749
|
+
"tenant-deploy.yml",
|
|
4750
|
+
"--repo",
|
|
4751
|
+
"mutmutco/MMI-Hub",
|
|
4752
|
+
"-f",
|
|
4753
|
+
`slug=${ctx.slug}`,
|
|
4754
|
+
"-f",
|
|
4755
|
+
`repo=${ctx.repo}`,
|
|
4756
|
+
"-f",
|
|
4757
|
+
`ref=${ref}`,
|
|
4758
|
+
"-f",
|
|
4759
|
+
`stage=${stage2}`
|
|
4760
|
+
]);
|
|
4761
|
+
return `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`;
|
|
4762
|
+
}
|
|
4763
|
+
if (model === "hub-serverless") {
|
|
4764
|
+
return ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
|
|
4765
|
+
}
|
|
4766
|
+
return `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`;
|
|
4767
|
+
}
|
|
4768
|
+
async function preflight(deps, ctx, stage2) {
|
|
4769
|
+
let meta = null;
|
|
4770
|
+
try {
|
|
4771
|
+
meta = JSON.parse(await deps.runSelf(["project", "get", ctx.repo]));
|
|
4772
|
+
} catch {
|
|
4773
|
+
meta = null;
|
|
4774
|
+
}
|
|
4775
|
+
const model = resolveDeployModel(meta, ctx.repo);
|
|
4776
|
+
if (model === "content") {
|
|
4777
|
+
throw new Error(`${ctx.repo} is a content repo (deployModel=content) \u2014 the release train does not apply (trunk-based; PR to main)`);
|
|
4778
|
+
}
|
|
4779
|
+
await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
|
|
4780
|
+
return model;
|
|
4781
|
+
}
|
|
4782
|
+
async function runTrainApply(command, deps) {
|
|
4783
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
4784
|
+
await requireCleanTree(deps);
|
|
4785
|
+
await deps.run("git", ["fetch", "origin"]);
|
|
4786
|
+
if (command === "rcand") {
|
|
4787
|
+
await requireBranch(deps, "development");
|
|
4788
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
4789
|
+
ensurePositiveCount(
|
|
4790
|
+
await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
|
|
4791
|
+
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
4792
|
+
);
|
|
4793
|
+
const deployModel2 = await preflight(deps, ctx, "rc");
|
|
4794
|
+
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
4795
|
+
await verifyHubDistributionIfChanged(deps, deployModel2, releaseTagFromRcTag(tag2));
|
|
4796
|
+
await deps.run("git", ["checkout", "rc"]);
|
|
4797
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
4798
|
+
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
4799
|
+
await deps.run("git", ["tag", tag2]);
|
|
4800
|
+
await deps.run("git", ["push", "origin", "rc"]);
|
|
4801
|
+
await deps.run("git", ["push", "origin", tag2]);
|
|
4802
|
+
const dispatch2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2);
|
|
4803
|
+
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, dispatch: dispatch2 };
|
|
4804
|
+
}
|
|
4805
|
+
await requireBranch(deps, "rc");
|
|
4806
|
+
ensurePositiveCount(
|
|
4807
|
+
await deps.run("git", ["rev-list", "--count", "origin/main..origin/rc"]),
|
|
4808
|
+
"nothing to release: origin/rc is not ahead of origin/main"
|
|
4809
|
+
);
|
|
4810
|
+
const deployModel = await preflight(deps, ctx, "main");
|
|
4811
|
+
await deps.run("git", ["checkout", "main"]);
|
|
4812
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
4813
|
+
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
4814
|
+
const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
4815
|
+
await verifyHubDistributionIfChanged(deps, deployModel, tag);
|
|
4816
|
+
await deps.run("git", ["tag", tag]);
|
|
4817
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
4818
|
+
await deps.run("git", ["push", "origin", tag]);
|
|
4819
|
+
await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo]);
|
|
4820
|
+
const dispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel);
|
|
4821
|
+
await deps.run("git", ["checkout", "development"]);
|
|
4822
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
4823
|
+
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
4824
|
+
await deps.run("git", ["push", "origin", "development"]);
|
|
4825
|
+
return { ...ctx, command, stage: "main", ref: "main", tag, deployModel, dispatch };
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4631
4828
|
// src/port-registry.ts
|
|
4632
4829
|
var import_node_fs3 = require("node:fs");
|
|
4633
4830
|
var BLOCK = 100;
|
|
@@ -4919,11 +5116,24 @@ var requiredProjectWorkflows = [
|
|
|
4919
5116
|
];
|
|
4920
5117
|
var requiredOrgRulesetTypes = ["pull_request", "non_fast_forward", "deletion"];
|
|
4921
5118
|
var requiredHubStatusChecks = ["cli", "infra", "docs"];
|
|
4922
|
-
var requiredActionsVariables = ["MMI_APP_ID"];
|
|
4923
|
-
var requiredActionsSecrets = ["MMI_APP_PRIVATE_KEY"];
|
|
4924
5119
|
function expectedBranches(repoClass) {
|
|
4925
5120
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
4926
5121
|
}
|
|
5122
|
+
function gcpProjectForSlug(slug) {
|
|
5123
|
+
return `mutmutco-${slug}`;
|
|
5124
|
+
}
|
|
5125
|
+
function findMissingGcpApis(declared, enabled) {
|
|
5126
|
+
const enabledSet = new Set(enabled.map((a) => a.trim().toLowerCase()).filter(Boolean));
|
|
5127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5128
|
+
const missing = [];
|
|
5129
|
+
for (const raw of declared) {
|
|
5130
|
+
const api = raw.trim().toLowerCase();
|
|
5131
|
+
if (!api || seen.has(api)) continue;
|
|
5132
|
+
seen.add(api);
|
|
5133
|
+
if (!enabledSet.has(api)) missing.push(api);
|
|
5134
|
+
}
|
|
5135
|
+
return missing;
|
|
5136
|
+
}
|
|
4927
5137
|
function safeJson2(text, fallback) {
|
|
4928
5138
|
try {
|
|
4929
5139
|
return JSON.parse(text);
|
|
@@ -5050,16 +5260,6 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
5050
5260
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
5051
5261
|
const actions = await ghJson2(deps, ["api", `repos/${repo}/actions/permissions`], {});
|
|
5052
5262
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
5053
|
-
const variables = await ghJson2(deps, ["variable", "list", "--repo", repo, "--json", "name"], []);
|
|
5054
|
-
const variableNames = new Set(variables.map((v) => v.name));
|
|
5055
|
-
for (const variable of requiredActionsVariables) {
|
|
5056
|
-
checks.push({ ok: variableNames.has(variable), label: `Actions variable exists: ${variable}` });
|
|
5057
|
-
}
|
|
5058
|
-
const secrets2 = await ghJson2(deps, ["secret", "list", "--repo", repo, "--json", "name"], []);
|
|
5059
|
-
const secretNames = new Set(secrets2.map((s) => s.name));
|
|
5060
|
-
for (const secret of requiredActionsSecrets) {
|
|
5061
|
-
checks.push({ ok: secretNames.has(secret), label: `Actions secret exists: ${secret}` });
|
|
5062
|
-
}
|
|
5063
5263
|
const config = safeJson2(await contentText(deps, repo, baseBranch, ".mmi/config.json") || "", null);
|
|
5064
5264
|
checks.push({
|
|
5065
5265
|
ok: Boolean(config?.projectOwner && config?.projectNumber),
|
|
@@ -5170,6 +5370,26 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
5170
5370
|
detail: optionDetail(missing)
|
|
5171
5371
|
});
|
|
5172
5372
|
}
|
|
5373
|
+
const declaredApis = (deps.requiredGcpApis ?? []).filter((a) => a && a.trim());
|
|
5374
|
+
if (declaredApis.length > 0) {
|
|
5375
|
+
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
5376
|
+
const gcpProject = gcpProjectForSlug(slug);
|
|
5377
|
+
const enabled = deps.listEnabledGcpApis ? await deps.listEnabledGcpApis(gcpProject) : null;
|
|
5378
|
+
if (enabled == null) {
|
|
5379
|
+
checks.push({
|
|
5380
|
+
ok: false,
|
|
5381
|
+
label: `required GCP APIs enabled in ${gcpProject}`,
|
|
5382
|
+
detail: `could not list enabled APIs (gcloud unavailable, not authed, or no project ${gcpProject})`
|
|
5383
|
+
});
|
|
5384
|
+
} else {
|
|
5385
|
+
const missing = findMissingGcpApis(declaredApis, enabled);
|
|
5386
|
+
checks.push({
|
|
5387
|
+
ok: missing.length === 0,
|
|
5388
|
+
label: `required GCP APIs enabled in ${gcpProject}`,
|
|
5389
|
+
detail: missing.length ? `disabled: ${missing.join(", ")}` : void 0
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5173
5393
|
const waived = applyWaivers(checks, config?.verifyWaivers ?? []);
|
|
5174
5394
|
return { ok: waived.every((c) => c.ok || c.waived), repo, class: repoClass, baseBranch, checks: waived };
|
|
5175
5395
|
}
|
|
@@ -5373,8 +5593,8 @@ function resolveKbSource(rawBase) {
|
|
|
5373
5593
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
5374
5594
|
}
|
|
5375
5595
|
function buildKbGetArgs(src, path) {
|
|
5376
|
-
const
|
|
5377
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
5596
|
+
const clean2 = path.replace(/^\/+/, "");
|
|
5597
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean2}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
5378
5598
|
}
|
|
5379
5599
|
function buildKbTreeArgs(src) {
|
|
5380
5600
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -5686,6 +5906,46 @@ async function secretsList(deps, opts) {
|
|
|
5686
5906
|
const { secrets: secrets2 } = await res.json();
|
|
5687
5907
|
deps.log(formatSecretList(secrets2 ?? []));
|
|
5688
5908
|
}
|
|
5909
|
+
var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
5910
|
+
function stringList(v) {
|
|
5911
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && isValidSecretKey(x)) : [];
|
|
5912
|
+
}
|
|
5913
|
+
function requiredRuntimeSecretNames(stage2, contract) {
|
|
5914
|
+
const extra = Array.isArray(contract) ? stringList(contract) : stringList(contract?.[stage2]);
|
|
5915
|
+
return [.../* @__PURE__ */ new Set([...DEFAULT_RUNTIME_SECRET_NAMES, ...extra])];
|
|
5916
|
+
}
|
|
5917
|
+
function stageKey(stage2, key) {
|
|
5918
|
+
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
5919
|
+
}
|
|
5920
|
+
async function secretsPreflight(deps, opts) {
|
|
5921
|
+
const repo = await targetRepo(deps, opts);
|
|
5922
|
+
const qs = new URLSearchParams({ repo }).toString();
|
|
5923
|
+
let res;
|
|
5924
|
+
try {
|
|
5925
|
+
res = await deps.fetch(`${deps.apiUrl}/secrets/list?${qs}`, {
|
|
5926
|
+
method: "GET",
|
|
5927
|
+
headers: await deps.headers(),
|
|
5928
|
+
signal: AbortSignal.timeout(TIMEOUT_MS2)
|
|
5929
|
+
});
|
|
5930
|
+
} catch (e) {
|
|
5931
|
+
deps.err(`secrets preflight: ${e.message}`);
|
|
5932
|
+
return false;
|
|
5933
|
+
}
|
|
5934
|
+
if (!res.ok) {
|
|
5935
|
+
deps.err(`secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
|
|
5936
|
+
return false;
|
|
5937
|
+
}
|
|
5938
|
+
const { secrets: secrets2 } = await res.json();
|
|
5939
|
+
const present = new Set((secrets2 ?? []).map((s) => s.key));
|
|
5940
|
+
const required = opts.required.map((key) => stageKey(opts.stage, key));
|
|
5941
|
+
const missing = required.filter((key) => !present.has(key));
|
|
5942
|
+
if (missing.length) {
|
|
5943
|
+
deps.log(`missing ${missing.join(", ")}`);
|
|
5944
|
+
return false;
|
|
5945
|
+
}
|
|
5946
|
+
deps.log(`all required ${opts.stage} secret names are present`);
|
|
5947
|
+
return true;
|
|
5948
|
+
}
|
|
5689
5949
|
async function secretsGet(deps, key, opts) {
|
|
5690
5950
|
if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
5691
5951
|
const repo = await targetRepo(deps, opts);
|
|
@@ -6272,6 +6532,15 @@ async function probeBackend(url) {
|
|
|
6272
6532
|
return false;
|
|
6273
6533
|
}
|
|
6274
6534
|
}
|
|
6535
|
+
async function probeSagaAccess(url, key) {
|
|
6536
|
+
try {
|
|
6537
|
+
const qs = new URLSearchParams(key).toString();
|
|
6538
|
+
const res = await fetch(`${url}/saga/state?${qs}`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(8e3) });
|
|
6539
|
+
return res.ok;
|
|
6540
|
+
} catch {
|
|
6541
|
+
return false;
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6275
6544
|
saga.command("health").option("--json", "machine-readable output").option("--banner", "one-line SessionStart banner; silent when healthy").option("--quiet", "suppress detail output").description("zero-write health check: auth, backend reachability, resolved key").action(async (o) => {
|
|
6276
6545
|
const cfg = await loadConfig();
|
|
6277
6546
|
const session = resolveSessionId();
|
|
@@ -6279,7 +6548,8 @@ saga.command("health").option("--json", "machine-readable output").option("--ban
|
|
|
6279
6548
|
const source = session.source;
|
|
6280
6549
|
const identity = await githubLogin();
|
|
6281
6550
|
const reachable = cfg.sagaApiUrl ? await probeBackend(cfg.sagaApiUrl) : false;
|
|
6282
|
-
const
|
|
6551
|
+
const authorized = cfg.sagaApiUrl && reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
|
|
6552
|
+
const report = buildHealth({ key, source, identity, reachable, authorized, sagaApiUrl: cfg.sagaApiUrl });
|
|
6283
6553
|
if (o.json) return console.log(JSON.stringify(report));
|
|
6284
6554
|
if (o.banner) {
|
|
6285
6555
|
const banner = healthBanner(report);
|
|
@@ -6516,6 +6786,22 @@ async function withSecrets(run) {
|
|
|
6516
6786
|
var secrets = program2.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
|
|
6517
6787
|
secrets.command("where").description("print where this repo\u2019s secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsWhere(d, o)));
|
|
6518
6788
|
secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
|
|
6789
|
+
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
|
|
6790
|
+
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
6791
|
+
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
6792
|
+
}
|
|
6793
|
+
const cfg = await loadConfig();
|
|
6794
|
+
if (!cfg.sagaApiUrl) {
|
|
6795
|
+
fail("secrets: sagaApiUrl not configured in .mmi/config.json (this repo is not bootstrapped)");
|
|
6796
|
+
return;
|
|
6797
|
+
}
|
|
6798
|
+
const d = makeSecretsDeps(cfg);
|
|
6799
|
+
const repo = o.repo ?? `mutmutco/${await d.slug()}`;
|
|
6800
|
+
const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(cfg));
|
|
6801
|
+
const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets);
|
|
6802
|
+
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
|
|
6803
|
+
if (!ok) process.exitCode = 1;
|
|
6804
|
+
});
|
|
6519
6805
|
secrets.command("get <key>").description("print one secret value over TLS (prints once, raw \u2014 do not log/paste it)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsGet(d, key, o)));
|
|
6520
6806
|
secrets.command("set <key>").description("write/rotate a secret; value is read from stdin (never an argument)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsSet(d, key, o)));
|
|
6521
6807
|
secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsEdit(d, key, o)));
|
|
@@ -6548,7 +6834,7 @@ project.command("list").description("list all projects (identity + board, never
|
|
|
6548
6834
|
return;
|
|
6549
6835
|
}
|
|
6550
6836
|
for (const p of projects) {
|
|
6551
|
-
console.log(`${p.slug ?? "?"} - ${p.name ?? ""}${p.division ? ` [${p.division}]` : ""}${p.class ? ` (${p.class})` : ""}`);
|
|
6837
|
+
console.log(`${p.slug ?? "?"} - ${p.name ?? ""}${p.division ? ` [${p.division}]` : ""}${p.class ? ` (${p.class})` : ""}${p.deployModel ? ` {${p.deployModel}}` : ""}`);
|
|
6552
6838
|
}
|
|
6553
6839
|
});
|
|
6554
6840
|
project.command("get <owner/repo>").description("a project's META (board ids + pointers) by repo or slug \u2014 identity, NOT deploy coords").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
@@ -6566,7 +6852,7 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
6566
6852
|
}
|
|
6567
6853
|
fail(msg);
|
|
6568
6854
|
});
|
|
6569
|
-
project.command("set <owner/repo>").description("MASTER-ONLY: upsert a project META (idempotent merge; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--var <KEY=VALUE...>", "META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
6855
|
+
project.command("set <owner/repo>").description("MASTER-ONLY: upsert a project META (idempotent merge; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--deploy-model <model>", "hub-serverless | serverless | tenant-container | content (release-train deploy path, #514)").option("--var <KEY=VALUE...>", "META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
6570
6856
|
const cfg = await loadConfig();
|
|
6571
6857
|
const slug = slugOf(repoOrSlug);
|
|
6572
6858
|
const patch = {};
|
|
@@ -6574,6 +6860,11 @@ project.command("set <owner/repo>").description("MASTER-ONLY: upsert a project M
|
|
|
6574
6860
|
if (o.class !== "deployable" && o.class !== "content") return fail("project set: --class must be deployable or content");
|
|
6575
6861
|
patch.class = o.class;
|
|
6576
6862
|
}
|
|
6863
|
+
if (o.deployModel) {
|
|
6864
|
+
const models = ["hub-serverless", "serverless", "tenant-container", "content"];
|
|
6865
|
+
if (!models.includes(o.deployModel)) return fail(`project set: --deploy-model must be one of: ${models.join(", ")}`);
|
|
6866
|
+
patch.deployModel = o.deployModel;
|
|
6867
|
+
}
|
|
6577
6868
|
for (let i = 0; i < process.argv.length - 1; i++) {
|
|
6578
6869
|
if (process.argv[i] === "--var") {
|
|
6579
6870
|
const eq = process.argv[i + 1].indexOf("=");
|
|
@@ -6660,8 +6951,10 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
6660
6951
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
6661
6952
|
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>", "urgent | high | medium | low (label + board Priority field when configured)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--no-related", "skip the auto related-issues comment").action(async (o) => {
|
|
6662
6953
|
let args;
|
|
6954
|
+
let priority;
|
|
6663
6955
|
try {
|
|
6664
|
-
|
|
6956
|
+
priority = normalizePriority(o.priority);
|
|
6957
|
+
args = buildIssueArgs({ type: o.type, title: o.title, body: o.body, priority, repo: o.repo, labels: o.label });
|
|
6665
6958
|
} catch (e) {
|
|
6666
6959
|
return fail(`issue create: ${e.message}`);
|
|
6667
6960
|
}
|
|
@@ -6674,9 +6967,9 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
6674
6967
|
}
|
|
6675
6968
|
}
|
|
6676
6969
|
const created = await ghCreate(args);
|
|
6677
|
-
const projectItemId = await attachToProject(created.number, o.repo,
|
|
6970
|
+
const projectItemId = await attachToProject(created.number, o.repo, priority);
|
|
6678
6971
|
if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title: o.title, body: o.body });
|
|
6679
|
-
console.log(JSON.stringify({ ...created, label: o.type, priority
|
|
6972
|
+
console.log(JSON.stringify({ ...created, label: o.type, priority, projectItemId }));
|
|
6680
6973
|
});
|
|
6681
6974
|
issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
|
|
6682
6975
|
const number = Number(o.number);
|
|
@@ -6918,13 +7211,25 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
6918
7211
|
}
|
|
6919
7212
|
});
|
|
6920
7213
|
program2.command("stage-live").description("explain that remote rc/live environments use /rcand, /release, and /hotfix; /stage is local only").option("--json", "machine-readable output").option("--apply", "always refused; there is no stage-live mutation path").action((o) => {
|
|
6921
|
-
if (o.apply) return fail("stage-live: not an org command; use mmi-cli stage for local tests, or the gated
|
|
7214
|
+
if (o.apply) return fail("stage-live: not an org command; use mmi-cli stage for local tests, or the gated rcand/release/hotfix train for remote environments");
|
|
6922
7215
|
const steps = stageLivePlan();
|
|
6923
7216
|
console.log(o.json ? JSON.stringify({ command: "stage-live", steps }, null, 2) : renderSteps("mmi-cli stage-live: not an org command", steps));
|
|
6924
7217
|
});
|
|
6925
|
-
for (const commandName of ["
|
|
6926
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit approval`).option("--json", "machine-readable output").option("--apply", "reserved
|
|
6927
|
-
if (o.apply)
|
|
7218
|
+
for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
7219
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--apply", commandName === "hotfix" ? "reserved; hotfix uses the /hotfix skill PR path" : "execute the guarded master-only train after explicit approval").action(async (o) => {
|
|
7220
|
+
if (o.apply) {
|
|
7221
|
+
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
7222
|
+
try {
|
|
7223
|
+
const result = await runTrainApply(commandName, {
|
|
7224
|
+
run: async (file, args) => (await execFileP3(file, args, { timeout: file === "gh" ? 3e4 : GIT_TIMEOUT_MS })).stdout,
|
|
7225
|
+
runSelf: async (args) => (await execFileP3(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout
|
|
7226
|
+
});
|
|
7227
|
+
const message = `mmi-cli ${commandName}: applied ${result.repo} ${result.stage} train at ${result.tag} [${result.deployModel}]; ${result.dispatch}`;
|
|
7228
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : message);
|
|
7229
|
+
} catch (e) {
|
|
7230
|
+
return fail(`${commandName}: ${e.message}`);
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
6928
7233
|
const steps = trainPlan(commandName);
|
|
6929
7234
|
console.log(o.json ? JSON.stringify({ command: commandName, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps));
|
|
6930
7235
|
});
|
|
@@ -6941,9 +7246,31 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
6941
7246
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap verify: --class must be deployable or content");
|
|
6942
7247
|
const cfg = await loadConfig();
|
|
6943
7248
|
const apiProjects = await fetchProjectsJson({ baseUrl: cfg.sagaApiUrl, token: githubToken });
|
|
7249
|
+
const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
7250
|
+
const meta = await fetchProjectBySlug(slug, { baseUrl: cfg.sagaApiUrl, token: githubToken });
|
|
6944
7251
|
const report = await verifyBootstrap(repo, o.class, {
|
|
6945
7252
|
gh: async (args) => execFileP3("gh", args, { timeout: 2e4 }),
|
|
6946
|
-
readLocalFile: (path) => path === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : null
|
|
7253
|
+
readLocalFile: (path) => path === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : null,
|
|
7254
|
+
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
7255
|
+
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
7256
|
+
requiredGcpApis: (() => {
|
|
7257
|
+
const v = meta?.requiredGcpApis;
|
|
7258
|
+
if (Array.isArray(v)) return v;
|
|
7259
|
+
if (typeof v === "string") return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7260
|
+
return void 0;
|
|
7261
|
+
})(),
|
|
7262
|
+
listEnabledGcpApis: async (gcpProject) => {
|
|
7263
|
+
try {
|
|
7264
|
+
const { stdout } = await execFileP3(
|
|
7265
|
+
"gcloud",
|
|
7266
|
+
["services", "list", "--enabled", "--project", gcpProject, "--format", "value(config.name)"],
|
|
7267
|
+
{ timeout: 3e4 }
|
|
7268
|
+
);
|
|
7269
|
+
return stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
7270
|
+
} catch {
|
|
7271
|
+
return null;
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
6947
7274
|
});
|
|
6948
7275
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
6949
7276
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -7100,6 +7427,21 @@ function writeProjectInstallRecord(record) {
|
|
|
7100
7427
|
return false;
|
|
7101
7428
|
}
|
|
7102
7429
|
}
|
|
7430
|
+
function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
7431
|
+
try {
|
|
7432
|
+
const file = readInstalledPlugins();
|
|
7433
|
+
if (!file) return false;
|
|
7434
|
+
if (!file.plugins) file.plugins = {};
|
|
7435
|
+
const path = installedPluginsPath();
|
|
7436
|
+
(0, import_node_fs4.copyFileSync)(path, `${path}.bak`);
|
|
7437
|
+
file.plugins[pluginId] = records;
|
|
7438
|
+
(0, import_node_fs4.writeFileSync)(path, `${JSON.stringify(file, null, 2)}
|
|
7439
|
+
`, "utf8");
|
|
7440
|
+
return true;
|
|
7441
|
+
} catch {
|
|
7442
|
+
return false;
|
|
7443
|
+
}
|
|
7444
|
+
}
|
|
7103
7445
|
var gitignorePath = () => (0, import_node_path4.join)(process.cwd(), ".gitignore");
|
|
7104
7446
|
function readGitignore() {
|
|
7105
7447
|
try {
|
|
@@ -7116,7 +7458,12 @@ function writeGitignore(content) {
|
|
|
7116
7458
|
return false;
|
|
7117
7459
|
}
|
|
7118
7460
|
}
|
|
7119
|
-
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, repo config, plugin git clone, plugin install record, .gitignore managed block) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--json", "machine-readable output").action(async (opts) => {
|
|
7461
|
+
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, repo config, plugin git clone, plugin install record, .gitignore managed block, plugin config drift) 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(async (opts) => {
|
|
7462
|
+
if (opts.guide) {
|
|
7463
|
+
if (opts.json) console.log(JSON.stringify({ resources: [MMI_AGENTIC_ONBOARDING_GUIDE] }, null, 2));
|
|
7464
|
+
else console.log(MMI_AGENTIC_ONBOARDING_GUIDE.url);
|
|
7465
|
+
return;
|
|
7466
|
+
}
|
|
7120
7467
|
const checks = [];
|
|
7121
7468
|
const login = await githubLogin();
|
|
7122
7469
|
let ghInstalled = true;
|
|
@@ -7189,17 +7536,27 @@ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi
|
|
|
7189
7536
|
}
|
|
7190
7537
|
}
|
|
7191
7538
|
checks.push(gitignoreCheck);
|
|
7539
|
+
let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), installed });
|
|
7540
|
+
if (!driftCheck.ok && driftCheck.recordsToWrite && !opts.json) {
|
|
7541
|
+
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
7542
|
+
driftCheck = { ...driftCheck, ok: true };
|
|
7543
|
+
if (!opts.banner) console.error(" \u21BB repaired: collapsed mmi@mmi to one user-scope entry (backup at installed_plugins.json.bak) \u2014 run /reload-plugins");
|
|
7544
|
+
}
|
|
7545
|
+
}
|
|
7546
|
+
checks.push(driftCheck);
|
|
7192
7547
|
const gaps = checks.filter((c) => !c.ok);
|
|
7548
|
+
const resources = doctorResourcesForGaps(gaps);
|
|
7193
7549
|
if (opts.json) {
|
|
7194
|
-
console.log(JSON.stringify({ ok: gaps.length === 0, checks }, null, 2));
|
|
7550
|
+
console.log(JSON.stringify({ ok: gaps.length === 0, checks, ...resources.length ? { resources } : {} }, null, 2));
|
|
7195
7551
|
return;
|
|
7196
7552
|
}
|
|
7197
7553
|
if (opts.banner) {
|
|
7198
|
-
if (gaps.length) console.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")}`);
|
|
7554
|
+
if (gaps.length) console.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
|
|
7199
7555
|
return;
|
|
7200
7556
|
}
|
|
7201
7557
|
for (const c of checks) console.log(c.ok ? `\u2713 ${c.label}` : `\u2717 ${c.label}
|
|
7202
7558
|
\u2192 ${c.fix}`);
|
|
7559
|
+
for (const r of resources) console.log(`Resource: ${r.label} \u2014 ${r.url}`);
|
|
7203
7560
|
console.log(gaps.length ? `
|
|
7204
7561
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
7205
7562
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|