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,691 @@
|
|
|
1
|
+
// The installed toolchain boundary (installed-toolchain plan, stage 2).
|
|
2
|
+
//
|
|
3
|
+
// One descriptor answers every "where does this artifact come from"
|
|
4
|
+
// question the CLI has, in one of two modes:
|
|
5
|
+
//
|
|
6
|
+
// contributor — this source workspace: sibling checkouts (env
|
|
7
|
+
// overridable), the prepared demo/vendor bundle and the pngine
|
|
8
|
+
// owner's pinned esbuild. Exactly what the demo has always used.
|
|
9
|
+
//
|
|
10
|
+
// installed — an npm-installed package: every artifact resolves through
|
|
11
|
+
// Node module resolution from this package's dependency graph
|
|
12
|
+
// (animader, pngine, @mr_hugo/pacer, the pinned esbuild, the @pngine/*
|
|
13
|
+
// and @mr_hugo/pacer-* platform packages), verified against the release
|
|
14
|
+
// manifest assembled at pack time. Installed mode never reads demo/,
|
|
15
|
+
// a sibling checkout, a zig cache or zig-out, and never consults the
|
|
16
|
+
// contributor environment overrides.
|
|
17
|
+
//
|
|
18
|
+
// Mode detection is the manifest's presence: the release assembly writes
|
|
19
|
+
// toolchain/manifest.json into the package before npm pack, and the source
|
|
20
|
+
// tree never carries one. ANIMADER_TOOLCHAIN=contributor|installed forces
|
|
21
|
+
// a mode for tests.
|
|
22
|
+
//
|
|
23
|
+
// Every missing or omitted artifact is a DemoError with an exit code and a
|
|
24
|
+
// remedy naming exactly what to install — never a stack trace, never a
|
|
25
|
+
// silent pass.
|
|
26
|
+
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
29
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { dirname, join, resolve } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
import { DemoError } from '../lib/errors.mjs';
|
|
35
|
+
import { VENDOR_FORMAT } from '../lib/vendor.mjs';
|
|
36
|
+
import { defaultContentRoot, resolveRoots, verifyVendor } from '../toolchain/prepare.mjs';
|
|
37
|
+
|
|
38
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
export const MANIFEST_RELATIVE_PATH = 'toolchain/manifest.json';
|
|
40
|
+
export const TOOLCHAIN_MANIFEST_FORMAT = 'animader-toolchain/1';
|
|
41
|
+
|
|
42
|
+
/** The npm package name of the pacer runtime. The producer key stays
|
|
43
|
+
* `pacer` (manifest producers, versions, vendor layout); this is only
|
|
44
|
+
* the published name the dependency graph resolves. */
|
|
45
|
+
export const PACER_PACKAGE = '@mr_hugo/pacer';
|
|
46
|
+
|
|
47
|
+
/** The platform packages, reusing pngine's own @pngine/* scheme and
|
|
48
|
+
* mirroring it for pacer's native pack CLI (stage 3). */
|
|
49
|
+
export const PNGINE_PLATFORM_PACKAGES = Object.freeze({
|
|
50
|
+
'darwin-arm64': '@pngine/darwin-arm64',
|
|
51
|
+
'darwin-x64': '@pngine/darwin-x64',
|
|
52
|
+
'linux-arm64': '@pngine/linux-arm64',
|
|
53
|
+
'linux-x64': '@pngine/linux-x64',
|
|
54
|
+
'openbsd-x64': '@pngine/openbsd-x64',
|
|
55
|
+
'win32-arm64': '@pngine/win32-arm64',
|
|
56
|
+
'win32-x64': '@pngine/win32-x64',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const PACER_PLATFORM_PACKAGES = Object.freeze(
|
|
60
|
+
Object.fromEntries(
|
|
61
|
+
Object.entries(PNGINE_PLATFORM_PACKAGES).map(([platform]) => [platform, `${PACER_PACKAGE}-${platform}`]),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
function toolchainError(code, message, remedy, extra = {}) {
|
|
66
|
+
return new DemoError(code, message, { exitCode: 4, remedy, ...extra });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function missingRemedy(packageName, version, artifact) {
|
|
70
|
+
return `Install ${packageName}@${version} (or reinstall animader-cli so its dependency graph pulls it): the ${artifact} comes from that package.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Detect the toolchain mode: forced by ANIMADER_TOOLCHAIN, else installed
|
|
75
|
+
* exactly when the package carries its release manifest (the source tree
|
|
76
|
+
* never does).
|
|
77
|
+
*
|
|
78
|
+
* @param {{env?: object, pkgRoot?: string}} [options]
|
|
79
|
+
* @returns {Promise<'contributor' | 'installed'>}
|
|
80
|
+
*/
|
|
81
|
+
export async function detectToolchainMode({ env = process.env, pkgRoot = PKG_ROOT } = {}) {
|
|
82
|
+
const forced = env.ANIMADER_TOOLCHAIN;
|
|
83
|
+
if (forced !== undefined) {
|
|
84
|
+
if (forced !== 'contributor' && forced !== 'installed') {
|
|
85
|
+
throw toolchainError(
|
|
86
|
+
'bad_toolchain_mode',
|
|
87
|
+
`ANIMADER_TOOLCHAIN=${JSON.stringify(forced)} is not a mode`,
|
|
88
|
+
'Set ANIMADER_TOOLCHAIN to "contributor" or "installed", or unset it to detect from the package layout.',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return forced;
|
|
92
|
+
}
|
|
93
|
+
return (await probeManifest(pkgRoot)) !== null ? 'installed' : 'contributor';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The manifest's absolute path when the package carries one. */
|
|
97
|
+
async function probeManifest(pkgRoot) {
|
|
98
|
+
const path = join(pkgRoot, MANIFEST_RELATIVE_PATH);
|
|
99
|
+
const info = await stat(path).catch(() => null);
|
|
100
|
+
return info !== null && info.isFile() ? path : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sha256(bytes) {
|
|
104
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function canonicalRows(rows) {
|
|
108
|
+
return rows
|
|
109
|
+
.map((row) => `${row.path}|${row.source ?? row.spec ?? ''}|${row.sha256}|${row.bytes}`)
|
|
110
|
+
.sort()
|
|
111
|
+
.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate the release manifest's shape. Returns a normalized copy.
|
|
116
|
+
*
|
|
117
|
+
* @param {unknown} manifest
|
|
118
|
+
* @returns {{format: string, toolchainVersion: string, target: {os: string, arch: string},
|
|
119
|
+
* producers: Object, runtime: Array, tools: Object, helpers: Object,
|
|
120
|
+
* pngineCore: Object, digest: string}}
|
|
121
|
+
*/
|
|
122
|
+
export function validateToolchainManifest(manifest) {
|
|
123
|
+
const bad = (detail) => toolchainError(
|
|
124
|
+
'bad_toolchain_manifest',
|
|
125
|
+
`the toolchain manifest is not valid: ${detail}`,
|
|
126
|
+
'Reinstall animader-cli; the release assembly writes this manifest, and a broken one means a broken package.',
|
|
127
|
+
);
|
|
128
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) throw bad('not an object');
|
|
129
|
+
if (manifest.format !== TOOLCHAIN_MANIFEST_FORMAT) {
|
|
130
|
+
throw bad(`format ${JSON.stringify(manifest.format)} is not ${TOOLCHAIN_MANIFEST_FORMAT}`);
|
|
131
|
+
}
|
|
132
|
+
if (typeof manifest.toolchainVersion !== 'string' || manifest.toolchainVersion === '') throw bad('no toolchainVersion');
|
|
133
|
+
const target = manifest.target;
|
|
134
|
+
if (target === null || typeof target !== 'object' || typeof target.os !== 'string' || typeof target.arch !== 'string') {
|
|
135
|
+
throw bad('no target {os, arch}');
|
|
136
|
+
}
|
|
137
|
+
if (target.os !== process.platform || target.arch !== process.arch) {
|
|
138
|
+
throw toolchainError(
|
|
139
|
+
'toolchain_platform_mismatch',
|
|
140
|
+
`this toolchain was assembled for ${target.os}-${target.arch}, not ${process.platform}-${process.arch}`,
|
|
141
|
+
`Install an animader-cli platform build for ${process.platform}-${process.arch}; this package's native binaries cannot run here.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
for (const key of ['producers', 'runtime', 'tools', 'helpers', 'pngineCore']) {
|
|
145
|
+
if (manifest[key] === null || typeof manifest[key] !== 'object') throw bad(`no ${key}`);
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(manifest.runtime) || manifest.runtime.length === 0) throw bad('no runtime rows');
|
|
148
|
+
for (const row of manifest.runtime) {
|
|
149
|
+
if (typeof row.path !== 'string' || typeof row.spec !== 'string' || !/^[0-9a-f]{64}$/.test(String(row.sha256 ?? '')) || !Number.isInteger(row.bytes)) {
|
|
150
|
+
throw bad(`runtime row ${JSON.stringify(row?.path)} is not a {path, spec, sha256, bytes} record`);
|
|
151
|
+
}
|
|
152
|
+
// A row may record a file inside its package (producer src modules
|
|
153
|
+
// the owner's exports map does not expose). It must name exactly
|
|
154
|
+
// that: a relative path that stays inside the package root.
|
|
155
|
+
if (row.file !== undefined
|
|
156
|
+
&& (typeof row.file !== 'string' || row.file === '' || row.file.startsWith('/') || row.file.includes('\\') || row.file.split('/').includes('..'))) {
|
|
157
|
+
throw bad(`runtime row ${JSON.stringify(row?.path)} carries an invalid package file ${JSON.stringify(row.file)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [name, entry] of Object.entries(manifest.tools)) {
|
|
161
|
+
if (typeof entry?.package !== 'string' || typeof entry.file !== 'string') {
|
|
162
|
+
throw bad(`tool ${name} is not a {package, file} record`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const digest = sha256(canonicalRows(manifest.runtime));
|
|
166
|
+
if (manifest.digest !== digest) {
|
|
167
|
+
throw bad('the runtime digest does not match its rows');
|
|
168
|
+
}
|
|
169
|
+
return { ...manifest, digest };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the toolchain for this run. One descriptor, both modes; see the
|
|
174
|
+
* module comment. Contributor mode wraps toolchain/prepare.mjs's roots.
|
|
175
|
+
*
|
|
176
|
+
* @param {{env?: object, pkgRoot?: string, contributorRoots?: object}} [options]
|
|
177
|
+
*/
|
|
178
|
+
export async function resolveToolchain({ env = process.env, pkgRoot = PKG_ROOT, contributorRoots } = {}) {
|
|
179
|
+
const mode = await detectToolchainMode({ env, pkgRoot });
|
|
180
|
+
if (mode === 'installed') {
|
|
181
|
+
return resolveInstalledToolchain({ env, pkgRoot });
|
|
182
|
+
}
|
|
183
|
+
return contributorToolchain({
|
|
184
|
+
env,
|
|
185
|
+
roots: contributorRoots,
|
|
186
|
+
vendorRoot: contributorRoots?.vendorRoot,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The contributor-workspace toolchain, constructed synchronously: sibling
|
|
192
|
+
* checkout roots (env overridable), the prepared demo/vendor bundle and
|
|
193
|
+
* the pngine owner's pinned esbuild. Path resolution never touches the
|
|
194
|
+
* filesystem here; each artifact is existence-checked when used.
|
|
195
|
+
*
|
|
196
|
+
* @param {{env?: object, roots?: object, vendorRoot?: string}} [options]
|
|
197
|
+
*/
|
|
198
|
+
export function contributorToolchain({ env = process.env, roots, vendorRoot } = {}) {
|
|
199
|
+
// A partial roots object (tests pass only pngineRoot or only a vendor
|
|
200
|
+
// root) merges over the resolved workspace defaults, so every root the
|
|
201
|
+
// descriptor needs is present.
|
|
202
|
+
const resolved = { ...resolveRoots({ env }), ...(roots ?? {}) };
|
|
203
|
+
const repoRoot = vendorRoot !== undefined && roots === undefined
|
|
204
|
+
? defaultContentRoot()
|
|
205
|
+
: resolve(resolved.contentRoot ?? defaultContentRoot());
|
|
206
|
+
const root = vendorRoot ?? join(repoRoot, 'demo', 'vendor');
|
|
207
|
+
const identity = 'source workspace (contributor mode)';
|
|
208
|
+
|
|
209
|
+
const file = (relative) => join(root, relative);
|
|
210
|
+
const requireFile = async (relative) => {
|
|
211
|
+
const path = file(relative);
|
|
212
|
+
const info = await stat(path).catch(() => null);
|
|
213
|
+
if (info === null || !info.isFile() || info.size === 0) {
|
|
214
|
+
throw toolchainError(
|
|
215
|
+
'toolchain_artifact_missing',
|
|
216
|
+
`the prepared runtime is missing ${relative}`,
|
|
217
|
+
'Run the one-time prepare: node demo/prepare.mjs (the contributor toolchain reads demo/vendor).',
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return path;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const pnginePackageRoot = join(resolved.pngineRoot, 'npm', 'pngine');
|
|
224
|
+
const pacerPackageRoot = join(resolved.pacerRoot, 'npm', 'pacer');
|
|
225
|
+
const cliPackage = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
226
|
+
|
|
227
|
+
const descriptor = {
|
|
228
|
+
mode: 'contributor',
|
|
229
|
+
identity,
|
|
230
|
+
target: { os: process.platform, arch: process.arch },
|
|
231
|
+
manifest: null,
|
|
232
|
+
runtime: {
|
|
233
|
+
kind: 'dir',
|
|
234
|
+
root,
|
|
235
|
+
/** Sync path answer: the vendor layout joined, no fs probe. */
|
|
236
|
+
path: (relative) => file(relative),
|
|
237
|
+
async requireFile(relative) { return requireFile(relative); },
|
|
238
|
+
async files() {
|
|
239
|
+
const verified = await verifyVendor({ vendorRoot: root });
|
|
240
|
+
return verified.files.map((row) => ({ path: row.path, source: join(root, row.path) }));
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
pngine: {
|
|
244
|
+
cli: resolved.pngineCli,
|
|
245
|
+
viewer: file('pngine.mjs'),
|
|
246
|
+
core: join(pnginePackageRoot, 'dist', 'core.mjs'),
|
|
247
|
+
uniformTypeName: join(pnginePackageRoot, 'src', 'uniform-convert.js'),
|
|
248
|
+
},
|
|
249
|
+
pacer: {
|
|
250
|
+
cli: join(resolved.pacerRoot, 'zig-out', 'bin', 'pacer'),
|
|
251
|
+
index: file('pacer/index.js'),
|
|
252
|
+
route: file('pacer/route.js'),
|
|
253
|
+
rt: file('pacer/rt.js'),
|
|
254
|
+
pacerWasm: file('pacer/pacer.wasm'),
|
|
255
|
+
pacerRtWasm: file('pacer/pacer-rt.wasm'),
|
|
256
|
+
},
|
|
257
|
+
animader: {
|
|
258
|
+
wasm: file('animader/animader.wasm'),
|
|
259
|
+
piece: file('animader/piece.js'),
|
|
260
|
+
},
|
|
261
|
+
versions: {
|
|
262
|
+
animader: null,
|
|
263
|
+
pngine: null,
|
|
264
|
+
pacer: null,
|
|
265
|
+
esbuild: cliPackage.dependencies?.esbuild ?? null,
|
|
266
|
+
},
|
|
267
|
+
esbuild: {
|
|
268
|
+
/** Resolve the pinned esbuild. Contributor mode reads the pngine
|
|
269
|
+
* owner's install (the plan's pinned owner) and refuses drift from
|
|
270
|
+
* the version this CLI declares. */
|
|
271
|
+
require() {
|
|
272
|
+
const requireFromPngine = createRequire(join(pnginePackageRoot, 'package.json'));
|
|
273
|
+
let esbuild;
|
|
274
|
+
try {
|
|
275
|
+
esbuild = requireFromPngine('esbuild');
|
|
276
|
+
} catch (cause) {
|
|
277
|
+
throw toolchainError(
|
|
278
|
+
'bundle_tool_missing',
|
|
279
|
+
'the pinned owner esbuild could not be resolved from the pngine checkout',
|
|
280
|
+
'Build/install the pngine npm workspace as prepared; the contributor toolchain bundles with the owner esbuild.',
|
|
281
|
+
{ cause },
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const pinned = cliPackage.dependencies?.esbuild;
|
|
285
|
+
if (typeof esbuild.version === 'string' && pinned && esbuild.version !== pinned) {
|
|
286
|
+
throw toolchainError(
|
|
287
|
+
'bundle_tool_version_drift',
|
|
288
|
+
`the pngine checkout carries esbuild ${esbuild.version}, but this CLI pins ${pinned}`,
|
|
289
|
+
`Align them: update the CLI's esbuild pin or the pngine workspace install to ${pinned}.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return esbuild;
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
async verifyRuntime() {
|
|
296
|
+
const verified = await verifyVendor({ vendorRoot: root });
|
|
297
|
+
return {
|
|
298
|
+
format: verified.format,
|
|
299
|
+
files: verified.files,
|
|
300
|
+
digest: verified.digest,
|
|
301
|
+
provenance: {
|
|
302
|
+
format: verified.format,
|
|
303
|
+
producers: verified.producers,
|
|
304
|
+
files: verified.files,
|
|
305
|
+
digest: verified.digest,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
async verifyTools() {
|
|
310
|
+
const rows = [];
|
|
311
|
+
for (const [name, path] of [['pngine CLI', resolved.pngineCli], ['pacer CLI', descriptor.pacer.cli]]) {
|
|
312
|
+
const info = await stat(path).catch(() => null);
|
|
313
|
+
if (info === null || !info.isFile()) {
|
|
314
|
+
throw toolchainError(
|
|
315
|
+
'toolchain_artifact_missing',
|
|
316
|
+
`the ${name} binary is missing at ${path}`,
|
|
317
|
+
name === 'pngine CLI'
|
|
318
|
+
? 'Build pngine in its checkout (zig build) or point PNGINE at the binary; contributor mode never installs it.'
|
|
319
|
+
: "Build the pacer CLI in its checkout (zig build cli) or set ANIMADER_PACER to that checkout; contributor mode never installs it.",
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
rows.push({ name, path, bytes: info.size, sha256: sha256(readFileSync(path)) });
|
|
323
|
+
}
|
|
324
|
+
return { tools: rows };
|
|
325
|
+
},
|
|
326
|
+
async licenseText() {
|
|
327
|
+
return readFile(join(repoRoot, 'LICENSE'), 'utf8');
|
|
328
|
+
},
|
|
329
|
+
async producerVersion(name) {
|
|
330
|
+
const packageRoot = name === 'pngine' ? pnginePackageRoot : name === 'pacer' ? pacerPackageRoot : null;
|
|
331
|
+
if (packageRoot === null) return null;
|
|
332
|
+
let text = '{"version":null}';
|
|
333
|
+
try {
|
|
334
|
+
text = readFileSync(join(packageRoot, 'package.json'), 'utf8');
|
|
335
|
+
} catch {
|
|
336
|
+
// A missing producer package answers no version, not a crash.
|
|
337
|
+
}
|
|
338
|
+
return JSON.parse(text).version ?? null;
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
return descriptor;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function resolveInstalledToolchain({ env, pkgRoot }) {
|
|
345
|
+
const manifestPath = await probeManifest(pkgRoot);
|
|
346
|
+
if (manifestPath === null) {
|
|
347
|
+
throw toolchainError(
|
|
348
|
+
'toolchain_manifest_missing',
|
|
349
|
+
'this package runs in installed mode but carries no toolchain manifest',
|
|
350
|
+
'Reinstall animader-cli from its packed tarball; the release assembly writes toolchain/manifest.json.',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
let manifest;
|
|
354
|
+
try {
|
|
355
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
356
|
+
} catch (cause) {
|
|
357
|
+
throw toolchainError(
|
|
358
|
+
'bad_toolchain_manifest',
|
|
359
|
+
`the toolchain manifest is not JSON: ${cause.message}`,
|
|
360
|
+
'Reinstall animader-cli; the release assembly writes this manifest.',
|
|
361
|
+
{ cause },
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
manifest = validateToolchainManifest(manifest);
|
|
365
|
+
|
|
366
|
+
const requireFromPackage = createRequire(join(pkgRoot, 'package.json'));
|
|
367
|
+
const platform = `${manifest.target.os}-${manifest.target.arch}`;
|
|
368
|
+
const versions = {
|
|
369
|
+
animader: manifest.producers?.animader?.version ?? null,
|
|
370
|
+
pngine: manifest.producers?.pngine?.version ?? null,
|
|
371
|
+
pacer: manifest.producers?.pacer?.version ?? null,
|
|
372
|
+
esbuild: manifest.producers?.esbuild?.version ?? null,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Resolve one spec from the installed dependency graph. Plain-string
|
|
377
|
+
* exports go through require.resolve; browser/import-conditioned
|
|
378
|
+
* exports (pngine's viewer and core) are resolved by reading the
|
|
379
|
+
* package's own exports map — Node's require.resolve refuses the
|
|
380
|
+
* "import" condition, and the installed CLI must not depend on a
|
|
381
|
+
* bundler to find its artifacts. Any reshape of the owner's exports
|
|
382
|
+
* turns into the named missing-artifact failure instead of a guess.
|
|
383
|
+
*/
|
|
384
|
+
function resolveSpec(spec, packageName, version, artifact) {
|
|
385
|
+
const missing = (cause) => toolchainError(
|
|
386
|
+
'toolchain_artifact_missing',
|
|
387
|
+
`the ${artifact} (${spec}) is not installed`,
|
|
388
|
+
missingRemedy(packageName, version, artifact),
|
|
389
|
+
{ cause, artifact: spec },
|
|
390
|
+
);
|
|
391
|
+
try {
|
|
392
|
+
return requireFromPackage.resolve(spec);
|
|
393
|
+
} catch (defaultError) {
|
|
394
|
+
const walked = resolveThroughExportsMap(spec);
|
|
395
|
+
if (walked !== null) return walked;
|
|
396
|
+
throw missing(defaultError);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The package root for an installed dependency, found from its main
|
|
401
|
+
* entry by walking up to the directory that owns its package.json. */
|
|
402
|
+
function packageRootOf(name) {
|
|
403
|
+
let main;
|
|
404
|
+
try {
|
|
405
|
+
main = requireFromPackage.resolve(name);
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
let directory = dirname(main);
|
|
410
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
411
|
+
if (existsSync(join(directory, 'package.json'))) return directory;
|
|
412
|
+
const parent = dirname(directory);
|
|
413
|
+
if (parent === directory) return null;
|
|
414
|
+
directory = parent;
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function resolveThroughExportsMap(spec) {
|
|
420
|
+
const separator = spec.indexOf('/');
|
|
421
|
+
const name = spec.startsWith('@') ? spec.slice(0, spec.indexOf('/', separator + 1)) : spec.slice(0, separator);
|
|
422
|
+
const subpath = `.${spec.slice(name.length)}`;
|
|
423
|
+
const root = packageRootOf(name);
|
|
424
|
+
if (root === null) return null;
|
|
425
|
+
let manifest;
|
|
426
|
+
try {
|
|
427
|
+
manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
428
|
+
} catch {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const exports = manifest?.exports;
|
|
432
|
+
if (exports === null || typeof exports !== 'object') return null;
|
|
433
|
+
const entry = exports[subpath] ?? (subpath === '.' ? exports['.'] : undefined);
|
|
434
|
+
const picked = pickExportTarget(entry);
|
|
435
|
+
if (picked === null || picked.startsWith('.') === false) return null;
|
|
436
|
+
const path = resolve(root, picked);
|
|
437
|
+
return existsSync(path) ? path : null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function pickExportTarget(entry, conditions = ['import', 'browser', 'default', 'node', 'require']) {
|
|
441
|
+
if (typeof entry === 'string') return entry;
|
|
442
|
+
if (entry === null || typeof entry !== 'object') return null;
|
|
443
|
+
for (const condition of conditions) {
|
|
444
|
+
if (condition in entry) {
|
|
445
|
+
const nested = pickExportTarget(entry[condition], conditions);
|
|
446
|
+
if (nested !== null) return nested;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function requireInstalledFile(spec, packageName, version, artifact, file) {
|
|
453
|
+
let path;
|
|
454
|
+
if (file === undefined) {
|
|
455
|
+
path = resolveSpec(spec, packageName, version, artifact);
|
|
456
|
+
} else {
|
|
457
|
+
// A producer src row: the file lives inside its package but outside
|
|
458
|
+
// the owner's exports map, so resolve the package root from the
|
|
459
|
+
// spec and join the recorded file — never a directory traversal.
|
|
460
|
+
const packageRoot = packageRootOf(spec);
|
|
461
|
+
if (packageRoot === null) {
|
|
462
|
+
throw toolchainError(
|
|
463
|
+
'toolchain_artifact_missing',
|
|
464
|
+
`the ${artifact}'s package (${spec}) is not installed`,
|
|
465
|
+
missingRemedy(packageName, version, artifact),
|
|
466
|
+
{ artifact: spec },
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
path = resolve(packageRoot, file);
|
|
470
|
+
}
|
|
471
|
+
const info = await stat(path).catch(() => null);
|
|
472
|
+
if (info === null || !info.isFile() || info.size === 0) {
|
|
473
|
+
throw toolchainError(
|
|
474
|
+
'toolchain_artifact_missing',
|
|
475
|
+
`the ${artifact} (${spec}) resolved to ${path}, which is not a non-empty file`,
|
|
476
|
+
missingRemedy(packageName, version, artifact),
|
|
477
|
+
{ artifact: spec },
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return path;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const runtimeRows = new Map(manifest.runtime.map((row) => [row.path, row]));
|
|
484
|
+
|
|
485
|
+
/** The producer a runtime row's npm package belongs to. Manifest rows
|
|
486
|
+
* carry npm package names (@mr_hugo/pacer); the producers map and the
|
|
487
|
+
* versions table speak producer names (pacer) — the remedy for a
|
|
488
|
+
* missing artifact must name the version, so map back before lookup. */
|
|
489
|
+
function producerOf(packageName) {
|
|
490
|
+
if (packageName === PACER_PACKAGE || packageName.startsWith(`${PACER_PACKAGE}-`)) return 'pacer';
|
|
491
|
+
return packageName;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function runtimeFile(relative) {
|
|
495
|
+
const row = runtimeRows.get(relative);
|
|
496
|
+
if (row === undefined) {
|
|
497
|
+
throw toolchainError(
|
|
498
|
+
'toolchain_artifact_missing',
|
|
499
|
+
`the installed runtime has no ${relative}`,
|
|
500
|
+
'Reinstall animader-cli; the release manifest does not describe this runtime file.',
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const packageName = row.package ?? row.spec.split('/').slice(0, row.spec.startsWith('@') ? 2 : 1).join('/');
|
|
504
|
+
const producer = producerOf(packageName);
|
|
505
|
+
const version = manifest.producers?.[producer]?.version ?? versions[producer] ?? '*';
|
|
506
|
+
return requireInstalledFile(row.spec, packageName, version, relative, row.file);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const pnginePlatform = PNGINE_PLATFORM_PACKAGES[platform];
|
|
510
|
+
const pacerPlatform = PACER_PLATFORM_PACKAGES[platform];
|
|
511
|
+
if (pnginePlatform === undefined || pacerPlatform === undefined) {
|
|
512
|
+
throw toolchainError(
|
|
513
|
+
'toolchain_platform_unsupported',
|
|
514
|
+
`no platform package is declared for ${platform}`,
|
|
515
|
+
`This animader-cli build cannot run on ${platform}; check the release matrix for tested targets.`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const descriptor = {
|
|
520
|
+
mode: 'installed',
|
|
521
|
+
identity: `installed toolchain ${manifest.toolchainVersion} for ${platform}`
|
|
522
|
+
+ ` (animader ${versions.animader}, pngine ${versions.pngine}, pacer ${versions.pacer}, esbuild ${versions.esbuild})`,
|
|
523
|
+
target: { ...manifest.target },
|
|
524
|
+
manifest,
|
|
525
|
+
runtime: {
|
|
526
|
+
kind: 'map',
|
|
527
|
+
/** Sync path answer over the manifest rows; a missing package or
|
|
528
|
+
* file throws the named toolchain_artifact_missing error. */
|
|
529
|
+
path(relative) {
|
|
530
|
+
const row = runtimeRows.get(relative);
|
|
531
|
+
if (row === undefined) {
|
|
532
|
+
throw toolchainError(
|
|
533
|
+
'toolchain_artifact_missing',
|
|
534
|
+
`the installed runtime has no ${relative}`,
|
|
535
|
+
'Reinstall animader-cli; the release manifest does not describe this runtime file.',
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (row.file !== undefined) {
|
|
539
|
+
const packageRoot = packageRootOf(row.spec);
|
|
540
|
+
if (packageRoot === null) {
|
|
541
|
+
throw toolchainError(
|
|
542
|
+
'toolchain_artifact_missing',
|
|
543
|
+
`the installed runtime's ${relative} has no package (${row.spec}) installed`,
|
|
544
|
+
`Reinstall animader-cli; the ${row.spec} package carries this runtime file.`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return resolve(packageRoot, row.file);
|
|
548
|
+
}
|
|
549
|
+
return resolveSpec(row.spec, row.package ?? row.spec, '*', relative);
|
|
550
|
+
},
|
|
551
|
+
async requireFile(relative) { return runtimeFile(relative); },
|
|
552
|
+
async files() {
|
|
553
|
+
const files = [];
|
|
554
|
+
for (const row of manifest.runtime) {
|
|
555
|
+
files.push({ path: row.path, source: await runtimeFile(row.path) });
|
|
556
|
+
}
|
|
557
|
+
return files;
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
pngine: {
|
|
561
|
+
cli: await platformBinary(pnginePlatform, versions.pngine, 'bin/pngine', 'pngine native CLI'),
|
|
562
|
+
viewer: await requireInstalledFile('pngine/viewer', 'pngine', versions.pngine, 'pngine viewer bundle'),
|
|
563
|
+
core: await requireInstalledFile('pngine/core', 'pngine', versions.pngine, 'pngine core reflection reader'),
|
|
564
|
+
uniformTypeName: await packageFile(pkgRoot, MANIFEST_RELATIVE_PATH.replace('manifest.json', 'uniform-type-name.js'), 'the packaged uniformTypeName helper'),
|
|
565
|
+
},
|
|
566
|
+
pacer: {
|
|
567
|
+
cli: await platformBinary(pacerPlatform, versions.pacer, 'bin/pacer', 'pacer native pack CLI'),
|
|
568
|
+
index: await requireInstalledFile(PACER_PACKAGE, PACER_PACKAGE, versions.pacer, 'pacer compile driver'),
|
|
569
|
+
route: await requireInstalledFile(`${PACER_PACKAGE}/route`, PACER_PACKAGE, versions.pacer, 'pacer route writer'),
|
|
570
|
+
rt: await requireInstalledFile(`${PACER_PACKAGE}/rt`, PACER_PACKAGE, versions.pacer, 'pacer playback runtime'),
|
|
571
|
+
pacerWasm: await requireInstalledFile(`${PACER_PACKAGE}/pacer.wasm`, PACER_PACKAGE, versions.pacer, 'pacer compile wasm'),
|
|
572
|
+
pacerRtWasm: await requireInstalledFile(`${PACER_PACKAGE}/pacer-rt.wasm`, PACER_PACKAGE, versions.pacer, 'pacer playback wasm'),
|
|
573
|
+
},
|
|
574
|
+
animader: {
|
|
575
|
+
wasm: await requireInstalledFile('animader/wasm', 'animader', versions.animader, 'animader host wasm'),
|
|
576
|
+
piece: await requireInstalledFile('animader/piece', 'animader', versions.animader, 'animader piece host'),
|
|
577
|
+
},
|
|
578
|
+
versions,
|
|
579
|
+
esbuild: {
|
|
580
|
+
require() {
|
|
581
|
+
try {
|
|
582
|
+
return requireFromPackage('esbuild');
|
|
583
|
+
} catch (cause) {
|
|
584
|
+
throw toolchainError(
|
|
585
|
+
'toolchain_artifact_missing',
|
|
586
|
+
'the pinned esbuild is not installed',
|
|
587
|
+
missingRemedy('esbuild', versions.esbuild ?? '*', 'release bundler'),
|
|
588
|
+
{ cause },
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
async verifyRuntime() {
|
|
594
|
+
const rows = [];
|
|
595
|
+
for (const row of manifest.runtime) {
|
|
596
|
+
const path = await runtimeFile(row.path);
|
|
597
|
+
const bytes = await readFile(path);
|
|
598
|
+
if (bytes.byteLength !== row.bytes || sha256(bytes) !== row.sha256) {
|
|
599
|
+
throw toolchainError(
|
|
600
|
+
'toolchain_content_drift',
|
|
601
|
+
`the installed ${row.path} does not match the release manifest (${sha256(bytes)} vs ${row.sha256})`,
|
|
602
|
+
'Reinstall animader-cli; installed artifacts must match the tested release manifest byte for byte.',
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
rows.push({ path: row.path, source: row.spec, sha256: row.sha256, bytes: row.bytes });
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
format: VENDOR_FORMAT,
|
|
609
|
+
files: rows,
|
|
610
|
+
digest: manifest.digest,
|
|
611
|
+
provenance: {
|
|
612
|
+
format: VENDOR_FORMAT,
|
|
613
|
+
producers: manifest.producers,
|
|
614
|
+
files: rows,
|
|
615
|
+
digest: manifest.digest,
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
async verifyTools() {
|
|
620
|
+
const rows = [];
|
|
621
|
+
for (const entry of Object.values(manifest.tools)) {
|
|
622
|
+
const path = await platformBinary(entry.package, entry.version ?? '*', entry.file, entry.artifact ?? 'native tool');
|
|
623
|
+
const bytes = await readFile(path);
|
|
624
|
+
if (entry.sha256 !== undefined && (bytes.byteLength !== entry.bytes || sha256(bytes) !== entry.sha256)) {
|
|
625
|
+
throw toolchainError(
|
|
626
|
+
'toolchain_content_drift',
|
|
627
|
+
`the installed ${entry.package} ${entry.file} does not match the release manifest`,
|
|
628
|
+
'Reinstall animader-cli; installed native tools must match the tested release manifest byte for byte.',
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
rows.push({ name: entry.package, path, bytes: bytes.byteLength, sha256: sha256(bytes) });
|
|
632
|
+
}
|
|
633
|
+
return { tools: rows };
|
|
634
|
+
},
|
|
635
|
+
async licenseText() {
|
|
636
|
+
return readFile(join(pkgRoot, 'toolchain', 'licenses', 'animader.txt'), 'utf8');
|
|
637
|
+
},
|
|
638
|
+
async producerVersion(name) {
|
|
639
|
+
return versions[name] ?? null;
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
return descriptor;
|
|
643
|
+
|
|
644
|
+
async function platformBinary(packageName, version, file, artifact) {
|
|
645
|
+
let packageJsonPath;
|
|
646
|
+
try {
|
|
647
|
+
packageJsonPath = requireFromPackage.resolve(`${packageName}/package.json`);
|
|
648
|
+
} catch (cause) {
|
|
649
|
+
throw toolchainError(
|
|
650
|
+
'toolchain_artifact_missing',
|
|
651
|
+
`the platform package ${packageName} is not installed, so the ${artifact} is missing`,
|
|
652
|
+
`Install ${packageName}@${version} (reinstall animader-cli so npm's platform selection pulls it), or use a target it ships.`,
|
|
653
|
+
{ cause, artifact },
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const binary = join(dirname(packageJsonPath), file);
|
|
657
|
+
const info = await stat(binary).catch(() => null);
|
|
658
|
+
if (info === null || !info.isFile()) {
|
|
659
|
+
throw toolchainError(
|
|
660
|
+
'toolchain_artifact_missing',
|
|
661
|
+
`the platform package ${packageName} is installed but carries no ${file}`,
|
|
662
|
+
`Reinstall ${packageName}@${version}; its packed binary is missing or stripped.`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return binary;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function packageFile(root, relative, what) {
|
|
669
|
+
const path = join(root, relative);
|
|
670
|
+
const info = await stat(path).catch(() => null);
|
|
671
|
+
if (info === null || !info.isFile()) {
|
|
672
|
+
throw toolchainError(
|
|
673
|
+
'toolchain_artifact_missing',
|
|
674
|
+
`${what} is missing from this installation: ${relative}`,
|
|
675
|
+
'Reinstall animader-cli; the release assembly ships this file inside the package.',
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return path;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* The identity line `animader --version` prints. Contributor mode keeps its
|
|
684
|
+
* honest label; installed mode names the manifest's tested combination.
|
|
685
|
+
*
|
|
686
|
+
* @param {{env?: object, pkgRoot?: string}} [options]
|
|
687
|
+
*/
|
|
688
|
+
export async function toolchainIdentity({ env = process.env, pkgRoot = PKG_ROOT } = {}) {
|
|
689
|
+
const toolchain = await resolveToolchain({ env, pkgRoot });
|
|
690
|
+
return toolchain.identity;
|
|
691
|
+
}
|