@mutmutco/installer-face 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -36,6 +36,23 @@ console.log(face.signOff());
36
36
  Colour is a DECISION the caller passes in, never sniffed here: the same face is used by processes
37
37
  whose stdout is a pipe, and a face drawn into a pipe is the bug this package exists to prevent.
38
38
 
39
+ ### Self re-exec continuation
40
+
41
+ A process that re-execs itself onto an inherited console sets `MM_FACE_CONTINUES` to the
42
+ comma-separated phase names already drawn. `createFace` reads it directly: `welcome()` returns no
43
+ lines, and each `face.continues(phase, kind)` consumes one inherited phase. A failure always returns
44
+ false and consumes the inherited phase, so neither it nor a later retry can be hidden.
45
+
46
+ ```ts
47
+ if (!face.continues('preflight', result.ok ? 'ok' : 'fail')) {
48
+ console.log(face.step('preflight', result.seconds, result.ok ? 'ok' : 'fail'));
49
+ }
50
+ ```
51
+
52
+ The optional `env` input exists for tests; normal callers use `process.env`. Missing or empty means a
53
+ fresh face. Only an inheriting spawn sets the variable — never a person starting a wrapped command
54
+ on a fresh console.
55
+
39
56
  ### Served one-liners
40
57
 
41
58
  A `curl … | sh` or `irm … | iex` script cannot import npm, so the package emits the block instead:
