@aelionsdk/material-sdk 0.1.0-beta.1 → 1.1.0-rc.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/README.md CHANGED
@@ -1,7 +1,54 @@
1
- # @aelionsdk/material-sdk
1
+ # `@aelionsdk/material-sdk`
2
2
 
3
- Type-safe authoring, validation and packaging tools for Aelion materials
3
+ Type-safe authoring, validation, packaging, signing and trust tools for Aelion
4
+ Material Protocol v1.
4
5
 
5
- Install with `npm install @aelionsdk/material-sdk@next`.
6
+ ## Install
6
7
 
7
- Version 0.1.0-beta.1 is a prerelease and its API may change before the first stable release. This package is part of [AelionSDK](https://github.com/FoyonaCZY/AelionSDK); see the repository README for supported browsers, examples and deployment requirements.
8
+ ```bash
9
+ npm install @aelionsdk/material-sdk@next
10
+ ```
11
+
12
+ ## Author CLI
13
+
14
+ The package installs `aelion-material`:
15
+
16
+ ```bash
17
+ pnpm exec aelion-material init ./my-pack
18
+ pnpm exec aelion-material build ./my-pack
19
+ pnpm exec aelion-material validate ./my-pack
20
+ pnpm exec aelion-material types ./my-pack
21
+ pnpm exec aelion-material preview ./my-pack
22
+ pnpm exec aelion-material prepublish ./my-pack
23
+ pnpm exec aelion-material pack ./my-pack --out ./dist/my-pack.aelionmat
24
+ pnpm exec aelion-material golden actual.rgba expected.rgba --tolerance 2
25
+ ```
26
+
27
+ `build` refreshes declared payload sizes/hashes and generates types plus the
28
+ preview report. `prepublish` requires valid payload hashes and schemas, no error diagnostics,
29
+ strict determinism, and WebGL2/WebGPU compiler parity for declarative Graph
30
+ materials. The `.aelionmat` archive is deterministic.
31
+
32
+ Signing keys are deliberately not read by the generic CLI. A host that owns a
33
+ publisher key should obtain a `CryptoKey` from its own key system and call
34
+ `signMaterialPackage()` explicitly.
35
+
36
+ Node build systems can import the same operations from
37
+ `@aelionsdk/material-sdk/author-cli`.
38
+
39
+ ## TypeScript API
40
+
41
+ ```ts
42
+ import {
43
+ MaterialLabSession,
44
+ materialDefinition,
45
+ materialGraph,
46
+ packMaterialPackage,
47
+ } from '@aelionsdk/material-sdk';
48
+ ```
49
+
50
+ See the [Material guide](https://foyonaczy.github.io/AelionSDK/guides/materials/)
51
+ and [AMP v1 reference](https://foyonaczy.github.io/AelionSDK/reference/material-protocol-v1/).
52
+
53
+ Version `1.1.0-rc.1` is a prerelease. Public changes before 1.0 are recorded
54
+ in the repository changelog and migration documentation. Licensed under MIT.
@@ -0,0 +1,34 @@
1
+ import { MaterialLabSession, compareMaterialGolden } from './lab.js';
2
+ export interface MaterialAuthorInspection {
3
+ readonly directory: string;
4
+ readonly packageId: string;
5
+ readonly version: string;
6
+ readonly integrity: string;
7
+ readonly files: number;
8
+ readonly materials: readonly {
9
+ readonly id: string;
10
+ readonly kind: string;
11
+ readonly parameters: number;
12
+ readonly webgl2: boolean;
13
+ readonly webgpu: boolean;
14
+ readonly budget: ReturnType<MaterialLabSession['analyze']>['budget'];
15
+ }[];
16
+ }
17
+ export declare function validateMaterialAuthorPackage(directory: string): Promise<MaterialAuthorInspection>;
18
+ export declare function synchronizeMaterialAuthorManifest(directory: string): Promise<string>;
19
+ export declare function generateMaterialTypes(directory: string, outputPath?: string): Promise<string>;
20
+ export declare function writeMaterialPreviewReport(directory: string, outputPath?: string): Promise<string>;
21
+ export declare function packMaterialAuthorPackage(directory: string, outputPath: string): Promise<{
22
+ readonly path: string;
23
+ readonly integrity: string;
24
+ readonly bytes: number;
25
+ }>;
26
+ export declare function buildMaterialAuthorPackage(directory: string): Promise<{
27
+ readonly inspection: MaterialAuthorInspection;
28
+ readonly typesPath: string;
29
+ readonly previewPath: string;
30
+ }>;
31
+ export declare function compareMaterialGoldenFiles(actualPath: string, expectedPath: string, tolerance?: number): Promise<ReturnType<typeof compareMaterialGolden>>;
32
+ export declare function prepublishMaterialAuthorPackage(directory: string): Promise<MaterialAuthorInspection>;
33
+ export declare function initializeMaterialAuthorPackage(directory: string): Promise<string>;
34
+ //# sourceMappingURL=cli-lib.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-lib.d.ts","sourceRoot":"","sources":["../src/cli-lib.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAmBrE,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS;QAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;KACtE,EAAE,CAAC;CACL;AAsID,wBAAsB,6BAA6B,CACjD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,wBAAwB,CAAC,CAEnC;AAED,wBAAsB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB1F;AAYD,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,SAAgD,GACzD,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAED,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,MAAM,EACjB,UAAU,SAA8C,GACvD,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAUxF;AAED,wBAAsB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3E,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,CAAC,CAQD;AAED,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,SAAS,SAAI,GACZ,OAAO,CAAC,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAGnD;AAED,wBAAsB,+BAA+B,CACnD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,wBAAsB,+BAA+B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA6HxF"}
@@ -0,0 +1,367 @@
1
+ import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, relative, resolve } from 'node:path';
3
+ import { canonicalMaterialBytes, sha256Hex } from './canonical.js';
4
+ import { MaterialLabSession, compareMaterialGolden } from './lab.js';
5
+ import { packMaterialPackage, verifyMaterialPackage } from './package.js';
6
+ import { assertMaterialDefinitionSchema, assertMaterialGraphSchema, assertMaterialManifestSchema, } from './schema-validation.js';
7
+ import { createDeterministicMaterialArchive } from './zip.js';
8
+ const decoder = new TextDecoder('utf-8', { fatal: true });
9
+ const encoder = new TextEncoder();
10
+ function safePath(root, path) {
11
+ const absolute = resolve(root, path);
12
+ const fromRoot = relative(root, absolute).replaceAll('\\', '/');
13
+ if (fromRoot.length === 0 ||
14
+ fromRoot === '..' ||
15
+ fromRoot.startsWith('../') ||
16
+ fromRoot.startsWith('/') ||
17
+ path.includes('\\')) {
18
+ throw new TypeError(`Unsafe Material path ${path}`);
19
+ }
20
+ return absolute;
21
+ }
22
+ function json(bytes, path) {
23
+ try {
24
+ return JSON.parse(decoder.decode(bytes));
25
+ }
26
+ catch (error) {
27
+ throw new TypeError(`Invalid JSON in ${path}`, { cause: error });
28
+ }
29
+ }
30
+ async function exists(path) {
31
+ try {
32
+ await access(path);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ async function loadAuthorPackage(directory) {
40
+ const root = resolve(directory);
41
+ const manifestPath = resolve(root, 'manifest.json');
42
+ const manifestSource = new Uint8Array(await readFile(manifestPath));
43
+ const manifestValue = json(manifestSource, manifestPath);
44
+ assertMaterialManifestSchema(manifestValue);
45
+ const manifest = manifestValue;
46
+ const manifestBytes = canonicalMaterialBytes(manifest);
47
+ const files = new Map([['manifest.json', manifestBytes]]);
48
+ for (const entry of manifest.files) {
49
+ const bytes = new Uint8Array(await readFile(safePath(root, entry.path)));
50
+ files.set(entry.path, bytes);
51
+ }
52
+ const archiveBytes = createDeterministicMaterialArchive(files);
53
+ const integrity = `sha256:${await sha256Hex(manifestBytes)}`;
54
+ const packed = {
55
+ manifest,
56
+ manifestBytes,
57
+ files,
58
+ archiveBytes,
59
+ integrity,
60
+ };
61
+ await verifyMaterialPackage(packed, integrity);
62
+ const definitionPaths = new Set(manifest.materials.map(material => material.definition));
63
+ const graphPaths = new Set();
64
+ const authored = manifest.materials.map(material => {
65
+ const definitionBytes = files.get(material.definition);
66
+ if (definitionBytes === undefined) {
67
+ throw new TypeError(`Definition ${material.definition} is missing`);
68
+ }
69
+ const definitionValue = json(definitionBytes, material.definition);
70
+ assertMaterialDefinitionSchema(definitionValue);
71
+ const definition = definitionValue;
72
+ if (definition.id !== material.id || definition.kind !== material.kind) {
73
+ throw new TypeError(`Definition identity differs for ${material.id}`);
74
+ }
75
+ const graphImplementation = definition.implementations.find(value => value.type === 'graph');
76
+ if (graphImplementation === undefined)
77
+ return { definition };
78
+ graphPaths.add(graphImplementation.graph);
79
+ const graphBytes = files.get(graphImplementation.graph);
80
+ if (graphBytes === undefined) {
81
+ throw new TypeError(`Graph ${graphImplementation.graph} is missing`);
82
+ }
83
+ const graphValue = json(graphBytes, graphImplementation.graph);
84
+ assertMaterialGraphSchema(graphValue);
85
+ return {
86
+ definition,
87
+ graph: graphValue,
88
+ definitionPath: material.definition,
89
+ graphPath: graphImplementation.graph,
90
+ };
91
+ });
92
+ const extraFiles = manifest.files.flatMap(entry => {
93
+ if (definitionPaths.has(entry.path) || graphPaths.has(entry.path))
94
+ return [];
95
+ const data = files.get(entry.path);
96
+ if (data === undefined)
97
+ throw new TypeError(`Package file ${entry.path} is missing`);
98
+ return [{ path: entry.path, mediaType: entry.mediaType, data }];
99
+ });
100
+ // Re-run the same authoring validation/compilation path used by package
101
+ // creation. The stored manifest verification above remains authoritative for
102
+ // exact payload bytes and transport integrity.
103
+ await packMaterialPackage({
104
+ metadata: manifest.package,
105
+ materials: authored,
106
+ files: extraFiles,
107
+ });
108
+ return { directory: root, manifest, packed, authored, extraFiles };
109
+ }
110
+ function inspection(loaded) {
111
+ return {
112
+ directory: loaded.directory,
113
+ packageId: loaded.manifest.package.id,
114
+ version: loaded.manifest.package.version,
115
+ integrity: loaded.packed.integrity,
116
+ files: loaded.manifest.files.length + 1,
117
+ materials: loaded.authored.map(material => {
118
+ const report = new MaterialLabSession(material).analyze();
119
+ return {
120
+ id: material.definition.id,
121
+ kind: material.definition.kind,
122
+ parameters: material.definition.parameters.length,
123
+ webgl2: report.webgl2.available,
124
+ webgpu: report.webgpu.available,
125
+ budget: report.budget,
126
+ };
127
+ }),
128
+ };
129
+ }
130
+ export async function validateMaterialAuthorPackage(directory) {
131
+ return inspection(await loadAuthorPackage(directory));
132
+ }
133
+ export async function synchronizeMaterialAuthorManifest(directory) {
134
+ const root = resolve(directory);
135
+ const manifestPath = resolve(root, 'manifest.json');
136
+ const manifestValue = json(new Uint8Array(await readFile(manifestPath)), manifestPath);
137
+ assertMaterialManifestSchema(manifestValue);
138
+ const manifest = manifestValue;
139
+ const files = await Promise.all(manifest.files.map(async (entry) => {
140
+ const bytes = new Uint8Array(await readFile(safePath(root, entry.path)));
141
+ return {
142
+ ...entry,
143
+ bytes: bytes.byteLength,
144
+ sha256: await sha256Hex(bytes),
145
+ };
146
+ }));
147
+ await writeFile(manifestPath, `${JSON.stringify({ ...manifest, files }, null, 2)}\n`, 'utf8');
148
+ return manifestPath;
149
+ }
150
+ function parameterType(type) {
151
+ if (type === 'boolean')
152
+ return 'boolean';
153
+ if (type === 'enum' || type === 'string')
154
+ return 'string';
155
+ if (['integer', 'float', 'angle', 'duration'].includes(type))
156
+ return 'number';
157
+ if (type === 'vec2')
158
+ return 'readonly [number, number]';
159
+ if (type === 'vec3')
160
+ return 'readonly [number, number, number]';
161
+ if (type === 'vec4' || type === 'color')
162
+ return 'readonly [number, number, number, number]';
163
+ return 'JsonValue';
164
+ }
165
+ export async function generateMaterialTypes(directory, outputPath = resolve(directory, 'material.generated.d.ts')) {
166
+ const loaded = await loadAuthorPackage(directory);
167
+ const needsJsonValue = loaded.authored.some(material => material.definition.parameters.some(parameter => ['gradient', 'curve'].includes(parameter.type)));
168
+ const sections = loaded.authored.map(material => {
169
+ const name = `${material.definition.id
170
+ .split(/[^A-Za-z0-9]+/u)
171
+ .filter(Boolean)
172
+ .map(part => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
173
+ .join('')}Parameters`;
174
+ const members = material.definition.parameters.map(parameter => ` readonly ${JSON.stringify(parameter.id)}: ${parameterType(parameter.type)};`);
175
+ return `export interface ${name} {\n${members.join('\n')}\n}`;
176
+ });
177
+ const source = `${needsJsonValue ? "import type { JsonValue } from '@aelionsdk/core';\n\n" : ''}${sections.join('\n\n')}\n`;
178
+ await mkdir(dirname(resolve(outputPath)), { recursive: true });
179
+ await writeFile(resolve(outputPath), source, 'utf8');
180
+ return resolve(outputPath);
181
+ }
182
+ export async function writeMaterialPreviewReport(directory, outputPath = resolve(directory, 'material-preview.json')) {
183
+ const loaded = await loadAuthorPackage(directory);
184
+ const report = {
185
+ reportVersion: '1.0.0',
186
+ package: {
187
+ id: loaded.manifest.package.id,
188
+ version: loaded.manifest.package.version,
189
+ integrity: loaded.packed.integrity,
190
+ },
191
+ materials: loaded.authored.map(material => ({
192
+ id: material.definition.id,
193
+ display: material.definition.display,
194
+ execution: material.definition.execution,
195
+ analysis: new MaterialLabSession(material).analyze(),
196
+ })),
197
+ };
198
+ await mkdir(dirname(resolve(outputPath)), { recursive: true });
199
+ await writeFile(resolve(outputPath), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
200
+ return resolve(outputPath);
201
+ }
202
+ export async function packMaterialAuthorPackage(directory, outputPath) {
203
+ await synchronizeMaterialAuthorManifest(directory);
204
+ const loaded = await loadAuthorPackage(directory);
205
+ await mkdir(dirname(resolve(outputPath)), { recursive: true });
206
+ await writeFile(resolve(outputPath), loaded.packed.archiveBytes);
207
+ return {
208
+ path: resolve(outputPath),
209
+ integrity: loaded.packed.integrity,
210
+ bytes: loaded.packed.archiveBytes.byteLength,
211
+ };
212
+ }
213
+ export async function buildMaterialAuthorPackage(directory) {
214
+ await synchronizeMaterialAuthorManifest(directory);
215
+ const inspection = await validateMaterialAuthorPackage(directory);
216
+ const [typesPath, previewPath] = await Promise.all([
217
+ generateMaterialTypes(directory),
218
+ writeMaterialPreviewReport(directory),
219
+ ]);
220
+ return { inspection, typesPath, previewPath };
221
+ }
222
+ export async function compareMaterialGoldenFiles(actualPath, expectedPath, tolerance = 2) {
223
+ const [actual, expected] = await Promise.all([readFile(actualPath), readFile(expectedPath)]);
224
+ return compareMaterialGolden(actual, expected, tolerance);
225
+ }
226
+ export async function prepublishMaterialAuthorPackage(directory) {
227
+ const loaded = await loadAuthorPackage(directory);
228
+ for (const material of loaded.authored) {
229
+ const report = new MaterialLabSession(material).analyze();
230
+ if (report.diagnostics.some(value => value.severity === 'error')) {
231
+ throw new TypeError(`Material ${material.definition.id} has error diagnostics`);
232
+ }
233
+ if (material.graph !== undefined && (!report.webgl2.available || !report.webgpu.available)) {
234
+ throw new TypeError(`Material ${material.definition.id} lacks WebGL2/WebGPU compiler parity`);
235
+ }
236
+ if (material.definition.execution.determinism === 'non-deterministic') {
237
+ throw new TypeError(`Material ${material.definition.id} is non-deterministic`);
238
+ }
239
+ }
240
+ return inspection(loaded);
241
+ }
242
+ export async function initializeMaterialAuthorPackage(directory) {
243
+ const root = resolve(directory);
244
+ if ((await exists(root)) && (await readdir(root)).length > 0) {
245
+ throw new TypeError(`Material directory is not empty: ${root}`);
246
+ }
247
+ const definition = {
248
+ $schema: 'https://schemas.aelion.dev/material/definition/v1.json',
249
+ protocolVersion: '1.0.0',
250
+ id: 'starter-filter',
251
+ kind: 'visual-filter',
252
+ display: { name: 'Starter Filter', category: 'starter' },
253
+ scopes: ['source', 'item', 'track', 'sequence'],
254
+ ports: [
255
+ {
256
+ id: 'source',
257
+ direction: 'input',
258
+ type: 'visual-frame',
259
+ role: 'source',
260
+ binding: 'host',
261
+ required: true,
262
+ },
263
+ {
264
+ id: 'result',
265
+ direction: 'output',
266
+ type: 'visual-frame',
267
+ role: 'result',
268
+ binding: 'host',
269
+ required: true,
270
+ },
271
+ ],
272
+ parameters: [
273
+ {
274
+ id: 'intensity',
275
+ type: 'float',
276
+ default: 1,
277
+ range: { min: 0, max: 1, step: 0.01 },
278
+ unit: 'ratio',
279
+ animatable: true,
280
+ interpolation: 'linear',
281
+ affects: 'uniform',
282
+ ui: { control: 'slider', group: 'main', order: 0, label: 'Intensity' },
283
+ },
284
+ ],
285
+ bundledResources: [],
286
+ resourceSlots: [],
287
+ execution: {
288
+ color: { input: 'working-linear', output: 'working-linear' },
289
+ alpha: {
290
+ input: 'premultiplied',
291
+ output: 'premultiplied',
292
+ preservesTransparency: true,
293
+ },
294
+ resolution: { policy: 'same-as-host' },
295
+ spatialPadding: { mode: 'none' },
296
+ temporal: { pastUs: 0, futureUs: 0, stateful: false, seekPolicy: 'stateless' },
297
+ determinism: 'strict',
298
+ supports: { realtime: true, offline: true, alpha: true, hdr: false, tiled: true },
299
+ },
300
+ implementations: [
301
+ {
302
+ type: 'graph',
303
+ graph: 'graphs/starter-filter.graph.json',
304
+ nodeSet: 'aelion.visual.nodes/1.0.0',
305
+ },
306
+ ],
307
+ splitPolicy: 'copy',
308
+ };
309
+ const graph = {
310
+ $schema: 'https://schemas.aelion.dev/material/graph/v1.json',
311
+ graphVersion: '1.0.0',
312
+ nodeSet: 'aelion.visual.nodes/1.0.0',
313
+ nodes: [
314
+ {
315
+ id: 'mix',
316
+ type: 'composite.mix',
317
+ typeVersion: '1.0.0',
318
+ inputs: {
319
+ a: { inputPort: 'source' },
320
+ b: { inputPort: 'source' },
321
+ amount: { parameter: 'intensity' },
322
+ },
323
+ },
324
+ ],
325
+ outputs: { result: { node: 'mix', output: 'frame' } },
326
+ };
327
+ const definitionPath = 'materials/starter-filter.material.json';
328
+ const graphPath = 'graphs/starter-filter.graph.json';
329
+ const definitionSource = `${JSON.stringify(definition, null, 2)}\n`;
330
+ const graphSource = `${JSON.stringify(graph, null, 2)}\n`;
331
+ const manifest = {
332
+ $schema: 'https://schemas.aelion.dev/material/package/v1.json',
333
+ protocolVersion: '1.0.0',
334
+ package: {
335
+ id: 'dev.example.starter',
336
+ version: '0.1.0',
337
+ displayName: 'Starter Material',
338
+ publisher: { id: 'dev.example', name: 'Example Publisher' },
339
+ license: 'MIT',
340
+ engines: { aelion: '>=1.0.0-rc.1 <2.0.0', nodeSet: 'aelion.visual.nodes/1.0.0' },
341
+ trust: 'declarative',
342
+ },
343
+ materials: [{ id: definition.id, kind: definition.kind, definition: definitionPath }],
344
+ files: [
345
+ {
346
+ path: graphPath,
347
+ mediaType: 'application/vnd.aelion.material-graph+json',
348
+ bytes: encoder.encode(graphSource).byteLength,
349
+ sha256: await sha256Hex(encoder.encode(graphSource)),
350
+ },
351
+ {
352
+ path: definitionPath,
353
+ mediaType: 'application/vnd.aelion.material+json',
354
+ bytes: encoder.encode(definitionSource).byteLength,
355
+ sha256: await sha256Hex(encoder.encode(definitionSource)),
356
+ },
357
+ ],
358
+ };
359
+ await mkdir(resolve(root, 'materials'), { recursive: true });
360
+ await mkdir(resolve(root, 'graphs'), { recursive: true });
361
+ await writeFile(resolve(root, definitionPath), definitionSource, 'utf8');
362
+ await writeFile(resolve(root, graphPath), graphSource, 'utf8');
363
+ await writeFile(resolve(root, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
364
+ await validateMaterialAuthorPackage(root);
365
+ await generateMaterialTypes(root);
366
+ return root;
367
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { buildMaterialAuthorPackage, compareMaterialGoldenFiles, generateMaterialTypes, initializeMaterialAuthorPackage, packMaterialAuthorPackage, prepublishMaterialAuthorPackage, validateMaterialAuthorPackage, writeMaterialPreviewReport, } from './cli-lib.js';
4
+ function option(name) {
5
+ const index = process.argv.indexOf(name);
6
+ return index < 0 ? undefined : process.argv[index + 1];
7
+ }
8
+ function required(value, description) {
9
+ if (value === undefined || value.length === 0)
10
+ throw new TypeError(`${description} is required`);
11
+ return value;
12
+ }
13
+ async function main() {
14
+ const command = process.argv[2];
15
+ const target = process.argv[3];
16
+ let result;
17
+ if (command === 'init') {
18
+ result = { directory: await initializeMaterialAuthorPackage(required(target, 'directory')) };
19
+ }
20
+ else if (command === 'build') {
21
+ result = await buildMaterialAuthorPackage(required(target, 'directory'));
22
+ }
23
+ else if (command === 'validate' || command === 'inspect') {
24
+ result = await validateMaterialAuthorPackage(required(target, 'directory'));
25
+ }
26
+ else if (command === 'types') {
27
+ result = {
28
+ path: await generateMaterialTypes(required(target, 'directory'), option('--out') ?? resolve(required(target, 'directory'), 'material.generated.d.ts')),
29
+ };
30
+ }
31
+ else if (command === 'preview') {
32
+ result = {
33
+ path: await writeMaterialPreviewReport(required(target, 'directory'), option('--out') ?? resolve(required(target, 'directory'), 'material-preview.json')),
34
+ };
35
+ }
36
+ else if (command === 'pack') {
37
+ result = await packMaterialAuthorPackage(required(target, 'directory'), required(option('--out'), '--out'));
38
+ }
39
+ else if (command === 'golden') {
40
+ const tolerance = Number.parseInt(option('--tolerance') ?? '2', 10);
41
+ const comparison = await compareMaterialGoldenFiles(required(target, 'actual file'), required(process.argv[4], 'expected file'), tolerance);
42
+ result = comparison;
43
+ if (!comparison.passed)
44
+ process.exitCode = 1;
45
+ }
46
+ else if (command === 'prepublish') {
47
+ result = await prepublishMaterialAuthorPackage(required(target, 'directory'));
48
+ }
49
+ else {
50
+ throw new TypeError('Usage: aelion-material <init|build|validate|inspect|types|preview|pack|golden|prepublish> ...');
51
+ }
52
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
53
+ }
54
+ void main().catch((error) => {
55
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
56
+ process.exitCode = 1;
57
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aelionsdk/material-sdk",
3
- "version": "0.1.0-beta.1",
3
+ "version": "1.1.0-rc.1",
4
4
  "description": "Type-safe authoring, validation and packaging tools for Aelion materials",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,27 +20,35 @@
20
20
  ".": {
21
21
  "types": "./dist/index.d.ts",
22
22
  "import": "./dist/index.js"
23
+ },
24
+ "./author-cli": {
25
+ "types": "./dist/cli-lib.d.ts",
26
+ "import": "./dist/cli-lib.js"
23
27
  }
24
28
  },
29
+ "bin": {
30
+ "aelion-material": "./dist/cli.js"
31
+ },
25
32
  "files": [
26
33
  "dist",
27
34
  "!dist/.tsbuildinfo"
28
35
  ],
29
36
  "engines": {
30
- "node": ">=20.19"
37
+ "node": ">=24 <25"
31
38
  },
32
39
  "publishConfig": {
33
40
  "access": "public",
34
41
  "provenance": true
35
42
  },
36
43
  "dependencies": {
37
- "@aelionsdk/core": "0.1.0-beta.1",
38
- "@aelionsdk/material-compiler": "0.1.0-beta.1",
39
- "ajv": "8.17.1",
44
+ "@aelionsdk/core": "1.1.0-rc.1",
45
+ "@aelionsdk/material-compiler": "1.1.0-rc.1",
46
+ "ajv": "8.18.0",
40
47
  "ajv-formats": "3.0.1"
41
48
  },
42
49
  "scripts": {
43
50
  "build": "tsc -b",
51
+ "cli": "corepack pnpm run build && node dist/cli.js",
44
52
  "typecheck": "tsc -b --pretty false"
45
53
  }
46
54
  }