@mutmutco/cli 3.35.1 → 3.36.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/main.cjs +105 -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
|
}
|
|
@@ -17142,7 +17167,7 @@ function unlauncheredLlmDrifts(entries) {
|
|
|
17142
17167
|
}
|
|
17143
17168
|
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
17144
17169
|
}
|
|
17145
|
-
function assembleReconciliation(githubEntries2, readRepos, registry2) {
|
|
17170
|
+
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
17146
17171
|
if (registry2 === null) {
|
|
17147
17172
|
return {
|
|
17148
17173
|
reconciliation: [],
|
|
@@ -17151,7 +17176,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2) {
|
|
|
17151
17176
|
};
|
|
17152
17177
|
}
|
|
17153
17178
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
17154
|
-
const drifts = reconcileGithubActions(live, registry2, readRepos);
|
|
17179
|
+
const drifts = reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames);
|
|
17155
17180
|
return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
|
|
17156
17181
|
}
|
|
17157
17182
|
|
|
@@ -17214,7 +17239,7 @@ async function githubEntries(client) {
|
|
|
17214
17239
|
const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
|
|
17215
17240
|
repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
|
|
17216
17241
|
} catch (e) {
|
|
17217
|
-
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos };
|
|
17242
|
+
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
|
|
17218
17243
|
}
|
|
17219
17244
|
const results = await Promise.all(
|
|
17220
17245
|
repos.map(async (repo) => {
|
|
@@ -17238,7 +17263,7 @@ async function githubEntries(client) {
|
|
|
17238
17263
|
}
|
|
17239
17264
|
const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
|
|
17240
17265
|
drift.push(...reconciliation.map(renderDrift));
|
|
17241
|
-
return { entries, incomplete, drift, reconciliation, readRepos };
|
|
17266
|
+
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
|
|
17242
17267
|
}
|
|
17243
17268
|
async function awsJson(args) {
|
|
17244
17269
|
const run = async () => {
|
|
@@ -17265,9 +17290,10 @@ async function awsEntries() {
|
|
|
17265
17290
|
incomplete.push(`aws: events list-rules failed \u2014 ${e.message}`);
|
|
17266
17291
|
}
|
|
17267
17292
|
try {
|
|
17268
|
-
const
|
|
17269
|
-
for (const
|
|
17270
|
-
const
|
|
17293
|
+
const refs = awsScheduleRefs(await awsJson(["scheduler", "list-schedules"]));
|
|
17294
|
+
for (const ref of refs) {
|
|
17295
|
+
const args = ["scheduler", "get-schedule", "--name", ref.name, ...ref.group ? ["--group-name", ref.group] : []];
|
|
17296
|
+
const entry = awsScheduleEntry(await awsJson(args));
|
|
17271
17297
|
if (entry) entries.push(entry);
|
|
17272
17298
|
}
|
|
17273
17299
|
} catch (e) {
|
|
@@ -17279,20 +17305,34 @@ async function awsEntries() {
|
|
|
17279
17305
|
async function defaultRegistryRead() {
|
|
17280
17306
|
return fetchSchedulesList(registryClientDeps(await loadConfig()));
|
|
17281
17307
|
}
|
|
17282
|
-
async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead) {
|
|
17283
|
-
const [gh, aws, registry2] = await Promise.all([
|
|
17308
|
+
async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead) {
|
|
17309
|
+
const [gh, aws, registry2, projects] = await Promise.all([
|
|
17284
17310
|
githubEntries(client),
|
|
17285
17311
|
awsEntries(),
|
|
17286
|
-
readRegistry().catch(() => null)
|
|
17312
|
+
readRegistry().catch(() => null),
|
|
17313
|
+
readProjects().catch(() => null)
|
|
17287
17314
|
]);
|
|
17288
|
-
const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2);
|
|
17315
|
+
const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames));
|
|
17316
|
+
const selfManaged = /* @__PURE__ */ new Set();
|
|
17317
|
+
for (const proj of projects ?? []) {
|
|
17318
|
+
if (proj?.schedulesMode !== "self-managed") continue;
|
|
17319
|
+
for (const r of proj.repos ?? []) selfManaged.add((r.split("/").pop() ?? "").toLowerCase());
|
|
17320
|
+
}
|
|
17321
|
+
const reconciliation = suppressSelfManagedDrifts(
|
|
17322
|
+
[...gh.reconciliation, ...aws.reconciliation, ...recon.reconciliation],
|
|
17323
|
+
selfManaged
|
|
17324
|
+
);
|
|
17325
|
+
const driftLines = reconciliation.map(renderDrift);
|
|
17289
17326
|
return {
|
|
17290
17327
|
entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
|
|
17291
17328
|
incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
|
|
17292
|
-
drift:
|
|
17293
|
-
reconciliation
|
|
17329
|
+
drift: driftLines,
|
|
17330
|
+
reconciliation
|
|
17294
17331
|
};
|
|
17295
17332
|
}
|
|
17333
|
+
async function defaultProjectsRead() {
|
|
17334
|
+
return fetchProjectsList(registryClientDeps(await loadConfig()));
|
|
17335
|
+
}
|
|
17296
17336
|
function warnIncomplete2(incomplete) {
|
|
17297
17337
|
for (const f of incomplete) console.error(`org schedules: WARNING \u2014 ${f}`);
|
|
17298
17338
|
if (incomplete.length) console.error("org schedules: this notebook is INCOMPLETE \u2014 schedules may be missing from it.");
|
|
@@ -17301,7 +17341,7 @@ function reportDrift(drift) {
|
|
|
17301
17341
|
for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
|
|
17302
17342
|
}
|
|
17303
17343
|
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) => {
|
|
17344
|
+
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
17345
|
try {
|
|
17306
17346
|
const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
|
|
17307
17347
|
if (o.doc) {
|
|
@@ -17329,6 +17369,44 @@ function registerSchedulesCommands(program3) {
|
|
|
17329
17369
|
await failGraceful(e.message);
|
|
17330
17370
|
}
|
|
17331
17371
|
});
|
|
17372
|
+
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) => {
|
|
17373
|
+
try {
|
|
17374
|
+
const res = await postSchedulesRun(scheduleId, registryClientDeps(await loadConfig()));
|
|
17375
|
+
if (!res.ok) {
|
|
17376
|
+
await failGraceful(`schedules run: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
|
|
17377
|
+
return;
|
|
17378
|
+
}
|
|
17379
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
17380
|
+
} catch (e) {
|
|
17381
|
+
await failGraceful(e.message);
|
|
17382
|
+
}
|
|
17383
|
+
});
|
|
17384
|
+
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) => {
|
|
17385
|
+
try {
|
|
17386
|
+
if (mode !== "managed" && mode !== "self-managed") {
|
|
17387
|
+
await failGraceful("schedules mode: mode must be 'managed' or 'self-managed'");
|
|
17388
|
+
return;
|
|
17389
|
+
}
|
|
17390
|
+
let repo = o.repo?.trim();
|
|
17391
|
+
if (!repo) {
|
|
17392
|
+
const url = await gitOut(["remote", "get-url", "origin"]);
|
|
17393
|
+
repo = url.trim().replace(/\.git$/, "").split(/[/:]/).filter(Boolean).pop() ?? "";
|
|
17394
|
+
}
|
|
17395
|
+
if (!repo) {
|
|
17396
|
+
await failGraceful("schedules mode: could not resolve the repo \u2014 pass --repo <name>");
|
|
17397
|
+
return;
|
|
17398
|
+
}
|
|
17399
|
+
const slug = repo.toLowerCase();
|
|
17400
|
+
const res = await postSchedulesMode(slug, mode, registryClientDeps(await loadConfig()));
|
|
17401
|
+
if (!res.ok) {
|
|
17402
|
+
await failGraceful(`schedules mode: HTTP ${res.status} \u2014 ${res.error ?? "write failed"}`);
|
|
17403
|
+
return;
|
|
17404
|
+
}
|
|
17405
|
+
console.log(JSON.stringify({ ok: true, slug, schedulesMode: mode }));
|
|
17406
|
+
} catch (e) {
|
|
17407
|
+
await failGraceful(e.message);
|
|
17408
|
+
}
|
|
17409
|
+
});
|
|
17332
17410
|
}
|
|
17333
17411
|
|
|
17334
17412
|
// src/schedules-lift-command.ts
|
|
@@ -17340,7 +17418,7 @@ var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor"
|
|
|
17340
17418
|
function parseScheduleHeader(yamlText) {
|
|
17341
17419
|
const out = {};
|
|
17342
17420
|
for (const rawLine of yamlText.split("\n")) {
|
|
17343
|
-
const m = /^#\s*(schedule|what|owner|cadence|executor|llm|output|breaks|kill):\s*(.+?)\s*$/.exec(rawLine);
|
|
17421
|
+
const m = /^#\s*(schedule|what|owner|cadence|executor|llm|output|breaks|kill|target):\s*(.+?)\s*$/.exec(rawLine);
|
|
17344
17422
|
if (m && out[m[1]] === void 0) out[m[1]] = m[2].trim();
|
|
17345
17423
|
}
|
|
17346
17424
|
return out;
|
|
@@ -17373,6 +17451,10 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
17373
17451
|
if (llmFromHeader(yamlText) === "unknown") {
|
|
17374
17452
|
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
17453
|
}
|
|
17454
|
+
const target = header.target;
|
|
17455
|
+
if (target !== void 0 && !/^[A-Za-z0-9_-]{1,140}$/.test(target)) {
|
|
17456
|
+
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).`);
|
|
17457
|
+
}
|
|
17376
17458
|
return {
|
|
17377
17459
|
id: expectedId,
|
|
17378
17460
|
what: h.what,
|
|
@@ -17384,7 +17466,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
17384
17466
|
breaks: h.breaks,
|
|
17385
17467
|
kill: h.kill,
|
|
17386
17468
|
repo,
|
|
17387
|
-
sourcePath: workflowPath
|
|
17469
|
+
sourcePath: workflowPath,
|
|
17470
|
+
...target !== void 0 ? { target } : {}
|
|
17388
17471
|
};
|
|
17389
17472
|
}
|
|
17390
17473
|
function scheduleRecordsFromWorkflows(repo, files) {
|
|
@@ -17475,7 +17558,7 @@ async function runSchedulesLift(opts, deps = {}) {
|
|
|
17475
17558
|
function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
17476
17559
|
const schedules = program3.commands.find((c) => c.name() === "schedules");
|
|
17477
17560
|
if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
|
|
17478
|
-
schedules.command("lift").description("
|
|
17561
|
+
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
17562
|
try {
|
|
17480
17563
|
const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
|
|
17481
17564
|
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.0",
|
|
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",
|