@mutmutco/installer-face 0.4.1 → 0.4.2
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/CHANGELOG.md +6 -0
- package/README.md +10 -2
- package/dist/index.js +64 -7
- package/dist/payload.d.ts +8 -1
- package/dist/run.d.ts +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# 0.4.2
|
|
2
|
+
|
|
3
|
+
- Preserve rollback and log facts, including retry guidance for early failures.
|
|
4
|
+
- Isolate injected output observers while keeping terminal and filesystem failures visible.
|
|
5
|
+
- Generate embedded-runtime convergence from a declaration without global npm installation.
|
|
6
|
+
|
|
1
7
|
# 0.4.1
|
|
2
8
|
|
|
3
9
|
- Capture bootstrap output and validate recorded installer transcripts.
|
package/README.md
CHANGED
|
@@ -27,12 +27,17 @@ URL, canonical doctor command, and a nonempty unique surface list
|
|
|
27
27
|
`createInstallerRun` is the lower-level owner used by the shared launcher, not a reason for each
|
|
28
28
|
product to author another installer. `renderPayloadEntry` generates the npm-install/converge entry
|
|
29
29
|
from tarball names and a declared convergence command. Product release scripts supply those facts.
|
|
30
|
+
For an embedded-runtime product, supply `installedRuntime: { entry, package, installRootEnv }`;
|
|
31
|
+
`converge` then contains entry arguments rather than a binary name. The shared entry resolves the
|
|
32
|
+
installed product and re-enters it with `--payload` and `--installed`, without invoking npm.
|
|
30
33
|
|
|
31
34
|
The package owns greeting, phase names, spinner, surface status, receipt and sign-off.
|
|
32
35
|
`phase` accepts preflight, resolve, download, verify, install, activate, doctor, sign-in, check,
|
|
33
|
-
arm
|
|
36
|
+
arm, verify-release and rollback. Its facts are state (running/ok/fail/note), seconds, measure and safe detail.
|
|
34
37
|
`surface` accepts updated/current/failed/skipped/retry/pending/kept. `finish` accepts version,
|
|
35
|
-
total, updated, failed and optional retry, dryRun, installed, deferred and safe detail.
|
|
38
|
+
total, updated, failed and optional retry, dryRun, installed, deferred, operationFailed and safe detail.
|
|
39
|
+
Failure facts can include rollback (`completed`, `partial`, `not-needed`, `unknown`), logPath,
|
|
40
|
+
or logState (`unavailable`, `omitted`). Early operation failures retain retry guidance even with zero surfaces.
|
|
36
41
|
Missing version or deferred work never reports ready. Counts must describe the same operation set.
|
|
37
42
|
Pass `dryRun: true` in options so individual rows also say “would update”.
|
|
38
43
|
|
|
@@ -46,6 +51,9 @@ redacted in transcripts. `relay(text, channel)` and `milestone({step,state,ms})`
|
|
|
46
51
|
child diagnostics: never pass authentication responses, credentials or raw provider errors.
|
|
47
52
|
|
|
48
53
|
TTY, color, width, environment, animation and output sink can be injected through options.
|
|
54
|
+
Omit `write` for production terminal output and worker animation during synchronous work.
|
|
55
|
+
An injected `write` observer uses timer animation; its exceptions are retained in `run.errors`.
|
|
56
|
+
Default terminal writes and transcript/outcome filesystem failures remain operation errors.
|
|
49
57
|
Non-TTY output is plain append-only text. NO_COLOR disables color, not the visual structure.
|
|
50
58
|
Nested processes suppress greetings and send their final outcome to the console owner.
|
|
51
59
|
The private outcome channel works through Windows command shims as well as direct child processes;
|
package/dist/index.js
CHANGED
|
@@ -794,7 +794,7 @@ import { appendFileSync as appendFileSync2 } from "node:fs";
|
|
|
794
794
|
// src/outcome.ts
|
|
795
795
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
796
796
|
var counts = ["total", "updated", "failed"];
|
|
797
|
-
var strings = ["version", "retry", "detail"];
|
|
797
|
+
var strings = ["version", "retry", "detail", "logPath"];
|
|
798
798
|
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
799
799
|
function validateInstallerOutcome(value) {
|
|
800
800
|
const invalid = () => {
|
|
@@ -802,12 +802,15 @@ function validateInstallerOutcome(value) {
|
|
|
802
802
|
};
|
|
803
803
|
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
|
|
804
804
|
const facts = value;
|
|
805
|
-
const allowed = [...counts, ...strings, ...flags];
|
|
805
|
+
const allowed = [...counts, ...strings, ...flags, "rollback", "logState"];
|
|
806
806
|
if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
|
|
807
807
|
for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
|
|
808
808
|
if (facts.updated + facts.failed > facts.total) return invalid();
|
|
809
809
|
for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
|
|
810
810
|
for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
|
|
811
|
+
if (facts.rollback !== void 0 && !["completed", "partial", "not-needed", "unknown"].includes(facts.rollback)) return invalid();
|
|
812
|
+
if (facts.logState !== void 0 && !["unavailable", "omitted"].includes(facts.logState)) return invalid();
|
|
813
|
+
if (facts.logPath !== void 0 && (!facts.logPath.trim() || /[\x00-\x1f\x7f]/u.test(facts.logPath))) return invalid();
|
|
811
814
|
return { ...facts };
|
|
812
815
|
}
|
|
813
816
|
function writeInstallerOutcome(path, value) {
|
|
@@ -880,7 +883,8 @@ var PHASES = {
|
|
|
880
883
|
verify: ["Verifying the payload", "Verified the payload"],
|
|
881
884
|
install: ["Installing the product", "Installed the product"],
|
|
882
885
|
activate: ["Activating surfaces", "Activated surfaces"],
|
|
883
|
-
doctor: ["Checking health", "Checked health"]
|
|
886
|
+
doctor: ["Checking health", "Checked health"],
|
|
887
|
+
rollback: ["Restoring the previous version", "Restored the previous version"]
|
|
884
888
|
};
|
|
885
889
|
function createInstallerRun(value, options = {}) {
|
|
886
890
|
const declaration = validateInstallerProduct(value);
|
|
@@ -893,9 +897,16 @@ function createInstallerRun(value, options = {}) {
|
|
|
893
897
|
env,
|
|
894
898
|
color: tty && options.color !== false && env.NO_COLOR === void 0
|
|
895
899
|
});
|
|
896
|
-
const
|
|
900
|
+
const errors = [];
|
|
901
|
+
const write = options.write ? (text, channel) => {
|
|
902
|
+
try {
|
|
903
|
+
options.write(text, channel);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
errors.push(`installer output observer: ${error instanceof Error ? error.message : String(error)}`);
|
|
906
|
+
}
|
|
907
|
+
} : (text, channel) => {
|
|
897
908
|
(channel === "stdout" ? process.stdout : process.stderr).write(text);
|
|
898
|
-
}
|
|
909
|
+
};
|
|
899
910
|
const emit = (text, channel = "stdout", recorded = text) => {
|
|
900
911
|
if (!text) return;
|
|
901
912
|
write(text, channel);
|
|
@@ -930,6 +941,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
930
941
|
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
931
942
|
};
|
|
932
943
|
const run = {
|
|
944
|
+
get errors() {
|
|
945
|
+
return [...errors];
|
|
946
|
+
},
|
|
933
947
|
start,
|
|
934
948
|
phase(id, facts = {}) {
|
|
935
949
|
if (finished) throw new Error("installer run already finished");
|
|
@@ -995,7 +1009,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
995
1009
|
changed,
|
|
996
1010
|
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
997
1011
|
...facts.detail ? [facts.detail] : [],
|
|
998
|
-
facts.
|
|
1012
|
+
...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
|
|
1013
|
+
...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
|
|
1014
|
+
facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
|
|
999
1015
|
];
|
|
1000
1016
|
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
|
|
1001
1017
|
lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
|
|
@@ -1021,7 +1037,7 @@ async function runInstaller(value, operation, options) {
|
|
|
1021
1037
|
}
|
|
1022
1038
|
|
|
1023
1039
|
// src/payload.ts
|
|
1024
|
-
function renderPayloadEntry({ tarballs, converge, shippedFlag }) {
|
|
1040
|
+
function renderPayloadEntry({ tarballs, converge, shippedFlag, installedRuntime }) {
|
|
1025
1041
|
if (!Array.isArray(tarballs) || tarballs.length === 0 || tarballs.some((name) => typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$/u.test(name))) {
|
|
1026
1042
|
throw new Error("installer payload: tarballs must be relative .tgz basenames");
|
|
1027
1043
|
}
|
|
@@ -1031,6 +1047,47 @@ function renderPayloadEntry({ tarballs, converge, shippedFlag }) {
|
|
|
1031
1047
|
if (shippedFlag !== void 0 && !/^--[a-z][a-z0-9-]*$/u.test(shippedFlag)) {
|
|
1032
1048
|
throw new Error("installer payload: invalid shipped flag");
|
|
1033
1049
|
}
|
|
1050
|
+
if (installedRuntime !== void 0) {
|
|
1051
|
+
if (!installedRuntime || typeof installedRuntime.entry !== "string" || typeof installedRuntime.package !== "string" || !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/u.test(installedRuntime.entry) || installedRuntime.entry.split("/").some((part) => part === "." || part === "..") || !/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/u.test(installedRuntime.package) || installedRuntime.package.split("/").some((part) => part === "." || part === "..") || !Array.isArray(installedRuntime.installRootEnv) || installedRuntime.installRootEnv.some((name) => !/^[A-Z][A-Z0-9_]*$/u.test(name))) {
|
|
1052
|
+
throw new Error("installer payload: invalid installed runtime");
|
|
1053
|
+
}
|
|
1054
|
+
if (shippedFlag) throw new Error("installer payload: installed runtime reads its own payload manifest");
|
|
1055
|
+
return String.raw`// Generated by @mutmutco/installer-face. Uses the installed product, never a registry.
|
|
1056
|
+
import { spawnSync } from 'node:child_process';
|
|
1057
|
+
import { statSync } from 'node:fs';
|
|
1058
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
1059
|
+
|
|
1060
|
+
const runtime = ${JSON.stringify(installedRuntime)};
|
|
1061
|
+
const argv = process.argv.slice(2);
|
|
1062
|
+
const at = argv.indexOf('--payload');
|
|
1063
|
+
const payloadDir = resolve(at >= 0 && argv[at + 1] ? argv[at + 1] : dirname(process.argv[1]));
|
|
1064
|
+
const productDir = dirname(payloadDir);
|
|
1065
|
+
const packaged = join('node_modules', runtime.package);
|
|
1066
|
+
const candidates = [
|
|
1067
|
+
...runtime.installRootEnv.map((name) => process.env[name]),
|
|
1068
|
+
productDir, join(productDir, 'app'), join(productDir, 'current'),
|
|
1069
|
+
join(productDir, packaged), payloadDir, join(payloadDir, packaged),
|
|
1070
|
+
];
|
|
1071
|
+
const root = candidates.find((candidate) => {
|
|
1072
|
+
if (!candidate) return false;
|
|
1073
|
+
try { return statSync(join(candidate, runtime.entry)).isFile(); }
|
|
1074
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false; throw error; }
|
|
1075
|
+
});
|
|
1076
|
+
if (!root) {
|
|
1077
|
+
process.stderr.write('Installer payload has no installed product to converge: ' + payloadDir + '\n');
|
|
1078
|
+
process.exitCode = 1;
|
|
1079
|
+
} else {
|
|
1080
|
+
const viaLauncher = !/^node(\.exe)?$/iu.test(basename(process.execPath));
|
|
1081
|
+
const args = [...(viaLauncher ? ['--run'] : []), join(root, runtime.entry),
|
|
1082
|
+
...${JSON.stringify(converge)}, '--payload', payloadDir, '--installed', root];
|
|
1083
|
+
const child = spawnSync(process.execPath, args, { cwd: root, stdio: 'inherit', windowsHide: true });
|
|
1084
|
+
if (child.error) {
|
|
1085
|
+
process.stderr.write('Installer could not re-enter the installed product: ' + child.error.message + '\n');
|
|
1086
|
+
process.exitCode = 1;
|
|
1087
|
+
} else process.exitCode = child.status === null ? 1 : child.status;
|
|
1088
|
+
}
|
|
1089
|
+
`;
|
|
1090
|
+
}
|
|
1034
1091
|
return String.raw`// Generated by @mutmutco/installer-face. Product code declares operations, never this program.
|
|
1035
1092
|
import { spawnSync } from 'node:child_process';
|
|
1036
1093
|
import { readFileSync } from 'node:fs';
|
package/dist/payload.d.ts
CHANGED
|
@@ -3,5 +3,12 @@ export interface PayloadEntryOptions {
|
|
|
3
3
|
tarballs: string[];
|
|
4
4
|
converge: string[];
|
|
5
5
|
shippedFlag?: string;
|
|
6
|
+
/** Converge an already installed product through this launcher's embedded runtime.
|
|
7
|
+
* In this mode converge contains the entry's arguments, without a binary name. */
|
|
8
|
+
installedRuntime?: {
|
|
9
|
+
entry: string;
|
|
10
|
+
package: string;
|
|
11
|
+
installRootEnv: string[];
|
|
12
|
+
};
|
|
6
13
|
}
|
|
7
|
-
export declare function renderPayloadEntry({ tarballs, converge, shippedFlag }: PayloadEntryOptions): string;
|
|
14
|
+
export declare function renderPayloadEntry({ tarballs, converge, shippedFlag, installedRuntime }: PayloadEntryOptions): string;
|
package/dist/run.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ declare const PHASES: {
|
|
|
24
24
|
readonly install: readonly ["Installing the product", "Installed the product"];
|
|
25
25
|
readonly activate: readonly ["Activating surfaces", "Activated surfaces"];
|
|
26
26
|
readonly doctor: readonly ["Checking health", "Checked health"];
|
|
27
|
+
readonly rollback: readonly ["Restoring the previous version", "Restored the previous version"];
|
|
27
28
|
};
|
|
28
29
|
export type InstallerPhase = keyof typeof PHASES;
|
|
29
30
|
export type InstallerChannel = 'stdout' | 'stderr' | 'spinner';
|
|
@@ -35,6 +36,8 @@ export interface InstallerRunOptions {
|
|
|
35
36
|
columns?: number;
|
|
36
37
|
env?: Readonly<NodeJS.ProcessEnv>;
|
|
37
38
|
animate?: boolean;
|
|
39
|
+
/** Optional output observer. Exceptions are recorded in run.errors, never operation failures.
|
|
40
|
+
* Omit for production terminal output: the default worker animates during synchronous work. */
|
|
38
41
|
write?: (text: string, channel: InstallerChannel) => void;
|
|
39
42
|
}
|
|
40
43
|
export interface InstallerFinish {
|
|
@@ -48,6 +51,9 @@ export interface InstallerFinish {
|
|
|
48
51
|
detail?: string;
|
|
49
52
|
deferred?: boolean;
|
|
50
53
|
operationFailed?: boolean;
|
|
54
|
+
rollback?: 'completed' | 'partial' | 'not-needed' | 'unknown';
|
|
55
|
+
logPath?: string;
|
|
56
|
+
logState?: 'unavailable' | 'omitted';
|
|
51
57
|
}
|
|
52
58
|
export interface InstallerPhaseFacts {
|
|
53
59
|
state?: 'running' | StepKind;
|
|
@@ -65,6 +71,7 @@ export interface InstallerSurfaceFacts {
|
|
|
65
71
|
}
|
|
66
72
|
/** One owner for the entire run. Callers supply measured facts, never terminal prose. */
|
|
67
73
|
export declare function createInstallerRun(value: unknown, options?: InstallerRunOptions): {
|
|
74
|
+
readonly errors: readonly string[];
|
|
68
75
|
start: () => void;
|
|
69
76
|
phase(id: InstallerPhase, facts?: InstallerPhaseFacts): void;
|
|
70
77
|
surface(facts: InstallerSurfaceFacts): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/installer-face",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|