@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.
Files changed (2) hide show
  1. package/dist/main.cjs +105 -22
  2. 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 awsScheduleNames(payload) {
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
- return schedules.map((s) => s && typeof s === "object" ? s.Name : void 0).filter((n) => typeof n === "string" && !isJervResource(n));
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 armed workflow",
17104
- remedy: `remove it from ${r.repo} (or re-arm the workflow), then re-run the schedules lift to prune the stale SCHEDULE# row`
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 names = awsScheduleNames(await awsJson(["scheduler", "list-schedules"]));
17269
- for (const name of names) {
17270
- const entry = awsScheduleEntry(await awsJson(["scheduler", "get-schedule", "--name", name]));
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: [...gh.drift, ...aws.drift, ...recon.driftLines],
17293
- reconciliation: [...gh.reconciliation, ...aws.reconciliation, ...recon.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("lift this repo's eight-field workflow headers into SCHEDULE# registry rows (C2, #3186) \u2014 a per-repo replace; aborts the whole lift 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) => {
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.35.1",
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",