@mutmutco/installer-face 0.4.3 → 0.4.5

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.
@@ -0,0 +1,250 @@
1
+ #Requires -Version 5.1
2
+ # One-line installer for %%PRODUCT%% (launcher %%VERSION%%).
3
+ # Served at %%BASE%% -- run with:
4
+ # irm %%BASE%% | iex
5
+ #
6
+ # The origin substitutes %%PRODUCT%%, %%BASE%% and %%VERSION%% before serving this file.
7
+ # The product name is never read from an argument, an environment variable or a prompt.
8
+ #
9
+ # Windows PowerShell 5.1 compatible: no ternary operator, no null-coalescing, no pwsh-only cmdlets.
10
+
11
+ $ErrorActionPreference = 'Stop'
12
+ $ProgressPreference = 'SilentlyContinue'
13
+
14
+ $Product = '%%PRODUCT%%'
15
+ $Base = '%%BASE%%'
16
+ $Version = '%%VERSION%%'
17
+
18
+ %%FACE%%
19
+
20
+ # A refusal wears the face too (#6789): `Write-FaceRefusal` draws it, so an install that succeeds
21
+ # looks like the product and one that fails does not read as a bare shell error — the moment a
22
+ # person most needs to see who is talking. Still stderr, still plain when piped or NO_COLOR; the
23
+ # package's block sets [Console]::OutputEncoding to UTF-8 before the first glyph.
24
+ function Write-Refusal {
25
+ param([string] $Message)
26
+ Write-FaceRefusal $Message
27
+ }
28
+
29
+ # Maps the running Windows architecture to the artifact token. Returns '' when unsupported.
30
+ # SHA-256 without a cmdlet (#6803). `Get-FileHash` lives in Microsoft.PowerShell.Utility and is
31
+ # normally autoloaded — but this script is pasted into WHATEVER shell the person happens to have,
32
+ # and a PSModulePath inherited from PowerShell 7 left Windows PowerShell 5.1 unable to resolve its
33
+ # own Utility cmdlets: the install downloaded, then died at the checksum. The verification step is
34
+ # the one place that must never depend on luck, so it uses .NET, which exists wherever PowerShell
35
+ # runs at all. Returns lowercase hex, or '' when the file cannot be read.
36
+ function Get-Sha256Hex {
37
+ param([string] $Path)
38
+ $sha = $null
39
+ $stream = $null
40
+ try {
41
+ $sha = [System.Security.Cryptography.SHA256]::Create()
42
+ $stream = [System.IO.File]::OpenRead($Path)
43
+ $bytes = $sha.ComputeHash($stream)
44
+ $builder = New-Object System.Text.StringBuilder
45
+ foreach ($b in $bytes) { [void] $builder.Append($b.ToString('x2')) }
46
+ return $builder.ToString()
47
+ }
48
+ catch {
49
+ return ''
50
+ }
51
+ finally {
52
+ if ($null -ne $stream) { $stream.Dispose() }
53
+ if ($null -ne $sha) { $sha.Dispose() }
54
+ }
55
+ }
56
+
57
+ # Download without a cmdlet, for the same reason (#6803): `Invoke-WebRequest` is Utility too, and a
58
+ # shell that cannot autoload it cannot install at all. TLS 1.2 is forced because 5.1 still defaults
59
+ # to SSL3/TLS1 on older builds and the box refuses those. Returns $true on success.
60
+ function Save-Download {
61
+ param([string] $Uri, [string] $OutFile, [string] $UserAgent)
62
+ try {
63
+ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
64
+ }
65
+ catch {
66
+ # An older .NET without the enum value: leave the default and let the request decide.
67
+ }
68
+ $client = $null
69
+ try {
70
+ $client = New-Object System.Net.WebClient
71
+ $client.Headers.Add('User-Agent', $UserAgent)
72
+ $client.DownloadFile($Uri, $OutFile)
73
+ return $true
74
+ }
75
+ catch {
76
+ return $false
77
+ }
78
+ finally {
79
+ if ($null -ne $client) { $client.Dispose() }
80
+ }
81
+ }
82
+
83
+ function Get-InstallerArch {
84
+ $raw = $env:PROCESSOR_ARCHITEW6432
85
+ if ([string]::IsNullOrEmpty($raw)) {
86
+ $raw = $env:PROCESSOR_ARCHITECTURE
87
+ }
88
+ if ($raw -eq 'AMD64') {
89
+ return 'x64'
90
+ }
91
+ return ''
92
+ }
93
+
94
+ # True when $Directory is already one of the entries in the ';'-separated $PathValue.
95
+ function Test-PathEntry {
96
+ param([string] $PathValue, [string] $Directory)
97
+ if ([string]::IsNullOrEmpty($PathValue)) {
98
+ return $false
99
+ }
100
+ $wanted = $Directory.TrimEnd('\')
101
+ foreach ($entry in $PathValue.Split(';')) {
102
+ $candidate = $entry.Trim().TrimEnd('\')
103
+ if ($candidate.Length -gt 0 -and $candidate -ieq $wanted) {
104
+ return $true
105
+ }
106
+ }
107
+ return $false
108
+ }
109
+
110
+ function Install-Launcher {
111
+ [Net.ServicePointManager]::SecurityProtocol =
112
+ [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
113
+
114
+ Write-FaceWelcome
115
+
116
+ $arch = Get-InstallerArch
117
+ if ($arch -ne 'x64') {
118
+ Write-Refusal "Unsupported CPU architecture for $Product on Windows; only x64 is supported."
119
+ return 1
120
+ }
121
+ Write-FaceStep "Checked this machine: Windows $arch" ([DateTime]::UtcNow)
122
+
123
+ $url = "https://$Base/dl/$Product/win-$arch"
124
+ $userAgent = "$Product-installer/$Version"
125
+ $tmpDir = Join-Path $env:TEMP ("$Product-install-" + [Guid]::NewGuid().ToString('N'))
126
+ $exitCode = 1
127
+
128
+ try {
129
+ [void] [System.IO.Directory]::CreateDirectory($tmpDir)
130
+ $payload = Join-Path $tmpDir 'payload'
131
+ $sumFile = Join-Path $tmpDir 'payload.sha256'
132
+
133
+ $stepStarted = [DateTime]::UtcNow
134
+ if (-not (Save-Download $url $payload $userAgent)) {
135
+ Write-Refusal "Could not download $Product from $url."
136
+ return 1
137
+ }
138
+
139
+ if (-not (Save-Download "$url.sha256" $sumFile $userAgent)) {
140
+ Write-Refusal "Could not download the checksum from $url.sha256."
141
+ return 1
142
+ }
143
+ Write-FaceStep 'Downloaded the launcher' $stepStarted
144
+
145
+ $expected = ''
146
+ try {
147
+ $firstLine = [System.IO.File]::ReadAllLines($sumFile) | Select-Object -First 1
148
+ if ($null -ne $firstLine) {
149
+ $fields = ([string] $firstLine).Trim() -split '\s+'
150
+ $expected = $fields[0]
151
+ }
152
+ }
153
+ catch {
154
+ $expected = ''
155
+ }
156
+ if ([string]::IsNullOrEmpty($expected)) {
157
+ Write-Refusal "The checksum published for $Product is empty or malformed."
158
+ return 1
159
+ }
160
+
161
+ $stepStarted = [DateTime]::UtcNow
162
+ $actual = Get-Sha256Hex $payload
163
+ if ([string]::IsNullOrEmpty($actual)) {
164
+ Write-Refusal "Could not read the downloaded file to verify it; nothing was installed."
165
+ return 1
166
+ }
167
+ if ($actual.ToLowerInvariant() -ne $expected.ToLowerInvariant()) {
168
+ Write-Refusal "Checksum mismatch for $Product; the download was discarded and nothing was installed."
169
+ return 1
170
+ }
171
+ Write-FaceStep 'Verified the SHA-256 checksum' $stepStarted
172
+
173
+ $stepStarted = [DateTime]::UtcNow
174
+ $binDir = Join-Path $env:LOCALAPPDATA (Join-Path $Product 'bin')
175
+ $exePath = Join-Path $binDir "$Product.exe"
176
+ try {
177
+ [void] [System.IO.Directory]::CreateDirectory($binDir)
178
+ if ([System.IO.File]::Exists($exePath)) { [System.IO.File]::Delete($exePath) }
179
+ [System.IO.File]::Move($payload, $exePath)
180
+ }
181
+ catch {
182
+ Write-Refusal "Could not install $Product into $binDir."
183
+ return 1
184
+ }
185
+
186
+ try {
187
+ $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
188
+ if ($null -eq $userPath) {
189
+ $userPath = ''
190
+ }
191
+ if (-not (Test-PathEntry $userPath $binDir)) {
192
+ if ($userPath.Length -eq 0) {
193
+ $newPath = $binDir
194
+ }
195
+ elseif ($userPath.EndsWith(';')) {
196
+ $newPath = $userPath + $binDir
197
+ }
198
+ else {
199
+ $newPath = $userPath + ';' + $binDir
200
+ }
201
+ [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
202
+ }
203
+ }
204
+ catch {
205
+ Write-Refusal "Could not add $binDir to your user PATH."
206
+ return 1
207
+ }
208
+
209
+ $env:Path = $binDir + ';' + $env:Path
210
+ Write-FaceStep 'Installed the launcher' $stepStarted
211
+
212
+ $script:MMInstallerLauncherPath = $exePath
213
+ $exitCode = 0
214
+ }
215
+ finally {
216
+ Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
217
+ }
218
+
219
+ return $exitCode
220
+ }
221
+
222
+ $installerExitCode = Install-Launcher
223
+ if ($installerExitCode -eq 0) {
224
+ # Invoke outside the setup function's captured return pipeline so the native launcher inherits
225
+ # the terminal. It continues our welcome and owns the only final receipt.
226
+ $previousOuter = [Environment]::GetEnvironmentVariable('MM_OUTER_CONSOLE', 'Process')
227
+ $previousContinues = [Environment]::GetEnvironmentVariable('MM_FACE_CONTINUES', 'Process')
228
+ try {
229
+ [Environment]::SetEnvironmentVariable('MM_OUTER_CONSOLE', $null, 'Process')
230
+ $env:MM_FACE_CONTINUES = 'welcome,preflight'
231
+ $global:LASTEXITCODE = 0
232
+ & $script:MMInstallerLauncherPath install
233
+ $installerExitCode = $LASTEXITCODE
234
+ }
235
+ catch {
236
+ if ($LASTEXITCODE -ne 0) {
237
+ $installerExitCode = $LASTEXITCODE
238
+ }
239
+ else {
240
+ Write-Refusal "Could not start the $Product launcher; installation did not finish."
241
+ $installerExitCode = 1
242
+ }
243
+ }
244
+ finally {
245
+ [Environment]::SetEnvironmentVariable('MM_OUTER_CONSOLE', $previousOuter, 'Process')
246
+ [Environment]::SetEnvironmentVariable('MM_FACE_CONTINUES', $previousContinues, 'Process')
247
+ }
248
+ }
249
+ # IEX may run inside a caller script too. Never terminate that caller's session.
250
+ $global:LASTEXITCODE = $installerExitCode
@@ -0,0 +1,157 @@
1
+ #!/bin/sh
2
+ # One-line installer for %%PRODUCT%% (launcher %%VERSION%%).
3
+ # Served at %%BASE%% — run with:
4
+ # curl -fsSL %%BASE%% | sh
5
+ #
6
+ # The origin substitutes %%PRODUCT%%, %%BASE%% and %%VERSION%% before serving this file.
7
+ # The product name is never read from an argument, an environment variable or a prompt.
8
+ #
9
+ # POSIX sh only (dash-safe): no bashisms, no eval of downloaded content, curl only.
10
+
11
+ set -u
12
+
13
+ PRODUCT='%%PRODUCT%%'
14
+ BASE='%%BASE%%'
15
+ VERSION='%%VERSION%%'
16
+
17
+ %%FACE%%
18
+
19
+ # A refusal wears the face too (#6789): `face_refusal` draws it, so an install that succeeds looks
20
+ # like the product and one that fails does not read as a bare shell error — the moment a person
21
+ # most needs to see who is talking. Still stderr, still plain when piped or NO_COLOR.
22
+ fail() {
23
+ face_refusal "$1"
24
+ exit 1
25
+ }
26
+
27
+ # >>> installer-common:begin
28
+ # Pure mapping helpers. installer/scripts/test/run-tests.sh extracts this block and
29
+ # unit-tests it on any host, so keep it free of side effects and of $PRODUCT/$BASE.
30
+
31
+ # detect_arch <uname -m output> -> arm64 | x64, non-zero exit when unsupported.
32
+ detect_arch() {
33
+ case "${1:-}" in
34
+ arm64|aarch64) printf '%s\n' 'arm64' ;;
35
+ x86_64) printf '%s\n' 'x64' ;;
36
+ *) return 1 ;;
37
+ esac
38
+ }
39
+
40
+ # map_platform <uname -s output> -> darwin | win, non-zero exit when unsupported.
41
+ map_platform() {
42
+ case "${1:-}" in
43
+ Darwin) printf '%s\n' 'darwin' ;;
44
+ MINGW*|MSYS*|CYGWIN*|Windows_NT) printf '%s\n' 'win' ;;
45
+ *) return 1 ;;
46
+ esac
47
+ }
48
+ # <<< installer-common:end
49
+
50
+ # sha256_of <file> -> lowercase hex digest on stdout, empty when no tool is available.
51
+ sha256_of() {
52
+ if command -v shasum >/dev/null 2>&1; then
53
+ shasum -a 256 "$1" | cut -d' ' -f1
54
+ elif command -v sha256sum >/dev/null 2>&1; then
55
+ sha256sum "$1" | cut -d' ' -f1
56
+ fi
57
+ }
58
+
59
+ # ensure_path_entry <rc file> — append one marked block, only when the marker is absent.
60
+ ensure_path_entry() {
61
+ ensure_rc="$1"
62
+ ensure_marker="# >>> $PRODUCT installer >>>"
63
+ if [ -f "$ensure_rc" ] && grep -Fq "$ensure_marker" "$ensure_rc"; then
64
+ return 0
65
+ fi
66
+ {
67
+ printf '\n%s\n' "$ensure_marker"
68
+ printf '%s\n' 'case ":$PATH:" in'
69
+ printf '%s\n' ' *":$HOME/.local/bin:"*) ;;'
70
+ printf '%s\n' ' *) PATH="$HOME/.local/bin:$PATH" ;;'
71
+ printf '%s\n' 'esac'
72
+ printf '%s\n' 'export PATH'
73
+ printf '%s\n' "# <<< $PRODUCT installer <<<"
74
+ } >>"$ensure_rc" || fail "Could not add $PRODUCT to your PATH in $ensure_rc."
75
+ }
76
+
77
+ main() {
78
+ face_welcome
79
+
80
+ if ! command -v curl >/dev/null 2>&1; then
81
+ fail "curl is required to install $PRODUCT and was not found."
82
+ fi
83
+
84
+ uname_s=$(uname -s)
85
+ uname_m=$(uname -m)
86
+
87
+ platform=$(map_platform "$uname_s") || platform=''
88
+ if [ "$platform" = 'win' ]; then
89
+ fail "This script installs $PRODUCT on macOS. On Windows run: irm $BASE | iex"
90
+ fi
91
+ if [ -z "$platform" ]; then
92
+ if [ "$uname_s" = 'Linux' ]; then
93
+ fail "$PRODUCT does not ship a Linux build; it supports macOS and Windows only."
94
+ fi
95
+ fail "Unsupported operating system: $uname_s. $PRODUCT supports macOS and Windows only."
96
+ fi
97
+
98
+ arch=$(detect_arch "$uname_m") ||
99
+ fail "Unsupported CPU architecture: $uname_m. $PRODUCT supports arm64 and x64."
100
+ if [ "$platform" = 'darwin' ] && [ "$arch" = 'x64' ]; then
101
+ fail "Intel Macs are not supported — use an Apple Silicon Mac or Windows."
102
+ fi
103
+
104
+ step_started=$(date +%s)
105
+ case "$platform" in
106
+ darwin) face_step "Checked this machine: macOS $arch" "$step_started" ;;
107
+ *) face_step "Checked this machine: $platform $arch" "$step_started" ;;
108
+ esac
109
+
110
+ url="https://$BASE/dl/$PRODUCT/$platform-$arch"
111
+
112
+ tmpdir=$(mktemp -d) || fail "Could not create a temporary directory."
113
+ trap 'rm -rf "$tmpdir"' EXIT
114
+ trap 'rm -rf "$tmpdir"; exit 1' INT TERM
115
+
116
+ step_started=$(date +%s)
117
+ curl -fsSL --proto '=https' --tlsv1.2 -A "$PRODUCT-installer/$VERSION" \
118
+ -o "$tmpdir/payload" "$url" || fail "Could not download $PRODUCT from $url."
119
+ curl -fsSL --proto '=https' --tlsv1.2 -A "$PRODUCT-installer/$VERSION" \
120
+ -o "$tmpdir/payload.sha256" "$url.sha256" || fail "Could not download the checksum from $url.sha256."
121
+ face_step 'Downloaded the launcher' "$step_started"
122
+
123
+ step_started=$(date +%s)
124
+ expected=$(head -n 1 "$tmpdir/payload.sha256" | cut -d' ' -f1 | tr -d '\r')
125
+ [ -n "$expected" ] || fail "The checksum published for $PRODUCT is empty or malformed."
126
+
127
+ actual=$(sha256_of "$tmpdir/payload")
128
+ [ -n "$actual" ] || fail "No SHA-256 tool was found; install shasum or sha256sum and retry."
129
+
130
+ if [ "$expected" != "$actual" ]; then
131
+ fail "Checksum mismatch for $PRODUCT; the download was discarded and nothing was installed."
132
+ fi
133
+ face_step 'Verified the SHA-256 checksum' "$step_started"
134
+
135
+ step_started=$(date +%s)
136
+ bindir="$HOME/.local/bin"
137
+ mkdir -p "$bindir" || fail "Could not create $bindir."
138
+ chmod 0755 "$tmpdir/payload" || fail "Could not make $PRODUCT executable."
139
+ mv -f "$tmpdir/payload" "$bindir/$PRODUCT" || fail "Could not install $PRODUCT into $bindir."
140
+
141
+ ensure_path_entry "$HOME/.zshenv"
142
+ ensure_path_entry "$HOME/.bashrc"
143
+
144
+ rm -rf "$tmpdir"
145
+ trap - EXIT INT TERM
146
+ face_step 'Installed the launcher' "$step_started"
147
+
148
+ PATH="$bindir:$PATH"
149
+ export PATH
150
+
151
+ # The bootstrap opened the run; the shared launcher owns its actual outcome and receipt.
152
+ # Exit zero may mean deferred, so the shell must never translate it into a ready claim.
153
+ MM_OUTER_CONSOLE='' MM_FACE_CONTINUES='welcome,preflight' "$bindir/$PRODUCT" install
154
+ return "$?"
155
+ }
156
+
157
+ main
@@ -8,6 +8,8 @@ export interface MaintenanceArm {
8
8
  detail: string;
9
9
  repeatFailure?: boolean;
10
10
  launchStaged?: boolean;
11
+ /** Engine-measured surface work, excluding discovery and time between callbacks. */
12
+ durationMs?: number;
11
13
  }
