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,46 @@
1
+ // Shared resolution for the project commands (dev/build/export): the
2
+ // project folder relative to the invocation directory and one validated
3
+ // animader.json reading. Precedence is the config module's own rule — an
4
+ // explicit --seconds wins only after a present config has fully validated,
5
+ // so an override can never conceal malformed JSON or an unsupported version.
6
+
7
+ import { createNodeIo } from '../../adapters/node-fs.mjs';
8
+ import { projectError } from '../../lib/errors.mjs';
9
+ import { PROJECT_CONFIG_NAME, resolveProjectConfig } from '../config.mjs';
10
+
11
+ /**
12
+ * The project folder a project command operates on: the explicit positional,
13
+ * else the invocation directory itself. No parent search happens here or
14
+ * anywhere else — a project is where the command is pointed.
15
+ *
16
+ * @param {{folder: string|null}} args the parsed command argv
17
+ * @returns {string}
18
+ */
19
+ export function commandFolder(args) {
20
+ return args.folder ?? '.';
21
+ }
22
+
23
+ /**
24
+ * Read and resolve animader.json for one command run.
25
+ *
26
+ * @param {Object} options
27
+ * @param {string} options.folder the project folder
28
+ * @param {string|undefined} options.secondsRaw the raw --seconds value
29
+ * @param {object} [options.io] the injected filesystem port (statFile, readFile)
30
+ * @returns {Promise<{seconds: number}>}
31
+ */
32
+ export async function resolveConfiguredSeconds({ folder, secondsRaw, io }) {
33
+ const effectiveIo = io ?? createNodeIo();
34
+ const configPath = `${String(folder).replace(/[\\/]+$/, '')}/${PROJECT_CONFIG_NAME}`;
35
+ const stats = await effectiveIo.statFile(configPath);
36
+ if (stats !== null && !stats.isFile) {
37
+ throw projectError(
38
+ 'config_not_file',
39
+ `${PROJECT_CONFIG_NAME} is not a regular file in ${folder}`,
40
+ `Replace ${configPath} with a regular file.`,
41
+ );
42
+ }
43
+ const configBytes = stats === null ? null : await effectiveIo.readFile(configPath);
44
+ const resolved = await resolveProjectConfig({ folder, configBytes, overrideSeconds: secondsRaw });
45
+ return { seconds: resolved.seconds };
46
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,110 @@
1
+ // The shared animader.json resolution rules: one reader of the authored
2
+ // project config for new/doctor now and dev/build/export later. This module
3
+ // is portable: no `node:` import and no process state — config bytes in, one
4
+ // frozen resolved object out; reading the file and spawning are host work.
5
+ //
6
+ // Rules (plan "Project format and defaults"): the file is animader.json at
7
+ // the project root, format animader-project/1, host settings only (seconds
8
+ // here, never tempo/lanes/length). Precedence is an explicit CLI option over
9
+ // the project config — but a present config validates first, so an override
10
+ // can never hide malformed JSON, an unsupported version or an unknown key.
11
+ //
12
+ // The duration bound, the --seconds wording and the error vocabulary are the
13
+ // package's own lib (contract.mjs, errors.mjs); installed-toolchain plan,
14
+ // stage 1 moved them into the package. The
15
+ // installed-toolchain boundary is plan section 2 and is not in this
16
+ // dispatch.
17
+
18
+ import { MAX_SECONDS, parseSeconds } from '../lib/contract.mjs';
19
+ import { projectError } from '../lib/errors.mjs';
20
+
21
+ export const PROJECT_CONFIG_NAME = 'animader.json';
22
+ export const PROJECT_CONFIG_FORMAT = 'animader-project/1';
23
+
24
+ const CONFIG_KEYS = Object.freeze(['format', 'seconds']);
25
+ const RANGE_WORDS = `a positive number at most ${MAX_SECONDS}`;
26
+
27
+ const DECODER = new TextDecoder();
28
+
29
+ function configPath(folder) {
30
+ return `${String(folder).replace(/[\\/]+$/, '')}/${PROJECT_CONFIG_NAME}`;
31
+ }
32
+
33
+ function assertConfigSeconds(value, path) {
34
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > MAX_SECONDS) {
35
+ throw projectError(
36
+ 'invalid_config_seconds',
37
+ `seconds in ${path} must be ${RANGE_WORDS}; got ${JSON.stringify(value) ?? String(value)}`,
38
+ `Set seconds in ${path} to ${RANGE_WORDS}, for example 8.`,
39
+ );
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Resolve and validate the authored project config.
45
+ *
46
+ * @param {Object} options
47
+ * @param {string} options.folder the project folder, for file-specific errors
48
+ * @param {Uint8Array|null|undefined} options.configBytes the raw animader.json
49
+ * bytes, or null/undefined when the file is absent
50
+ * @param {string|number|undefined} [options.overrideSeconds] the explicit CLI
51
+ * --seconds value, parsed with the demo's own rules when present
52
+ * @returns {{seconds: number}} the one resolved object consumers receive
53
+ */
54
+ export async function resolveProjectConfig({ folder, configBytes, overrideSeconds }) {
55
+ const path = configPath(folder);
56
+
57
+ let document = null;
58
+ let present = false;
59
+ if (configBytes !== null && configBytes !== undefined) {
60
+ present = true;
61
+ try {
62
+ document = JSON.parse(DECODER.decode(configBytes));
63
+ } catch (error) {
64
+ throw projectError(
65
+ 'invalid_config_json',
66
+ `${path} is not valid JSON: ${error?.message ?? String(error)}`,
67
+ `Fix the JSON syntax in ${path}; the file must hold one JSON object.`,
68
+ );
69
+ }
70
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
71
+ throw projectError(
72
+ 'config_not_object',
73
+ `${path} must contain a JSON object; got ${JSON.stringify(document)}`,
74
+ `Write one JSON object in ${path}, for example {"format": ${JSON.stringify(PROJECT_CONFIG_FORMAT)}, "seconds": 8}.`,
75
+ );
76
+ }
77
+ if (document.format !== PROJECT_CONFIG_FORMAT) {
78
+ throw projectError(
79
+ 'unsupported_config_format',
80
+ `${path} names format ${JSON.stringify(document.format ?? null)}; this tool supports ${PROJECT_CONFIG_FORMAT} only`,
81
+ `Set "format" to ${JSON.stringify(PROJECT_CONFIG_FORMAT)} in ${path}.`,
82
+ );
83
+ }
84
+ for (const key of Object.keys(document)) {
85
+ if (!CONFIG_KEYS.includes(key)) {
86
+ throw projectError(
87
+ 'unknown_config_setting',
88
+ `${path} has an unknown setting ${JSON.stringify(key)}; the supported settings are ${CONFIG_KEYS.join(', ')}`,
89
+ `Remove ${JSON.stringify(key)} from ${path}; tempo, lanes and length live in timeline.piece.sjon.`,
90
+ );
91
+ }
92
+ }
93
+ if (document.seconds !== undefined) assertConfigSeconds(document.seconds, path);
94
+ }
95
+
96
+ // A present config has fully validated by here, so applying the override
97
+ // now cannot conceal a malformed or unsupported config.
98
+ if (overrideSeconds !== undefined) {
99
+ return Object.freeze({ seconds: parseSeconds(overrideSeconds) });
100
+ }
101
+ if (!present || document.seconds === undefined) {
102
+ const where = present ? `${path} does not set seconds` : `${path} is absent`;
103
+ throw projectError(
104
+ 'missing_seconds',
105
+ `seconds is missing: ${where}; no host duration is inferred from audio or timeline length`,
106
+ `Create ${path} with {"format": ${JSON.stringify(PROJECT_CONFIG_FORMAT)}, "seconds": 8}, or pass --seconds <n> for this run.`,
107
+ );
108
+ }
109
+ return Object.freeze({ seconds: document.seconds });
110
+ }
package/src/router.mjs ADDED
@@ -0,0 +1,149 @@
1
+ // The `animader` command table and top-level argv grammar. This module is the
2
+ // seam the later dev/build/export commands slot into: one row here registers a
3
+ // command, and nothing else in the router changes. Parse decisions live here;
4
+ // each command owns its own argv grammar; the bin maps thrown DemoErrors to
5
+ // their exit codes and error:/remedy: lines.
6
+ //
7
+ // The error vocabulary is the package's own lib/errors.mjs (installed-toolchain
8
+ // plan, stage 1). Stage 2 replaces the contributor-workspace toolchain
9
+ // identity below with the installed, versioned toolchain descriptor.
10
+
11
+ import { usageError } from '../lib/errors.mjs';
12
+
13
+ /**
14
+ * The contributor-workspace identity printed by --version. An installed
15
+ * package (installed-toolchain plan, stage 2) prints the manifest's tested
16
+ * combination instead: see toolchainIdentity() in src/toolchain.mjs.
17
+ */
18
+ export const TOOLCHAIN_IDENTITY = 'source workspace (contributor mode)';
19
+
20
+ // Rows resolve their command module lazily, so a command lands in this
21
+ // dispatch without a router rewrite; each loader returns { run }.
22
+ const COMMANDS = [
23
+ {
24
+ name: 'new',
25
+ summary: 'create an editable project from a tracked starter',
26
+ load: () => import('./commands/new.mjs'),
27
+ },
28
+ {
29
+ name: 'dev',
30
+ summary: 'serve a project on loopback, watch edits and reload the page',
31
+ load: () => import('./commands/dev.mjs'),
32
+ },
33
+ {
34
+ name: 'build',
35
+ summary: 'run the full build (bake, pack, host audit) into out/',
36
+ load: () => import('./commands/build.mjs'),
37
+ },
38
+ {
39
+ name: 'export',
40
+ summary: 'write a standalone release to a fresh --out folder',
41
+ load: () => import('./commands/export.mjs'),
42
+ },
43
+ {
44
+ name: 'doctor',
45
+ summary: 'report host and project health, each problem with a remedy',
46
+ load: () => import('./commands/doctor.mjs'),
47
+ },
48
+ ];
49
+
50
+ const COMMAND_NAMES = Object.freeze(COMMANDS.map((command) => command.name));
51
+
52
+ function commandList() {
53
+ return COMMAND_NAMES.join(', ');
54
+ }
55
+
56
+ /**
57
+ * Parse the top-level argv without touching the filesystem. Global options
58
+ * are only global before the command name; after it, tokens belong to the
59
+ * command's own grammar.
60
+ *
61
+ * @param {string[]} argv
62
+ * @returns {{kind: 'help'} | {kind: 'version'} | {kind: 'command', name: string, argv: string[]}}
63
+ */
64
+ export function parseRouterArgv(argv) {
65
+ if (!Array.isArray(argv)) {
66
+ throw usageError('invalid_argv', 'the CLI adapter must pass an argument array', 'Call the animader bin with process.argv.slice(2)');
67
+ }
68
+
69
+ for (let index = 0; index < argv.length; index++) {
70
+ const argument = String(argv[index]);
71
+ if (argument === '--help' || argument === '-h') return { kind: 'help' };
72
+ if (argument === '--version') return { kind: 'version' };
73
+ if (argument.startsWith('-')) {
74
+ throw usageError(
75
+ 'unknown_option',
76
+ `unknown option ${argument}`,
77
+ `The top-level options are --help and --version; run animader --help for the command list`,
78
+ );
79
+ }
80
+ const row = COMMANDS.find((command) => command.name === argument);
81
+ if (!row) {
82
+ throw usageError(
83
+ 'unknown_command',
84
+ `unknown command ${JSON.stringify(argument)}`,
85
+ `Run animader --help for the command list (available: ${commandList()})`,
86
+ );
87
+ }
88
+ return { kind: 'command', name: row.name, argv: argv.slice(index + 1).map(String) };
89
+ }
90
+
91
+ throw usageError(
92
+ 'missing_command',
93
+ 'a command is required',
94
+ `Run animader --help for the command list (available: ${commandList()})`,
95
+ );
96
+ }
97
+
98
+ export function routerUsageText() {
99
+ return [
100
+ 'animader — create, check and run Animader projects',
101
+ '',
102
+ 'Usage:',
103
+ ' animader <command> [arguments]',
104
+ '',
105
+ 'Commands:',
106
+ ...COMMANDS.map((command) => ` animader ${command.name.padEnd(8)} ${command.summary}`),
107
+ '',
108
+ 'Options:',
109
+ ' --help, -h this text',
110
+ ' --version print the CLI version and the toolchain identity',
111
+ '',
112
+ "Run `animader <command> --help` for that command's options.",
113
+ ].join('\n');
114
+ }
115
+
116
+ /** The two --version lines: the CLI version and one toolchain identity. */
117
+ export function versionLines({ version, identity = TOOLCHAIN_IDENTITY }) {
118
+ return [
119
+ `animader-cli ${version}`,
120
+ `toolchain: ${identity}`,
121
+ ];
122
+ }
123
+
124
+ /**
125
+ * Dispatch parsed argv: print help/version through the context, or load and
126
+ * run the named command. The context carries everything host-shaped:
127
+ * `version`, `out`/`err` line writers, the filesystem `io`, and `env`.
128
+ *
129
+ * @param {string[]} argv
130
+ * @param {{version: string, out: (line: string) => void, err: (line: string) => void,
131
+ * io: object, env: object}} context
132
+ */
133
+ export async function runRouter(argv, context) {
134
+ const parsed = parseRouterArgv(argv);
135
+ if (parsed.kind === 'help') {
136
+ context.out(routerUsageText());
137
+ return parsed;
138
+ }
139
+ if (parsed.kind === 'version') {
140
+ const { toolchainIdentity } = await import('./toolchain.mjs');
141
+ const identity = context.identity ?? await toolchainIdentity({ env: context.env });
142
+ for (const line of versionLines({ version: context.version, identity })) context.out(line);
143
+ return parsed;
144
+ }
145
+ const row = COMMANDS.find((command) => command.name === parsed.name);
146
+ const command = await row.load();
147
+ await command.run(parsed.argv, context);
148
+ return parsed;
149
+ }