mercury-tech-cli 1.0.0-beta.10
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/LICENSE.md +2 -0
- package/README.md +26 -0
- package/bin/mercury.js +173 -0
- package/package.json +32 -0
package/LICENSE.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# mercury-tech-cli
|
|
2
|
+
|
|
3
|
+
The `mercury` command from npm or bun, on macOS (Apple silicon and Intel), Linux x64 and
|
|
4
|
+
Windows x64.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
bun install -g mercury-tech-cli # or: npm install -g mercury-tech-cli
|
|
8
|
+
mercury
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Mercury runs on its own bundled runtime, never on npm's or bun's engine. The
|
|
12
|
+
first `mercury` run downloads the release archive for your machine from
|
|
13
|
+
[GitHub Releases](https://github.com/Whq02/MercuryCLI/releases), verifies its
|
|
14
|
+
SHA-256, and runs the archive's own user-local installer (no administrator
|
|
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.
|
|
18
|
+
|
|
19
|
+
No Intel Mac build in this release; the command says so plainly. The
|
|
20
|
+
PowerShell installer is the other Windows route:
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
irm https://mercury-cli.ai/install.ps1 | iex
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Website: https://mercury-cli.ai · Source and issues: https://github.com/Whq02/MercuryCLI
|
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.10'
|
|
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
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mercury-tech-cli",
|
|
3
|
+
"version": "1.0.0-beta.10",
|
|
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
|
+
"homepage": "https://mercury-cli.ai",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Whq02/MercuryCLI.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/Whq02/MercuryCLI/issues"
|
|
12
|
+
},
|
|
13
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
14
|
+
"bin": {
|
|
15
|
+
"mercury": "bin/mercury.js"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE.md"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mercury",
|
|
27
|
+
"cli",
|
|
28
|
+
"terminal",
|
|
29
|
+
"agent",
|
|
30
|
+
"harness"
|
|
31
|
+
]
|
|
32
|
+
}
|