12
14
  export interface MaintenanceSummary {
13
15
  target: string | null;
package/dist/run.d.ts CHANGED
@@ -22,6 +22,7 @@ declare const PHASES: {
22
22
  readonly 'verify-release': readonly ["Checking the release version", "Verified the release version"];
23
23
  readonly verify: readonly ["Verifying the payload", "Verified the payload"];
24
24
  readonly install: readonly ["Installing the product", "Installed the product"];
25
+ readonly configure: readonly ["Configuring the product", "Configured the product"];
25
26
  readonly activate: readonly ["Activating surfaces", "Activated surfaces"];
26
27
  readonly doctor: readonly ["Checking health", "Checked health"];
27
28
  readonly rollback: readonly ["Restoring the previous version", "Restored the previous version"];
@@ -76,16 +77,18 @@ export declare function createInstallerRun(value: unknown, options?: InstallerRu
76
77
  start: () => void;
77
78
  phase(id: InstallerPhase, facts?: InstallerPhaseFacts): void;
78
79
  surface(facts: InstallerSurfaceFacts): void;
79
- milestone({ step, state, ms }: {
80
+ milestone({ step, state, ms, measure }: {
80
81
  step: string;
81
- state: StepKind;
82
+ state: StepKind | "running";
82
83
  ms?: number;
84
+ measure?: string;
83
85
  }): void;
84
86
  signIn({ url, code }: {
85
87
  url: string;
86
88
  code: string;
87
89
  }): void;
88
90
  relay(text: string, channel?: InstallerChannel, record?: boolean): void;
91
+ relayChunk(text: string, channel?: InstallerChannel): void;
89
92
  cancel(): void;
90
93
  finish(facts: InstallerFinish): void;
91
94
  stop(): void;
package/dist/spinner.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface Spinner {
6
6
  say(text: string, measure?: StepMeasure): void;
7
7
  /** Stop and CLEAR the line. Always call before writing the durable step line. */
8
8
  stop(): void;
9
+ pause(): void;
10
+ resume(): void;
9
11
  }
10
12
  export interface SpinnerOptions {
11
13
  /** False writes nothing at all: no frames, no escape codes, no cleared lines. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-face",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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",
@@ -8,6 +8,9 @@
8
8
  "node": ">=22"
9
9
  },
10
10
  "exports": {
11
+ "./install.html.template": "./dist/install.html.template",
12
+ "./install.sh": "./dist/install.sh",
13
+ "./install.ps1": "./dist/install.ps1",
11
14
  ".": {
12
15
  "types": "./dist/index.d.ts",
13
16
  "import": "./dist/index.js",