incanto 0.68.0 → 0.70.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/assets/catalog.json +9 -5
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +19 -5
- package/bin/incanto-check.mjs +92 -15
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +128 -5
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +165 -44
- package/bin/incanto.mjs +4 -2
- package/dist/2d.d.ts +285 -36
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +158 -10
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-CmNF01gA.js} +61 -8
- package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-DaMxqfNE.d.ts} +33 -14
- package/dist/{behavior-DoFPYrgo.d.ts → behavior-DZExDn9o.d.ts} +809 -44
- package/dist/{create-game-IZIydDwI.js → create-game-Bwvh6q8A.js} +148 -61
- package/dist/{create-game-DbWtVTxD.js → create-game-C7ffQWW7.js} +103 -49
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1066 -864
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-CybQXNqS.js → environment-presets-D6Q5BxeE.js} +299 -46
- package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
- package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
- package/dist/{gameplay-DM1eu_cV.js → gameplay-BfHkuzVb.js} +825 -221
- package/dist/gameplay.d.ts +187 -7
- package/dist/gameplay.js +1 -1
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +156 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-CcB533FR.d.ts → loader-Cff09LMm.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CE0Qvy3V.js} +136 -11
- package/dist/{physics-3d-bG3n70Ky.js → physics-3d-CpH-2gn5.js} +104 -23
- package/dist/{teardown-D2NEmxPB.js → picking-CQJ_PJKh.js} +106 -14
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-3ta-2Xig.js → register-6DYnKZcy.js} +652 -831
- package/dist/{register-ibCjm-wH.js → register-Bkk0wSDB.js} +348 -30
- package/dist/{replay-CYvhVHHN.js → replay-DjAkAzMq.js} +224 -14
- package/dist/{replay-Dvn8aeBd.d.ts → replay-Dmw-PKQu.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-BC4PNtJX.js → save-slots-BXVg148r.js} +4558 -2294
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-DhrSzZIB.d.ts → split-screen--k-XpBjr.d.ts} +36 -4
- package/dist/{split-screen-CYwDkbLF.js → split-screen-PL78oVXP.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-D7RIqXYF.js → src-Cxfiv1Hg.js} +2 -17
- package/dist/test-iHYVUcDK.js +4036 -0
- package/dist/test.d.ts +542 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +54 -3
- package/dist/vite.js +349 -17
- package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-C8aDbGmR.js} +56 -1
- package/editor/assets/agent8-D0MS174y.js +1 -0
- package/editor/assets/{debug-BBhKuBNV.js → debug-BnXkKuYu.js} +2 -2
- package/editor/assets/index-CIu3uc3l.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +7 -16
- package/schemas/scene.schema.json +30 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +25 -3
- package/skills/incanto-audio.md +27 -5
- package/skills/incanto-behaviors-and-scripts.md +83 -6
- package/skills/incanto-building-2d-games.md +106 -8
- package/skills/incanto-building-3d-games.md +118 -6
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +121 -14
- package/skills/incanto-hud.md +128 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +222 -58
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +78 -2
- package/skills/incanto-save-slots.md +188 -6
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +292 -12
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +5 -2
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +103 -378
- package/templates-app/beacon-isle-3d/src/main.ts +24 -4
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1255 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +143 -625
- package/templates-app/platformer-2d/src/main.ts +35 -13
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +41 -195
- package/templates-app/star-survivor/src/main.ts +28 -7
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +78 -217
- package/templates-app/tps-3d/src/main.ts +39 -17
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +21 -0
- package/templates-app/village-quest-3d/src/grove.scene.json +54 -221
- package/templates-app/village-quest-3d/src/main.ts +24 -4
- package/templates-app/village-quest-3d/src/village.scene.json +199 -838
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-IWIqk0HJ.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-8hoHeRmo.js +0 -2340
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BrrHOjMJ.js +0 -1
- package/editor/assets/index-eVd0BToA.js +0 -10958
package/bin/incanto-playtest.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdirSync,
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* incanto-playtest — the engine plays your game and tells you whether it works.
|
|
6
6
|
*
|
|
@@ -23,26 +23,50 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
23
23
|
* Exit code 1 when no run reached a win, so CI can gate on it.
|
|
24
24
|
*/
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
-
import { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
26
|
+
import { behaviorFiles, behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
27
|
+
import { readJson } from './_read-json.mjs';
|
|
27
28
|
|
|
28
29
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const PKG_VERSION = readJson(join(PKG, 'package.json')).version;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A numeric flag with a missing or non-numeric value is a TYPO.
|
|
34
|
+
*
|
|
35
|
+
* `--runs $RUNS` with RUNS unset swallowed the NEXT flag as its value and gave
|
|
36
|
+
* `Number(undefined)` = NaN, which ran ZERO runs and then printed a confident
|
|
37
|
+
* report asserting the opposite of the truth about the scene — exit 0.
|
|
38
|
+
*/
|
|
39
|
+
function num(raw, flag) {
|
|
40
|
+
const n = Number(raw);
|
|
41
|
+
if (raw === undefined || raw === '' || !Number.isFinite(n)) {
|
|
42
|
+
console.error(`${flag} needs a number, got ${raw === undefined ? 'nothing' : `'${raw}'`}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
29
47
|
|
|
30
48
|
function parseArgs(argv) {
|
|
31
49
|
const args = { runs: 20, seconds: 60, seed: 1, out: '.incanto/playtest' };
|
|
32
50
|
for (let i = 0; i < argv.length; i++) {
|
|
33
51
|
const a = argv[i];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
else if (a === '--
|
|
37
|
-
else if (a === '--
|
|
38
|
-
else if (a === '--
|
|
39
|
-
else if (a === '--
|
|
52
|
+
// Repeatable: behaviours legitimately live in more than one file.
|
|
53
|
+
if (a === '--behaviors') args.behaviors = [...(args.behaviors ?? []), argv[++i]];
|
|
54
|
+
else if (a === '--runs') args.runs = num(argv[++i], '--runs');
|
|
55
|
+
else if (a === '--seconds') args.seconds = num(argv[++i], '--seconds');
|
|
56
|
+
else if (a === '--seed') args.seed = num(argv[++i], '--seed');
|
|
57
|
+
else if (a === '--fall-below') args.fallBelow = num(argv[++i], '--fall-below');
|
|
58
|
+
else if (a === '--reach-radius') args.reachRadius = num(argv[++i], '--reach-radius');
|
|
40
59
|
else if (a === '--out') args.out = argv[++i];
|
|
41
60
|
else if (a === '--no-replays') args.noReplays = true;
|
|
42
61
|
else if (a === '--json') args.json = true;
|
|
43
62
|
else if (a === '--help' || a === '-h') args.help = true;
|
|
44
63
|
else if (a.startsWith('--')) {
|
|
64
|
+
// `invalid`, not `help`: printing usage and exiting 0 told an agent its
|
|
65
|
+
// typo was a request. `--runs=4`, `--behaviours`, `--no-replay` all landed
|
|
66
|
+
// here and looked like success — and with `--json` the machine path was
|
|
67
|
+
// literally silent (0 bytes on stdout).
|
|
45
68
|
console.error(`unknown flag: ${a}`);
|
|
69
|
+
args.invalid = true;
|
|
46
70
|
args.help = true;
|
|
47
71
|
} else if (!args.scene) args.scene = a;
|
|
48
72
|
}
|
|
@@ -60,24 +84,28 @@ if (args.help || !args.scene) {
|
|
|
60
84
|
3D, 1000 px under it in 2D — 2D counts DOWN the screen)
|
|
61
85
|
--reach-radius R how close counts as "reached" (default 2 in 3D, 32 px in 2D)
|
|
62
86
|
--behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
|
|
87
|
+
comma-separate or repeat for several files
|
|
63
88
|
--out DIR where failing replays go (default .incanto/playtest)
|
|
64
89
|
--no-replays report only, write nothing
|
|
65
90
|
--json the full report as JSON instead of prose
|
|
66
91
|
|
|
67
|
-
Exits 1 when
|
|
68
|
-
|
|
92
|
+
Exits 1 when a run THREW or left the world, or when a win is declared and no
|
|
93
|
+
run reached it. A scene that declares no win, or has nobody to drive, exits 0 —
|
|
94
|
+
the report marks those · rather than ✗, and the exit code says the same.`);
|
|
95
|
+
process.exit(!args.invalid && args.help && args.scene !== undefined ? 0 : 1);
|
|
69
96
|
}
|
|
70
97
|
|
|
71
|
-
const { playtest, playtestText, failingReplays } = await import(
|
|
98
|
+
const { playtest, playtestText, playtestFailed, failingReplays } = await import(
|
|
72
99
|
pathToFileURL(join(PKG, 'dist', 'test.js')).href
|
|
73
100
|
);
|
|
74
101
|
const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
|
|
75
102
|
|
|
76
103
|
const behaviors = {};
|
|
77
|
-
|
|
104
|
+
const behaviorSources = behaviorFiles(args.behaviors);
|
|
105
|
+
for (const file of behaviorSources) {
|
|
78
106
|
let mod;
|
|
79
107
|
try {
|
|
80
|
-
mod = await import(pathToFileURL(resolve(
|
|
108
|
+
mod = await import(pathToFileURL(resolve(file)).href);
|
|
81
109
|
} catch (e) {
|
|
82
110
|
const why = e?.message ?? String(e);
|
|
83
111
|
// NODE CANNOT IMPORT THE PATTERN WE TEACH. Every template's behaviors.ts
|
|
@@ -94,7 +122,7 @@ if (args.behaviors) {
|
|
|
94
122
|
// `village-quest-3d` starter, and it breaks for any author who splits their
|
|
95
123
|
// behaviours across files — which is most of them, eventually.
|
|
96
124
|
reexecUnderBun(import.meta.url, why);
|
|
97
|
-
console.error(behaviorsHelp(
|
|
125
|
+
console.error(behaviorsHelp(file, why));
|
|
98
126
|
process.exit(1);
|
|
99
127
|
}
|
|
100
128
|
for (const [name, value] of Object.entries(mod)) {
|
|
@@ -102,13 +130,13 @@ if (args.behaviors) {
|
|
|
102
130
|
behaviors[name] = value;
|
|
103
131
|
}
|
|
104
132
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
133
|
+
}
|
|
134
|
+
if (behaviorSources.length > 0 && Object.keys(behaviors).length === 0) {
|
|
135
|
+
console.error(`warning: no Behavior subclasses exported from ${behaviorSources.join(', ')}`);
|
|
108
136
|
}
|
|
109
137
|
|
|
110
138
|
const scenePath = resolve(args.scene);
|
|
111
|
-
const sceneJson =
|
|
139
|
+
const sceneJson = readJson(scenePath);
|
|
112
140
|
const sceneDir = dirname(scenePath);
|
|
113
141
|
|
|
114
142
|
let report;
|
|
@@ -118,11 +146,11 @@ try {
|
|
|
118
146
|
seconds: args.seconds,
|
|
119
147
|
seed: args.seed,
|
|
120
148
|
behaviors,
|
|
121
|
-
stubMissingBehaviors:
|
|
149
|
+
stubMissingBehaviors: behaviorSources.length === 0,
|
|
122
150
|
...(Number.isFinite(args.fallBelow) ? { fallBelow: args.fallBelow } : {}),
|
|
123
151
|
...(Number.isFinite(args.reachRadius) ? { reachRadius: args.reachRadius } : {}),
|
|
124
152
|
// sub-scenes resolve relative to the scene file, as the loader does
|
|
125
|
-
resolveScene: (p) =>
|
|
153
|
+
resolveScene: (p) => readJson(resolve(sceneDir, p)),
|
|
126
154
|
});
|
|
127
155
|
} catch (e) {
|
|
128
156
|
console.error(`playtest failed: ${e?.message ?? e}`);
|
|
@@ -140,12 +168,27 @@ if (args.json) {
|
|
|
140
168
|
if (!args.noReplays && !args.json) {
|
|
141
169
|
const seen = new Set();
|
|
142
170
|
const written = [];
|
|
171
|
+
/*
|
|
172
|
+
* Stamp WHICH GAME each recording is of.
|
|
173
|
+
*
|
|
174
|
+
* `.incanto/playtest` is keyed by `(outcome, seed)` and nothing else, and
|
|
175
|
+
* never cleaned. So the second playtest in a project either leaves a stale
|
|
176
|
+
* `stuck-seed3.json` from a scene that no longer exists sitting beside the
|
|
177
|
+
* current one, or silently overwrites another scene's — and
|
|
178
|
+
* `incanto-play --commands` happily replays any recording against any scene
|
|
179
|
+
* and reports success, because nothing in the file said what it was of.
|
|
180
|
+
*/
|
|
181
|
+
const stamp = {
|
|
182
|
+
path: relative(process.cwd(), scenePath) || scenePath,
|
|
183
|
+
name: typeof sceneJson?.name === 'string' ? sceneJson.name : undefined,
|
|
184
|
+
engine: PKG_VERSION,
|
|
185
|
+
};
|
|
143
186
|
for (const { seed, outcome, replay } of failingReplays(report)) {
|
|
144
187
|
if (seen.has(outcome)) continue;
|
|
145
188
|
seen.add(outcome);
|
|
146
189
|
const file = join(resolve(args.out), `${outcome}-seed${seed}.json`);
|
|
147
190
|
mkdirSync(dirname(file), { recursive: true });
|
|
148
|
-
writeFileSync(file, JSON.stringify(replay, null, 2));
|
|
191
|
+
writeFileSync(file, JSON.stringify({ ...replay, scene: stamp }, null, 2));
|
|
149
192
|
written.push(file);
|
|
150
193
|
}
|
|
151
194
|
if (written.length > 0) {
|
|
@@ -157,4 +200,4 @@ if (!args.noReplays && !args.json) {
|
|
|
157
200
|
// asynchronously, and exiting discards whatever has not flushed. A --json
|
|
158
201
|
// report read by another program came back truncated — silently, and only
|
|
159
202
|
// when piped, which is the only way a program reads it.
|
|
160
|
-
process.exitCode = report
|
|
203
|
+
process.exitCode = playtestFailed(report) ? 1 : 0;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* Serve a BUILT game, and let the inspect tools see it.
|
|
5
|
+
*
|
|
6
|
+
* `incanto-frame` and `incanto-logs` ask a dev server, which asks the page over
|
|
7
|
+
* vite's HMR channel. A production build has no HMR, so nothing could look at
|
|
8
|
+
* the artifact an author actually deploys:
|
|
9
|
+
*
|
|
10
|
+
* $ node …/incanto-logs.mjs --port 8791 # dist/ on a static server
|
|
11
|
+
* no incanto dev server found.
|
|
12
|
+
* $ incanto-verify src/game.scene.json
|
|
13
|
+
* ? draws — no incanto dev server found.
|
|
14
|
+
* ? says — no incanto dev server found.
|
|
15
|
+
*
|
|
16
|
+
* The boot path is the only code a BUILD adds over a SCENE — `main.ts`, the
|
|
17
|
+
* loading overlay, the asset base path, everything after `createGame*` resolves
|
|
18
|
+
* — and it was the one thing no rung could reach.
|
|
19
|
+
*
|
|
20
|
+
* This serves `dist/` as plain static files (no bundler, no transform) and adds
|
|
21
|
+
* exactly two things: the `/__incanto/*` endpoints the CLIs already speak, and
|
|
22
|
+
* one inline script injected into `index.html` that relays them to the page.
|
|
23
|
+
* Nothing is added to the files on disk, so what you measure is what you ship.
|
|
24
|
+
*
|
|
25
|
+
* bunx incanto-serve # ./dist on :4180
|
|
26
|
+
* bunx incanto-serve build --port 5050
|
|
27
|
+
* bunx incanto-verify # now measures the BUILT game
|
|
28
|
+
*/
|
|
29
|
+
import { createServer } from 'node:http';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { extname, join, normalize, resolve, sep } from 'node:path';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const PKG = resolve(fileURLToPath(import.meta.url), '../..');
|
|
35
|
+
const DIST = join(PKG, 'dist');
|
|
36
|
+
if (!existsSync(join(DIST, 'vite.js'))) {
|
|
37
|
+
console.error('incanto: the package is not built (dist/vite.js missing).');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const { createStaticInspectHost, serveFrameEndpoints } = await import(
|
|
41
|
+
pathToFileURL(join(DIST, 'vite.js')).href
|
|
42
|
+
);
|
|
43
|
+
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
44
|
+
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
47
|
+
console.error(`Usage: incanto-serve [dir] [--port N] [--host H]
|
|
48
|
+
|
|
49
|
+
Serve a BUILT game (default ./dist) so \`incanto-frame\`, \`incanto-logs\` and
|
|
50
|
+
\`incanto-verify\` can measure the artifact you actually deploy. Files are served
|
|
51
|
+
byte-for-byte; the inspect client is injected into index.html at serve time and
|
|
52
|
+
never written to disk.`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const flag = (name, fallback) => {
|
|
56
|
+
const at = args.indexOf(name);
|
|
57
|
+
return at >= 0 && args[at + 1] ? args[at + 1] : fallback;
|
|
58
|
+
};
|
|
59
|
+
const port = Number(flag('--port', '4180'));
|
|
60
|
+
const host = flag('--host', '127.0.0.1');
|
|
61
|
+
const dir = resolve(
|
|
62
|
+
args.find(
|
|
63
|
+
(a) =>
|
|
64
|
+
!a.startsWith('--') &&
|
|
65
|
+
args[args.indexOf(a) - 1] !== '--port' &&
|
|
66
|
+
args[args.indexOf(a) - 1] !== '--host',
|
|
67
|
+
) ?? 'dist',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
71
|
+
console.error(
|
|
72
|
+
`incanto-serve: no such directory: ${dir}\n` +
|
|
73
|
+
' Build first (`bun run build`), or name the output directory.',
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
if (!existsSync(join(dir, 'index.html'))) {
|
|
78
|
+
console.error(
|
|
79
|
+
`incanto-serve: ${dir} has no index.html — that is not a built game.\n` +
|
|
80
|
+
' Point this at the directory your bundler wrote.',
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inspect = createStaticInspectHost(process.cwd());
|
|
86
|
+
serveFrameEndpoints(inspect, VERSION);
|
|
87
|
+
|
|
88
|
+
const TYPES = {
|
|
89
|
+
'.html': 'text/html; charset=utf-8',
|
|
90
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
91
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
92
|
+
'.css': 'text/css; charset=utf-8',
|
|
93
|
+
'.json': 'application/json; charset=utf-8',
|
|
94
|
+
'.png': 'image/png',
|
|
95
|
+
'.jpg': 'image/jpeg',
|
|
96
|
+
'.jpeg': 'image/jpeg',
|
|
97
|
+
'.webp': 'image/webp',
|
|
98
|
+
'.gif': 'image/gif',
|
|
99
|
+
'.svg': 'image/svg+xml',
|
|
100
|
+
'.ico': 'image/x-icon',
|
|
101
|
+
'.glb': 'model/gltf-binary',
|
|
102
|
+
'.gltf': 'model/gltf+json',
|
|
103
|
+
'.mp3': 'audio/mpeg',
|
|
104
|
+
'.ogg': 'audio/ogg',
|
|
105
|
+
'.wav': 'audio/wav',
|
|
106
|
+
'.wasm': 'application/wasm',
|
|
107
|
+
'.woff2': 'font/woff2',
|
|
108
|
+
'.ttf': 'font/ttf',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Inject the relay ahead of everything, so it exists before the game boots. */
|
|
112
|
+
function withInspectClient(html) {
|
|
113
|
+
const tag = `<script>${inspect.clientScript()}</script>`;
|
|
114
|
+
if (html.includes('</head>')) return html.replace('</head>', `${tag}</head>`);
|
|
115
|
+
return tag + html;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const server = createServer((req, res) => {
|
|
119
|
+
const url = (req.url ?? '/').split('?')[0] ?? '/';
|
|
120
|
+
if (url.startsWith('/__incanto/')) {
|
|
121
|
+
// `originalUrl` is what the endpoints read for the query string; connect
|
|
122
|
+
// strips the mount path off `url` and these were written against that.
|
|
123
|
+
req.originalUrl = req.url;
|
|
124
|
+
if (inspect.handleInspect(req, res)) return;
|
|
125
|
+
res.statusCode = 404;
|
|
126
|
+
res.end('{}');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// A path outside the served directory is a traversal attempt, not a typo.
|
|
130
|
+
const rel = normalize(decodeURIComponent(url)).replace(/^(\.\.[/\\])+/, '');
|
|
131
|
+
let file = join(dir, rel);
|
|
132
|
+
if (!file.startsWith(dir + sep) && file !== dir) file = dir;
|
|
133
|
+
if (!existsSync(file) || statSync(file).isDirectory()) file = join(dir, 'index.html');
|
|
134
|
+
|
|
135
|
+
const type = TYPES[extname(file)] ?? 'application/octet-stream';
|
|
136
|
+
if (file.endsWith('index.html')) {
|
|
137
|
+
const html = withInspectClient(readFileSync(file, 'utf8'));
|
|
138
|
+
res.statusCode = 200;
|
|
139
|
+
res.setHeader('content-type', TYPES['.html']);
|
|
140
|
+
res.end(html);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
res.statusCode = 200;
|
|
144
|
+
res.setHeader('content-type', type);
|
|
145
|
+
res.end(readFileSync(file));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.on('error', (e) => {
|
|
149
|
+
if (e?.code === 'EADDRINUSE') {
|
|
150
|
+
console.error(`incanto-serve: port ${port} is already in use — pass --port N.`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
throw e;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
server.listen(port, host, () => {
|
|
157
|
+
console.log(`incanto-serve ${dir}`);
|
|
158
|
+
console.log(` http://${host}:${port}/`);
|
|
159
|
+
console.log(' open it, then run `incanto-verify` / `incanto-frame` / `incanto-logs`.');
|
|
160
|
+
});
|
package/bin/incanto-skills.mjs
CHANGED
|
@@ -22,8 +22,19 @@ function parseArgs(argv) {
|
|
|
22
22
|
if (argv[i] === '--format') args.format = argv[++i];
|
|
23
23
|
else if (argv[i] === '--out') args.out = argv[++i];
|
|
24
24
|
else if (argv[i] === '--help' || argv[i] === '-h') args.help = true;
|
|
25
|
+
/*
|
|
26
|
+
* The bare word, because that is what everything tells you to type.
|
|
27
|
+
*
|
|
28
|
+
* `incanto-new`'s closing message — the last thing a scaffolded project
|
|
29
|
+
* prints, and the line that installs the skills the whole workflow depends
|
|
30
|
+
* on — said `bunx incanto-skills claude`, and that was an unknown argument.
|
|
31
|
+
* The flag form stays; this makes the printed line true.
|
|
32
|
+
*/ else if (argv[i] === 'claude' || argv[i] === 'opencode') args.format = argv[i];
|
|
25
33
|
else {
|
|
34
|
+
// Not `help`: printing usage and exiting 0 for a typo meant
|
|
35
|
+
// `incanto-skills --oops` installed ZERO skills and reported success.
|
|
26
36
|
console.error(`unknown argument: ${argv[i]}`);
|
|
37
|
+
args.invalid = true;
|
|
27
38
|
args.help = true;
|
|
28
39
|
}
|
|
29
40
|
}
|
|
@@ -32,7 +43,8 @@ function parseArgs(argv) {
|
|
|
32
43
|
|
|
33
44
|
const args = parseArgs(process.argv.slice(2));
|
|
34
45
|
if (args.help || !['claude', 'opencode'].includes(args.format)) {
|
|
35
|
-
console.log(`Usage: npx incanto-skills [
|
|
46
|
+
console.log(`Usage: npx incanto-skills [claude|opencode] [--out <dir>]
|
|
47
|
+
npx incanto-skills [--format claude|opencode] [--out <dir>]
|
|
36
48
|
|
|
37
49
|
claude (default) Claude Code project skills:
|
|
38
50
|
<out>/<skill-name>/SKILL.md (default out: .claude/skills)
|
|
@@ -40,7 +52,7 @@ if (args.help || !['claude', 'opencode'].includes(args.format)) {
|
|
|
40
52
|
<out>/<skill-name>.md (default out: skills)
|
|
41
53
|
|
|
42
54
|
Skills are read from the installed incanto package and match its version.`);
|
|
43
|
-
process.exit(args.help ? 0 : 1);
|
|
55
|
+
process.exit(!args.invalid && args.help ? 0 : 1);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
const outRoot = resolve(args.out ?? (args.format === 'claude' ? '.claude/skills' : 'skills'));
|