mercury-tech-cli 1.0.0-beta.2 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # mercury-tech-cli
2
2
 
3
- The `mercury` command from npm or bun.
3
+ The `mercury` command from npm or bun, on macOS (Apple silicon and Intel), Linux x64 and
4
+ Windows x64.
4
5
 
5
6
  ```sh
6
7
  bun install -g mercury-tech-cli # or: npm install -g mercury-tech-cli
@@ -11,10 +12,12 @@ Mercury runs on its own bundled runtime, never on npm's or bun's engine. The
11
12
  first `mercury` run downloads the release archive for your machine from
12
13
  [GitHub Releases](https://github.com/Whq02/MercuryCLI/releases), verifies its
13
14
  SHA-256, and runs the archive's own user-local installer (no administrator
14
- access: `~/.mercury/versions/` and `~/.local/bin/mercury`). Later runs go
15
- straight to the installed command; `mercury update` moves between versions.
15
+ access: `~/.mercury/versions/` and `~/.local/bin/mercury`; on Windows
16
+ `%LOCALAPPDATA%\Mercury\bin`). Later runs go straight to the installed
17
+ command; `mercury update` moves between versions.
16
18
 
17
- macOS and Linux. On Windows use PowerShell:
19
+ No Intel Mac build in this release; the command says so plainly. The
20
+ PowerShell installer is the other Windows route:
18
21
 
19
22
  ```powershell
20
23
  irm https://mercury-cli.ai/install.ps1 | iex
package/bin/mercury.js ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+ // mercury-tech-cli — the `mercury` command from npm or bun, on macOS (Apple
4
+ // silicon and Intel), Linux x64 and Windows x64.
5
+ //
6
+ // Mercury runs on its own bundled runtime, never on npm's or bun's engine, so
7
+ // this launcher only fetches: on first run it downloads the release archive
8
+ // for this machine from GitHub Releases, verifies its SHA-256, unpacks it,
9
+ // and runs the archive's own `mercury install` (user-local, no administrator
10
+ // access: ~/.mercury/versions + ~/.local/bin/mercury; on Windows
11
+ // %LOCALAPPDATA%\Mercury\bin\mercury.cmd). Every later run hands straight to
12
+ // the installed command. Rerunning is safe.
13
+ //
14
+ // Knobs (all optional):
15
+ // MERCURY_VERSION a release tag to install instead of this package's
16
+ // MERCURY_INSTALL_BASE_URL where the assets live (https:// or file://)
17
+ // MERCURY_INSTALL_ARGS extra `mercury install` args, e.g. --force
18
+ // MERCURYCLI_PIN=1 run this package's release even when another
19
+ // version is installed and active (by default an
20
+ // installed Mercury is never downgraded; `mercury
21
+ // update` moves versions)
22
+ const fs = require('node:fs')
23
+ const os = require('node:os')
24
+ const path = require('node:path')
25
+ const crypto = require('node:crypto')
26
+ const { spawn, spawnSync } = require('node:child_process')
27
+
28
+ const PACKAGE_RELEASE = '1.0.0-beta.3'
29
+ const REPO = 'Whq02/MercuryCLI'
30
+ const WIN = process.platform === 'win32'
31
+ const release = (process.env.MERCURY_VERSION || PACKAGE_RELEASE).replace(/^v/, '')
32
+ const tag = `v${release}`
33
+
34
+ // The same roots the product's own stable command resolves.
35
+ const versionsDir = process.env.MERCURY_VERSIONS_DIR
36
+ || path.join(process.env.MERCURY_CONFIG_DIR || process.env.MERCURY_HOME || path.join(os.homedir(), '.mercury'), 'versions')
37
+ const stable = WIN
38
+ ? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'Mercury', 'bin', 'mercury.cmd')
39
+ : path.join(os.homedir(), '.local', 'bin', 'mercury')
40
+
41
+ function say(msg) { process.stderr.write(`mercury: ${msg}\n`) }
42
+ function die(msg) { say(msg); process.exit(1) }
43
+
44
+ function installedVersion() {
45
+ try { return fs.readFileSync(path.join(versionsDir, 'current.txt'), 'utf8').split(/\r?\n/)[0].trim() } catch { return '' }
46
+ }
47
+ function stableExists() {
48
+ try { fs.accessSync(stable, WIN ? fs.constants.F_OK : fs.constants.X_OK); return true } catch { return false }
49
+ }
50
+
51
+ // Windows: a .cmd runs only through cmd.exe. Each argument is quoted the way
52
+ // the C runtime unquotes it; cmd's /s strips the outer pair around the line.
53
+ function winQuote(arg) {
54
+ if (arg !== '' && !/[\s"]/.test(arg)) return arg
55
+ return '"' + arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1') + '"'
56
+ }
57
+ function start(cmd, args, stdio) {
58
+ if (WIN) {
59
+ const line = [cmd, ...args].map(winQuote).join(' ')
60
+ return spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', `"${line}"`], { stdio, windowsVerbatimArguments: true })
61
+ }
62
+ return spawn(cmd, args, { stdio })
63
+ }
64
+
65
+ function handOver(args) {
66
+ const child = start(stable, args, 'inherit')
67
+ // Ctrl+C reaches Mercury directly; this launcher only waits for it.
68
+ const hold = () => {}
69
+ process.on('SIGINT', hold)
70
+ process.on('SIGTERM', hold)
71
+ child.on('error', e => die(`could not start ${stable}: ${e.message}`))
72
+ child.on('exit', (code, signal) => process.exit(code === null ? (signal ? 1 : 0) : code))
73
+ }
74
+
75
+ function target() {
76
+ const { platform, arch } = process
77
+ if (platform === 'darwin') {
78
+ // A x64 node under Rosetta still runs on Apple silicon: ask the kernel.
79
+ const apple = arch === 'arm64'
80
+ || (spawnSync('sysctl', ['-n', 'hw.optional.arm64'], { encoding: 'utf8' }).stdout || '').trim() === '1'
81
+ if (apple) return 'macos-arm64'
82
+ return 'macos-x64'
83
+ }
84
+ if (platform === 'linux' && arch === 'x64') return 'linux-x64'
85
+ if (platform === 'linux' && arch === 'arm64') die(`no Linux arm64 build yet — build from source: https://github.com/${REPO}`)
86
+ if (platform === 'win32') {
87
+ if (arch === 'arm64') say('no Windows arm64 build yet — installing the x64 build (runs under emulation)')
88
+ return 'windows-x64'
89
+ }
90
+ die(`no build for ${platform} ${arch} — build from source: https://github.com/${REPO}`)
91
+ }
92
+
93
+ async function fetchBytes(url) {
94
+ if (url.startsWith('file://')) return fs.readFileSync(new URL(url))
95
+ let last
96
+ for (let attempt = 1; attempt <= 3; attempt++) {
97
+ try {
98
+ const res = await fetch(url, { redirect: 'follow' })
99
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
100
+ return Buffer.from(await res.arrayBuffer())
101
+ } catch (e) {
102
+ last = e
103
+ await new Promise(r => setTimeout(r, 1000 * attempt))
104
+ }
105
+ }
106
+ throw last
107
+ }
108
+
109
+ function unpack(archive, dir) {
110
+ if (WIN) {
111
+ // Windows 10 1803+ ships tar.exe (bsdtar), which reads zip; PowerShell is the fallback.
112
+ if (spawnSync('tar', ['-xf', archive, '-C', dir], { stdio: 'ignore' }).status === 0) return
113
+ const q = s => `'${s.replace(/'/g, "''")}'`
114
+ const ps = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command',
115
+ `Expand-Archive -LiteralPath ${q(archive)} -DestinationPath ${q(dir)} -Force`], { stdio: 'ignore' })
116
+ if (ps.status === 0) return
117
+ die('could not unpack the archive: neither tar nor PowerShell Expand-Archive worked')
118
+ }
119
+ const tar = spawnSync('tar', ['-xzf', archive, '-C', dir], { stdio: ['ignore', 'ignore', 'inherit'] })
120
+ if (tar.status !== 0) die('could not unpack the archive: first run needs tar')
121
+ }
122
+
123
+ // The installer's chatter goes to stderr: stdout stays Mercury's own.
124
+ function runInstaller(launcher, extra) {
125
+ return new Promise(resolve => {
126
+ const child = start(launcher, ['install', ...extra], ['inherit', 'pipe', 'inherit'])
127
+ child.stdout.pipe(process.stderr)
128
+ child.on('error', e => die(`could not run the installer: ${e.message}`))
129
+ child.on('exit', code => resolve(code === null ? 1 : code))
130
+ })
131
+ }
132
+
133
+ async function main() {
134
+ const args = process.argv.slice(2)
135
+ const installed = installedVersion()
136
+ if (stableExists() && (installed === release || (installed !== '' && process.env.MERCURYCLI_PIN !== '1'))) {
137
+ handOver(args)
138
+ return
139
+ }
140
+
141
+ const t = target()
142
+ const asset = `mercury-${tag}-${t}.${WIN ? 'zip' : 'tar.gz'}`
143
+ const base = process.env.MERCURY_INSTALL_BASE_URL || `https://github.com/${REPO}/releases/download/${tag}`
144
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mercury-tech-cli-'))
145
+ const cleanup = () => { try { fs.rmSync(tmp, { recursive: true, force: true }) } catch {} }
146
+ process.on('exit', cleanup)
147
+
148
+ say(`first run — installing Mercury ${tag} for ${t}`)
149
+ let bytes, sums
150
+ try { bytes = await fetchBytes(`${base}/${asset}`) } catch (e) { die(`download failed: ${base}/${asset} (${e.message})`) }
151
+ try { sums = (await fetchBytes(`${base}/SHA256SUMS.txt`)).toString('utf8') } catch (e) { die(`download failed: ${base}/SHA256SUMS.txt (${e.message})`) }
152
+ const want = sums.split(/\r?\n/)
153
+ .map(l => l.trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/))
154
+ .find(m => m && m[2] === asset)
155
+ if (!want) die(`${asset} is not listed in SHA256SUMS.txt`)
156
+ const got = crypto.createHash('sha256').update(bytes).digest('hex')
157
+ if (got !== want[1].toLowerCase()) die(`checksum mismatch for ${asset} — nothing installed`)
158
+ say('checksum ok')
159
+
160
+ const archive = path.join(tmp, asset)
161
+ fs.writeFileSync(archive, bytes)
162
+ unpack(archive, tmp)
163
+ const launcher = path.join(tmp, 'mercury', WIN ? 'mercury.cmd' : 'mercury')
164
+ if (!fs.existsSync(launcher)) die('the archive has no launcher')
165
+ const extra = (process.env.MERCURY_INSTALL_ARGS || '').split(' ').filter(Boolean)
166
+ const code = await runInstaller(launcher, extra)
167
+ if (code !== 0) die(`mercury install exited ${code}`)
168
+ if (!stableExists()) die(`installed, but ${stable} is missing — rerun with MERCURY_INSTALL_ARGS=--force`)
169
+ cleanup()
170
+ handOver(args)
171
+ }
172
+
173
+ main().catch(e => die(e && e.message ? e.message : String(e)))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mercury-tech-cli",
3
- "version": "1.0.0-beta.2",
4
- "description": "Mercury \u2014 a terminal harness for software development. This package installs Mercury's own runtime on first run; it never runs Mercury on npm's or bun's engine.",
3
+ "version": "1.0.0-beta.3",
4
+ "description": "Mercury a terminal harness for software development. This package installs Mercury's own runtime on first run; it never runs Mercury on npm's or bun's engine. macOS (Apple silicon and Intel), Linux x64, Windows x64.",
5
5
  "homepage": "https://mercury-cli.ai",
6
6
  "repository": {
7
7
  "type": "git",
@@ -12,12 +12,11 @@
12
12
  },
13
13
  "license": "SEE LICENSE IN LICENSE.md",
14
14
  "bin": {
15
- "mercury": "bin/mercury"
15
+ "mercury": "bin/mercury.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
16
19
  },
17
- "os": [
18
- "darwin",
19
- "linux"
20
- ],
21
20
  "files": [
22
21
  "bin",
23
22
  "README.md",
package/bin/mercury DELETED
@@ -1,55 +0,0 @@
1
- #!/bin/sh
2
- # mercury-tech-cli — the `mercury` command from npm or bun.
3
- #
4
- # Mercury runs on its own bundled runtime, never on npm's or bun's engine, so
5
- # this file is a plain shell script: on first run it downloads the release
6
- # archive for this machine from GitHub Releases, verifies its SHA-256, and
7
- # runs the archive's own `mercury install` (user-local, no administrator
8
- # access: ~/.mercury/versions/ + ~/.local/bin/mercury). Every later run
9
- # hands straight to the installed command. Rerunning is safe.
10
- set -eu
11
- PKG_VERSION="1.0.0-beta.2"
12
- REPO="Whq02/MercuryCLI"
13
- home=${MERCURY_CONFIG_DIR:-"$HOME/.mercury"}
14
- current="$home/versions/current.txt"
15
- installed=""
16
- if [ -f "$current" ]; then installed=$(head -1 "$current" 2>/dev/null | tr -d '[:space:]'); fi
17
- stable="$HOME/.local/bin/mercury"
18
-
19
- if [ "$installed" = "$PKG_VERSION" ] && [ -x "$stable" ]; then
20
- exec "$stable" "$@"
21
- fi
22
- if [ -n "$installed" ] && [ -x "$stable" ] && [ "${MERCURYCLI_PIN:-}" != "1" ]; then
23
- # A different version is installed and active: run it — `mercury update`
24
- # moves versions; this package never downgrades what you have. Set
25
- # MERCURYCLI_PIN=1 to force this package's version.
26
- exec "$stable" "$@"
27
- fi
28
-
29
- die() { printf 'mercury: %s\n' "$*" >&2; exit 1; }
30
- command -v curl >/dev/null 2>&1 || die "first run needs 'curl'"
31
- command -v tar >/dev/null 2>&1 || die "first run needs 'tar'"
32
- os=$(uname -s); arch=$(uname -m)
33
- case "$os/$arch" in
34
- Darwin/arm64) target=macos-arm64 ;;
35
- Darwin/x86_64) die "no Intel Mac build in this release (Apple silicon, Linux x64 and Windows x64 ship) — build from source: https://github.com/$REPO" ;;
36
- Linux/x86_64|Linux/amd64) target=linux-x64 ;;
37
- *) die "no build for $os $arch — Windows: irm https://mercury-cli.ai/install.ps1 | iex ; others: build from source at https://github.com/$REPO" ;;
38
- esac
39
- version="v$PKG_VERSION"
40
- asset="mercury-$version-$target.tar.gz"
41
- base=${MERCURY_INSTALL_BASE_URL:-"https://github.com/$REPO/releases/download/$version"}
42
- tmp=$(mktemp -d "${TMPDIR:-/tmp}/mercury-tech-cli.XXXXXX")
43
- trap 'rm -rf "$tmp"' EXIT
44
- printf 'mercury: first run — installing Mercury %s for %s\n' "$version" "$target" >&2
45
- curl -fsSL --retry 3 -o "$tmp/$asset" "$base/$asset" || die "download failed: $base/$asset"
46
- curl -fsSL --retry 3 -o "$tmp/SHA256SUMS.txt" "$base/SHA256SUMS.txt" || die "download failed: $base/SHA256SUMS.txt"
47
- want=$(grep " $asset\$" "$tmp/SHA256SUMS.txt" | awk '{print $1}' | head -1)
48
- [ -n "$want" ] || die "$asset is not listed in SHA256SUMS.txt"
49
- if command -v sha256sum >/dev/null 2>&1; then got=$(sha256sum "$tmp/$asset" | awk '{print $1}'); else got=$(shasum -a 256 "$tmp/$asset" | awk '{print $1}'); fi
50
- [ "$got" = "$want" ] || die "checksum mismatch for $asset — nothing installed"
51
- tar -xzf "$tmp/$asset" -C "$tmp"
52
- [ -x "$tmp/mercury/mercury" ] || die "the archive has no launcher"
53
- "$tmp/mercury/mercury" install >&2
54
- [ -x "$stable" ] || die "installed, but $stable is missing — run: $tmp/mercury/mercury install --force"
55
- exec "$stable" "$@"