@mutmutco/cli 2.15.0 → 2.16.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 +796 -166
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3393,7 +3393,7 @@ var program = new Command();
|
|
|
3393
3393
|
|
|
3394
3394
|
// src/index.ts
|
|
3395
3395
|
var import_promises2 = require("node:fs/promises");
|
|
3396
|
-
var
|
|
3396
|
+
var import_node_fs7 = require("node:fs");
|
|
3397
3397
|
var import_node_crypto3 = require("node:crypto");
|
|
3398
3398
|
|
|
3399
3399
|
// src/rules-sync.ts
|
|
@@ -3473,7 +3473,10 @@ function buildSessionStartPlan(verbs) {
|
|
|
3473
3473
|
parallel: [
|
|
3474
3474
|
{ name: "rules sync", run: verbs.rulesSync },
|
|
3475
3475
|
{ name: "saga show", run: verbs.sagaShow },
|
|
3476
|
-
{ name: "saga health", run: verbs.sagaHealth }
|
|
3476
|
+
{ name: "saga health", run: verbs.sagaHealth },
|
|
3477
|
+
// whoami (#879): the resolved human lands in the banner so agents act --for them without asking.
|
|
3478
|
+
// Identity reads are memoized process-wide, so this adds no extra gh/Hub round-trip.
|
|
3479
|
+
{ name: "whoami", run: verbs.whoami }
|
|
3477
3480
|
],
|
|
3478
3481
|
sequential: [{ name: "doctor", run: verbs.doctor }]
|
|
3479
3482
|
};
|
|
@@ -3488,6 +3491,29 @@ function northstarPointer() {
|
|
|
3488
3491
|
return "North Stars: run `mmi-cli northstar relevant` to load plans relevant to your task (`northstar list` for all).";
|
|
3489
3492
|
}
|
|
3490
3493
|
|
|
3494
|
+
// src/whoami.ts
|
|
3495
|
+
async function resolveWhoami(deps) {
|
|
3496
|
+
let session;
|
|
3497
|
+
try {
|
|
3498
|
+
session = await deps.hubSession();
|
|
3499
|
+
} catch {
|
|
3500
|
+
session = void 0;
|
|
3501
|
+
}
|
|
3502
|
+
if (session?.login) return { login: session.login, source: "hub-session", sessionExpiresAt: session.expiresAt };
|
|
3503
|
+
let ghLogin;
|
|
3504
|
+
try {
|
|
3505
|
+
ghLogin = await deps.ghLogin();
|
|
3506
|
+
} catch {
|
|
3507
|
+
ghLogin = void 0;
|
|
3508
|
+
}
|
|
3509
|
+
if (ghLogin) return { login: ghLogin, source: "github", sessionExpiresAt: session?.expiresAt };
|
|
3510
|
+
return { source: "unknown" };
|
|
3511
|
+
}
|
|
3512
|
+
function whoamiLine(report) {
|
|
3513
|
+
if (!report.login) return null;
|
|
3514
|
+
return `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3491
3517
|
// src/saga-capture.ts
|
|
3492
3518
|
function parseHookInput(stdin) {
|
|
3493
3519
|
try {
|
|
@@ -3501,7 +3527,7 @@ function parseHookInput(stdin) {
|
|
|
3501
3527
|
// src/index.ts
|
|
3502
3528
|
var import_node_child_process6 = require("node:child_process");
|
|
3503
3529
|
var import_node_util6 = require("node:util");
|
|
3504
|
-
var
|
|
3530
|
+
var import_node_path8 = require("node:path");
|
|
3505
3531
|
var import_node_os3 = require("node:os");
|
|
3506
3532
|
|
|
3507
3533
|
// src/saga-head-maintainer.ts
|
|
@@ -3823,6 +3849,49 @@ function defaultHubUrl() {
|
|
|
3823
3849
|
return process.env.MMI_HUB_URL || DEFAULT_HUB_URL;
|
|
3824
3850
|
}
|
|
3825
3851
|
|
|
3852
|
+
// src/client-version.ts
|
|
3853
|
+
var import_node_fs3 = require("node:fs");
|
|
3854
|
+
var import_node_path4 = require("node:path");
|
|
3855
|
+
|
|
3856
|
+
// ../infra/compat.mjs
|
|
3857
|
+
var CLIENT_VERSION_HEADER = "x-client-version";
|
|
3858
|
+
function parseSemver(s) {
|
|
3859
|
+
if (typeof s !== "string") return null;
|
|
3860
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+]|$)/.exec(s.trim());
|
|
3861
|
+
if (!m) return null;
|
|
3862
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
|
|
3863
|
+
}
|
|
3864
|
+
function versionAtLeast(v, min) {
|
|
3865
|
+
const a = parseSemver(v);
|
|
3866
|
+
const b = parseSemver(min);
|
|
3867
|
+
if (!a || !b) return false;
|
|
3868
|
+
if (a.major !== b.major) return a.major > b.major;
|
|
3869
|
+
if (a.minor !== b.minor) return a.minor > b.minor;
|
|
3870
|
+
return a.patch >= b.patch;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// src/client-version.ts
|
|
3874
|
+
function resolveClientVersion() {
|
|
3875
|
+
try {
|
|
3876
|
+
const manifest = (0, import_node_path4.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
3877
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(manifest, "utf8")).version || "0.0.0";
|
|
3878
|
+
} catch {
|
|
3879
|
+
try {
|
|
3880
|
+
const pkg = (0, import_node_path4.join)(__dirname, "..", "package.json");
|
|
3881
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
3882
|
+
} catch {
|
|
3883
|
+
return "0.0.0";
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
function clientVersionHeaders() {
|
|
3888
|
+
return { [CLIENT_VERSION_HEADER]: resolveClientVersion() };
|
|
3889
|
+
}
|
|
3890
|
+
function upgradeRequiredError(res, body) {
|
|
3891
|
+
const minVersion = body && typeof body === "object" && typeof body.minVersion === "string" ? body.minVersion : "a newer version";
|
|
3892
|
+
return `Hub requires mmi-cli >= ${minVersion} \u2014 run mmi-cli doctor (installed ${resolveClientVersion()})`;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3826
3895
|
// src/saga-health.ts
|
|
3827
3896
|
function buildHealth(i) {
|
|
3828
3897
|
const problems = [];
|
|
@@ -3885,6 +3954,30 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
3885
3954
|
throw lastErr;
|
|
3886
3955
|
}
|
|
3887
3956
|
|
|
3957
|
+
// src/clean-exit.ts
|
|
3958
|
+
function globalDispatcher() {
|
|
3959
|
+
const g = globalThis;
|
|
3960
|
+
const sym = Object.getOwnPropertySymbols(g).find(
|
|
3961
|
+
(s) => s.description === "undici.globalDispatcher.1" || s.description?.startsWith("undici.globalDispatcher.")
|
|
3962
|
+
);
|
|
3963
|
+
return sym ? g[sym] : void 0;
|
|
3964
|
+
}
|
|
3965
|
+
function destroyHttpPool() {
|
|
3966
|
+
try {
|
|
3967
|
+
const dispatcher = globalDispatcher();
|
|
3968
|
+
if (dispatcher?.destroy) {
|
|
3969
|
+
void dispatcher.destroy();
|
|
3970
|
+
return true;
|
|
3971
|
+
}
|
|
3972
|
+
} catch {
|
|
3973
|
+
}
|
|
3974
|
+
return false;
|
|
3975
|
+
}
|
|
3976
|
+
function hardExit(code) {
|
|
3977
|
+
destroyHttpPool();
|
|
3978
|
+
process.exit(code);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3888
3981
|
// src/saga-note.ts
|
|
3889
3982
|
var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
|
|
3890
3983
|
var ROUTE_LEVEL_403 = "saga API route-level 403 from HubSessionAuthorizer/session policy";
|
|
@@ -5718,6 +5811,20 @@ function buildPluginConfigDriftCheck(input) {
|
|
|
5718
5811
|
}
|
|
5719
5812
|
var GITIGNORE_BLOCK_LABEL = "org .gitignore managed block (.playwright-mcp/, .claude/worktrees/, scratch *.png)";
|
|
5720
5813
|
var GITIGNORE_BLOCK_FIX = "run `mmi-cli doctor` to auto-insert the `# >>> mmi-managed >>>` block (or copy it from MMI-Hub's .gitignore)";
|
|
5814
|
+
var GITIGNORE_BLOCK_DEFERRED_FIX = "run `mmi-cli doctor --apply` (without --no-repo-writes) after the release train to write the org-managed .gitignore block, then stage & commit .gitignore";
|
|
5815
|
+
function applyGitignoreRepoWritePolicy(check, repoWritesAllowed) {
|
|
5816
|
+
if (repoWritesAllowed || check.ok || !check.contentToWrite) return check;
|
|
5817
|
+
return { ...check, fix: GITIGNORE_BLOCK_DEFERRED_FIX };
|
|
5818
|
+
}
|
|
5819
|
+
function decideGitignoreRepair(check, flags) {
|
|
5820
|
+
if (check.ok || !check.contentToWrite) return { action: "none", check };
|
|
5821
|
+
if (!flags.repoWritesAllowed) {
|
|
5822
|
+
if (!flags.repairFull) return { action: "none", check };
|
|
5823
|
+
return { action: "suppress", check: applyGitignoreRepoWritePolicy(check, false) };
|
|
5824
|
+
}
|
|
5825
|
+
if (flags.repairFull) return { action: "write", check };
|
|
5826
|
+
return { action: "none", check };
|
|
5827
|
+
}
|
|
5721
5828
|
function buildGitignoreManagedBlockCheck(input) {
|
|
5722
5829
|
const base = { ok: true, label: GITIGNORE_BLOCK_LABEL, fix: GITIGNORE_BLOCK_FIX };
|
|
5723
5830
|
if (!input.isOrgRepo) return base;
|
|
@@ -5812,6 +5919,57 @@ function pluginRecoveryFix(surface) {
|
|
|
5812
5919
|
return "npm install -g @mutmutco/cli@latest";
|
|
5813
5920
|
}
|
|
5814
5921
|
}
|
|
5922
|
+
var PLUGIN_UPDATE_RECIPES = {
|
|
5923
|
+
claude: ["claude plugin update mmi@mmi"],
|
|
5924
|
+
codex: ["codex plugin marketplace upgrade mmi", "codex plugin list # verify mmi@mmi shows the new version"],
|
|
5925
|
+
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
5926
|
+
};
|
|
5927
|
+
function highestSemver(versions) {
|
|
5928
|
+
return versions.reduce((best, v) => {
|
|
5929
|
+
if (!isSemverVersion(v)) return best;
|
|
5930
|
+
if (best === void 0) return v;
|
|
5931
|
+
return compareVersions(v, best) > 0 ? v : best;
|
|
5932
|
+
}, void 0);
|
|
5933
|
+
}
|
|
5934
|
+
function buildPluginUpdateReport(input) {
|
|
5935
|
+
const claudePlugin = highestSemver(input.claudePluginVersions ?? []);
|
|
5936
|
+
const codexMarketplace = highestSemver(input.codexPluginVersions ?? []);
|
|
5937
|
+
const codexActiveCache = highestSemver(input.codexCacheVersions ?? []);
|
|
5938
|
+
return {
|
|
5939
|
+
versions: {
|
|
5940
|
+
...isSemverVersion(input.cliVersion) ? { cli: input.cliVersion } : {},
|
|
5941
|
+
...claudePlugin ? { claudePlugin } : {},
|
|
5942
|
+
...codexMarketplace ? { codexMarketplace } : {},
|
|
5943
|
+
...codexActiveCache ? { codexActiveCache } : {},
|
|
5944
|
+
...isSemverVersion(input.releasedVersion) ? { released: input.releasedVersion } : {}
|
|
5945
|
+
},
|
|
5946
|
+
recipes: PLUGIN_UPDATE_RECIPES
|
|
5947
|
+
};
|
|
5948
|
+
}
|
|
5949
|
+
function renderPluginUpdateReport(report) {
|
|
5950
|
+
const v = report.versions;
|
|
5951
|
+
const show = (x) => x ?? "unknown";
|
|
5952
|
+
return [
|
|
5953
|
+
"MMI versions:",
|
|
5954
|
+
` CLI: ${show(v.cli)}`,
|
|
5955
|
+
` Claude plugin: ${show(v.claudePlugin)}`,
|
|
5956
|
+
` Codex marketplace: ${show(v.codexMarketplace)}`,
|
|
5957
|
+
` Codex active cache: ${show(v.codexActiveCache)}`,
|
|
5958
|
+
` latest release: ${show(v.released)}`,
|
|
5959
|
+
"Update recipes (per surface):",
|
|
5960
|
+
` Claude: ${report.recipes.claude.join(" ; ")}`,
|
|
5961
|
+
` Codex: ${report.recipes.codex.join(" ; ")}`,
|
|
5962
|
+
` npm CLI: ${report.recipes.cli.join(" ; ")}`
|
|
5963
|
+
];
|
|
5964
|
+
}
|
|
5965
|
+
function buildDoctorJsonPayload(input) {
|
|
5966
|
+
return {
|
|
5967
|
+
ok: input.checks.every((c) => c.ok),
|
|
5968
|
+
checks: input.checks,
|
|
5969
|
+
updateReport: input.updateReport,
|
|
5970
|
+
...input.resources.length ? { resources: input.resources } : {}
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5815
5973
|
var INSTALLED_PLUGIN_VERSION_LABEL = "installed MMI plugin version (vs latest release)";
|
|
5816
5974
|
function isSemverVersion(v) {
|
|
5817
5975
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
@@ -5922,16 +6080,25 @@ function buildCursorPluginInstallCheck(input) {
|
|
|
5922
6080
|
}
|
|
5923
6081
|
return { ...base, cacheRoot: input.cacheRoot, pins: input.pins };
|
|
5924
6082
|
}
|
|
6083
|
+
var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
|
|
6084
|
+
function buildHubCompatCheck(input) {
|
|
6085
|
+
const label = "Hub compatibility (client version vs Hub minimum)";
|
|
6086
|
+
const min = input.versionInfo?.minClientVersion;
|
|
6087
|
+
if (!input.isOrgRepo || !min || !parseSemver(min) || !parseSemver(input.installedVersion)) {
|
|
6088
|
+
return { ok: true, label, fix: HUB_COMPAT_FIX };
|
|
6089
|
+
}
|
|
6090
|
+
return { ok: versionAtLeast(input.installedVersion, min), label: `${label}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
|
|
6091
|
+
}
|
|
5925
6092
|
|
|
5926
6093
|
// src/stage-runner.ts
|
|
5927
6094
|
var import_node_child_process5 = require("node:child_process");
|
|
5928
|
-
var
|
|
5929
|
-
var
|
|
6095
|
+
var import_node_fs4 = require("node:fs");
|
|
6096
|
+
var import_node_path5 = require("node:path");
|
|
5930
6097
|
var import_node_net = require("node:net");
|
|
5931
6098
|
var import_node_util5 = require("node:util");
|
|
5932
6099
|
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
|
|
5933
6100
|
function stageStatePath(cwd = process.cwd()) {
|
|
5934
|
-
return (0,
|
|
6101
|
+
return (0, import_node_path5.join)(cwd, "tmp", "stage", "state.json");
|
|
5935
6102
|
}
|
|
5936
6103
|
var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
|
|
5937
6104
|
function posixOnlyShellProblems(command, field, platform = process.platform) {
|
|
@@ -5993,9 +6160,9 @@ async function shell(command, cwd, timeoutMs) {
|
|
|
5993
6160
|
});
|
|
5994
6161
|
}
|
|
5995
6162
|
function readState(path2) {
|
|
5996
|
-
if (!(0,
|
|
6163
|
+
if (!(0, import_node_fs4.existsSync)(path2)) return null;
|
|
5997
6164
|
try {
|
|
5998
|
-
return JSON.parse((0,
|
|
6165
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(path2, "utf8"));
|
|
5999
6166
|
} catch {
|
|
6000
6167
|
return null;
|
|
6001
6168
|
}
|
|
@@ -6047,7 +6214,7 @@ async function stopStage(opts = {}) {
|
|
|
6047
6214
|
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
6048
6215
|
}
|
|
6049
6216
|
await killTree(state.pid);
|
|
6050
|
-
(0,
|
|
6217
|
+
(0, import_node_fs4.rmSync)(statePath, { force: true });
|
|
6051
6218
|
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
6052
6219
|
}
|
|
6053
6220
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -6056,7 +6223,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
6056
6223
|
const cwd = opts.cwd ?? process.cwd();
|
|
6057
6224
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
6058
6225
|
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
6059
|
-
(0,
|
|
6226
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
6060
6227
|
let stagePort;
|
|
6061
6228
|
if (config.portRange) {
|
|
6062
6229
|
const [s, e] = config.portRange;
|
|
@@ -6066,9 +6233,9 @@ async function startStage(config = {}, opts = {}) {
|
|
|
6066
6233
|
}
|
|
6067
6234
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
6068
6235
|
if (config.ensureEnv) {
|
|
6069
|
-
const target = (0,
|
|
6070
|
-
const example = (0,
|
|
6071
|
-
if (!(0,
|
|
6236
|
+
const target = (0, import_node_path5.join)(cwd, config.ensureEnv.target);
|
|
6237
|
+
const example = (0, import_node_path5.join)(cwd, config.ensureEnv.example);
|
|
6238
|
+
if (!(0, import_node_fs4.existsSync)(target) && (0, import_node_fs4.existsSync)(example)) (0, import_node_fs4.copyFileSync)(example, target);
|
|
6072
6239
|
}
|
|
6073
6240
|
const extraEnv = {};
|
|
6074
6241
|
for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
|
|
@@ -6092,12 +6259,12 @@ async function startStage(config = {}, opts = {}) {
|
|
|
6092
6259
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
6093
6260
|
port: stagePort
|
|
6094
6261
|
};
|
|
6095
|
-
(0,
|
|
6262
|
+
(0, import_node_fs4.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
6096
6263
|
try {
|
|
6097
6264
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
6098
6265
|
} catch (e) {
|
|
6099
6266
|
await killTree(state.pid);
|
|
6100
|
-
(0,
|
|
6267
|
+
(0, import_node_fs4.rmSync)(statePath, { force: true });
|
|
6101
6268
|
throw e;
|
|
6102
6269
|
}
|
|
6103
6270
|
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
@@ -6167,14 +6334,18 @@ function requireValue(value, label) {
|
|
|
6167
6334
|
if (!value) throw new Error(`${label} could not be resolved`);
|
|
6168
6335
|
return value;
|
|
6169
6336
|
}
|
|
6170
|
-
function releaseTagFromRcTag(tag) {
|
|
6171
|
-
return tag.replace(/-rc\.\d+$/, "");
|
|
6172
|
-
}
|
|
6173
6337
|
async function verifyHubDistributionVersion(deps, model, releaseTag) {
|
|
6174
6338
|
if (model !== "hub-serverless") return;
|
|
6175
6339
|
await deps.run("node", ["scripts/release-distribution.mjs", "verify", releaseTag, "--skip-npm-view"]);
|
|
6176
6340
|
}
|
|
6177
|
-
var ORG_SPINE_FILES = [
|
|
6341
|
+
var ORG_SPINE_FILES = [
|
|
6342
|
+
"AGENTS.md",
|
|
6343
|
+
"CLAUDE.md",
|
|
6344
|
+
"GEMINI.md",
|
|
6345
|
+
".claude/settings.json",
|
|
6346
|
+
".claude/output-styles/mmi-plain.md",
|
|
6347
|
+
".cursor/rules/mmi-plain-language.mdc"
|
|
6348
|
+
];
|
|
6178
6349
|
function isSpinePath(path2) {
|
|
6179
6350
|
return ORG_SPINE_FILES.includes(path2);
|
|
6180
6351
|
}
|
|
@@ -6395,6 +6566,18 @@ async function ensureTagPushed(deps, tag, sha) {
|
|
|
6395
6566
|
await deps.run("git", ["push", "origin", tag]);
|
|
6396
6567
|
return `tag ${tag} pushed at ${sha.slice(0, 7)}`;
|
|
6397
6568
|
}
|
|
6569
|
+
async function resolveRcResumeTag(deps, base, sha) {
|
|
6570
|
+
const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
|
|
6571
|
+
const onSha = clean(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
|
|
6572
|
+
const unique = [...new Set(onSha)];
|
|
6573
|
+
if (unique.length === 0) return { tag: null };
|
|
6574
|
+
const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
|
|
6575
|
+
const sorted = unique.sort((a, b) => rcNum(b) - rcNum(a));
|
|
6576
|
+
const newest = sorted[0];
|
|
6577
|
+
const leftovers = sorted.slice(1);
|
|
6578
|
+
const note = leftovers.length > 0 ? `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} (newest of ${sorted.length}); harmless leftover tag(s) on the same SHA: ${leftovers.join(", ")}` : `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} instead of minting a fresh rc`;
|
|
6579
|
+
return { tag: newest, note };
|
|
6580
|
+
}
|
|
6398
6581
|
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
|
|
6399
6582
|
if (model === "tenant-container") {
|
|
6400
6583
|
const since = (deps.now ?? Date.now)();
|
|
@@ -6455,18 +6638,21 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6455
6638
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
6456
6639
|
);
|
|
6457
6640
|
const deployModel2 = await preflight(deps, ctx, "rc");
|
|
6458
|
-
const
|
|
6459
|
-
await verifyHubDistributionVersion(deps, deployModel2,
|
|
6641
|
+
const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
6642
|
+
await verifyHubDistributionVersion(deps, deployModel2, releaseBase);
|
|
6460
6643
|
await deps.run("git", ["checkout", "rc"]);
|
|
6461
6644
|
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
6462
6645
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
6463
6646
|
const rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
6647
|
+
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
6648
|
+
const tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
6649
|
+
const resumeNote = resume.tag ? resume.note : void 0;
|
|
6464
6650
|
await ensureTagPushed(deps, tag2, rcSha);
|
|
6465
6651
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
6466
6652
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks2);
|
|
6467
6653
|
await deps.run("git", ["push", "origin", "rc"]);
|
|
6468
6654
|
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch);
|
|
6469
|
-
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
|
|
6655
|
+
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
|
|
6470
6656
|
}
|
|
6471
6657
|
await requireBranch(deps, "rc");
|
|
6472
6658
|
ensurePositiveCount(
|
|
@@ -6496,13 +6682,15 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6496
6682
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
6497
6683
|
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
6498
6684
|
await deps.run("git", ["push", "origin", "main"]);
|
|
6499
|
-
await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo]);
|
|
6685
|
+
const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
6686
|
+
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
6500
6687
|
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch);
|
|
6501
6688
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
6502
6689
|
await deps.run("git", ["checkout", "development"]);
|
|
6503
6690
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6504
6691
|
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
6505
6692
|
await deps.run("git", ["push", "origin", "development"]);
|
|
6693
|
+
const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
|
|
6506
6694
|
return {
|
|
6507
6695
|
...ctx,
|
|
6508
6696
|
command,
|
|
@@ -6517,9 +6705,44 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6517
6705
|
runUrl: d.runUrl,
|
|
6518
6706
|
deployStatus: d.deployStatus,
|
|
6519
6707
|
rcRetirement: retirement.status,
|
|
6520
|
-
rcRetirementNote: retirement.note
|
|
6708
|
+
rcRetirementNote: retirement.note,
|
|
6709
|
+
rcRetirementCategory: retirement.category,
|
|
6710
|
+
announceNote,
|
|
6711
|
+
release: { tag, url: releaseUrl, targetSha: releaseSha },
|
|
6712
|
+
environments
|
|
6521
6713
|
};
|
|
6522
6714
|
}
|
|
6715
|
+
async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
|
|
6716
|
+
if (model !== "tenant-container") return void 0;
|
|
6717
|
+
const domains = deps.fetchEdgeDomains ? await deps.fetchEdgeDomains(ctx.slug).catch(() => null) : null;
|
|
6718
|
+
const mainDomains = domains?.main;
|
|
6719
|
+
const rcDomains = domains?.rc;
|
|
6720
|
+
const healthStatus = deployStatus === "success" ? "success" : deployStatus === "failure" ? "failure" : "pending";
|
|
6721
|
+
const main = { healthStatus };
|
|
6722
|
+
if (mainDomains?.length) {
|
|
6723
|
+
main.domains = mainDomains;
|
|
6724
|
+
main.healthUrl = `https://${mainDomains[0]}/`;
|
|
6725
|
+
}
|
|
6726
|
+
const rc = { retirement: retirement.status };
|
|
6727
|
+
if (retirement.category) rc.retirementCategory = retirement.category;
|
|
6728
|
+
if (rcDomains?.length) rc.domains = rcDomains;
|
|
6729
|
+
return { main, rc };
|
|
6730
|
+
}
|
|
6731
|
+
var RETIRE_CATEGORIES = /* @__PURE__ */ new Set([
|
|
6732
|
+
"retired",
|
|
6733
|
+
"retired-edge-pending",
|
|
6734
|
+
"ssm-command-failed",
|
|
6735
|
+
"wait-timeout",
|
|
6736
|
+
"transport-failed"
|
|
6737
|
+
]);
|
|
6738
|
+
function retireCategoryFrom(text) {
|
|
6739
|
+
try {
|
|
6740
|
+
const c = JSON.parse(text).category;
|
|
6741
|
+
return typeof c === "string" && RETIRE_CATEGORIES.has(c) ? c : void 0;
|
|
6742
|
+
} catch {
|
|
6743
|
+
return void 0;
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6523
6746
|
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
6524
6747
|
if (model !== "tenant-container") {
|
|
6525
6748
|
return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
|
|
@@ -6544,16 +6767,28 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
6544
6767
|
}
|
|
6545
6768
|
const out = await deps.runSelf(["tenant", "control", ctx.repo, "rc", "retire"]);
|
|
6546
6769
|
let commandId = "";
|
|
6770
|
+
let category = retireCategoryFrom(out);
|
|
6547
6771
|
try {
|
|
6548
6772
|
commandId = String(JSON.parse(out).commandId ?? "");
|
|
6549
6773
|
} catch {
|
|
6550
6774
|
}
|
|
6775
|
+
if (category === "retired-edge-pending") {
|
|
6776
|
+
return {
|
|
6777
|
+
status: "retired",
|
|
6778
|
+
category,
|
|
6779
|
+
note: `rc runtime retired; edge vhost reconcile pending (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept`
|
|
6780
|
+
};
|
|
6781
|
+
}
|
|
6782
|
+
category = category ?? "retired";
|
|
6551
6783
|
return {
|
|
6552
6784
|
status: "retired",
|
|
6785
|
+
category,
|
|
6553
6786
|
note: `rc runtime retired (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
6554
6787
|
};
|
|
6555
6788
|
} catch (e) {
|
|
6556
|
-
|
|
6789
|
+
const err = e;
|
|
6790
|
+
const category = retireCategoryFrom(err.stdout ?? "") ?? "transport-failed";
|
|
6791
|
+
return { status: "failed", category, note: `rc retirement failed (the release itself succeeded): ${err.message}` };
|
|
6557
6792
|
}
|
|
6558
6793
|
}
|
|
6559
6794
|
async function runTenantRedeploy(deps, options) {
|
|
@@ -6748,7 +6983,7 @@ async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
|
6748
6983
|
}
|
|
6749
6984
|
return { workflow, conclusion: "not-found" };
|
|
6750
6985
|
}
|
|
6751
|
-
async function runHotfixRelease(deps, versionInput) {
|
|
6986
|
+
async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
6752
6987
|
const ctx = await buildTrainApplyContext(deps);
|
|
6753
6988
|
const { tag, version } = normalizeHotfixVersion(versionInput);
|
|
6754
6989
|
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
@@ -6772,12 +7007,16 @@ async function runHotfixRelease(deps, versionInput) {
|
|
|
6772
7007
|
releaseExists = true;
|
|
6773
7008
|
} catch {
|
|
6774
7009
|
}
|
|
7010
|
+
let announceNote;
|
|
6775
7011
|
if (releaseExists) {
|
|
6776
7012
|
releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
|
|
6777
7013
|
} else {
|
|
6778
7014
|
const tagCommit = clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
|
|
6779
7015
|
await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
|
|
6780
7016
|
releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
|
|
7017
|
+
if (deps.announce) {
|
|
7018
|
+
announceNote = (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note;
|
|
7019
|
+
}
|
|
6781
7020
|
}
|
|
6782
7021
|
const runs = [];
|
|
6783
7022
|
for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
|
|
@@ -6794,9 +7033,9 @@ async function runHotfixRelease(deps, versionInput) {
|
|
|
6794
7033
|
} finally {
|
|
6795
7034
|
if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]);
|
|
6796
7035
|
}
|
|
6797
|
-
return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote };
|
|
7036
|
+
return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote, announceNote };
|
|
6798
7037
|
}
|
|
6799
|
-
function
|
|
7038
|
+
function versionAtLeast2(actual, wanted) {
|
|
6800
7039
|
const pa = actual.split(".").map(Number);
|
|
6801
7040
|
const pw = wanted.split(".").map(Number);
|
|
6802
7041
|
if (pa.length < 3 || pa.some(Number.isNaN) || pw.length < 3 || pw.some(Number.isNaN)) return false;
|
|
@@ -6898,12 +7137,138 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
6898
7137
|
},
|
|
6899
7138
|
() => "unknown"
|
|
6900
7139
|
);
|
|
6901
|
-
const devDistribution = { version: devVersion, aligned:
|
|
7140
|
+
const devDistribution = { version: devVersion, aligned: versionAtLeast2(devVersion, version) };
|
|
6902
7141
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion, devDistribution };
|
|
6903
7142
|
}
|
|
6904
7143
|
|
|
7144
|
+
// src/release-announce.ts
|
|
7145
|
+
var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
|
|
7146
|
+
var SSM_REGION = "eu-central-1";
|
|
7147
|
+
var SSM_TOKEN_PARAM = "/mmi-future/shared/SLACK_BOT_TOKEN";
|
|
7148
|
+
var SSM_CHANNEL_PARAM = "/mmi-future/shared/SLACK_ALERTS_CHANNEL";
|
|
7149
|
+
var SLACK_TIMEOUT_MS = 1e4;
|
|
7150
|
+
var MAX_BULLETS = 6;
|
|
7151
|
+
var MAX_SUMMARY_LINES = 8;
|
|
7152
|
+
function escapeMrkdwn(text) {
|
|
7153
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7154
|
+
}
|
|
7155
|
+
function bulletToLine(raw) {
|
|
7156
|
+
const m = /^[*-]\s+(.*)$/.exec(raw.trim());
|
|
7157
|
+
if (!m) return null;
|
|
7158
|
+
let text = m[1].trim();
|
|
7159
|
+
const tail = /\s+by\s+@([\w[\]-]+)\s+in\s+(\S+)\s*$/.exec(text);
|
|
7160
|
+
let link = "";
|
|
7161
|
+
if (tail) {
|
|
7162
|
+
const author = tail[1].toLowerCase();
|
|
7163
|
+
if (author.includes("dependabot") || author.includes("[bot]") || author === "github-actions") return null;
|
|
7164
|
+
const url = tail[2];
|
|
7165
|
+
const pr2 = /\/pull\/(\d+)$/.exec(url);
|
|
7166
|
+
link = pr2 ? ` (<${url}|#${pr2[1]}>)` : "";
|
|
7167
|
+
text = text.slice(0, tail.index).trim();
|
|
7168
|
+
}
|
|
7169
|
+
if (!text) return null;
|
|
7170
|
+
return `\u2022 ${escapeMrkdwn(text)}${link}`;
|
|
7171
|
+
}
|
|
7172
|
+
function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
|
|
7173
|
+
const lines = [];
|
|
7174
|
+
let bullets = 0;
|
|
7175
|
+
let dropped = 0;
|
|
7176
|
+
let section = "";
|
|
7177
|
+
let pendingHeader = "";
|
|
7178
|
+
for (const raw of notesMd.split("\n")) {
|
|
7179
|
+
const header = /^(#{2,3})\s+(.*)$/.exec(raw.trim());
|
|
7180
|
+
if (header) {
|
|
7181
|
+
section = header[2].trim();
|
|
7182
|
+
if (/what's changed/i.test(section) || /new contributors/i.test(section)) {
|
|
7183
|
+
pendingHeader = "";
|
|
7184
|
+
continue;
|
|
7185
|
+
}
|
|
7186
|
+
pendingHeader = `*${escapeMrkdwn(section)}*`;
|
|
7187
|
+
continue;
|
|
7188
|
+
}
|
|
7189
|
+
if (/new contributors/i.test(section)) continue;
|
|
7190
|
+
const line = bulletToLine(raw);
|
|
7191
|
+
if (!line) continue;
|
|
7192
|
+
if (bullets >= maxBullets) {
|
|
7193
|
+
dropped += 1;
|
|
7194
|
+
continue;
|
|
7195
|
+
}
|
|
7196
|
+
if (pendingHeader) {
|
|
7197
|
+
lines.push(pendingHeader);
|
|
7198
|
+
pendingHeader = "";
|
|
7199
|
+
}
|
|
7200
|
+
lines.push(line);
|
|
7201
|
+
bullets += 1;
|
|
7202
|
+
}
|
|
7203
|
+
if (dropped > 0) lines.push(`\u2026and ${dropped} more`);
|
|
7204
|
+
return lines;
|
|
7205
|
+
}
|
|
7206
|
+
function formatAnnouncement(args) {
|
|
7207
|
+
const name = args.repo.split("/")[1] ?? args.repo;
|
|
7208
|
+
return [
|
|
7209
|
+
`:rocket: *${name} ${args.tag} released*`,
|
|
7210
|
+
"",
|
|
7211
|
+
...args.lines,
|
|
7212
|
+
"",
|
|
7213
|
+
`<${args.releaseUrl}|Release notes>`
|
|
7214
|
+
].join("\n");
|
|
7215
|
+
}
|
|
7216
|
+
function summaryFileLines(content) {
|
|
7217
|
+
return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) => escapeMrkdwn(l.replace(/^[•*-]\s*/, ""))).filter(Boolean).map((l) => `\u2022 ${l}`);
|
|
7218
|
+
}
|
|
7219
|
+
async function ssmParameter(deps, name, decrypt) {
|
|
7220
|
+
const args = [
|
|
7221
|
+
"ssm",
|
|
7222
|
+
"get-parameter",
|
|
7223
|
+
"--region",
|
|
7224
|
+
SSM_REGION,
|
|
7225
|
+
"--name",
|
|
7226
|
+
name,
|
|
7227
|
+
"--query",
|
|
7228
|
+
"Parameter.Value",
|
|
7229
|
+
"--output",
|
|
7230
|
+
"text",
|
|
7231
|
+
...decrypt ? ["--with-decryption"] : []
|
|
7232
|
+
];
|
|
7233
|
+
const value = (await deps.run("aws", args)).trim();
|
|
7234
|
+
if (!value || value === "None") throw new Error(`SSM parameter ${name} is empty`);
|
|
7235
|
+
return value;
|
|
7236
|
+
}
|
|
7237
|
+
async function announceRelease(deps, args) {
|
|
7238
|
+
if (args.repo !== ANNOUNCE_REPO) {
|
|
7239
|
+
return { status: "skipped", note: `announce is Hub-only \u2014 ${args.repo} skipped` };
|
|
7240
|
+
}
|
|
7241
|
+
try {
|
|
7242
|
+
const viewRaw = await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body,url"]);
|
|
7243
|
+
const view = JSON.parse(viewRaw);
|
|
7244
|
+
const releaseUrl = view.url ?? `https://github.com/${args.repo}/releases/tag/${args.tag}`;
|
|
7245
|
+
let lines = [];
|
|
7246
|
+
if (args.summaryFile) {
|
|
7247
|
+
if (!deps.readFile) throw new Error("summary file given but deps.readFile is missing");
|
|
7248
|
+
lines = summaryFileLines(await deps.readFile(args.summaryFile));
|
|
7249
|
+
}
|
|
7250
|
+
if (lines.length === 0) lines = curateReleaseNotes(view.body ?? "");
|
|
7251
|
+
if (lines.length === 0) lines = ["\u2022 maintenance release \u2014 see the notes for details"];
|
|
7252
|
+
const token = await ssmParameter(deps, SSM_TOKEN_PARAM, true);
|
|
7253
|
+
const channel = await ssmParameter(deps, SSM_CHANNEL_PARAM, false);
|
|
7254
|
+
const text = formatAnnouncement({ repo: args.repo, tag: args.tag, lines, releaseUrl });
|
|
7255
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
7256
|
+
const res = await fetchImpl("https://slack.com/api/chat.postMessage", {
|
|
7257
|
+
method: "POST",
|
|
7258
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json; charset=utf-8" },
|
|
7259
|
+
body: JSON.stringify({ channel, text, unfurl_links: false }),
|
|
7260
|
+
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
7261
|
+
});
|
|
7262
|
+
const json = await res.json().catch(() => ({}));
|
|
7263
|
+
if (!json.ok) throw new Error(`slack postMessage failed: ${json.error ?? `http ${res.status}`}`);
|
|
7264
|
+
return { status: "announced", note: `announced ${args.tag} to the alerts channel` };
|
|
7265
|
+
} catch (e) {
|
|
7266
|
+
return { status: "failed", note: `announce failed (release unaffected): ${e.message}` };
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
|
|
6905
7270
|
// src/port-registry.ts
|
|
6906
|
-
var
|
|
7271
|
+
var import_node_fs5 = require("node:fs");
|
|
6907
7272
|
|
|
6908
7273
|
// ../infra/port-geometry.mjs
|
|
6909
7274
|
var PORT_BLOCK = 100;
|
|
@@ -6917,8 +7282,8 @@ function nextPortBlock(registry2) {
|
|
|
6917
7282
|
return [base, base + PORT_SPAN];
|
|
6918
7283
|
}
|
|
6919
7284
|
function loadPortRegistry(path2) {
|
|
6920
|
-
if (!(0,
|
|
6921
|
-
const raw = JSON.parse((0,
|
|
7285
|
+
if (!(0, import_node_fs5.existsSync)(path2)) return {};
|
|
7286
|
+
const raw = JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8"));
|
|
6922
7287
|
const out = {};
|
|
6923
7288
|
for (const [key, value] of Object.entries(raw)) {
|
|
6924
7289
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -6932,9 +7297,9 @@ function ensurePortRange(repo, path2) {
|
|
|
6932
7297
|
const existing = registry2[repo];
|
|
6933
7298
|
if (existing) return existing;
|
|
6934
7299
|
const range = nextPortBlock(registry2);
|
|
6935
|
-
const raw = (0,
|
|
7300
|
+
const raw = (0, import_node_fs5.existsSync)(path2) ? JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8")) : {};
|
|
6936
7301
|
raw[repo] = range;
|
|
6937
|
-
(0,
|
|
7302
|
+
(0, import_node_fs5.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
6938
7303
|
return range;
|
|
6939
7304
|
}
|
|
6940
7305
|
function portCursorSeed(registry2) {
|
|
@@ -7518,8 +7883,8 @@ function renderBootstrapVerifyReport(report) {
|
|
|
7518
7883
|
|
|
7519
7884
|
// src/hub-auth.ts
|
|
7520
7885
|
var import_node_crypto2 = require("node:crypto");
|
|
7521
|
-
var
|
|
7522
|
-
var
|
|
7886
|
+
var import_node_fs6 = require("node:fs");
|
|
7887
|
+
var import_node_path6 = require("node:path");
|
|
7523
7888
|
var import_node_os2 = require("node:os");
|
|
7524
7889
|
var REFRESH_WINDOW_MS = 10 * 60 * 1e3;
|
|
7525
7890
|
var EXCHANGE_TIMEOUT_MS = 8e3;
|
|
@@ -7533,15 +7898,15 @@ function tokenFingerprint(token) {
|
|
|
7533
7898
|
function defaultHubSessionCachePath(env = process.env) {
|
|
7534
7899
|
if (env.MMI_HUB_SESSION_CACHE) return env.MMI_HUB_SESSION_CACHE;
|
|
7535
7900
|
if (process.platform === "win32") {
|
|
7536
|
-
const base2 = env.LOCALAPPDATA || (0,
|
|
7537
|
-
return (0,
|
|
7901
|
+
const base2 = env.LOCALAPPDATA || (0, import_node_path6.join)((0, import_node_os2.homedir)(), "AppData", "Local");
|
|
7902
|
+
return (0, import_node_path6.join)(base2, "MMI Future", "mmi-cli", "hub-session.json");
|
|
7538
7903
|
}
|
|
7539
|
-
const base = env.XDG_STATE_HOME || (0,
|
|
7540
|
-
return (0,
|
|
7904
|
+
const base = env.XDG_STATE_HOME || (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".mmi");
|
|
7905
|
+
return (0, import_node_path6.join)(base, "mmi-cli", "hub-session.json");
|
|
7541
7906
|
}
|
|
7542
7907
|
function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
7543
7908
|
try {
|
|
7544
|
-
const session = JSON.parse((0,
|
|
7909
|
+
const session = JSON.parse((0, import_node_fs6.readFileSync)(path2, "utf8"));
|
|
7545
7910
|
if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return null;
|
|
7546
7911
|
if (session.githubTokenFingerprint !== githubTokenFingerprint) return null;
|
|
7547
7912
|
if (new Date(session.expiresAt).getTime() <= now.getTime() + REFRESH_WINDOW_MS) return null;
|
|
@@ -7551,16 +7916,16 @@ function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
|
7551
7916
|
}
|
|
7552
7917
|
}
|
|
7553
7918
|
function writeCache(path2, session) {
|
|
7554
|
-
(0,
|
|
7919
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
|
|
7555
7920
|
const tmp = `${path2}.${process.pid}.${Date.now()}.tmp`;
|
|
7556
|
-
(0,
|
|
7921
|
+
(0, import_node_fs6.writeFileSync)(tmp, JSON.stringify(session, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
7557
7922
|
try {
|
|
7558
|
-
(0,
|
|
7923
|
+
(0, import_node_fs6.chmodSync)(tmp, 384);
|
|
7559
7924
|
} catch {
|
|
7560
7925
|
}
|
|
7561
|
-
(0,
|
|
7926
|
+
(0, import_node_fs6.renameSync)(tmp, path2);
|
|
7562
7927
|
try {
|
|
7563
|
-
(0,
|
|
7928
|
+
(0, import_node_fs6.chmodSync)(path2, 384);
|
|
7564
7929
|
} catch {
|
|
7565
7930
|
}
|
|
7566
7931
|
}
|
|
@@ -7578,7 +7943,7 @@ async function hubAuthSession(deps) {
|
|
|
7578
7943
|
const res = await fetchWithRetry(
|
|
7579
7944
|
deps.fetch ?? fetch,
|
|
7580
7945
|
`${apiUrl}/auth/session`,
|
|
7581
|
-
{ method: "POST", headers: { Authorization: `Bearer ${ghToken}` } },
|
|
7946
|
+
{ method: "POST", headers: { ...clientVersionHeaders(), Authorization: `Bearer ${ghToken}` } },
|
|
7582
7947
|
{ attempts: EXCHANGE_ATTEMPTS, timeoutMs: EXCHANGE_TIMEOUT_MS }
|
|
7583
7948
|
);
|
|
7584
7949
|
if (!res.ok) return void 0;
|
|
@@ -7733,9 +8098,11 @@ var PROJECTS_ENVELOPE_KEY = "projects";
|
|
|
7733
8098
|
|
|
7734
8099
|
// src/registry-client.ts
|
|
7735
8100
|
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
8101
|
+
var WAITED_TENANT_CONTROL_TIMEOUT_MS = 13e3;
|
|
7736
8102
|
var RETRY_ATTEMPTS = 3;
|
|
7737
8103
|
function retriedFetch(deps, url, init) {
|
|
7738
|
-
|
|
8104
|
+
const headers = { ...clientVersionHeaders(), ...init.headers };
|
|
8105
|
+
return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
|
|
7739
8106
|
attempts: RETRY_ATTEMPTS,
|
|
7740
8107
|
timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
7741
8108
|
});
|
|
@@ -7749,6 +8116,7 @@ async function fetchTrainAuthority(repo, deps) {
|
|
|
7749
8116
|
method: "GET",
|
|
7750
8117
|
headers: { Authorization: `Bearer ${token}` }
|
|
7751
8118
|
});
|
|
8119
|
+
if (res.status === 426) return { ok: false, error: upgradeRequiredError(res, await res.json().catch(() => null)) };
|
|
7752
8120
|
if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
|
|
7753
8121
|
const body = await res.json();
|
|
7754
8122
|
if (typeof body?.train !== "boolean" || !body.role) return { ok: false, error: "malformed train-authority response" };
|
|
@@ -7789,6 +8157,7 @@ async function fetchProjectBySlugChecked(slug, deps) {
|
|
|
7789
8157
|
headers: { Authorization: `Bearer ${token}` }
|
|
7790
8158
|
});
|
|
7791
8159
|
if (res.status === 404) return { ok: true, project: null };
|
|
8160
|
+
if (res.status === 426) return { ok: false, error: upgradeRequiredError(res, await res.json().catch(() => null)) };
|
|
7792
8161
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
7793
8162
|
return { ok: true, project: await res.json() };
|
|
7794
8163
|
} catch (e) {
|
|
@@ -7831,14 +8200,17 @@ async function fetchOrgConfig(deps) {
|
|
|
7831
8200
|
return null;
|
|
7832
8201
|
}
|
|
7833
8202
|
}
|
|
7834
|
-
async function postJson(pathSuffix, payload, deps, method = "POST") {
|
|
8203
|
+
async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
|
|
7835
8204
|
if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
|
|
7836
8205
|
const token = await deps.token();
|
|
7837
8206
|
if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
|
|
8207
|
+
const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
8208
|
+
const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
|
|
8209
|
+
const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
|
|
7838
8210
|
try {
|
|
7839
|
-
const res = await
|
|
8211
|
+
const res = await send(`${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
|
|
7840
8212
|
method,
|
|
7841
|
-
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8213
|
+
headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
7842
8214
|
body: JSON.stringify(payload)
|
|
7843
8215
|
});
|
|
7844
8216
|
let body = null;
|
|
@@ -7861,10 +8233,15 @@ async function attestAppGaps(slug, repo, deps) {
|
|
|
7861
8233
|
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
7862
8234
|
}
|
|
7863
8235
|
async function tenantControl(payload, deps) {
|
|
7864
|
-
|
|
8236
|
+
const noRetry = payload.action === "retire";
|
|
8237
|
+
const timeoutMs = payload.wait ? WAITED_TENANT_CONTROL_TIMEOUT_MS : void 0;
|
|
8238
|
+
return postJson("/tenant-control", payload, deps, "POST", { noRetry, timeoutMs });
|
|
7865
8239
|
}
|
|
7866
8240
|
|
|
7867
8241
|
// src/project-readiness.ts
|
|
8242
|
+
function dnsErrorToResolution(code) {
|
|
8243
|
+
return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
|
|
8244
|
+
}
|
|
7868
8245
|
var STAGES = ["dev", "rc", "main"];
|
|
7869
8246
|
var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
7870
8247
|
function slugOfRepo(repoOrSlug) {
|
|
@@ -7882,8 +8259,17 @@ function projectRequiresGoogleOAuth(meta, model) {
|
|
|
7882
8259
|
if (projectType !== "web-app") return false;
|
|
7883
8260
|
return Boolean(meta.oauth && typeof meta.oauth === "object");
|
|
7884
8261
|
}
|
|
7885
|
-
function
|
|
7886
|
-
|
|
8262
|
+
function declaresNoPublicEdge(meta) {
|
|
8263
|
+
const ed = meta?.edgeDomains;
|
|
8264
|
+
return Boolean(ed && typeof ed === "object" && !Array.isArray(ed) && Object.keys(ed).length === 0);
|
|
8265
|
+
}
|
|
8266
|
+
function isNoEdgeTenantWorker(meta, model) {
|
|
8267
|
+
return model === "tenant-container" && declaresNoPublicEdge(meta);
|
|
8268
|
+
}
|
|
8269
|
+
function projectRequiresDeployCoords(model, stage2, meta) {
|
|
8270
|
+
if (model !== "tenant-container") return false;
|
|
8271
|
+
if (stage2 && isNoEdgeTenantWorker(meta, model)) return stage2 === "main";
|
|
8272
|
+
return true;
|
|
7887
8273
|
}
|
|
7888
8274
|
function projectRequiresDeployState(model, stage2) {
|
|
7889
8275
|
return model === "hub-serverless" && stage2 !== "dev";
|
|
@@ -7892,6 +8278,7 @@ function stageRequiredSecrets(stage2, meta) {
|
|
|
7892
8278
|
const contract = meta.requiredRuntimeSecrets;
|
|
7893
8279
|
const extra = !Array.isArray(contract) && Array.isArray(contract?.[stage2]) ? contract[stage2] ?? [] : [];
|
|
7894
8280
|
const model = resolveDeployModel(meta, meta.repos?.[0] ?? "");
|
|
8281
|
+
if (isNoEdgeTenantWorker(meta, model) && stage2 !== "main") return [];
|
|
7895
8282
|
const defaults = projectRequiresGoogleOAuth(meta, model) ? DEFAULT_RUNTIME_SECRET_NAMES : [];
|
|
7896
8283
|
return [.../* @__PURE__ */ new Set([...defaults, ...extra])];
|
|
7897
8284
|
}
|
|
@@ -7947,6 +8334,9 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
7947
8334
|
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
7948
8335
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
7949
8336
|
];
|
|
8337
|
+
if (isNoEdgeTenantWorker(meta, model)) {
|
|
8338
|
+
gaps.unshift("No public edge declared (`edgeDomains: {}`): worker/outbound-only tenant; skip Cloudflare edge auto-heal, OAuth defaults, and dev/rc remote deploy coords unless META explicitly adds them.");
|
|
8339
|
+
}
|
|
7950
8340
|
if (contractUndeclared) {
|
|
7951
8341
|
gaps.unshift(CONTRACT_UNDECLARED_LINE);
|
|
7952
8342
|
}
|
|
@@ -7956,6 +8346,31 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
7956
8346
|
if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
|
|
7957
8347
|
return gaps;
|
|
7958
8348
|
}
|
|
8349
|
+
function edgeDomainsByStage(meta) {
|
|
8350
|
+
const ed = meta?.edgeDomains;
|
|
8351
|
+
if (!ed || typeof ed !== "object" || Array.isArray(ed)) return {};
|
|
8352
|
+
const out = {};
|
|
8353
|
+
for (const stage2 of STAGES) {
|
|
8354
|
+
const v = ed[stage2];
|
|
8355
|
+
if (typeof v === "string" && v.trim()) out[stage2] = v.trim();
|
|
8356
|
+
}
|
|
8357
|
+
return out;
|
|
8358
|
+
}
|
|
8359
|
+
async function probeEdgeDomains(meta, resolveDns) {
|
|
8360
|
+
const byStage = edgeDomainsByStage(meta);
|
|
8361
|
+
const results = await Promise.all(
|
|
8362
|
+
Object.entries(byStage).map(async ([stage2, host]) => {
|
|
8363
|
+
let resolved;
|
|
8364
|
+
try {
|
|
8365
|
+
resolved = await resolveDns(host);
|
|
8366
|
+
} catch {
|
|
8367
|
+
resolved = void 0;
|
|
8368
|
+
}
|
|
8369
|
+
return resolved === false ? { stage: stage2, host } : void 0;
|
|
8370
|
+
})
|
|
8371
|
+
);
|
|
8372
|
+
return results.filter((r) => r !== void 0);
|
|
8373
|
+
}
|
|
7959
8374
|
function contractByStage(contract) {
|
|
7960
8375
|
return contract && !Array.isArray(contract) ? contract : {};
|
|
7961
8376
|
}
|
|
@@ -8086,7 +8501,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
8086
8501
|
secretsError = e?.message || "secrets list failed";
|
|
8087
8502
|
}
|
|
8088
8503
|
const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
|
|
8089
|
-
const required = projectRequiresDeployCoords(model);
|
|
8504
|
+
const required = projectRequiresDeployCoords(model, stage2, meta);
|
|
8090
8505
|
return [stage2, { required, ok: required ? await deps.hasDeployCoords(slug, stage2) : true }];
|
|
8091
8506
|
})));
|
|
8092
8507
|
const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
|
|
@@ -8101,6 +8516,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
8101
8516
|
}));
|
|
8102
8517
|
const metaMissing = ["class", "projectType", "deployModel", "vaultPath", "kbPointer"].filter((key) => meta[key] === void 0);
|
|
8103
8518
|
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets2).every((v) => v.missing.length === 0);
|
|
8519
|
+
const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
|
|
8104
8520
|
return {
|
|
8105
8521
|
ok,
|
|
8106
8522
|
repo,
|
|
@@ -8112,6 +8528,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
8112
8528
|
secretsError,
|
|
8113
8529
|
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
8114
8530
|
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
8531
|
+
...edgeDomainWarnings.length ? { edgeDomainWarnings } : {},
|
|
8115
8532
|
appAttested: appAttestationOf(meta) ?? void 0
|
|
8116
8533
|
};
|
|
8117
8534
|
}
|
|
@@ -8139,6 +8556,9 @@ function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
|
8139
8556
|
`- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
|
|
8140
8557
|
...stageLines,
|
|
8141
8558
|
...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
|
|
8559
|
+
...(report.edgeDomainWarnings ?? []).map(
|
|
8560
|
+
(w) => `- \u26A0 edge domain does not resolve in DNS (advisory): ${w.stage} \u2192 ${w.host}; verify the registry edgeDomains value against the live public host`
|
|
8561
|
+
),
|
|
8142
8562
|
"",
|
|
8143
8563
|
"### Auto-heal applied / available",
|
|
8144
8564
|
...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
|
|
@@ -8180,6 +8600,29 @@ function parseRuntimeSecretsVar(raw) {
|
|
|
8180
8600
|
}
|
|
8181
8601
|
return out;
|
|
8182
8602
|
}
|
|
8603
|
+
function parseEdgeDomainsVar(raw) {
|
|
8604
|
+
let parsed;
|
|
8605
|
+
try {
|
|
8606
|
+
parsed = JSON.parse(raw);
|
|
8607
|
+
} catch {
|
|
8608
|
+
throw new Error('project set: edgeDomains must be JSON, e.g. {"dev":"dev.example.co","rc":"rc.example.co","main":"example.co"}');
|
|
8609
|
+
}
|
|
8610
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8611
|
+
throw new Error("project set: edgeDomains must be a {dev,rc,main} map of domain strings");
|
|
8612
|
+
}
|
|
8613
|
+
const map = parsed;
|
|
8614
|
+
const out = {};
|
|
8615
|
+
for (const [stage2, domain] of Object.entries(map)) {
|
|
8616
|
+
if (!RUNTIME_SECRET_STAGES.includes(stage2)) {
|
|
8617
|
+
throw new Error(`project set: edgeDomains stage "${stage2}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
8618
|
+
}
|
|
8619
|
+
if (typeof domain !== "string" || !domain.trim()) {
|
|
8620
|
+
throw new Error(`project set: edgeDomains.${stage2} must be a non-empty domain string`);
|
|
8621
|
+
}
|
|
8622
|
+
out[stage2] = domain.trim();
|
|
8623
|
+
}
|
|
8624
|
+
return out;
|
|
8625
|
+
}
|
|
8183
8626
|
function buildProjectSetPatch(input) {
|
|
8184
8627
|
const patch = {};
|
|
8185
8628
|
if (input.class) {
|
|
@@ -8211,6 +8654,8 @@ function buildProjectSetPatch(input) {
|
|
|
8211
8654
|
patch[key] = n;
|
|
8212
8655
|
} else if (key === "requiredRuntimeSecrets") {
|
|
8213
8656
|
patch[key] = parseRuntimeSecretsVar(raw);
|
|
8657
|
+
} else if (key === "edgeDomains") {
|
|
8658
|
+
patch[key] = parseEdgeDomainsVar(raw);
|
|
8214
8659
|
} else {
|
|
8215
8660
|
patch[key] = raw;
|
|
8216
8661
|
}
|
|
@@ -8270,7 +8715,7 @@ function parseKbTree(stdout, prefix) {
|
|
|
8270
8715
|
}
|
|
8271
8716
|
|
|
8272
8717
|
// src/plan.ts
|
|
8273
|
-
var
|
|
8718
|
+
var import_node_path7 = require("node:path");
|
|
8274
8719
|
|
|
8275
8720
|
// src/frontmatter.ts
|
|
8276
8721
|
function splitFrontmatter(content) {
|
|
@@ -8353,8 +8798,8 @@ function rankPlansByRelevance(plans, signals, opts = {}) {
|
|
|
8353
8798
|
|
|
8354
8799
|
// src/plan.ts
|
|
8355
8800
|
var PLANS_DIR = "plans";
|
|
8356
|
-
var META_FILE = (0,
|
|
8357
|
-
var planPath = (slug) => (0,
|
|
8801
|
+
var META_FILE = (0, import_node_path7.join)(PLANS_DIR, ".plan-meta.json");
|
|
8802
|
+
var planPath = (slug) => (0, import_node_path7.join)(PLANS_DIR, `${slug}.md`);
|
|
8358
8803
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
8359
8804
|
function parseMeta(raw) {
|
|
8360
8805
|
if (!raw) return {};
|
|
@@ -8402,6 +8847,10 @@ ${next.join("\n")}
|
|
|
8402
8847
|
---
|
|
8403
8848
|
${body.replace(/^\n+/, "")}`;
|
|
8404
8849
|
}
|
|
8850
|
+
async function httpFailMessage(op, res) {
|
|
8851
|
+
if (res.status === 426) return upgradeRequiredError(res, await res.json().catch(() => null));
|
|
8852
|
+
return `plan ${op} failed: HTTP ${res.status}`;
|
|
8853
|
+
}
|
|
8405
8854
|
async function planPush(deps, slug, opts = {}) {
|
|
8406
8855
|
const raw = deps.readLocal(slug);
|
|
8407
8856
|
if (raw == null) {
|
|
@@ -8433,7 +8882,7 @@ async function planPush(deps, slug, opts = {}) {
|
|
|
8433
8882
|
deps.err(staleHint(slug));
|
|
8434
8883
|
return false;
|
|
8435
8884
|
} else {
|
|
8436
|
-
deps.err(
|
|
8885
|
+
deps.err(await httpFailMessage("push", res));
|
|
8437
8886
|
return false;
|
|
8438
8887
|
}
|
|
8439
8888
|
}
|
|
@@ -8457,7 +8906,7 @@ async function planPull(deps, slug, opts = {}) {
|
|
|
8457
8906
|
return false;
|
|
8458
8907
|
}
|
|
8459
8908
|
if (!res.ok) {
|
|
8460
|
-
deps.err(
|
|
8909
|
+
deps.err(await httpFailMessage("pull", res));
|
|
8461
8910
|
return false;
|
|
8462
8911
|
}
|
|
8463
8912
|
const doc = await res.json();
|
|
@@ -8475,6 +8924,7 @@ async function fetchPlanList(deps, project2) {
|
|
|
8475
8924
|
headers: await deps.headers(),
|
|
8476
8925
|
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
8477
8926
|
});
|
|
8927
|
+
if (res.status === 426) throw new Error(upgradeRequiredError(res, await res.json().catch(() => null)));
|
|
8478
8928
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
8479
8929
|
const { plans } = await res.json();
|
|
8480
8930
|
return plans ?? [];
|
|
@@ -8530,7 +8980,7 @@ async function planDelete(deps, slug, opts = {}) {
|
|
|
8530
8980
|
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
8531
8981
|
});
|
|
8532
8982
|
if (!res.ok) {
|
|
8533
|
-
deps.err(
|
|
8983
|
+
deps.err(await httpFailMessage("delete", res));
|
|
8534
8984
|
return;
|
|
8535
8985
|
}
|
|
8536
8986
|
deps.removeLocal(slug);
|
|
@@ -8617,6 +9067,52 @@ function formatVaultPointer(p) {
|
|
|
8617
9067
|
}
|
|
8618
9068
|
var TIMEOUT_MS2 = 8e3;
|
|
8619
9069
|
var repoOf = (slug) => `${OWNER2}/${slug}`;
|
|
9070
|
+
var RECALL_REGIONS = ["us-east-1", "us-west-2", "eu-central-1", "ap-northeast-1"];
|
|
9071
|
+
var PROVIDER_VERIFY_TIMEOUT_MS = 8e3;
|
|
9072
|
+
function secretLeafName(key) {
|
|
9073
|
+
return key.split("/").pop() ?? key;
|
|
9074
|
+
}
|
|
9075
|
+
function providerForSecretKey(key) {
|
|
9076
|
+
return secretLeafName(key) === "RECALL_API_KEY" ? "recall" : void 0;
|
|
9077
|
+
}
|
|
9078
|
+
function recallUsageUrl(region) {
|
|
9079
|
+
const end = /* @__PURE__ */ new Date();
|
|
9080
|
+
const start = new Date(end.getTime() - 60 * 60 * 1e3);
|
|
9081
|
+
const qs = new URLSearchParams({ start: start.toISOString(), end: end.toISOString() });
|
|
9082
|
+
return `https://${region}.recall.ai/api/v1/billing/usage/?${qs.toString()}`;
|
|
9083
|
+
}
|
|
9084
|
+
async function verifyRecallApiKey(deps, value) {
|
|
9085
|
+
const checked = [];
|
|
9086
|
+
for (const region of RECALL_REGIONS) {
|
|
9087
|
+
try {
|
|
9088
|
+
const res = await deps.fetch(recallUsageUrl(region), {
|
|
9089
|
+
method: "GET",
|
|
9090
|
+
headers: { Authorization: `Token ${value}` },
|
|
9091
|
+
signal: AbortSignal.timeout(PROVIDER_VERIFY_TIMEOUT_MS)
|
|
9092
|
+
});
|
|
9093
|
+
if (res.ok) {
|
|
9094
|
+
return { ok: true, provider: "recall", message: `validated RECALL_API_KEY with Recall (${region})`, checked };
|
|
9095
|
+
}
|
|
9096
|
+
checked.push(`${region}: HTTP ${res.status}`);
|
|
9097
|
+
} catch (e) {
|
|
9098
|
+
checked.push(`${region}: ${e.message}`);
|
|
9099
|
+
}
|
|
9100
|
+
}
|
|
9101
|
+
return {
|
|
9102
|
+
ok: false,
|
|
9103
|
+
provider: "recall",
|
|
9104
|
+
message: `Recall rejected the key in all configured regions (${checked.join("; ")})`,
|
|
9105
|
+
checked
|
|
9106
|
+
};
|
|
9107
|
+
}
|
|
9108
|
+
async function verifySecretValue(deps, key, value, _opts) {
|
|
9109
|
+
const provider = providerForSecretKey(key);
|
|
9110
|
+
if (!provider) {
|
|
9111
|
+
return { ok: false, message: `no provider verifier configured for ${secretLeafName(key)}`, checked: [] };
|
|
9112
|
+
}
|
|
9113
|
+
if (provider === "recall") return verifyRecallApiKey(deps, value);
|
|
9114
|
+
return { ok: false, provider, message: `no provider verifier configured for ${secretLeafName(key)}`, checked: [] };
|
|
9115
|
+
}
|
|
8620
9116
|
async function vaultSlug(deps, opts) {
|
|
8621
9117
|
return (opts.repo ? opts.repo.split("/").pop() : await deps.slug()).toLowerCase();
|
|
8622
9118
|
}
|
|
@@ -8646,6 +9142,10 @@ function errorDetail(body) {
|
|
|
8646
9142
|
const error = typeof body.error === "string" ? body.error : "";
|
|
8647
9143
|
return error ? `: ${error}` : "";
|
|
8648
9144
|
}
|
|
9145
|
+
async function upgradeMessage(res, body) {
|
|
9146
|
+
if (res.status !== 426) return null;
|
|
9147
|
+
return upgradeRequiredError(res, body ?? await readJsonBody(res));
|
|
9148
|
+
}
|
|
8649
9149
|
async function fetchSecretValue(deps, key, opts) {
|
|
8650
9150
|
if (!isValidSecretKey(key)) return null;
|
|
8651
9151
|
const repo = await targetRepo(deps, opts);
|
|
@@ -8678,7 +9178,7 @@ async function secretsList(deps, opts) {
|
|
|
8678
9178
|
return;
|
|
8679
9179
|
}
|
|
8680
9180
|
if (!res.ok) {
|
|
8681
|
-
deps.err(`secrets list failed: HTTP ${res.status}${await readErr(res)}`);
|
|
9181
|
+
deps.err(await upgradeMessage(res) ?? `secrets list failed: HTTP ${res.status}${await readErr(res)}`);
|
|
8682
9182
|
return;
|
|
8683
9183
|
}
|
|
8684
9184
|
const { secrets: secrets2 } = await res.json();
|
|
@@ -8711,7 +9211,7 @@ async function secretsPreflight(deps, opts) {
|
|
|
8711
9211
|
return false;
|
|
8712
9212
|
}
|
|
8713
9213
|
if (!res.ok) {
|
|
8714
|
-
deps.err(`secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
|
|
9214
|
+
deps.err(await upgradeMessage(res) ?? `secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
|
|
8715
9215
|
return false;
|
|
8716
9216
|
}
|
|
8717
9217
|
const { secrets: secrets2 } = await res.json();
|
|
@@ -8746,7 +9246,7 @@ async function secretsGet(deps, key, opts) {
|
|
|
8746
9246
|
return false;
|
|
8747
9247
|
}
|
|
8748
9248
|
deps.err(
|
|
8749
|
-
res.status === 403 ? `secrets get: not authorized for ${key} (HTTP 403)${errorDetail(body)}` : `secrets get failed: HTTP ${res.status}${errorDetail(body)}`
|
|
9249
|
+
await upgradeMessage(res, body) ?? (res.status === 403 ? `secrets get: not authorized for ${key} (HTTP 403)${errorDetail(body)}` : `secrets get failed: HTTP ${res.status}${errorDetail(body)}`)
|
|
8750
9250
|
);
|
|
8751
9251
|
return false;
|
|
8752
9252
|
}
|
|
@@ -8754,6 +9254,28 @@ async function secretsGet(deps, key, opts) {
|
|
|
8754
9254
|
deps.log(value ?? "");
|
|
8755
9255
|
return true;
|
|
8756
9256
|
}
|
|
9257
|
+
async function secretsVerify(deps, key, opts) {
|
|
9258
|
+
if (!isValidSecretKey(key)) {
|
|
9259
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
9260
|
+
return false;
|
|
9261
|
+
}
|
|
9262
|
+
if (!providerForSecretKey(key)) {
|
|
9263
|
+
deps.err(`no provider verifier configured for ${secretLeafName(key)}`);
|
|
9264
|
+
return false;
|
|
9265
|
+
}
|
|
9266
|
+
const value = await fetchSecretValue(deps, key, opts);
|
|
9267
|
+
if (!value) {
|
|
9268
|
+
deps.err(`secrets verify: could not read ${key}; no value was printed`);
|
|
9269
|
+
return false;
|
|
9270
|
+
}
|
|
9271
|
+
const result = await verifySecretValue(deps, key, value, opts);
|
|
9272
|
+
if (result.ok) {
|
|
9273
|
+
deps.log(result.message);
|
|
9274
|
+
return true;
|
|
9275
|
+
}
|
|
9276
|
+
deps.err(result.message);
|
|
9277
|
+
return false;
|
|
9278
|
+
}
|
|
8757
9279
|
async function secretsRequest(deps, key, opts) {
|
|
8758
9280
|
if (!isValidSecretKey(key)) {
|
|
8759
9281
|
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
@@ -8774,7 +9296,7 @@ async function secretsRequest(deps, key, opts) {
|
|
|
8774
9296
|
});
|
|
8775
9297
|
const body = await readJsonBody(res);
|
|
8776
9298
|
if (!res.ok) {
|
|
8777
|
-
deps.err(`secrets request failed: HTTP ${res.status}${errorDetail(body)}`);
|
|
9299
|
+
deps.err(await upgradeMessage(res, body) ?? `secrets request failed: HTTP ${res.status}${errorDetail(body)}`);
|
|
8778
9300
|
return false;
|
|
8779
9301
|
}
|
|
8780
9302
|
if (opts.json) {
|
|
@@ -8804,21 +9326,34 @@ async function putSecret(deps, key, value, opts) {
|
|
|
8804
9326
|
});
|
|
8805
9327
|
if (!res.ok) {
|
|
8806
9328
|
deps.err(
|
|
8807
|
-
res.status === 403 ? `secrets set: not authorized to write ${key} (HTTP 403)${await readErr(res)}` : `secrets set failed: HTTP ${res.status}${await readErr(res)}`
|
|
9329
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets set: not authorized to write ${key} (HTTP 403)${await readErr(res)}` : `secrets set failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8808
9330
|
);
|
|
8809
9331
|
return false;
|
|
8810
9332
|
}
|
|
9333
|
+
const provider = providerForSecretKey(key);
|
|
9334
|
+
if (provider) {
|
|
9335
|
+
const result = await verifySecretValue(deps, key, value, opts);
|
|
9336
|
+
if (!result.ok) {
|
|
9337
|
+
deps.err(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier), but ${result.message}`);
|
|
9338
|
+
return false;
|
|
9339
|
+
}
|
|
9340
|
+
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier); ${result.message}`);
|
|
9341
|
+
return true;
|
|
9342
|
+
}
|
|
8811
9343
|
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
8812
9344
|
return true;
|
|
8813
9345
|
}
|
|
8814
9346
|
async function secretsSet(deps, key, opts) {
|
|
8815
|
-
if (!isValidSecretKey(key))
|
|
9347
|
+
if (!isValidSecretKey(key)) {
|
|
9348
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
9349
|
+
return false;
|
|
9350
|
+
}
|
|
8816
9351
|
const value = await deps.readSecretValue(`value for ${key} (input hidden; will not be echoed): `);
|
|
8817
9352
|
if (!value) {
|
|
8818
9353
|
deps.err("secrets set: empty value \u2014 aborted (nothing written)");
|
|
8819
|
-
return;
|
|
9354
|
+
return false;
|
|
8820
9355
|
}
|
|
8821
|
-
|
|
9356
|
+
return putSecret(deps, key, value, opts);
|
|
8822
9357
|
}
|
|
8823
9358
|
async function secretsEdit(deps, key, opts) {
|
|
8824
9359
|
return secretsSet(deps, key, opts);
|
|
@@ -8834,7 +9369,7 @@ async function secretsRemove(deps, key, opts) {
|
|
|
8834
9369
|
});
|
|
8835
9370
|
if (!res.ok) {
|
|
8836
9371
|
deps.err(
|
|
8837
|
-
res.status === 403 ? `secrets rm: not authorized to remove ${key} (HTTP 403)${await readErr(res)}` : `secrets rm failed: HTTP ${res.status}${await readErr(res)}`
|
|
9372
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets rm: not authorized to remove ${key} (HTTP 403)${await readErr(res)}` : `secrets rm failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8838
9373
|
);
|
|
8839
9374
|
return;
|
|
8840
9375
|
}
|
|
@@ -8849,7 +9384,7 @@ async function secretsGrant(deps, repo, login, key, _opts) {
|
|
|
8849
9384
|
});
|
|
8850
9385
|
if (!res.ok) {
|
|
8851
9386
|
deps.err(
|
|
8852
|
-
res.status === 403 ? `secrets grant: master-admin only (HTTP 403)${await readErr(res)}` : `secrets grant failed: HTTP ${res.status}${await readErr(res)}`
|
|
9387
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets grant: master-admin only (HTTP 403)${await readErr(res)}` : `secrets grant failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8853
9388
|
);
|
|
8854
9389
|
return;
|
|
8855
9390
|
}
|
|
@@ -8864,7 +9399,7 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
|
|
|
8864
9399
|
});
|
|
8865
9400
|
if (!res.ok) {
|
|
8866
9401
|
deps.err(
|
|
8867
|
-
res.status === 403 ? `secrets revoke: master-admin only (HTTP 403)${await readErr(res)}` : `secrets revoke failed: HTTP ${res.status}${await readErr(res)}`
|
|
9402
|
+
await upgradeMessage(res) ?? (res.status === 403 ? `secrets revoke: master-admin only (HTTP 403)${await readErr(res)}` : `secrets revoke failed: HTTP ${res.status}${await readErr(res)}`)
|
|
8868
9403
|
);
|
|
8869
9404
|
return;
|
|
8870
9405
|
}
|
|
@@ -9001,7 +9536,8 @@ async function awsCallerArn() {
|
|
|
9001
9536
|
async function hubHeaders(extra = {}) {
|
|
9002
9537
|
const cfg = await loadConfig();
|
|
9003
9538
|
const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
|
|
9004
|
-
|
|
9539
|
+
const base = { ...clientVersionHeaders(), ...extra };
|
|
9540
|
+
return t ? { ...base, Authorization: `Bearer ${t}` } : base;
|
|
9005
9541
|
}
|
|
9006
9542
|
async function loadConfig() {
|
|
9007
9543
|
let file = {};
|
|
@@ -9064,7 +9600,7 @@ function sessionDeps() {
|
|
|
9064
9600
|
env: process.env,
|
|
9065
9601
|
readPersisted: () => {
|
|
9066
9602
|
try {
|
|
9067
|
-
return (0,
|
|
9603
|
+
return (0, import_node_fs7.readFileSync)(SESSION_FILE, "utf8");
|
|
9068
9604
|
} catch {
|
|
9069
9605
|
return null;
|
|
9070
9606
|
}
|
|
@@ -9077,8 +9613,8 @@ function sessionDeps() {
|
|
|
9077
9613
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
9078
9614
|
function persistSession(id) {
|
|
9079
9615
|
try {
|
|
9080
|
-
(0,
|
|
9081
|
-
(0,
|
|
9616
|
+
(0, import_node_fs7.mkdirSync)(".mmi", { recursive: true });
|
|
9617
|
+
(0, import_node_fs7.writeFileSync)(SESSION_FILE, id, "utf8");
|
|
9082
9618
|
} catch {
|
|
9083
9619
|
}
|
|
9084
9620
|
}
|
|
@@ -9197,22 +9733,20 @@ async function applyGcPlan(plan2, remote) {
|
|
|
9197
9733
|
}
|
|
9198
9734
|
return result;
|
|
9199
9735
|
}
|
|
9200
|
-
function
|
|
9736
|
+
async function fetchHubVersionInfo(baseUrl) {
|
|
9737
|
+
if (!baseUrl) return null;
|
|
9201
9738
|
try {
|
|
9202
|
-
const
|
|
9203
|
-
|
|
9739
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/version`, { signal: AbortSignal.timeout(4e3) });
|
|
9740
|
+
if (!res.ok) return null;
|
|
9741
|
+
const body = await res.json();
|
|
9742
|
+
return body && typeof body === "object" ? body : null;
|
|
9204
9743
|
} catch {
|
|
9205
|
-
|
|
9206
|
-
const pkg = (0, import_node_path7.join)(__dirname, "..", "package.json");
|
|
9207
|
-
return JSON.parse((0, import_node_fs6.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
9208
|
-
} catch {
|
|
9209
|
-
return "0.0.0";
|
|
9210
|
-
}
|
|
9744
|
+
return null;
|
|
9211
9745
|
}
|
|
9212
9746
|
}
|
|
9213
9747
|
function readRepoVersion() {
|
|
9214
9748
|
try {
|
|
9215
|
-
return JSON.parse((0,
|
|
9749
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path8.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
9216
9750
|
} catch {
|
|
9217
9751
|
return void 0;
|
|
9218
9752
|
}
|
|
@@ -9251,7 +9785,7 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
9251
9785
|
}
|
|
9252
9786
|
async function requireFreshTrainCli(commandName) {
|
|
9253
9787
|
const report = buildVersionLagReport({
|
|
9254
|
-
currentVersion:
|
|
9788
|
+
currentVersion: resolveClientVersion(),
|
|
9255
9789
|
repoVersion: readRepoVersion(),
|
|
9256
9790
|
releasedVersion: await fetchReleasedVersion()
|
|
9257
9791
|
});
|
|
@@ -9281,7 +9815,7 @@ async function applyClaudePluginHeal(surface, log) {
|
|
|
9281
9815
|
return true;
|
|
9282
9816
|
}
|
|
9283
9817
|
var program2 = new Command();
|
|
9284
|
-
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(
|
|
9818
|
+
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
|
|
9285
9819
|
async function runRulesSync(opts, io = consoleIo) {
|
|
9286
9820
|
const cfg = await loadConfig();
|
|
9287
9821
|
if (isRulesSource(cfg.orgRulesSource)) {
|
|
@@ -9291,7 +9825,13 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
9291
9825
|
const base = resolveRulesBase(cfg.orgRulesSource, DEFAULT_RULES_SOURCE);
|
|
9292
9826
|
const token = await githubToken();
|
|
9293
9827
|
let changed = 0;
|
|
9294
|
-
const files = [
|
|
9828
|
+
const files = [
|
|
9829
|
+
"AGENTS.md",
|
|
9830
|
+
"CLAUDE.md",
|
|
9831
|
+
".claude/settings.json",
|
|
9832
|
+
".claude/output-styles/mmi-plain.md",
|
|
9833
|
+
".cursor/rules/mmi-plain-language.mdc"
|
|
9834
|
+
];
|
|
9295
9835
|
const fetched = await Promise.all(files.map(async (file) => {
|
|
9296
9836
|
try {
|
|
9297
9837
|
const url = `${base}/${file}`;
|
|
@@ -9309,10 +9849,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
9309
9849
|
for (const entry of fetched) {
|
|
9310
9850
|
if ("error" in entry) continue;
|
|
9311
9851
|
const { file, source } = entry;
|
|
9312
|
-
const current = (0,
|
|
9852
|
+
const current = (0, import_node_fs7.existsSync)(file) ? await (0, import_promises2.readFile)(file, "utf8") : null;
|
|
9313
9853
|
if (needsUpdate(source, current)) {
|
|
9314
9854
|
const slash = file.lastIndexOf("/");
|
|
9315
|
-
if (slash > 0) (0,
|
|
9855
|
+
if (slash > 0) (0, import_node_fs7.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
9316
9856
|
await (0, import_promises2.writeFile)(file, normalizeEol(source), "utf8");
|
|
9317
9857
|
changed++;
|
|
9318
9858
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -9322,7 +9862,7 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
9322
9862
|
return failures.length === 0;
|
|
9323
9863
|
}
|
|
9324
9864
|
var rules = program2.command("rules").description("org rules delivery");
|
|
9325
|
-
rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch AGENTS.md / CLAUDE.md / .claude/settings.json from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
|
|
9865
|
+
rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch the org-delivered files (AGENTS.md / CLAUDE.md / .claude/settings.json / output style / Cursor rule) from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
|
|
9326
9866
|
if (!await runRulesSync(opts)) process.exitCode = 1;
|
|
9327
9867
|
});
|
|
9328
9868
|
async function runDocsSync(opts, io = consoleIo) {
|
|
@@ -9338,7 +9878,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
9338
9878
|
return null;
|
|
9339
9879
|
}
|
|
9340
9880
|
},
|
|
9341
|
-
localContent: async (f) => (0,
|
|
9881
|
+
localContent: async (f) => (0, import_node_fs7.existsSync)(f) ? await (0, import_promises2.readFile)(f, "utf8") : null,
|
|
9342
9882
|
writeDoc: async (f, c) => {
|
|
9343
9883
|
await (0, import_promises2.writeFile)(f, c, "utf8");
|
|
9344
9884
|
}
|
|
@@ -9486,6 +10026,18 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
9486
10026
|
io.log(`saga health: ${report.ok ? "OK" : "NOT OK"}`);
|
|
9487
10027
|
if (report.problems.length) io.log(report.problems.map((p) => ` - ${p}`).join("\n"));
|
|
9488
10028
|
}
|
|
10029
|
+
async function runWhoami(io = consoleIo) {
|
|
10030
|
+
const cfg = await loadConfig();
|
|
10031
|
+
const report = await resolveWhoami({
|
|
10032
|
+
hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
10033
|
+
ghLogin: githubLogin
|
|
10034
|
+
});
|
|
10035
|
+
io.log(JSON.stringify(report));
|
|
10036
|
+
return report;
|
|
10037
|
+
}
|
|
10038
|
+
program2.command("whoami").description('resolve the logged-in human: {login, source, sessionExpiresAt} JSON; source "unknown" (exit 0) when neither the Hub session nor gh can name them').action(async () => {
|
|
10039
|
+
await runWhoami();
|
|
10040
|
+
});
|
|
9489
10041
|
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((o) => runSagaHealth(o));
|
|
9490
10042
|
program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
9491
10043
|
if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
|
|
@@ -9614,7 +10166,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
9614
10166
|
}
|
|
9615
10167
|
}
|
|
9616
10168
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
9617
|
-
const ensureDir = () => (0,
|
|
10169
|
+
const ensureDir = () => (0, import_node_fs7.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
9618
10170
|
return {
|
|
9619
10171
|
apiUrl: cfg.sagaApiUrl,
|
|
9620
10172
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
@@ -9622,31 +10174,31 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
9622
10174
|
project: async () => (await sagaKey(cfg)).project,
|
|
9623
10175
|
readLocal: (slug) => {
|
|
9624
10176
|
try {
|
|
9625
|
-
return (0,
|
|
10177
|
+
return (0, import_node_fs7.readFileSync)(planPath(slug), "utf8");
|
|
9626
10178
|
} catch {
|
|
9627
10179
|
return null;
|
|
9628
10180
|
}
|
|
9629
10181
|
},
|
|
9630
10182
|
writeLocal: (slug, content) => {
|
|
9631
10183
|
ensureDir();
|
|
9632
|
-
(0,
|
|
10184
|
+
(0, import_node_fs7.writeFileSync)(planPath(slug), content, "utf8");
|
|
9633
10185
|
},
|
|
9634
10186
|
removeLocal: (slug) => {
|
|
9635
10187
|
try {
|
|
9636
|
-
(0,
|
|
10188
|
+
(0, import_node_fs7.rmSync)(planPath(slug));
|
|
9637
10189
|
} catch {
|
|
9638
10190
|
}
|
|
9639
10191
|
},
|
|
9640
10192
|
readMetaRaw: () => {
|
|
9641
10193
|
try {
|
|
9642
|
-
return (0,
|
|
10194
|
+
return (0, import_node_fs7.readFileSync)(META_FILE, "utf8");
|
|
9643
10195
|
} catch {
|
|
9644
10196
|
return null;
|
|
9645
10197
|
}
|
|
9646
10198
|
},
|
|
9647
10199
|
writeMetaRaw: (raw) => {
|
|
9648
10200
|
ensureDir();
|
|
9649
|
-
(0,
|
|
10201
|
+
(0, import_node_fs7.writeFileSync)(META_FILE, raw, "utf8");
|
|
9650
10202
|
},
|
|
9651
10203
|
log: (m) => io.log(m),
|
|
9652
10204
|
err: (m) => io.err(m),
|
|
@@ -9787,8 +10339,18 @@ secrets.command("request <key>").description("approved escalation: create a Hub
|
|
|
9787
10339
|
const ok = await secretsRequest(d, key, o);
|
|
9788
10340
|
if (!ok) process.exitCode = 1;
|
|
9789
10341
|
}));
|
|
9790
|
-
secrets.command("
|
|
9791
|
-
|
|
10342
|
+
secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
10343
|
+
const ok = await secretsVerify(d, key, o);
|
|
10344
|
+
if (!ok) process.exitCode = 1;
|
|
10345
|
+
}));
|
|
10346
|
+
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(async (d) => {
|
|
10347
|
+
const ok = await secretsSet(d, key, o);
|
|
10348
|
+
if (!ok) process.exitCode = 1;
|
|
10349
|
+
}));
|
|
10350
|
+
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(async (d) => {
|
|
10351
|
+
const ok = await secretsEdit(d, key, o);
|
|
10352
|
+
if (!ok) process.exitCode = 1;
|
|
10353
|
+
}));
|
|
9792
10354
|
secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
9793
10355
|
secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
|
|
9794
10356
|
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
@@ -9811,7 +10373,13 @@ function reportWrite(label, res) {
|
|
|
9811
10373
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
9812
10374
|
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, plus rc-only retire) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action) => {
|
|
9813
10375
|
const cfg = await loadConfig();
|
|
9814
|
-
const
|
|
10376
|
+
const wait = action === "status" || action === "retire";
|
|
10377
|
+
const res = await tenantControl({ repo, stage: stage2, action, wait }, registryClientDeps(cfg));
|
|
10378
|
+
const body = res.body;
|
|
10379
|
+
if (!res.ok && body?.category) {
|
|
10380
|
+
console.log(JSON.stringify(body));
|
|
10381
|
+
return fail(`tenant control ${stage2} ${action}: ${body.category}`);
|
|
10382
|
+
}
|
|
9815
10383
|
reportWrite("tenant control", res);
|
|
9816
10384
|
});
|
|
9817
10385
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
|
|
@@ -9823,9 +10391,18 @@ tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the cen
|
|
|
9823
10391
|
return fail(`tenant redeploy: ${e.message}`);
|
|
9824
10392
|
}
|
|
9825
10393
|
});
|
|
10394
|
+
async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
10395
|
+
const { lookup } = await import("node:dns/promises");
|
|
10396
|
+
const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
|
|
10397
|
+
const timeout = new Promise((resolve) => {
|
|
10398
|
+
setTimeout(() => resolve(void 0), timeoutMs).unref?.();
|
|
10399
|
+
});
|
|
10400
|
+
return Promise.race([probe, timeout]);
|
|
10401
|
+
}
|
|
9826
10402
|
async function v2ReadinessDeps(cfg) {
|
|
9827
10403
|
const reg = registryClientDeps(cfg);
|
|
9828
10404
|
return {
|
|
10405
|
+
resolveDns: (host) => resolveDnsBounded(host),
|
|
9829
10406
|
// Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
|
|
9830
10407
|
getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
|
|
9831
10408
|
hasDeployCoords: async (slug, stage2) => {
|
|
@@ -10267,7 +10844,7 @@ async function remoteBranchExists(branch) {
|
|
|
10267
10844
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
10268
10845
|
function teardownWorktreeStage(worktreePath) {
|
|
10269
10846
|
return runWorktreeStageTeardown(worktreePath, {
|
|
10270
|
-
hasStageState: (wt) => (0,
|
|
10847
|
+
hasStageState: (wt) => (0, import_node_fs7.existsSync)(stageStatePath(wt)),
|
|
10271
10848
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
10272
10849
|
listComposeProjects: async () => {
|
|
10273
10850
|
const { stdout } = await execFileP4("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -10454,7 +11031,7 @@ function rawValues(flag) {
|
|
|
10454
11031
|
return out;
|
|
10455
11032
|
}
|
|
10456
11033
|
function printLine(value) {
|
|
10457
|
-
(0,
|
|
11034
|
+
(0, import_node_fs7.writeSync)(1, `${value}
|
|
10458
11035
|
`);
|
|
10459
11036
|
}
|
|
10460
11037
|
function stageKeepAlive() {
|
|
@@ -10471,8 +11048,8 @@ async function resolveStage() {
|
|
|
10471
11048
|
local,
|
|
10472
11049
|
shell: shellFor(),
|
|
10473
11050
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
10474
|
-
hasCompose: (0,
|
|
10475
|
-
hasEnvExample: (0,
|
|
11051
|
+
hasCompose: (0, import_node_fs7.existsSync)((0, import_node_path8.join)(process.cwd(), "docker-compose.yml")),
|
|
11052
|
+
hasEnvExample: (0, import_node_fs7.existsSync)((0, import_node_path8.join)(process.cwd(), ".env.example"))
|
|
10476
11053
|
});
|
|
10477
11054
|
}
|
|
10478
11055
|
function stageStepsFor(res, stops = true) {
|
|
@@ -10495,9 +11072,9 @@ function reportedStageUrl(res, result) {
|
|
|
10495
11072
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
10496
11073
|
}
|
|
10497
11074
|
program2.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (committed-file fallback)").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
10498
|
-
const path2 = (0,
|
|
11075
|
+
const path2 = (0, import_node_path8.join)(process.cwd(), "infra", "port-ranges.json");
|
|
10499
11076
|
const allocate = async (seed) => {
|
|
10500
|
-
const { stdout } = await execFileP4("node", [(0,
|
|
11077
|
+
const { stdout } = await execFileP4("node", [(0, import_node_path8.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
10501
11078
|
const parsed = JSON.parse(stdout);
|
|
10502
11079
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
10503
11080
|
return parsed.range;
|
|
@@ -10626,6 +11203,18 @@ function trainApplyDeps() {
|
|
|
10626
11203
|
trainAuthority: async (repo) => {
|
|
10627
11204
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
10628
11205
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
11206
|
+
},
|
|
11207
|
+
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
11208
|
+
announce: (args) => announceRelease({
|
|
11209
|
+
run: async (file, cmdArgs) => (await execFileP4(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
11210
|
+
readFile: (path2) => (0, import_promises2.readFile)(path2, "utf8")
|
|
11211
|
+
}, args),
|
|
11212
|
+
fetchEdgeDomains: async (slug) => {
|
|
11213
|
+
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
11214
|
+
const ed = proj?.edgeDomains;
|
|
11215
|
+
if (!ed) return null;
|
|
11216
|
+
const toArr = (v) => v ? [v] : void 0;
|
|
11217
|
+
return { rc: toArr(ed.rc), main: toArr(ed.main) };
|
|
10629
11218
|
}
|
|
10630
11219
|
};
|
|
10631
11220
|
}
|
|
@@ -10638,14 +11227,16 @@ function renderDeployLine(d) {
|
|
|
10638
11227
|
return parts.join("; ");
|
|
10639
11228
|
}
|
|
10640
11229
|
function renderTrainApply(commandName, r) {
|
|
10641
|
-
|
|
10642
|
-
|
|
11230
|
+
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
11231
|
+
if (r.resumeNote) base = `${base}; ${r.resumeNote}`;
|
|
11232
|
+
if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
|
|
11233
|
+
return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
|
|
10643
11234
|
}
|
|
10644
11235
|
function renderTenantRedeploy(r) {
|
|
10645
11236
|
return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
10646
11237
|
}
|
|
10647
11238
|
for (const commandName of ["rcand", "release"]) {
|
|
10648
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the dispatched tenant-deploy.yml run and report its outcome (tenant-container)").option("--apply", "execute the guarded master-only train after explicit approval").action(async (o) => {
|
|
11239
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the dispatched tenant-deploy.yml run and report its outcome (tenant-container)").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").action(async (o) => {
|
|
10649
11240
|
try {
|
|
10650
11241
|
await requireFreshTrainCli(commandName);
|
|
10651
11242
|
} catch (e) {
|
|
@@ -10653,7 +11244,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
10653
11244
|
}
|
|
10654
11245
|
if (o.apply) {
|
|
10655
11246
|
try {
|
|
10656
|
-
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch });
|
|
11247
|
+
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
|
|
10657
11248
|
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
|
|
10658
11249
|
} catch (e) {
|
|
10659
11250
|
return fail(`${commandName}: ${e.message}`);
|
|
@@ -10674,6 +11265,7 @@ function renderHotfixRelease(r) {
|
|
|
10674
11265
|
` - ${r.releaseNote}`,
|
|
10675
11266
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
10676
11267
|
` - ${r.verifyNote}`,
|
|
11268
|
+
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
10677
11269
|
` - next: mmi-cli hotfix status ${r.tag} (no back-merge \u2014 development already has the fix; align its distribution manifests by PR if status says behind)`
|
|
10678
11270
|
].join("\n");
|
|
10679
11271
|
}
|
|
@@ -10706,7 +11298,7 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
|
|
|
10706
11298
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli hotfix: dry-run plan", steps));
|
|
10707
11299
|
});
|
|
10708
11300
|
hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
|
|
10709
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version), o.json, renderHotfixRelease));
|
|
11301
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
|
|
10710
11302
|
hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
|
|
10711
11303
|
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) => {
|
|
10712
11304
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
@@ -10726,7 +11318,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
10726
11318
|
const report = await verifyBootstrap(repo, o.class, {
|
|
10727
11319
|
client: defaultGitHubClient(),
|
|
10728
11320
|
projectMeta: meta,
|
|
10729
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
11321
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs7.existsSync)(path2) ? (0, import_node_fs7.readFileSync)(path2, "utf8") : null,
|
|
10730
11322
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
10731
11323
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
10732
11324
|
requiredGcpApis: (() => {
|
|
@@ -10767,12 +11359,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
10767
11359
|
return fail(`bootstrap apply: ${e.message}`);
|
|
10768
11360
|
}
|
|
10769
11361
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
10770
|
-
if (!(0,
|
|
10771
|
-
const manifest = loadBootstrapSeeds((0,
|
|
11362
|
+
if (!(0, import_node_fs7.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
11363
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs7.readFileSync)(manifestPath, "utf8"));
|
|
10772
11364
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
10773
11365
|
const slug = parsedRepo.slug;
|
|
10774
11366
|
const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
|
|
10775
|
-
const readFile2 = (p) => (0,
|
|
11367
|
+
const readFile2 = (p) => (0, import_node_fs7.existsSync)(p) ? (0, import_node_fs7.readFileSync)(p, "utf8") : null;
|
|
10776
11368
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
10777
11369
|
const vars = {};
|
|
10778
11370
|
for (const value of rawValues("--var")) {
|
|
@@ -10896,16 +11488,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
10896
11488
|
if (o.class !== "deployable" && o.class !== "content") return fail("access audit: --class must be deployable or content");
|
|
10897
11489
|
targets = [{ repo: o.repo, class: o.class }];
|
|
10898
11490
|
} else {
|
|
10899
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
11491
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs7.existsSync)("projects.json") ? (0, import_node_fs7.readFileSync)("projects.json", "utf8") : null;
|
|
10900
11492
|
if (!projectsJson) return fail("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
|
|
10901
|
-
const fanoutJson = (0,
|
|
11493
|
+
const fanoutJson = (0, import_node_fs7.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs7.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
10902
11494
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
10903
11495
|
}
|
|
10904
11496
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
10905
|
-
const fileMatrix = (0,
|
|
11497
|
+
const fileMatrix = (0, import_node_fs7.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs7.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
10906
11498
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
10907
11499
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
10908
|
-
const fileContracts = (0,
|
|
11500
|
+
const fileContracts = (0, import_node_fs7.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs7.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
10909
11501
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
10910
11502
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
10911
11503
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -10914,20 +11506,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
10914
11506
|
var isWin = process.platform === "win32";
|
|
10915
11507
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
10916
11508
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
10917
|
-
return (0,
|
|
11509
|
+
return (0, import_node_path8.join)((0, import_node_os3.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
10918
11510
|
};
|
|
10919
11511
|
function readInstalledPlugins() {
|
|
10920
11512
|
try {
|
|
10921
|
-
return JSON.parse((0,
|
|
11513
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(installedPluginsPath(), "utf8"));
|
|
10922
11514
|
} catch {
|
|
10923
11515
|
return null;
|
|
10924
11516
|
}
|
|
10925
11517
|
}
|
|
10926
11518
|
function installedPluginSources() {
|
|
10927
11519
|
return ["claude", "codex"].map((surface) => {
|
|
10928
|
-
const recordPath = (0,
|
|
11520
|
+
const recordPath = (0, import_node_path8.join)((0, import_node_os3.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
10929
11521
|
try {
|
|
10930
|
-
return { surface, installed: JSON.parse((0,
|
|
11522
|
+
return { surface, installed: JSON.parse((0, import_node_fs7.readFileSync)(recordPath, "utf8")), recordPath };
|
|
10931
11523
|
} catch {
|
|
10932
11524
|
return { surface, installed: null, recordPath };
|
|
10933
11525
|
}
|
|
@@ -10935,7 +11527,7 @@ function installedPluginSources() {
|
|
|
10935
11527
|
}
|
|
10936
11528
|
function readClaudeSettings() {
|
|
10937
11529
|
try {
|
|
10938
|
-
return JSON.parse((0,
|
|
11530
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path8.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
10939
11531
|
} catch {
|
|
10940
11532
|
return null;
|
|
10941
11533
|
}
|
|
@@ -10957,7 +11549,7 @@ function writeProjectInstallRecord(record) {
|
|
|
10957
11549
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
10958
11550
|
list.push(record);
|
|
10959
11551
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
10960
|
-
(0,
|
|
11552
|
+
(0, import_node_fs7.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
10961
11553
|
`, "utf8");
|
|
10962
11554
|
return true;
|
|
10963
11555
|
} catch {
|
|
@@ -10970,9 +11562,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
10970
11562
|
if (!file) return false;
|
|
10971
11563
|
if (!file.plugins) file.plugins = {};
|
|
10972
11564
|
const path2 = installedPluginsPath();
|
|
10973
|
-
(0,
|
|
11565
|
+
(0, import_node_fs7.copyFileSync)(path2, `${path2}.bak`);
|
|
10974
11566
|
file.plugins[pluginId] = records;
|
|
10975
|
-
(0,
|
|
11567
|
+
(0, import_node_fs7.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
10976
11568
|
`, "utf8");
|
|
10977
11569
|
return true;
|
|
10978
11570
|
} catch {
|
|
@@ -10980,26 +11572,26 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
10980
11572
|
}
|
|
10981
11573
|
}
|
|
10982
11574
|
function cursorPluginCacheRoot() {
|
|
10983
|
-
return (0,
|
|
11575
|
+
return (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
|
|
10984
11576
|
}
|
|
10985
11577
|
function cursorPluginCachePinSnapshots() {
|
|
10986
11578
|
const root = cursorPluginCacheRoot();
|
|
10987
11579
|
try {
|
|
10988
|
-
return (0,
|
|
10989
|
-
const path2 = (0,
|
|
10990
|
-
const pluginJson = (0,
|
|
10991
|
-
const hooksJson = (0,
|
|
11580
|
+
return (0, import_node_fs7.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
11581
|
+
const path2 = (0, import_node_path8.join)(root, entry.name);
|
|
11582
|
+
const pluginJson = (0, import_node_path8.join)(path2, ".cursor-plugin", "plugin.json");
|
|
11583
|
+
const hooksJson = (0, import_node_path8.join)(path2, "hooks", "hooks.json");
|
|
10992
11584
|
let isEmpty = true;
|
|
10993
11585
|
try {
|
|
10994
|
-
isEmpty = (0,
|
|
11586
|
+
isEmpty = (0, import_node_fs7.readdirSync)(path2).length === 0;
|
|
10995
11587
|
} catch {
|
|
10996
11588
|
isEmpty = true;
|
|
10997
11589
|
}
|
|
10998
11590
|
return {
|
|
10999
11591
|
name: entry.name,
|
|
11000
11592
|
path: path2,
|
|
11001
|
-
hasPluginJson: (0,
|
|
11002
|
-
hasHooksJson: (0,
|
|
11593
|
+
hasPluginJson: (0, import_node_fs7.existsSync)(pluginJson),
|
|
11594
|
+
hasHooksJson: (0, import_node_fs7.existsSync)(hooksJson),
|
|
11003
11595
|
isEmpty
|
|
11004
11596
|
};
|
|
11005
11597
|
});
|
|
@@ -11008,19 +11600,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
11008
11600
|
}
|
|
11009
11601
|
}
|
|
11010
11602
|
function hubCheckoutForCursorSeed() {
|
|
11011
|
-
const manifest = (0,
|
|
11012
|
-
return (0,
|
|
11603
|
+
const manifest = (0, import_node_path8.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
11604
|
+
return (0, import_node_fs7.existsSync)(manifest) ? process.cwd() : void 0;
|
|
11013
11605
|
}
|
|
11014
11606
|
function mmiPluginCacheRootSnapshots() {
|
|
11015
11607
|
const roots = [
|
|
11016
|
-
{ surface: "claude", root: (0,
|
|
11017
|
-
{ surface: "codex", root: (0,
|
|
11608
|
+
{ surface: "claude", root: (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
11609
|
+
{ surface: "codex", root: (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
11018
11610
|
];
|
|
11019
11611
|
return roots.flatMap(({ surface, root }) => {
|
|
11020
11612
|
try {
|
|
11021
|
-
const entries = (0,
|
|
11613
|
+
const entries = (0, import_node_fs7.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
11022
11614
|
name: entry.name,
|
|
11023
|
-
path: (0,
|
|
11615
|
+
path: (0, import_node_path8.join)(root, entry.name),
|
|
11024
11616
|
isDirectory: entry.isDirectory()
|
|
11025
11617
|
}));
|
|
11026
11618
|
return [{ surface, root, entries }];
|
|
@@ -11030,10 +11622,10 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
11030
11622
|
});
|
|
11031
11623
|
}
|
|
11032
11624
|
function uniqueQuarantineTarget(path2) {
|
|
11033
|
-
if (!(0,
|
|
11625
|
+
if (!(0, import_node_fs7.existsSync)(path2)) return path2;
|
|
11034
11626
|
for (let i = 1; i < 100; i += 1) {
|
|
11035
11627
|
const candidate = `${path2}-${i}`;
|
|
11036
|
-
if (!(0,
|
|
11628
|
+
if (!(0, import_node_fs7.existsSync)(candidate)) return candidate;
|
|
11037
11629
|
}
|
|
11038
11630
|
return `${path2}-${Date.now()}`;
|
|
11039
11631
|
}
|
|
@@ -11041,27 +11633,27 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
11041
11633
|
let moved = 0;
|
|
11042
11634
|
for (const move of plan2) {
|
|
11043
11635
|
try {
|
|
11044
|
-
if (!(0,
|
|
11636
|
+
if (!(0, import_node_fs7.existsSync)(move.from)) continue;
|
|
11045
11637
|
const target = uniqueQuarantineTarget(move.to);
|
|
11046
|
-
(0,
|
|
11047
|
-
(0,
|
|
11638
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.dirname)(target), { recursive: true });
|
|
11639
|
+
(0, import_node_fs7.renameSync)(move.from, target);
|
|
11048
11640
|
moved += 1;
|
|
11049
11641
|
} catch {
|
|
11050
11642
|
}
|
|
11051
11643
|
}
|
|
11052
11644
|
return moved;
|
|
11053
11645
|
}
|
|
11054
|
-
var gitignorePath = () => (0,
|
|
11646
|
+
var gitignorePath = () => (0, import_node_path8.join)(process.cwd(), ".gitignore");
|
|
11055
11647
|
function readGitignore() {
|
|
11056
11648
|
try {
|
|
11057
|
-
return (0,
|
|
11649
|
+
return (0, import_node_fs7.readFileSync)(gitignorePath(), "utf8");
|
|
11058
11650
|
} catch {
|
|
11059
11651
|
return null;
|
|
11060
11652
|
}
|
|
11061
11653
|
}
|
|
11062
11654
|
function writeGitignore(content) {
|
|
11063
11655
|
try {
|
|
11064
|
-
(0,
|
|
11656
|
+
(0, import_node_fs7.writeFileSync)(gitignorePath(), content, "utf8");
|
|
11065
11657
|
return true;
|
|
11066
11658
|
} catch {
|
|
11067
11659
|
return false;
|
|
@@ -11075,6 +11667,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11075
11667
|
}
|
|
11076
11668
|
const repairLocal = !opts.json || Boolean(opts.apply);
|
|
11077
11669
|
const repairFull = !opts.json && !opts.banner || Boolean(opts.apply);
|
|
11670
|
+
const repoWritesAllowed = !opts.noRepoWrites;
|
|
11078
11671
|
const checks = [];
|
|
11079
11672
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
11080
11673
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
@@ -11099,13 +11692,13 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11099
11692
|
let onPath = pathProbe;
|
|
11100
11693
|
if (!onPath) {
|
|
11101
11694
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
11102
|
-
if (root && (0,
|
|
11695
|
+
if (root && (0, import_node_fs7.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
11103
11696
|
}
|
|
11104
11697
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
11105
11698
|
const surface = detectSurface(process.env);
|
|
11106
11699
|
const reloadHint = reloadAction(surface);
|
|
11107
11700
|
let versionReport = buildVersionLagReport({
|
|
11108
|
-
currentVersion:
|
|
11701
|
+
currentVersion: resolveClientVersion(),
|
|
11109
11702
|
repoVersion: readRepoVersion(),
|
|
11110
11703
|
releasedVersion
|
|
11111
11704
|
});
|
|
@@ -11113,6 +11706,13 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11113
11706
|
if (!versionReport.ok) versionReport = { ...versionReport, fix: pluginRecoveryFix(surface) };
|
|
11114
11707
|
checks.push(versionReport);
|
|
11115
11708
|
checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
|
|
11709
|
+
checks.push(
|
|
11710
|
+
buildHubCompatCheck({
|
|
11711
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11712
|
+
versionInfo: await fetchHubVersionInfo(cfg.sagaApiUrl),
|
|
11713
|
+
installedVersion: resolveClientVersion()
|
|
11714
|
+
})
|
|
11715
|
+
);
|
|
11116
11716
|
checks.push(buildAwsCrossAccountCheck({ callerArn }));
|
|
11117
11717
|
let cloneOk = cloneProbe;
|
|
11118
11718
|
if (!cloneOk && repairFull) {
|
|
@@ -11141,13 +11741,18 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11141
11741
|
}
|
|
11142
11742
|
checks.push(pluginCheck);
|
|
11143
11743
|
let gitignoreCheck = buildGitignoreManagedBlockCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), content: readGitignore() });
|
|
11144
|
-
|
|
11744
|
+
const gitignoreDecision = decideGitignoreRepair(gitignoreCheck, { repoWritesAllowed, repairFull });
|
|
11745
|
+
gitignoreCheck = gitignoreDecision.check;
|
|
11746
|
+
if (gitignoreDecision.action === "suppress") {
|
|
11747
|
+
io.err(" \u23F8 skipped (--no-repo-writes): org-managed .gitignore block repair would dirty the working tree");
|
|
11748
|
+
io.err(` apply it after the release train: ${gitignoreCheck.fix}`);
|
|
11749
|
+
} else if (gitignoreDecision.action === "write") {
|
|
11145
11750
|
if (writeGitignore(gitignoreCheck.contentToWrite)) {
|
|
11146
|
-
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
11147
11751
|
const drift = gitignoreCheck.seeded ? "inserted the org-managed block" : [
|
|
11148
11752
|
gitignoreCheck.added?.length ? `added ${gitignoreCheck.added.join(", ")}` : "",
|
|
11149
11753
|
gitignoreCheck.removed?.length ? `removed ${gitignoreCheck.removed.join(", ")}` : ""
|
|
11150
11754
|
].filter(Boolean).join("; ") || "normalized the block";
|
|
11755
|
+
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
11151
11756
|
io.err(` \u21BB repaired: org-managed .gitignore block \u2014 ${drift}`);
|
|
11152
11757
|
io.err(" this is an org-managed update (not unrelated churn) \u2014 stage & commit .gitignore so it stops recurring");
|
|
11153
11758
|
}
|
|
@@ -11186,7 +11791,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11186
11791
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
11187
11792
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11188
11793
|
roots: mmiPluginCacheRootSnapshots(),
|
|
11189
|
-
activeVersion:
|
|
11794
|
+
activeVersion: resolveClientVersion(),
|
|
11190
11795
|
releasedVersion,
|
|
11191
11796
|
installedVersions: installedPluginVersions(installed)
|
|
11192
11797
|
});
|
|
@@ -11201,7 +11806,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11201
11806
|
...buildMmiPluginCacheCleanupCheck({
|
|
11202
11807
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11203
11808
|
roots: mmiPluginCacheRootSnapshots(),
|
|
11204
|
-
activeVersion:
|
|
11809
|
+
activeVersion: resolveClientVersion(),
|
|
11205
11810
|
releasedVersion
|
|
11206
11811
|
}),
|
|
11207
11812
|
...moved > 0 ? { cleanedCount: moved } : {}
|
|
@@ -11214,29 +11819,44 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
11214
11819
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11215
11820
|
surface,
|
|
11216
11821
|
cacheRoot: cursorCacheRoot,
|
|
11217
|
-
cacheRootExists: (0,
|
|
11822
|
+
cacheRootExists: (0, import_node_fs7.existsSync)(cursorCacheRoot),
|
|
11218
11823
|
pins: cursorPluginCachePinSnapshots() ?? [],
|
|
11219
11824
|
hubCheckout: hubCheckoutForCursorSeed()
|
|
11220
11825
|
})
|
|
11221
11826
|
);
|
|
11222
11827
|
const gaps = checks.filter((c) => !c.ok);
|
|
11223
|
-
const resources = doctorResourcesForGaps(gaps);
|
|
11224
|
-
if (opts.json) {
|
|
11225
|
-
io.log(JSON.stringify({ ok: gaps.length === 0, checks, ...resources.length ? { resources } : {} }, null, 2));
|
|
11226
|
-
return;
|
|
11227
|
-
}
|
|
11228
11828
|
if (opts.banner) {
|
|
11229
11829
|
if (gaps.length) io.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
|
|
11230
11830
|
return;
|
|
11231
11831
|
}
|
|
11832
|
+
const cacheRoots = mmiPluginCacheRootSnapshots();
|
|
11833
|
+
const cacheVersionsFor = (s) => cacheRoots.filter((r) => r.surface === s).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
11834
|
+
const sourceVersions = (s) => installedPluginVersions(installedPluginSources().find((src) => src.surface === s)?.installed ?? null);
|
|
11835
|
+
const updateReport = buildPluginUpdateReport({
|
|
11836
|
+
cliVersion: resolveClientVersion(),
|
|
11837
|
+
claudePluginVersions: sourceVersions("claude"),
|
|
11838
|
+
codexPluginVersions: sourceVersions("codex"),
|
|
11839
|
+
codexCacheVersions: cacheVersionsFor("codex"),
|
|
11840
|
+
releasedVersion
|
|
11841
|
+
});
|
|
11842
|
+
const resources = doctorResourcesForGaps(gaps);
|
|
11843
|
+
if (opts.json) {
|
|
11844
|
+
io.log(JSON.stringify(buildDoctorJsonPayload({ checks, updateReport, resources }), null, 2));
|
|
11845
|
+
return;
|
|
11846
|
+
}
|
|
11232
11847
|
for (const c of checks) io.log(c.ok ? `\u2713 ${c.label}` : `\u2717 ${c.label}
|
|
11233
11848
|
\u2192 ${c.fix}`);
|
|
11234
11849
|
for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
|
|
11850
|
+
io.log("");
|
|
11851
|
+
for (const line of renderPluginUpdateReport(updateReport)) io.log(line);
|
|
11235
11852
|
io.log(gaps.length ? `
|
|
11236
11853
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
11237
11854
|
}
|
|
11238
|
-
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install) and
|
|
11239
|
-
|
|
11855
|
+
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install), print fixes, and report per-surface versions + update recipes").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 (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for release-train prep)").action((opts) => (
|
|
11856
|
+
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
11857
|
+
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
11858
|
+
));
|
|
11859
|
+
program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor) in one process; docs sync runs detached").action(async () => {
|
|
11240
11860
|
try {
|
|
11241
11861
|
const hook = parseHookInput(await readStdin());
|
|
11242
11862
|
if (hook.session_id) persistSession(hook.session_id);
|
|
@@ -11248,6 +11868,16 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
11248
11868
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
11249
11869
|
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
11250
11870
|
sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
|
|
11871
|
+
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
11872
|
+
// when unknown — a missing gh login must not noise or fail the banner.
|
|
11873
|
+
whoami: async (io) => {
|
|
11874
|
+
const report = await resolveWhoami({
|
|
11875
|
+
hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
11876
|
+
ghLogin: githubLogin
|
|
11877
|
+
});
|
|
11878
|
+
const line = whoamiLine(report);
|
|
11879
|
+
if (line) io.log(line);
|
|
11880
|
+
},
|
|
11251
11881
|
doctor: (io) => runDoctor({ banner: true }, io)
|
|
11252
11882
|
});
|
|
11253
11883
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
@@ -11255,7 +11885,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
11255
11885
|
});
|
|
11256
11886
|
function fail(msg) {
|
|
11257
11887
|
console.error(`mmi-cli ${msg}`);
|
|
11258
|
-
|
|
11888
|
+
hardExit(1);
|
|
11259
11889
|
}
|
|
11260
11890
|
process.on("unhandledRejection", (reason) => fail(reason instanceof Error ? reason.message : String(reason)));
|
|
11261
11891
|
process.on("uncaughtException", (err) => fail(err instanceof Error ? err.message : String(err)));
|