hayao 0.2.0 → 0.3.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/README.md +113 -24
- package/bin/create-hayao.mjs +380 -0
- package/bin/hayao-mcp-cli.mjs +11 -0
- package/dist/app/browser.d.ts +33 -2
- package/dist/app/game.d.ts +47 -2
- package/dist/app/tuning.d.ts +68 -0
- package/dist/art/palette.d.ts +41 -0
- package/dist/audio/adaptive.d.ts +58 -0
- package/dist/audio/album.d.ts +16 -0
- package/dist/audio/analysis.d.ts +59 -0
- package/dist/audio/audio.d.ts +21 -0
- package/dist/audio/chord.d.ts +17 -0
- package/dist/audio/genres.d.ts +11 -0
- package/dist/audio/lint.d.ts +29 -0
- package/dist/audio/match.d.ts +38 -0
- package/dist/audio/music.d.ts +88 -0
- package/dist/audio/pcm.d.ts +54 -0
- package/dist/audio/quality.d.ts +28 -0
- package/dist/audio/reverb.d.ts +15 -0
- package/dist/audio/synth.d.ts +56 -0
- package/dist/audio/theory.d.ts +64 -0
- package/dist/content/campaign.d.ts +69 -0
- package/dist/content/generate.d.ts +78 -0
- package/dist/content/level.d.ts +93 -0
- package/dist/content/worldgraph.d.ts +73 -0
- package/dist/core/dmath.d.ts +2 -0
- package/dist/hayao.global.js +15 -0
- package/dist/index.d.ts +30 -1
- package/dist/index.js +4293 -434
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +15 -0
- package/dist/input/source.d.ts +52 -1
- package/dist/mcp.js +31225 -0
- package/dist/rasterize-worker-lite.mjs +13 -0
- package/dist/render/canvas.d.ts +6 -1
- package/dist/render/commands.d.ts +46 -0
- package/dist/render/paint.d.ts +33 -0
- package/dist/render/renderer.d.ts +22 -0
- package/dist/render/svg.d.ts +4 -1
- package/dist/render/svgString.d.ts +6 -2
- package/dist/scene/cameraController.d.ts +42 -0
- package/dist/scene/node.d.ts +17 -4
- package/dist/scene/nodes.d.ts +14 -0
- package/dist/scene/parallax.d.ts +15 -0
- package/dist/studio/mcpMain.d.ts +1 -0
- package/dist/studio/mcpServer.d.ts +2 -0
- package/dist/studio/record.d.ts +54 -0
- package/dist/studio/run.d.ts +78 -0
- package/dist/studio/session.d.ts +80 -0
- package/dist/studio/timeline.d.ts +35 -0
- package/dist/studio/vitePlugin.d.ts +6 -0
- package/dist/studio-plugin.js +228 -0
- package/dist/ui/overlay.d.ts +6 -0
- package/dist/verify/audioFilmstrip.d.ts +39 -0
- package/dist/verify/ethnography.d.ts +67 -0
- package/dist/verify/gates.d.ts +160 -0
- package/dist/verify/layout.d.ts +30 -3
- package/dist/verify/ramp.d.ts +40 -0
- package/dist/world.d.ts +38 -2
- package/dist-studio/assets/index-C7tty_Wo.js +109 -0
- package/dist-studio/assets/index-CM3tjRQo.css +1 -0
- package/dist-studio/index.html +18 -0
- package/docs/API.md +233 -9
- package/docs/CONVENTIONS.md +223 -0
- package/docs/EMBED.md +85 -0
- package/docs/QUICKSTART.md +129 -0
- package/docs/STUDIO.md +97 -0
- package/docs/VERIFICATION.md +226 -0
- package/package.json +39 -6
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Verification — how an AI proves a hayao game works
|
|
2
|
+
|
|
3
|
+
The whole engine is shaped so you can verify a game **without a browser and
|
|
4
|
+
without playing it**. Build the harness *before* the content; unverified content
|
|
5
|
+
is presumed broken (in-head verification has a documented failure rate).
|
|
6
|
+
|
|
7
|
+
Two channels, in order:
|
|
8
|
+
|
|
9
|
+
1. **Numeric channel** — headless `step()`, probes, the solver, and determinism
|
|
10
|
+
checks. Proves *behavior* and *beatability*. This is where correctness lives.
|
|
11
|
+
2. **Visual channel** — a headless SVG screenshot. Judges *looks* only — palette,
|
|
12
|
+
layout, legibility. Never correctness.
|
|
13
|
+
|
|
14
|
+
Everything below runs in Node under Vitest (`npm test`) or the CI script
|
|
15
|
+
(`npm run verify`).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Channel 1a — scripted playthrough (any game)
|
|
20
|
+
|
|
21
|
+
Drive a world with an input log and assert on its probe. No DOM.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createWorld } from '@hayao';
|
|
25
|
+
import { myGame } from './game';
|
|
26
|
+
|
|
27
|
+
const world = createWorld(myGame); // a live, deterministic World
|
|
28
|
+
world.step(['right']); // one fixed step, 'right' held
|
|
29
|
+
world.step([]); // release (edges need a rising edge)
|
|
30
|
+
world.step(['confirm']);
|
|
31
|
+
expect(world.probe().score).toBe(1); // assert on game state, not pixels
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Helpers: `scriptedPlaythrough(world, [hold(['right'], 30), wait(10)])` returns a
|
|
35
|
+
probe after each segment; `pump(world, n, actions)` advances n steps. Hold keys
|
|
36
|
+
for several frames — a 1-frame synthetic tap is treated as a real short tap.
|
|
37
|
+
|
|
38
|
+
## Channel 1b — winnability solver (puzzles / turn-based)
|
|
39
|
+
|
|
40
|
+
Keep the rules in a pure module implementing `Puzzle<State, Move>` (no engine
|
|
41
|
+
imports), then prove every level beatable by search:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { solve, assertSolvable, type Puzzle } from '@hayao';
|
|
45
|
+
|
|
46
|
+
const puzzle: Puzzle<State, Move> = {
|
|
47
|
+
initial: (level) => …,
|
|
48
|
+
moves: (s) => […],
|
|
49
|
+
apply: (s, m) => …, // fold in a DETERMINISTIC opponent here if any
|
|
50
|
+
isWin: (s) => …,
|
|
51
|
+
isDead: (s) => …, // optional: prune lost states
|
|
52
|
+
key: (s) => canonicalString(s),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const r = assertSolvable(puzzle, { level: 0, maxDepth: 80 }); // throws if unwinnable
|
|
56
|
+
console.log(r.depth, r.nodes); // shortest solution length + search cost
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A deterministic opponent means winnability is single-agent BFS — no adversarial
|
|
60
|
+
branching. `solve` returns the shortest `path`, which you can replay through the
|
|
61
|
+
real game (Channel 1a) to prove the *view* agrees with the *logic*.
|
|
62
|
+
|
|
63
|
+
## Channel 1c — determinism (the regression net)
|
|
64
|
+
|
|
65
|
+
Same seed + same input log must produce bit-identical state hashes. If not, some
|
|
66
|
+
nondeterminism leaked in (Math.random, Date.now, Set iteration, unordered map).
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { assertDeterministic, assertSnapshotStable } from '@hayao';
|
|
70
|
+
|
|
71
|
+
assertDeterministic(() => createWorld(myGame), inputLog); // runs twice, compares every step's hash
|
|
72
|
+
assertSnapshotStable(() => createWorld(myGame), inputLog, 20); // snapshot→restore reproduces the tail exactly
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`world.hash()` is the structural hash; `world.snapshot()` / `world.restore()` are
|
|
76
|
+
save/undo/time-travel. Mark pure-view nodes `cosmetic = true` so transient display
|
|
77
|
+
(move counters, particles, tweened positions) never pollutes the canonical hash.
|
|
78
|
+
|
|
79
|
+
## Channel 1d — the golden replay corpus (portfolio-wide refactor net)
|
|
80
|
+
|
|
81
|
+
Every example pins its full scripted run's final `world.hash()` in a committed
|
|
82
|
+
`examples/<slug>/golden.json`, checked by `t.golden(label, hash)` in its
|
|
83
|
+
verify suite. This turns "did my engine refactor change ANY game's behavior?"
|
|
84
|
+
into one command: `npm run verify` — a pure refactor leaves every golden hash
|
|
85
|
+
untouched; any divergence names the game and the run that changed.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// in examples/<slug>/verify.ts, after the full run:
|
|
89
|
+
t.golden('full run', world.hash());
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Re-record policy.** Golden divergence is a failing gate, on purpose. When a
|
|
93
|
+
behavior change is *intentional* (tuning, new content, a deliberate engine
|
|
94
|
+
change), re-record and say so:
|
|
95
|
+
|
|
96
|
+
1. `UPDATE_GOLDEN=1 npm run verify` — rewrites the affected `golden.json`.
|
|
97
|
+
2. Commit the new hashes IN THE SAME COMMIT as the change, with a line in the
|
|
98
|
+
message naming which games' behavior changed and why.
|
|
99
|
+
|
|
100
|
+
Never re-record to silence a divergence you can't explain — an unexplained
|
|
101
|
+
golden change IS the bug the corpus exists to catch.
|
|
102
|
+
|
|
103
|
+
## Channel 2 — the visual screenshot (looks only)
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { HeadlessRenderer, createWorld } from '@hayao';
|
|
107
|
+
|
|
108
|
+
const world = createWorld(myGame);
|
|
109
|
+
world.step([]);
|
|
110
|
+
const r = new HeadlessRenderer({ width: 1280, height: 720, background: '#f3ecdb' });
|
|
111
|
+
r.draw(world.render());
|
|
112
|
+
writeFileSync('shots/title.svg', r.toSVGString()); // a real vector screenshot, in Node
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Because rendering is SVG, the screenshot is **resolution-independent and text is
|
|
116
|
+
crisp** — no canvas fuzz, no GPU. In the browser, `?capture=1` exposes
|
|
117
|
+
`window.__hayao` (`pump`, `probe`, `hash`, `shot`, `save`, `key`) for scripted
|
|
118
|
+
sessions and aesthetic captures — but you rarely need it, because the DOM is
|
|
119
|
+
already inspectable and the sim already runs in Node.
|
|
120
|
+
|
|
121
|
+
For MOTION, use the filmstrip instead of a single frame: it samples a whole
|
|
122
|
+
run into one SVG contact sheet, so pacing, readability under load, and
|
|
123
|
+
layering during play are judgeable from a file.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { renderFilmstrip } from '@hayao';
|
|
127
|
+
t.artifact('run-filmstrip.svg', renderFilmstrip(createWorld(myGame), inputFrames, {
|
|
128
|
+
width: myGame.width, height: myGame.height, background: myGame.background, panels: 12,
|
|
129
|
+
}));
|
|
130
|
+
// → shots/<slug>/run-filmstrip.svg — open it and actually look.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Channel 3 — feel probes (quality proxies)
|
|
134
|
+
|
|
135
|
+
Correctness channels prove the game is *not broken*; they say nothing about
|
|
136
|
+
whether it is *good*. Feel probes give quality a measurable proxy: compute
|
|
137
|
+
metrics from a per-frame probe timeline of the winning run, and gate them on
|
|
138
|
+
windows YOU tune per game. A window is a design decision recorded as a test —
|
|
139
|
+
retuning that wrecks pacing fails loudly instead of silently.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { recordTimeline, firstFrame, inputDensity, longestLull, changeFrames, isMonotonic } from '@hayao';
|
|
143
|
+
|
|
144
|
+
const tl = recordTimeline(createWorld(myGame), winningLog);
|
|
145
|
+
firstFrame(tl, (p) => p.kills > 0); // time-to-first-meaningful-action
|
|
146
|
+
inputDensity(winningLog); // engagement: waiting ↔ mashing
|
|
147
|
+
longestLull(changeFrames(tl, 'score'), tl.length); // dead-air detector
|
|
148
|
+
isMonotonic(depths, 'up', slack); // difficulty ramps with breathers
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
What to gate is genre truth: a stealth game gates its hide/move alternation,
|
|
152
|
+
an incremental its unlock cadence, a puzzle its solve-depth curve. Two rules:
|
|
153
|
+
(1) derive windows from a run you have actually judged as feeling right —
|
|
154
|
+
never invent them; (2) keep them generous enough that tuning breathes, tight
|
|
155
|
+
enough that pacing regressions fail. These are proxies, not proof of fun —
|
|
156
|
+
the filmstrip and a human/AI look stay in the loop.
|
|
157
|
+
|
|
158
|
+
## Channel 4 — feel gates (the professional floor, made checkable)
|
|
159
|
+
|
|
160
|
+
Channel 3 measures *pacing* from a timeline; Channel 4 gates the small set of feel
|
|
161
|
+
**fundamentals** that separate amateur from professional and that are genuinely
|
|
162
|
+
mechanical, not matters of taste. Each returns human-readable issues (empty = pass),
|
|
163
|
+
exactly like `layoutIssues` — so wiring one in is one line. This is the direct
|
|
164
|
+
answer to "green tests, dead game": turn more of *feel* green.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { forgivenessIssues, graceWindowIssues, feedbackIssues,
|
|
168
|
+
salienceIssues, telegraphIssues, cameraIssues, lookAheadIssues } from '@hayao';
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
1. **Forgiveness.** `forgivenessIssues(cfg)` audits the grace windows (coyote,
|
|
172
|
+
jump-buffer, corner-nudge) against a floor; `graceWindowIssues(label, W, accepts)`
|
|
173
|
+
*behaviourally* proves a window accepts an input `0..W` frames late and refuses
|
|
174
|
+
`W+1` — the exact edge FUN law 5 demands. (updrift proves a 4-frame coyote window
|
|
175
|
+
by walking a rig off a lip and jumping D frames later.)
|
|
176
|
+
2. **Feedback completeness.** Declare a `FeedbackContract` (event → channels +
|
|
177
|
+
shake/hit-stop), then `feedbackIssues(contract, events)` proves every significant
|
|
178
|
+
event answers on ≥ 2 senses within the frame, with juice bounded (min to read,
|
|
179
|
+
max to not nauseate).
|
|
180
|
+
3. **Readability.** `salienceIssues(render(), avatarFill, background)` proves the
|
|
181
|
+
avatar out-contrasts both the background and the median scenery fill (WCAG
|
|
182
|
+
luminance, from pure draw data). `telegraphIssues(timeline, minFrames)` proves
|
|
183
|
+
every threat activation is preceded by ≥ `minFrames` of telegraph — reactive play
|
|
184
|
+
needs danger to announce itself.
|
|
185
|
+
4. **Camera lawfulness.** `cameraIssues(samples)` proves the follow never snaps
|
|
186
|
+
(bounded speed) or jerks (bounded acceleration); `lookAheadIssues(cam, target)`
|
|
187
|
+
proves it leads the motion rather than trailing. Sample the *follow* position
|
|
188
|
+
(exclude screen shake), and drop the pre-start frame (no camera exists yet).
|
|
189
|
+
|
|
190
|
+
The full four-gate suite wired on a real game lives in
|
|
191
|
+
[`examples/updrift/verify.ts`](../examples/updrift/verify.ts); the authoring side is
|
|
192
|
+
[docs/JUICE.md](JUICE.md). Gate on the fundamentals; the *soul* above the floor
|
|
193
|
+
stays authored (a director, not a proof) — but the floor no amateur clears is now
|
|
194
|
+
machine-enforced.
|
|
195
|
+
|
|
196
|
+
**Portfolio floor (`npm run feel`).** A game declares its contract once —
|
|
197
|
+
`export const feel: FeelSpec` in `game.ts` (avatar fill → salience; controller
|
|
198
|
+
config → forgiveness; feedback contract → feedback; `scrolls` → camera). The audit
|
|
199
|
+
runs every gate each spec enables across all games and exits non-zero on any
|
|
200
|
+
failure; it is part of `npm run verify`, so the floor rises for ALL output, not one
|
|
201
|
+
game. Declare only what is honestly true — a false contract is worse than none.
|
|
202
|
+
|
|
203
|
+
## Channel 5 — the vision judge (`npm run judge`)
|
|
204
|
+
|
|
205
|
+
The gates prove the floor mechanically; they can't see that a scene is *empty* or
|
|
206
|
+
*flat*. `npm run judge` renders each game headlessly to PNG (SVG → `@resvg/resvg-js`,
|
|
207
|
+
no browser) so a multimodal model LOOKS at the pixels and scores them against
|
|
208
|
+
[JUDGE.md](JUDGE.md) — then fixes what a human would wince at, staying cosmetic (the
|
|
209
|
+
golden hash unchanged). It's the one judge that can't be a pure function, which is
|
|
210
|
+
why an AI-first engine — deterministic sim, headless render — is uniquely built to
|
|
211
|
+
run it in a loop. Drive it with the `/judge` skill.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## The gate (`npm run verify`)
|
|
216
|
+
|
|
217
|
+
The CI verifier proves every example winnable, replays a solution through the
|
|
218
|
+
real game, and checks determinism — exiting non-zero on any failure. Wire your
|
|
219
|
+
game's puzzle + game into `scripts/verify.ts` and it becomes a pipeline gate.
|
|
220
|
+
|
|
221
|
+
## Invariants
|
|
222
|
+
|
|
223
|
+
- All randomness flows through `world.rng`. No `Math.random()` in `src/` or games.
|
|
224
|
+
- No wall-clock in the sim (`Date.now`, `performance.now`, argless `new Date`).
|
|
225
|
+
- Fixed tree order; ordered collections for logic (no `Set`/object-key order).
|
|
226
|
+
- Every shipped level: a solver proof OR a scripted playthrough. No exceptions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hayao",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An AI-first game engine: a deterministic, headless-native simulation kernel with a Godot-style scene tree, pluggable renderers (SVG/Canvas/headless), and a built-in verification harness — designed so an LLM can author, test, and prove correct a whole game without ever opening a browser.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,15 +26,31 @@
|
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"module": "./dist/index.js",
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
|
+
"bin": {
|
|
30
|
+
"create-hayao": "./bin/create-hayao.mjs",
|
|
31
|
+
"hayao-mcp": "./bin/hayao-mcp-cli.mjs"
|
|
32
|
+
},
|
|
29
33
|
"exports": {
|
|
30
34
|
".": {
|
|
31
35
|
"types": "./dist/index.d.ts",
|
|
32
36
|
"import": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./studio": {
|
|
39
|
+
"types": "./dist/studio/vitePlugin.d.ts",
|
|
40
|
+
"import": "./dist/studio-plugin.js"
|
|
33
41
|
}
|
|
34
42
|
},
|
|
35
43
|
"files": [
|
|
36
44
|
"dist",
|
|
37
|
-
"
|
|
45
|
+
"dist-studio",
|
|
46
|
+
"bin/create-hayao.mjs",
|
|
47
|
+
"bin/hayao-mcp-cli.mjs",
|
|
48
|
+
"docs/QUICKSTART.md",
|
|
49
|
+
"docs/API.md",
|
|
50
|
+
"docs/VERIFICATION.md",
|
|
51
|
+
"docs/CONVENTIONS.md",
|
|
52
|
+
"docs/EMBED.md",
|
|
53
|
+
"docs/STUDIO.md"
|
|
38
54
|
],
|
|
39
55
|
"engines": {
|
|
40
56
|
"node": ">=18"
|
|
@@ -45,19 +61,36 @@
|
|
|
45
61
|
"scripts": {
|
|
46
62
|
"dev": "vite",
|
|
47
63
|
"build": "tsc --noEmit && vite build",
|
|
48
|
-
"build:lib": "rm -rf dist && esbuild src/index.ts --bundle --format=esm --platform=neutral --target=es2022 --outfile=dist/index.js --sourcemap && tsc -p tsconfig.build.json",
|
|
64
|
+
"build:lib": "rm -rf dist && esbuild src/index.ts --bundle --format=esm --platform=neutral --target=es2022 --outfile=dist/index.js --sourcemap && esbuild src/index.ts --bundle --minify --format=esm --platform=neutral --target=es2022 --outfile=dist/index.min.js && esbuild src/index.ts --bundle --minify --format=iife --global-name=hayao --platform=neutral --target=es2022 --outfile=dist/hayao.global.js && esbuild src/studio/mcpMain.ts --bundle --format=esm --platform=node --target=es2022 --outfile=dist/mcp.js --external:@resvg/resvg-js --external:hayao && esbuild src/studio/vitePlugin.ts --bundle --format=esm --platform=node --target=es2022 --outfile=dist/studio-plugin.js --external:vite && cp scripts/rasterize-worker-lite.mjs dist/rasterize-worker-lite.mjs && tsc -p tsconfig.build.json",
|
|
65
|
+
"build:studio": "vite build -c vite.studio.config.ts",
|
|
49
66
|
"check": "tsc --noEmit",
|
|
50
67
|
"test": "vitest run",
|
|
51
68
|
"test:watch": "vitest",
|
|
52
69
|
"api": "tsx scripts/api-digest.ts",
|
|
53
70
|
"invariants": "tsx scripts/invariants.ts",
|
|
54
|
-
"verify": "tsx scripts/
|
|
55
|
-
"
|
|
56
|
-
"
|
|
71
|
+
"verify": "tsx scripts/verify-all.ts",
|
|
72
|
+
"eval": "tsx scripts/eval.ts",
|
|
73
|
+
"create": "node bin/create-hayao.mjs",
|
|
74
|
+
"prepublishOnly": "npm run check && npm test && npm run build:lib && npm run build:studio",
|
|
75
|
+
"relay": "node scripts/relay.mjs",
|
|
76
|
+
"palette": "tsx scripts/palette-audit.ts",
|
|
77
|
+
"audio": "tsx scripts/audio-render.ts",
|
|
78
|
+
"feel": "tsx scripts/feel.ts",
|
|
79
|
+
"judge": "tsx scripts/judge.ts",
|
|
80
|
+
"thumbs": "tsx scripts/site-thumbs.ts"
|
|
57
81
|
},
|
|
58
82
|
"devDependencies": {
|
|
83
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
84
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
59
85
|
"@types/node": "^22.10.0",
|
|
86
|
+
"@types/qrcode": "^1.5.6",
|
|
87
|
+
"@types/react": "^19.2.17",
|
|
88
|
+
"@types/react-dom": "^19.2.3",
|
|
60
89
|
"esbuild": "^0.28.0",
|
|
90
|
+
"leva": "^0.10.1",
|
|
91
|
+
"qrcode": "^1.5.4",
|
|
92
|
+
"react": "^19.2.7",
|
|
93
|
+
"react-dom": "^19.2.7",
|
|
61
94
|
"tsx": "^4.19.0",
|
|
62
95
|
"typescript": "^5.7.0",
|
|
63
96
|
"vite": "^6.0.0",
|