@mutmutco/cli 4.5.43 → 4.5.44

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 +187 -18
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -7620,6 +7620,33 @@ function cadenceContainsCron(cadence, cron) {
7620
7620
  if (!cronTokens.length || cronTokens.length > cadenceTokens.length) return false;
7621
7621
  return cadenceTokens.some((_, i) => i + cronTokens.length <= cadenceTokens.length && cronTokens.every((t, j) => cadenceTokens[i + j] === t));
7622
7622
  }
7623
+ var DECLARED_UNSCHEDULED = /* @__PURE__ */ new Set(["on demand", "on-demand", "manual", "manual only", "event-driven"]);
7624
+ var CRON_FIELD2 = /^[0-9A-Za-z*,/\-]+$/;
7625
+ var DOW_NAMES = /* @__PURE__ */ new Set(["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]);
7626
+ function dowOk(dow) {
7627
+ return dow.split(",").every((part) => {
7628
+ const slash2 = part.indexOf("/");
7629
+ const step = slash2 === -1 ? "" : part.slice(slash2);
7630
+ if (step !== "" && !/^\/[1-9]\d*$/.test(step)) return false;
7631
+ const tokens2 = (slash2 === -1 ? part : part.slice(0, slash2)).split("-");
7632
+ if (tokens2.length > 2 || tokens2.some((t) => t === "")) return false;
7633
+ return tokens2.every((t) => t === "*" || (/^\d+$/.test(t) ? Number(t) <= 7 : DOW_NAMES.has(t.toUpperCase())));
7634
+ });
7635
+ }
7636
+ function cadenceProblem(cadence) {
7637
+ const c = cadence.trim();
7638
+ const cut = c.search(/[(+]/);
7639
+ const lead = (cut === -1 ? c : c.slice(0, cut)).trim();
7640
+ if (DECLARED_UNSCHEDULED.has(lead.toLowerCase())) return null;
7641
+ if (/^((?:rate|cron)\([^()]+\))(?:\s*[(+].*)?$/is.test(c)) return null;
7642
+ const f = lead.split(/\s+/);
7643
+ if (f.length !== 5) return `cadence not mappable to a Scheduler expression: ${JSON.stringify(cadence)}`;
7644
+ const [minute, hour, dom, month, dow] = f;
7645
+ if (![minute, hour, dom, month].every((x) => CRON_FIELD2.test(x)) || dom !== "*" && dow !== "*" || !dowOk(dow)) {
7646
+ return `cadence not mappable to a Scheduler expression: ${JSON.stringify(cadence)}`;
7647
+ }
7648
+ return null;
7649
+ }
7623
7650
  function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7624
7651
  const crons = extractWorkflowCrons(yamlText);
7625
7652
  const header = parseScheduleHeader(yamlText);
@@ -7638,6 +7665,10 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7638
7665
  if (!SCHEDULE_EXECUTORS.has(h.executor)) {
7639
7666
  throw new Error(`schedules register: ${expectedId} header \`executor: ${h.executor}\` is not a member of the closed executor taxonomy (${[...SCHEDULE_EXECUTORS].join(", ")}) \u2014 an unknown executor would register a row the harbour can never dispatch (docs/schedules.md "The entry template", H2 #4429).`);
7640
7667
  }
7668
+ const problem = cadenceProblem(h.cadence);
7669
+ if (problem) {
7670
+ throw new Error(`schedules register: ${expectedId} ${problem} \u2014 lead with a 5-field cron, rate(...) or cron(...), then any note in (...) or after +; or declare one of: ${[...DECLARED_UNSCHEDULED].join(", ")} (docs/schedules.md "The entry template", #7415).`);
7671
+ }
7641
7672
  for (const cron of crons) {
7642
7673
  if (!cadenceContainsCron(h.cadence, cron)) {
7643
7674
  throw new Error(`schedules register: ${expectedId} cadence \`${h.cadence}\` does not contain the workflow cron \`${cron}\` (whole-token comparison) \u2014 the declared cadence disagrees with the actual schedule.`);
@@ -14366,7 +14397,7 @@ function loadBootstrapSeeds(manifestJson) {
14366
14397
  };
14367
14398
  }
14368
14399
  function renderSeed(template, vars) {
14369
- return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key] : match);
14400
+ return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key].replace(PLACEHOLDER_RE, (inner, innerKey) => innerKey !== key && innerKey in vars ? vars[innerKey] : inner) : match);
14370
14401
  }
14371
14402
  function missingPlaceholders(rendered) {
14372
14403
  const out = /* @__PURE__ */ new Set();
@@ -15370,7 +15401,10 @@ function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches
15370
15401
  GATE_MAX_SECONDS: DEFAULT_GATE_MAX_SECONDS,
15371
15402
  // The pin is the CLI's blessed SHA, not an operator knob — one central place to bump (#3178).
15372
15403
  GATE_BUDGET_SHA: BLESSED_RUN_WITH_BUDGET_SHA,
15373
- GATE_RUNNER_SHA: BLESSED_RUNNER_GATE_SHA
15404
+ GATE_RUNNER_SHA: BLESSED_RUNNER_GATE_SHA,
15405
+ // #7407: repo-declared service containers and extra check steps; empty renders today's gate byte-for-byte.
15406
+ GATE_SERVICES_YAML: "",
15407
+ GATE_EXTRA_STEPS_YAML: ""
15374
15408
  };
15375
15409
  const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
15376
15410
  const trackBranches = track === "trunk" ? ["main"] : track === "direct" ? ["development", "main"] : ["development", "rc", "main"];
@@ -15417,6 +15451,112 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack, requiredCheckBranc
15417
15451
  if (!out.GATE_AFFECTED_CMD.trim()) out.GATE_AFFECTED_CMD = out.GATE_CMD;
15418
15452
  return out;
15419
15453
  }
