@rig-c/parts 0.0.0-stage → 1.5.3

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +134 -0
  3. package/README.md +431 -2
  4. package/bin/rig-parts.cjs +43 -0
  5. package/cli.ts +1240 -0
  6. package/docs/AUTHORING.md +2387 -0
  7. package/docs/MIGRATION.md +137 -0
  8. package/docs/STABILITY.md +262 -0
  9. package/package.json +70 -4
  10. package/skills/rig-parts/SKILL.md +167 -0
  11. package/src/apng.ts +381 -0
  12. package/src/assemble.ts +2259 -0
  13. package/src/autoamplitude.ts +194 -0
  14. package/src/autoenvelope.ts +404 -0
  15. package/src/automesh.ts +585 -0
  16. package/src/automotion.ts +447 -0
  17. package/src/autoreplay.ts +516 -0
  18. package/src/build.ts +1240 -0
  19. package/src/check.ts +2942 -0
  20. package/src/comfy/client.ts +245 -0
  21. package/src/comfy/index.ts +19 -0
  22. package/src/comfy/painting.ts +136 -0
  23. package/src/comfy/seethrough.ts +171 -0
  24. package/src/config.ts +2283 -0
  25. package/src/contour.ts +1901 -0
  26. package/src/coords.ts +26 -0
  27. package/src/diagnostics.ts +198 -0
  28. package/src/errors.ts +39 -0
  29. package/src/gif.ts +213 -0
  30. package/src/graphs.ts +302 -0
  31. package/src/headbox.ts +173 -0
  32. package/src/heat.ts +313 -0
  33. package/src/inputs.ts +100 -0
  34. package/src/instruments.ts +1145 -0
  35. package/src/keypoints.ts +296 -0
  36. package/src/layers.ts +598 -0
  37. package/src/localweights.ts +217 -0
  38. package/src/mesh.ts +362 -0
  39. package/src/motion.ts +311 -0
  40. package/src/palette.ts +208 -0
  41. package/src/parts.ts +410 -0
  42. package/src/propose.ts +2540 -0
  43. package/src/pyfmt.ts +135 -0
  44. package/src/raster/blur.ts +80 -0
  45. package/src/raster/components.ts +165 -0
  46. package/src/raster/composite.ts +81 -0
  47. package/src/raster/index.ts +9 -0
  48. package/src/raster/morph.ts +116 -0
  49. package/src/raster/png.ts +82 -0
  50. package/src/raster/poly.ts +166 -0
  51. package/src/raster/resize.ts +301 -0
  52. package/src/raster/types.ts +89 -0
  53. package/src/raster/warp.ts +121 -0
  54. package/src/requirements.ts +1240 -0
  55. package/src/rig.ts +1440 -0
  56. package/src/round.ts +62 -0
  57. package/src/scene.ts +1414 -0
  58. package/src/sheet.ts +128 -0
  59. package/src/skeleton.ts +400 -0
  60. package/src/structure.ts +858 -0
  61. package/src/tags.ts +121 -0
  62. package/src/weights.ts +124 -0
  63. package/workflows/seethrough.json +68 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 firejune
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,134 @@
1
+ # Third-party notices
2
+
3
+ rig-parts' own code is MIT (see `LICENSE`). It depends on, reads the output
4
+ of, or reimplements the behaviour of the third-party work below, each under its
5
+ own terms. Naming a project here identifies where something comes from; it does
6
+ not imply that its authors endorse rig-parts.
7
+
8
+ Every statement below is either quoted from the named source or marked
9
+ **[observed]** with the date and the place it was read. Upstream terms can
10
+ change; check the source when it matters.
11
+
12
+ ## 1. See-through
13
+
14
+ rig-parts takes See-through's output as its input. **rig-parts does not
15
+ vendor, embed or redistribute See-through code or weights**: the user runs
16
+ See-through by one of the routes in the README (*See-through routes*) and points
17
+ rig-parts at what it wrote.
18
+
19
+ - Project: [shitagaki-lab/see-through](https://github.com/shitagaki-lab/see-through)
20
+ - Paper: Jian Lin, Chengze Li, Haoyun Qin, Kwun Wang Chan, Yanghua Jin, Hanyuan
21
+ Liu, Stephen Chun Wang Choy, Xueting Liu, *See-through: Single-image Layer
22
+ Decomposition for Anime Characters*, SIGGRAPH 2026,
23
+ [arXiv:2602.03749](https://arxiv.org/abs/2602.03749)
24
+ - Licence: Apache License 2.0 — **[observed 2026-09-27]** the repository's
25
+ licence as GitHub reports it.
26
+
27
+ ### The ComfyUI wrapper
28
+
29
+ - Project: [jtydhr88/ComfyUI-See-through](https://github.com/jtydhr88/ComfyUI-See-through)
30
+ - Licence: **[observed 2026-09-27]** there is no `LICENSE` file in the
31
+ repository (`LICENSE` on the default branch `master` returns 404, and GitHub
32
+ reports no licence); `pyproject.toml` declares `license = "MIT"`.
33
+
34
+ rig-parts reads the wrapper's `layers.json` manifest format; it contains no
35
+ wrapper code.
36
+
37
+ ### Weights
38
+
39
+ | Weights | Licence as stated on the model card | Observed |
40
+ | --- | --- | --- |
41
+ | [`layerdifforg/seethroughv0.0.2_layerdiff3d`](https://huggingface.co/layerdifforg/seethroughv0.0.2_layerdiff3d) | `license:apache-2.0` tag | 2026-09-27, Hugging Face model API |
42
+ | (same card, after its 2026-09-28 commit *"Add licence terms, NOTICE and base model metadata"*) | `license:openrail++` tag; the card's *Licence* section says the maintainers' own contributions are Apache-2.0 and the weights also inherit the licences of the models they are derived from — Animagine XL 4.0 / Stable Diffusion XL 1.0 (CreativeML Open RAIL++-M) and LayerDiffuse (CreativeML Open RAIL-M) | 2026-09-28, Hugging Face model API and card |
43
+ | [`layerdifforg/seethroughv0.0.1_marigold`](https://huggingface.co/layerdifforg/seethroughv0.0.1_marigold) | no licence tag on the card | 2026-09-27, Hugging Face model API |
44
+ | (same card, after its 2026-09-28 revision) | `license:openrail++` tag | 2026-09-28, Hugging Face model API |
45
+
46
+ Maintainer statement, on the Hugging Face discussion
47
+ [`layerdifforg/seethroughv0.0.2_layerdiff3d/discussions/1`](https://huggingface.co/layerdifforg/seethroughv0.0.2_layerdiff3d/discussions/1)
48
+ (user `24yearsold`, 2026-04-07): *"Yes all our models are aligned with our main
49
+ repo to use Apache 2.0."* The 2026-09-28 card revision above narrows that: Apache-2.0 for the maintainers' contributions, with the base models' Open RAIL terms inherited. rig-parts does not distribute these weights; a user who fetches them accepts the terms the card states on the day they fetch.
50
+
51
+ ### Training data
52
+
53
+ The paper describes its own supervision this way: *"we introduce a scalable
54
+ engine that bootstraps high-quality supervision from commercial Live2D models,
55
+ capturing pixel-perfect semantics and hidden geometry."* That is the paper's
56
+ description of how See-through was trained. rig-parts does not use,
57
+ include or redistribute any of that data, any Live2D model, or anything derived
58
+ from them beyond the layers See-through itself writes for the user's own image.
59
+
60
+ ## 2. Spine Runtimes, through rig-c
61
+
62
+ rig-parts depends on [`rig-c`](https://www.npmjs.com/package/rig-c)
63
+ (MIT, same author), which compiles and validates Spine skeleton data. Its round
64
+ trip runs through `@esotericsoftware/spine-core`, part of the
65
+ [Spine Runtimes](https://github.com/EsotericSoftware/spine-runtimes), under the
66
+ [Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license).
67
+ Since rig-c 2.0 neither package installs the runtime: rig-c carries it
68
+ only as a development dependency, and an install of rig-c gates a build with
69
+ its own validator. rig-parts does not install it either; it is this repository's
70
+ development dependency, at the version rig-c develops against, for the round
71
+ trip in the selftest and CI and the selftest's posing oracle.
72
+ rig-c's [NOTICE.md](https://github.com/firejune/rigc/blob/main/NOTICE.md)
73
+ sets out the chain in full; briefly, as a restatement of Esoteric Software's
74
+ terms and not a term of this project:
75
+
76
+ 1. What rig-parts produces is input to rig-c, whose output **is Spine
77
+ skeleton data**.
78
+ 2. Playing Spine skeleton data in a product requires a Spine Runtime, and the
79
+ Spine Runtimes License requires **each user of such a product to own a
80
+ Spine Editor license**.
81
+ 3. rig-c no longer installs `spine-core`, and rig-parts does not either;
82
+ the obligation in 2 attaches to the Spine skeleton data rig-c writes,
83
+ which the stages of rig-parts that call it produce, and to `spine-core`
84
+ wherever it is installed beside them — this repository's development
85
+ dependency included.
86
+
87
+ rig-parts also imports rig-c's own modules directly, by deep path: its PNG codec
88
+ (`tools/plate.ts`, `src/png.ts`), its 5x7 label font (`tools/font5x7.ts`) and
89
+ its coordinate conversion (`src/transform.ts`). Those are rig-c's MIT code.
90
+
91
+ ## 3. Checkpoints
92
+
93
+ Generating the painting is an **optional** adapter (`rig-parts comfy paint`).
94
+ The checkpoint, any LoRA and any ControlNet it runs are the user's: rig-parts
95
+ ships none and downloads none, and the terms that govern an image they produce
96
+ are the terms of those models. A painting from any other source works the same
97
+ way.
98
+
99
+ The images this repository ships — `assets/demo-*` and `assets/sample-parts.png`
100
+ — and the public examples' paintings were generated with
101
+ `ponyDiffusionV6XL_v6StartWithThisOne.safetensors`,
102
+ [Pony Diffusion V6 XL](https://civitai.com/models/257749), with no LoRA. The
103
+ checkpoint itself is not redistributed. **[observed 2026-09-27]** CivitAI's model
104
+ API reported for it `allowCommercialUse: ["Image", "RentCivit"]`,
105
+ `allowNoCredit: false`, `allowDerivatives: true` and
106
+ `allowDifferentLicense: false`: generated images may be used commercially and
107
+ the model must be credited, which this line and the
108
+ [spine-parts-examples](https://github.com/firejune/spine-parts-examples) README do.
109
+
110
+ ## 4. Behaviour reimplemented, not code copied
111
+
112
+ The raster operations in `src/raster/` reproduce the documented behaviour of
113
+ calls the reference implementation made, so that the port measures the same
114
+ numbers — [OpenCV](https://github.com/opencv/opencv) (Apache-2.0),
115
+ [SciPy](https://github.com/scipy/scipy) (BSD-3-Clause) and
116
+ [Pillow](https://github.com/python-pillow/Pillow) (MIT-CMU). They are written in
117
+ TypeScript for this package and contain none of those projects' code; each
118
+ function's comment says which call it stands in for and what was measured.
119
+
120
+ ## 5. npm dependencies
121
+
122
+ | Package | Licence | Why |
123
+ | --- | --- | --- |
124
+ | [`rig-c`](https://www.npmjs.com/package/rig-c) | MIT | compile, gate, render and check; its PNG codec, font and coordinate conversion |
125
+ | [`ag-psd`](https://www.npmjs.com/package/ag-psd) | MIT | reading an upstream See-through `.psd` |
126
+ | ↳ [`pako`](https://www.npmjs.com/package/pako) | MIT AND Zlib | ag-psd's deflate |
127
+ | ↳ [`base64-js`](https://www.npmjs.com/package/base64-js) | MIT | ag-psd's base64 |
128
+
129
+ Development only, not installed with the package: `typescript` (Apache-2.0),
130
+ `eslint` (MIT), `typescript-eslint` (MIT), `@types/bun` (MIT), and
131
+ [`@esotericsoftware/spine-core`](https://www.npmjs.com/package/@esotericsoftware/spine-core)
132
+ (Spine Runtimes License; section 2), pinned to the version rig-c develops
133
+ against (`TY10`): the round trip in the selftest and CI, the selftest's posing
134
+ oracle, and `tools/atlas_population.ts` and `tools/idle_cost.ts`.
package/README.md CHANGED
@@ -1,3 +1,432 @@
1
- # Temporary Holding Version
1
+ # rig-parts
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **AI-authored 2D character rigs from one anime painting, verified before they
4
+ are written.** rig-parts takes a single character painting and the layers
5
+ [See-through](https://github.com/shitagaki-lab/see-through) decomposed it into,
6
+ merges them into measured rig-space parts, authors weighted meshes, bone chains
7
+ and a looping idle over them in [rig-c](https://github.com/firejune/rigc)'s
8
+ spec, and hands that spec to rig-c to compile, gate, pack, render and check.
9
+ It is built for agents that cannot see the image: every stage prints named,
10
+ numeric findings, a refusal names the object, the value found and the value
11
+ required, nothing is written after a red, and no value is invented where the
12
+ input is silent.
13
+
14
+ ## What you get
15
+
16
+ <p align="center">
17
+ <img src="https://raw.githubusercontent.com/firejune/rig-parts/main/assets/demo-source.png" alt="The demo painting: a generated full-body character in a white and pink frilled dress with long pink twin tails, standing with her hands clasped" height="420" />
18
+ <img src="https://raw.githubusercontent.com/firejune/rig-parts/main/assets/demo-idle.png" alt="The same character rigged, breathing, blinking once and swaying her hair, sleeves and skirt in a four-second loop" height="420" />
19
+ </p>
20
+
21
+ <p align="center">
22
+ <img src="https://raw.githubusercontent.com/firejune/rig-parts/main/assets/demo-parts.png" alt="Contact sheet of the painting and the 22 assembled parts it was split into: hair, dress, sleeves, shoes, face, eyes, brows, mouth and ornaments" width="100%" />
23
+ </p>
24
+
25
+ <p align="center"><em>
26
+ The painting was generated for this repository with a public checkpoint (Pony
27
+ Diffusion V6 XL) and no LoRA; its generation record, See-through layers and licence
28
+ are in <a href="https://github.com/firejune/spine-parts-examples">spine-parts-examples</a>
29
+ (that repository keeps the name it was published under before this package's rename).
30
+ See-through ran twice, once on the whole figure and once on a square head crop.
31
+ Then one command: <code>rig-parts build --seam silhouette --loop</code>. The
32
+ proposer's bones, meshes, regions and motion were used <b>unedited</b>. Two input-stage
33
+ edits were made by hand for the reference run this example reproduces: the head box
34
+ was shifted down into the canvas (its proposal ran 118 px above the top edge;
35
+ <code>propose --head-box</code> now makes that shift itself), and <code>hair_back</code>
36
+ is taken from the full run, because the twin tails leave the head crop sideways
37
+ (a plan edit). <code>check</code> printed, verbatim:<br/>
38
+ <code>50 assertions: 24 measured (24 passed, 0 failed), 26 skipped, 0 not in profile "spine-html"</code><br/>
39
+ <code>loop: idle 49 frame(s) at 12 fps, f0000 vs f0048 (t = 4s): max |d| 0 (0 required)</code><br/>
40
+ <code>seam: setup pose at 714x1216, scale 0.9422: mean |d| 0.234 (&lt;= 1.0), 2 px over 40, 2.25 rig px (the count / s²; &lt;= 50), 0 px over 80 (reported)</code><br/>
41
+ <code>pack: skeleton.png 922x1348, 22 region(s), 94.9% covered, padding 2, page edges free, shape polygon</code><br/>
42
+ <code>--seam silhouette</code> repaired the navy blobs the default rule leaves on the white
43
+ blouse (recomposite error pixels 10,340 → 8,644); the default stays
44
+ <code>near-white</code>, the reference implementation's rule, so the examples stay
45
+ comparable with it. The loop is <code>rig-parts loop --palette</code>'s indexed APNG, 48 frames at
46
+ 12 fps, 1,706,719 bytes, one 256-entry palette at a measured error of max 57, mean 1.601
47
+ per channel over every frame (the lossless APNG beside it, the exactness record, is
48
+ 13,645,503 bytes; the GIF, at the same error, 1,812,288). Frame 29 (2.417 s) is the
49
+ closed eye: the blink holds for 0.084 s, one 12 fps frame rounded up, where the
50
+ reference implementation held 0.04 s and no frame of its loop showed the eyes shut
51
+ (issue #32); the painting is shown at half size, resampled and written
52
+ by this package's PNG codec (nothing here encodes JPEG). <code>rig-parts sheet</code>
53
+ made the contact sheet.
54
+ </em></p>
55
+
56
+ <p align="center">
57
+ <img src="https://raw.githubusercontent.com/firejune/rig-parts/main/assets/sample-parts.png" alt="Contact sheet of the sample character's painting and its 20 parts" width="560" />
58
+ </p>
59
+
60
+ <p align="center"><em>
61
+ The plain fixture, <code>examples/sample</code>: same checkpoint, no LoRA, and a rig
62
+ built from its own proposal with no hand edit at all —
63
+ <code>seam: setup pose at 379x1216, scale 1.0116: mean |d| 0.204 (&lt;= 1.0), 2 px over 40, 1.95 rig px (the count / s²; &lt;= 50), 0 px over 80 (reported)</code>, loop max |d| 0.
64
+ </em></p>
65
+
66
+ <p align="center"><em>
67
+ The hanging element, <code>examples/scarf</code>: same checkpoint, no LoRA, a very long
68
+ red scarf whose ends hang free, and the first character run through this pipeline
69
+ with its proposal used unedited — the only hand corrections are four
70
+ <code>assemble.patches</code>, each answering an <code>uncovered hole</code> line.
71
+ <code>check</code> passes 9 of 9 with every mesh on the lattice and with every mesh
72
+ switched to the contour mode. It has no reference output: its <code>expected/</code>
73
+ is this port's own build (<a href="examples/scarf/README.md">examples/scarf</a>).
74
+ </em></p>
75
+
76
+ ## What it takes in
77
+
78
+ - **One painting** — a PNG of one character, front-facing, full body, taller than
79
+ wide.
80
+ - **Its See-through layers, twice**: one run on the whole figure (the painting padded
81
+ white to a square) and one on a square crop around the head, because the eyes of a
82
+ full-figure run are too small to separate. Either form See-through reaches you in
83
+ is read:
84
+ - the **ComfyUI wrapper form** — a directory holding `layers.json` and one RGBA PNG
85
+ per layer (flat beside the manifest, or under `parts/<tag>.png`);
86
+ - an **upstream `.psd`** — one pixel layer per tag, the layer name being the tag,
87
+ the stacking order being the draw order.
88
+ - **A character config** (`config.json`) — the part plan, the head box, the bones,
89
+ which bones may pull which layer, the idle's sines, and optionally `constraints`
90
+ in rig-c's own rig-spec shape (ik, transform, path, physics, slider), handed
91
+ to rigc as written once every bone they name resolves; a scene target is a bone
92
+ under `root` a constraint names. `src/config.ts` is its
93
+ schema and documents every field; the loader refuses an unknown or a missing field
94
+ by name, and reads nothing under `note`, a `<name>_note` (a string) or a key
95
+ beginning `x-` (any JSON — a project's own records, such as a past build's gate
96
+ results). [docs/AUTHORING.md](docs/AUTHORING.md) says where each value comes from.
97
+
98
+ ## What it gives out
99
+
100
+ `rig-parts build` ends the way a Spine editor export does — with **one packed atlas
101
+ page** (issue #2):
102
+
103
+ | path under `--out` | what |
104
+ | --- | --- |
105
+ | `check/build/skeleton.json`, `skeleton.atlas`, `skeleton.png` | **the artifact** — Spine 4.3 skeleton data and one packed page, written by `rigc build --pack --page-edges free --pack-shape polygon` and gated under `spine-html`, which holds every rule `spine` measures (on the demo, rigc 2.1.3 and 2.10.1: the 14 rules `validate --profile spine` measures are among the build's 24; 16 are not in `spine`) |
106
+ | `parts/*.png`, `parts.json`, `recomposite_rig.png`, `recomposite_error_rig.png` | the loose parts, each cropped to its alpha box; the record of where every part came from, how many of its pixels were re-taken from the painting, and the recomposite's uncovered holes with their boxes; the flat stack of parts; its error map — red where no part covers a pixel the painting has, blue where a part covers it in the wrong colour |
107
+ | `rig/` | `rig.json` and `motion.json` in rig-c's spec, `mesh_report.json`, the padded `images/` |
108
+ | `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `contact.png`, `motion_heat.png`, `check.json` (with `pack_mode`, the `--page-edges` and `--pack-shape` the page was packed under) |
109
+ | `idle.png`, `idle-indexed.png`, `idle.gif` | with `--loop`: the idle as a lossless APNG (the exactness record), an indexed APNG with one shared palette (the small one) and a GIF; the last two print their palette error |
110
+
111
+ The last lines of a green build are the pack line, printed beside the Spine example
112
+ export's `spineboy.png` as a yardstick (a reference, not a bar), and the three
113
+ artifact paths — here for `examples/sample`:
114
+
115
+ ```
116
+ build: PASS — the packed atlas is the artifact; parts/ and rig/ are the intermediates it was made from
117
+ pack: skeleton.png 477x1151, 20 region(s), 95.0% covered, padding 2, page edges free, shape polygon; page opaque 54.8% (alpha > 0) — spineboy yardstick 1024x256, 40 region(s), 45.8% opaque (alpha > 0), a reference and not a bar
118
+ out/check/build/skeleton.json
119
+ out/check/build/skeleton.atlas
120
+ out/check/build/skeleton.png
121
+ ```
122
+
123
+ The page's edges are free by default: rigc picks the least-area page the parts need,
124
+ not the next power of two. On the two examples, packed by rectangles, that is 967x1338
125
+ and 479x1166, where a power-of-two page is 1024x2048 and 512x2048, and covered goes from 56.3 % to 91.2 % and
126
+ from 49.7 % to 93.4 %. The cost is the one rigc's help states: *"region attachments
127
+ sampling within 1 LSB of the loose build rather than exactly"*. Measured on both
128
+ examples, every idle frame stays within 1 level of the power-of-two build's. The atlas says
129
+ `filter: Linear, Linear` and has no `repeat` line, so `--page-edges pot` is for a
130
+ consumer that mipmaps or repeats the page; it writes exactly the page builds wrote
131
+ before the flag existed. The measurements are in
132
+ [AUTHORING §5, *Page edges*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
133
+
134
+ The pack shape is `polygon` by default (rig-c 2.1): a region only meshes draw is
135
+ packed by its emitted hull, so a neighbour may sit inside its rectangle where the hull
136
+ is not; a region attachment stays its rectangle. On the two examples that takes the
137
+ free page from 967x1338 to 922x1348 (covered 91.2 % to 95.0 %, 3.9 % less area) and
138
+ from 479x1166 to 477x1151 (93.4 % to 95.0 %, 1.7 % less), at the cost class `free`
139
+ already has: every idle frame within 1 level of the `rect` build's, and no figure in
140
+ `check.json` but `pack_mode` moves. `--pack-shape rect` writes the page and atlas
141
+ 0.7.0 wrote. The measurements are in
142
+ [AUTHORING §5, *Pack shape*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
143
+
144
+ The packer is rig-c's, and no other: a packed region is a lossless copy, and an
145
+ atlas written by anything else would have no oracle behind it.
146
+
147
+ ## Painting → parts → rig → browser
148
+
149
+ | stage | tool | what it owns |
150
+ | --- | --- | --- |
151
+ | painting + See-through layers → parts and specs | **rig-parts** | the merge of two runs, the measurements, the rig spec and motion spec |
152
+ | specs → Spine skeleton data | **[rig-c](https://github.com/firejune/rigc)** | compile, the gate (rigc's own validator in an install; the round trip through `spine-core` where the runtime is beside it, as in this repository's CI), the named assertions, the packer, the renderer |
153
+ | skeleton data → a page | **[rig-play](https://github.com/firejune/rig-play)** (formerly spine-html), a sibling project | a DOM renderer; rigc's `spine-html` profile is its policy, and every build here is gated under that profile as well as under `spine` |
154
+
155
+ rig-parts never writes Spine data itself. Everything on disk under `check/build/`
156
+ was written by rig-c after its own gate passed.
157
+
158
+ ## Getting See-through layers
159
+
160
+ See-through is required; how you run it is not. rig-parts does not vendor, embed or
161
+ redistribute See-through code or weights — run it by any of these and point
162
+ rig-parts at what it wrote:
163
+
164
+ | Route | Where | Output rig-parts reads |
165
+ | --- | --- | --- |
166
+ | Hugging Face Space | [24yearsold/see-through-demo](https://huggingface.co/spaces/24yearsold/see-through-demo) (ZeroGPU; upstream states 1-2 extractions a day for a registered user) | the `.psd` it produces |
167
+ | ModelScope demo | [ljsabc/See-Through](https://modelscope.cn/studios/ljsabc/See-Through), linked from the upstream README | the `.psd` it produces |
168
+ | Upstream CLI | `python inference/scripts/inference_psd.py --srcp <image> --save_to_psd` in a checkout of [shitagaki-lab/see-through](https://github.com/shitagaki-lab/see-through) | the `.psd` in `workspace/layerdiff_output/` |
169
+ | ComfyUI wrapper | [jtydhr88/ComfyUI-See-through](https://github.com/jtydhr88/ComfyUI-See-through) on your own ComfyUI box; the optional `comfy` adapter (`rig-parts comfy seethrough`) drives it | the `layers.json` + PNGs it writes |
170
+
171
+ **The head run is needed on every route that was read.** See-through's model
172
+ already separates the head in a second stage of its own: it crops the head the
173
+ first stage found, runs the face tags on that crop at the same `resolution`, and
174
+ pastes the result back **resized to the full image's scale**. So the face comes
175
+ back at the full run's pixel density whichever route ran it, and the separate
176
+ head run on `st_input_head.png` is what gives the eyes more pixels
177
+ (read on 2026-09-28 at see-through `a25a549` and wrapper 0.5.0 `98d754b`):
178
+
179
+ - *Hugging Face Space* — the same two-stage code, with `resolution` capped at
180
+ 1280 (768 by default): the head run is still needed.
181
+ - *ModelScope demo* — its code was not read, so what the head run adds on that
182
+ route has not been measured.
183
+ - *Upstream CLI* — the `.psd` frame is the `resolution`-sided square the first
184
+ stage ran on (1280 by default), and the head stage is pasted into it at that
185
+ scale: the head run is still needed.
186
+ - *ComfyUI wrapper* — `SeeThrough_GenerateLayers` runs both stages with no input
187
+ to change the second, at most `resolution` 2048, which on the examples' 2432 px
188
+ square is 1.19 source px per layer px against 0.77 and 0.47 for their head
189
+ crops; at the 1024 the examples ran, the full run's right eye white is 22×13 px
190
+ and the head run's 60×38: the head run is still needed.
191
+
192
+ **See-through is required; ComfyUI is not.** The wrapper is one route among
193
+ four, and `rig-parts comfy` is only a convenience for that route: every
194
+ stage after See-through reads files, whichever route wrote them. On the two
195
+ public examples each run took 171–199 s through the wrapper (each run's
196
+ `meta.json`, in [spine-parts-examples](https://github.com/firejune/spine-parts-examples)).
197
+
198
+ ### The two images See-through is fed: `inputs`
199
+
200
+ ```sh
201
+ rig-parts inputs --source painting.png --config config.json --out <dir>
202
+ ```
203
+
204
+ cuts the two images See-through is fed: the painting centred on a white square
205
+ (`st_input_full.png`) and, when the config sets `seethrough.head_box`, that
206
+ box at its exact size (`st_input_head.png`). Pure raster, no GPU.
207
+
208
+ ### The optional ComfyUI adapter
209
+
210
+ For a user who has a ComfyUI box, two
211
+ commands drive it; nothing else in the pipeline needs one:
212
+
213
+ ```sh
214
+ rig-parts comfy paint --config config.json --out <dir> --host http://<box>:8188
215
+ rig-parts comfy seethrough --image st_input_full.png --out layers/full --host http://<box>:8188 --offload
216
+ ```
217
+
218
+ `comfy paint` generates `painting_<seed>.png` from the config's inline
219
+ `generation` block — checkpoint, LoRAs, sampler, and an optional OpenPose
220
+ control skeleton it draws itself — and records the prompts verbatim beside it.
221
+ `comfy seethrough` runs the [ComfyUI-See-through](https://github.com/jtydhr88/ComfyUI-See-through)
222
+ wrapper on one image and writes the wrapper form `layers` reads. The host comes
223
+ from `--host` or `COMFY_HOST` and has no default; before anything is uploaded
224
+ the graph is checked against the box's `/object_info`, so a missing node class
225
+ or model is refused by name, and the adapter waits for an empty queue rather
226
+ than queueing behind someone else's job.
227
+
228
+ ## The loop, for an agent
229
+
230
+ ```sh
231
+ rig-parts inputs --source painting.png --config config.json --out inputs # st_input_full.png
232
+ # See-through on st_input_full.png (external, or `rig-parts comfy seethrough`) -> layers/full
233
+ rig-parts layers layers/full # every layer: box, opaque px, depth, plausibility figures; WARN lines
234
+ rig-parts propose --head-box --full layers/full --canvas 1664x2432
235
+ # -> seethrough.head_box into config.json
236
+ rig-parts inputs --source painting.png --config config.json --out inputs # now st_input_head.png too
237
+ # See-through on st_input_head.png (external, or `rig-parts comfy seethrough`) -> layers/head
238
+ rig-parts sheet --source painting.png --layers layers/full --layers layers/head --out sheets/layers.png
239
+ rig-parts assemble --propose-plan --source painting.png --full layers/full --head layers/head --config config.json
240
+ # -> assemble.plan and extend_below_crop
241
+ rig-parts assemble --source painting.png --full layers/full --head layers/head --config config.json --out work
242
+ # -> work/rig: parts.json and parts/; the config holds no bones, meshes, regions or motion yet
243
+ # -> read the `uncovered hole N:` lines and look at work/render/recomposite_error_rig.png:
244
+ # red is painting that no part holds, and no later gate can see it
245
+ # -> a large red hole neither run holds? add an assemble.patches entry (cut from the painting) and assemble again
246
+ rig-parts propose --parts work/rig --source painting.png --out work
247
+ # -> proposal.json, basis.json (what each bone rests on) and render/landmarks.png; per-bone coverage lines;
248
+ # correct it, copy bones/meshes/regions/motion into config.json
249
+ rig-parts propose --parts work/rig --source painting.png --out work --from-config config.json
250
+ # -> LINT lines; exit 1 while any remain
251
+ rig-parts build --config config.json --source painting.png --full layers/full --head layers/head --out out --loop
252
+ # -> read out/check/check.json; every FAIL line names what has to change
253
+ ```
254
+
255
+ At each step the config holds only what that step reads; the one table of what
256
+ that is, step by step, is [docs/AUTHORING.md §4](docs/AUTHORING.md#4-the-command-order).
257
+ The selftest runs this block in order, command by command, on each fetched example,
258
+ from a config holding only what the first step reads (`RL01`). On `scarf` the
259
+ proposal pasted uncorrected stops at the last step, naming its `bottomwear` on
260
+ `CHECK_TIP_OVER_ROOT` — the one correction its tracked config records, a mesh mode
261
+ (issue #118, [its README](examples/scarf/README.md)).
262
+
263
+ `propose` is deliberately not a step of `build`: the proposal is a draft to correct
264
+ against its overlay, and a config with bones is `build`'s input. `rig`, `check` and
265
+ `loop` are the same stages one at a time. [docs/AUTHORING.md](docs/AUTHORING.md) is
266
+ the guide an agent authors from — every config field, what to look at after each
267
+ stage, what each refusal means and which field it points at — and
268
+ [`skills/rig-parts/SKILL.md`](skills/rig-parts/SKILL.md) is the same loop as an
269
+ agent skill.
270
+
271
+ ## Commands
272
+
273
+ | command | does |
274
+ | --- | --- |
275
+ | `inputs --source <png> --config <json> --out <dir>` | the two images See-through is fed: the painting on a white square, and the head box's crop once the config has one |
276
+ | `comfy paint --config --out [--host]` | optional: generate the painting on a ComfyUI box from the config's `generation` block |
277
+ | `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
278
+ | `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth, and its translucent, background and area figures; a `WARN` line for a layer `--propose-plan` will leave out |
279
+ | `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
280
+ | `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule |
281
+ | `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json`, the recomposite and its error map, and list the uncovered holes |
282
+ | `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
283
+ | `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay; print, per bone, which check read it or that none did and why (its role, its kind of segment), and write `basis.json` beside the proposal — what each bone rests on (a joint, a part's mask, a ratio, other bones) and, with `--compare`, whether the config's bone differs |
284
+ | `propose … --keypoints <file> [--person <id>]` | read a posed figure's joints from one explicit file (space, image size, people, each joint observed, occluded or missing, its source): a joint with a position places the neck, hip, chest or a sleeve chain as given, a note says how every joint was used or which rule stood in, and LINT reads the torso and sleeves along the joints instead of by screen y ([AUTHORING §3](docs/AUTHORING.md)) |
285
+ | `propose … --from-config <config>` | draw and LINT the config's current bones (by the joints too, given `--keypoints`) |
286
+ | `compare --left <file> --right <file> [--map <bonemap.json>]` | compare two skeletons — a config, a proposal or a `rig.json` — bone by bone: origin, parent, tip, length and direction per pair, the bones neither side pairs, the required ones missing, and each side's roles read off the spec; no frame is guessed, and nothing pairs by resemblance (`propose --compare` reads origins only) |
287
+ | `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after rig-c's gate is green; each mesh is in the mode its config entry names — `grid` (the lattice, what `propose` writes), `contour` (the traced outline with declared interior points) or `auto` (the contour mesh at alpha 1 and above, reduced and locally refined by rig-c's `reduceMesh` under the author's declared bounds, written only when its motion against the unreduced source on the idle passes the author's `motion` bounds through rig-c's `compareMeshesInMotion`; [AUTHORING §3](docs/AUTHORING.md)); every chain link is turned along its chain and carries its `length`, so a physics constraint added later has a lever, with every offset under it in the turned frame so nothing moves; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared; the config's `constraints` go to rigc as written, and a two-bone ik over keyed links needs `--idle-keys direct` (on `rig` or `build`) |
288
+ | `check --rig --out [--parts] [--source] [--requirements]` | build packed, gated under `spine-html`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json`; on a rig spec with no `parts.json` or no `idle` (a merged rig, issue #77) the gate still runs and every line that needs the missing input says SKIP by name; `--source <painting.png>` adds `SETUP_POSE_VS_SOURCE`, the setup pose against the painting by assemble's recomposite figures; `--requirements <scene.json>` measures what a scene declares of the motion — a contact, a follow fraction, an aim, a joint range, a mesh's stretch, the seam between two parts over the motion — each against the author's own bar, from rig-c's posed frames, one line each, PASS, FAIL or NOT MEASURABLE (AUTHORING §7, *Declared requirements*) |
289
+ | `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
290
+ | `build --config --source --full --head --out [--seam] [--project] [--loop] [--requirements] [--idle-keys ctl\|direct]` | assemble, rig and check in one process, stopping at the first refusal; `--requirements` is forwarded to check, `--idle-keys` to rig |
291
+ | `compose --scene <scene.json> --out [--requirements] [--accept]` | bind several characters' green builds into one rig (or, under `--accept`, builds failing only the bars named — for looking, not shipping): every name prefixed `<id>:` under one shared root, each character's offset added at its first-level bones, an optional background plate (its provenance recorded, judged by nothing) drawn first, and the draw order between the characters' slots as the scene file declares it — nothing inferred, nothing scaled; gated through rig-c as `rig` is, then checked ([AUTHORING §7](docs/AUTHORING.md), *Composing several characters*) |
292
+
293
+ `rig-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
294
+ reason is a FAIL line), 2 a usage error or a command this version does not implement.
295
+
296
+ What 1.0.0 holds stable — these commands and their flags, the config
297
+ fields, the files a build writes and their spec ids, the environment, and what this
298
+ package relies on in rig-c — and what is not, is [docs/STABILITY.md](docs/STABILITY.md).
299
+ Coming from 0.16.0: [docs/MIGRATION.md](docs/MIGRATION.md).
300
+
301
+ ## What it does not do
302
+
303
+ These are limits of the approach, stated so nobody reads more into a green run:
304
+
305
+ - **One depth value per layer.** See-through gives each layer a single depth, so a
306
+ layer that is in front of another in one place and behind it in another is drawn
307
+ right in the still (the painting's own pixels are projected onto it) and wrong once
308
+ it moves.
309
+ - **No expression or lip-sync, and no jiggle.** The mouth is one layer; there is no
310
+ mouth-shape set and no expression axis, and nothing here proposes, keys or judges a
311
+ soft region. An expression that moves the face's parts, and a jiggle, can be built on
312
+ the rig it writes ([below](#what-can-be-built-on-top)); lip-sync cannot.
313
+ - **Occluded pixels are See-through's synthesis**, not the artist's. How much of a rig
314
+ that is, is below.
315
+ - **No success rate is claimed.** Ten characters have been measured stage by stage
316
+ against the reference implementation this package ports — the two public examples
317
+ here and eight private ones, one See-through seed each — and all ten check green
318
+ on the gates, the seam and the loop. The judgement lines `check` added since
319
+ (issue #11) have been measured on the three public examples only (the third,
320
+ `scarf`, was made after the port and has no reference output). That is an
321
+ existence proof, not a rate.
322
+ - **`loop` writes GIF and APNG (lossless and indexed), not WebP**: an animated WebP needs a VP8/VP8L
323
+ encoder, which this package does not carry.
324
+ - **The proposer assumes a standing figure unless it is told the pose.** Its ratios
325
+ put the hip below the chest and hang the sleeves from the shoulders. A seated or
326
+ reclining figure needs `propose --keypoints`, a file of its joints from any pose
327
+ estimator or a hand; this package does not estimate a pose itself, and its
328
+ keypoint support has been measured on generated figures and on files read off the
329
+ examples' own configs, never on an estimator's output.
330
+ - **The proposer reads tags, not pictures.** A swinging element painted inside another
331
+ layer (a sash tail in the skirt), hair that is none of the shapes it knows, and
332
+ whether an accessory swings are the corrector's to add. The one accessory shape it
333
+ measures is a hanging strand on a headwear or earwear layer: each gets a pendulum
334
+ chain, and a strand it cannot chain is named in a note rather than left stiff.
335
+
336
+ ### What can be built on top
337
+
338
+ This package makes no expression and no jiggle, but the rig it writes is the material for
339
+ both, and the config has a field for each. [docs/AUTHORING.md](docs/AUTHORING.md) has a recipe
340
+ for each — a config fragment, the `check` lines it passes with, and its limit:
341
+
342
+ - [**Expressions and other named animations**](docs/AUTHORING.md#recipe-expressions-and-other-named-animations):
343
+ the face's parts are bones; animations written over them in rig-c's motion-spec shape and
344
+ named by `motion.animations_from` are built and gated beside the idle on every build. One
345
+ attachment per slot, so a part moves, turns and scales but is never swapped for another
346
+ image; `check` measures the idle alone.
347
+ - [**A soft region driven by physics**](docs/AUTHORING.md#recipe-a-soft-region-driven-by-physics)
348
+ (a bust, the hips): a bone the config authors, a contour-mode region that hands it the patch,
349
+ and a `physics` constraint on it; `examples/sample` carries one. Under the sample's slow
350
+ 4 s idle the bust moves at most 0.066 rig px and the hips not at all: the spring answers
351
+ faster keys on the body. The sample's `hop`, a named animation keying only `hip`, moves the
352
+ bust up to 6.583 and the hips up to 4.432 rig px; the region's `band` caps how far a patch may
353
+ go before the texture-stretch ceiling, and `check` measures the idle alone.
354
+
355
+ ### How much of a rig the model painted
356
+
357
+ `parts.json` splits each part's opaque pixels (`opaque_px`) into `visible_px` — no
358
+ later layer of its See-through run is opaque in front of them — and `occluded_px`,
359
+ and counts the visible ones whose colour was not taken from the painting
360
+ (`visible_not_projected_px`). On the three public examples, from the totals of their
361
+ `expected/parts.json` (the default `--project core`; `scarf`'s painting patches are
362
+ whole painting pixels, so they count as taken):
363
+
364
+ | | opaque px | occluded | visible, not projected | taken from the painting |
365
+ | --- | --- | --- | --- | --- |
366
+ | `sample` | 298,363 | 79,122 (26.5 %) | 35,979 (12.1 %) | 183,262 (61.4 %) |
367
+ | `demo` | 765,924 | 202,343 (26.4 %) | 75,626 (9.9 %) | 487,955 (63.7 %) |
368
+ | `scarf` | 283,813 | 78,503 (27.7 %) | 56,699 (20.0 %) | 148,611 (52.4 %) |
369
+
370
+ The occluded share is art the painting does not show — a back-hair layer, a neck, the
371
+ parts of an ear under the hair — and is See-through's synthesis by necessity; it is
372
+ what the decomposition is for. The visible-but-not-projected share is synthesis where
373
+ the painting was there to be taken: a part too thin for the reference rule's 5x5
374
+ eroded core, an anti-aliased fringe below alpha 250, a rim beside a layer in front, a
375
+ pixel refused for drift. `--project visible` keeps the erosion only along a rim with a
376
+ layer in front, which takes that share to 22,476 (7.5 %) on `sample` and 49,672 (6.5 %)
377
+ on `demo` (from the `parts.json` of a `--project visible` build). On the eleven thin
378
+ head parts — brows, lashes, irises, eye whites, ears, mouth — it takes 296 of
379
+ `sample`'s 1,289 visible pixels where `core` takes 110, and 824 of `demo`'s 2,162
380
+ where `core` takes 343. What neither rule takes is the fringe: a per-pixel
381
+ classification of those parts under `visible`, recorded with the change that added
382
+ the flag, puts 813 of `sample`'s 993 still-unprojected pixels and 879 of `demo`'s
383
+ 1,338 below alpha 250. Where such a fringe keeps the stack off the painting and a part
384
+ beneath holds the pixel in the painting's colour, `assemble` clears it instead, and
385
+ the assemble line and `parts.json` say how many pixels, of which part, over which
386
+ (AUTHORING §5, *The fringe push-back*; the figures in this section were measured
387
+ before it, at v0.14.0).
388
+
389
+ ## Requirements
390
+
391
+ [Bun](https://bun.sh) 1.2 or later. `npm install -g rig-parts` installs the
392
+ `rig-parts` command; it hands off to Bun and says so in one sentence if Bun is not
393
+ on `PATH`. **`rig-parts` is the package's name; versions up to 0.16.0 were published
394
+ as `spine-parts`, its former name**, and the install keeps a `spine-parts` command
395
+ beside `rig-parts` for the transition, the same launcher under both names. On npm
396
+ the alias `spine-parts` stays published beside `rig-parts` at every version,
397
+ carrying the same files with only the `name` in `package.json` changed, so a
398
+ project that depends on the former name keeps receiving every release; a new
399
+ install names `rig-parts` ([RELEASING.md](RELEASING.md), *The registry side*). rig-c
400
+ comes with it as a dependency. No Spine runtime does: since
401
+ rig-c 2.0 the installed `rigc` gates every build with its own validator over the
402
+ compiled model document, and `rigc --version` says `entry: cli_core.ts —
403
+ @esotericsoftware/spine-core absent`. With `@esotericsoftware/spine-core` installed
404
+ beside it, the same `rigc` runs the spine-core round trip instead, as this repository's
405
+ selftest and CI do; `check.json`'s `rigc_entry` records which one gated a build.
406
+
407
+ ## Licence posture
408
+
409
+ rig-parts is MIT, and it depends on rig-c, whose output is Spine skeleton data:
410
+ using what it produces in a product requires a Spine Editor licence, as any Spine data
411
+ does. Neither package installs Esoteric Software's `spine-core` any more; this
412
+ repository uses it in development, for the round trip in its selftest and CI. [NOTICE.md](NOTICE.md) records that chain and every other
413
+ third-party term this package touches, See-through's included.
414
+
415
+ ## Development
416
+
417
+ ```sh
418
+ bun install
419
+ bun run typecheck # tsc --noEmit, strict
420
+ bun run lint # one rule: no explicit any
421
+ bun run fetch-examples # the public examples' paintings and layers, into examples/*/inputs (needs a network)
422
+ bun run selftest # every gate's controls; with the examples fetched, build on each of them against its expected/
423
+ bun run smoke # pack, install into an empty directory, run from the install (needs a network)
424
+ ```
425
+
426
+ [CLAUDE.md](CLAUDE.md) is the doctrine, [CONTRIBUTING.md](CONTRIBUTING.md) the
427
+ practice, [RELEASING.md](RELEASING.md) the cut, [docs/STABILITY.md](docs/STABILITY.md)
428
+ the interface it holds.
429
+
430
+ ## Licence
431
+
432
+ MIT — see [LICENSE](LICENSE). Third-party terms: [NOTICE.md](NOTICE.md).
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * rig-parts' `bin` entry has one job: hand off to Bun.
6
+ *
7
+ * npm's `bin` field has to be something any installed Node can run, but
8
+ * rig-parts is a Bun program (it runs its TypeScript sources directly, and so
9
+ * does rig-c underneath it). Without this file, a machine with no Bun
10
+ * would fail as a bare `env: bun: No such file or directory`, with no hint
11
+ * why. So: if `bun` is on PATH, run the real CLI (cli.ts, next to this file)
12
+ * under it and disappear — argv, stdio, the exit code and signals all pass
13
+ * straight through. If it isn't, say so once and stop. No downloads, no
14
+ * network, no writes — just the hand-off or the message.
15
+ *
16
+ * package.json installs this one file under two command names: `rig-parts`,
17
+ * and the package's former name, kept as a second `bin` entry for the
18
+ * transition so a script written against it still runs.
19
+ */
20
+
21
+ const path = require('node:path');
22
+ const { spawnSync } = require('node:child_process');
23
+
24
+ const cli = path.join(__dirname, '..', 'cli.ts');
25
+ const result = spawnSync('bun', [cli, ...process.argv.slice(2)], { stdio: 'inherit' });
26
+
27
+ if (result.error) {
28
+ if (result.error.code === 'ENOENT') {
29
+ process.stderr.write('rig-parts runs on Bun, which was not found on PATH — install it from https://bun.sh\n');
30
+ } else {
31
+ process.stderr.write(`rig-parts: could not launch bun: ${result.error.message}\n`);
32
+ }
33
+ process.exit(1);
34
+ }
35
+
36
+ if (result.signal) {
37
+ // A signal (e.g. Ctrl-C) killed the child — die the same way instead of
38
+ // inventing an exit code, so the caller sees what it would have seen running
39
+ // bun directly.
40
+ process.kill(process.pid, result.signal);
41
+ } else {
42
+ process.exit(typeof result.status === 'number' ? result.status : 1);
43
+ }