@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.
Files changed (2) hide show
  1. package/dist/main.cjs +106 -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
  }
@@ -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 names = awsScheduleNames(await awsJson(["scheduler", "list-schedules"]));
17269
- for (const name of names) {
17270
- const entry = awsScheduleEntry(await awsJson(["scheduler", "get-schedule", "--name", name]));
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: [...gh.drift, ...aws.drift, ...recon.driftLines],
17293
- reconciliation: [...gh.reconciliation, ...aws.reconciliation, ...recon.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("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) => {
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.35.2",
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",