img2
Plugin harness for the img2 ecosystem: everything is a plugin. The harness ships no domain
capability — it owns install/link plumbing, the plugin registry, the workspace state envelope,
the gate runner, and the contract.
Documentation, and which one to read
docs/PLUGIN_ARCHITECTURE.md is a generated single-page build of the wiki — read the wiki instead,
unless you want the whole thing in one file.
Ecosystem discovery: GitHub topic img2threejs-plugin.
Quickstart
npx @img2threejs/img2 install
img2 add img2threejs/plugin-img2glb
img2 add npm:@img2threejs/plugin-cs2
img2 list
img2 update
img2 doctor
img2 sync --check
img2 remove img2glb
Every command also runs without the launcher: npx @img2threejs/img2 <command>.
Install output
install and add show numbered phases, actual host-link outcomes, the selected ref/pin,
backup locations when replacing an installation, and a copyable doctor command on success.
Plugin CLI wrappers use the same presentation for offline previews and already-registered installs.
Registration alone is not a health check; run doctor for that.
Color is enabled only on the corresponding terminal stream. Set NO_COLOR=1 to disable it;
TERM=dumb is also plain. Redirected/piped output stays uncolored even with FORCE_COLOR.
Narrow terminals stack fields and wrap messages without truncating paths or refs.
Machine-readable --json output and exit codes are unchanged.
Use npx @img2threejs/img2@latest <command> to pick up installer updates.
install keeps an existing harness checkout; it does not automatically upgrade an older
img2 launcher pointing into that checkout.
The harness publishes to npm as the scoped package @img2threejs/img2. There is no unscoped
fallback: a wrapper that depends on img2 resolves a different package on npm and will not
satisfy the wrapper's @img2threejs/img2/plugin-cli import.
Plugin CLI wrapper
A plugin's author can ship a CLI wrapper alongside the plugin repo. The wrapper lives in the
plugin's cli/ subdirectory, is published to npm under a name like img2-<plugin-id>, and lets
end users install/update/remove the plugin without the harness on their machine first:
npx --package=@img2threejs/img2 create-img2-plugin-cli \
--directory ~/src/plugin-foo \
--name img2-foo \
--source img2threejs/plugin-foo \
--workflow-ref 7ef63107b39f77df3a11062a1a0a2245f500921c
The generator creates an installer-only cli/ package, isolated-home regression tests,
public CLI README/license, .gitignore, a SHA-pinned .github/workflows/cli-publish.yml,
and a complete root CLI_QUICKSTART.md. For a private GitHub source, add --private-repo.
End users run npx img2-foo install --dry-run, then npx img2-foo install --yes.
The package ships only its executable, package metadata and public README/license—not the
plugin repository's README, tools or assets. The shared runtime is pinned to the generator's
exact harness version.
See docs/WRITING_A_PLUGIN.md and the
generated quick-start template.
Prerequisites
- Node.js ≥ 18
- Git (the harness uses it for clone/checkout)
- Python 3.10+ (the bundled
img2_core/ is stdlib-only Python)
Detected agent hosts: Claude Code (~/.claude), Codex (~/.codex), and
OpenCode ($XDG_CONFIG_HOME/opencode or ~/.config/opencode). The harness does not
support any other host; check img2 doctor --json for the current host detection on your
machine.
Asking which provider serves a capability
img2 capabilities answers "which installed plugin turns X into Y" for a caller — a base pipeline
consulting it at a step, or you at a prompt. It is read-only, takes no lock and writes nothing.
img2 capabilities --from-kind image --to-kind glb --json
{ "version": 1, "contract": 14,
"query": { "from": "image", "to": "glb" },
"status": "answered",
"providers": [{ "plugin": "img2glb", "version": "0.1.0", "resolvedSha": "353a8ea…",
"dir": "/Users/you/.img2/plugins/img2glb",
"steps": [{ "id": "generate-glb", "argv": ["python3", "…/tools/img2glb.py",
"--image", "{image}", "--workspace", "{workspace}"] }],
"gateRunner": { "argv": ["python3", "-m", "img2_core.gate_runner", "…"] } }],
"problems": [] }
Four properties a caller can rely on:
- One JSON envelope on stdout for every outcome it owns, including failures. Branch on
status
(answered | ambiguous | data-fault) — never on stderr text.
- Step commands come back as
argv arrays, already tokenised, with {plugin_dir} resolved and
{workspace}/{image} left as single elements to replace by value. You never build a shell string,
so a path containing a space cannot split and a plugin cannot smuggle in a second command.
- Exit codes reuse the harness table:
0 answered (zero providers is a normal answer), 1 a data
fault, 3 an ambiguous edge — resolve it with --plugin <id>. This subcommand never exits 2, so
2 still means "this harness does not understand you".
- One broken plugin cannot deny an unrelated answer. A plugin whose manifest fails to read lands
in
problems[] while providers[] still answers.
To detect whether a harness supports this at all, without parsing prose:
img2 --version --json
An unrecognised --json, or capabilities missing from commands, means the feature is absent —
treat that as "no provider" and take your built-in path, rather than as an error.
Developing a plugin locally:
img2 add --link ~/src/plugin-hello-cube
A plugin can also be distributed as an npm package instead of a git repo — img2 add npm:<name>
or img2 add npm:<name>@<version> (default source trust is the @img2threejs scope, same rule as
the img2threejs/* git org: anything else needs --allow-any-source). ref records the resolved
version and resolvedSha records npm's own dist.integrity (a sha512-… string) rather than a git
SHA — the harness's own notion of "pinned content hash" for a package it fetched instead of cloned.
img2 update [<id>] [--check] re-checks every row's source (npm version, git's newest semver tag) for
something newer and fetches it in place; --check reports what is pending without changing anything.
Layout under $IMG2_HOME (default ~/.img2; the deprecated IMG2THREEJS_HOME is honoured
for one release with a warning):
harness/ canonical harness checkout (img2_core lives here)
plugins/<id>/ one clone (or --link symlink, or npm fetch) per registry row
generated/ index.md + routes.json, regenerated by `img2 sync`
plugins.json the registry: flat rows {id, repo, ref, resolvedSha, addedAt}
repo is "org/repo", a git URL, "npm:<name>", or "link:<path>"
receipts.json what was linked where
backups/ displaced directories; nothing is ever deleted in place
Plugin tools reach the Python core through the generated _img2_local.py (gitignored by
contract) or $IMG2_HOME; img2_core.require_core_api(1) asserts the core API at runtime.
Gates run through python3 -m img2_core.gate_runner --plugin-dir <dir> --workspace <dir>,
which topo-sorts gates.json, expects one img2.gate-verdict JSON envelope per gate on
stdout, and stops on a blocking fail.
Tests
node --test tests/*.test.mjs
python3 -m unittest discover -s tests/python -p 'test_*.py'
Both suites are dependency-free (node:test and unittest only). Run them from the repo root.