@liustack/modlens 2.8.0 → 3.1.0
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 +194 -0
- package/README.md +31 -72
- package/README.zh-CN.md +46 -87
- package/SECURITY.md +17 -0
- package/dist/main.js +801 -401
- package/docs/commit.md +76 -0
- package/docs/harness-setup.md +49 -0
- package/docs/research-gemini-claude-skills.md +48 -0
- package/docs/security.md +31 -0
- package/docs/testing.md +29 -0
- package/docs/troubleshooting.md +140 -0
- package/package.json +23 -12
- package/skills/modlens/SKILL.md +23 -8
- package/skills/modlens/references/cli.md +77 -0
- package/skills/modlens/references/configure.md +36 -0
- package/skills/modlens/references/runtime.md +95 -0
- package/skills/modlens/scripts/run.ps1 +250 -0
- package/skills/modlens/scripts/run.sh +283 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# modlens runtime reference
|
|
2
|
+
|
|
3
|
+
How the skill launches the `modlens` CLI, what version it pins, and how it
|
|
4
|
+
diagnoses a machine where nothing can run. The launchers `scripts/run.sh`
|
|
5
|
+
(macOS / Linux) and `scripts/run.ps1` (Windows) implement everything below and
|
|
6
|
+
must stay byte-for-byte identical apart from their version constants and their
|
|
7
|
+
shell syntax.
|
|
8
|
+
|
|
9
|
+
## Pinned version
|
|
10
|
+
|
|
11
|
+
- Pinned CLI version: 3.1.0
|
|
12
|
+
- npm package: `@liustack/modlens`
|
|
13
|
+
- CLI binary name: `modlens`
|
|
14
|
+
|
|
15
|
+
The pinned version line above and the constants inside both launchers are
|
|
16
|
+
stamped by `scripts/release.mjs` at release time from `package.json`. Do not
|
|
17
|
+
edit them by hand. `scripts/stamp.test.mjs` fails the build if the three
|
|
18
|
+
launcher/reference copies ever drift from `package.json`.
|
|
19
|
+
|
|
20
|
+
## Resolution order
|
|
21
|
+
|
|
22
|
+
Each call resolves a way to run the CLI, in this order:
|
|
23
|
+
|
|
24
|
+
1. **A compatible `modlens` already on `PATH`** — run it directly, by name.
|
|
25
|
+
2. **`npx` present** — `npx --yes --package @liustack/modlens@<pinned> modlens <args>`.
|
|
26
|
+
3. **`bunx` present** — `bunx --bun @liustack/modlens@<pinned> <args>`.
|
|
27
|
+
4. **A native artifact** — reserved for phase B. None is published yet, so this
|
|
28
|
+
branch reports `nativeArtifact.available: false` and moves on.
|
|
29
|
+
5. **Nothing usable** — print a structured diagnosis and exit `78` (`EX_CONFIG`).
|
|
30
|
+
|
|
31
|
+
The launcher forwards stdin, stdout, stderr, and the exit code unchanged, so the
|
|
32
|
+
CLI's JSON output contract is identical however it was launched.
|
|
33
|
+
|
|
34
|
+
## Compatibility rule
|
|
35
|
+
|
|
36
|
+
A `modlens` found on `PATH` counts as compatible only when it is **the same
|
|
37
|
+
major version as the pinned version and not older than it**. Same major keeps a
|
|
38
|
+
user who already installed a matching CLI from being forced through an `npx`
|
|
39
|
+
re-download (the "no regression" requirement in the design). Not-older refuses a
|
|
40
|
+
stale global build that predates the version this skill was written against; in
|
|
41
|
+
that case the launcher skips `PATH` and uses the pinned `npx` / `bunx` version
|
|
42
|
+
instead.
|
|
43
|
+
|
|
44
|
+
## Cache and permissions (phase B, not active yet)
|
|
45
|
+
|
|
46
|
+
Phase A never downloads anything. When native artifacts land in phase B, the
|
|
47
|
+
launchers will cache them per user, keyed by version, and launch them by
|
|
48
|
+
absolute path:
|
|
49
|
+
|
|
50
|
+
- macOS: `~/Library/Caches/liustack/modlens/<version>/`
|
|
51
|
+
- Linux: `${XDG_CACHE_HOME:-$HOME/.cache}/liustack/modlens/<version>/`
|
|
52
|
+
- Windows: `%LOCALAPPDATA%\liustack\modlens\<version>\`
|
|
53
|
+
|
|
54
|
+
With these constraints: no `sudo` or admin rights, no system directories, no
|
|
55
|
+
`PATH` edits, download to a temp file and verify SHA-256 before an atomic move,
|
|
56
|
+
and keep no unverified executable on failure. Any download uses `curl` (on
|
|
57
|
+
Windows, `curl.exe` written in full), which does not stamp quarantine or
|
|
58
|
+
Mark-of-the-Web, and the launcher never removes a security marker a browser
|
|
59
|
+
would have set.
|
|
60
|
+
|
|
61
|
+
## Diagnostic fields
|
|
62
|
+
|
|
63
|
+
`run.sh doctor --json` (and `run.ps1 doctor --json`) print this shape:
|
|
64
|
+
|
|
65
|
+
- `tool`, `package`, `pinnedVersion` — what this skill targets.
|
|
66
|
+
- `os`, `arch` — normalized host identity (`darwin` / `linux` / `windows`,
|
|
67
|
+
`arm64` / `x64`).
|
|
68
|
+
- `checked.pathCli` — `{ present, path, version, compatible }` for a `modlens`
|
|
69
|
+
on `PATH`, with `compatible` applying the rule above.
|
|
70
|
+
- `checked.npx`, `checked.bunx` — `{ present, path }` visibility of each runner.
|
|
71
|
+
- `checked.node` — `{ present, version }`.
|
|
72
|
+
- `nativeArtifact` — `{ available, note }`; `available` is `false` in phase A.
|
|
73
|
+
- `selected` — the resolved path: `path`, `npx`, `bunx`, or `none`.
|
|
74
|
+
- `nextSteps` — when `selected` is `none`, one or two plain-language actions for
|
|
75
|
+
the user (install Node 22.13+, or Bun); empty otherwise.
|
|
76
|
+
- `cliDoctor` — when a CLI is resolvable, the CLI's own `doctor --json` report
|
|
77
|
+
(provider, config, and harness diagnosis) is nested here; `null` otherwise.
|
|
78
|
+
|
|
79
|
+
`doctor` is offline and spends no quota: it inspects the local environment and,
|
|
80
|
+
when it can, chains the CLI's own offline `doctor`. It makes no network request
|
|
81
|
+
of its own.
|
|
82
|
+
|
|
83
|
+
## Delivery form: local CLI, long term
|
|
84
|
+
|
|
85
|
+
modlens stays a local CLI on purpose, and section 10 of the distribution design
|
|
86
|
+
(move capabilities to a remote MCP when they need no local execution) does
|
|
87
|
+
**not** apply to it. The reasons are the product itself: the vision-provider key
|
|
88
|
+
is held on the user's machine, the quota billed is the user's own, and there is
|
|
89
|
+
no central service in the middle. modlens also reads local files directly. Its
|
|
90
|
+
`recover-paste` pulls pasted images out of the harness's own session storage on
|
|
91
|
+
disk, which a remote service structurally cannot reach. A hosted MCP would move
|
|
92
|
+
the key and the quota off the user's machine and still could not see those local
|
|
93
|
+
files, which is the opposite of what this tool is for. Phase D may retire native
|
|
94
|
+
artifacts for some future tool, but modlens keeps its local-CLI form for as long
|
|
95
|
+
as those properties hold.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# modlens skill launcher (Windows, PowerShell 5.1 compatible).
|
|
2
|
+
#
|
|
3
|
+
# The Windows twin of run.sh: identical resolution order, identical diagnostic
|
|
4
|
+
# fields, identical exit codes. One stable action for the agent ("run
|
|
5
|
+
# modlens"); this script picks a working way to run it here.
|
|
6
|
+
#
|
|
7
|
+
# Invoke it per-process so no global policy is touched:
|
|
8
|
+
# powershell -ExecutionPolicy Bypass -File run.ps1 -q "test"
|
|
9
|
+
#
|
|
10
|
+
# Resolution order (kept identical in run.sh):
|
|
11
|
+
# 1. A compatible modlens already on PATH -> run it directly.
|
|
12
|
+
# 2. npx present -> run the pinned npm version.
|
|
13
|
+
# 3. bunx present -> run the pinned version via Bun.
|
|
14
|
+
# 4. (phase B placeholder) a native artifact -> not published yet.
|
|
15
|
+
# 5. Nothing usable -> structured diagnosis, exit 78.
|
|
16
|
+
#
|
|
17
|
+
# It never writes PATH, never needs admin rights, never fetches a second script,
|
|
18
|
+
# and has no postinstall step.
|
|
19
|
+
|
|
20
|
+
$ErrorActionPreference = 'Stop'
|
|
21
|
+
|
|
22
|
+
# --- Version constants: stamped by scripts/release.mjs at release time. --------
|
|
23
|
+
# Do not edit $Pinned by hand; scripts/stamp.test.mjs asserts it equals the
|
|
24
|
+
# package.json version, and the release script rewrites it on every bump.
|
|
25
|
+
$Package = '@liustack/modlens'
|
|
26
|
+
$Bin = 'modlens'
|
|
27
|
+
$Pinned = '3.1.0'
|
|
28
|
+
# -------------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
$NativeNote = 'no native artifact is published for this tool yet; phase A ships npm launch paths only'
|
|
31
|
+
|
|
32
|
+
# Environment snapshot, filled by Collect and read by the emitters.
|
|
33
|
+
$script:Arch = ''
|
|
34
|
+
$script:CliPresent = $false
|
|
35
|
+
$script:CliPath = $null
|
|
36
|
+
$script:CliVer = $null
|
|
37
|
+
$script:CliCompat = $false
|
|
38
|
+
$script:NpxPresent = $false
|
|
39
|
+
$script:NpxPath = $null
|
|
40
|
+
$script:BunxPresent = $false
|
|
41
|
+
$script:BunxPath = $null
|
|
42
|
+
$script:NodePresent = $false
|
|
43
|
+
$script:NodeVer = $null
|
|
44
|
+
$script:Selected = 'none'
|
|
45
|
+
|
|
46
|
+
# First "X.Y.Z" token printed by `$Bin --version`.
|
|
47
|
+
function Get-CliVersion {
|
|
48
|
+
try { $out = & $Bin --version 2>$null } catch { return '' }
|
|
49
|
+
if (-not $out) { return '' }
|
|
50
|
+
$line = [string]($out | Select-Object -First 1)
|
|
51
|
+
$m = [regex]::Match($line, '[0-9]+\.[0-9]+\.[0-9]+')
|
|
52
|
+
if ($m.Success) { return $m.Value } else { return '' }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Compatible = same major version as $Pinned AND not older than $Pinned.
|
|
56
|
+
# Same major keeps a globally installed CLI usable without a forced re-download;
|
|
57
|
+
# not-older refuses a stale build that predates the version this skill needs.
|
|
58
|
+
function Test-Compatible {
|
|
59
|
+
param([string] $Ver)
|
|
60
|
+
$f = $Ver -split '\.'
|
|
61
|
+
$p = $Pinned -split '\.'
|
|
62
|
+
if ($f.Count -lt 3 -or $p.Count -lt 3) { return $false }
|
|
63
|
+
$fMaj = [int]$f[0]; $fMin = [int]$f[1]; $fPat = [int]$f[2]
|
|
64
|
+
$pMaj = [int]$p[0]; $pMin = [int]$p[1]; $pPat = [int]$p[2]
|
|
65
|
+
if ($fMaj -ne $pMaj) { return $false }
|
|
66
|
+
if ($fMin -gt $pMin) { return $true }
|
|
67
|
+
if ($fMin -lt $pMin) { return $false }
|
|
68
|
+
return ($fPat -ge $pPat)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Return exactly one word: the chosen launch path.
|
|
72
|
+
function Resolve-LaunchKind {
|
|
73
|
+
$cli = Get-Command $Bin -ErrorAction SilentlyContinue
|
|
74
|
+
if ($cli) {
|
|
75
|
+
$v = Get-CliVersion
|
|
76
|
+
if ($v -and (Test-Compatible $v)) { return 'path' }
|
|
77
|
+
}
|
|
78
|
+
if (Get-Command npx -ErrorAction SilentlyContinue) { return 'npx' }
|
|
79
|
+
if (Get-Command bunx -ErrorAction SilentlyContinue) { return 'bunx' }
|
|
80
|
+
# Phase B goes here: check a versioned user cache, then download and verify a
|
|
81
|
+
# native artifact into it. Any such download must use curl.exe (written in
|
|
82
|
+
# full so PowerShell 5.1 does not resolve `curl` to its Invoke-WebRequest
|
|
83
|
+
# alias), never Invoke-WebRequest, which stamps Mark-of-the-Web; see
|
|
84
|
+
# design.md 8.3.
|
|
85
|
+
return 'none'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Run the resolved CLI and return its output (used to chain the CLI's own
|
|
89
|
+
# doctor). Passes every argument through untouched.
|
|
90
|
+
function Invoke-Cli {
|
|
91
|
+
param([string[]] $CliArgs)
|
|
92
|
+
switch ($script:Selected) {
|
|
93
|
+
'path' { & $Bin @CliArgs }
|
|
94
|
+
'npx' { & npx --yes --package "$Package@$Pinned" $Bin @CliArgs }
|
|
95
|
+
'bunx' { & bunx --bun "$Package@$Pinned" @CliArgs }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function Get-Arch {
|
|
100
|
+
switch ($env:PROCESSOR_ARCHITECTURE) {
|
|
101
|
+
'AMD64' { return 'x64' }
|
|
102
|
+
'ARM64' { return 'arm64' }
|
|
103
|
+
'x86' { return 'x86' }
|
|
104
|
+
default { return $env:PROCESSOR_ARCHITECTURE }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Probe the environment once into the $script:* snapshot.
|
|
109
|
+
function Collect {
|
|
110
|
+
$script:Arch = Get-Arch
|
|
111
|
+
|
|
112
|
+
$cli = Get-Command $Bin -ErrorAction SilentlyContinue
|
|
113
|
+
if ($cli) {
|
|
114
|
+
$script:CliPresent = $true
|
|
115
|
+
$script:CliPath = $cli.Source
|
|
116
|
+
$script:CliVer = Get-CliVersion
|
|
117
|
+
$script:CliCompat = [bool]($script:CliVer -and (Test-Compatible $script:CliVer))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$npx = Get-Command npx -ErrorAction SilentlyContinue
|
|
121
|
+
if ($npx) { $script:NpxPresent = $true; $script:NpxPath = $npx.Source }
|
|
122
|
+
|
|
123
|
+
$bunx = Get-Command bunx -ErrorAction SilentlyContinue
|
|
124
|
+
if ($bunx) { $script:BunxPresent = $true; $script:BunxPath = $bunx.Source }
|
|
125
|
+
|
|
126
|
+
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
127
|
+
if ($node) {
|
|
128
|
+
$script:NodePresent = $true
|
|
129
|
+
try { $script:NodeVer = ((& node --version 2>$null) -replace '^v', '') } catch { $script:NodeVer = $null }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$script:Selected = Resolve-LaunchKind
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Assemble the structured diagnosis. $Chained, when a parsed object, becomes
|
|
136
|
+
# cliDoctor; otherwise cliDoctor is null.
|
|
137
|
+
function Build-DiagnosisJson {
|
|
138
|
+
param($Chained)
|
|
139
|
+
$checked = [ordered]@{
|
|
140
|
+
pathCli = [ordered]@{ present = $script:CliPresent; path = $script:CliPath; version = $script:CliVer; compatible = $script:CliCompat }
|
|
141
|
+
npx = [ordered]@{ present = $script:NpxPresent; path = $script:NpxPath }
|
|
142
|
+
bunx = [ordered]@{ present = $script:BunxPresent; path = $script:BunxPath }
|
|
143
|
+
node = [ordered]@{ present = $script:NodePresent; version = $script:NodeVer }
|
|
144
|
+
}
|
|
145
|
+
$steps = @()
|
|
146
|
+
if ($script:Selected -eq 'none') {
|
|
147
|
+
$major = $Pinned.Split('.')[0]
|
|
148
|
+
$steps = @(
|
|
149
|
+
"Install Node 22.13+ from https://nodejs.org so npx can run $Package@$Pinned, then re-run this launcher.",
|
|
150
|
+
"No JavaScript runtime? Install Bun from https://bun.sh to use bunx, or put a compatible $Bin (major $major, at or above $Pinned) on PATH."
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
$obj = [ordered]@{
|
|
154
|
+
tool = $Bin
|
|
155
|
+
package = $Package
|
|
156
|
+
pinnedVersion = $Pinned
|
|
157
|
+
os = 'windows'
|
|
158
|
+
arch = $script:Arch
|
|
159
|
+
checked = $checked
|
|
160
|
+
nativeArtifact = [ordered]@{ available = $false; note = $NativeNote }
|
|
161
|
+
selected = $script:Selected
|
|
162
|
+
nextSteps = @($steps)
|
|
163
|
+
cliDoctor = $Chained
|
|
164
|
+
}
|
|
165
|
+
return ($obj | ConvertTo-Json -Depth 20)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Human-readable diagnosis for `doctor` without --json.
|
|
169
|
+
function Write-DiagnosisText {
|
|
170
|
+
Write-Output "$Bin launcher diagnosis"
|
|
171
|
+
Write-Output ''
|
|
172
|
+
Write-Output (" os / arch: windows / {0}" -f $script:Arch)
|
|
173
|
+
Write-Output (" pinned version: {0} ({1})" -f $Pinned, $Package)
|
|
174
|
+
if ($script:CliPresent) {
|
|
175
|
+
$verdict = if ($script:CliCompat) { 'compatible' } else { 'incompatible' }
|
|
176
|
+
Write-Output (" {0} on PATH: {1} (version {2}, {3})" -f $Bin, $script:CliPath, $script:CliVer, $verdict)
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
Write-Output (" {0} on PATH: no" -f $Bin)
|
|
180
|
+
}
|
|
181
|
+
Write-Output (" npx: {0}" -f $(if ($script:NpxPresent) { $script:NpxPath } else { 'no' }))
|
|
182
|
+
Write-Output (" bunx: {0}" -f $(if ($script:BunxPresent) { $script:BunxPath } else { 'no' }))
|
|
183
|
+
Write-Output (" node: {0}" -f $(if ($script:NodePresent) { $script:NodeVer } else { 'no' }))
|
|
184
|
+
Write-Output (" selected path: {0}" -f $script:Selected)
|
|
185
|
+
if ($script:Selected -eq 'none') {
|
|
186
|
+
Write-Output ''
|
|
187
|
+
Write-Output ("No runtime can launch {0} here. {1}" -f $Bin, $NativeNote)
|
|
188
|
+
Write-Output 'Next steps:'
|
|
189
|
+
Write-Output ' - Install Node 22.13+ from https://nodejs.org, then re-run this launcher.'
|
|
190
|
+
Write-Output (" - Or install Bun from https://bun.sh, or put a compatible {0} on PATH." -f $Bin)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# `doctor [--json] [extra...]`: launcher selection diagnosis. When a CLI is
|
|
195
|
+
# resolvable, chain the CLI's own doctor so one call reports both layers. Extra
|
|
196
|
+
# flags pass through to the chained CLI doctor.
|
|
197
|
+
function Invoke-Doctor {
|
|
198
|
+
param([string[]] $DocArgs)
|
|
199
|
+
Collect
|
|
200
|
+
$json = $false
|
|
201
|
+
foreach ($a in $DocArgs) { if ($a -eq '--json') { $json = $true } }
|
|
202
|
+
if ($json) {
|
|
203
|
+
$chained = $null
|
|
204
|
+
if ($script:Selected -ne 'none') {
|
|
205
|
+
try {
|
|
206
|
+
$raw = (Invoke-Cli -CliArgs (@('doctor') + $DocArgs) 2>$null | Out-String).Trim()
|
|
207
|
+
if ($raw.StartsWith('{')) { $chained = ($raw | ConvertFrom-Json) }
|
|
208
|
+
}
|
|
209
|
+
catch { $chained = $null }
|
|
210
|
+
}
|
|
211
|
+
Write-Output (Build-DiagnosisJson $chained)
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
Write-DiagnosisText
|
|
215
|
+
if ($script:Selected -ne 'none') {
|
|
216
|
+
Write-Output ''
|
|
217
|
+
Write-Output "--- $Bin doctor ---"
|
|
218
|
+
Invoke-Cli -CliArgs (@('doctor') + $DocArgs)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Default action: forward every argument to the resolved CLI and exit with its
|
|
224
|
+
# code. No usable runtime -> structured diagnosis on stderr, exit 78 (EX_CONFIG)
|
|
225
|
+
# so the agent never mistakes the diagnosis for a result.
|
|
226
|
+
function Invoke-Run {
|
|
227
|
+
param([string[]] $CliArgs)
|
|
228
|
+
$sel = Resolve-LaunchKind
|
|
229
|
+
switch ($sel) {
|
|
230
|
+
'path' { & $Bin @CliArgs; exit $LASTEXITCODE }
|
|
231
|
+
'npx' { & npx --yes --package "$Package@$Pinned" $Bin @CliArgs; exit $LASTEXITCODE }
|
|
232
|
+
'bunx' { & bunx --bun "$Package@$Pinned" @CliArgs; exit $LASTEXITCODE }
|
|
233
|
+
'none' {
|
|
234
|
+
Collect
|
|
235
|
+
[Console]::Error.WriteLine((Build-DiagnosisJson $null))
|
|
236
|
+
exit 78
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
$Command = ''
|
|
242
|
+
if ($args.Count -ge 1) { $Command = [string]$args[0] }
|
|
243
|
+
$Rest = @()
|
|
244
|
+
if ($args.Count -gt 1) { $Rest = $args[1..($args.Count - 1)] }
|
|
245
|
+
|
|
246
|
+
switch ($Command) {
|
|
247
|
+
'doctor' { Invoke-Doctor -DocArgs $Rest }
|
|
248
|
+
'where' { Collect; Write-Output $script:Selected }
|
|
249
|
+
default { Invoke-Run -CliArgs $args }
|
|
250
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# modlens skill launcher (macOS / Linux).
|
|
3
|
+
#
|
|
4
|
+
# One stable action for the agent ("run modlens"); this script picks a working
|
|
5
|
+
# way to run it in the current environment. Written to POSIX sh so it runs under
|
|
6
|
+
# dash, busybox ash, and bash alike. Invoke it with `bash run.sh ...` (or plain
|
|
7
|
+
# `sh run.sh ...`) so a lost execute bit after a file copy never matters.
|
|
8
|
+
#
|
|
9
|
+
# Resolution order (kept identical in run.ps1):
|
|
10
|
+
# 1. A compatible modlens already on PATH -> run it directly.
|
|
11
|
+
# 2. npx present -> run the pinned npm version.
|
|
12
|
+
# 3. bunx present -> run the pinned version via Bun.
|
|
13
|
+
# 4. (phase B placeholder) a native artifact -> not published yet.
|
|
14
|
+
# 5. Nothing usable -> structured diagnosis, exit 78.
|
|
15
|
+
#
|
|
16
|
+
# It never writes PATH, never needs admin rights, never fetches a second script,
|
|
17
|
+
# and has no postinstall step.
|
|
18
|
+
set -eu
|
|
19
|
+
|
|
20
|
+
# --- Version constants: stamped by scripts/release.mjs at release time. --------
|
|
21
|
+
# Do not edit PINNED by hand; scripts/stamp.test.mjs asserts it equals the
|
|
22
|
+
# package.json version, and the release script rewrites it on every bump.
|
|
23
|
+
PKG="@liustack/modlens"
|
|
24
|
+
BIN="modlens"
|
|
25
|
+
PINNED="3.1.0"
|
|
26
|
+
# -------------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|
|
29
|
+
|
|
30
|
+
# Split "X.Y.Z" (extra suffix ignored) into the globals _MAJ, _MIN, _PAT.
|
|
31
|
+
# Any non-numeric component becomes 0 so integer tests below never abort.
|
|
32
|
+
parse_semver() {
|
|
33
|
+
_raw="$1"
|
|
34
|
+
_MAJ="${_raw%%.*}"
|
|
35
|
+
_rest="${_raw#*.}"
|
|
36
|
+
if [ "$_rest" = "$_raw" ]; then
|
|
37
|
+
_MIN=0
|
|
38
|
+
_PAT=0
|
|
39
|
+
else
|
|
40
|
+
_MIN="${_rest%%.*}"
|
|
41
|
+
_rest2="${_rest#*.}"
|
|
42
|
+
if [ "$_rest2" = "$_rest" ]; then _PAT=0; else _PAT="${_rest2%%.*}"; fi
|
|
43
|
+
fi
|
|
44
|
+
case "$_MAJ" in '' | *[!0-9]*) _MAJ=0 ;; esac
|
|
45
|
+
case "$_MIN" in '' | *[!0-9]*) _MIN=0 ;; esac
|
|
46
|
+
case "$_PAT" in '' | *[!0-9]*) _PAT=0 ;; esac
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Compatible = same major version as PINNED AND not older than PINNED.
|
|
50
|
+
# Same major keeps a globally installed CLI usable without a forced re-download;
|
|
51
|
+
# not-older refuses a stale build that predates the version this skill needs.
|
|
52
|
+
compatible() {
|
|
53
|
+
parse_semver "$1"
|
|
54
|
+
_f_maj=$_MAJ
|
|
55
|
+
_f_min=$_MIN
|
|
56
|
+
_f_pat=$_PAT
|
|
57
|
+
parse_semver "$PINNED"
|
|
58
|
+
[ "$_f_maj" = "$_MAJ" ] || return 1
|
|
59
|
+
if [ "$_f_min" -gt "$_MIN" ]; then return 0; fi
|
|
60
|
+
if [ "$_f_min" -lt "$_MIN" ]; then return 1; fi
|
|
61
|
+
[ "$_f_pat" -ge "$_PAT" ]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# First "X.Y.Z" token printed by `$BIN --version`.
|
|
65
|
+
cli_version() {
|
|
66
|
+
"$BIN" --version 2>/dev/null | head -n 1 |
|
|
67
|
+
sed -n 's/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Echo exactly one word: the chosen launch path.
|
|
71
|
+
resolve() {
|
|
72
|
+
if command -v "$BIN" >/dev/null 2>&1; then
|
|
73
|
+
_v="$(cli_version)"
|
|
74
|
+
if [ -n "$_v" ] && compatible "$_v"; then
|
|
75
|
+
echo "path"
|
|
76
|
+
return
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
if command -v npx >/dev/null 2>&1; then
|
|
80
|
+
echo "npx"
|
|
81
|
+
return
|
|
82
|
+
fi
|
|
83
|
+
if command -v bunx >/dev/null 2>&1; then
|
|
84
|
+
echo "bunx"
|
|
85
|
+
return
|
|
86
|
+
fi
|
|
87
|
+
# Phase B goes here: check a versioned user cache, then download and verify a
|
|
88
|
+
# native artifact into it. Any such download must use curl (never a piped
|
|
89
|
+
# second script), which does not stamp quarantine / Mark-of-the-Web the way a
|
|
90
|
+
# browser does, matching design.md 8.3.
|
|
91
|
+
echo "none"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Run the resolved CLI without exec, so its output can be captured (used to
|
|
95
|
+
# chain the CLI's own doctor). Passes every argument through untouched.
|
|
96
|
+
run_cli() {
|
|
97
|
+
case "$G_SEL" in
|
|
98
|
+
path) "$BIN" "$@" ;;
|
|
99
|
+
npx) npx --yes --package "$PKG@$PINNED" "$BIN" "$@" ;;
|
|
100
|
+
bunx) bunx --bun "$PKG@$PINNED" "$@" ;;
|
|
101
|
+
esac
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
detect_os() { uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]'; }
|
|
105
|
+
|
|
106
|
+
detect_arch() {
|
|
107
|
+
_a="$(uname -m 2>/dev/null)"
|
|
108
|
+
case "$_a" in
|
|
109
|
+
x86_64 | amd64) echo "x64" ;;
|
|
110
|
+
aarch64 | arm64) echo "arm64" ;;
|
|
111
|
+
*) echo "$_a" ;;
|
|
112
|
+
esac
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Escape a value for a JSON string literal (backslash and double quote).
|
|
116
|
+
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
|
117
|
+
|
|
118
|
+
# Render "null" for an empty value, else an escaped JSON string.
|
|
119
|
+
jstr() {
|
|
120
|
+
if [ -z "$1" ]; then printf 'null'; else printf '"%s"' "$(json_escape "$1")"; fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# 1 -> true, anything else -> false.
|
|
124
|
+
jbool() { if [ "$1" = "1" ]; then printf 'true'; else printf 'false'; fi; }
|
|
125
|
+
|
|
126
|
+
# Probe the environment once into G_* globals shared by the emitters.
|
|
127
|
+
collect() {
|
|
128
|
+
G_OS="$(detect_os)"
|
|
129
|
+
G_ARCH="$(detect_arch)"
|
|
130
|
+
|
|
131
|
+
G_CLI_PRESENT=0
|
|
132
|
+
G_CLI_PATH=""
|
|
133
|
+
G_CLI_VER=""
|
|
134
|
+
G_CLI_COMPAT=0
|
|
135
|
+
if command -v "$BIN" >/dev/null 2>&1; then
|
|
136
|
+
G_CLI_PRESENT=1
|
|
137
|
+
G_CLI_PATH="$(command -v "$BIN")"
|
|
138
|
+
G_CLI_VER="$(cli_version)"
|
|
139
|
+
if [ -n "$G_CLI_VER" ] && compatible "$G_CLI_VER"; then G_CLI_COMPAT=1; fi
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
G_NPX_PRESENT=0
|
|
143
|
+
G_NPX_PATH=""
|
|
144
|
+
if command -v npx >/dev/null 2>&1; then
|
|
145
|
+
G_NPX_PRESENT=1
|
|
146
|
+
G_NPX_PATH="$(command -v npx)"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
G_BUNX_PRESENT=0
|
|
150
|
+
G_BUNX_PATH=""
|
|
151
|
+
if command -v bunx >/dev/null 2>&1; then
|
|
152
|
+
G_BUNX_PRESENT=1
|
|
153
|
+
G_BUNX_PATH="$(command -v bunx)"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
G_NODE_PRESENT=0
|
|
157
|
+
G_NODE_VER=""
|
|
158
|
+
if command -v node >/dev/null 2>&1; then
|
|
159
|
+
G_NODE_PRESENT=1
|
|
160
|
+
G_NODE_VER="$(node --version 2>/dev/null | sed 's/^v//')"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
G_SEL="$(resolve)"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Build the nextSteps JSON array body (without the brackets) into G_NEXTSTEPS.
|
|
167
|
+
compute_next_steps() {
|
|
168
|
+
if [ "$G_SEL" = "none" ]; then
|
|
169
|
+
_s1="Install Node 22.13+ from https://nodejs.org so npx can run $PKG@$PINNED, then re-run this launcher."
|
|
170
|
+
_s2="No JavaScript runtime? Install Bun from https://bun.sh to use bunx, or put a compatible $BIN (major ${PINNED%%.*}, at or above $PINNED) on PATH."
|
|
171
|
+
G_NEXTSTEPS="$(printf '"%s", "%s"' "$(json_escape "$_s1")" "$(json_escape "$_s2")")"
|
|
172
|
+
else
|
|
173
|
+
G_NEXTSTEPS=""
|
|
174
|
+
fi
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Emit the structured diagnosis. $1, when a JSON object, is embedded as cliDoctor.
|
|
178
|
+
emit_json() {
|
|
179
|
+
_chained="${1:-}"
|
|
180
|
+
compute_next_steps
|
|
181
|
+
printf '{\n'
|
|
182
|
+
printf ' "tool": %s,\n' "$(jstr "$BIN")"
|
|
183
|
+
printf ' "package": %s,\n' "$(jstr "$PKG")"
|
|
184
|
+
printf ' "pinnedVersion": %s,\n' "$(jstr "$PINNED")"
|
|
185
|
+
printf ' "os": %s,\n' "$(jstr "$G_OS")"
|
|
186
|
+
printf ' "arch": %s,\n' "$(jstr "$G_ARCH")"
|
|
187
|
+
printf ' "checked": {\n'
|
|
188
|
+
printf ' "pathCli": { "present": %s, "path": %s, "version": %s, "compatible": %s },\n' \
|
|
189
|
+
"$(jbool "$G_CLI_PRESENT")" "$(jstr "$G_CLI_PATH")" "$(jstr "$G_CLI_VER")" "$(jbool "$G_CLI_COMPAT")"
|
|
190
|
+
printf ' "npx": { "present": %s, "path": %s },\n' "$(jbool "$G_NPX_PRESENT")" "$(jstr "$G_NPX_PATH")"
|
|
191
|
+
printf ' "bunx": { "present": %s, "path": %s },\n' "$(jbool "$G_BUNX_PRESENT")" "$(jstr "$G_BUNX_PATH")"
|
|
192
|
+
printf ' "node": { "present": %s, "version": %s }\n' "$(jbool "$G_NODE_PRESENT")" "$(jstr "$G_NODE_VER")"
|
|
193
|
+
printf ' },\n'
|
|
194
|
+
printf ' "nativeArtifact": { "available": false, "note": %s },\n' "$(jstr "$NATIVE_NOTE")"
|
|
195
|
+
printf ' "selected": %s,\n' "$(jstr "$G_SEL")"
|
|
196
|
+
printf ' "nextSteps": [%s],\n' "$G_NEXTSTEPS"
|
|
197
|
+
# First character of the captured output, via POSIX parameter expansion
|
|
198
|
+
# (cut -c1 would take the first char of every line, not of the whole string).
|
|
199
|
+
_first="${_chained%"${_chained#?}"}"
|
|
200
|
+
if [ -n "$_chained" ] && [ "$_first" = "{" ]; then
|
|
201
|
+
printf ' "cliDoctor": %s\n' "$_chained"
|
|
202
|
+
else
|
|
203
|
+
printf ' "cliDoctor": null\n'
|
|
204
|
+
fi
|
|
205
|
+
printf '}\n'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Human-readable diagnosis for `doctor` without --json.
|
|
209
|
+
emit_text() {
|
|
210
|
+
printf '%s launcher diagnosis\n\n' "$BIN"
|
|
211
|
+
printf ' os / arch: %s / %s\n' "$G_OS" "$G_ARCH"
|
|
212
|
+
printf ' pinned version: %s (%s)\n' "$PINNED" "$PKG"
|
|
213
|
+
if [ "$G_CLI_PRESENT" = 1 ]; then
|
|
214
|
+
printf ' %s on PATH: %s (version %s, %s)\n' "$BIN" "$G_CLI_PATH" \
|
|
215
|
+
"${G_CLI_VER:-unknown}" "$([ "$G_CLI_COMPAT" = 1 ] && echo compatible || echo incompatible)"
|
|
216
|
+
else
|
|
217
|
+
printf ' %s on PATH: no\n' "$BIN"
|
|
218
|
+
fi
|
|
219
|
+
printf ' npx: %s\n' "$([ "$G_NPX_PRESENT" = 1 ] && echo "$G_NPX_PATH" || echo no)"
|
|
220
|
+
printf ' bunx: %s\n' "$([ "$G_BUNX_PRESENT" = 1 ] && echo "$G_BUNX_PATH" || echo no)"
|
|
221
|
+
printf ' node: %s\n' "$([ "$G_NODE_PRESENT" = 1 ] && echo "${G_NODE_VER:-yes}" || echo no)"
|
|
222
|
+
printf ' selected path: %s\n' "$G_SEL"
|
|
223
|
+
if [ "$G_SEL" = "none" ]; then
|
|
224
|
+
printf '\nNo runtime can launch %s here. %s\n' "$BIN" "$NATIVE_NOTE"
|
|
225
|
+
printf 'Next steps:\n'
|
|
226
|
+
printf ' - Install Node 22.13+ from https://nodejs.org, then re-run this launcher.\n'
|
|
227
|
+
printf ' - Or install Bun from https://bun.sh, or put a compatible %s on PATH.\n' "$BIN"
|
|
228
|
+
fi
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# `doctor [--json] [extra...]`: launcher selection diagnosis. When a CLI is
|
|
232
|
+
# resolvable, chain the CLI's own doctor (engine/config diagnosis) so one call
|
|
233
|
+
# reports both layers. Extra flags pass through to the chained CLI doctor.
|
|
234
|
+
doctor() {
|
|
235
|
+
collect
|
|
236
|
+
_json=0
|
|
237
|
+
for _a in "$@"; do
|
|
238
|
+
if [ "$_a" = "--json" ]; then _json=1; fi
|
|
239
|
+
done
|
|
240
|
+
if [ "$_json" = 1 ]; then
|
|
241
|
+
_chained=""
|
|
242
|
+
if [ "$G_SEL" != "none" ]; then
|
|
243
|
+
_chained="$(run_cli doctor "$@" 2>/dev/null)" || _chained=""
|
|
244
|
+
fi
|
|
245
|
+
emit_json "$_chained"
|
|
246
|
+
else
|
|
247
|
+
emit_text
|
|
248
|
+
if [ "$G_SEL" != "none" ]; then
|
|
249
|
+
printf '\n--- %s doctor ---\n' "$BIN"
|
|
250
|
+
run_cli doctor "$@" || true
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Default action: forward every argument to the resolved CLI, inheriting stdio
|
|
256
|
+
# and exit code. No usable runtime -> structured diagnosis on stderr, exit 78
|
|
257
|
+
# (EX_CONFIG) so the agent never mistakes the diagnosis for a result.
|
|
258
|
+
run() {
|
|
259
|
+
_sel="$(resolve)"
|
|
260
|
+
case "$_sel" in
|
|
261
|
+
path) exec "$BIN" "$@" ;;
|
|
262
|
+
npx) exec npx --yes --package "$PKG@$PINNED" "$BIN" "$@" ;;
|
|
263
|
+
bunx) exec bunx --bun "$PKG@$PINNED" "$@" ;;
|
|
264
|
+
none)
|
|
265
|
+
collect
|
|
266
|
+
emit_json "" >&2
|
|
267
|
+
exit 78
|
|
268
|
+
;;
|
|
269
|
+
esac
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case "${1:-}" in
|
|
273
|
+
doctor)
|
|
274
|
+
shift
|
|
275
|
+
doctor "$@"
|
|
276
|
+
;;
|
|
277
|
+
where)
|
|
278
|
+
resolve
|
|
279
|
+
;;
|
|
280
|
+
*)
|
|
281
|
+
run "$@"
|
|
282
|
+
;;
|
|
283
|
+
esac
|