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.
- package/LICENSE +121 -0
- package/adapters/build-node.mjs +265 -0
- package/adapters/devserver.mjs +481 -0
- package/adapters/devsession.mjs +619 -0
- package/adapters/export-node.mjs +603 -0
- package/adapters/export-run.mjs +126 -0
- package/adapters/generations.mjs +183 -0
- package/adapters/http.mjs +294 -0
- package/adapters/launch.mjs +83 -0
- package/adapters/node-fs.mjs +80 -0
- package/adapters/pngine.mjs +87 -0
- package/adapters/snapshot.mjs +143 -0
- package/adapters/target.mjs +104 -0
- package/adapters/watch.mjs +355 -0
- package/adapters/webaudio.mjs +102 -0
- package/bin/animader.mjs +31 -0
- package/hello/join.js +46 -0
- package/hello/run.js +259 -0
- package/lib/build.mjs +126 -0
- package/lib/buildloop.mjs +254 -0
- package/lib/contract.mjs +371 -0
- package/lib/errors.mjs +51 -0
- package/lib/export.mjs +445 -0
- package/lib/new.mjs +304 -0
- package/lib/pipeline.mjs +247 -0
- package/lib/receipt.mjs +331 -0
- package/lib/rmtree.mjs +49 -0
- package/lib/routes.mjs +170 -0
- package/lib/score.mjs +434 -0
- package/lib/show.mjs +458 -0
- package/lib/soundtrack.mjs +67 -0
- package/lib/vendor.mjs +99 -0
- package/lib/wav.mjs +197 -0
- package/package.json +47 -4
- package/player/boot.mjs +792 -0
- package/player/controller.mjs +366 -0
- package/player/devchannel.mjs +306 -0
- package/player/index.html +35 -0
- package/player/page.mjs +134 -0
- package/player/player.css +393 -0
- package/player/release.mjs +76 -0
- package/src/commands/build.mjs +201 -0
- package/src/commands/dev.mjs +214 -0
- package/src/commands/doctor.mjs +348 -0
- package/src/commands/export.mjs +158 -0
- package/src/commands/new.mjs +164 -0
- package/src/commands/project.mjs +46 -0
- package/src/config.mjs +110 -0
- package/src/router.mjs +149 -0
- package/src/toolchain.mjs +691 -0
- package/starters/circle/README.md +50 -0
- package/starters/circle/animader.json +4 -0
- package/starters/circle/gitignore +9 -0
- package/starters/circle/kernels/circle.wgsl +32 -0
- package/starters/circle/program.sjon +34 -0
- package/starters/circle/timeline.piece.sjon +26 -0
- package/starters/ember/bake/spans.bin +0 -0
- package/starters/ember/kernels/curl.wgsl +10 -0
- package/starters/ember/kernels/ember.wgsl +10 -0
- package/starters/ember/kernels/hash.wgsl +12 -0
- package/starters/ember/kernels/simplex.wgsl +27 -0
- package/starters/ember/program.sjon +328 -0
- package/starters/ember/timeline.piece.sjon +34 -0
- package/toolchain/licenses/animader.txt +121 -0
- package/toolchain/licenses/esbuild.txt +21 -0
- package/toolchain/licenses/pacer.txt +121 -0
- package/toolchain/licenses/pngine.txt +121 -0
- package/toolchain/manifest.json +212 -0
- package/toolchain/prepare.mjs +446 -0
- package/toolchain/uniform-type-name.js +184 -0
- 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
|
+
}
|