@mutmutco/cli 2.1.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/dist/index.cjs +229 -51
- package/package.json +1 -1
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
|
|
@@ -4412,6 +4419,13 @@ ${block}
|
|
|
4412
4419
|
// src/doctor.ts
|
|
4413
4420
|
var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
|
|
4414
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
|
+
}
|
|
4415
4429
|
function buildGithubAuthCheck(input) {
|
|
4416
4430
|
const ok = Boolean(input.login?.trim());
|
|
4417
4431
|
return {
|
|
@@ -4467,6 +4481,45 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
4467
4481
|
recordToInsert
|
|
4468
4482
|
};
|
|
4469
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
|
+
}
|
|
4470
4523
|
var GITIGNORE_BLOCK_LABEL = "org .gitignore managed block (.playwright-mcp/, .claude/worktrees/, scratch *.png)";
|
|
4471
4524
|
var GITIGNORE_BLOCK_FIX = "run `mmi-cli doctor` to auto-insert the `# >>> mmi-managed >>>` block (or copy it from MMI-Hub's .gitignore)";
|
|
4472
4525
|
function buildGitignoreManagedBlockCheck(input) {
|
|
@@ -4635,6 +4688,13 @@ async function runStage(config = {}, opts = {}) {
|
|
|
4635
4688
|
}
|
|
4636
4689
|
|
|
4637
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
|
+
}
|
|
4638
4698
|
function clean(out) {
|
|
4639
4699
|
return out.trim();
|
|
4640
4700
|
}
|
|
@@ -4642,6 +4702,13 @@ function requireValue(value, label) {
|
|
|
4642
4702
|
if (!value) throw new Error(`${label} could not be resolved`);
|
|
4643
4703
|
return value;
|
|
4644
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
|
+
}
|
|
4645
4712
|
function ensurePositiveCount(out, emptyMessage) {
|
|
4646
4713
|
const count = Number.parseInt(out.trim(), 10);
|
|
4647
4714
|
if (!Number.isFinite(count)) throw new Error(`could not parse ahead count: ${out.trim() || "(empty)"}`);
|
|
@@ -4674,26 +4741,43 @@ async function requireBranch(deps, branch) {
|
|
|
4674
4741
|
const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
4675
4742
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
4676
4743
|
}
|
|
4677
|
-
async function dispatchDeploy(deps, ctx, stage2, ref) {
|
|
4678
|
-
|
|
4679
|
-
"
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
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`;
|
|
4693
4767
|
}
|
|
4694
4768
|
async function preflight(deps, ctx, stage2) {
|
|
4695
|
-
|
|
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
|
+
}
|
|
4696
4779
|
await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
|
|
4780
|
+
return model;
|
|
4697
4781
|
}
|
|
4698
4782
|
async function runTrainApply(command, deps) {
|
|
4699
4783
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -4706,37 +4790,39 @@ async function runTrainApply(command, deps) {
|
|
|
4706
4790
|
await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
|
|
4707
4791
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
4708
4792
|
);
|
|
4709
|
-
await preflight(deps, ctx, "rc");
|
|
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));
|
|
4710
4796
|
await deps.run("git", ["checkout", "rc"]);
|
|
4711
4797
|
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
4712
4798
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
4713
|
-
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
4714
4799
|
await deps.run("git", ["tag", tag2]);
|
|
4715
4800
|
await deps.run("git", ["push", "origin", "rc"]);
|
|
4716
4801
|
await deps.run("git", ["push", "origin", tag2]);
|
|
4717
|
-
await dispatchDeploy(deps, ctx, "rc", "rc");
|
|
4718
|
-
return { ...ctx, command, stage: "rc", ref: "rc", tag: 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 };
|
|
4719
4804
|
}
|
|
4720
4805
|
await requireBranch(deps, "rc");
|
|
4721
4806
|
ensurePositiveCount(
|
|
4722
4807
|
await deps.run("git", ["rev-list", "--count", "origin/main..origin/rc"]),
|
|
4723
4808
|
"nothing to release: origin/rc is not ahead of origin/main"
|
|
4724
4809
|
);
|
|
4725
|
-
await preflight(deps, ctx, "main");
|
|
4810
|
+
const deployModel = await preflight(deps, ctx, "main");
|
|
4726
4811
|
await deps.run("git", ["checkout", "main"]);
|
|
4727
4812
|
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
4728
4813
|
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
4729
4814
|
const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
4815
|
+
await verifyHubDistributionIfChanged(deps, deployModel, tag);
|
|
4730
4816
|
await deps.run("git", ["tag", tag]);
|
|
4731
4817
|
await deps.run("git", ["push", "origin", "main"]);
|
|
4732
4818
|
await deps.run("git", ["push", "origin", tag]);
|
|
4733
4819
|
await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo]);
|
|
4734
|
-
await dispatchDeploy(deps, ctx, "main", "main");
|
|
4820
|
+
const dispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel);
|
|
4735
4821
|
await deps.run("git", ["checkout", "development"]);
|
|
4736
4822
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
4737
4823
|
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
4738
4824
|
await deps.run("git", ["push", "origin", "development"]);
|
|
4739
|
-
return { ...ctx, command, stage: "main", ref: "main", tag };
|
|
4825
|
+
return { ...ctx, command, stage: "main", ref: "main", tag, deployModel, dispatch };
|
|
4740
4826
|
}
|
|
4741
4827
|
|
|
4742
4828
|
// src/port-registry.ts
|
|
@@ -5030,11 +5116,24 @@ var requiredProjectWorkflows = [
|
|
|
5030
5116
|
];
|
|
5031
5117
|
var requiredOrgRulesetTypes = ["pull_request", "non_fast_forward", "deletion"];
|
|
5032
5118
|
var requiredHubStatusChecks = ["cli", "infra", "docs"];
|
|
5033
|
-
var requiredActionsVariables = ["MMI_APP_ID"];
|
|
5034
|
-
var requiredActionsSecrets = ["MMI_APP_PRIVATE_KEY"];
|
|
5035
5119
|
function expectedBranches(repoClass) {
|
|
5036
5120
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
5037
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
|
+
}
|
|
5038
5137
|
function safeJson2(text, fallback) {
|
|
5039
5138
|
try {
|
|
5040
5139
|
return JSON.parse(text);
|
|
@@ -5161,16 +5260,6 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
5161
5260
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
5162
5261
|
const actions = await ghJson2(deps, ["api", `repos/${repo}/actions/permissions`], {});
|
|
5163
5262
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
5164
|
-
const variables = await ghJson2(deps, ["variable", "list", "--repo", repo, "--json", "name"], []);
|
|
5165
|
-
const variableNames = new Set(variables.map((v) => v.name));
|
|
5166
|
-
for (const variable of requiredActionsVariables) {
|
|
5167
|
-
checks.push({ ok: variableNames.has(variable), label: `Actions variable exists: ${variable}` });
|
|
5168
|
-
}
|
|
5169
|
-
const secrets2 = await ghJson2(deps, ["secret", "list", "--repo", repo, "--json", "name"], []);
|
|
5170
|
-
const secretNames = new Set(secrets2.map((s) => s.name));
|
|
5171
|
-
for (const secret of requiredActionsSecrets) {
|
|
5172
|
-
checks.push({ ok: secretNames.has(secret), label: `Actions secret exists: ${secret}` });
|
|
5173
|
-
}
|
|
5174
5263
|
const config = safeJson2(await contentText(deps, repo, baseBranch, ".mmi/config.json") || "", null);
|
|
5175
5264
|
checks.push({
|
|
5176
5265
|
ok: Boolean(config?.projectOwner && config?.projectNumber),
|
|
@@ -5281,6 +5370,26 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
5281
5370
|
detail: optionDetail(missing)
|
|
5282
5371
|
});
|
|
5283
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
|
+
}
|
|
5284
5393
|
const waived = applyWaivers(checks, config?.verifyWaivers ?? []);
|
|
5285
5394
|
return { ok: waived.every((c) => c.ok || c.waived), repo, class: repoClass, baseBranch, checks: waived };
|
|
5286
5395
|
}
|
|
@@ -6423,6 +6532,15 @@ async function probeBackend(url) {
|
|
|
6423
6532
|
return false;
|
|
6424
6533
|
}
|
|
6425
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
|
+
}
|
|
6426
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) => {
|
|
6427
6545
|
const cfg = await loadConfig();
|
|
6428
6546
|
const session = resolveSessionId();
|
|
@@ -6430,7 +6548,8 @@ saga.command("health").option("--json", "machine-readable output").option("--ban
|
|
|
6430
6548
|
const source = session.source;
|
|
6431
6549
|
const identity = await githubLogin();
|
|
6432
6550
|
const reachable = cfg.sagaApiUrl ? await probeBackend(cfg.sagaApiUrl) : false;
|
|
6433
|
-
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 });
|
|
6434
6553
|
if (o.json) return console.log(JSON.stringify(report));
|
|
6435
6554
|
if (o.banner) {
|
|
6436
6555
|
const banner = healthBanner(report);
|
|
@@ -6715,7 +6834,7 @@ project.command("list").description("list all projects (identity + board, never
|
|
|
6715
6834
|
return;
|
|
6716
6835
|
}
|
|
6717
6836
|
for (const p of projects) {
|
|
6718
|
-
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}}` : ""}`);
|
|
6719
6838
|
}
|
|
6720
6839
|
});
|
|
6721
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) => {
|
|
@@ -6733,7 +6852,7 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
6733
6852
|
}
|
|
6734
6853
|
fail(msg);
|
|
6735
6854
|
});
|
|
6736
|
-
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) => {
|
|
6737
6856
|
const cfg = await loadConfig();
|
|
6738
6857
|
const slug = slugOf(repoOrSlug);
|
|
6739
6858
|
const patch = {};
|
|
@@ -6741,6 +6860,11 @@ project.command("set <owner/repo>").description("MASTER-ONLY: upsert a project M
|
|
|
6741
6860
|
if (o.class !== "deployable" && o.class !== "content") return fail("project set: --class must be deployable or content");
|
|
6742
6861
|
patch.class = o.class;
|
|
6743
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
|
+
}
|
|
6744
6868
|
for (let i = 0; i < process.argv.length - 1; i++) {
|
|
6745
6869
|
if (process.argv[i] === "--var") {
|
|
6746
6870
|
const eq = process.argv[i + 1].indexOf("=");
|
|
@@ -6827,8 +6951,10 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
6827
6951
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
6828
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) => {
|
|
6829
6953
|
let args;
|
|
6954
|
+
let priority;
|
|
6830
6955
|
try {
|
|
6831
|
-
|
|
6956
|
+
priority = normalizePriority(o.priority);
|
|
6957
|
+
args = buildIssueArgs({ type: o.type, title: o.title, body: o.body, priority, repo: o.repo, labels: o.label });
|
|
6832
6958
|
} catch (e) {
|
|
6833
6959
|
return fail(`issue create: ${e.message}`);
|
|
6834
6960
|
}
|
|
@@ -6841,9 +6967,9 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
6841
6967
|
}
|
|
6842
6968
|
}
|
|
6843
6969
|
const created = await ghCreate(args);
|
|
6844
|
-
const projectItemId = await attachToProject(created.number, o.repo,
|
|
6970
|
+
const projectItemId = await attachToProject(created.number, o.repo, priority);
|
|
6845
6971
|
if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title: o.title, body: o.body });
|
|
6846
|
-
console.log(JSON.stringify({ ...created, label: o.type, priority
|
|
6972
|
+
console.log(JSON.stringify({ ...created, label: o.type, priority, projectItemId }));
|
|
6847
6973
|
});
|
|
6848
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) => {
|
|
6849
6975
|
const number = Number(o.number);
|
|
@@ -7098,7 +7224,7 @@ for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
|
7098
7224
|
run: async (file, args) => (await execFileP3(file, args, { timeout: file === "gh" ? 3e4 : GIT_TIMEOUT_MS })).stdout,
|
|
7099
7225
|
runSelf: async (args) => (await execFileP3(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout
|
|
7100
7226
|
});
|
|
7101
|
-
const message = `mmi-cli ${commandName}: applied ${result.repo} ${result.stage} train at ${result.tag};
|
|
7227
|
+
const message = `mmi-cli ${commandName}: applied ${result.repo} ${result.stage} train at ${result.tag} [${result.deployModel}]; ${result.dispatch}`;
|
|
7102
7228
|
return printLine(o.json ? JSON.stringify(result, null, 2) : message);
|
|
7103
7229
|
} catch (e) {
|
|
7104
7230
|
return fail(`${commandName}: ${e.message}`);
|
|
@@ -7120,9 +7246,31 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
7120
7246
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap verify: --class must be deployable or content");
|
|
7121
7247
|
const cfg = await loadConfig();
|
|
7122
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 });
|
|
7123
7251
|
const report = await verifyBootstrap(repo, o.class, {
|
|
7124
7252
|
gh: async (args) => execFileP3("gh", args, { timeout: 2e4 }),
|
|
7125
|
-
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
|
+
}
|
|
7126
7274
|
});
|
|
7127
7275
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
7128
7276
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -7279,6 +7427,21 @@ function writeProjectInstallRecord(record) {
|
|
|
7279
7427
|
return false;
|
|
7280
7428
|
}
|
|
7281
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
|
+
}
|
|
7282
7445
|
var gitignorePath = () => (0, import_node_path4.join)(process.cwd(), ".gitignore");
|
|
7283
7446
|
function readGitignore() {
|
|
7284
7447
|
try {
|
|
@@ -7295,7 +7458,12 @@ function writeGitignore(content) {
|
|
|
7295
7458
|
return false;
|
|
7296
7459
|
}
|
|
7297
7460
|
}
|
|
7298
|
-
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
|
+
}
|
|
7299
7467
|
const checks = [];
|
|
7300
7468
|
const login = await githubLogin();
|
|
7301
7469
|
let ghInstalled = true;
|
|
@@ -7368,17 +7536,27 @@ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi
|
|
|
7368
7536
|
}
|
|
7369
7537
|
}
|
|
7370
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);
|
|
7371
7547
|
const gaps = checks.filter((c) => !c.ok);
|
|
7548
|
+
const resources = doctorResourcesForGaps(gaps);
|
|
7372
7549
|
if (opts.json) {
|
|
7373
|
-
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));
|
|
7374
7551
|
return;
|
|
7375
7552
|
}
|
|
7376
7553
|
if (opts.banner) {
|
|
7377
|
-
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}`);
|
|
7378
7555
|
return;
|
|
7379
7556
|
}
|
|
7380
7557
|
for (const c of checks) console.log(c.ok ? `\u2713 ${c.label}` : `\u2717 ${c.label}
|
|
7381
7558
|
\u2192 ${c.fix}`);
|
|
7559
|
+
for (const r of resources) console.log(`Resource: ${r.label} \u2014 ${r.url}`);
|
|
7382
7560
|
console.log(gaps.length ? `
|
|
7383
7561
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
7384
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",
|