animader-cli 0.0.0-stage → 0.1.1

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 (71) hide show
  1. package/LICENSE +121 -0
  2. package/adapters/build-node.mjs +265 -0
  3. package/adapters/devserver.mjs +481 -0
  4. package/adapters/devsession.mjs +619 -0
  5. package/adapters/export-node.mjs +603 -0
  6. package/adapters/export-run.mjs +126 -0
  7. package/adapters/generations.mjs +183 -0
  8. package/adapters/http.mjs +294 -0
  9. package/adapters/launch.mjs +83 -0
  10. package/adapters/node-fs.mjs +80 -0
  11. package/adapters/pngine.mjs +87 -0
  12. package/adapters/snapshot.mjs +143 -0
  13. package/adapters/target.mjs +104 -0
  14. package/adapters/watch.mjs +355 -0
  15. package/adapters/webaudio.mjs +102 -0
  16. package/bin/animader.mjs +31 -0
  17. package/hello/join.js +46 -0
  18. package/hello/run.js +259 -0
  19. package/lib/build.mjs +126 -0
  20. package/lib/buildloop.mjs +254 -0
  21. package/lib/contract.mjs +371 -0
  22. package/lib/errors.mjs +51 -0
  23. package/lib/export.mjs +445 -0
  24. package/lib/new.mjs +304 -0
  25. package/lib/pipeline.mjs +247 -0
  26. package/lib/receipt.mjs +331 -0
  27. package/lib/rmtree.mjs +49 -0
  28. package/lib/routes.mjs +170 -0
  29. package/lib/score.mjs +434 -0
  30. package/lib/show.mjs +458 -0
  31. package/lib/soundtrack.mjs +67 -0
  32. package/lib/vendor.mjs +99 -0
  33. package/lib/wav.mjs +197 -0
  34. package/package.json +47 -4
  35. package/player/boot.mjs +792 -0
  36. package/player/controller.mjs +366 -0
  37. package/player/devchannel.mjs +306 -0
  38. package/player/index.html +35 -0
  39. package/player/page.mjs +134 -0
  40. package/player/player.css +393 -0
  41. package/player/release.mjs +76 -0
  42. package/src/commands/build.mjs +201 -0
  43. package/src/commands/dev.mjs +214 -0
  44. package/src/commands/doctor.mjs +348 -0
  45. package/src/commands/export.mjs +158 -0
  46. package/src/commands/new.mjs +164 -0
  47. package/src/commands/project.mjs +46 -0
  48. package/src/config.mjs +110 -0
  49. package/src/router.mjs +149 -0
  50. package/src/toolchain.mjs +691 -0
  51. package/starters/circle/README.md +50 -0
  52. package/starters/circle/animader.json +4 -0
  53. package/starters/circle/gitignore +9 -0
  54. package/starters/circle/kernels/circle.wgsl +32 -0
  55. package/starters/circle/program.sjon +34 -0
  56. package/starters/circle/timeline.piece.sjon +26 -0
  57. package/starters/ember/bake/spans.bin +0 -0
  58. package/starters/ember/kernels/curl.wgsl +10 -0
  59. package/starters/ember/kernels/ember.wgsl +10 -0
  60. package/starters/ember/kernels/hash.wgsl +12 -0
  61. package/starters/ember/kernels/simplex.wgsl +27 -0
  62. package/starters/ember/program.sjon +328 -0
  63. package/starters/ember/timeline.piece.sjon +34 -0
  64. package/toolchain/licenses/animader.txt +121 -0
  65. package/toolchain/licenses/esbuild.txt +21 -0
  66. package/toolchain/licenses/pacer.txt +121 -0
  67. package/toolchain/licenses/pngine.txt +121 -0
  68. package/toolchain/manifest.json +212 -0
  69. package/toolchain/prepare.mjs +446 -0
  70. package/toolchain/uniform-type-name.js +184 -0
  71. package/README.md +0 -3