package/dist/face.d.ts CHANGED
@@ -41,8 +41,10 @@ export type StepKind = 'ok' | 'fail' | 'note';
41
41
  export interface Face {
42
42
  readonly identity: ProductIdentity;
43
43
  readonly width: number;
44
- /** `◆ <product> — Mutatis Mutandis` and the product's own warm line. */
44
+ /** `◆ <product> — Mutatis Mutandis` and the product's own warm line; empty on a continuation. */
45
45
  welcome(): string[];
46
+ /** Consume one phase inherited through MM_FACE_CONTINUES; failures are always new information. */
47
+ continues(phase: string, kind?: StepKind): boolean;
46
48
  /** One durable line for one finished step: glyph, title, and a measured time when truthful. */
47
49
  step(title: string, seconds?: number | null, kind?: StepKind): string;
48
50
  /** Another program's output, kept inside the face at the step-title column. */
@@ -60,5 +62,7 @@ export interface FaceOptions {
60
62
  /** False emits no escape codes at all, so a piped or logged run is byte-stable plain text. */
61
63
  color?: boolean;
62
64
  columns?: number;
65
+ /** Injectable only so tests do not mutate process.env; normal callers inherit process.env. */
66
+ env?: Readonly<NodeJS.ProcessEnv>;
63
67
  }
64
- export declare function createFace({ product, color, columns }: FaceOptions): Face;
68
+ export declare function createFace({ product, color, columns, env }: FaceOptions): Face;
package/dist/index.js CHANGED
@@ -85,20 +85,26 @@ function wrapWords(text, width) {
85
85
  if (line) lines.push(line);
86
86
  return lines.length ? lines : [""];
87
87
  }
88
- function createFace({ product, color = false, columns }) {
88
+ function createFace({ product, color = false, columns, env = process.env }) {
89
89
  const identity = identityFor(product);
90
90
  const width = faceWidth(columns);
91
91
  const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
92
92
  const bar = () => paint(PALETTE.muted, GLYPH.bar);
93
93
  const indent = " ".repeat(TITLE_COLUMN - 1);
94
- const welcome = () => [
94
+ const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
95
+ const continuesFace = continuedPhases.size > 0;
96
+ const welcome = () => continuesFace ? [] : [
95
97
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
96
98
  bar(),
97
99
  `${bar()} ${identity.warm}`,
98
100
  bar()
99
101
  ];
102
+ const continues = (phase, kind = "ok") => {
103
+ const inherited = continuedPhases.delete(String(phase).trim());
104
+ return kind === "fail" ? false : inherited;
105
+ };
100
106
  const step = (title, seconds = null, kind = "ok") => {
101
- const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.hollow) : paint(PALETTE.green, GLYPH.check);
107
+ const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
102
108
  const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
103
109
  const column = Math.min(44, Math.max(0, width - 8));
104
110
  const reserved = time ? 6 : 0;
@@ -112,8 +118,7 @@ function createFace({ product, color = false, columns }) {
112
118
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
113
119
  const line = String(raw);
114
120
  if (visibleWidth(line) <= width - 6) return [line];
115
- const lead = /^\s*/u.exec(line)?.[0] ?? "";
116
- return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
121
+ return wrapWords(line, width - 6);
117
122
  });
118
123
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
119
124
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
@@ -127,7 +132,7 @@ function createFace({ product, color = false, columns }) {
127
132
  };
128
133
  const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
129
134
  const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
130
- return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
135
+ return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
131
136
  }
132
137
 
133
138
  // src/shell.ts
@@ -200,10 +205,15 @@ face_rule() {
200
205
  }
201
206
 
202
207
  # face_receipt <headline> <next command> <location> \u2014 the closing box.
208
+ #
209
+ # The PACKAGE owns the box's internal left edge (#6859): body rows sit at the column right after the
210
+ # frame bar's two spaces \u2014 the SAME column the headline's check occupies \u2014 so the box reads as one
211
+ # block. No body string carries its own indent; the four spaces that used to sit here were the empty
212
+ # glyph column the owner read as stray indentation.
203
213
  face_receipt() {
204
214
  face_text1="$1"
205
- face_text2=" Installed into $3"
206
- face_text3=" Check health any time: $2"
215
+ face_text2="Installed into $3"
216
+ face_text3="Check health any time: $2"
207
217
  face_w1=$(( \${#face_text1} + 3 ))
208
218
  face_w=$face_w1
209
219
  [ \${#face_text2} -gt "$face_w" ] && face_w=\${#face_text2}
@@ -323,8 +333,8 @@ function Write-FaceRelay {
323
333
  function Write-FaceReceipt {
324
334
  param([string] $Location, [string] $Next)
325
335
  $first = [string] $FaceCheck + ' ' + $FaceName + ' is ready.'
326
- $second = ' Installed into ' + $Location
327
- $third = ' Check health any time: ' + $Next
336
+ $second = 'Installed into ' + $Location
337
+ $third = 'Check health any time: ' + $Next
328
338
  $inner = ($first.Length, $second.Length, $third.Length | Measure-Object -Maximum).Maximum
329
339
  $rule = [string]::new([char] 0x2500, $inner + 4)
330
340
  Write-Host (Write-Paint $FaceAccent ([string] [char] 0x256D + $rule + [char] 0x256E))
@@ -419,7 +429,7 @@ function createSpinner(face, { animate, stream = process.stderr, intervalMs = 90
419
429
  };
420
430
  const draw = () => {
421
431
  if (!animate) return;
422
- const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.hollow, FRAMES[frame % FRAMES.length]);
432
+ const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.dot, FRAMES[frame % FRAMES.length]);
423
433
  stream.write(`\r\x1B[2K${line}`);
424
434
  frame += 1;
425
435
  };
@@ -476,6 +486,11 @@ function assertFaceConformance(lines, options) {
476
486
  }
477
487
  return;
478
488
  }
489
+ for (const line of plain) {
490
+ if (line.startsWith(`${GLYPH.bar} ${GLYPH.hollow} `)) {
491
+ fail("note step uses U+25CF", `U+25C7 is the note-box opener in ${JSON.stringify(line)}`);
492
+ }
493
+ }
479
494
  for (const line of plain) {
480
495
  if (/[\u25c6\u25c7\u2714\u2716\u25cf] (?! )/u.test(line)) {
481
496
  fail("R4 two spaces after a glyph", `a single space follows the glyph in ${JSON.stringify(line)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-face",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "The MM Terminal Line installer face: one renderer, the canonical product table, shell/PowerShell fragments for served one-liners, and the drift guard every surface runs.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",