@kitsra/kavio-cli 0.1.0
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 +94 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1037 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Elastic License 2.0
|
|
2
|
+
|
|
3
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
4
|
+
|
|
5
|
+
Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
13
|
+
available, and prepare derivative works of the software, in each case subject to
|
|
14
|
+
the limitations and conditions below.
|
|
15
|
+
|
|
16
|
+
Limitations
|
|
17
|
+
|
|
18
|
+
You may not provide the software to third parties as a hosted or managed
|
|
19
|
+
service, where the service provides users with access to any substantial set of
|
|
20
|
+
the features or functionality of the software.
|
|
21
|
+
|
|
22
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
23
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
24
|
+
software that is protected by the license key.
|
|
25
|
+
|
|
26
|
+
You may not alter, remove, or obscure any licensing, copyright, or other
|
|
27
|
+
notices of the licensor in the software. Any use of the licensor's trademarks
|
|
28
|
+
is subject to applicable law.
|
|
29
|
+
|
|
30
|
+
Patents
|
|
31
|
+
|
|
32
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
33
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
34
|
+
sale, import and have imported the software, in each case subject to the
|
|
35
|
+
limitations and conditions in this license. This license does not cover any
|
|
36
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
37
|
+
the software.
|
|
38
|
+
|
|
39
|
+
If you or your company make any written claim that the software infringes or
|
|
40
|
+
contributes to infringement of any patent, your patent license for the software
|
|
41
|
+
granted under these terms ends immediately. If your company makes such a claim,
|
|
42
|
+
your patent license ends immediately for work on behalf of your company.
|
|
43
|
+
|
|
44
|
+
Notices
|
|
45
|
+
|
|
46
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
47
|
+
you also gets a copy of these terms.
|
|
48
|
+
|
|
49
|
+
If you modify the software, you must include in any modified copies of the
|
|
50
|
+
software prominent notices stating that you have modified the software.
|
|
51
|
+
|
|
52
|
+
No Other Rights
|
|
53
|
+
|
|
54
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
55
|
+
these terms.
|
|
56
|
+
|
|
57
|
+
Termination
|
|
58
|
+
|
|
59
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
60
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
61
|
+
with a notice of your violation, and you cease all violation of this license no
|
|
62
|
+
later than 30 days after you receive that notice, your licenses will be
|
|
63
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
64
|
+
reinstatement, any additional violation of these terms will cause your licenses
|
|
65
|
+
to terminate automatically and permanently.
|
|
66
|
+
|
|
67
|
+
No Liability
|
|
68
|
+
|
|
69
|
+
As far as the law allows, the software comes as is, without any warranty or
|
|
70
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
71
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
72
|
+
legal claim.
|
|
73
|
+
|
|
74
|
+
Definitions
|
|
75
|
+
|
|
76
|
+
The licensor is the entity offering these terms, and the software is the
|
|
77
|
+
software the licensor makes available under these terms, including any portion
|
|
78
|
+
of it.
|
|
79
|
+
|
|
80
|
+
you refers to the individual or entity agreeing to these terms.
|
|
81
|
+
|
|
82
|
+
your company is any legal entity, sole proprietorship, or other kind of
|
|
83
|
+
organization that you work for, plus all organizations that have control over,
|
|
84
|
+
are under the control of, or are under common control with that organization.
|
|
85
|
+
control means ownership of substantially all the assets of an entity, or the
|
|
86
|
+
power to direct its management and policies by vote, contract, or otherwise.
|
|
87
|
+
Control can be direct or indirect.
|
|
88
|
+
|
|
89
|
+
your licenses are all the licenses granted to you for the software under these
|
|
90
|
+
terms.
|
|
91
|
+
|
|
92
|
+
use means anything you do with the software requiring one of your licenses.
|
|
93
|
+
|
|
94
|
+
trademark means trademarks, service marks, and similar rights.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { socialMediaPresets } from "@kitsra/kavio-builder";
|
|
6
|
+
import { compileTransitionOverlapWindows } from "@kitsra/kavio-core";
|
|
7
|
+
import { schemaVersion, validateComposition } from "@kitsra/kavio-schema";
|
|
8
|
+
import { renderBatch } from "@kitsra/kavio-render";
|
|
9
|
+
const commands = new Set(["validate", "inspect", "migrate", "preview", "render", "presets"]);
|
|
10
|
+
const VALUE_FLAGS = new Set(["--export", "--props", "--batch", "--out", "--concurrency"]);
|
|
11
|
+
async function main(argv) {
|
|
12
|
+
const parsed = parseArgs(argv);
|
|
13
|
+
if ("errors" in parsed) {
|
|
14
|
+
emitFailure(parsed, parsed.command === "unknown" ? argv.includes("--json") : false);
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
if (parsed.command === "help") {
|
|
18
|
+
writeStdout(helpText());
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
if (parsed.command === "presets") {
|
|
22
|
+
return runPresets(parsed.file, parsed.json);
|
|
23
|
+
}
|
|
24
|
+
if (parsed.file === undefined) {
|
|
25
|
+
emitFailure({
|
|
26
|
+
command: parsed.command,
|
|
27
|
+
ok: false,
|
|
28
|
+
errors: [
|
|
29
|
+
cliError("CLI_FILE_REQUIRED", "", `Missing file argument for kavio ${parsed.command}.`, `Usage: kavio ${parsed.command} <file>`)
|
|
30
|
+
]
|
|
31
|
+
}, parsed.json);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
switch (parsed.command) {
|
|
35
|
+
case "validate":
|
|
36
|
+
return await runValidate(parsed.file, parsed.json);
|
|
37
|
+
case "inspect":
|
|
38
|
+
return await runInspect(parsed.file, parsed.json);
|
|
39
|
+
case "migrate":
|
|
40
|
+
return await runMigrate(parsed.file, parsed.json);
|
|
41
|
+
case "preview":
|
|
42
|
+
return await runPreview(parsed.file, parsed.json);
|
|
43
|
+
case "render":
|
|
44
|
+
return await runRender(parsed);
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
function runPresets(name, json) {
|
|
49
|
+
if (name !== undefined) {
|
|
50
|
+
const preset = findSocialMediaPreset(name);
|
|
51
|
+
if (preset === undefined) {
|
|
52
|
+
emitFailure({
|
|
53
|
+
command: "presets",
|
|
54
|
+
ok: false,
|
|
55
|
+
errors: [
|
|
56
|
+
cliError("CLI_UNKNOWN_PRESET", "", `Unknown social media preset: ${name}`, `Run kavio presets to list supported preset ids.`)
|
|
57
|
+
]
|
|
58
|
+
}, json);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
if (json) {
|
|
62
|
+
const output = { command: "presets", ok: true, preset };
|
|
63
|
+
writeJson(output);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
writeJson(preset.preset);
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
if (json) {
|
|
71
|
+
const output = { command: "presets", ok: true, presets: socialMediaPresets };
|
|
72
|
+
writeJson(output);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
writeSocialMediaPresets(socialMediaPresets);
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
function parseArgs(argv) {
|
|
80
|
+
let json = false;
|
|
81
|
+
let help = false;
|
|
82
|
+
let allExports = false;
|
|
83
|
+
let failFast = false;
|
|
84
|
+
let continueOnFrameError = false;
|
|
85
|
+
let exportName;
|
|
86
|
+
let propsFile;
|
|
87
|
+
let batchFile;
|
|
88
|
+
let outDir;
|
|
89
|
+
let concurrency;
|
|
90
|
+
const positional = [];
|
|
91
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
92
|
+
const arg = argv[index];
|
|
93
|
+
if (arg === undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (arg === "--json") {
|
|
97
|
+
json = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === "--help" || arg === "-h") {
|
|
101
|
+
help = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (arg === "--all-exports") {
|
|
105
|
+
allExports = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg === "--fail-fast") {
|
|
109
|
+
failFast = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg === "--continue-on-frame-error") {
|
|
113
|
+
continueOnFrameError = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
117
|
+
const value = argv[index + 1];
|
|
118
|
+
if (value === undefined || value.startsWith("-")) {
|
|
119
|
+
return flagFailure(`Option ${arg} requires a value.`);
|
|
120
|
+
}
|
|
121
|
+
index += 1;
|
|
122
|
+
if (arg === "--export") {
|
|
123
|
+
exportName = value;
|
|
124
|
+
}
|
|
125
|
+
else if (arg === "--props") {
|
|
126
|
+
propsFile = value;
|
|
127
|
+
}
|
|
128
|
+
else if (arg === "--batch") {
|
|
129
|
+
batchFile = value;
|
|
130
|
+
}
|
|
131
|
+
else if (arg === "--out") {
|
|
132
|
+
outDir = value;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const parsed = Number(value);
|
|
136
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
137
|
+
return flagFailure("--concurrency must be a positive integer.");
|
|
138
|
+
}
|
|
139
|
+
concurrency = parsed;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (arg.startsWith("-")) {
|
|
144
|
+
return flagFailure(`Unknown option: ${arg}`);
|
|
145
|
+
}
|
|
146
|
+
positional.push(arg);
|
|
147
|
+
}
|
|
148
|
+
if (help || positional.length === 0) {
|
|
149
|
+
return { command: "help", json, allExports, failFast, continueOnFrameError };
|
|
150
|
+
}
|
|
151
|
+
const [command, file] = positional;
|
|
152
|
+
if (command === undefined || !commands.has(command)) {
|
|
153
|
+
return {
|
|
154
|
+
command: "unknown",
|
|
155
|
+
ok: false,
|
|
156
|
+
errors: [
|
|
157
|
+
cliError("CLI_UNKNOWN_COMMAND", "", command === undefined ? "Missing command." : `Unknown command: ${command}`, "Run kavio --help for supported commands.")
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (positional.length > 2) {
|
|
162
|
+
return {
|
|
163
|
+
command: command,
|
|
164
|
+
ok: false,
|
|
165
|
+
errors: [
|
|
166
|
+
cliError("CLI_TOO_MANY_ARGUMENTS", "", `Too many arguments for kavio ${command}.`, `Usage: kavio ${command} <file>`)
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const parsed = { command: command, json, allExports, failFast, continueOnFrameError };
|
|
171
|
+
if (file !== undefined) {
|
|
172
|
+
parsed.file = file;
|
|
173
|
+
}
|
|
174
|
+
if (exportName !== undefined) {
|
|
175
|
+
parsed.exportName = exportName;
|
|
176
|
+
}
|
|
177
|
+
if (propsFile !== undefined) {
|
|
178
|
+
parsed.propsFile = propsFile;
|
|
179
|
+
}
|
|
180
|
+
if (batchFile !== undefined) {
|
|
181
|
+
parsed.batchFile = batchFile;
|
|
182
|
+
}
|
|
183
|
+
if (outDir !== undefined) {
|
|
184
|
+
parsed.outDir = outDir;
|
|
185
|
+
}
|
|
186
|
+
if (concurrency !== undefined) {
|
|
187
|
+
parsed.concurrency = concurrency;
|
|
188
|
+
}
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
191
|
+
function flagFailure(message) {
|
|
192
|
+
return {
|
|
193
|
+
command: "unknown",
|
|
194
|
+
ok: false,
|
|
195
|
+
errors: [cliError("CLI_UNKNOWN_FLAG", "", message, "Run kavio --help for supported options.")]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async function runValidate(file, json) {
|
|
199
|
+
const loaded = await loadJson(file);
|
|
200
|
+
if (!loaded.ok) {
|
|
201
|
+
emitFailure({ command: "validate", ok: false, errors: [loaded.error] }, json);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
const validation = validateComposition(loaded.document);
|
|
205
|
+
const output = {
|
|
206
|
+
command: "validate",
|
|
207
|
+
ok: validation.ok,
|
|
208
|
+
file: loaded.filePath,
|
|
209
|
+
errors: validation.errors
|
|
210
|
+
};
|
|
211
|
+
const version = readVersion(loaded.document);
|
|
212
|
+
if (version !== undefined) {
|
|
213
|
+
output.version = version;
|
|
214
|
+
}
|
|
215
|
+
if (json) {
|
|
216
|
+
writeJson(output);
|
|
217
|
+
}
|
|
218
|
+
else if (validation.ok) {
|
|
219
|
+
writeStdout(`Valid Kavio composition: ${loaded.filePath}\n`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
writeValidationErrors("Invalid Kavio composition", loaded.filePath, validation);
|
|
223
|
+
}
|
|
224
|
+
return validation.ok ? 0 : 1;
|
|
225
|
+
}
|
|
226
|
+
async function runInspect(file, json) {
|
|
227
|
+
const loaded = await loadJson(file);
|
|
228
|
+
if (!loaded.ok) {
|
|
229
|
+
emitFailure({ command: "inspect", ok: false, errors: [loaded.error] }, json);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
const validation = validateComposition(loaded.document);
|
|
233
|
+
if (!validation.ok) {
|
|
234
|
+
if (json) {
|
|
235
|
+
writeJson({
|
|
236
|
+
command: "inspect",
|
|
237
|
+
ok: false,
|
|
238
|
+
file: loaded.filePath,
|
|
239
|
+
errors: validation.errors
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
writeValidationErrors("Cannot inspect invalid Kavio composition", loaded.filePath, validation);
|
|
244
|
+
}
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
const summary = inspectDocument(loaded.filePath, loaded.document);
|
|
248
|
+
const output = { command: "inspect", ok: true, summary };
|
|
249
|
+
if (json) {
|
|
250
|
+
writeJson(output);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
writeInspection(summary);
|
|
254
|
+
}
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
async function runMigrate(file, json) {
|
|
258
|
+
const loaded = await loadJson(file);
|
|
259
|
+
if (!loaded.ok) {
|
|
260
|
+
emitFailure({ command: "migrate", ok: false, errors: [loaded.error] }, json);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
const version = readVersion(loaded.document);
|
|
264
|
+
if (version === undefined) {
|
|
265
|
+
emitFailure({
|
|
266
|
+
command: "migrate",
|
|
267
|
+
ok: false,
|
|
268
|
+
errors: [
|
|
269
|
+
cliError("CLI_MIGRATION_VERSION_REQUIRED", "version", "Cannot migrate a document without a string version.", `Current supported schema version is ${schemaVersion}.`)
|
|
270
|
+
]
|
|
271
|
+
}, json);
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
if (version !== schemaVersion) {
|
|
275
|
+
emitFailure({
|
|
276
|
+
command: "migrate",
|
|
277
|
+
ok: false,
|
|
278
|
+
errors: [
|
|
279
|
+
cliError("CLI_MIGRATION_UNSUPPORTED_VERSION", "version", `No migration path is available from ${version} to ${schemaVersion}.`, "This CLI currently supports the no-op 0.1 to 0.1 migration path.")
|
|
280
|
+
]
|
|
281
|
+
}, json);
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
const validation = validateComposition(loaded.document);
|
|
285
|
+
if (!validation.ok) {
|
|
286
|
+
if (json) {
|
|
287
|
+
writeJson({
|
|
288
|
+
command: "migrate",
|
|
289
|
+
ok: false,
|
|
290
|
+
file: loaded.filePath,
|
|
291
|
+
fromVersion: version,
|
|
292
|
+
toVersion: schemaVersion,
|
|
293
|
+
errors: validation.errors
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
writeValidationErrors("Cannot migrate invalid Kavio composition", loaded.filePath, validation);
|
|
298
|
+
}
|
|
299
|
+
return 1;
|
|
300
|
+
}
|
|
301
|
+
const output = {
|
|
302
|
+
command: "migrate",
|
|
303
|
+
ok: true,
|
|
304
|
+
file: loaded.filePath,
|
|
305
|
+
changed: false,
|
|
306
|
+
fromVersion: version,
|
|
307
|
+
toVersion: schemaVersion,
|
|
308
|
+
document: loaded.document
|
|
309
|
+
};
|
|
310
|
+
if (json) {
|
|
311
|
+
writeJson(output);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
writeStderr(`No migration needed: ${loaded.filePath} is already schema ${schemaVersion}.\n`);
|
|
315
|
+
writeJson(loaded.document);
|
|
316
|
+
}
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
async function runPreview(file, json) {
|
|
320
|
+
const loaded = await loadJson(file);
|
|
321
|
+
if (!loaded.ok) {
|
|
322
|
+
emitFailure({ command: "preview", ok: false, errors: [loaded.error] }, json);
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
const validation = validateComposition(loaded.document);
|
|
326
|
+
if (!validation.ok) {
|
|
327
|
+
if (json) {
|
|
328
|
+
writeJson({
|
|
329
|
+
command: "preview",
|
|
330
|
+
ok: false,
|
|
331
|
+
file: loaded.filePath,
|
|
332
|
+
errors: validation.errors
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
writeValidationErrors("Cannot preview invalid Kavio composition", loaded.filePath, validation);
|
|
337
|
+
}
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
const document = loaded.document;
|
|
341
|
+
const summary = inspectDocument(loaded.filePath, document);
|
|
342
|
+
const assets = await loadPreviewAssets();
|
|
343
|
+
const server = createServer((request, response) => {
|
|
344
|
+
void servePreviewRequest(request, response, document, summary, assets).catch((error) => {
|
|
345
|
+
response.statusCode = 500;
|
|
346
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
347
|
+
response.end(`Preview server error: ${errorMessage(error)}\n`);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
const port = await listenOnLocalhost(server);
|
|
351
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
352
|
+
const output = {
|
|
353
|
+
command: "preview",
|
|
354
|
+
ok: true,
|
|
355
|
+
file: loaded.filePath,
|
|
356
|
+
url,
|
|
357
|
+
renderer: assets.renderer,
|
|
358
|
+
summary
|
|
359
|
+
};
|
|
360
|
+
if (json) {
|
|
361
|
+
writeJson(output);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
writeStdout(`Kavio preview: ${url}\n`);
|
|
365
|
+
writeStdout(`Composition: ${loaded.filePath}\n`);
|
|
366
|
+
writeStdout(`Renderer: ${formatPreviewRendererStatus(assets.renderer)}\n`);
|
|
367
|
+
writeStdout("Press Ctrl+C to stop the preview server.\n");
|
|
368
|
+
}
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
async function runRender(parsed) {
|
|
372
|
+
const file = parsed.file;
|
|
373
|
+
if (file === undefined) {
|
|
374
|
+
emitFailure({
|
|
375
|
+
command: "render",
|
|
376
|
+
ok: false,
|
|
377
|
+
errors: [cliError("CLI_FILE_REQUIRED", "", "Missing file argument for kavio render.", "Usage: kavio render <file>")]
|
|
378
|
+
}, parsed.json);
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
const loaded = await loadJson(file);
|
|
382
|
+
if (!loaded.ok) {
|
|
383
|
+
emitFailure({ command: "render", ok: false, errors: [loaded.error] }, parsed.json);
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
386
|
+
const rows = await resolveRenderRows(parsed);
|
|
387
|
+
if (!rows.ok) {
|
|
388
|
+
emitFailure({ command: "render", ok: false, errors: [rows.error] }, parsed.json);
|
|
389
|
+
return 1;
|
|
390
|
+
}
|
|
391
|
+
const input = { template: loaded.document, rows: rows.rows };
|
|
392
|
+
if (parsed.exportName !== undefined) {
|
|
393
|
+
input.presets = [parsed.exportName];
|
|
394
|
+
}
|
|
395
|
+
const options = {
|
|
396
|
+
outDir: parsed.outDir ?? "renders",
|
|
397
|
+
failFast: parsed.failFast,
|
|
398
|
+
continueOnFrameError: parsed.continueOnFrameError
|
|
399
|
+
};
|
|
400
|
+
if (parsed.concurrency !== undefined) {
|
|
401
|
+
options.concurrency = parsed.concurrency;
|
|
402
|
+
}
|
|
403
|
+
let results;
|
|
404
|
+
try {
|
|
405
|
+
results = await renderBatch(input, options);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
emitFailure({
|
|
409
|
+
command: "render",
|
|
410
|
+
ok: false,
|
|
411
|
+
errors: [cliError("CLI_RENDER_FAILED", "", errorMessage(error), "Check export preset names and render inputs.")]
|
|
412
|
+
}, parsed.json);
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
const succeeded = results.filter((item) => item.result.ok).length;
|
|
416
|
+
const ok = results.length > 0 && succeeded === results.length;
|
|
417
|
+
if (parsed.json) {
|
|
418
|
+
writeJson({
|
|
419
|
+
command: "render",
|
|
420
|
+
ok,
|
|
421
|
+
total: results.length,
|
|
422
|
+
succeeded,
|
|
423
|
+
outputs: results.map((item) => item.result.ok
|
|
424
|
+
? { id: item.id, outputName: item.outputName, ok: true, outputPath: item.result.outputPath }
|
|
425
|
+
: { id: item.id, outputName: item.outputName, ok: false, errors: item.result.errors })
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
for (const item of results) {
|
|
430
|
+
if (item.result.ok) {
|
|
431
|
+
writeStdout(`Rendered ${item.result.outputPath}\n`);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
writeStderr(`Failed ${item.outputName}:\n`);
|
|
435
|
+
for (const error of item.result.errors) {
|
|
436
|
+
const path = error.path.length > 0 ? ` at ${error.path}` : "";
|
|
437
|
+
writeStderr(` - [${error.code}]${path} ${error.message}\n`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
writeStdout(`${succeeded}/${results.length} renders succeeded.\n`);
|
|
442
|
+
}
|
|
443
|
+
return ok ? 0 : 1;
|
|
444
|
+
}
|
|
445
|
+
async function resolveRenderRows(parsed) {
|
|
446
|
+
if (parsed.batchFile !== undefined) {
|
|
447
|
+
const batch = await loadJson(parsed.batchFile);
|
|
448
|
+
if (!batch.ok) {
|
|
449
|
+
return { ok: false, error: batch.error };
|
|
450
|
+
}
|
|
451
|
+
if (!Array.isArray(batch.document)) {
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
error: cliError("CLI_BATCH_INVALID", "", "Batch file must be a JSON array of prop rows.", 'Each row is { "id"?: string, "props"?: object }.')
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return { ok: true, rows: batch.document };
|
|
458
|
+
}
|
|
459
|
+
if (parsed.propsFile !== undefined) {
|
|
460
|
+
const props = await loadJson(parsed.propsFile);
|
|
461
|
+
if (!props.ok) {
|
|
462
|
+
return { ok: false, error: props.error };
|
|
463
|
+
}
|
|
464
|
+
if (typeof props.document !== "object" || props.document === null || Array.isArray(props.document)) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
error: cliError("CLI_PROPS_INVALID", "", "Props file must be a JSON object.", 'Provide { "propName": value, ... }.')
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const propValues = props.document;
|
|
471
|
+
return { ok: true, rows: [{ props: propValues }] };
|
|
472
|
+
}
|
|
473
|
+
return { ok: true, rows: [{}] };
|
|
474
|
+
}
|
|
475
|
+
async function loadJson(file) {
|
|
476
|
+
const filePath = resolve(process.cwd(), file);
|
|
477
|
+
try {
|
|
478
|
+
const source = await readFile(filePath, "utf8");
|
|
479
|
+
try {
|
|
480
|
+
return { ok: true, filePath, document: JSON.parse(source) };
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
error: cliError("CLI_JSON_PARSE_FAILED", filePath, `Failed to parse JSON: ${errorMessage(error)}`, "Check the file for invalid JSON syntax.")
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
error: cliError("CLI_FILE_READ_FAILED", filePath, `Failed to read file: ${errorMessage(error)}`, "Check that the path exists and is readable.")
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function loadPreviewAssets() {
|
|
497
|
+
const browserRendererUrl = new URL("../../browser-renderer/dist/index.js", import.meta.url);
|
|
498
|
+
const coreUrl = new URL("../../core/dist/index.js", import.meta.url);
|
|
499
|
+
try {
|
|
500
|
+
const [browserRendererSource, coreSource] = await Promise.all([
|
|
501
|
+
readFile(browserRendererUrl, "utf8"),
|
|
502
|
+
readFile(coreUrl, "utf8")
|
|
503
|
+
]);
|
|
504
|
+
return {
|
|
505
|
+
browserRendererSource,
|
|
506
|
+
coreSource,
|
|
507
|
+
renderer: {
|
|
508
|
+
available: true,
|
|
509
|
+
mode: "browser-renderer"
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
return {
|
|
515
|
+
renderer: {
|
|
516
|
+
available: false,
|
|
517
|
+
mode: "placeholder",
|
|
518
|
+
reason: `Browser renderer package output is not available: ${errorMessage(error)}`
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function listenOnLocalhost(server) {
|
|
524
|
+
return new Promise((resolveListen, rejectListen) => {
|
|
525
|
+
server.listen(0, "127.0.0.1", () => {
|
|
526
|
+
const address = server.address();
|
|
527
|
+
if (isListenAddress(address)) {
|
|
528
|
+
resolveListen(address.port);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
rejectListen(new Error("Preview server did not return a TCP address."));
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
function isListenAddress(value) {
|
|
536
|
+
return isRecord(value) && typeof value.port === "number";
|
|
537
|
+
}
|
|
538
|
+
async function servePreviewRequest(request, response, document, summary, assets) {
|
|
539
|
+
const path = normalizeRequestPath(request.url);
|
|
540
|
+
if (request.method !== undefined && request.method !== "GET" && request.method !== "HEAD") {
|
|
541
|
+
sendText(response, 405, "Method not allowed.\n");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (path === "/" || path === "/index.html") {
|
|
545
|
+
sendHtml(response, renderPreviewHtml(summary, assets.renderer));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (path === "/composition.json") {
|
|
549
|
+
sendJson(response, document);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (path === "/healthz") {
|
|
553
|
+
sendJson(response, {
|
|
554
|
+
ok: true,
|
|
555
|
+
command: "preview",
|
|
556
|
+
renderer: assets.renderer
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (path === "/vendor/browser-renderer/index.js" && assets.browserRendererSource !== undefined) {
|
|
561
|
+
sendJavaScript(response, assets.browserRendererSource);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (path === "/vendor/core/index.js" && assets.coreSource !== undefined) {
|
|
565
|
+
sendJavaScript(response, assets.coreSource);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
sendText(response, 404, "Not found.\n");
|
|
569
|
+
}
|
|
570
|
+
function normalizeRequestPath(url) {
|
|
571
|
+
return (url ?? "/").split("?", 1)[0] ?? "/";
|
|
572
|
+
}
|
|
573
|
+
function inspectDocument(filePath, document) {
|
|
574
|
+
const fps = Number(document.composition.fps);
|
|
575
|
+
const durationSeconds = fps > 0 ? document.composition.durationFrames / fps : 0;
|
|
576
|
+
const composition = {
|
|
577
|
+
width: document.composition.width,
|
|
578
|
+
height: document.composition.height,
|
|
579
|
+
fps,
|
|
580
|
+
durationFrames: document.composition.durationFrames,
|
|
581
|
+
durationSeconds
|
|
582
|
+
};
|
|
583
|
+
if (document.composition.background !== undefined) {
|
|
584
|
+
composition.background = document.composition.background;
|
|
585
|
+
}
|
|
586
|
+
if (document.composition.colorSpace !== undefined) {
|
|
587
|
+
composition.colorSpace = document.composition.colorSpace;
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
file: filePath,
|
|
591
|
+
version: document.version,
|
|
592
|
+
composition,
|
|
593
|
+
props: {
|
|
594
|
+
count: document.props === undefined ? 0 : Object.keys(document.props).length
|
|
595
|
+
},
|
|
596
|
+
assets: {
|
|
597
|
+
count: Object.keys(document.assets).length,
|
|
598
|
+
types: countTypes(Object.values(document.assets))
|
|
599
|
+
},
|
|
600
|
+
layers: {
|
|
601
|
+
count: document.layers.length,
|
|
602
|
+
types: countTypes(document.layers)
|
|
603
|
+
},
|
|
604
|
+
masks: inspectMasks(document),
|
|
605
|
+
tracks: {
|
|
606
|
+
count: document.tracks === undefined ? 0 : document.tracks.length,
|
|
607
|
+
clipCount: document.tracks === undefined ? 0 : document.tracks.reduce((total, track) => total + track.clips.length, 0),
|
|
608
|
+
transitionWindows: compileTransitionOverlapWindows(document.tracks).map((window) => ({
|
|
609
|
+
trackId: window.trackId,
|
|
610
|
+
previousClipId: window.previousClipId,
|
|
611
|
+
previousLayerId: window.previousLayerId,
|
|
612
|
+
nextClipId: window.nextClipId,
|
|
613
|
+
nextLayerId: window.nextLayerId,
|
|
614
|
+
startFrame: window.startFrame,
|
|
615
|
+
endFrame: window.endFrame,
|
|
616
|
+
durationFrames: window.durationFrames,
|
|
617
|
+
transitionType: window.transition.type
|
|
618
|
+
}))
|
|
619
|
+
},
|
|
620
|
+
audio: {
|
|
621
|
+
count: document.audio === undefined ? 0 : document.audio.length
|
|
622
|
+
},
|
|
623
|
+
exports: {
|
|
624
|
+
count: document.exports.length,
|
|
625
|
+
names: document.exports.map((entry, index) => readName(entry) ?? `export-${index + 1}`)
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function inspectMasks(document) {
|
|
630
|
+
const summary = {
|
|
631
|
+
count: 0,
|
|
632
|
+
shapeCount: 0,
|
|
633
|
+
assetMasks: [],
|
|
634
|
+
proceduralMasks: [],
|
|
635
|
+
invertedCount: 0
|
|
636
|
+
};
|
|
637
|
+
for (const layer of document.layers) {
|
|
638
|
+
const mask = layer.mask;
|
|
639
|
+
if (!mask) {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
summary.count += 1;
|
|
643
|
+
if (mask.invert === true) {
|
|
644
|
+
summary.invertedCount += 1;
|
|
645
|
+
}
|
|
646
|
+
switch (mask.source.kind) {
|
|
647
|
+
case "shape":
|
|
648
|
+
summary.shapeCount += 1;
|
|
649
|
+
break;
|
|
650
|
+
case "asset":
|
|
651
|
+
summary.assetMasks.push(withResolution({
|
|
652
|
+
layerId: layer.id,
|
|
653
|
+
asset: mask.source.asset,
|
|
654
|
+
mode: mask.source.mode ?? "alpha"
|
|
655
|
+
}, mask.source.resolution));
|
|
656
|
+
break;
|
|
657
|
+
case "procedural":
|
|
658
|
+
summary.proceduralMasks.push(withResolution({
|
|
659
|
+
layerId: layer.id,
|
|
660
|
+
type: mask.source.type,
|
|
661
|
+
seed: mask.source.seed,
|
|
662
|
+
...(mask.source.direction === undefined ? {} : { direction: mask.source.direction })
|
|
663
|
+
}, mask.source.resolution));
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return summary;
|
|
668
|
+
}
|
|
669
|
+
function withResolution(summary, resolution) {
|
|
670
|
+
return (resolution === undefined
|
|
671
|
+
? summary
|
|
672
|
+
: {
|
|
673
|
+
...summary,
|
|
674
|
+
width: resolution.width,
|
|
675
|
+
height: resolution.height
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function countTypes(items) {
|
|
679
|
+
const counts = {};
|
|
680
|
+
for (const item of items) {
|
|
681
|
+
const type = readType(item) ?? "unknown";
|
|
682
|
+
counts[type] = (counts[type] ?? 0) + 1;
|
|
683
|
+
}
|
|
684
|
+
return counts;
|
|
685
|
+
}
|
|
686
|
+
function readVersion(value) {
|
|
687
|
+
if (!isRecord(value) || typeof value.version !== "string") {
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
return value.version;
|
|
691
|
+
}
|
|
692
|
+
function readType(value) {
|
|
693
|
+
if (!isRecord(value) || typeof value.type !== "string") {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
return value.type;
|
|
697
|
+
}
|
|
698
|
+
function readName(value) {
|
|
699
|
+
if (!isRecord(value) || typeof value.name !== "string") {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
return value.name;
|
|
703
|
+
}
|
|
704
|
+
function isRecord(value) {
|
|
705
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
706
|
+
}
|
|
707
|
+
function writeInspection(summary) {
|
|
708
|
+
writeStdout(`Kavio composition: ${summary.file}\n`);
|
|
709
|
+
writeStdout(`Version: ${summary.version}\n`);
|
|
710
|
+
writeStdout(`Size: ${summary.composition.width}x${summary.composition.height} @ ${summary.composition.fps}fps\n`);
|
|
711
|
+
writeStdout(`Duration: ${summary.composition.durationFrames} frames (${summary.composition.durationSeconds.toFixed(2)}s)\n`);
|
|
712
|
+
writeStdout(`Props: ${summary.props.count}\n`);
|
|
713
|
+
writeStdout(`Assets: ${summary.assets.count}${formatTypeCounts(summary.assets.types)}\n`);
|
|
714
|
+
writeStdout(`Layers: ${summary.layers.count}${formatTypeCounts(summary.layers.types)}\n`);
|
|
715
|
+
writeStdout(`Masks: ${summary.masks.count} (shape: ${summary.masks.shapeCount}, asset: ${summary.masks.assetMasks.length}, procedural: ${summary.masks.proceduralMasks.length}, inverted: ${summary.masks.invertedCount})\n`);
|
|
716
|
+
for (const mask of summary.masks.assetMasks) {
|
|
717
|
+
writeStdout(` - asset ${mask.asset} on ${mask.layerId}, ${mask.mode}${formatResolution(mask.width, mask.height)}\n`);
|
|
718
|
+
}
|
|
719
|
+
for (const mask of summary.masks.proceduralMasks) {
|
|
720
|
+
writeStdout(` - procedural ${mask.type} on ${mask.layerId}, seed ${mask.seed}${formatResolution(mask.width, mask.height)}\n`);
|
|
721
|
+
}
|
|
722
|
+
writeStdout(`Tracks: ${summary.tracks.count} (${summary.tracks.clipCount} clips, ${summary.tracks.transitionWindows.length} transition windows)\n`);
|
|
723
|
+
for (const window of summary.tracks.transitionWindows) {
|
|
724
|
+
writeStdout(` - ${window.trackId}: ${window.previousClipId} -> ${window.nextClipId}, ${window.transitionType}, frames ${window.startFrame}-${window.endFrame - 1}\n`);
|
|
725
|
+
}
|
|
726
|
+
writeStdout(`Audio: ${summary.audio.count}\n`);
|
|
727
|
+
writeStdout(`Exports: ${summary.exports.count}\n`);
|
|
728
|
+
for (const exportName of summary.exports.names) {
|
|
729
|
+
writeStdout(` - ${exportName}\n`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function formatResolution(width, height) {
|
|
733
|
+
return width === undefined || height === undefined ? "" : `, ${width}x${height}`;
|
|
734
|
+
}
|
|
735
|
+
function writeSocialMediaPresets(presets) {
|
|
736
|
+
writeStdout("Kavio social media export presets:\n");
|
|
737
|
+
for (const preset of presets) {
|
|
738
|
+
writeStdout(` ${preset.id.padEnd(24)} ${preset.label.padEnd(24)} ${String(preset.width).padStart(4)}x${String(preset.height).padEnd(4)} ${preset.aspectRatio.padEnd(4)} export: ${preset.defaultName}\n`);
|
|
739
|
+
}
|
|
740
|
+
writeStdout("\nUse `kavio presets <id>` to print a copy/pasteable export preset JSON object.\n");
|
|
741
|
+
writeStdout("Use `kavio presets --json` to print the full machine-readable preset catalog.\n");
|
|
742
|
+
}
|
|
743
|
+
function findSocialMediaPreset(name) {
|
|
744
|
+
const normalized = normalizePresetName(name);
|
|
745
|
+
return socialMediaPresets.find((preset) => normalizePresetName(preset.id) === normalized || normalizePresetName(preset.defaultName) === normalized);
|
|
746
|
+
}
|
|
747
|
+
function normalizePresetName(value) {
|
|
748
|
+
return value.trim().toLowerCase().replaceAll(/[\s_]+/g, "-");
|
|
749
|
+
}
|
|
750
|
+
function renderPreviewHtml(summary, renderer) {
|
|
751
|
+
const frameCount = Math.max(1, summary.composition.durationFrames);
|
|
752
|
+
const initialScale = Math.min(1, 720 / summary.composition.height, 960 / summary.composition.width);
|
|
753
|
+
return `<!doctype html>
|
|
754
|
+
<html lang="en">
|
|
755
|
+
<head>
|
|
756
|
+
<meta charset="utf-8">
|
|
757
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
758
|
+
<title>Kavio Preview</title>
|
|
759
|
+
${renderer.available ? `<script type="importmap">${escapeHtml(JSON.stringify({ imports: { "@kitsra/kavio-core": "/vendor/core/index.js" } }))}</script>` : ""}
|
|
760
|
+
<style>
|
|
761
|
+
:root {
|
|
762
|
+
color-scheme: dark;
|
|
763
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
764
|
+
background: #101820;
|
|
765
|
+
color: #f5f7fa;
|
|
766
|
+
}
|
|
767
|
+
body {
|
|
768
|
+
margin: 0;
|
|
769
|
+
min-height: 100vh;
|
|
770
|
+
display: grid;
|
|
771
|
+
grid-template-columns: minmax(260px, 320px) 1fr;
|
|
772
|
+
}
|
|
773
|
+
aside {
|
|
774
|
+
border-right: 1px solid #26313d;
|
|
775
|
+
padding: 20px;
|
|
776
|
+
background: #151f29;
|
|
777
|
+
}
|
|
778
|
+
main {
|
|
779
|
+
display: grid;
|
|
780
|
+
place-items: center;
|
|
781
|
+
min-width: 0;
|
|
782
|
+
padding: 24px;
|
|
783
|
+
}
|
|
784
|
+
h1 {
|
|
785
|
+
margin: 0 0 16px;
|
|
786
|
+
font-size: 20px;
|
|
787
|
+
font-weight: 650;
|
|
788
|
+
}
|
|
789
|
+
dl {
|
|
790
|
+
display: grid;
|
|
791
|
+
grid-template-columns: auto 1fr;
|
|
792
|
+
gap: 8px 12px;
|
|
793
|
+
margin: 0 0 18px;
|
|
794
|
+
font-size: 13px;
|
|
795
|
+
}
|
|
796
|
+
dt {
|
|
797
|
+
color: #a7b3c0;
|
|
798
|
+
}
|
|
799
|
+
dd {
|
|
800
|
+
margin: 0;
|
|
801
|
+
overflow-wrap: anywhere;
|
|
802
|
+
}
|
|
803
|
+
.status {
|
|
804
|
+
border: 1px solid #334150;
|
|
805
|
+
padding: 12px;
|
|
806
|
+
background: #101820;
|
|
807
|
+
font-size: 13px;
|
|
808
|
+
line-height: 1.45;
|
|
809
|
+
}
|
|
810
|
+
.viewport {
|
|
811
|
+
display: grid;
|
|
812
|
+
place-items: center;
|
|
813
|
+
max-width: 100%;
|
|
814
|
+
overflow: auto;
|
|
815
|
+
}
|
|
816
|
+
#stage {
|
|
817
|
+
width: ${summary.composition.width}px;
|
|
818
|
+
height: ${summary.composition.height}px;
|
|
819
|
+
transform: scale(${initialScale});
|
|
820
|
+
transform-origin: center;
|
|
821
|
+
background: #0b0f14;
|
|
822
|
+
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
|
|
823
|
+
}
|
|
824
|
+
#placeholder {
|
|
825
|
+
box-sizing: border-box;
|
|
826
|
+
width: 100%;
|
|
827
|
+
height: 100%;
|
|
828
|
+
display: grid;
|
|
829
|
+
align-content: center;
|
|
830
|
+
justify-items: center;
|
|
831
|
+
gap: 12px;
|
|
832
|
+
padding: 32px;
|
|
833
|
+
text-align: center;
|
|
834
|
+
color: #d9e2ec;
|
|
835
|
+
background:
|
|
836
|
+
linear-gradient(90deg, rgba(255,255,255,.06) 1px, transparent 1px),
|
|
837
|
+
linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px),
|
|
838
|
+
#0b0f14;
|
|
839
|
+
background-size: 48px 48px;
|
|
840
|
+
}
|
|
841
|
+
code {
|
|
842
|
+
color: #b7f7d5;
|
|
843
|
+
}
|
|
844
|
+
@media (max-width: 760px) {
|
|
845
|
+
body {
|
|
846
|
+
grid-template-columns: 1fr;
|
|
847
|
+
}
|
|
848
|
+
aside {
|
|
849
|
+
border-right: 0;
|
|
850
|
+
border-bottom: 1px solid #26313d;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
</style>
|
|
854
|
+
</head>
|
|
855
|
+
<body>
|
|
856
|
+
<aside>
|
|
857
|
+
<h1>Kavio Preview</h1>
|
|
858
|
+
<dl>
|
|
859
|
+
<dt>File</dt><dd>${escapeHtml(summary.file)}</dd>
|
|
860
|
+
<dt>Version</dt><dd>${escapeHtml(summary.version)}</dd>
|
|
861
|
+
<dt>Size</dt><dd>${summary.composition.width}x${summary.composition.height}</dd>
|
|
862
|
+
<dt>Frames</dt><dd>${summary.composition.durationFrames} @ ${summary.composition.fps}fps</dd>
|
|
863
|
+
<dt>Layers</dt><dd>${summary.layers.count}${escapeHtml(formatTypeCounts(summary.layers.types))}</dd>
|
|
864
|
+
<dt>Assets</dt><dd>${summary.assets.count}${escapeHtml(formatTypeCounts(summary.assets.types))}</dd>
|
|
865
|
+
</dl>
|
|
866
|
+
<div class="status" id="status">${escapeHtml(formatPreviewRendererStatus(renderer))}</div>
|
|
867
|
+
</aside>
|
|
868
|
+
<main>
|
|
869
|
+
<div class="viewport">
|
|
870
|
+
<div id="stage">
|
|
871
|
+
<div id="placeholder">
|
|
872
|
+
<strong>Preview shell ready</strong>
|
|
873
|
+
<span>Loaded ${frameCount} frame${frameCount === 1 ? "" : "s"} from <code>/composition.json</code>.</span>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
</main>
|
|
878
|
+
<script type="application/json" id="kavio-summary">${escapeHtml(JSON.stringify(summary))}</script>
|
|
879
|
+
${renderer.available
|
|
880
|
+
? `<script type="module">
|
|
881
|
+
const status = document.getElementById("status");
|
|
882
|
+
const stage = document.getElementById("stage");
|
|
883
|
+
try {
|
|
884
|
+
const composition = await fetch("/composition.json").then((response) => response.json());
|
|
885
|
+
const { installBrowserRendererRuntime } = await import("/vendor/browser-renderer/index.js");
|
|
886
|
+
const runtime = installBrowserRendererRuntime({ root: stage });
|
|
887
|
+
await runtime.loadComposition(composition);
|
|
888
|
+
const rendered = await runtime.renderFrame(0);
|
|
889
|
+
status.textContent = "Browser renderer loaded. Showing frame 0 with " + rendered.layers.length + " active layer(s).";
|
|
890
|
+
} catch (error) {
|
|
891
|
+
status.textContent = "Preview placeholder active. Browser renderer package output was found, but could not run in this shell: " + String(error instanceof Error ? error.message : error);
|
|
892
|
+
}
|
|
893
|
+
</script>`
|
|
894
|
+
: ""}
|
|
895
|
+
</body>
|
|
896
|
+
</html>
|
|
897
|
+
`;
|
|
898
|
+
}
|
|
899
|
+
function formatPreviewRendererStatus(renderer) {
|
|
900
|
+
if (renderer.available) {
|
|
901
|
+
return "browser-renderer package output available";
|
|
902
|
+
}
|
|
903
|
+
return renderer.reason === undefined ? "placeholder preview shell" : `placeholder preview shell (${renderer.reason})`;
|
|
904
|
+
}
|
|
905
|
+
function sendHtml(response, value) {
|
|
906
|
+
send(response, 200, "text/html; charset=utf-8", value);
|
|
907
|
+
}
|
|
908
|
+
function sendJson(response, value) {
|
|
909
|
+
send(response, 200, "application/json; charset=utf-8", `${JSON.stringify(value, null, 2)}\n`);
|
|
910
|
+
}
|
|
911
|
+
function sendJavaScript(response, value) {
|
|
912
|
+
send(response, 200, "text/javascript; charset=utf-8", value);
|
|
913
|
+
}
|
|
914
|
+
function sendText(response, statusCode, value) {
|
|
915
|
+
send(response, statusCode, "text/plain; charset=utf-8", value);
|
|
916
|
+
}
|
|
917
|
+
function send(response, statusCode, contentType, value) {
|
|
918
|
+
response.statusCode = statusCode;
|
|
919
|
+
response.setHeader("content-type", contentType);
|
|
920
|
+
response.setHeader("cache-control", "no-store");
|
|
921
|
+
response.end(value);
|
|
922
|
+
}
|
|
923
|
+
function writeValidationErrors(title, filePath, validation) {
|
|
924
|
+
writeStderr(`${title}: ${filePath}\n`);
|
|
925
|
+
for (const error of validation.errors) {
|
|
926
|
+
const path = error.path.length > 0 ? ` at ${error.path}` : "";
|
|
927
|
+
const hint = error.hint === undefined ? "" : ` Hint: ${error.hint}`;
|
|
928
|
+
writeStderr(` - [${error.code}]${path} ${error.message}${hint}\n`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function emitFailure(failure, json) {
|
|
932
|
+
if (json) {
|
|
933
|
+
writeJson(failure);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
for (const error of failure.errors) {
|
|
937
|
+
const hint = error.hint === undefined ? "" : `\n${error.hint}`;
|
|
938
|
+
writeStderr(`${error.message}${hint}\n`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function cliError(code, path, message, hint) {
|
|
942
|
+
const error = {
|
|
943
|
+
code,
|
|
944
|
+
severity: "error",
|
|
945
|
+
message,
|
|
946
|
+
path,
|
|
947
|
+
stage: "io",
|
|
948
|
+
retryable: false
|
|
949
|
+
};
|
|
950
|
+
if (hint !== undefined) {
|
|
951
|
+
error.hint = hint;
|
|
952
|
+
}
|
|
953
|
+
return error;
|
|
954
|
+
}
|
|
955
|
+
function errorMessage(error) {
|
|
956
|
+
if (error instanceof Error) {
|
|
957
|
+
return error.message;
|
|
958
|
+
}
|
|
959
|
+
return String(error);
|
|
960
|
+
}
|
|
961
|
+
function formatTypeCounts(counts) {
|
|
962
|
+
const entries = Object.entries(counts);
|
|
963
|
+
if (entries.length === 0) {
|
|
964
|
+
return "";
|
|
965
|
+
}
|
|
966
|
+
return ` (${entries.map(([type, count]) => `${type}: ${count}`).join(", ")})`;
|
|
967
|
+
}
|
|
968
|
+
function escapeHtml(value) {
|
|
969
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
970
|
+
switch (character) {
|
|
971
|
+
case "&":
|
|
972
|
+
return "&";
|
|
973
|
+
case "<":
|
|
974
|
+
return "<";
|
|
975
|
+
case ">":
|
|
976
|
+
return ">";
|
|
977
|
+
case '"':
|
|
978
|
+
return """;
|
|
979
|
+
case "'":
|
|
980
|
+
return "'";
|
|
981
|
+
default:
|
|
982
|
+
return character;
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
function helpText() {
|
|
987
|
+
return `Kavio CLI
|
|
988
|
+
|
|
989
|
+
Usage:
|
|
990
|
+
kavio --help
|
|
991
|
+
kavio [--json] validate <file>
|
|
992
|
+
kavio [--json] inspect <file>
|
|
993
|
+
kavio [--json] migrate <file>
|
|
994
|
+
kavio [--json] preview <file>
|
|
995
|
+
kavio [--json] render <file> [render options]
|
|
996
|
+
kavio [--json] presets [preset-id]
|
|
997
|
+
|
|
998
|
+
Options:
|
|
999
|
+
--json Emit machine-readable JSON for CI and repair loops.
|
|
1000
|
+
-h, --help Show this help.
|
|
1001
|
+
|
|
1002
|
+
Render options:
|
|
1003
|
+
--export <name> Render one named export preset.
|
|
1004
|
+
--all-exports Render every export preset (default).
|
|
1005
|
+
--props <file.json> Prop values for a single render.
|
|
1006
|
+
--batch <file.json> Array of prop rows -> rows x presets.
|
|
1007
|
+
--out <dir> Output directory (default: renders).
|
|
1008
|
+
--concurrency <n> Parallel jobs (default: 1).
|
|
1009
|
+
--fail-fast Abort the batch on first job failure.
|
|
1010
|
+
--continue-on-frame-error Tolerate per-frame capture failures.
|
|
1011
|
+
|
|
1012
|
+
Commands:
|
|
1013
|
+
validate Validate a Kavio composition JSON file.
|
|
1014
|
+
inspect Print a composition summary.
|
|
1015
|
+
migrate Write the latest-schema JSON document to stdout.
|
|
1016
|
+
preview Start a local browser preview server.
|
|
1017
|
+
render Render a composition to MP4 (browser capture + FFmpeg).
|
|
1018
|
+
presets List social media export presets or print one export JSON object.
|
|
1019
|
+
`;
|
|
1020
|
+
}
|
|
1021
|
+
function writeJson(value) {
|
|
1022
|
+
writeStdout(`${JSON.stringify(value, null, 2)}\n`);
|
|
1023
|
+
}
|
|
1024
|
+
function writeStdout(value) {
|
|
1025
|
+
process.stdout.write(value);
|
|
1026
|
+
}
|
|
1027
|
+
function writeStderr(value) {
|
|
1028
|
+
process.stderr.write(value);
|
|
1029
|
+
}
|
|
1030
|
+
void main(process.argv.slice(2))
|
|
1031
|
+
.then((exitCode) => {
|
|
1032
|
+
process.exitCode = exitCode;
|
|
1033
|
+
})
|
|
1034
|
+
.catch((error) => {
|
|
1035
|
+
writeStderr(`Unexpected CLI failure: ${errorMessage(error)}\n`);
|
|
1036
|
+
process.exitCode = 1;
|
|
1037
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitsra/kavio-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local CLI for Kavio validation, migration, preview, and rendering.",
|
|
5
|
+
"license": "Elastic-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"kavio": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@kitsra/kavio-builder": "0.1.0",
|
|
15
|
+
"@kitsra/kavio-core": "0.1.0",
|
|
16
|
+
"@kitsra/kavio-render": "0.1.0",
|
|
17
|
+
"@kitsra/kavio-schema": "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"!dist/**/*.tsbuildinfo"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
32
|
+
"test": "tsc -b tsconfig.json && node --test test/*.test.mjs"
|
|
33
|
+
}
|
|
34
|
+
}
|