@mutmutco/installer-face 0.2.0 → 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 +17 -0
- package/dist/face.d.ts +6 -2
- package/dist/index.js +35 -17
- package/package.json +1 -1
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
|
|
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.
|
|
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,21 +118,21 @@ 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
|
-
|
|
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));
|
|
120
125
|
const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
|
|
126
|
+
const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
|
|
121
127
|
return [
|
|
122
128
|
frame(GLYPH.boxTop, GLYPH.boxTopEnd),
|
|
123
|
-
...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${line}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
|
|
129
|
+
...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
|
|
124
130
|
frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
|
|
125
131
|
];
|
|
126
132
|
};
|
|
127
133
|
const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
128
134
|
const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
|
|
129
|
-
return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
|
|
135
|
+
return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
// src/shell.ts
|
|
@@ -199,10 +205,15 @@ face_rule() {
|
|
|
199
205
|
}
|
|
200
206
|
|
|
201
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.
|
|
202
213
|
face_receipt() {
|
|
203
214
|
face_text1="$1"
|
|
204
|
-
face_text2="
|
|
205
|
-
face_text3="
|
|
215
|
+
face_text2="Installed into $3"
|
|
216
|
+
face_text3="Check health any time: $2"
|
|
206
217
|
face_w1=$(( \${#face_text1} + 3 ))
|
|
207
218
|
face_w=$face_w1
|
|
208
219
|
[ \${#face_text2} -gt "$face_w" ] && face_w=\${#face_text2}
|
|
@@ -322,8 +333,8 @@ function Write-FaceRelay {
|
|
|
322
333
|
function Write-FaceReceipt {
|
|
323
334
|
param([string] $Location, [string] $Next)
|
|
324
335
|
$first = [string] $FaceCheck + ' ' + $FaceName + ' is ready.'
|
|
325
|
-
$second = '
|
|
326
|
-
$third = '
|
|
336
|
+
$second = 'Installed into ' + $Location
|
|
337
|
+
$third = 'Check health any time: ' + $Next
|
|
327
338
|
$inner = ($first.Length, $second.Length, $third.Length | Measure-Object -Maximum).Maximum
|
|
328
339
|
$rule = [string]::new([char] 0x2500, $inner + 4)
|
|
329
340
|
Write-Host (Write-Paint $FaceAccent ([string] [char] 0x256D + $rule + [char] 0x256E))
|
|
@@ -391,8 +402,8 @@ function renderRows(face, rows) {
|
|
|
391
402
|
const body = `${clip(subject, column).padEnd(column)} ${row.mark ? `${markGlyph(face, row.mark)} ` : ""}${tail}`;
|
|
392
403
|
lines.push(...face.relay(body).split("\n"));
|
|
393
404
|
if (row.note) {
|
|
394
|
-
for (const part of wrapWords(row.note, Math.max(16, face.width -
|
|
395
|
-
lines.push(...face.relay(
|
|
405
|
+
for (const part of wrapWords(row.note, Math.max(16, face.width - TITLE_COLUMN))) {
|
|
406
|
+
lines.push(...face.relay(part).split("\n"));
|
|
396
407
|
}
|
|
397
408
|
}
|
|
398
409
|
}
|
|
@@ -418,7 +429,7 @@ function createSpinner(face, { animate, stream = process.stderr, intervalMs = 90
|
|
|
418
429
|
};
|
|
419
430
|
const draw = () => {
|
|
420
431
|
if (!animate) return;
|
|
421
|
-
const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.
|
|
432
|
+
const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.dot, FRAMES[frame % FRAMES.length]);
|
|
422
433
|
stream.write(`\r\x1B[2K${line}`);
|
|
423
434
|
frame += 1;
|
|
424
435
|
};
|
|
@@ -461,6 +472,9 @@ function assertFaceConformance(lines, options) {
|
|
|
461
472
|
const width = options.columns ?? 100;
|
|
462
473
|
const rendered = lines.flatMap((line) => String(line).split("\n"));
|
|
463
474
|
const plain = rendered.map(stripColor);
|
|
475
|
+
const head = plain[0] ?? "";
|
|
476
|
+
const signOffLine = `${GLYPH.diamond} ${identity.name} \xB7 Mutatis Mutandis`;
|
|
477
|
+
const opensWithWelcome = head.startsWith(GLYPH.diamond) && head !== signOffLine;
|
|
464
478
|
if (rendered.length === 0) fail("R7 render before you ship", "no rendered lines were given to the guard");
|
|
465
479
|
if (options.tty === false) {
|
|
466
480
|
for (const line of plain) {
|
|
@@ -472,6 +486,11 @@ function assertFaceConformance(lines, options) {
|
|
|
472
486
|
}
|
|
473
487
|
return;
|
|
474
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
|
+
}
|
|
475
494
|
for (const line of plain) {
|
|
476
495
|
if (/[\u25c6\u25c7\u2714\u2716\u25cf] (?! )/u.test(line)) {
|
|
477
496
|
fail("R4 two spaces after a glyph", `a single space follows the glyph in ${JSON.stringify(line)}`);
|
|
@@ -494,7 +513,7 @@ function assertFaceConformance(lines, options) {
|
|
|
494
513
|
if (widths.size !== 1) fail("R5 receipt frame", `the box is ragged: widths ${[...widths].join(", ")}`);
|
|
495
514
|
}
|
|
496
515
|
const inReceipt = (index) => top !== -1 && index >= top && index <= plain.findIndex((line, at) => at > top && line.startsWith(GLYPH.boxBottom));
|
|
497
|
-
const welcomeEnd =
|
|
516
|
+
const welcomeEnd = opensWithWelcome ? 4 : 0;
|
|
498
517
|
for (const [index, line] of plain.entries()) {
|
|
499
518
|
if (index < welcomeEnd || inReceipt(index)) continue;
|
|
500
519
|
if (!line.startsWith(GLYPH.bar) || line.trim() === GLYPH.bar) continue;
|
|
@@ -522,8 +541,7 @@ function assertFaceConformance(lines, options) {
|
|
|
522
541
|
fail("width cap", `a line is ${visibleWidth(line)} columns wide, cap ${width}: ${JSON.stringify(line)}`);
|
|
523
542
|
}
|
|
524
543
|
}
|
|
525
|
-
|
|
526
|
-
if (head.startsWith(GLYPH.diamond)) {
|
|
544
|
+
if (opensWithWelcome) {
|
|
527
545
|
const expected = `${GLYPH.diamond} ${identity.name} \u2014 Mutatis Mutandis`;
|
|
528
546
|
if (head !== expected) fail("welcome is the product table's", `expected ${JSON.stringify(expected)}, got ${JSON.stringify(head)}`);
|
|
529
547
|
const warm = plain.slice(1, 4).find((line) => line.includes(identity.warm));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/installer-face",
|
|
3
|
-
"version": "0.2.
|
|
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",
|