@genex-ai/cli-demo 1.5.2-dev.398 → 1.6.0-dev.403

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.
@@ -0,0 +1,134 @@
1
+ // Phase 2 of the manifest: rewrite dist/genex-asset.json in place with the
2
+ // measured geometry, the measured space and the gate block, so what ships is
3
+ // what was actually measured rather than what the author claimed.
4
+ //
5
+ // Refuses to stamp a stale report (one taken against different source), and
6
+ // re-validates the finished bytes - a build whose manifest cannot offer the two
7
+ // dashboard buttons must not reach a deploy.
8
+ //
9
+ // exit 0 stamped
10
+ // exit 1 refused (stale, failing, or invalid)
11
+ //
12
+ // Copied verbatim into every asset project and hashed by gate G13.
13
+ //
14
+ // Usage: node tools/stamp-manifest.mjs [--dist dist] [--report .gates/report.json]
15
+ // [--play-url <url>] [--allow-failed]
16
+
17
+ import fs from "node:fs";
18
+ import fsp from "node:fs/promises";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ import {
23
+ MANIFEST_FILENAME,
24
+ MANIFEST_MAX_BYTES,
25
+ defaultPlayUrl,
26
+ readAssetConfig,
27
+ validateManifest,
28
+ } from "./emit-manifest.mjs";
29
+
30
+ const PROJECT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
31
+
32
+ function parseArgs(argv) {
33
+ const args = { dist: "dist", report: path.join(".gates", "report.json"), playUrl: null, allowFailed: false };
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const a = argv[i];
36
+ if (a === "--dist") args.dist = argv[++i];
37
+ else if (a === "--report") args.report = argv[++i];
38
+ else if (a === "--play-url") args.playUrl = argv[++i];
39
+ else if (a === "--allow-failed") args.allowFailed = true;
40
+ else if (a === "--help" || a === "-h") args.help = true;
41
+ else throw new Error(`unknown flag: ${a}`);
42
+ }
43
+ return args;
44
+ }
45
+
46
+ async function main() {
47
+ const args = parseArgs(process.argv.slice(2));
48
+ if (args.help) {
49
+ console.log("usage: node tools/stamp-manifest.mjs [--dist dist] [--report .gates/report.json] [--play-url <url>] [--allow-failed]");
50
+ return 0;
51
+ }
52
+
53
+ const distDir = path.resolve(PROJECT_DIR, args.dist);
54
+ const manifestPath = path.join(distDir, MANIFEST_FILENAME);
55
+ const reportPath = path.resolve(PROJECT_DIR, args.report);
56
+
57
+ if (!fs.existsSync(manifestPath)) throw new Error(`no ${MANIFEST_FILENAME} in ${args.dist} - run \`npm run build\` first`);
58
+ if (!fs.existsSync(reportPath)) throw new Error(`no gate report at ${args.report} - run \`npm run gates\` first`);
59
+
60
+ const config = readAssetConfig(PROJECT_DIR);
61
+ const manifest = JSON.parse(await fsp.readFile(manifestPath, "utf8"));
62
+ const report = JSON.parse(await fsp.readFile(reportPath, "utf8"));
63
+
64
+ // A report taken against different source text describes a different asset.
65
+ if (report.sourceSha256 !== manifest.source.sha256) {
66
+ throw new Error(
67
+ `gate report is stale: it was run against source ${String(report.sourceSha256).slice(0, 12)}… but dist holds ${manifest.source.sha256.slice(0, 12)}… - rebuild and re-run the gates`,
68
+ );
69
+ }
70
+ if (!report.allPassed && !args.allowFailed) {
71
+ const failed = (report.results ?? []).filter((r) => !r.passed).map((r) => r.id);
72
+ throw new Error(`gates failed (${failed.join(", ")}) - fix them, or pass --allow-failed to stamp a build that says so`);
73
+ }
74
+
75
+ const stamped = {
76
+ ...manifest,
77
+ geometry: report.geometry,
78
+ space: {
79
+ ...manifest.space,
80
+ boundingBox: report.space.boundingBox,
81
+ sizeMeters: report.space.sizeMeters,
82
+ statedSizeMeters: config.statedSizeMeters,
83
+ sizeDeltaPct: report.space.sizeDeltaPct,
84
+ },
85
+ gates: {
86
+ runAt: report.runAt,
87
+ runner: report.runner,
88
+ allPassed: Boolean(report.allPassed),
89
+ results: (report.results ?? []).map(({ id, name, threshold, measured, passed }) => ({
90
+ id,
91
+ name,
92
+ threshold,
93
+ measured,
94
+ passed,
95
+ })),
96
+ },
97
+ preview: {
98
+ ...manifest.preview,
99
+ playUrl: args.playUrl ?? manifest.preview?.playUrl ?? defaultPlayUrl(config.slug),
100
+ },
101
+ };
102
+
103
+ const body = JSON.stringify(stamped, null, 2) + "\n";
104
+ const bytes = Buffer.byteLength(body, "utf8");
105
+ const shape = validateManifest(stamped, { bytes });
106
+ if (!shape.ok) throw new Error(`refusing to stamp an invalid manifest:\n - ${shape.errors.join("\n - ")}`);
107
+ if (bytes > MANIFEST_MAX_BYTES) {
108
+ throw new Error(`refusing to stamp: ${bytes} bytes exceeds the ${MANIFEST_MAX_BYTES} byte ceiling`);
109
+ }
110
+
111
+ await fsp.writeFile(manifestPath, body, "utf8");
112
+
113
+ console.log(
114
+ [
115
+ "",
116
+ ` stamped ${path.relative(PROJECT_DIR, manifestPath)} (${(bytes / 1024).toFixed(1)} KB)`,
117
+ ` ${stamped.geometry.triangles.toLocaleString("en-US")} tris · ${stamped.geometry.drawCalls} draws · ${stamped.geometry.materials} materials`,
118
+ ` ${stamped.space.sizeMeters.map((n) => n.toFixed(2)).join(" × ")} m`,
119
+ ` gates: ${stamped.gates.allPassed ? `all ${stamped.gates.results.length} passed` : `${stamped.gates.results.filter((r) => !r.passed).length} FAILED`}`,
120
+ ` preview: ${stamped.preview.playUrl}`,
121
+ "",
122
+ ].join("\n"),
123
+ );
124
+
125
+ return 0;
126
+ }
127
+
128
+ main().then(
129
+ (code) => process.exit(code),
130
+ (error) => {
131
+ console.error(`\n[stamp] ${error?.message ?? error}\n`);
132
+ process.exit(1);
133
+ },
134
+ );
@@ -0,0 +1,24 @@
1
+ // Shared viewer build config. Copied verbatim into every asset project and
2
+ // hashed by gate G13 - edit it in packages/asset-viewer-template/template.
3
+ import { defineConfig } from "vite";
4
+ import { emitManifest } from "./tools/emit-manifest.mjs";
5
+
6
+ export default defineConfig({
7
+ // Games serve at the domain root, but relative keeps the build origin-agnostic.
8
+ base: "./",
9
+ build: {
10
+ // `dist` is the ONLY outDir name the Genex CLI's deployGame recognises;
11
+ // anything else uploads the whole source folder instead of the build.
12
+ outDir: "dist",
13
+ // Fingerprinted chunks opt into the serve worker's immutable cache dir.
14
+ // dist/genex-asset.json deliberately stays outside it, so a republish is
15
+ // always picked up.
16
+ assetsDir: "_immutable",
17
+ emptyOutDir: true,
18
+ target: "es2022",
19
+ },
20
+ server: {
21
+ port: Number(process.env.PORT) || 5190,
22
+ },
23
+ plugins: [emitManifest()],
24
+ });
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: genex-asset-author
3
+ description: Author and publish ONE asset to the free Genex asset library via `genex asset new` — STRICTLY user-initiated. Use ONLY when the user explicitly asks to publish something from their game as an asset, or to create a standalone asset for the library. Never suggest, offer, or trigger this lane on your own — building a game never requires it.
4
+ ---
5
+
6
+ # Genex Asset Author
7
+
8
+ Ship one procedural Three.js asset to the free, MIT-licensed Genex asset
9
+ library: a standalone project whose build is a uniform orbitable viewer, whose
10
+ only varying file is ONE self-contained TypeScript factory module, and whose
11
+ quality bar is 17 machine-checked gates.
12
+
13
+ ## When this lane runs — and when it must not
14
+
15
+ Run it ONLY when the user explicitly asks — the two real shapes:
16
+
17
+ - **"Publish this <thing> from my game as an asset"** — extract an object out
18
+ of the game they're building.
19
+ - **"I want to make an asset / assets"** — asset-first authoring.
20
+
21
+ NEVER offer this unprompted. Finishing a game, noticing a nice prop, or
22
+ wanting to showcase work is NOT a trigger; the user's request is. If they ask
23
+ what publishing an asset means, explain (free, MIT, copyable by anyone from the
24
+ dashboard, listed under the gallery's Assets tab) and let them decide.
25
+
26
+ ## The one non-negotiable before publish: MIT with THEIR name
27
+
28
+ A published asset gets a **Copy code** button on its page — anyone can take the
29
+ file. Publishing = releasing it under MIT. Before scaffolding, ask the user for
30
+ their attribution name and pass it as `--holder "<name>"`. If it was skipped,
31
+ set it before publish in BOTH places: `asset.config.json` → `license.holder`,
32
+ and line 2 of the module (the MIT header). Never publish with the placeholder.
33
+
34
+ ## Scaffold
35
+
36
+ ```bash
37
+ npx genex asset new <kebab-slug> --title "<Display Name>" \
38
+ --dims WxDxH --tri-band LOW-HIGH --holder "<user's name>"
39
+ cd <kebab-slug>
40
+ npm install
41
+ npx playwright-core install chromium # once per machine
42
+ ```
43
+
44
+ One asset = one project = one turntable page. Several assets → run the command
45
+ once per asset. `--dims` is width x depth x height in metres as a human states
46
+ it (gate G4 measures the built model against it ±5%); tune it and the config
47
+ honestly to the real object before gating, not to whatever the code produced.
48
+
49
+ The viewer is hash-locked: never edit `index.html`, `src/viewer/*`, `src/main.js`
50
+ or `tools/*` — gate G13 fails the build if you do. Your entire surface is
51
+ `src/asset/<name>.ts` and `asset.config.json`.
52
+
53
+ ## The module contract
54
+
55
+ `export function create<Name>Model(options): THREE.Group` — one self-contained
56
+ file:
57
+
58
+ - Core `three` ONLY. No `three/examples/jsm`, no `ShaderMaterial`, no
59
+ `CanvasTexture` (needs a DOM), no `fetch`, no external URL anywhere in the
60
+ file (gate G11 scans for `http` — comments included).
61
+ - Real metres, Y-up, +Z forward, origin at base centre (y = 0).
62
+ - Seeded determinism: same seed + options → identical vertex data (G10).
63
+ - `userData.dispose()` returns the renderer to baseline (G12) — every
64
+ geometry, material and texture you create is released.
65
+ - Textures are in-code `DataTexture`s; `MeshStandardMaterial` for surfaces.
66
+ - Keep the 4-line header (name / MIT + holder / units / requires) — it is what
67
+ makes a copied file self-describing.
68
+
69
+ Extracting from a game: COPY the code out (the game keeps its own), then strip
70
+ every game dependency, re-origin to base centre, convert to real metres, seed
71
+ the randomness, and add the dispose contract. The result must run in a project
72
+ holding nothing but `three` — that is literally gate G8 (paste-and-run).
73
+
74
+ ## Gate loop
75
+
76
+ ```bash
77
+ npm run verify # vite build + 17 gates + manifest stamp, in one command
78
+ ```
79
+
80
+ Iterate until 17/17. Each failure names its gate and what it measured. The
81
+ gates are the design bar of the Assets tab — never loosen `asset.config.json`
82
+ ceilings to sneak a failing model through; fix the model.
83
+
84
+ Two measured traps: metalness 1.0 needs the viewer's environment map to read
85
+ correctly — it renders BLACK in a game with no envmap, so prefer 0.2-0.9 for
86
+ believable metals; and a mirrored basis can pass every numeric gate while
87
+ being silently inside-out — LOOK at the turntable from several angles before
88
+ calling it done.
89
+
90
+ ## Ship
91
+
92
+ ```bash
93
+ npx genex init # create the project (sign-in if needed)
94
+ npx genex preview --no-build # staging turntable — show the user this link
95
+ npx genex publish --no-build --categories assets # only after the user says ship it
96
+ ```
97
+
98
+ `--no-build` matters: `npm run verify` already built AND stamped the manifest
99
+ into `dist/` — a plain preview/publish would rebuild and ship an unstamped
100
+ manifest, and the dashboard's Copy-code button reads that manifest. Publish
101
+ ONLY on the user's explicit go, with the licence holder set to their real name.
@@ -67,6 +67,7 @@ copy demo architecture.
67
67
  | players talking to each other by TEXT — chat, room chat, "let them type to each other" (`room.chat` + `npx genex controller chat`) | `$genex-threejs-multiplayer` |
68
68
  | players TALKING — voice chat, mic, "hear each other", proximity/positional voice (`room.voice` + `npx genex controller voice`; party-sized, max 6 per room — say that number out loud before building the feature) | `$genex-threejs-multiplayer` |
69
69
  | player identity, sign-in, guests, saves/progress, per-player state, shared persistent world, leaderboards—mandatory for every game | `$genex-threejs-embed-auth` |
70
+ | the player EXPLICITLY asks to make an asset or publish something from their game as an asset for the free library | `$genex-asset-author` (`genex asset new`) — never offer this unprompted; no game duty routes here |
70
71
 
71
72
  ## Request-sized execution
72
73