Skip to content

feat: npm plugin source, img2 update, and CI/publish - #3

Open
kokorolx wants to merge 6 commits into
mainfrom
npm-source
Open

kokorolx wants to merge 6 commits into
mainfrom
npm-source

Conversation

@kokorolx

@kokorolx kokorolx commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds an npm: plugin source alongside the existing git source: img2 add npm:<name>[@<version>] resolves the version and dist.integrity via npm view, fetches the tarball via npm pack, and extracts it itself (stripping the package/ prefix) into the same staging → validate → link flow a git install already goes through. Default trust is the @img2threejs scope, mirroring the img2threejs/* git org — anything else needs --allow-any-source, same confirmation prompt the git path already uses.
  • Adds img2 update [<id>] [--check] (there was no update command before this PR). It re-resolves each registered plugin's source — newest npm version, or newest reachable git tag — and re-fetches it in place if it differs from the recorded ref, backing up the previous clone; a link: row is a local dev checkout and is left alone. --check reports what's pending without fetching, exiting non-zero if anything is.
  • Row shape is unchanged: still {id, repo, ref, resolvedSha, addedAt}. For an npm row, repo becomes npm:<name>, ref is the resolved version, and resolvedSha is npm's own dist.integrity (a sha512-… string) rather than a git SHA — this harness's only other notion of "pinned content hash" for a package it fetched instead of cloning.
  • Git-assumption audit (grepped every use of resolvedSha, git(, cloneDir, and doctor/verify logic): found two real ones and fixed both — (1) doctor's .gitignore covers _img2_local.py check unconditionally ran git check-ignore against every plugin dir and would always FAIL an npm-fetched or bare --link'd directory (no .git to have a .gitignore in); it's now guarded by isGitRepo(dir) and skipped where there's no git checkout to have the hazard it guards against. (2) the short-SHA display in img2 add/img2 list sliced the first 7 characters of resolvedSha, which for an npm integrity string (sha512-…) just printed the literal sha512- prefix; it now shows a useful slice of whichever part actually varies.
  • Unrelated packaging bug found and fixed: npm pack --dry-run showed this session's own leftover docs/.omc/state/** and img2_core/__pycache__/** in the tarball. Verified against real npm 10.9.8 in an isolated repro: npm does not consult .gitignore for a directory explicitly listed in package.json's files field when there's no .npmignore — it packs the directory's full contents regardless. Fixed by negating those patterns directly in files (the one mechanism that does work here), and cleaned up the stray untracked cruft from the working tree.
  • CI: .github/workflows/ci.yml and publish.yml are thin callers into the org's shared img2threejs/ci-workflows reusable workflows, pinned by full commit SHA per org policy — ci.yml uses python-ci.yml to run the full test suite on every PR and push to main; publish.yml uses npm-publish.yml to publish img2 to npm on a vX.Y.Z tag push. The reusable workflow validates the tag against package.json, refuses install-time lifecycle scripts, runs the suite in a job with no access to the npm token, and publishes with npm publish --provenance --access public authenticated by an NPM_TOKEN secret (a granular npm automation token — not OIDC trusted publishing). Both refs currently point at a feature branch of ci-workflows pending img2threejs/ci-workflows#2this PR has that cross-repo dependency; re-pin both uses: lines to ci-workflows's merged main SHA once it lands. CONTRIBUTING.md documents all of this, including that NPM_TOKEN must exist as an org/repo secret before the first tag push (no separate manual first-publish step is needed once it does).
  • Docs: README (quickstart, layout, the npm: source, img2 update) and docs/PLUGIN_CONTRACT.md §6/§7 (registry row shape, trust boundary) now describe the npm source and update command. CHANGELOG.md has a new [Unreleased] entry.

Test evidence

Full suite, run locally after every change (final run on the committed tree):

ℹ tests 106
ℹ pass 106
ℹ fail 0
...
Ran 43 tests in 2.888s
OK

New coverage: unit tests for parseNpmSpec/resolveNpmSource (scope parsing, default-org trust); e2e tests for img2 add npm:... (default scope, explicit @version, non-default scope needing --allow-any-source + confirmation, npm missing from PATH, tolerating non-empty stderr on success, doctor staying clean on a non-git plugin dir) against a PATH-shimmed fake npm (a real npm view/npm pack registry doesn't exist in CI, and the harness's own extraction code — the part actually worth testing — still runs against a tarball built with the real tar); and e2e tests for img2 update (npm row fetches a newer version and updates the row, --check reports pending without touching anything, a link: row is left alone, a git row updates to its newest tag).

Decisions made that weren't fully specified

  • img2 update didn't exist before this PR — grepped the whole repo and history for it; there was no command to extend, so this PR designs and implements it from scratch (task 5 assumed there was an existing one to extend). Scope: for a git row, "newer" means a newer reachable semver tag — a plugin pinned to a moving branch (no tags on the remote) is reported as "no tag; nothing to compare against" rather than guessed at, since the registry doesn't record whether ref was an explicit branch pin.
  • Re-confirms an already-registered non-default-org/scope source on every update, the same as add --force already does on every re-add, rather than trusting it forever once accepted once.
  • A 4-way parallel /simplify pass (reuse/simplification/efficiency/altitude) on the diff caught three real issues, now fixed: npm()/tar() shared one subprocess wrapper instead of two near-identical copies; the staging-directory rm+mkdir pattern (repeated 3×) is now one freshStagingDir helper; and img2 update's npm path no longer calls npm view twice (once to check, once inside the fetch) for a package it's about to update — it views once and reuses the result. The efficiency pass's other finding (resolveRefAndClone calls git ls-remote --tags before a git clone that would also fetch the tags) is pre-existing behavior I only extracted into a named helper, not something this PR introduced — left alone as out of scope.

Anomalies

  • The npm registry scope trust check treats unscoped npm packages the same as a non-default git org (needs --allow-any-source) — there's no unscoped equivalent of img2threejs/*, so this seemed like the only sound default.

img2 add npm:<name>[@<version>] fetches a plugin from npm instead of git: npm view
resolves the version and dist.integrity, npm pack fetches the tarball, and the harness
extracts it itself (stripping the package/ prefix) into the same staging -> validate ->
link flow git installs already go through. Default trust is the @img2threejs scope,
mirroring the img2threejs/* git org.

img2 update [<id>] [--check] re-checks every registered plugin's source (newest npm
version, or newest reachable git tag) and re-fetches it in place if newer, backing up
the previous clone; a link: row is left alone.

Also fixes two git assumptions doctor/list/add made about a plugin directory: the
.gitignore check now only runs against an actual git checkout, and the short-sha
display shows a useful slice of an npm dist.integrity string instead of the literal
sha512- prefix.
…arball

npm pack does not consult .gitignore for a directory explicitly listed in package.json's
files field (verified against npm 10.9.8) -- a local __pycache__/ or leftover .omc/ state
dir under docs/ or img2_core/ would ship in the published tarball otherwise. Negate them
directly in files, which npm does apply.
ci.yml runs the full test suite on every PR and push to main. publish.yml publishes
img2 to npm via trusted publishing (OIDC, no token secret) on a vX.Y.Z tag push, after
re-running the suite and checking the tag matches package.json's version.

CONTRIBUTING.md documents the release-and-publish steps, including that the first-ever
publish needs a manual, logged-in npm publish before trusted publishing can take over.
README's quickstart and layout section, and PLUGIN_CONTRACT.md's registry-row and
trust-boundary sections (6, 7), now cover npm:<name> as a repo form alongside org/repo,
a git URL, and link:<path>, plus the img2 update command. CHANGELOG records the release.
… npm view

npm() and tar() now share one runTool wrapper instead of repeating the ENOENT/failure
message shape. The staging-directory rm+mkdir pattern (repeated for git clone staging,
npm staging, and npm pack staging) is now one freshStagingDir helper. resolveNpmAndFetch
is split into npmViewInfo (view + parse + validate) and fetchNpmTarball (pack + extract)
so img2 update can view a package once and reuse that result to fetch it, instead of
viewing it a second time right after.
…inline logic

ci.yml and publish.yml now uses: img2threejs/ci-workflows's python-ci.yml and
npm-publish.yml, pinned by full commit SHA per org policy, rather than carrying
their own test/publish steps. Publish authenticates via the NPM_TOKEN secret
(a granular npm automation token), not OIDC trusted publishing; the reusable
workflow validates the tag against package.json, refuses install-time lifecycle
scripts, and no-ops on an already-published version.

Both workflow refs point at a feature branch of ci-workflows pending
img2threejs/ci-workflows#2; re-pin to its merged main SHA once that lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant