@mutmutco/installer-face 0.4.1 → 0.4.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/CHANGELOG.md +14 -0
- package/README.md +29 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +248 -44
- package/dist/maintenance.d.ts +57 -15
- package/dist/payload.d.ts +8 -1
- package/dist/run.d.ts +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# 0.4.3
|
|
2
|
+
|
|
3
|
+
- Resolve freshly installed npm commands outside the inherited PATH.
|
|
4
|
+
- Preserve typed engine phases, safe authentication relay and product JSON through shared maintenance.
|
|
5
|
+
- Own quiet output, cancellation and failure diagnostics in the shared lifecycle.
|
|
6
|
+
- Preserve mid-run authentication handoff, measured progress timing and terminal routing.
|
|
7
|
+
- Use ASCII plain labels without changing Unicode paths, names or authorization URLs.
|
|
8
|
+
|
|
9
|
+
# 0.4.2
|
|
10
|
+
|
|
11
|
+
- Preserve rollback and log facts, including retry guidance for early failures.
|
|
12
|
+
- Isolate injected output observers while keeping terminal and filesystem failures visible.
|
|
13
|
+
- Generate embedded-runtime convergence from a declaration without global npm installation.
|
|
14
|
+
|
|
1
15
|
# 0.4.1
|
|
2
16
|
|
|
3
17
|
- Capture bootstrap output and validate recorded installer transcripts.
|
package/README.md
CHANGED
|
@@ -20,6 +20,15 @@ only shared code and a shared guard can.
|
|
|
20
20
|
Product install/update entrypoints call `runMaintenance(declaration, args, engines)`.
|
|
21
21
|
The package owns phase ordering, self-update handoff, scheduler completion, outcome reporting and
|
|
22
22
|
cleanup. Product engines return version, surface and failure facts; they do not draw a run.
|
|
23
|
+
Engines receive `onPhaseEvent({ id, state, detail?, measure?, measured? })` for phase facts.
|
|
24
|
+
Repeated running progress retains the original start time; the package measures elapsed time.
|
|
25
|
+
Typed `measured` facts carry percent, bytes, or a count with an optional total. Unknown totals stay unknown.
|
|
26
|
+
Each completed phase needs its own verdict: starting another phase does not imply success.
|
|
27
|
+
`relay(text, channel?, record?)` defaults to live-only output, keeping authorization URLs out of transcripts.
|
|
28
|
+
Optional `jsonResult(summary, schedule)` preserves product machine output while the shared owner serializes it.
|
|
29
|
+
`safeErrorDetail(error)` supplies an actionable, non-sensitive failure message; the default is generic.
|
|
30
|
+
Scheduling and self-handoff callbacks are optional, but a requested handoff without an implementation fails.
|
|
31
|
+
Return `cancelled: true` to end without readiness. The package also owns `--quiet` output selection.
|
|
23
32
|
The declaration contains the canonical product key (`mmi` and `jerv` aliases are accepted), gate
|
|
24
33
|
URL, canonical doctor command, and a nonempty unique surface list
|
|
25
34
|
`{ id, npm?, bin?, kind?, activation? }`.
|
|
@@ -27,26 +36,42 @@ URL, canonical doctor command, and a nonempty unique surface list
|
|
|
27
36
|
`createInstallerRun` is the lower-level owner used by the shared launcher, not a reason for each
|
|
28
37
|
product to author another installer. `renderPayloadEntry` generates the npm-install/converge entry
|
|
29
38
|
from tarball names and a declared convergence command. Product release scripts supply those facts.
|
|
39
|
+
The npm mode resolves that command from the global prefix after installation; it never relies on a refreshed PATH.
|
|
40
|
+
For an embedded-runtime product, supply `installedRuntime: { entry, package, installRootEnv }`;
|
|
41
|
+
`converge` then contains entry arguments rather than a binary name. The shared entry resolves the
|
|
42
|
+
installed product and re-enters it with `--payload` and `--installed`, without invoking npm.
|
|
30
43
|
|
|
31
44
|
The package owns greeting, phase names, spinner, surface status, receipt and sign-off.
|
|
32
45
|
`phase` accepts preflight, resolve, download, verify, install, activate, doctor, sign-in, check,
|
|
33
|
-
arm
|
|
46
|
+
arm, verify-release and rollback. Its facts are state (running/ok/fail/note), seconds, measure and safe detail.
|
|
34
47
|
`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.
|
|
48
|
+
total, updated, failed and optional retry, dryRun, installed, deferred, operationFailed and safe detail.
|
|
49
|
+
Failure facts can include rollback (`completed`, `partial`, `not-needed`, `unknown`), logPath,
|
|
50
|
+
or logState (`unavailable`, `omitted`). Early operation failures retain retry guidance even with zero surfaces.
|
|
36
51
|
Missing version or deferred work never reports ready. Counts must describe the same operation set.
|
|
37
52
|
Pass `dryRun: true` in options so individual rows also say “would update”.
|
|
38
53
|
|
|
39
54
|
`operation: "install"` selects the canonical first-install welcome.
|
|
40
55
|
`start()` and `finish()` are idempotent. `stop()` clears transient work. The orchestration wrapper
|
|
41
56
|
always stops the spinner, including when an operation throws; the caller retains error handling.
|
|
42
|
-
Use the
|
|
57
|
+
Use the shared maintenance handoff for self-reexec. For a mid-engine authentication retry,
|
|
58
|
+
`withCustody` pauses the shared output and supplies completed `faceContinues` phases to the product
|
|
59
|
+
operation. Return `{ kind: "resume", value }` to continue, or `{ kind: "reexec", code }` when the
|
|
60
|
+
child owns the final result. The shared owner resumes timing or transfers receipt ownership.
|
|
43
61
|
|
|
44
62
|
`signIn({url, code})` owns device-login instructions. The code is visible in the terminal and
|
|
45
63
|
redacted in transcripts. `relay(text, channel)` and `milestone({step,state,ms})` accept only safe
|
|
46
64
|
child diagnostics: never pass authentication responses, credentials or raw provider errors.
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
Maintenance callers can declare `render: { tty, color, columns, env, animate }` without supplying
|
|
67
|
+
a writer or constructing a run. `--no-color`, `NO_COLOR`, and `TERM=dumb` disable color.
|
|
68
|
+
The lower-level owner also accepts an output observer for tests.
|
|
69
|
+
Omit `write` for production terminal output and worker animation during synchronous work.
|
|
70
|
+
An injected `write` observer uses timer animation; its exceptions are retained in `run.errors`.
|
|
71
|
+
Default terminal writes and transcript/outcome filesystem failures remain operation errors.
|
|
49
72
|
Non-TTY output is plain append-only text. NO_COLOR disables color, not the visual structure.
|
|
73
|
+
The shared plain lane uses ASCII labels and separators while preserving dynamic Unicode data.
|
|
74
|
+
Do not transliterate paths or authorization URLs through a consumer output callback.
|
|
50
75
|
Nested processes suppress greetings and send their final outcome to the console owner.
|
|
51
76
|
The private outcome channel works through Windows command shims as well as direct child processes;
|
|
52
77
|
a successful process exit never turns a deferred result into readiness.
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { InstallerProduct, InstallerRun, InstallerRunOptions, InstallerPhas
|
|
|
14
14
|
export { renderPayloadEntry } from './payload.js';
|
|
15
15
|
export type { PayloadEntryOptions } from './payload.js';
|
|
16
16
|
export { runMaintenance } from './maintenance.js';
|
|
17
|
-
export type { MaintenanceArm, MaintenanceSummary } from './maintenance.js';
|
|
17
|
+
export type { MaintenanceArm, MaintenanceSummary, MaintenancePhaseEvent, MaintenanceCustodyResult, MaintenanceEngineOptions, MaintenanceSchedule } from './maintenance.js';
|
|
18
18
|
export { validateInstallerOutcome, readInstallerOutcome, writeInstallerOutcome } from './outcome.js';
|
|
19
19
|
export type { InstallerOutcome } from './outcome.js';
|
|
20
20
|
export { assertInstallerTranscript } from './transcript.js';
|
package/dist/index.js
CHANGED
|
@@ -655,7 +655,7 @@ function assertFaceConformance(lines, options) {
|
|
|
655
655
|
}
|
|
656
656
|
}
|
|
657
657
|
if (rendered.length === 0) fail("R7 render before you ship", "no rendered lines were given to the guard");
|
|
658
|
-
const greetings = plain.filter((line) => line.includes(`${identity.name}
|
|
658
|
+
const greetings = plain.filter((line) => ["\u2014", "-"].some((separator) => line.includes(`${identity.name} ${separator} Mutatis Mutandis`)));
|
|
659
659
|
if (greetings.length > 1) fail("R8 one welcome per run", "duplicate product greeting");
|
|
660
660
|
if (plain.filter((line) => line.startsWith(GLYPH.boxTop)).length > 1) {
|
|
661
661
|
fail("R8 one receipt per run", "duplicate receipt");
|
|
@@ -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);
|
|
@@ -891,11 +895,18 @@ function createInstallerRun(value, options = {}) {
|
|
|
891
895
|
product: declaration.product,
|
|
892
896
|
columns: options.columns,
|
|
893
897
|
env,
|
|
894
|
-
color: tty && options.color !== false && env.NO_COLOR === void 0
|
|
898
|
+
color: tty && options.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb"
|
|
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);
|
|
@@ -920,9 +931,13 @@ function createInstallerRun(value, options = {}) {
|
|
|
920
931
|
const start = () => {
|
|
921
932
|
if (started || finished) return;
|
|
922
933
|
started = true;
|
|
934
|
+
if (options.quiet) return;
|
|
923
935
|
const welcome = face.welcome();
|
|
924
936
|
if (tty) lines(welcome);
|
|
925
|
-
else if (welcome.length)
|
|
937
|
+
else if (welcome.length) {
|
|
938
|
+
const warm = options.operation === "install" ? face.identity.installWarm : face.identity.warm;
|
|
939
|
+
lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
|
|
940
|
+
}
|
|
926
941
|
};
|
|
927
942
|
const durable = (title, measure, kind) => {
|
|
928
943
|
spinner.stop();
|
|
@@ -930,6 +945,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
930
945
|
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
931
946
|
};
|
|
932
947
|
const run = {
|
|
948
|
+
get errors() {
|
|
949
|
+
return [...errors];
|
|
950
|
+
},
|
|
933
951
|
start,
|
|
934
952
|
phase(id, facts = {}) {
|
|
935
953
|
if (finished) throw new Error("installer run already finished");
|
|
@@ -949,12 +967,13 @@ function createInstallerRun(value, options = {}) {
|
|
|
949
967
|
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
|
|
950
968
|
if (!surface) throw new Error("installer run: undeclared surface");
|
|
951
969
|
start();
|
|
952
|
-
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
|
|
970
|
+
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
|
|
953
971
|
const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
|
|
954
972
|
if (!status) throw new Error("installer run: unknown surface state");
|
|
955
|
-
const
|
|
973
|
+
const separator = tty ? "\xB7" : "-";
|
|
974
|
+
const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
|
|
956
975
|
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
957
|
-
durable(`${facts.id}${versions}
|
|
976
|
+
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
958
977
|
if (facts.detail) run.relay(facts.detail);
|
|
959
978
|
},
|
|
960
979
|
milestone({ step, state, ms }) {
|
|
@@ -981,6 +1000,20 @@ function createInstallerRun(value, options = {}) {
|
|
|
981
1000
|
`);
|
|
982
1001
|
}
|
|
983
1002
|
},
|
|
1003
|
+
cancel() {
|
|
1004
|
+
if (finished) return;
|
|
1005
|
+
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(
|
|
1006
|
+
env.MM_INSTALLER_OUTCOME_FILE,
|
|
1007
|
+
{ total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
|
|
1008
|
+
);
|
|
1009
|
+
start();
|
|
1010
|
+
spinner.stop();
|
|
1011
|
+
finished = true;
|
|
1012
|
+
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
|
|
1013
|
+
lines(tty ? face.receipt(["Operation cancelled."], { ready: false }) : ["Operation cancelled."]);
|
|
1014
|
+
}
|
|
1015
|
+
if (tty) lines([face.signOff()]);
|
|
1016
|
+
},
|
|
984
1017
|
finish(facts) {
|
|
985
1018
|
if (finished) return;
|
|
986
1019
|
validateInstallerOutcome(facts);
|
|
@@ -988,6 +1021,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
988
1021
|
start();
|
|
989
1022
|
spinner.stop();
|
|
990
1023
|
finished = true;
|
|
1024
|
+
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
991
1025
|
const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
|
|
992
1026
|
const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
|
|
993
1027
|
const body = [
|
|
@@ -995,7 +1029,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
995
1029
|
changed,
|
|
996
1030
|
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
997
1031
|
...facts.detail ? [facts.detail] : [],
|
|
998
|
-
facts.
|
|
1032
|
+
...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
|
|
1033
|
+
...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
|
|
1034
|
+
facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
|
|
999
1035
|
];
|
|
1000
1036
|
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
|
|
1001
1037
|
lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
|
|
@@ -1021,7 +1057,7 @@ async function runInstaller(value, operation, options) {
|
|
|
1021
1057
|
}
|
|
1022
1058
|
|
|
1023
1059
|
// src/payload.ts
|
|
1024
|
-
function renderPayloadEntry({ tarballs, converge, shippedFlag }) {
|
|
1060
|
+
function renderPayloadEntry({ tarballs, converge, shippedFlag, installedRuntime }) {
|
|
1025
1061
|
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
1062
|
throw new Error("installer payload: tarballs must be relative .tgz basenames");
|
|
1027
1063
|
}
|
|
@@ -1031,26 +1067,84 @@ function renderPayloadEntry({ tarballs, converge, shippedFlag }) {
|
|
|
1031
1067
|
if (shippedFlag !== void 0 && !/^--[a-z][a-z0-9-]*$/u.test(shippedFlag)) {
|
|
1032
1068
|
throw new Error("installer payload: invalid shipped flag");
|
|
1033
1069
|
}
|
|
1070
|
+
if (installedRuntime !== void 0) {
|
|
1071
|
+
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))) {
|
|
1072
|
+
throw new Error("installer payload: invalid installed runtime");
|
|
1073
|
+
}
|
|
1074
|
+
if (shippedFlag) throw new Error("installer payload: installed runtime reads its own payload manifest");
|
|
1075
|
+
return String.raw`// Generated by @mutmutco/installer-face. Uses the installed product, never a registry.
|
|
1076
|
+
import { spawnSync } from 'node:child_process';
|
|
1077
|
+
import { statSync } from 'node:fs';
|
|
1078
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
1079
|
+
|
|
1080
|
+
const runtime = ${JSON.stringify(installedRuntime)};
|
|
1081
|
+
const argv = process.argv.slice(2);
|
|
1082
|
+
const at = argv.indexOf('--payload');
|
|
1083
|
+
const payloadDir = resolve(at >= 0 && argv[at + 1] ? argv[at + 1] : dirname(process.argv[1]));
|
|
1084
|
+
const productDir = dirname(payloadDir);
|
|
1085
|
+
const packaged = join('node_modules', runtime.package);
|
|
1086
|
+
const candidates = [
|
|
1087
|
+
...runtime.installRootEnv.map((name) => process.env[name]),
|
|
1088
|
+
productDir, join(productDir, 'app'), join(productDir, 'current'),
|
|
1089
|
+
join(productDir, packaged), payloadDir, join(payloadDir, packaged),
|
|
1090
|
+
];
|
|
1091
|
+
const root = candidates.find((candidate) => {
|
|
1092
|
+
if (!candidate) return false;
|
|
1093
|
+
try { return statSync(join(candidate, runtime.entry)).isFile(); }
|
|
1094
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false; throw error; }
|
|
1095
|
+
});
|
|
1096
|
+
if (!root) {
|
|
1097
|
+
const archives = ${JSON.stringify(tarballs)}.map((name) => '"' + join(payloadDir, name) + '"').join(' ');
|
|
1098
|
+
const viaLauncher = !/^node(\.exe)?$/iu.test(basename(process.execPath));
|
|
1099
|
+
process.stderr.write([
|
|
1100
|
+
'Installer payload has no installed product to converge: ' + payloadDir,
|
|
1101
|
+
'NOT READY — nothing was armed and no host projection was written.',
|
|
1102
|
+
'Finish by installing the payload archive offline:',
|
|
1103
|
+
' npm install --global --offline ' + archives,
|
|
1104
|
+
'Then retry: "' + process.execPath + '" ' + (viaLauncher ? '--run ' : '') + '"' + process.argv[1] + '" --payload "' + payloadDir + '"',
|
|
1105
|
+
].join('\n') + '\n');
|
|
1106
|
+
process.exitCode = 1;
|
|
1107
|
+
} else {
|
|
1108
|
+
const viaLauncher = !/^node(\.exe)?$/iu.test(basename(process.execPath));
|
|
1109
|
+
const args = [...(viaLauncher ? ['--run'] : []), join(root, runtime.entry),
|
|
1110
|
+
...${JSON.stringify(converge)}, '--payload', payloadDir, '--installed', root];
|
|
1111
|
+
const child = spawnSync(process.execPath, args, { cwd: root, stdio: 'inherit', windowsHide: true });
|
|
1112
|
+
if (child.error) {
|
|
1113
|
+
process.stderr.write('Installer could not re-enter the installed product: ' + child.error.message + '\n');
|
|
1114
|
+
process.exitCode = 1;
|
|
1115
|
+
} else process.exitCode = child.status === null ? 1 : child.status;
|
|
1116
|
+
}
|
|
1117
|
+
`;
|
|
1118
|
+
}
|
|
1119
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(converge[0])) {
|
|
1120
|
+
throw new Error("installer payload: npm converge command must be a bin name");
|
|
1121
|
+
}
|
|
1034
1122
|
return String.raw`// Generated by @mutmutco/installer-face. Product code declares operations, never this program.
|
|
1035
1123
|
import { spawnSync } from 'node:child_process';
|
|
1036
|
-
import { readFileSync } from 'node:fs';
|
|
1037
|
-
import { dirname, join } from 'node:path';
|
|
1124
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
1125
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
1038
1126
|
|
|
1039
1127
|
const payloadDir = dirname(process.argv[1]);
|
|
1040
1128
|
const tarballs = ${JSON.stringify(tarballs.map((name) => `./${name}`))};
|
|
1041
1129
|
const converge = ${JSON.stringify(converge)};
|
|
1042
1130
|
const shippedFlag = ${JSON.stringify(shippedFlag ?? null)};
|
|
1043
1131
|
|
|
1044
|
-
function step(command, args) {
|
|
1045
|
-
const options = { cwd: payloadDir, stdio: 'inherit', windowsHide: true };
|
|
1132
|
+
function step(command, args, capture = false) {
|
|
1133
|
+
const options = { cwd: payloadDir, stdio: capture ? 'pipe' : 'inherit', windowsHide: true, encoding: 'utf8' };
|
|
1134
|
+
if (process.platform === 'win32' && [command, ...args].some((arg) => /[\x00-\x1f"%!]/u.test(arg))) {
|
|
1135
|
+
process.stderr.write('Installer command contains unsupported Windows shell characters.\n');
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1046
1138
|
// One prequoted command and no argv avoids Node's args+shell warning for Windows .cmd shims.
|
|
1047
1139
|
const result = process.platform === 'win32'
|
|
1048
|
-
? spawnSync([command, ...args].map((arg) =>
|
|
1140
|
+
? spawnSync([command, ...args].map((arg) => /[\s&|<>^()]/u.test(arg) ? '"' + arg + '"' : arg).join(' '), [], { ...options, shell: true })
|
|
1049
1141
|
: spawnSync(command, args, options);
|
|
1050
1142
|
if (result.error || result.status !== 0) {
|
|
1143
|
+
if (capture && result.stderr) process.stderr.write(result.stderr);
|
|
1051
1144
|
process.stderr.write('Installer operation failed.\n');
|
|
1052
1145
|
process.exit(result.status === null || result.status === undefined ? 1 : result.status);
|
|
1053
1146
|
}
|
|
1147
|
+
return result.stdout ?? '';
|
|
1054
1148
|
}
|
|
1055
1149
|
|
|
1056
1150
|
step('npm', ['install', '--global', '--no-fund', '--no-audit', ...tarballs]);
|
|
@@ -1068,44 +1162,129 @@ if (shippedFlag) {
|
|
|
1068
1162
|
}
|
|
1069
1163
|
if (!shipped) process.stdout.write('Payload carries no shipped version; running the declared operation.\n');
|
|
1070
1164
|
}
|
|
1071
|
-
|
|
1165
|
+
// The new global bin directory may not be on this process's inherited PATH.
|
|
1166
|
+
const prefix = step('npm', ['prefix', '--global'], true).trim();
|
|
1167
|
+
if (!prefix || !isAbsolute(prefix)) {
|
|
1168
|
+
process.stderr.write('Installer could not resolve the global npm prefix.\n');
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
const candidates = process.platform === 'win32'
|
|
1172
|
+
? [join(prefix, converge[0] + '.cmd'), join(prefix, converge[0])]
|
|
1173
|
+
: [join(prefix, 'bin', converge[0]), join(prefix, converge[0])];
|
|
1174
|
+
const command = candidates.find((candidate) => {
|
|
1175
|
+
try { return statSync(candidate).isFile(); }
|
|
1176
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false; throw error; }
|
|
1177
|
+
});
|
|
1178
|
+
if (!command) {
|
|
1179
|
+
process.stderr.write('Installer could not find the installed command: ' + converge[0] + '\n');
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}
|
|
1182
|
+
step(command, [...converge.slice(1), ...(shipped ? [shippedFlag, shipped] : [])]);
|
|
1072
1183
|
`;
|
|
1073
1184
|
}
|
|
1074
1185
|
|
|
1075
1186
|
// src/maintenance.ts
|
|
1076
1187
|
async function runMaintenance(product, args, dependencies) {
|
|
1077
1188
|
const json = args.includes("--json");
|
|
1189
|
+
const quiet = args.includes("--quiet");
|
|
1078
1190
|
const dryRun = args.includes("--dry-run");
|
|
1079
1191
|
const installed = args.includes("install");
|
|
1192
|
+
const render = dependencies.render;
|
|
1193
|
+
const env = render?.env ?? process.env;
|
|
1194
|
+
const tty = render?.tty ?? Boolean(process.stdout.isTTY);
|
|
1080
1195
|
const run = json ? null : dependencies.run ?? createInstallerRun(product, {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1196
|
+
...render,
|
|
1197
|
+
tty,
|
|
1198
|
+
env,
|
|
1199
|
+
color: tty && render?.color !== false && !args.includes("--no-color") && env.TERM !== "dumb",
|
|
1200
|
+
columns: render?.columns ?? process.stdout.columns,
|
|
1084
1201
|
dryRun,
|
|
1202
|
+
quiet,
|
|
1085
1203
|
operation: installed ? "install" : "update"
|
|
1086
1204
|
});
|
|
1087
|
-
run?.start();
|
|
1205
|
+
if (!quiet) run?.start();
|
|
1088
1206
|
let mark = Date.now();
|
|
1207
|
+
let active;
|
|
1208
|
+
let activeFacts;
|
|
1209
|
+
let handedOff;
|
|
1210
|
+
const shown = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
1211
|
+
if (!quiet && !json) shown.add("welcome");
|
|
1212
|
+
let phaseFailed = false;
|
|
1213
|
+
let armFailed = false;
|
|
1214
|
+
let knownFailure;
|
|
1215
|
+
const phaseStarted = /* @__PURE__ */ new Map();
|
|
1216
|
+
const phase = (event) => {
|
|
1217
|
+
if (handedOff !== void 0) return;
|
|
1218
|
+
if (event.state === "fail") phaseFailed = true;
|
|
1219
|
+
const started = phaseStarted.get(event.id);
|
|
1220
|
+
if (event.state === "running") {
|
|
1221
|
+
if (started === void 0) phaseStarted.set(event.id, Date.now());
|
|
1222
|
+
active = event.id;
|
|
1223
|
+
} else {
|
|
1224
|
+
phaseStarted.delete(event.id);
|
|
1225
|
+
if (active === event.id) active = void 0;
|
|
1226
|
+
}
|
|
1227
|
+
if (!quiet && event.state === "running" && event.detail) run?.relay(event.detail);
|
|
1228
|
+
const measured = event.measured;
|
|
1229
|
+
const measure = measured?.kind === "percent" ? `${Math.max(0, Math.min(100, Math.round(measured.percent)))}%` : measured?.kind === "bytes" ? `${measured.done}/${measured.total} B` : measured?.kind === "count" ? `${measured.done}${measured.total === void 0 ? "" : `/${measured.total}`}` : event.measure;
|
|
1230
|
+
const facts = {
|
|
1231
|
+
state: event.state,
|
|
1232
|
+
detail: event.detail,
|
|
1233
|
+
measure,
|
|
1234
|
+
...event.state !== "running" && started !== void 0 ? { seconds: (Date.now() - started) / 1e3 } : {}
|
|
1235
|
+
};
|
|
1236
|
+
if (event.state === "running") activeFacts = facts;
|
|
1237
|
+
if (!quiet) {
|
|
1238
|
+
run?.phase(event.id, facts);
|
|
1239
|
+
if (event.state === "ok" && run) shown.add(event.id);
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1089
1242
|
const pending = [];
|
|
1090
1243
|
let streaming = false;
|
|
1091
1244
|
const emitArm = ({ arm, seconds }) => {
|
|
1245
|
+
if (quiet) return;
|
|
1092
1246
|
run?.surface({
|
|
1093
1247
|
id: arm.surface,
|
|
1094
1248
|
from: arm.from ?? void 0,
|
|
1095
1249
|
to: arm.to ?? void 0,
|
|
1096
1250
|
state: arm.verdict === "fail" ? "failed" : arm.verdict === "defer" ? "retry" : arm.verdict === "skip" ? "kept" : arm.from === arm.to && !arm.launchStaged ? "current" : "updated",
|
|
1097
1251
|
seconds,
|
|
1098
|
-
detail: dependencies.diagnose(arm)
|
|
1252
|
+
detail: dependencies.diagnose?.(arm)
|
|
1099
1253
|
});
|
|
1100
1254
|
};
|
|
1255
|
+
const writeJson = (summary, schedule) => {
|
|
1256
|
+
process.stdout.write(JSON.stringify(dependencies.jsonResult ? dependencies.jsonResult(summary, schedule) : installed ? { convergence: summary, autoupdate: schedule } : summary, null, 2) + "\n");
|
|
1257
|
+
};
|
|
1101
1258
|
try {
|
|
1102
1259
|
const summary = await dependencies.engine({
|
|
1103
1260
|
dryRun,
|
|
1261
|
+
withCustody: async (operation) => {
|
|
1262
|
+
if (handedOff !== void 0) throw new Error("installer terminal ownership already transferred");
|
|
1263
|
+
run?.stop();
|
|
1264
|
+
const result = await operation({ faceContinues: [...shown] });
|
|
1265
|
+
if (result.kind === "reexec") {
|
|
1266
|
+
if (!Number.isSafeInteger(result.code) || result.code < 0) throw new Error("invalid handoff exit code");
|
|
1267
|
+
handedOff = result.code;
|
|
1268
|
+
} else if (result.kind === "resume") {
|
|
1269
|
+
if (!quiet && active) run?.phase(active, activeFacts);
|
|
1270
|
+
} else throw new Error("invalid terminal custody result");
|
|
1271
|
+
return result;
|
|
1272
|
+
},
|
|
1104
1273
|
shippedTarget: installed ? dependencies.shippedTarget : void 0,
|
|
1105
|
-
narrate: args.includes("--verbose") ? (text) =>
|
|
1106
|
-
|
|
1107
|
-
|
|
1274
|
+
narrate: !quiet && !json && args.includes("--verbose") ? (text) => {
|
|
1275
|
+
if (handedOff === void 0) run?.relay(text, "stderr", false);
|
|
1276
|
+
} : void 0,
|
|
1277
|
+
relay: (text, channel = "stdout", record = false) => {
|
|
1278
|
+
if (handedOff === void 0) run?.relay(text, channel, record);
|
|
1279
|
+
},
|
|
1280
|
+
onTargetResolved: (target) => phase({ id: "verify-release", detail: target, state: "ok" }),
|
|
1281
|
+
onPhase: () => {
|
|
1282
|
+
if (!quiet && handedOff === void 0) run?.phase("check", { state: "running" });
|
|
1283
|
+
},
|
|
1284
|
+
onPhaseEvent: phase,
|
|
1108
1285
|
onArm: (arm) => {
|
|
1286
|
+
if (handedOff !== void 0) return;
|
|
1287
|
+
if (arm.verdict === "fail") armFailed = true;
|
|
1109
1288
|
const seconds = (Date.now() - mark) / 1e3;
|
|
1110
1289
|
mark = Date.now();
|
|
1111
1290
|
pending.push({ arm, seconds });
|
|
@@ -1113,43 +1292,68 @@ async function runMaintenance(product, args, dependencies) {
|
|
|
1113
1292
|
if (streaming) for (const result of pending.splice(0)) emitArm(result);
|
|
1114
1293
|
}
|
|
1115
1294
|
});
|
|
1295
|
+
if (handedOff !== void 0) return handedOff;
|
|
1116
1296
|
if (summary.reexec) {
|
|
1297
|
+
if (!dependencies.handOff) {
|
|
1298
|
+
knownFailure = "The updated version needs a handoff, but no handoff is configured.";
|
|
1299
|
+
throw new Error(knownFailure);
|
|
1300
|
+
}
|
|
1117
1301
|
run?.stop();
|
|
1118
|
-
|
|
1302
|
+
knownFailure = "The updated updater could not launch.";
|
|
1303
|
+
return await dependencies.handOff(summary.reexec, args, { ...process.env, MM_FACE_CONTINUES: "welcome" });
|
|
1304
|
+
}
|
|
1305
|
+
const counts2 = dependencies.counts(summary);
|
|
1306
|
+
validateInstallerOutcome({ total: counts2.total, updated: counts2.updated, failed: counts2.failed });
|
|
1307
|
+
if (!Number.isSafeInteger(counts2.retry) || counts2.retry < 0 || counts2.retry > counts2.total) throw new Error("invalid retry count");
|
|
1308
|
+
if (summary.cancelled && summary.exit === 0 && !phaseFailed && !armFailed && counts2.failed === 0 && counts2.updated === 0) {
|
|
1309
|
+
if (json) writeJson(summary, null);
|
|
1310
|
+
else run?.cancel();
|
|
1311
|
+
return 0;
|
|
1119
1312
|
}
|
|
1120
1313
|
for (const arm of pending) emitArm(arm);
|
|
1121
1314
|
const waiting = summary.surfaces.filter((surface) => surface.present && surface.action === "arm-pending");
|
|
1122
|
-
for (const surface of waiting) run?.surface({ id: surface.id, state: "pending" });
|
|
1315
|
+
if (!quiet) for (const surface of waiting) run?.surface({ id: surface.id, state: "pending" });
|
|
1123
1316
|
let schedule = null;
|
|
1124
|
-
if (installed && !dryRun) {
|
|
1125
|
-
|
|
1317
|
+
if (installed && !dryRun && dependencies.schedule) {
|
|
1318
|
+
phase({ id: "arm", state: "running" });
|
|
1126
1319
|
schedule = dependencies.schedule();
|
|
1127
|
-
|
|
1128
|
-
state: schedule.ok ? "ok" : "fail",
|
|
1129
|
-
seconds: (Date.now() - started) / 1e3,
|
|
1130
|
-
detail: schedule.ok ? void 0 : schedule.detail
|
|
1131
|
-
});
|
|
1320
|
+
if (schedule) phase({ id: "arm", state: schedule.ok ? "ok" : "fail", detail: schedule.ok ? void 0 : schedule.detail });
|
|
1132
1321
|
}
|
|
1133
|
-
const
|
|
1322
|
+
const failed = phaseFailed || armFailed || counts2.failed > 0 || Boolean(schedule && !schedule.ok);
|
|
1323
|
+
const exit = Math.max(summary.exit, failed ? 1 : 0);
|
|
1134
1324
|
if (json) {
|
|
1135
|
-
|
|
1325
|
+
writeJson({ ...summary, exit }, schedule);
|
|
1136
1326
|
} else {
|
|
1137
1327
|
run.finish({
|
|
1138
1328
|
version: summary.target ?? void 0,
|
|
1139
1329
|
total: counts2.total,
|
|
1140
1330
|
updated: counts2.updated,
|
|
1141
1331
|
failed: counts2.failed,
|
|
1142
|
-
operationFailed:
|
|
1143
|
-
deferred: Boolean(counts2.retry || waiting.length || summary.deferred || !summary.target),
|
|
1332
|
+
operationFailed: exit !== 0,
|
|
1333
|
+
deferred: !failed && Boolean(counts2.retry || waiting.length || summary.deferred || summary.cancelled || !summary.target),
|
|
1144
1334
|
dryRun,
|
|
1145
1335
|
installed,
|
|
1146
|
-
detail: schedule && !schedule.ok ? schedule.detail : summary.detail
|
|
1336
|
+
detail: schedule && !schedule.ok ? schedule.detail : summary.detail ?? (summary.cancelled ? "Operation cancelled." : void 0),
|
|
1337
|
+
retry: summary.retry,
|
|
1338
|
+
rollback: summary.rollback,
|
|
1339
|
+
logPath: summary.logPath,
|
|
1340
|
+
logState: summary.logState
|
|
1147
1341
|
});
|
|
1148
1342
|
}
|
|
1149
|
-
return
|
|
1343
|
+
return exit;
|
|
1150
1344
|
} catch (error) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1345
|
+
if (handedOff !== void 0) throw error;
|
|
1346
|
+
let detail = knownFailure ?? (active === "sign-in" ? "Sign-in did not complete." : installed ? "Installation did not complete." : "Update did not complete.");
|
|
1347
|
+
if (!knownFailure && dependencies.safeErrorDetail) {
|
|
1348
|
+
try {
|
|
1349
|
+
detail = dependencies.safeErrorDetail(error);
|
|
1350
|
+
} catch {
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (active) phase({ id: active, state: "fail" });
|
|
1354
|
+
run?.relay(detail, "stderr");
|
|
1355
|
+
run?.finish({ total: 0, updated: 0, failed: 0, operationFailed: true, detail });
|
|
1356
|
+
if (json) process.stdout.write(JSON.stringify({ error: detail }) + "\n");
|
|
1153
1357
|
return 1;
|
|
1154
1358
|
} finally {
|
|
1155
1359
|
run?.stop();
|
package/dist/maintenance.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type InstallerRun } from './run.js';
|
|
1
|
+
import { type InstallerChannel, type InstallerFinish, type InstallerPhase, type InstallerPhaseFacts, type InstallerRunOptions, type InstallerRun } from './run.js';
|
|
2
2
|
/** Facts produced by a product's convergence engine. No terminal labels or layout. */
|
|
3
3
|
export interface MaintenanceArm {
|
|
4
4
|
surface: string;
|
|
@@ -19,34 +19,76 @@ export interface MaintenanceSummary {
|
|
|
19
19
|
exit: number;
|
|
20
20
|
detail?: string;
|
|
21
21
|
deferred?: boolean;
|
|
22
|
+
cancelled?: boolean;
|
|
23
|
+
retry?: string;
|
|
24
|
+
rollback?: InstallerFinish['rollback'];
|
|
25
|
+
logPath?: string;
|
|
26
|
+
logState?: InstallerFinish['logState'];
|
|
22
27
|
reexec: {
|
|
23
28
|
dist: string;
|
|
24
29
|
target: string;
|
|
25
30
|
from: string | null;
|
|
26
31
|
} | null;
|
|
27
32
|
}
|
|
33
|
+
export interface MaintenancePhaseEvent {
|
|
34
|
+
id: InstallerPhase;
|
|
35
|
+
state: 'running' | 'ok' | 'fail' | 'note';
|
|
36
|
+
detail?: string;
|
|
37
|
+
measure?: InstallerPhaseFacts['measure'];
|
|
38
|
+
measured?: {
|
|
39
|
+
kind: 'percent';
|
|
40
|
+
percent: number;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'bytes';
|
|
43
|
+
done: number;
|
|
44
|
+
total: number;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'count';
|
|
47
|
+
done: number;
|
|
48
|
+
total?: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export type MaintenanceCustodyResult<T> = {
|
|
52
|
+
kind: 'reexec';
|
|
53
|
+
code: number;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'resume';
|
|
56
|
+
value: T;
|
|
57
|
+
};
|
|
58
|
+
export interface MaintenanceEngineOptions {
|
|
59
|
+
dryRun: boolean;
|
|
60
|
+
withCustody: <T>(operation: (context: {
|
|
61
|
+
faceContinues: readonly string[];
|
|
62
|
+
}) => Promise<MaintenanceCustodyResult<T>>) => Promise<MaintenanceCustodyResult<T>>;
|
|
63
|
+
shippedTarget?: string;
|
|
64
|
+
narrate?: (line: string) => void;
|
|
65
|
+
onTargetResolved?: (target: string, gate: string | null) => void;
|
|
66
|
+
/** Legacy engine notification; it does not imply a completed phase. */
|
|
67
|
+
onPhase?: (line: string) => void;
|
|
68
|
+
onPhaseEvent?: (event: MaintenancePhaseEvent) => void;
|
|
69
|
+
relay?: (text: string, channel?: InstallerChannel, record?: boolean) => void;
|
|
70
|
+
onArm?: (arm: MaintenanceArm) => void;
|
|
71
|
+
}
|
|
72
|
+
export type MaintenanceSchedule = {
|
|
73
|
+
ok: boolean;
|
|
74
|
+
detail: string;
|
|
75
|
+
} | null;
|
|
28
76
|
/** One install/update lifecycle, including scheduler ordering and self-update continuation. */
|
|
29
77
|
export declare function runMaintenance<S extends MaintenanceSummary>(product: unknown, args: string[], dependencies: {
|
|
30
|
-
engine: (options:
|
|
31
|
-
dryRun: boolean;
|
|
32
|
-
shippedTarget?: string;
|
|
33
|
-
narrate?: (line: string) => void;
|
|
34
|
-
onTargetResolved?: (target: string, gate: string | null) => void;
|
|
35
|
-
onPhase?: (line: string) => void;
|
|
36
|
-
onArm?: (arm: MaintenanceArm) => void;
|
|
37
|
-
}) => Promise<S>;
|
|
78
|
+
engine: (options: MaintenanceEngineOptions) => Promise<S>;
|
|
38
79
|
counts: (summary: S) => {
|
|
39
80
|
total: number;
|
|
40
81
|
updated: number;
|
|
41
82
|
failed: number;
|
|
42
83
|
retry: number;
|
|
43
84
|
};
|
|
44
|
-
diagnose
|
|
45
|
-
schedule
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
diagnose?: (arm: MaintenanceArm) => string | undefined;
|
|
86
|
+
schedule?: () => MaintenanceSchedule;
|
|
87
|
+
handOff?: (next: NonNullable<S['reexec']>, args: string[], env: NodeJS.ProcessEnv) => number | Promise<number>;
|
|
88
|
+
jsonResult?: (summary: S, schedule: MaintenanceSchedule) => object;
|
|
89
|
+
/** Return a safe, actionable diagnosis, never raw authentication output. */
|
|
90
|
+
safeErrorDetail?: (error: unknown) => string;
|
|
50
91
|
shippedTarget?: string;
|
|
51
92
|
run?: InstallerRun;
|
|
93
|
+
render?: Pick<InstallerRunOptions, 'tty' | 'color' | 'columns' | 'env' | 'animate'>;
|
|
52
94
|
}): Promise<number>;
|
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';
|
|
@@ -31,10 +32,13 @@ export interface InstallerRunOptions {
|
|
|
31
32
|
operation?: 'install' | 'update';
|
|
32
33
|
tty?: boolean;
|
|
33
34
|
dryRun?: boolean;
|
|
35
|
+
quiet?: boolean;
|
|
34
36
|
color?: boolean;
|
|
35
37
|
columns?: number;
|
|
36
38
|
env?: Readonly<NodeJS.ProcessEnv>;
|
|
37
39
|
animate?: boolean;
|
|
40
|
+
/** Optional output observer. Exceptions are recorded in run.errors, never operation failures.
|
|
41
|
+
* Omit for production terminal output: the default worker animates during synchronous work. */
|
|
38
42
|
write?: (text: string, channel: InstallerChannel) => void;
|
|
39
43
|
}
|
|
40
44
|
export interface InstallerFinish {
|
|
@@ -48,6 +52,9 @@ export interface InstallerFinish {
|
|
|
48
52
|
detail?: string;
|
|
49
53
|
deferred?: boolean;
|
|
50
54
|
operationFailed?: boolean;
|
|
55
|
+
rollback?: 'completed' | 'partial' | 'not-needed' | 'unknown';
|
|
56
|
+
logPath?: string;
|
|
57
|
+
logState?: 'unavailable' | 'omitted';
|
|
51
58
|
}
|
|
52
59
|
export interface InstallerPhaseFacts {
|
|
53
60
|
state?: 'running' | StepKind;
|
|
@@ -65,6 +72,7 @@ export interface InstallerSurfaceFacts {
|
|
|
65
72
|
}
|
|
66
73
|
/** One owner for the entire run. Callers supply measured facts, never terminal prose. */
|
|
67
74
|
export declare function createInstallerRun(value: unknown, options?: InstallerRunOptions): {
|
|
75
|
+
readonly errors: readonly string[];
|
|
68
76
|
start: () => void;
|
|
69
77
|
phase(id: InstallerPhase, facts?: InstallerPhaseFacts): void;
|
|
70
78
|
surface(facts: InstallerSurfaceFacts): void;
|
|
@@ -78,6 +86,7 @@ export declare function createInstallerRun(value: unknown, options?: InstallerRu
|
|
|
78
86
|
code: string;
|
|
79
87
|
}): void;
|
|
80
88
|
relay(text: string, channel?: InstallerChannel, record?: boolean): void;
|
|
89
|
+
cancel(): void;
|
|
81
90
|
finish(facts: InstallerFinish): void;
|
|
82
91
|
stop(): void;
|
|
83
92
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/installer-face",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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",
|