@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 +17 -0
- package/dist/face.d.ts +6 -2
- package/dist/index.js +26 -11
- 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,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
|
-
|
|
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="
|
|
206
|
-
face_text3="
|
|
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 = '
|
|
327
|
-
$third = '
|
|
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.
|
|
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.
|
|
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",
|