From 99eb44e48375e838d515378883fad9979bc92576 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 5 Nov 2026 13:31:42 -0700 Subject: [PATCH] feat(release): ship watch.mjs with the reference assets An installed game project reads releases/latest/download/ for the reference data. The gameplay watcher rides along in the same release as watch.mjs, so the authoring layer can fetch and run it with no checkout of this repo. --release copies scripts/watch.mjs into .release/; the fixture test pins the asset set and that the copy is byte-identical to the source. --- README.md | 4 ++++ scripts/validate.mjs | 5 ++++- tests/library.spec.mjs | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1dd49d9..be158d3 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,10 @@ for more or fewer slices. Each frame is 640px wide and named for its timestamp; records the video, the segment and every frame time, so a claim about the mechanic can name the clip it came from. Cards still cite source code at the pinned SHA; they never cite a video. +Every release also ships `watch.mjs`, so an installed game project fetches the watcher from the +same base URL the reference tools already read +(`releases/latest/download/watch.mjs`) and runs it with no checkout of this repository. + The default window opens 10% into the video, past the title card and menu. A vertical slice of an open-ended session is still a guess: the first sheet is for deciding where to look next. diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 0ef144d..37a8d5a 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -1,5 +1,5 @@ import Ajv from 'ajv/dist/2020.js'; -import { readFileSync, readdirSync, mkdirSync, existsSync, writeFileSync } from 'node:fs'; +import { readFileSync, readdirSync, mkdirSync, existsSync, writeFileSync, copyFileSync } from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { gzipSync } from 'node:zlib'; @@ -180,6 +180,9 @@ export function run(args = process.argv.slice(2)) { mkdirSync(path.join(root, '.release'), { recursive: true }); writeFileSync(path.join(root, '.release', 'index.json.gz'), gzipSync(JSON.stringify({ version: 1, games: catalog, stubs }))); for (const game of games) writeFileSync(path.join(root, '.release', `game-${game.id}.json`), JSON.stringify(game)); + // The gameplay watcher rides along so an installed game project can fetch it from the same + // release the reference tools already read, with no checkout of this repository. + copyFileSync(new URL('../scripts/watch.mjs', import.meta.url), path.join(root, '.release', 'watch.mjs')); } } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { diff --git a/tests/library.spec.mjs b/tests/library.spec.mjs index f15de9f..88654d2 100644 --- a/tests/library.spec.mjs +++ b/tests/library.spec.mjs @@ -139,7 +139,8 @@ test('release assets keep the version-1 contract and exact asset set', t => { t.after(() => cleanup(dir)); const result = runNpm(dir, ['release:assets', '--', '--source', source.dir]); assert.equal(result.status, 0, result.stderr); - assert.deepEqual(readdirSync(path.join(dir, '.release')).sort(), ['game-demo.json', 'game-second.json', 'index.json.gz']); + assert.deepEqual(readdirSync(path.join(dir, '.release')).sort(), ['game-demo.json', 'game-second.json', 'index.json.gz', 'watch.mjs']); + assert.equal(readFileSync(path.join(dir, '.release', 'watch.mjs'), 'utf8'), readFileSync(path.join(dir, 'scripts', 'watch.mjs'), 'utf8')); const index = JSON.parse(gunzipSync(readFileSync(path.join(dir, '.release', 'index.json.gz'))).toString()); assert.equal(index.version, 1); assert.deepEqual(index.games, config);