@open-mercato/starter 0.6.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.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @open-mercato/starter
2
+
3
+ The central, cross-platform way to stand up an Open Mercato dev environment. One command, idempotent, platform-aware:
4
+
5
+ ```bash
6
+ # Anywhere (clones the repo if you are not inside one):
7
+ npx @open-mercato/starter
8
+
9
+ # Inside a clone:
10
+ yarn om # alias for node packages/starter/bin/om-start.mjs
11
+ ```
12
+
13
+ No Node yet? The package ships pre-Node bootstraps that install a **portable, checksum-verified Node 24 without admin rights**, then run the CLI:
14
+
15
+ - **Windows**: double-click `packages\starter\platform\start.cmd`, or `irm https://raw.githubusercontent.com/open-mercato/open-mercato/main/packages/starter/platform/start.ps1 | iex`
16
+ - **macOS/Linux**: `./packages/starter/platform/start.sh`, or `curl -fsSL https://raw.githubusercontent.com/open-mercato/open-mercato/main/packages/starter/platform/start.sh | bash`
17
+
18
+ ## Commands
19
+
20
+ | Command | What it does |
21
+ |---------|--------------|
22
+ | `up` (default) | Idempotent converge + start: toolchain → container-runtime gate → corporate TLS trust → `.env`/secrets → `yarn install` → infra containers → database → supervised dev runtime. Re-running is always safe; completed steps are skipped. |
23
+ | `up --detach` | Same, but the dev runtime keeps running in the background (pidfile-managed). |
24
+ | `up --mode docker` | Everything containerized (for machines where host Node workloads are not allowed). Mode is remembered. |
25
+ | `stop` | Stop the dev runtime and the infra containers (`--keep-infra`, `--volumes --yes`). |
26
+ | `status` | Processes, containers, health, URLs. |
27
+ | `logs [--follow]` | Tail the newest dev log. |
28
+ | `doctor` | Read-only audit with per-item remediation and a **"hand this to IT" sheet** for admin-gated items. |
29
+ | `reset` | Destructive cleanup (containers, volumes, starter state) — asks first. |
30
+ | `infra up\|down` | Just the infra containers. |
31
+
32
+ Useful flags: `--non-interactive`, `--skip-llm-prompt`, `--skip-db`, `--no-infra`, `--rebuild`, `--clean`, `--profile <name>`, `-- <args for yarn dev>`.
33
+
34
+ `up --clean` forces a full re-converge: it clears the completed-step markers (install, build, migrations) and rebuilds the OpenCode service image, without touching any data — the one-time database seed stays done, and `reset` remains the destructive variant. Conversely, a plain `up` now also skips the database step entirely when the database is initialized and no `Migration*.ts` file changed since the last run (new migrations are detected by fingerprint and applied automatically).
35
+
36
+ ## Design rules
37
+
38
+ - **Node stdlib only, no build step.** The CLI must run from a fresh clone before any `yarn install` — that is what makes it usable behind broken proxies.
39
+ - **Install nothing system-level.** WSL2, Docker Desktop, and Rancher Desktop are detected and *proposed* (with exact commands and an IT handout), never installed. The starter installs only what it fully owns: portable Node (platform scripts), yarn via corepack, env files, CA bundles, repo state.
40
+ - **Corporate TLS interception is a first-class citizen.** `up` probes real egress hosts, captures the interception CA (wire + Windows certificate store + vendor bundles like Netskope's), and provisions one PEM bundle everywhere: `NODE_EXTRA_CA_CERTS` + `--use-system-ca` for host tooling, `docker/certs/` + `docker/opencode/certs/` for image builds and container egress, a Rancher Desktop provisioning script for engine pulls. Prefer handing the starter your official bundle via `starters/company/config.mjs`.
41
+ - **Probes use `127.0.0.1`, never `localhost`** (Windows resolves `localhost` to `::1` first).
42
+ - **The clone's vendored starter wins.** `npx` defers to `packages/starter` inside the repo it operates on, so the starter version always matches the compose files it drives.
43
+
44
+ ## Company tailoring
45
+
46
+ Organizations customize behavior (mirrors, CA bundles, extra checks/steps, env defaults) via `starters/company/config.mjs` in the repo — see `starters/company/README.md`. The package stays generic; the repo carries the policy.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // Central entry for every way of starting Open Mercato:
3
+ // npx @open-mercato/starter [command] — anywhere; clones first if needed
4
+ // yarn om [command] — inside a clone
5
+ // packages/starter/platform/start.* — pre-Node bootstrap wrappers
6
+ //
7
+ // This bin only solves "where is the repo?" (cloning when necessary) and
8
+ // version skew (a clone's own vendored starter wins over the npx copy), then
9
+ // defers every real decision to src/cli.mjs. Node stdlib only — it must run
10
+ // on a fresh clone before any yarn install.
11
+ import { spawn, spawnSync } from 'node:child_process'
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ import { resolveRepoRoot } from '../src/compose.mjs'
17
+
18
+ const REPO_URL = 'https://github.com/open-mercato/open-mercato.git'
19
+
20
+ function extractBootstrapFlag(args, flag) {
21
+ const index = args.indexOf(flag)
22
+ if (index === -1 || index + 1 >= args.length) return null
23
+ const value = args[index + 1]
24
+ args.splice(index, 2)
25
+ return value
26
+ }
27
+
28
+ async function ensureRepo(args) {
29
+ const found = resolveRepoRoot()
30
+ if (found) return found
31
+
32
+ const dir = extractBootstrapFlag(args, '--dir') ?? path.join(process.cwd(), 'open-mercato')
33
+ const branch = extractBootstrapFlag(args, '--branch') ?? 'main'
34
+ if (fs.existsSync(path.join(dir, '.git'))) {
35
+ return resolveRepoRoot(dir)
36
+ }
37
+ const git = spawnSync('git', ['--version'], { stdio: 'ignore' })
38
+ if (git.error || git.status !== 0) {
39
+ console.error('❌ No Open Mercato checkout found here and git is not installed.')
40
+ console.error(' Install git, or clone manually and re-run this command inside the clone:')
41
+ console.error(` git clone ${REPO_URL}`)
42
+ process.exit(2)
43
+ }
44
+ console.log(`No checkout found — cloning open-mercato (${branch}) into ${dir} ...`)
45
+ const clone = spawnSync('git', ['clone', '--branch', branch, REPO_URL, dir], { stdio: 'inherit' })
46
+ if (clone.status !== 0) {
47
+ console.error('❌ git clone failed. Behind a corporate proxy? Configure git first (ask IT for the proxy address):')
48
+ console.error(' git config --global http.proxy http://proxy.example.com:8080')
49
+ console.error(' On Windows also: git config --global http.sslBackend schannel')
50
+ process.exit(clone.status ?? 1)
51
+ }
52
+ return resolveRepoRoot(dir)
53
+ }
54
+
55
+ async function main() {
56
+ const args = process.argv.slice(2)
57
+ const repoRoot = await ensureRepo(args)
58
+ if (!repoRoot) {
59
+ console.error('❌ Could not locate an Open Mercato repo root.')
60
+ process.exit(2)
61
+ }
62
+ process.chdir(repoRoot)
63
+
64
+ // Prefer the repo's own vendored starter (gradlew pattern): the clone knows
65
+ // which starter version matches its compose files and runtime contract.
66
+ const selfDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
67
+ const vendored = path.join(repoRoot, 'packages', 'starter', 'bin', 'om-start.mjs')
68
+ if (fs.existsSync(vendored) && path.resolve(selfDir, 'bin', 'om-start.mjs') !== path.resolve(vendored)) {
69
+ const child = spawn(process.execPath, [vendored, ...args], { stdio: 'inherit', cwd: repoRoot })
70
+ child.on('close', (code) => process.exit(code ?? 0))
71
+ return
72
+ }
73
+
74
+ process.argv = [process.argv[0], process.argv[1], ...args]
75
+ await import('../src/cli.mjs')
76
+ }
77
+
78
+ await main()
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@open-mercato/starter",
3
+ "version": "0.6.5",
4
+ "description": "One-command, cross-platform bootstrap for Open Mercato dev environments: prerequisite doctor, corporate TLS trust, idempotent setup steps, and a supervised hybrid or fully containerized stack.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "om-start": "./bin/om-start.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "platform",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20.19.0"
18
+ },
19
+ "scripts": {
20
+ "test": "echo \"tested from the repo root jest suite\" && exit 0"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/open-mercato/open-mercato",
25
+ "directory": "packages/starter"
26
+ }
27
+ }
@@ -0,0 +1,24 @@
1
+ @echo off
2
+ rem Open Mercato starter — double-click entry for Windows.
3
+ rem Runs the PowerShell bootstrap next to this file, which guarantees Node 24
4
+ rem (portable, no admin) and hands off to the cross-platform starter CLI.
5
+ title Open Mercato Starter
6
+ setlocal
7
+ rem UTF-8 console first: cmd.exe decodes batch files with the code page it
8
+ rem started with, and Yarn Berry's temp .cmd wrappers embed UTF-8 paths —
9
+ rem OEM code pages mangle non-ASCII checkout paths into MODULE_NOT_FOUND.
10
+ chcp 65001 >nul
11
+ set "PS1=%~dp0start.ps1"
12
+ if not exist "%PS1%" (
13
+ echo start.ps1 was not found next to this launcher.
14
+ pause
15
+ exit /b 1
16
+ )
17
+ powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" %*
18
+ set "RC=%ERRORLEVEL%"
19
+ if not "%RC%"=="0" (
20
+ echo.
21
+ echo The starter exited with code %RC%. Review the messages above.
22
+ pause
23
+ )
24
+ endlocal & exit /b %RC%
@@ -0,0 +1,174 @@
1
+ # Open Mercato starter — Windows bootstrap.
2
+ #
3
+ # The only job of this script is to guarantee Node 24 — WITHOUT admin rights,
4
+ # WITHOUT winget, and without touching machine state — then hand off to the
5
+ # cross-platform starter CLI (packages/starter/bin/om-start.mjs), which does
6
+ # everything else (doctor, corporate TLS trust, env/secrets, install, infra
7
+ # containers, database, supervised dev runtime).
8
+ #
9
+ # Managed-device design notes:
10
+ # * Node is installed as a portable ZIP under %LOCALAPPDATA%\OpenMercato —
11
+ # no elevation, no MSI, no PATH pollution beyond this session.
12
+ # * Downloads go through Invoke-WebRequest (schannel), which trusts the
13
+ # Windows certificate store — GPO-deployed corporate interception CAs work
14
+ # out of the box.
15
+ # * The script avoids .NET static calls where possible and degrades with a
16
+ # clear message under AppLocker/WDAC Constrained Language Mode.
17
+ # * WSL2 / Docker Desktop / Rancher Desktop are NEVER installed here — the
18
+ # CLI detects and proposes them with an IT handout (doctor command).
19
+ #
20
+ # Inside a clone: .\packages\starter\platform\start.ps1 [command] [flags]
21
+ # Standalone: irm https://raw.githubusercontent.com/open-mercato/open-mercato/main/packages/starter/platform/start.ps1 | iex
22
+ #
23
+ # Env: OM_NODE_DIST_MIRROR — base URL mirroring https://nodejs.org/dist.
24
+
25
+ $ErrorActionPreference = 'Stop'
26
+
27
+ # UTF-8 console first: cmd.exe decodes batch files with the code page captured
28
+ # at its start, and Yarn Berry's temp .cmd wrappers embed absolute UTF-8 paths.
29
+ # Under an OEM code page a non-ASCII checkout path (e.g. C:\Users\Michał) turns
30
+ # into mojibake and installs/builds die with MODULE_NOT_FOUND. chcp.com is a
31
+ # plain executable, so this stays Constrained-Language-Mode safe.
32
+ try { & "$env:SystemRoot\System32\chcp.com" 65001 | Out-Null } catch { }
33
+
34
+ $NodeMajor = 24
35
+ $NodeDistBase = if ($env:OM_NODE_DIST_MIRROR) { $env:OM_NODE_DIST_MIRROR.TrimEnd('/') } else { 'https://nodejs.org/dist' }
36
+
37
+ function Write-Info([string]$Message) { Write-Host "[starter] $Message" -ForegroundColor Cyan }
38
+ function Write-Warn([string]$Message) { Write-Host "[starter] $Message" -ForegroundColor Yellow }
39
+ function Fail([string]$Message) { Write-Host "[starter] $Message" -ForegroundColor Red; exit 1 }
40
+
41
+ # Two-tone Open Mercato wordmark (7-bit ASCII — safe on conhost/OEM code pages).
42
+ $LogoLeft = @(
43
+ ' ___ ____ _____ _ _ ',
44
+ ' / _ \| _ \| ____| \ | |',
45
+ '| | | | |_) | _| | \| |',
46
+ '| |_| | __/| |___| |\ |',
47
+ ' \___/|_| |_____|_| \_|'
48
+ )
49
+ $LogoRight = @(
50
+ ' __ __ _____ ____ ____ _ _____ ___',
51
+ '| \/ | ____| _ \ / ___| / \|_ _/ _ \',
52
+ '| |\/| | _| | |_) | | / _ \ | || | | |',
53
+ '| | | | |___| _ <| |___ / ___ \| || |_| |',
54
+ '|_| |_|_____|_| \_\\____/_/ \_\_| \___/'
55
+ )
56
+ Write-Host ''
57
+ for ($i = 0; $i -lt $LogoLeft.Count; $i++) {
58
+ Write-Host (" {0}" -f $LogoLeft[$i]) -ForegroundColor DarkCyan -NoNewline
59
+ Write-Host (" {0}" -f $LogoRight[$i]) -ForegroundColor Cyan
60
+ }
61
+ Write-Host ''
62
+ Write-Host " platform bootstrap - guarantees Node $NodeMajor (no admin), then hands off to the starter CLI" -ForegroundColor DarkGray
63
+ Write-Host ''
64
+
65
+ # Constrained Language Mode (AppLocker/WDAC): everything below sticks to
66
+ # cmdlets and core types, but surface the state early so failures make sense.
67
+ $languageMode = $ExecutionContext.SessionState.LanguageMode
68
+ if ("$languageMode" -ne 'FullLanguage') {
69
+ Write-Warn "PowerShell runs in $languageMode (AppLocker/WDAC policy). Continuing with cmdlet-only operations; if this script is blocked, ask IT to allow it or run from an exempted path."
70
+ }
71
+
72
+ # PS 5.1 defaults can lack TLS 1.2 on older builds; a .NET static call is
73
+ # blocked under CLM, so guard it — Windows 10/11 22H2 negotiate TLS 1.2 anyway.
74
+ try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }
75
+
76
+ function Get-NodeMajor {
77
+ # Scoped EAP: under 'Stop', PS 5.1 turns node's stderr (e.g. a NODE_OPTIONS
78
+ # warning inherited from the environment) into a terminating error once
79
+ # 2>$null wraps it — which would misreport a working Node as missing.
80
+ try {
81
+ $previousPreference = $ErrorActionPreference
82
+ $ErrorActionPreference = 'Continue'
83
+ try {
84
+ $version = (& node -v) 2>$null
85
+ } finally {
86
+ $ErrorActionPreference = $previousPreference
87
+ }
88
+ if ("$version" -match 'v(\d+)\.') { return [int]$Matches[1] }
89
+ } catch { }
90
+ return 0
91
+ }
92
+
93
+ function Find-PortableNode {
94
+ $portableRoot = Join-Path $env:LOCALAPPDATA 'OpenMercato\node'
95
+ if (-not (Test-Path $portableRoot)) { return $null }
96
+ $candidate = Get-ChildItem -Path $portableRoot -Directory -Filter "node-v$NodeMajor.*" -ErrorAction SilentlyContinue |
97
+ Sort-Object Name -Descending | Select-Object -First 1
98
+ if ($candidate -and (Test-Path (Join-Path $candidate.FullName 'node.exe'))) { return $candidate.FullName }
99
+ return $null
100
+ }
101
+
102
+ function Install-PortableNode {
103
+ $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
104
+ Write-Info "Resolving latest Node $NodeMajor release ..."
105
+ $index = Invoke-RestMethod -Uri "$NodeDistBase/index.json" -UseBasicParsing
106
+ $release = $index | Where-Object { $_.version -match "^v$NodeMajor\." } | Select-Object -First 1
107
+ if (-not $release) { Fail "Could not resolve a Node $NodeMajor version from $NodeDistBase. Behind a proxy? Set HTTPS_PROXY, or point OM_NODE_DIST_MIRROR at an internal mirror." }
108
+ $version = $release.version
109
+ $zipName = "node-$version-win-$arch.zip"
110
+ $portableRoot = Join-Path $env:LOCALAPPDATA 'OpenMercato\node'
111
+ $tempDir = Join-Path $env:TEMP "open-mercato-starter"
112
+ New-Item -ItemType Directory -Force -Path $portableRoot | Out-Null
113
+ New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
114
+ $zipPath = Join-Path $tempDir $zipName
115
+
116
+ Write-Info "Downloading $zipName (no admin required) ..."
117
+ Invoke-WebRequest -Uri "$NodeDistBase/$version/$zipName" -OutFile $zipPath -UseBasicParsing
118
+
119
+ Write-Info 'Verifying checksum ...'
120
+ $shasums = (Invoke-WebRequest -Uri "$NodeDistBase/$version/SHASUMS256.txt" -UseBasicParsing).Content
121
+ $expectedLine = $shasums -split "`n" | Where-Object { $_ -match [regex]::Escape($zipName) } | Select-Object -First 1
122
+ if (-not $expectedLine) { Fail "SHASUMS256.txt has no entry for $zipName." }
123
+ $expected = ($expectedLine -split '\s+')[0].Trim().ToLowerInvariant()
124
+ $actual = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
125
+ if ($expected -ne $actual) { Fail "Checksum verification FAILED for $zipName (expected $expected, got $actual). Refusing to continue." }
126
+
127
+ Write-Info "Extracting to $portableRoot ..."
128
+ Expand-Archive -Path $zipPath -DestinationPath $portableRoot -Force
129
+ Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
130
+
131
+ $installed = Find-PortableNode
132
+ if (-not $installed) { Fail 'Portable Node extraction failed.' }
133
+ return $installed
134
+ }
135
+
136
+ if ((Get-NodeMajor) -ne $NodeMajor) {
137
+ $portable = Find-PortableNode
138
+ if (-not $portable) { $portable = Install-PortableNode }
139
+ $env:Path = "$portable;$env:Path"
140
+ Write-Warn "Using portable Node from $portable (PATH updated for this session)."
141
+ Write-Warn "Persist it for your own shells: setx PATH `"$portable;%PATH%`""
142
+ }
143
+ if ((Get-NodeMajor) -ne $NodeMajor) { Fail "Node $NodeMajor is required (found: $(try { & node -v } catch { 'none' }))." }
144
+ Write-Info "Node $(& node -v) ready"
145
+
146
+ # Locate the repo: walk up from this script first (in-clone use), then from
147
+ # the working directory (irm-standalone use).
148
+ function Find-RepoRoot([string]$StartDir) {
149
+ $dir = $StartDir
150
+ while ($dir -and (Test-Path $dir)) {
151
+ if (Test-Path (Join-Path $dir 'starters\docker\compose.infra.yml')) { return $dir }
152
+ $parent = Split-Path -Parent $dir
153
+ if (-not $parent -or $parent -eq $dir) { return $null }
154
+ $dir = $parent
155
+ }
156
+ return $null
157
+ }
158
+
159
+ $repoRoot = $null
160
+ if ($PSScriptRoot) { $repoRoot = Find-RepoRoot $PSScriptRoot }
161
+ if (-not $repoRoot) { $repoRoot = Find-RepoRoot (Get-Location).Path }
162
+
163
+ if ($repoRoot) {
164
+ & node (Join-Path $repoRoot 'packages\starter\bin\om-start.mjs') @args
165
+ exit $LASTEXITCODE
166
+ }
167
+
168
+ # Standalone: no clone yet — npx fetches the published starter, which clones
169
+ # and continues. --use-system-ca keeps npm's TLS working behind corporate
170
+ # interception (the GPO-deployed CA lives in the Windows store).
171
+ Write-Info 'No checkout found — bootstrapping via npx @open-mercato/starter ...'
172
+ $env:NODE_OPTIONS = "$($env:NODE_OPTIONS) --use-system-ca".Trim()
173
+ & npx --yes '@open-mercato/starter' @args
174
+ exit $LASTEXITCODE
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bash
2
+ # Open Mercato starter — macOS/Linux bootstrap.
3
+ #
4
+ # The only job of this script is to guarantee Node 24, then hand off to the
5
+ # cross-platform starter CLI (packages/starter/bin/om-start.mjs), which does
6
+ # everything else: doctor, corporate TLS trust, env/secrets, install, infra
7
+ # containers, database, and the supervised dev runtime.
8
+ #
9
+ # Inside a clone: ./packages/starter/platform/start.sh [command] [flags]
10
+ # Standalone: curl -fsSL https://raw.githubusercontent.com/open-mercato/open-mercato/main/packages/starter/platform/start.sh | bash
11
+ #
12
+ # Env: OM_NODE_DIST_MIRROR — base URL mirroring https://nodejs.org/dist for
13
+ # proxy-blocked networks (defaults to the official CDN).
14
+ set -euo pipefail
15
+
16
+ NODE_MAJOR=24
17
+ NODE_DIST_BASE="${OM_NODE_DIST_MIRROR:-https://nodejs.org/dist}"
18
+
19
+ log() { printf '\033[36m[starter]\033[0m %s\n' "$*"; }
20
+ warn() { printf '\033[33m[starter]\033[0m %s\n' "$*" >&2; }
21
+ fail() { printf '\033[31m[starter]\033[0m %s\n' "$*" >&2; exit 1; }
22
+
23
+ # Two-tone Open Mercato wordmark (7-bit ASCII — safe on any terminal).
24
+ if [ -t 1 ]; then
25
+ printf '\n'
26
+ printf '\033[2;36m ___ ____ _____ _ _ \033[0m\033[36m __ __ _____ ____ ____ _ _____ ___\033[0m\n'
27
+ printf '\033[2;36m / _ \\| _ \\| ____| \\ | |\033[0m\033[36m | \\/ | ____| _ \\ / ___| / \\|_ _/ _ \\\033[0m\n'
28
+ printf '\033[2;36m | | | | |_) | _| | \\| |\033[0m\033[36m | |\\/| | _| | |_) | | / _ \\ | || | | |\033[0m\n'
29
+ printf '\033[2;36m | |_| | __/| |___| |\\ |\033[0m\033[36m | | | | |___| _ <| |___ / ___ \\| || |_| |\033[0m\n'
30
+ printf '\033[2;36m \\___/|_| |_____|_| \\_|\033[0m\033[36m |_| |_|_____|_| \\_\\\\____/_/ \\_\\_| \\___/\033[0m\n'
31
+ printf '\n \033[2mplatform bootstrap - guarantees Node %s, then hands off to the starter CLI\033[0m\n\n' "$NODE_MAJOR"
32
+ fi
33
+
34
+ OS="$(uname -s)"
35
+ case "$OS" in
36
+ Darwin|Linux) ;;
37
+ *) fail "Unsupported OS: $OS. On Windows run packages\\starter\\platform\\start.cmd instead." ;;
38
+ esac
39
+
40
+ ARCH="$(uname -m)"
41
+ case "$ARCH" in
42
+ x86_64|amd64) NODE_ARCH="x64" ;;
43
+ arm64|aarch64) NODE_ARCH="arm64" ;;
44
+ *) fail "Unsupported CPU architecture: $ARCH" ;;
45
+ esac
46
+
47
+ node_major() { command -v node >/dev/null 2>&1 && node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0; }
48
+
49
+ install_node() {
50
+ if [ "$OS" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
51
+ log "Installing Node $NODE_MAJOR via Homebrew ..."
52
+ brew install "node@$NODE_MAJOR"
53
+ BREW_NODE="$(brew --prefix "node@$NODE_MAJOR")/bin"
54
+ export PATH="$BREW_NODE:$PATH"
55
+ warn "Added $BREW_NODE to PATH for this session — persist it in your shell profile."
56
+ return
57
+ fi
58
+ # Distro-agnostic official tarball into the user's home — no sudo, checksum
59
+ # verified. curl uses the system trust store, so a corporate CA deployed by
60
+ # IT is honored automatically.
61
+ NODE_PREFIX="$HOME/.local/share/open-mercato/node"
62
+ OS_SLUG=$([ "$OS" = "Darwin" ] && echo darwin || echo linux)
63
+ log "Resolving latest Node $NODE_MAJOR release ..."
64
+ NODE_VERSION="$(curl -fsSL "$NODE_DIST_BASE/index.json" | grep -o "\"v$NODE_MAJOR\.[0-9]*\.[0-9]*\"" | head -1 | tr -d '"')"
65
+ [ -n "$NODE_VERSION" ] || fail "Could not resolve a Node $NODE_MAJOR version from $NODE_DIST_BASE. Behind a proxy? Set HTTPS_PROXY, or point OM_NODE_DIST_MIRROR at an internal mirror."
66
+ TARBALL="node-$NODE_VERSION-$OS_SLUG-$NODE_ARCH.tar.gz"
67
+ TMP_DIR="$(mktemp -d)"
68
+ trap 'rm -rf "$TMP_DIR"' EXIT
69
+ log "Downloading $TARBALL ..."
70
+ curl -fsSL "$NODE_DIST_BASE/$NODE_VERSION/$TARBALL" -o "$TMP_DIR/$TARBALL"
71
+ curl -fsSL "$NODE_DIST_BASE/$NODE_VERSION/SHASUMS256.txt" -o "$TMP_DIR/SHASUMS256.txt"
72
+ (cd "$TMP_DIR" && { command -v sha256sum >/dev/null 2>&1 && grep " $TARBALL\$" SHASUMS256.txt | sha256sum -c - ; } || { command -v shasum >/dev/null 2>&1 && grep " $TARBALL\$" SHASUMS256.txt | shasum -a 256 -c - ; }) \
73
+ || fail "Checksum verification failed for $TARBALL."
74
+ mkdir -p "$NODE_PREFIX"
75
+ tar -xzf "$TMP_DIR/$TARBALL" -C "$NODE_PREFIX" --strip-components=1
76
+ export PATH="$NODE_PREFIX/bin:$PATH"
77
+ warn "Installed Node $NODE_VERSION to $NODE_PREFIX (PATH updated for this session)."
78
+ warn "Persist it: export PATH=\"$NODE_PREFIX/bin:\$PATH\""
79
+ }
80
+
81
+ if [ "$(node_major)" -ne "$NODE_MAJOR" ]; then
82
+ install_node
83
+ fi
84
+ [ "$(node_major)" -eq "$NODE_MAJOR" ] || fail "Node $NODE_MAJOR is required (found $(node -v 2>/dev/null || echo none))."
85
+ log "Node $(node -v) ready"
86
+
87
+ # Locate the repo: walk up from this script first (in-clone use), then from
88
+ # the working directory (curl-standalone use).
89
+ find_root_from() {
90
+ local dir="$1"
91
+ while [ "$dir" != "/" ]; do
92
+ if [ -f "$dir/starters/docker/compose.infra.yml" ]; then
93
+ printf '%s' "$dir"
94
+ return 0
95
+ fi
96
+ dir="$(dirname "$dir")"
97
+ done
98
+ return 1
99
+ }
100
+
101
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || pwd)"
102
+ REPO_ROOT="$(find_root_from "$SCRIPT_DIR" || find_root_from "$PWD" || true)"
103
+
104
+ if [ -n "${REPO_ROOT:-}" ]; then
105
+ exec node "$REPO_ROOT/packages/starter/bin/om-start.mjs" "$@"
106
+ fi
107
+
108
+ # Standalone: no clone yet — npx fetches the published starter, which clones
109
+ # and continues. --use-system-ca keeps this working behind corporate TLS
110
+ # interception when the CA is in the system store.
111
+ log "No checkout found — bootstrapping via npx @open-mercato/starter ..."
112
+ NODE_OPTIONS="${NODE_OPTIONS:-} --use-system-ca" exec npx --yes @open-mercato/starter "$@"