@mutmutco/hub 4.4.11 → 4.4.12

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/index.cjs +113 -21
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -95,30 +95,30 @@ var import_node_fs = require("node:fs");
95
95
  var PRODUCTS = Object.freeze({
96
96
  "mm-strategy": Object.freeze({
97
97
  name: "MM Strategy",
98
- installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
98
+ installWarm: "Welcome. Let's get you ready.",
99
99
  accent: "38;2;249;115;22",
100
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
100
+ warm: "Welcome back. Checking for updates...",
101
101
  doctor: "mm-strategy doctor"
102
102
  }),
103
103
  "mmi-hub": Object.freeze({
104
104
  name: "mmi-hub",
105
- installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
105
+ installWarm: "Welcome. Let's get you ready.",
106
106
  accent: "38;2;125;211;252",
107
- warm: "Welcome back. Checking your surfaces\u2026",
107
+ warm: "Welcome back. Checking for updates...",
108
108
  doctor: "mmi doctor"
109
109
  }),
110
110
  "jerv-hub": Object.freeze({
111
111
  name: "jerv-hub",
112
- installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
112
+ installWarm: "Welcome. Let's get you ready.",
113
113
  accent: "38;2;248;113;113",
114
- warm: "Welcome back. Checking your surfaces\u2026",
114
+ warm: "Welcome back. Checking for updates...",
115
115
  doctor: "jerv doctor"
116
116
  }),
117
117
  jervcode: Object.freeze({
118
118
  name: "JervCode",
119
- installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
119
+ installWarm: "Welcome. Let's get you ready.",
120
120
  accent: "38;2;192;132;252",
121
- warm: "Welcome back. Keeping JervCode current\u2026",
121
+ warm: "Welcome back. Checking for updates...",
122
122
  doctor: "jervcode doctor"
123
123
  })
124
124
  });
@@ -132,6 +132,7 @@ function identityFor(product) {
132
132
 
133
133
  // ../installer/face/src/face.ts
134
134
  var GLYPH = Object.freeze({
135
+ clock: "\u23F0",
135
136
  diamond: "\u25C6",
136
137
  hollow: "\u25C7",
137
138
  bar: "\u2502",
@@ -203,12 +204,16 @@ function createFace({ product, color = false, columns, env = process.env, operat
203
204
  const continuesFace = continuedPhases.size > 0;
204
205
  const nested = env.MM_OUTER_CONSOLE === "1";
205
206
  const progressFd = readProgressFd(env);
207
+ const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
206
208
  const emitMilestone = (title, measure, kind) => {
207
- if (progressFd === null) return false;
209
+ if (progressFd === null && !progressFile) return false;
208
210
  const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
209
211
  if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
212
+ if (kind === "running" && typeof measure === "string") record.measure = measure;
210
213
  try {
211
- (0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
214
+ if (progressFile) (0, import_node_fs.appendFileSync)(progressFile, `${JSON.stringify(record)}
215
+ `);
216
+ else (0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
212
217
  `);
213
218
  return true;
214
219
  } catch {
@@ -227,7 +232,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
227
232
  };
228
233
  const step = (title, measure = null, kind = "ok") => {
229
234
  if (emitMilestone(title, measure, kind)) return "";
230
- const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
235
+ const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
231
236
  const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
232
237
  const time = measured;
233
238
  const column = Math.min(44, Math.max(0, width - 8));
@@ -237,7 +242,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
237
242
  const pad = Math.max(1, column - visibleWidth(head));
238
243
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
239
244
  };
240
- const relay = (text) => String(text).split("\n").flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
245
+ const relay = (text) => String(text).replace(/\r?\n$/, "").split(/\r?\n/).flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
241
246
  const receipt = (lines, { ready = true } = {}) => {
242
247
  if (ready && nested) return [];
243
248
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
@@ -268,7 +273,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
268
273
  };
269
274
  const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
270
275
  const refusal = (message3) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message3}`;
271
- return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
276
+ return { identity, width, nested, emitsProgress: progressFd !== null || Boolean(progressFile), welcome, continues, step, progress: (title, measure = null) => emitMilestone(title, measure, "running"), relay, receipt, outcome, signOff, refusal, paint };
272
277
  }
273
278
 
274
279
  // ../installer/face/src/shell.ts
@@ -287,7 +292,7 @@ function draw() {
287
292
  if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
288
293
  try {
289
294
  if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
290
- const text = frames[frame++ % frames.length];
295
+ const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
291
296
  writeSync(2, text);
292
297
  if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
293
298
  } finally {
@@ -299,7 +304,6 @@ parentPort.on('message', (next) => {
299
304
  if (Atomics.load(control, 2)) return;
300
305
  frames = next.frames;
301
306
  frame = next.frame;
302
- Atomics.store(control, 0, 0);
303
307
  });
304
308
  setInterval(draw, workerData.intervalMs);
305
309
  `;
@@ -310,6 +314,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
310
314
  let control = null;
311
315
  let frames = [];
312
316
  let frame = 0;
317
+ let started = 0;
318
+ let suspended = false;
313
319
  const write = (text) => {
314
320
  if (stream) stream.write(text);
315
321
  else {
@@ -319,11 +325,11 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
319
325
  }
320
326
  };
321
327
  const render = (text, measure) => {
322
- const line = face.step(text, measure, "note").split("\n")[0];
328
+ const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
323
329
  frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
324
330
  };
325
331
  const draw = () => {
326
- if (animate && frames.length) write(frames[frame++ % frames.length]);
332
+ if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
327
333
  };
328
334
  const pause = () => {
329
335
  if (!control) return;
@@ -343,6 +349,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
343
349
  start(text, measure = null) {
344
350
  if (!animate) return;
345
351
  halt();
352
+ suspended = false;
353
+ started = Date.now();
346
354
  render(text, measure);
347
355
  frame = 0;
348
356
  draw();
@@ -356,7 +364,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
356
364
  frames,
357
365
  frame,
358
366
  intervalMs,
359
- transcriptPath
367
+ transcriptPath,
368
+ started
360
369
  } });
361
370
  const active = worker;
362
371
  worker.on("error", () => {
@@ -374,9 +383,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
374
383
  render(text, measure);
375
384
  draw();
376
385
  worker?.postMessage({ frames, frame });
386
+ if (control && !suspended) Atomics.store(control, 0, 0);
387
+ },
388
+ pause() {
389
+ if (suspended) return;
390
+ suspended = true;
391
+ pause();
392
+ if (animate && frames.length) write("\r\x1B[2K");
393
+ },
394
+ resume() {
395
+ suspended = false;
396
+ if (control) Atomics.store(control, 0, 0);
397
+ if (timer || worker) draw();
377
398
  },
378
399
  stop() {
379
400
  halt();
401
+ frames = [];
380
402
  if (animate) write("\r\x1B[2K");
381
403
  }
382
404
  };
@@ -607,6 +629,7 @@ var PHASES = {
607
629
  "verify-release": ["Checking the release version", "Verified the release version"],
608
630
  verify: ["Verifying the payload", "Verified the payload"],
609
631
  install: ["Installing the product", "Installed the product"],
632
+ configure: ["Configuring the product", "Configured the product"],
610
633
  activate: ["Activating surfaces", "Activated surfaces"],
611
634
  doctor: ["Checking health", "Checked health"],
612
635
  rollback: ["Restoring the previous version", "Restored the previous version"]
@@ -651,6 +674,17 @@ function createInstallerRun(value, options = {}) {
651
674
  return true;
652
675
  } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
653
676
  });
677
+ let activeTitle;
678
+ let chunkOpen = false;
679
+ let chunkChannel = "stdout";
680
+ let chunkColumn = 0;
681
+ const trailingCR = /* @__PURE__ */ new Set();
682
+ let deferredActivity;
683
+ const closeChunk = () => {
684
+ if (chunkOpen) emit("\n", chunkChannel);
685
+ chunkOpen = false;
686
+ chunkColumn = 0;
687
+ };
654
688
  let started = false;
655
689
  let finished = false;
656
690
  const start = () => {
@@ -665,6 +699,9 @@ function createInstallerRun(value, options = {}) {
665
699
  }
666
700
  };
667
701
  const durable = (title, measure, kind) => {
702
+ closeChunk();
703
+ activeTitle = void 0;
704
+ deferredActivity = void 0;
668
705
  spinner.stop();
669
706
  const rendered = face.step(title, measure, kind);
670
707
  if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
@@ -681,7 +718,12 @@ function createInstallerRun(value, options = {}) {
681
718
  const state = facts.state ?? "ok";
682
719
  const title = PHASES[id][state === "ok" ? 1 : 0];
683
720
  if (state === "running") {
684
- spinner.start(title, facts.measure ?? null);
721
+ if (face.progress(title, facts.measure ?? null)) return;
722
+ if (activeTitle === title) spinner.say(title, facts.measure ?? null);
723
+ else {
724
+ activeTitle = title;
725
+ spinner.start(title, facts.measure ?? null);
726
+ }
685
727
  return;
686
728
  }
687
729
  if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
@@ -701,8 +743,20 @@ function createInstallerRun(value, options = {}) {
701
743
  durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
702
744
  if (facts.detail) run.relay(facts.detail);
703
745
  },
704
- milestone({ step, state, ms }) {
746
+ milestone({ step, state, ms, measure }) {
705
747
  start();
748
+ if (state === "running") {
749
+ if (chunkOpen) {
750
+ deferredActivity = { title: step, measure: measure ?? null };
751
+ return;
752
+ }
753
+ if (activeTitle === step) spinner.say(step, measure ?? null);
754
+ else {
755
+ activeTitle = step;
756
+ spinner.start(step, measure ?? null);
757
+ }
758
+ return;
759
+ }
706
760
  durable(step, ms === void 0 ? null : ms / 1e3, state);
707
761
  },
708
762
  signIn({ url, code }) {
@@ -716,7 +770,8 @@ function createInstallerRun(value, options = {}) {
716
770
  },
717
771
  // Only pass safe diagnostic text, never authentication output or credentials.
718
772
  relay(text, channel = "stdout", record = true) {
719
- spinner.stop();
773
+ closeChunk();
774
+ spinner.pause();
720
775
  const rendered = tty ? face.relay(text) : stripColor(text);
721
776
  for (const row of rendered.split("\n")) if (row) {
722
777
  emit(`${row}
@@ -724,6 +779,37 @@ function createInstallerRun(value, options = {}) {
724
779
  ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
725
780
  `);
726
781
  }
782
+ spinner.resume();
783
+ },
784
+ relayChunk(text, channel = "stdout") {
785
+ spinner.pause();
786
+ if (chunkOpen && chunkChannel !== channel) closeChunk();
787
+ chunkChannel = channel;
788
+ if (trailingCR.delete(channel) && text.startsWith("\n")) text = text.slice(1);
789
+ if (text.endsWith("\r")) trailingCR.add(channel);
790
+ const parts = text.replace(/\r\n|\r/g, "\n").split("\n");
791
+ for (let i = 0; i < parts.length; i++) {
792
+ const characters = [...stripColor(parts[i])];
793
+ while (characters.length) {
794
+ if (tty && chunkColumn >= face.width - 6) closeChunk();
795
+ const part = characters.splice(0, tty ? face.width - 6 - chunkColumn : characters.length).join("");
796
+ emit(tty && !chunkOpen ? face.relay(part) : part, channel, chunkOpen ? "" : tty ? face.relay("[external output omitted]") : "[external output omitted]");
797
+ chunkColumn += [...part].length;
798
+ chunkOpen = true;
799
+ }
800
+ if (i < parts.length - 1) {
801
+ emit("\n", channel);
802
+ chunkOpen = false;
803
+ chunkColumn = 0;
804
+ }
805
+ }
806
+ if (!chunkOpen) {
807
+ if (deferredActivity) {
808
+ activeTitle = deferredActivity.title;
809
+ spinner.start(deferredActivity.title, deferredActivity.measure);
810
+ deferredActivity = void 0;
811
+ } else spinner.resume();
812
+ }
727
813
  },
728
814
  cancel() {
729
815
  if (finished) return;
@@ -732,6 +818,8 @@ function createInstallerRun(value, options = {}) {
732
818
  { total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
733
819
  );
734
820
  start();
821
+ closeChunk();
822
+ activeTitle = void 0;
735
823
  spinner.stop();
736
824
  finished = true;
737
825
  if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
@@ -744,6 +832,8 @@ function createInstallerRun(value, options = {}) {
744
832
  validateInstallerOutcome(facts);
745
833
  if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
746
834
  start();
835
+ closeChunk();
836
+ activeTitle = void 0;
747
837
  spinner.stop();
748
838
  finished = true;
749
839
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
@@ -764,6 +854,8 @@ function createInstallerRun(value, options = {}) {
764
854
  if (tty) lines([face.signOff()]);
765
855
  },
766
856
  stop() {
857
+ closeChunk();
858
+ activeTitle = void 0;
767
859
  spinner.stop();
768
860
  }
769
861
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "4.4.11",
3
+ "version": "4.4.12",
4
4
  "description": "Install and maintain the MMI CLI and every present host surface from one release-gated Hub command.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",