@liustack/modlens 3.0.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 +9 -1
- package/README.md +18 -123
- package/README.zh-CN.md +38 -143
- package/dist/main.js +14 -11
- package/docs/harness-setup.md +6 -0
- package/docs/troubleshooting.md +9 -1
- package/package.json +15 -13
- package/skills/modlens/SKILL.md +23 -8
- package/skills/modlens/references/cli.md +77 -0
- package/skills/modlens/references/configure.md +34 -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
package/skills/modlens/SKILL.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: modlens
|
|
3
3
|
description: "Plug-in vision for text-only models. Use whenever the user shares an image (local path, screenshot, photo, chart, document scan, or image URL) and the active model cannot see images or has no vision tool. Runs the modlens CLI to convert the image into structured JSON evidence: every word transcribed, layout regions, semantics, visual clues. Also use when the user asks how to install, configure, or switch modlens providers (Gemini API key, OpenAI-compatible endpoints, Claude API or Claude Code CLI)."
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
compatibility: Requires network access and one of node 22+/npx, bun/bunx, or a preinstalled modlens binary on PATH.
|
|
5
|
+
allowed-tools: Bash
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# ModLens — Vision Bridge Skill
|
|
@@ -21,13 +21,28 @@ Do not use this skill for:
|
|
|
21
21
|
|
|
22
22
|
## Prerequisites
|
|
23
23
|
|
|
24
|
+
Run every modlens command through the launcher bundled with this skill.
|
|
25
|
+
Replace `<skill-dir>` with the directory this SKILL.md lives in:
|
|
26
|
+
|
|
24
27
|
```bash
|
|
25
|
-
|
|
28
|
+
bash <skill-dir>/scripts/run.sh -i <image-path-or-url> # macOS / Linux
|
|
29
|
+
powershell -ExecutionPolicy Bypass -File <skill-dir>\scripts\run.ps1 -i <image> # Windows
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
The launcher finds a working way to run modlens and forwards your arguments to it unchanged. It tries, in order: a compatible `modlens` already on `PATH`, then `npx`, then `bunx`. If none of those exists it prints a JSON diagnosis to stderr and exits 78, with a `nextSteps` list for the user. Relay those steps instead of retrying. To see the full diagnosis, run `bash <skill-dir>/scripts/run.sh doctor --json` (on a machine that can launch the CLI it also chains modlens's own provider/config `doctor`).
|
|
33
|
+
|
|
34
|
+
### If you cannot run the launcher script
|
|
35
|
+
|
|
36
|
+
Some harnesses forbid running scripts. Reason through the same order by hand and run the first line that works (the pinned version is 3.0.0):
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.0.0: `modlens <args>`.
|
|
39
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.0.0 modlens <args>`.
|
|
40
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.0.0 <args>`.
|
|
41
|
+
4. Otherwise none of these runtimes is here. Tell the user no JavaScript runtime was found and that installing Node 22.13+ (https://nodejs.org) or Bun (https://bun.sh) is the next step. Do not claim modlens itself failed.
|
|
42
|
+
|
|
43
|
+
`references/runtime.md` documents the version pin, the compatibility rule, and the diagnostic fields.
|
|
44
|
+
|
|
45
|
+
ModLens supports five vision providers. Check what is configured (through the launcher, as above):
|
|
31
46
|
|
|
32
47
|
```bash
|
|
33
48
|
modlens config show
|
|
@@ -43,12 +58,12 @@ modlens config show
|
|
|
43
58
|
|
|
44
59
|
## Command
|
|
45
60
|
|
|
61
|
+
In the examples below, `modlens` means the command run through the launcher above (`bash <skill-dir>/scripts/run.sh ...`, or the PowerShell form on Windows).
|
|
62
|
+
|
|
46
63
|
```bash
|
|
47
64
|
modlens -i <image-path-or-url>
|
|
48
65
|
# pick a provider explicitly
|
|
49
66
|
modlens -i <image> -p gemini-api
|
|
50
|
-
# or without a global install
|
|
51
|
-
npx @liustack/modlens -i <image-path-or-url>
|
|
52
67
|
```
|
|
53
68
|
|
|
54
69
|
Optional flags:
|
|
@@ -101,7 +116,7 @@ Structure is enforced by schema on antigravity-cli and claude-cli (`--json-schem
|
|
|
101
116
|
|
|
102
117
|
## Failure Handling
|
|
103
118
|
|
|
104
|
-
Every error this CLI prints
|
|
119
|
+
Every error this CLI prints names its cause, and most already name the fix, so read the message first.
|
|
105
120
|
|
|
106
121
|
- `Provider CLI not found`: Antigravity CLI is not installed. Install it, or switch provider: `-p gemini-api`.
|
|
107
122
|
- Missing key errors name the exact env var and `config set` command to run. Relay that to the user.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# ModLens CLI manual
|
|
2
|
+
|
|
3
|
+
The skill drives this CLI through its launcher. This page is for running it directly.
|
|
4
|
+
|
|
5
|
+
## Direct usage
|
|
6
|
+
|
|
7
|
+
With the skill installed you do not type commands: paste an image or drop a path, ask anything, and it fires on its own. By hand:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
modlens -i screenshot.png # local image
|
|
11
|
+
modlens -i https://example.com/chart.png # remote image
|
|
12
|
+
modlens -i chart.png --prompt "focus on axes" # extra focus
|
|
13
|
+
modlens recover-paste # pull a pasted image into a file
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Output is a fixed JSON shape:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"image": "/path/to/screenshot.png",
|
|
21
|
+
"provider": "gemini-api",
|
|
22
|
+
"result": {
|
|
23
|
+
"summary": "A workflow diagram with four nodes connected by labeled arrows.",
|
|
24
|
+
"ocr": { "full_text": "/shaping\nBEFORE YOU BUILD\n...", "lines": [] },
|
|
25
|
+
"layout": { "regions": [{ "reading_order": 1, "type": "title", "text": "/shaping" }] },
|
|
26
|
+
"uncertainty": []
|
|
27
|
+
},
|
|
28
|
+
"meta": {
|
|
29
|
+
"generatedAt": "2026-08-06T12:00:00.000Z",
|
|
30
|
+
"model": "gemini-3.6-flash",
|
|
31
|
+
"conversationId": null,
|
|
32
|
+
"durationSeconds": 6.4,
|
|
33
|
+
"usage": { "promptTokenCount": 1234, "candidatesTokenCount": 567 }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`meta` records how the result was produced: when (`generatedAt`), which `model`, the provider's `conversationId` when it has one, wall-clock `durationSeconds`, and the raw `usage` the provider reported (shape varies by provider, `null` when none).
|
|
39
|
+
|
|
40
|
+
## Flags
|
|
41
|
+
|
|
42
|
+
`modlens analyze` (the default command):
|
|
43
|
+
|
|
44
|
+
| Flag | Meaning | Default |
|
|
45
|
+
| :-- | :-- | :-- |
|
|
46
|
+
| `-i, --input <path\|url>` | Image to analyze (required) | |
|
|
47
|
+
| `-p, --provider <name>` | Vision provider | `antigravity-cli` |
|
|
48
|
+
| `-m, --model <name>` | Provider model | per provider (below) |
|
|
49
|
+
| `-o, --output <path>` | Also write JSON to a file | |
|
|
50
|
+
| `--prompt <text>` | Extra focus | |
|
|
51
|
+
| `--timeout <ms>` | Provider timeout | `180000` |
|
|
52
|
+
| `--provider-bin <path>` | Provider binary path | `agy` / `claude` |
|
|
53
|
+
| `--workdir <path>` | Working directory for the provider | a fresh isolated directory per run |
|
|
54
|
+
|
|
55
|
+
The default `-m` model depends on the provider:
|
|
56
|
+
|
|
57
|
+
| Provider | Default model |
|
|
58
|
+
| :-- | :-- |
|
|
59
|
+
| `antigravity-cli` (default) | `gemini-3.6-flash-low` |
|
|
60
|
+
| `gemini-api` | `gemini-3.6-flash` |
|
|
61
|
+
| `anthropic` | `claude-haiku-4-5-20251001` |
|
|
62
|
+
| `claude-cli` | `haiku` |
|
|
63
|
+
| `openai` | none, `-m` is required |
|
|
64
|
+
|
|
65
|
+
`modlens recover-paste`:
|
|
66
|
+
|
|
67
|
+
| Flag | Meaning | Default |
|
|
68
|
+
| :-- | :-- | :-- |
|
|
69
|
+
| `--count <n>` | How many recent pasted images to recover | `1` |
|
|
70
|
+
| `--out-dir <path>` | Where to write recovered images | a fresh private `<tmpdir>/modlens-paste-*` per run |
|
|
71
|
+
| `--session <id>` | Session id for exact targeting | auto-detect |
|
|
72
|
+
| `--transcript <path>` | Explicit transcript `.jsonl` or `.db` (overrides `--session`) | |
|
|
73
|
+
| `--harness <name>` | Force storage scope: `claude-code`, `pi`, `opencode`, `none` | auto-detect |
|
|
74
|
+
| `--cwd <path>` | Project directory the image was pasted in | current directory |
|
|
75
|
+
|
|
76
|
+
Five providers: `antigravity-cli` (default, no key), `gemini-api` (fastest free route), `openai` (any OpenAI-compatible multimodal endpoint), `anthropic`, and `claude-cli` (uses your existing Claude subscription). Two more subcommands: `modlens config <init|set|show>`, and `modlens doctor` (checks Node, provider readiness, which provider will be selected and why, and the detected harness, without spending quota or touching the network. Add `--json` for a machine-readable report).
|
|
77
|
+
|
|
@@ -15,6 +15,40 @@ modlens config set <provider>.<field> <value> # fields: apiKey, baseUrl, model
|
|
|
15
15
|
|
|
16
16
|
`config set` writes the file with 0600 permissions.
|
|
17
17
|
|
|
18
|
+
## The file's exact shape
|
|
19
|
+
|
|
20
|
+
Everything lives under two top-level keys, both optional. A missing file means all defaults. Provider settings sit under `providers.<name>`, not at the top level, which is the mistake hand-editors make most.
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"provider": "gemini-api",
|
|
25
|
+
"providers": {
|
|
26
|
+
"antigravity-cli": { "model": "gemini-3.6-flash-low" },
|
|
27
|
+
"gemini-api": {
|
|
28
|
+
"apiKey": "AIza...",
|
|
29
|
+
"baseUrl": "https://generativelanguage.googleapis.com",
|
|
30
|
+
"model": "gemini-3.6-flash"
|
|
31
|
+
},
|
|
32
|
+
"openai": {
|
|
33
|
+
"apiKey": "sk-...",
|
|
34
|
+
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
35
|
+
"model": "qwen3.6-27b"
|
|
36
|
+
},
|
|
37
|
+
"anthropic": { "apiKey": "sk-ant-..." },
|
|
38
|
+
"claude-cli": { "model": "haiku" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Field semantics:
|
|
44
|
+
|
|
45
|
+
- `provider`: which provider runs when `-p` is not given. Canonical names or aliases both work (`agy`/`antigravity` for `antigravity-cli`, `gemini` for `gemini-api`, `openai-compat` for `openai`, `claude` for `anthropic`, `claude-code` for `claude-cli`). Empty or absent means `antigravity-cli`.
|
|
46
|
+
- `providers.<name>.<field>`: only three fields exist, `apiKey`, `baseUrl`, `model`. Every provider entry is optional, and every field inside it is optional. Alias keys are read too (settings saved under `gemini` are found when `gemini-api` resolves), with the canonical key winning on conflict.
|
|
47
|
+
- Environment variables override the file for these bindings: `GEMINI_API_KEY`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`. Nothing else is read from the environment except `MODLENS_HARNESS` (paste-recovery scope, unrelated to this file).
|
|
48
|
+
- Unknown top-level keys and unknown provider names are ignored rather than rejected, so a typo fails quiet: run `modlens doctor` after hand-editing, it shows which file and env values are actually in effect.
|
|
49
|
+
|
|
50
|
+
Hand-editing is fine (keep the file valid JSON and its permissions 0600). `modlens config set` does the same thing with guardrails.
|
|
51
|
+
|
|
18
52
|
## Provider setup recipes
|
|
19
53
|
|
|
20
54
|
### antigravity-cli (default, free, no key)
|
|
@@ -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
|
+
}
|