@mutmutco/cli 3.35.2 → 3.36.1
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/main.cjs +106 -22
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3887,6 +3887,10 @@ function writeCache(path2, session) {
|
|
|
3887
3887
|
async function hubAuthSession(deps) {
|
|
3888
3888
|
if (!deps.baseUrl) return void 0;
|
|
3889
3889
|
const apiUrl = normalizeBaseUrl(deps.baseUrl);
|
|
3890
|
+
const envToken = process.env.MMI_HUB_TOKEN;
|
|
3891
|
+
if (envToken) {
|
|
3892
|
+
return { ...{ token: envToken }, expiresAt: new Date(Date.now() + 30 * 60 * 1e3).toISOString(), apiUrl, githubTokenFingerprint: "env-override" };
|
|
3893
|
+
}
|
|
3890
3894
|
const now = deps.now?.() ?? /* @__PURE__ */ new Date();
|
|
3891
3895
|
const cachePath = deps.cachePath ?? defaultHubSessionCachePath();
|
|
3892
3896
|
const ghToken = await deps.githubToken();
|
|
@@ -5104,6 +5108,12 @@ async function postSchedulesLift(repo, schedules, deps) {
|
|
|
5104
5108
|
if (!res.ok && res.status === 404) return { ...res, unreachable: "route-absent" };
|
|
5105
5109
|
return res;
|
|
5106
5110
|
}
|
|
5111
|
+
async function postSchedulesMode(slug, mode, deps) {
|
|
5112
|
+
return postJson("/projects/schedules-mode", { slug, mode }, deps);
|
|
5113
|
+
}
|
|
5114
|
+
async function postSchedulesRun(id, deps) {
|
|
5115
|
+
return postJson("/schedules/run", { id }, deps, "POST", { noRetry: true });
|
|
5116
|
+
}
|
|
5107
5117
|
async function tenantControl(payload, deps) {
|
|
5108
5118
|
return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
|
|
5109
5119
|
}
|
|
@@ -9026,6 +9036,7 @@ function commandLadderHint() {
|
|
|
9026
9036
|
for (const e of COMMAND_LADDER_BOARD) {
|
|
9027
9037
|
lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
|
|
9028
9038
|
}
|
|
9039
|
+
lines.push("[mmi] Schedules doctrine (#3228): scheduled jobs + LLM crons go through the Hub registry \u2014 eight-field header \u2192 PR \u2192 `mmi-cli org schedules register`; LLM crons ONLY via the harbour (llm: yes \u21D2 executor cursor-agent). Never hand-build an unregistered cron. Opt out per repo: `mmi-cli org schedules mode self-managed`.");
|
|
9029
9040
|
return lines.join("\n");
|
|
9030
9041
|
}
|
|
9031
9042
|
|
|
@@ -16992,11 +17003,18 @@ function awsScheduleEntry(payload) {
|
|
|
16992
17003
|
source: `aws scheduler schedule ${Name} (eu-central-1)`
|
|
16993
17004
|
};
|
|
16994
17005
|
}
|
|
16995
|
-
function
|
|
17006
|
+
function awsScheduleRefs(payload) {
|
|
16996
17007
|
if (!payload || typeof payload !== "object") return [];
|
|
16997
17008
|
const { Schedules: schedules } = payload;
|
|
16998
17009
|
if (!Array.isArray(schedules)) return [];
|
|
16999
|
-
|
|
17010
|
+
const refs = [];
|
|
17011
|
+
for (const s of schedules) {
|
|
17012
|
+
if (!s || typeof s !== "object") continue;
|
|
17013
|
+
const { Name, GroupName } = s;
|
|
17014
|
+
if (typeof Name !== "string" || isJervResource(Name)) continue;
|
|
17015
|
+
refs.push(typeof GroupName === "string" && GroupName !== "default" && GroupName !== "" ? { name: Name, group: GroupName } : { name: Name });
|
|
17016
|
+
}
|
|
17017
|
+
return refs;
|
|
17000
17018
|
}
|
|
17001
17019
|
var DECLARED_ENTRIES = [
|
|
17002
17020
|
{ name: "mmi-backup (mm-central, mmi-fofu, zuber, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh (verify: mmi-cli runtime box get <box> --ssh)" },
|
|
@@ -17065,7 +17083,7 @@ function cadenceStale(registryCadence, liveCadence) {
|
|
|
17065
17083
|
if (!liveCrons.length) return false;
|
|
17066
17084
|
return !liveCrons.every((cron) => registered.has(cron));
|
|
17067
17085
|
}
|
|
17068
|
-
function reconcileGithubActions(liveGithub, registry2, readRepos) {
|
|
17086
|
+
function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
17069
17087
|
const registryGithub = registry2.filter((r) => r.executor === "github-actions");
|
|
17070
17088
|
const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
|
|
17071
17089
|
const registryById = new Map(registryGithub.map((r) => [r.id, r]));
|
|
@@ -17095,18 +17113,25 @@ function reconcileGithubActions(liveGithub, registry2, readRepos) {
|
|
|
17095
17113
|
}
|
|
17096
17114
|
continue;
|
|
17097
17115
|
}
|
|
17116
|
+
if (activeWorkflowNames.has(r.id)) continue;
|
|
17098
17117
|
if (readRepos.has(r.repo)) {
|
|
17099
17118
|
drifts.push({
|
|
17100
17119
|
class: "registered-but-dead",
|
|
17101
17120
|
name: r.id,
|
|
17102
17121
|
executor: r.executor || "github-actions",
|
|
17103
|
-
detail: "registered with no
|
|
17104
|
-
remedy: `
|
|
17122
|
+
detail: "registered with no workflow file behind it",
|
|
17123
|
+
remedy: `restore the workflow in ${r.repo} (or, if it was retired on purpose, re-run the schedules lift so the replace prunes the stale SCHEDULE# row)`
|
|
17105
17124
|
});
|
|
17106
17125
|
}
|
|
17107
17126
|
}
|
|
17108
17127
|
return drifts.sort((a, b) => a.class.localeCompare(b.class) || a.name.localeCompare(b.name));
|
|
17109
17128
|
}
|
|
17129
|
+
function suppressSelfManagedDrifts(drifts, selfManagedRepos) {
|
|
17130
|
+
return drifts.filter((d) => {
|
|
17131
|
+
if (d.class !== "stray-cron" && d.class !== "unlaunchered-llm") return true;
|
|
17132
|
+
return !selfManagedRepos.has((d.name.split("/")[0] ?? "").toLowerCase());
|
|
17133
|
+
});
|
|
17134
|
+
}
|
|
17110
17135
|
function renderDrift(d) {
|
|
17111
17136
|
return `${d.class}: ${d.name} (${d.executor}) \u2014 ${d.detail}; remedy: ${d.remedy}`;
|
|
17112
17137
|
}
|
|
@@ -17125,6 +17150,7 @@ function strayCronDrifts(workflows) {
|
|
|
17125
17150
|
function unlauncheredLlmDrift(entry) {
|
|
17126
17151
|
if (entry.llm !== "yes") return null;
|
|
17127
17152
|
if (isHarbourLlmLauncher(entry.executor)) return null;
|
|
17153
|
+
if (entry.executor.startsWith("aws-scheduler") && entry.executor.includes("mmi-hub-schedule-dispatcher")) return null;
|
|
17128
17154
|
const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
|
|
17129
17155
|
return {
|
|
17130
17156
|
class: "unlaunchered-llm",
|
|
@@ -17142,7 +17168,7 @@ function unlauncheredLlmDrifts(entries) {
|
|
|
17142
17168
|
}
|
|
17143
17169
|
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
17144
17170
|
}
|
|
17145
|
-
function assembleReconciliation(githubEntries2, readRepos, registry2) {
|
|
17171
|
+
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
17146
17172
|
if (registry2 === null) {
|
|
17147
17173
|
return {
|
|
17148
17174
|
reconciliation: [],
|
|
@@ -17151,7 +17177,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2) {
|
|
|
17151
17177
|
};
|
|
17152
17178
|
}
|
|
17153
17179
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
17154
|
-
const drifts = reconcileGithubActions(live, registry2, readRepos);
|
|
17180
|
+
const drifts = reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames);
|
|
17155
17181
|
return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
|
|
17156
17182
|
}
|
|
17157
17183
|
|
|
@@ -17214,7 +17240,7 @@ async function githubEntries(client) {
|
|
|
17214
17240
|
const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
|
|
17215
17241
|
repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
|
|
17216
17242
|
} catch (e) {
|
|
17217
|
-
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos };
|
|
17243
|
+
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
|
|
17218
17244
|
}
|
|
17219
17245
|
const results = await Promise.all(
|
|
17220
17246
|
repos.map(async (repo) => {
|
|
@@ -17238,7 +17264,7 @@ async function githubEntries(client) {
|
|
|
17238
17264
|
}
|
|
17239
17265
|
const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
|
|
17240
17266
|
drift.push(...reconciliation.map(renderDrift));
|
|
17241
|
-
return { entries, incomplete, drift, reconciliation, readRepos };
|
|
17267
|
+
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
|
|
17242
17268
|
}
|
|
17243
17269
|
async function awsJson(args) {
|
|
17244
17270
|
const run = async () => {
|
|
@@ -17265,9 +17291,10 @@ async function awsEntries() {
|
|
|
17265
17291
|
incomplete.push(`aws: events list-rules failed \u2014 ${e.message}`);
|
|
17266
17292
|
}
|
|
17267
17293
|
try {
|
|
17268
|
-
const
|
|
17269
|
-
for (const
|
|
17270
|
-
const
|
|
17294
|
+
const refs = awsScheduleRefs(await awsJson(["scheduler", "list-schedules"]));
|
|
17295
|
+
for (const ref of refs) {
|
|
17296
|
+
const args = ["scheduler", "get-schedule", "--name", ref.name, ...ref.group ? ["--group-name", ref.group] : []];
|
|
17297
|
+
const entry = awsScheduleEntry(await awsJson(args));
|
|
17271
17298
|
if (entry) entries.push(entry);
|
|
17272
17299
|
}
|
|
17273
17300
|
} catch (e) {
|
|
@@ -17279,20 +17306,34 @@ async function awsEntries() {
|
|
|
17279
17306
|
async function defaultRegistryRead() {
|
|
17280
17307
|
return fetchSchedulesList(registryClientDeps(await loadConfig()));
|
|
17281
17308
|
}
|
|
17282
|
-
async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead) {
|
|
17283
|
-
const [gh, aws, registry2] = await Promise.all([
|
|
17309
|
+
async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead) {
|
|
17310
|
+
const [gh, aws, registry2, projects] = await Promise.all([
|
|
17284
17311
|
githubEntries(client),
|
|
17285
17312
|
awsEntries(),
|
|
17286
|
-
readRegistry().catch(() => null)
|
|
17313
|
+
readRegistry().catch(() => null),
|
|
17314
|
+
readProjects().catch(() => null)
|
|
17287
17315
|
]);
|
|
17288
|
-
const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2);
|
|
17316
|
+
const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames));
|
|
17317
|
+
const selfManaged = /* @__PURE__ */ new Set();
|
|
17318
|
+
for (const proj of projects ?? []) {
|
|
17319
|
+
if (proj?.schedulesMode !== "self-managed") continue;
|
|
17320
|
+
for (const r of proj.repos ?? []) selfManaged.add((r.split("/").pop() ?? "").toLowerCase());
|
|
17321
|
+
}
|
|
17322
|
+
const reconciliation = suppressSelfManagedDrifts(
|
|
17323
|
+
[...gh.reconciliation, ...aws.reconciliation, ...recon.reconciliation],
|
|
17324
|
+
selfManaged
|
|
17325
|
+
);
|
|
17326
|
+
const driftLines = reconciliation.map(renderDrift);
|
|
17289
17327
|
return {
|
|
17290
17328
|
entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
|
|
17291
17329
|
incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
|
|
17292
|
-
drift:
|
|
17293
|
-
reconciliation
|
|
17330
|
+
drift: driftLines,
|
|
17331
|
+
reconciliation
|
|
17294
17332
|
};
|
|
17295
17333
|
}
|
|
17334
|
+
async function defaultProjectsRead() {
|
|
17335
|
+
return fetchProjectsList(registryClientDeps(await loadConfig()));
|
|
17336
|
+
}
|
|
17296
17337
|
function warnIncomplete2(incomplete) {
|
|
17297
17338
|
for (const f of incomplete) console.error(`org schedules: WARNING \u2014 ${f}`);
|
|
17298
17339
|
if (incomplete.length) console.error("org schedules: this notebook is INCOMPLETE \u2014 schedules may be missing from it.");
|
|
@@ -17301,7 +17342,7 @@ function reportDrift(drift) {
|
|
|
17301
17342
|
for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
|
|
17302
17343
|
}
|
|
17303
17344
|
function registerSchedulesCommands(program3) {
|
|
17304
|
-
program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "splice the generated inventory into the given doc between the schedules:inventory markers (docs/schedules.md)").action(async (o) => {
|
|
17345
|
+
const schedules = program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "splice the generated inventory into the given doc between the schedules:inventory markers (docs/schedules.md)").action(async (o) => {
|
|
17305
17346
|
try {
|
|
17306
17347
|
const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
|
|
17307
17348
|
if (o.doc) {
|
|
@@ -17329,6 +17370,44 @@ function registerSchedulesCommands(program3) {
|
|
|
17329
17370
|
await failGraceful(e.message);
|
|
17330
17371
|
}
|
|
17331
17372
|
});
|
|
17373
|
+
schedules.command("run <schedule-id>").description("fire one registered schedule NOW through the harbour dispatcher (same guardrails as a clocked run) \u2014 master or a repo-admin of the schedule\u2019s repo (#3220)").action(async (scheduleId) => {
|
|
17374
|
+
try {
|
|
17375
|
+
const res = await postSchedulesRun(scheduleId, registryClientDeps(await loadConfig()));
|
|
17376
|
+
if (!res.ok) {
|
|
17377
|
+
await failGraceful(`schedules run: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
|
|
17378
|
+
return;
|
|
17379
|
+
}
|
|
17380
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
17381
|
+
} catch (e) {
|
|
17382
|
+
await failGraceful(e.message);
|
|
17383
|
+
}
|
|
17384
|
+
});
|
|
17385
|
+
schedules.command("mode <mode>").description("fleet schedules doctrine for a repo: 'managed' (default \u2014 Hub registry + drift enforcement) or 'self-managed' (opt out; the repo owns its crons). Project-admin self-serve (#3228).").option("--repo <name>", "bare repo name (defaults to the origin remote basename)").action(async (mode, o) => {
|
|
17386
|
+
try {
|
|
17387
|
+
if (mode !== "managed" && mode !== "self-managed") {
|
|
17388
|
+
await failGraceful("schedules mode: mode must be 'managed' or 'self-managed'");
|
|
17389
|
+
return;
|
|
17390
|
+
}
|
|
17391
|
+
let repo = o.repo?.trim();
|
|
17392
|
+
if (!repo) {
|
|
17393
|
+
const url = await gitOut(["remote", "get-url", "origin"]);
|
|
17394
|
+
repo = url.trim().replace(/\.git$/, "").split(/[/:]/).filter(Boolean).pop() ?? "";
|
|
17395
|
+
}
|
|
17396
|
+
if (!repo) {
|
|
17397
|
+
await failGraceful("schedules mode: could not resolve the repo \u2014 pass --repo <name>");
|
|
17398
|
+
return;
|
|
17399
|
+
}
|
|
17400
|
+
const slug = repo.toLowerCase();
|
|
17401
|
+
const res = await postSchedulesMode(slug, mode, registryClientDeps(await loadConfig()));
|
|
17402
|
+
if (!res.ok) {
|
|
17403
|
+
await failGraceful(`schedules mode: HTTP ${res.status} \u2014 ${res.error ?? "write failed"}`);
|
|
17404
|
+
return;
|
|
17405
|
+
}
|
|
17406
|
+
console.log(JSON.stringify({ ok: true, slug, schedulesMode: mode }));
|
|
17407
|
+
} catch (e) {
|
|
17408
|
+
await failGraceful(e.message);
|
|
17409
|
+
}
|
|
17410
|
+
});
|
|
17332
17411
|
}
|
|
17333
17412
|
|
|
17334
17413
|
// src/schedules-lift-command.ts
|
|
@@ -17340,7 +17419,7 @@ var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor"
|
|
|
17340
17419
|
function parseScheduleHeader(yamlText) {
|
|
17341
17420
|
const out = {};
|
|
17342
17421
|
for (const rawLine of yamlText.split("\n")) {
|
|
17343
|
-
const m = /^#\s*(schedule|what|owner|cadence|executor|llm|output|breaks|kill):\s*(.+?)\s*$/.exec(rawLine);
|
|
17422
|
+
const m = /^#\s*(schedule|what|owner|cadence|executor|llm|output|breaks|kill|target):\s*(.+?)\s*$/.exec(rawLine);
|
|
17344
17423
|
if (m && out[m[1]] === void 0) out[m[1]] = m[2].trim();
|
|
17345
17424
|
}
|
|
17346
17425
|
return out;
|
|
@@ -17373,6 +17452,10 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
17373
17452
|
if (llmFromHeader(yamlText) === "unknown") {
|
|
17374
17453
|
throw new Error(`schedules lift: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
|
|
17375
17454
|
}
|
|
17455
|
+
const target = header.target;
|
|
17456
|
+
if (target !== void 0 && !/^[A-Za-z0-9_-]{1,140}$/.test(target)) {
|
|
17457
|
+
throw new Error(`schedules lift: ${expectedId} header \`target: ${target}\` is not a bare lambda function name (allowed: A-Z a-z 0-9 _ -; max 140).`);
|
|
17458
|
+
}
|
|
17376
17459
|
return {
|
|
17377
17460
|
id: expectedId,
|
|
17378
17461
|
what: h.what,
|
|
@@ -17384,7 +17467,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
17384
17467
|
breaks: h.breaks,
|
|
17385
17468
|
kill: h.kill,
|
|
17386
17469
|
repo,
|
|
17387
|
-
sourcePath: workflowPath
|
|
17470
|
+
sourcePath: workflowPath,
|
|
17471
|
+
...target !== void 0 ? { target } : {}
|
|
17388
17472
|
};
|
|
17389
17473
|
}
|
|
17390
17474
|
function scheduleRecordsFromWorkflows(repo, files) {
|
|
@@ -17475,7 +17559,7 @@ async function runSchedulesLift(opts, deps = {}) {
|
|
|
17475
17559
|
function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
17476
17560
|
const schedules = program3.commands.find((c) => c.name() === "schedules");
|
|
17477
17561
|
if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
|
|
17478
|
-
schedules.command("lift").description("
|
|
17562
|
+
schedules.command("register").alias("lift").description("register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from `lift`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187)").option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write").action(async (o) => {
|
|
17479
17563
|
try {
|
|
17480
17564
|
const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
|
|
17481
17565
|
if (o.dryRun) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.36.1",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|