15454
+ var GATE_SERVICE_KEYS = /* @__PURE__ */ new Set(["image", "env", "ports", "options", "tmpfs"]);
15455
+ var GATE_STEP_KEYS = /* @__PURE__ */ new Set(["name", "command", "env", "maxSeconds"]);
15456
+ function gateLine(value, field) {
15457
+ if (typeof value !== "string" || !value.trim() || /[\r\n]/.test(value)) throw new Error(`${field} must be a non-empty single-line string`);
15458
+ return value.trim();
15459
+ }
15460
+ function gateEnv(value, field) {
15461
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object of NAME: "value" strings`);
15462
+ const out = {};
15463
+ for (const [name, entry] of Object.entries(value)) {
15464
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`${field}.${name} is not a valid environment variable name`);
15465
+ if (typeof entry !== "string" || /[\r\n]/.test(entry)) throw new Error(`${field}.${name} must be a single-line string`);
15466
+ out[name] = entry;
15467
+ }
15468
+ return out;
15469
+ }
15470
+ function gateSeconds(value, field) {
15471
+ const text = typeof value === "number" ? String(value) : value;
15472
+ if (typeof text !== "string" || !/^\d+$/.test(text.trim()) || Number(text) <= 0) throw new Error(`${field} must be a positive whole number of seconds`);
15473
+ return text.trim();
15474
+ }
15475
+ function validateGateExtras(gate) {
15476
+ const out = {};
15477
+ if (gate.services !== void 0) {
15478
+ if (!gate.services || typeof gate.services !== "object" || Array.isArray(gate.services)) throw new Error("gate.services must be an object keyed by service name");
15479
+ out.services = {};
15480
+ for (const [name, raw] of Object.entries(gate.services)) {
15481
+ const field = `gate.services.${name}`;
15482
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`${field}: service name must be lowercase letters, digits, - or _`);
15483
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${field} must be an object with an image`);
15484
+ const service = raw;
15485
+ for (const key of Object.keys(service)) if (!GATE_SERVICE_KEYS.has(key)) throw new Error(`${field}.${key} is not a service field (expected image/env/ports/options/tmpfs)`);
15486
+ const parsed = { image: gateLine(service.image, `${field}.image`) };
15487
+ if (service.env !== void 0) parsed.env = gateEnv(service.env, `${field}.env`);
15488
+ if (service.ports !== void 0) {
15489
+ if (!Array.isArray(service.ports) || service.ports.length === 0) throw new Error(`${field}.ports must be a non-empty array`);
15490
+ parsed.ports = service.ports.map((port, index) => {
15491
+ const ok = typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536 || typeof port === "string" && /^\d{1,5}(:\d{1,5})?$/.test(port);
15492
+ if (!ok) throw new Error(`${field}.ports[${index}] must be a port number or "host:container"`);
15493
+ return port;
15494
+ });
15495
+ }
15496
+ if (service.options !== void 0) parsed.options = gateLine(service.options, `${field}.options`);
15497
+ if (service.tmpfs !== void 0) {
15498
+ if (!Array.isArray(service.tmpfs) || service.tmpfs.length === 0) throw new Error(`${field}.tmpfs must be a non-empty array of paths`);
15499
+ parsed.tmpfs = service.tmpfs.map((path2, index) => gateLine(path2, `${field}.tmpfs[${index}]`));
15500
+ }
15501
+ out.services[name] = parsed;
15502
+ }
15503
+ }
15504
+ if (gate.steps !== void 0) {
15505
+ if (!Array.isArray(gate.steps)) throw new Error("gate.steps must be an array");
15506
+ out.steps = gate.steps.map((raw, index) => {
15507
+ const field = `gate.steps[${index}]`;
15508
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${field} must be an object with name and command`);
15509
+ const step = raw;
15510
+ for (const key of Object.keys(step)) if (!GATE_STEP_KEYS.has(key)) throw new Error(`${field}.${key} is not a step field (expected name/command/env/maxSeconds)`);
15511
+ const parsed = { name: gateLine(step.name, `${field}.name`), command: gateLine(step.command, `${field}.command`) };
15512
+ if (step.env !== void 0) parsed.env = gateEnv(step.env, `${field}.env`);
15513
+ if (step.maxSeconds !== void 0) parsed.maxSeconds = gateSeconds(step.maxSeconds, `${field}.maxSeconds`);
15514
+ return parsed;
15515
+ });
15516
+ }
15517
+ return out;
15518
+ }
15519
+ function yamlScalar(value) {
15520
+ const plainSafe = /^[A-Za-z0-9_./$(][^\r\n]*$/.test(value) && !/: |\s#|[\s:]$/.test(value) && !/^(?:true|false|yes|no|on|off|null|~|[-+]?[\d.][\d_.eE+-]*)$/i.test(value);
15521
+ return plainSafe ? value : JSON.stringify(value);
15522
+ }
15523
+ function envYaml(env, indent) {
15524
+ return Object.entries(env).map(([name, value]) => `
15525
+ ${indent}${name}: ${yamlScalar(value)}`).join("");
15526
+ }
15527
+ function gateServicesYaml(services) {
15528
+ let out = "\n services:";
15529
+ for (const [name, service] of Object.entries(services)) {
15530
+ out += `
15531
+ ${name}:
15532
+ image: ${yamlScalar(service.image)}`;
15533
+ if (service.env && Object.keys(service.env).length) out += `
15534
+ env:${envYaml(service.env, " ")}`;
15535
+ if (service.ports) out += `
15536
+ ports:${service.ports.map((port) => `
15537
+ - ${typeof port === "number" ? port : yamlScalar(port)}`).join("")}`;
15538
+ const options = [...service.options ? [service.options] : [], ...(service.tmpfs ?? []).map((path2) => `--tmpfs ${path2}`)];
15539
+ if (options.length) out += `
15540
+ options: >-${options.map((line) => `
15541
+ ${line}`).join("")}`;
15542
+ }
15543
+ return out;
15544
+ }
15545
+ function gateExtraStepsYaml(steps) {
15546
+ return steps.map((step) => [
15547
+ `
15548
+ - name: ${yamlScalar(step.name)}`,
15549
+ step.env && Object.keys(step.env).length ? `
15550
+ env:${envYaml(step.env, " ")}` : "",
15551
+ "\n uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}",
15552
+ "\n with:",
15553
+ `
15554
+ command: ${yamlScalar(step.command)}`,
15555
+ `
15556
+ max-seconds: ${step.maxSeconds ?? "{{GATE_MAX_SECONDS}}"}`,
15557
+ "\n working-directory: {{GATE_WORKDIR}}"
15558
+ ].join("")).join("");
15559
+ }
15420
15560
  function gateConfigToVars(gate) {
15421
15561
  const out = {};
15422
15562
  if (!gate || typeof gate !== "object") return out;
@@ -15428,6 +15568,9 @@ function gateConfigToVars(gate) {
15428
15568
  if (typeof gate.pyVersion === "string" && gate.pyVersion.trim()) out.GATE_PY_VERSION = gate.pyVersion;
15429
15569
  const seconds = typeof gate.maxSeconds === "number" ? String(gate.maxSeconds) : gate.maxSeconds;
15430
15570
  if (typeof seconds === "string" && /^\d+$/.test(seconds.trim()) && Number(seconds) > 0) out.GATE_MAX_SECONDS = seconds.trim();
15571
+ const extras = validateGateExtras(gate);
15572
+ if (extras.services && Object.keys(extras.services).length) out.GATE_SERVICES_YAML = gateServicesYaml(extras.services);
15573
+ if (extras.steps?.length) out.GATE_EXTRA_STEPS_YAML = gateExtraStepsYaml(extras.steps);
15431
15574
  return out;
15432
15575
  }
15433
15576
  function seedMatchesDeployModel(seed, deployModel) {
@@ -16871,10 +17014,10 @@ var rollout_plan_default = {
16871
17014
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
16872
17015
  },
16873
17016
  baseline: {
16874
- version: "4.5.43",
16875
- tag: "v4.5.43",
16876
- commit: "daaa2fa607cd",
16877
- npm: "@mutmutco/cli@4.5.43"
17017
+ version: "4.5.44",
17018
+ tag: "v4.5.44",
17019
+ commit: "dda7c3cc34f2",
17020
+ npm: "@mutmutco/cli@4.5.44"
16878
17021
  },
16879
17022
  exitCriterion: "fleet-n-of-n",
16880
17023
  hubOnlyShortcut: "forbidden",
@@ -16891,14 +17034,14 @@ var rollout_plan_default = {
16891
17034
  repo: "mutmutco/mmi-hub",
16892
17035
  role: "canary",
16893
17036
  schedule: "train",
16894
- v3Target: "v4.5.43"
17037
+ v3Target: "v4.5.44"
16895
17038
  }
16896
17039
  ],
16897
17040
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
16898
17041
  rollback: {
16899
17042
  independent: true,
16900
- mechanism: "npm dist-tag latest -> 4.5.43 and redeploy the Hub Lambda from tag v4.5.43 (daaa2fa607cd); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16901
- v3Target: "v4.5.43 (@mutmutco/cli@4.5.43, tag commit daaa2fa607cd \u2014 last known-good release carrying the repo-index v4-only contract)"
17043
+ mechanism: "npm dist-tag latest -> 4.5.44 and redeploy the Hub Lambda from tag v4.5.44 (dda7c3cc34f2); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17044
+ v3Target: "v4.5.44 (@mutmutco/cli@4.5.44, tag commit dda7c3cc34f2 \u2014 last known-good release carrying the repo-index v4-only contract)"
16902
17045
  }
16903
17046
  },
16904
17047
  {
@@ -27801,8 +27944,15 @@ async function seedGateYml(repo, deps, meta, result) {
27801
27944
  result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli oracle org project set ${repo} --var gate={...}\` first, then re-run reconcile`);
27802
27945
  return "none";
27803
27946
  }
27947
+ let gateVars;
27948
+ try {
27949
+ gateVars = gateConfigToVars(gate);
27950
+ } catch (e) {
27951
+ result.errors.push(`gate.yml re-seed: registry gate config is invalid \u2014 ${e.message}`);
27952
+ return "none";
27953
+ }
27804
27954
  const derivedVars = withDerivedRepoVars(
27805
- { ...gateConfigToVars(gate), REPO_SLUG: parsed.slug },
27955
+ { ...gateVars, REPO_SLUG: parsed.slug },
27806
27956
  parsed,
27807
27957
  "deployable",
27808
27958
  releaseTrack,
@@ -29906,13 +30056,15 @@ function parseManifest(text) {
29906
30056
  const where = `box control manifest: files[${i}]`;
29907
30057
  if (typeof f.src !== "string" || !f.src || f.src.startsWith("/") || f.src.includes("..")) throw new Error(`${where}: src must be a repo-relative path`);
29908
30058
  if (f.mode !== "0644" && f.mode !== "0755") throw new Error(`${where} (${f.src}): mode must be "0644" or "0755"`);
29909
- if (f.post !== void 0 && (typeof f.post !== "string" || !ALLOWED_POST.has(f.post))) {
29910
- throw new Error(`${where} (${f.src}): post must be one of ${[...ALLOWED_POST].join(", ")}`);
29911
- }
29912
30059
  const targets = [];
29913
30060
  if (Array.isArray(f.targets)) targets.push(...f.targets);
29914
30061
  if (f.lanes !== void 0) targets.push(...expandLanes(f.lanes, where));
29915
30062
  if (!targets.length) throw new Error(`${where} (${f.src}): needs targets or lanes`);
30063
+ const ownSysctl = typeof f.post === "string" && /^sysctl -p (\/etc\/sysctl\.d\/[A-Za-z0-9._-]+\.conf)$/.exec(f.post);
30064
+ const postOk = f.post === void 0 || typeof f.post === "string" && ALLOWED_POST.has(f.post) || !!ownSysctl && targets.every((t) => t.dest === ownSysctl[1]);
30065
+ if (!postOk) {
30066
+ throw new Error(`${where} (${f.src}): post must be one of ${[...ALLOWED_POST].join(", ")}, or \`sysctl -p <its own /etc/sysctl.d dest>\``);
30067
+ }
29916
30068
  for (const t of targets) {
29917
30069
  if (typeof t.box !== "string" || !/^[a-z0-9-]+$/.test(t.box)) throw new Error(`${where} (${f.src}): bad box name`);
29918
30070
  if (typeof t.dest !== "string" || !/^\/[A-Za-z0-9._\/-]+$/.test(t.dest) || t.dest.includes("..")) {
@@ -29947,9 +30099,19 @@ function planRemoteScript(dests) {
29947
30099
  for (const d of dests) {
29948
30100
  lines2.push(`if [ -f '${d}' ]; then printf '%s %s %s\\n' "$(md5sum < '${d}' | cut -d' ' -f1)" "$(stat -c %a '${d}')" '${d}'; else printf 'MISSING - %s\\n' '${d}'; fi`);
29949
30101
  }
30102
+ lines2.push(`for f in ${ALERT_DIR}/*; do [ -f "$f" ] && printf 'ALERT %s %s\\n' "$(basename "$f")" "$(head -c 300 "$f" | tr '\\n' ' ')"; done; true`);
29950
30103
  return `${lines2.join("\n")}
29951
30104
  `;
29952
30105
  }
30106
+ var ALERT_DIR = "/var/lib/mmi-control/alerts";
30107
+ function parseAlerts(output) {
30108
+ const out = [];
30109
+ for (const line of output.split("\n")) {
30110
+ const m = /^ALERT ([A-Za-z0-9._-]+) ?(.*)$/.exec(line.trim());
30111
+ if (m) out.push({ name: m[1], message: m[2].trim() });
30112
+ }
30113
+ return out;
30114
+ }
29953
30115
  function parsePlanOutput(output, rows) {
29954
30116
  const seen = /* @__PURE__ */ new Map();
29955
30117
  for (const line of output.split("\n")) {
@@ -30415,14 +30577,20 @@ function parseGateVar(raw) {
30415
30577
  const map = parsed;
30416
30578
  const out = {};
30417
30579
  for (const [key, value] of Object.entries(map)) {
30418
- if (key === "runtime") {
30580
+ if (key === "services" || key === "steps") {
30581
+ try {
30582
+ Object.assign(out, validateGateExtras({ [key]: value }));
30583
+ } catch (error) {
30584
+ throw new Error(`org project set: ${error.message}`);
30585
+ }
30586
+ } else if (key === "runtime") {
30419
30587
  if (value !== "node" && value !== "python") throw new Error('org project set: gate.runtime must be "node" or "python"');
30420
30588
  out.runtime = value;
30421
30589
  } else if (key === "cmd" || key === "affectedCmd" || key === "workdir" || key === "cacheDepPath" || key === "pyVersion") {
30422
30590
  if (typeof value !== "string" || !value.trim()) throw new Error(`org project set: gate.${key} must be a non-empty string`);
30423
30591
  out[key] = value.trim();
30424
30592
  } else {
30425
- throw new Error(`org project set: gate key "${key}" \u2014 expected only runtime/cmd/affectedCmd/workdir/cacheDepPath/pyVersion`);
30593
+ throw new Error(`org project set: gate key "${key}" \u2014 expected only runtime/cmd/affectedCmd/workdir/cacheDepPath/pyVersion/services/steps`);
30426
30594
  }
30427
30595
  }
30428
30596
  return out;
@@ -31625,10 +31793,11 @@ ${SSH_RECIPE_AGENT_NOTE}`);
31625
31793
  const results = parsePlanOutput(output, probeRows);
31626
31794
  const drift = results.filter((r) => r.status !== "in-sync").length;
31627
31795
  const unread = results.some((r) => r.status === "unread");
31628
- const payload = { box: found.box.name, ref: o.ref, mode: o.apply ? "apply" : "plan", files: results, drift, ok: drift === 0 };
31796
+ const alerts = parseAlerts(output);
31797
+ const payload = { box: found.box.name, ref: o.ref, mode: o.apply ? "apply" : "plan", files: results, drift, alerts, ok: drift === 0 && alerts.length === 0 };
31629
31798
  if (o.json) console.log(JSON.stringify(payload, null, 2));
31630
- else console.log(formatControl(payload.mode, found.box.name, results));
31631
- process.exitCode = unread ? 2 : drift ? 1 : 0;
31799
+ else console.log([formatControl(payload.mode, found.box.name, results), ...alerts.map((a) => `ALERT ${a.name}: ${a.message}`)].join("\n"));
31800
+ process.exitCode = unread ? 2 : drift || alerts.length ? 1 : 0;
31632
31801
  } catch (e) {
31633
31802
  if (o.json) console.log(JSON.stringify({ box: name, ref: o.ref, ok: false, error: e.message }, null, 2));
31634
31803
  else console.error(e.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.43",
3
+ "version": "4.5.44",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",