@@ -0,0 +1,201 @@
1
+ // The `animader build` command: its argv grammar and honesty rules. Build is
2
+ // the real full build — the shared pipeline (graphics bake, score
3
+ // derivation, payload reflection, the reflected target join, the real pacer
4
+ // pack, the playable-by-this-host audit, the score-build receipt) over an
5
+ // input snapshot this build owns. An unknown or ill-typed score target and
6
+ // a packed score this host cannot play both fail the build by name; a green
7
+ // build states the score was validated and names both artifacts. Build
8
+ // still does not prove browser WebGPU availability or audio decoding — the
9
+ // player page owns those diagnostics.
10
+ //
11
+ // The build operation and lib are the package's own adapters/ and lib/
12
+ // (installed-toolchain plan, stage 1); the vendored runtime default still
13
+ // points at the contributor checkout until stage 2's toolchain descriptor.
14
+
15
+ import { realpath } from 'node:fs/promises';
16
+ import { basename, join } from 'node:path';
17
+
18
+ import { createBuildProducers, runFullBuild } from '../../adapters/build-node.mjs';
19
+ import { createNodeIo } from '../../adapters/node-fs.mjs';
20
+ import { createPngineNative } from '../../adapters/pngine.mjs';
21
+ import { DemoError, usageError } from '../../lib/errors.mjs';
22
+ import { SCORE_RECEIPT_FORMAT } from '../../lib/receipt.mjs';
23
+ import { contributorToolchain, resolveToolchain } from '../toolchain.mjs';
24
+ import {
25
+ BAKED_PAYLOAD,
26
+ BUILD_RECEIPT,
27
+ GENERATED_DIR,
28
+ PACKED_SCORE,
29
+ inspectProject,
30
+ } from '../../lib/contract.mjs';
31
+ import { commandFolder, resolveConfiguredSeconds } from './project.mjs';
32
+
33
+
34
+ /**
35
+ * Parse the `animader build` argv without touching the filesystem.
36
+ *
37
+ * @param {string[]} argv
38
+ * @returns {{help: boolean, folder: string|null, secondsRaw: string|undefined}}
39
+ */
40
+ export function parseBuildCommandArgv(argv) {
41
+ if (!Array.isArray(argv)) {
42
+ throw usageError('invalid_argv', 'the CLI adapter must pass an argument array', 'Call animader build [folder] [--seconds S]');
43
+ }
44
+
45
+ let folder = null;
46
+ let secondsRaw;
47
+ let help = false;
48
+ let endOptions = false;
49
+
50
+ for (let index = 0; index < argv.length; index++) {
51
+ const argument = String(argv[index]);
52
+ if (!endOptions && argument === '--') {
53
+ endOptions = true;
54
+ continue;
55
+ }
56
+ if (!endOptions && (argument === '--help' || argument === '-h')) {
57
+ help = true;
58
+ continue;
59
+ }
60
+ if (!endOptions && argument.startsWith('--')) {
61
+ const equals = argument.indexOf('=');
62
+ const name = equals === -1 ? argument : argument.slice(0, equals);
63
+ const inline = equals === -1 ? undefined : argument.slice(equals + 1);
64
+ if (name === '--seconds') {
65
+ if (inline !== undefined) {
66
+ secondsRaw = inline;
67
+ } else {
68
+ if (index + 1 >= argv.length) {
69
+ throw usageError('missing_option_value', `${name} needs a value`, 'Pass --seconds <n>, for example --seconds 8');
70
+ }
71
+ index++;
72
+ secondsRaw = String(argv[index]);
73
+ }
74
+ } else {
75
+ throw usageError(
76
+ 'unknown_option',
77
+ `unknown option ${name}`,
78
+ 'The supported options are --seconds and --help; use --help for usage.',
79
+ );
80
+ }
81
+ continue;
82
+ }
83
+ if (!endOptions && argument.startsWith('-') && argument !== '-') {
84
+ throw usageError(
85
+ 'unknown_option',
86
+ `unknown option ${argument}`,
87
+ 'The supported options are --seconds and --help; use --help for usage.',
88
+ );
89
+ }
90
+ if (folder !== null) {
91
+ throw usageError('extra_argument', `extra folder argument ${JSON.stringify(argument)}`, 'Pass at most one project folder');
92
+ }
93
+ folder = argument;
94
+ }
95
+
96
+ return { help, folder, secondsRaw };
97
+ }
98
+
99
+ export function buildCommandUsageText() {
100
+ return [
101
+ 'animader build — run the full build into out/',
102
+ '',
103
+ 'Usage:',
104
+ ' animader build [folder] [--seconds S]',
105
+ '',
106
+ 'The folder is the project folder relative to the invocation directory',
107
+ '(default: the current directory; parent folders are never searched).',
108
+ 'The host duration comes from animader.json; an explicit --seconds wins',
109
+ 'after the config has validated.',
110
+ '',
111
+ 'build verifies the prepared runtime, validates and bakes the program',
112
+ 'with pngine, derives the timeline score, joins exactly the targets the',
113
+ 'baked program reflects, packs it with the real pacer CLI, audits the',
114
+ 'packed score through the playback runtime (the same load the player',
115
+ 'runs) and writes the score-build receipt. A green build means the',
116
+ 'score compiled and this host can load it — not that a browser has',
117
+ 'WebGPU or decodes the soundtrack.',
118
+ '',
119
+ 'Options:',
120
+ ' --seconds S explicit host duration, a positive number at most 600',
121
+ ' -h, --help this text',
122
+ '',
123
+ `Generated: ${GENERATED_DIR}/${BAKED_PAYLOAD}, ${GENERATED_DIR}/${PACKED_SCORE} and ${GENERATED_DIR}/${BUILD_RECEIPT}`,
124
+ ].join('\n');
125
+ }
126
+
127
+ /**
128
+ * Run `animader build`: the shared full-build pipeline over an input
129
+ * snapshot this command owns, then the artifact lines.
130
+ *
131
+ * @param {string[]} argv
132
+ * @param {{out: (line: string) => void, err: (line: string) => void,
133
+ * io?: object, native?: object, vendorRoot?: string,
134
+ * producers?: object}} context
135
+ */
136
+ export async function run(argv, context) {
137
+ const args = parseBuildCommandArgv(argv);
138
+ if (args.help) {
139
+ context.out(buildCommandUsageText());
140
+ return;
141
+ }
142
+
143
+ const folder = commandFolder(args);
144
+ const { seconds } = await resolveConfiguredSeconds({
145
+ folder,
146
+ secondsRaw: args.secondsRaw,
147
+ io: context.io ?? createNodeIo(),
148
+ });
149
+
150
+ let real;
151
+ try {
152
+ real = await realpath(folder);
153
+ } catch {
154
+ throw new DemoError('folder_missing', `the project folder does not exist: ${folder}`, {
155
+ exitCode: 3,
156
+ remedy: 'Pass the folder that holds program.sjon, timeline.piece.sjon and one of soundtrack.wav, soundtrack.ogg, soundtrack.opus.',
157
+ });
158
+ }
159
+
160
+ const io = context.io ?? createNodeIo();
161
+ const project = await inspectProject({ folder: real, io });
162
+ // The toolchain descriptor owns every artifact root: the contributor
163
+ // workspace's vendored bundle and checkout binaries, or the installed
164
+ // package tree's module-resolved artifacts (installed-toolchain plan,
165
+ // stage 2). A missing artifact fails inside it with its named remedy.
166
+ const toolchain = context.toolchain
167
+ ?? (context.vendorRoot !== undefined
168
+ ? contributorToolchain({ vendorRoot: context.vendorRoot })
169
+ : await resolveToolchain());
170
+
171
+ context.out('animader build — full build');
172
+ context.out(`project: ${real}`);
173
+ context.out(`show: ${seconds}s (host policy; no pacer transport is inferred)`);
174
+
175
+ const built = await runFullBuild({
176
+ project,
177
+ seconds,
178
+ paths: {
179
+ folder: real,
180
+ outDir: join(real, GENERATED_DIR),
181
+ payload: join(real, GENERATED_DIR, BAKED_PAYLOAD),
182
+ score: join(real, GENERATED_DIR, PACKED_SCORE),
183
+ receipt: join(real, GENERATED_DIR, BUILD_RECEIPT),
184
+ projectName: basename(real),
185
+ },
186
+ native: context.native ?? createPngineNative({ binary: toolchain.pngine.cli }),
187
+ runtime: { verify: () => toolchain.verifyRuntime() },
188
+ producers: context.producers ?? createBuildProducers({ toolchain }),
189
+ io,
190
+ });
191
+
192
+ context.out(`stages: ${built.stages.join(' -> ')}`);
193
+ context.out(`runtime: ${built.receipt.runtime.files} files, digest ${built.receipt.runtime.digest}`);
194
+ context.out(`payload: ${GENERATED_DIR}/${BAKED_PAYLOAD} (${built.receipt.outputs.payload.bytes} bytes, sha256 ${built.receipt.outputs.payload.sha256})`);
195
+ context.out(`score: ${GENERATED_DIR}/${PACKED_SCORE} (${built.receipt.outputs.score.bytes} bytes, sha256 ${built.receipt.outputs.score.sha256})`);
196
+ context.out('audit: playable by this host (loadPackedShow over the packed bytes and the baked reflection)');
197
+ context.out(`receipt: ${GENERATED_DIR}/${BUILD_RECEIPT} (${SCORE_RECEIPT_FORMAT}; score validated)`);
198
+ context.out(`inputs: ${built.receipt.inputs.files.length} files hashed (method ${built.receipt.inputs.method})`);
199
+ context.out(`pngine: ${built.receipt.toolchain.pngine.sha256}`);
200
+ context.out(`pacer: ${built.receipt.toolchain.pacer.sha256}`);
201
+ }
@@ -0,0 +1,214 @@
1
+ // The `animader dev` command: its argv grammar and the process-level
2
+ // lifecycle around one dev session. The session itself is the demo's
3
+ // `runDevSession`: the shared score-validating build (bake, derive,
4
+ // reflect, join, pack, the playable-by-this-host audit, the score-build
5
+ // receipt) staged as immutable generations outside the project tree, the
6
+ // loopback server pinning every project byte to one generation, the
7
+ // recursive watcher feeding a coalescing build loop, and the
8
+ // development-only /dev channel reloading the page on successful builds.
9
+ // `--no-watch` serves one build with no watcher.
10
+ //
11
+ // seconds and port rules: the host duration comes from the shared
12
+ // animader.json resolution (--seconds wins after a present config
13
+ // validates) re-resolved on every build, and the port keeps the demo's
14
+ // free-port default and bounds.
15
+ //
16
+ // The session operation is the package's own adapters/devsession.mjs
17
+ // (installed-toolchain plan, stage 1); its artifact roots come from the
18
+ // contributor workspace until stage 2 installs the toolchain descriptor.
19
+
20
+ import { createNodeIo } from '../../adapters/node-fs.mjs';
21
+ import { parsePort } from '../../lib/contract.mjs';
22
+ import { usageError } from '../../lib/errors.mjs';
23
+ import { runDevSession } from '../../adapters/devsession.mjs';
24
+ import { commandFolder, resolveConfiguredSeconds } from './project.mjs';
25
+ import { resolve } from 'node:path';
26
+
27
+ /**
28
+ * Parse the `animader dev` argv without touching the filesystem.
29
+ *
30
+ * @param {string[]} argv
31
+ * @returns {{help: boolean, folder: string|null, secondsRaw: string|undefined,
32
+ * port: number, open: boolean, watch: boolean}}
33
+ */
34
+ export function parseDevCommandArgv(argv) {
35
+ if (!Array.isArray(argv)) {
36
+ throw usageError('invalid_argv', 'the CLI adapter must pass an argument array', 'Call animader dev [folder] [--seconds S] [--port N] [--no-open] [--no-watch]');
37
+ }
38
+
39
+ let folder = null;
40
+ let secondsRaw;
41
+ let portRaw;
42
+ let open = true;
43
+ let watch = true;
44
+ let help = false;
45
+ let endOptions = false;
46
+
47
+ for (let index = 0; index < argv.length; index++) {
48
+ const argument = String(argv[index]);
49
+ if (!endOptions && argument === '--') {
50
+ endOptions = true;
51
+ continue;
52
+ }
53
+ if (!endOptions && (argument === '--help' || argument === '-h')) {
54
+ help = true;
55
+ continue;
56
+ }
57
+ if (!endOptions && argument.startsWith('--')) {
58
+ const equals = argument.indexOf('=');
59
+ const name = equals === -1 ? argument : argument.slice(0, equals);
60
+ const inline = equals === -1 ? undefined : argument.slice(equals + 1);
61
+ const takeValue = () => {
62
+ if (inline !== undefined) return inline;
63
+ if (index + 1 >= argv.length) {
64
+ throw usageError('missing_option_value', `${name} needs a value`, `Pass ${name} <value>`);
65
+ }
66
+ index++;
67
+ return String(argv[index]);
68
+ };
69
+ if (name === '--seconds') {
70
+ secondsRaw = takeValue();
71
+ } else if (name === '--port') {
72
+ portRaw = takeValue();
73
+ } else if (name === '--no-open') {
74
+ if (inline !== undefined) {
75
+ throw usageError('unexpected_option_value', '--no-open takes no value', 'Use --no-open by itself');
76
+ }
77
+ open = false;
78
+ } else if (name === '--no-watch') {
79
+ if (inline !== undefined) {
80
+ throw usageError('unexpected_option_value', '--no-watch takes no value', 'Use --no-watch by itself');
81
+ }
82
+ watch = false;
83
+ } else {
84
+ throw usageError(
85
+ 'unknown_option',
86
+ `unknown option ${name}`,
87
+ 'The supported options are --seconds, --port, --no-open, --no-watch and --help; use --help for usage.',
88
+ );
89
+ }
90
+ continue;
91
+ }
92
+ if (!endOptions && argument.startsWith('-') && argument !== '-') {
93
+ throw usageError(
94
+ 'unknown_option',
95
+ `unknown option ${argument}`,
96
+ 'The supported options are --seconds, --port, --no-open, --no-watch and --help; use --help for usage.',
97
+ );
98
+ }
99
+ if (folder !== null) {
100
+ throw usageError('extra_argument', `extra folder argument ${JSON.stringify(argument)}`, 'Pass at most one project folder');
101
+ }
102
+ folder = argument;
103
+ }
104
+
105
+ if (help) {
106
+ return { help: true, folder, secondsRaw, port: 0, open, watch };
107
+ }
108
+ return { help: false, folder, secondsRaw, port: parsePort(portRaw), open, watch };
109
+ }
110
+
111
+ export function devCommandUsageText() {
112
+ return [
113
+ 'animader dev — serve a project on loopback, watch edits and reload',
114
+ '',
115
+ 'Usage:',
116
+ ' animader dev [folder] [--seconds S] [--port N] [--no-open] [--no-watch]',
117
+ '',
118
+ 'The folder is the project folder relative to the invocation directory',
119
+ '(default: the current directory; parent folders are never searched).',
120
+ 'The host duration comes from animader.json; an explicit --seconds wins',
121
+ 'after the config has validated.',
122
+ '',
123
+ 'dev runs the full build (bake, derive, reflect, join, pack, the host',
124
+ 'audit and the score-build receipt) into staging outside the project,',
125
+ 'serves it pinned to one immutable generation, watches the authored',
126
+ 'tree (kernels and nested assets included; out/ and dist/ excluded)',
127
+ 'and rebuilds on edits: bursts coalesce, a change during a build',
128
+ 'discards that candidate and queues exactly one rebuild, and the page',
129
+ 'is told to reload only on a successful build — a full reload into',
130
+ 'READY, Start pressed again, no state preserved. A failed build keeps',
131
+ 'serving the last successful generation and reports the error; before',
132
+ 'the first successful build the root serves an error/recovery page.',
133
+ '',
134
+ 'Options:',
135
+ ` --seconds S explicit host duration, a positive number at most 600`,
136
+ ' --port N 127.0.0.1 port; 0 (the default) chooses a free one',
137
+ ' --no-open do not attempt to launch the browser',
138
+ ' --no-watch serve this one build without watching or reloading',
139
+ ' -h, --help this text',
140
+ '',
141
+ "The player page, not the terminal, reports WebGPU and audio health:",
142
+ 'READY is earned only in the browser\'s own semantic chain.',
143
+ ].join('\n');
144
+ }
145
+
146
+ /**
147
+ * Run `animader dev`. The preflight per build is the shared full build; a
148
+ * score this host cannot play never reaches a served generation. Serves
149
+ * until SIGINT/SIGTERM, then closes the watcher, the server, the channel
150
+ * connections and the generation store, and prints the stopping signal.
151
+ * If nothing was ever published, the last build error owns the exit code.
152
+ * `context.signals` may replace the default signal set (an empty array
153
+ * returns immediately with the live session so a test caller owns the
154
+ * wait).
155
+ *
156
+ * @param {string[]} argv
157
+ * @param {{out: (line: string) => void, err: (line: string) => void,
158
+ * io?: object, native?: object, vendorRoot?: string,
159
+ * producers?: object, launch?: object, signals?: string[]}} context
160
+ */
161
+ export async function run(argv, context) {
162
+ const args = parseDevCommandArgv(argv);
163
+ if (args.help) {
164
+ context.out(devCommandUsageText());
165
+ return;
166
+ }
167
+
168
+ // The folder resolves once, against the invocation directory; the
169
+ // session and every per-build re-resolution use this absolute path, so
170
+ // a cwd change after the command starts can never move the project.
171
+ const folderPath = resolve(commandFolder(args));
172
+ const io = context.io ?? createNodeIo();
173
+ const session = await runDevSession({
174
+ folder: folderPath,
175
+ port: args.port,
176
+ open: args.open,
177
+ watch: args.watch,
178
+ resolveSeconds: () => resolveConfiguredSeconds({ folder: folderPath, secondsRaw: args.secondsRaw, io }),
179
+ out: context.out,
180
+ err: context.err,
181
+ hosts: { native: context.native, vendorRoot: context.vendorRoot, producers: context.producers, launch: context.launch },
182
+ });
183
+
184
+ const signals = context.signals ?? ['SIGINT', 'SIGTERM'];
185
+ if (signals.length === 0) {
186
+ return { served: session.served, session };
187
+ }
188
+ const signal = await new Promise((resolve) => {
189
+ const attached = [];
190
+ const stop = (name) => {
191
+ // Deregister every listener, including the one that fired, so no
192
+ // stray signal handlers survive this command in the host process.
193
+ for (const { name: other, handler } of attached) process.off(other, handler);
194
+ resolve(name);
195
+ };
196
+ for (const name of signals) {
197
+ const handler = () => stop(name);
198
+ process.once(name, handler);
199
+ attached.push({ name, handler });
200
+ }
201
+ });
202
+ const outcome = await session.stop();
203
+ context.out(`stopped (${signal})`);
204
+ if (!outcome.published && outcome.lastError !== null) {
205
+ // Nothing was ever served and a build actually failed: the last error
206
+ // keeps its exit class so scripts and CI see the same identity a
207
+ // pre-build failure had. A stop before the first publish with no
208
+ // failure — the operator interrupted the initial build, and the loop
209
+ // discarded the abandoned candidate — is a clean stop, not an error
210
+ // exit, so the process exit code is left alone.
211
+ process.exitCode = outcome.lastError.exitCode ?? 1;
212
+ }
213
+ return { served: session.served, session };
214
+ }