@vosjs/cli 0.53.6 → 0.54.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/README.md +14 -0
- package/dist/{chunk-MJYXP4AA.js → chunk-M22MAUMZ.js} +1534 -181
- package/dist/chunk-M22MAUMZ.js.map +1 -0
- package/dist/{chunk-WLUWDQQO.js → chunk-SAWRKTRX.js} +245 -1
- package/dist/chunk-SAWRKTRX.js.map +1 -0
- package/dist/{chunk-2EHIFIYC.js → chunk-WZGCEAXB.js} +13 -1
- package/dist/chunk-WZGCEAXB.js.map +1 -0
- package/dist/cli.js +11 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +6 -8
- package/dist/manifest-Q4QMVIYR.js +8 -0
- package/dist/{run-DYNCGXWO.js → run-UL7IYGQH.js} +3 -3
- package/package.json +6 -4
- package/skills/VERSION +1 -1
- package/skills/vos-port/SKILL.md +25 -22
- package/dist/chunk-2EHIFIYC.js.map +0 -1
- package/dist/chunk-MJYXP4AA.js.map +0 -1
- package/dist/chunk-WLUWDQQO.js.map +0 -1
- package/dist/chunk-ZCLGXETO.js +0 -249
- package/dist/chunk-ZCLGXETO.js.map +0 -1
- package/dist/manifest-IG2WSARS.js +0 -8
- /package/dist/{manifest-IG2WSARS.js.map → manifest-Q4QMVIYR.js.map} +0 -0
- /package/dist/{run-DYNCGXWO.js.map → run-UL7IYGQH.js.map} +0 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
apiError,
|
|
10
10
|
apiJson,
|
|
11
11
|
clientId,
|
|
12
|
+
configDuration,
|
|
12
13
|
containerMismatch,
|
|
13
14
|
deriveSlug,
|
|
14
15
|
directoryKind,
|
|
@@ -16,12 +17,16 @@ import {
|
|
|
16
17
|
formatChanges,
|
|
17
18
|
hasFlag,
|
|
18
19
|
launchBrowser,
|
|
20
|
+
loadVosConfig,
|
|
19
21
|
nameForType,
|
|
20
22
|
numFlag2 as numFlag,
|
|
21
23
|
parseArgs2 as parseArgs,
|
|
24
|
+
parseTimes,
|
|
22
25
|
parseVosId,
|
|
23
26
|
platformOrigin,
|
|
24
27
|
readSyncState,
|
|
28
|
+
reencodeStill,
|
|
29
|
+
renderStill,
|
|
25
30
|
requireCredential,
|
|
26
31
|
resolveCredential,
|
|
27
32
|
resolveMediaType,
|
|
@@ -31,7 +36,7 @@ import {
|
|
|
31
36
|
videoFormat,
|
|
32
37
|
writeCredential,
|
|
33
38
|
writeSyncState
|
|
34
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-SAWRKTRX.js";
|
|
35
40
|
import {
|
|
36
41
|
deadLine,
|
|
37
42
|
deadWarns
|
|
@@ -94,8 +99,8 @@ function landedLines(l) {
|
|
|
94
99
|
|
|
95
100
|
// src/plugin/run.ts
|
|
96
101
|
import { mkdir as mkdir11, readFile as readFile17, rm as rm5 } from "fs/promises";
|
|
97
|
-
import { existsSync as
|
|
98
|
-
import { join as
|
|
102
|
+
import { existsSync as existsSync23 } from "fs";
|
|
103
|
+
import { join as join27, resolve as resolve15 } from "path";
|
|
99
104
|
import {
|
|
100
105
|
END_CARD_FROM as END_CARD_FROM2,
|
|
101
106
|
docOutputDuration as docOutputDuration3,
|
|
@@ -2334,10 +2339,10 @@ function startTakeServer(rootDir, pages) {
|
|
|
2334
2339
|
res.writeHead(404).end();
|
|
2335
2340
|
});
|
|
2336
2341
|
return new Promise(
|
|
2337
|
-
(
|
|
2342
|
+
(resolve16) => server.listen(0, () => {
|
|
2338
2343
|
const addr = server.address();
|
|
2339
2344
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
2340
|
-
|
|
2345
|
+
resolve16({ base: `http://localhost:${port}`, close: () => server.close() });
|
|
2341
2346
|
})
|
|
2342
2347
|
);
|
|
2343
2348
|
}
|
|
@@ -3584,7 +3589,7 @@ async function momentsFor(dir, doc) {
|
|
|
3584
3589
|
}
|
|
3585
3590
|
return momentsFromDoc2(doc, planForDigest2(doc));
|
|
3586
3591
|
}
|
|
3587
|
-
function
|
|
3592
|
+
function parseTimes2(spec, duration) {
|
|
3588
3593
|
return spec.split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
3589
3594
|
if (s.endsWith("%")) {
|
|
3590
3595
|
const p = Number(s.slice(0, -1));
|
|
@@ -4211,11 +4216,11 @@ var DUPLICATE_BITS = 6;
|
|
|
4211
4216
|
var POPULATED_INK = 0.12;
|
|
4212
4217
|
function pickMoments(measured, opts = {}) {
|
|
4213
4218
|
const minInk = opts.minInk ?? 0.06;
|
|
4214
|
-
const
|
|
4219
|
+
const relative3 = opts.relativeInk ?? 0.4;
|
|
4215
4220
|
const bits = opts.duplicateBits ?? DUPLICATE_BITS;
|
|
4216
4221
|
const inks = measured.map((m) => m.ink).sort((a, b) => a - b);
|
|
4217
4222
|
const median2 = inks[inks.length >> 1] ?? 0;
|
|
4218
|
-
const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 *
|
|
4223
|
+
const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 * relative3));
|
|
4219
4224
|
const kept = [];
|
|
4220
4225
|
const dropped = [];
|
|
4221
4226
|
for (const m of measured) {
|
|
@@ -4846,7 +4851,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4846
4851
|
const vh = meta.captureHeight ?? meta.height;
|
|
4847
4852
|
const probeDir = await mkdtemp(join7(tmpdir(), "vos-moments-"));
|
|
4848
4853
|
try {
|
|
4849
|
-
const
|
|
4854
|
+
const probe3 = await framesTake(browser, dir, {
|
|
4850
4855
|
times: candidates.map((c) => c.time),
|
|
4851
4856
|
width: PROBE_WIDTH,
|
|
4852
4857
|
height: Math.max(2, Math.round(PROBE_WIDTH * vh / vw / 2) * 2),
|
|
@@ -4857,7 +4862,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
|
|
|
4857
4862
|
}
|
|
4858
4863
|
});
|
|
4859
4864
|
const byTime = /* @__PURE__ */ new Map();
|
|
4860
|
-
for (const frame of
|
|
4865
|
+
for (const frame of probe3.frames) {
|
|
4861
4866
|
const img = decodePng(new Uint8Array(await readFile7(frame.file)));
|
|
4862
4867
|
if (!img) continue;
|
|
4863
4868
|
const whole = { x: 0, y: 0, w: img.w, h: img.h };
|
|
@@ -6059,10 +6064,10 @@ function duplicateFindings(stills) {
|
|
|
6059
6064
|
};
|
|
6060
6065
|
});
|
|
6061
6066
|
}
|
|
6062
|
-
async function videoFrame(file, at2,
|
|
6067
|
+
async function videoFrame(file, at2, ffmpeg2) {
|
|
6063
6068
|
try {
|
|
6064
6069
|
const { stdout } = await execFileP(
|
|
6065
|
-
|
|
6070
|
+
ffmpeg2,
|
|
6066
6071
|
[
|
|
6067
6072
|
"-v",
|
|
6068
6073
|
"error",
|
|
@@ -6097,7 +6102,7 @@ async function pictureChecks(assets) {
|
|
|
6097
6102
|
const findings = [];
|
|
6098
6103
|
const measured = [];
|
|
6099
6104
|
const stills = [];
|
|
6100
|
-
let
|
|
6105
|
+
let ffmpeg2 = null;
|
|
6101
6106
|
for (const a of assets) {
|
|
6102
6107
|
if (/\.png$/i.test(a.file)) {
|
|
6103
6108
|
const img = decodePng(new Uint8Array(await readFile11(a.file)));
|
|
@@ -6126,8 +6131,8 @@ async function pictureChecks(assets) {
|
|
|
6126
6131
|
continue;
|
|
6127
6132
|
}
|
|
6128
6133
|
if (/\.(mp4|webm|mov)$/i.test(a.file)) {
|
|
6129
|
-
if (
|
|
6130
|
-
if (!
|
|
6134
|
+
if (ffmpeg2 === null) ffmpeg2 = await onPath("ffmpeg");
|
|
6135
|
+
if (!ffmpeg2) {
|
|
6131
6136
|
findings.push({
|
|
6132
6137
|
code: "firstlast",
|
|
6133
6138
|
severity: "info",
|
|
@@ -6514,10 +6519,10 @@ async function openSession(name, url, opts = { log: () => {
|
|
|
6514
6519
|
opts.log(
|
|
6515
6520
|
`a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
|
|
6516
6521
|
);
|
|
6517
|
-
return new Promise((
|
|
6522
|
+
return new Promise((resolve16, reject) => {
|
|
6518
6523
|
const child = spawn(exe, args, { stdio: "ignore" });
|
|
6519
6524
|
child.on("error", reject);
|
|
6520
|
-
child.on("exit", (code) =>
|
|
6525
|
+
child.on("exit", (code) => resolve16({ code }));
|
|
6521
6526
|
});
|
|
6522
6527
|
}
|
|
6523
6528
|
async function withSession(name, fn, opts = {}) {
|
|
@@ -7339,7 +7344,7 @@ async function cmdPull(argv) {
|
|
|
7339
7344
|
members.push({ dir, id: v.id, title: v.title, take });
|
|
7340
7345
|
}
|
|
7341
7346
|
const assets = payload.assets.filter((a) => a.category !== "recipe");
|
|
7342
|
-
const
|
|
7347
|
+
const summary3 = [
|
|
7343
7348
|
`Pulled ${folder.name} (${folder.slug}) \u2192 ${out}/`,
|
|
7344
7349
|
...folder.id && payload.folder.description ? [` ${payload.folder.description}`] : [],
|
|
7345
7350
|
` recipes (${recipes.length}) \u2014 read EVERY one:`,
|
|
@@ -7366,7 +7371,7 @@ async function cmdPull(argv) {
|
|
|
7366
7371
|
assets,
|
|
7367
7372
|
subfolders: payload.subfolders
|
|
7368
7373
|
},
|
|
7369
|
-
|
|
7374
|
+
summary3
|
|
7370
7375
|
);
|
|
7371
7376
|
return EXIT_OK;
|
|
7372
7377
|
}
|
|
@@ -8561,9 +8566,9 @@ async function cmdLogin(argv) {
|
|
|
8561
8566
|
return storeKey(key, origin, r);
|
|
8562
8567
|
}
|
|
8563
8568
|
async function storeKey(key, origin, r) {
|
|
8564
|
-
const
|
|
8565
|
-
if (
|
|
8566
|
-
throw new Error(apiError("validate key",
|
|
8569
|
+
const probe3 = await apiJson(origin, "/api/folders", { key });
|
|
8570
|
+
if (probe3.status === 401 || probe3.status === 403) {
|
|
8571
|
+
throw new Error(apiError("validate key", probe3));
|
|
8567
8572
|
}
|
|
8568
8573
|
const path = writeCredential(key);
|
|
8569
8574
|
r.done(
|
|
@@ -8813,20 +8818,1354 @@ async function cmdDelete(argv) {
|
|
|
8813
8818
|
return EXIT_OK;
|
|
8814
8819
|
}
|
|
8815
8820
|
|
|
8816
|
-
// src/plugin/
|
|
8817
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
8818
|
-
import { basename as basename6 } from "path";
|
|
8821
|
+
// src/plugin/buildProgram.ts
|
|
8822
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
8823
|
+
import { basename as basename6, dirname as dirname5, join as join19, resolve as resolve7 } from "path";
|
|
8824
|
+
import { pathToFileURL } from "url";
|
|
8825
|
+
import { parse } from "acorn";
|
|
8826
|
+
import { fullAncestor } from "acorn-walk";
|
|
8827
|
+
var FUNCTION_KEYS = [
|
|
8828
|
+
"setup",
|
|
8829
|
+
"createContent",
|
|
8830
|
+
"createTimeline",
|
|
8831
|
+
"onFrame",
|
|
8832
|
+
"retime"
|
|
8833
|
+
];
|
|
8834
|
+
var PAGE_GLOBALS = /* @__PURE__ */ new Set([
|
|
8835
|
+
"window",
|
|
8836
|
+
"document",
|
|
8837
|
+
"navigator",
|
|
8838
|
+
"location",
|
|
8839
|
+
"performance",
|
|
8840
|
+
"requestAnimationFrame",
|
|
8841
|
+
"cancelAnimationFrame",
|
|
8842
|
+
"devicePixelRatio",
|
|
8843
|
+
"Image",
|
|
8844
|
+
"ImageData",
|
|
8845
|
+
"ImageBitmap",
|
|
8846
|
+
"createImageBitmap",
|
|
8847
|
+
"OffscreenCanvas",
|
|
8848
|
+
"HTMLCanvasElement",
|
|
8849
|
+
"CanvasRenderingContext2D",
|
|
8850
|
+
"Path2D",
|
|
8851
|
+
"DOMMatrix",
|
|
8852
|
+
"FontFace",
|
|
8853
|
+
"Blob",
|
|
8854
|
+
"URL",
|
|
8855
|
+
"fetch",
|
|
8856
|
+
"Response",
|
|
8857
|
+
"TextEncoder",
|
|
8858
|
+
"TextDecoder",
|
|
8859
|
+
"AudioContext",
|
|
8860
|
+
"OfflineAudioContext",
|
|
8861
|
+
"getComputedStyle",
|
|
8862
|
+
"matchMedia",
|
|
8863
|
+
"atob",
|
|
8864
|
+
"btoa"
|
|
8865
|
+
]);
|
|
8866
|
+
var JS_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
|
|
8867
|
+
JS_GLOBALS.add("undefined");
|
|
8868
|
+
JS_GLOBALS.add("arguments");
|
|
8869
|
+
function functionSource(fn) {
|
|
8870
|
+
const src = fn.toString();
|
|
8871
|
+
if (parsesAsExpression(src)) return src;
|
|
8872
|
+
const asyncMethod = /^async\s+/.test(src);
|
|
8873
|
+
const body = asyncMethod ? src.replace(/^async\s+/, "") : src;
|
|
8874
|
+
const candidate = `${asyncMethod ? "async " : ""}function ${body}`;
|
|
8875
|
+
if (parsesAsExpression(candidate)) return candidate;
|
|
8876
|
+
return src;
|
|
8877
|
+
}
|
|
8878
|
+
function parsesAsExpression(src) {
|
|
8879
|
+
try {
|
|
8880
|
+
parse(`(${src})`, { ecmaVersion: "latest" });
|
|
8881
|
+
return true;
|
|
8882
|
+
} catch {
|
|
8883
|
+
return false;
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
function freeIdentifiers(src) {
|
|
8887
|
+
const ast = parse(`(${src})`, { ecmaVersion: "latest" });
|
|
8888
|
+
const declared = /* @__PURE__ */ new Set();
|
|
8889
|
+
const read = /* @__PURE__ */ new Set();
|
|
8890
|
+
const addPattern = (p) => {
|
|
8891
|
+
if (!p) return;
|
|
8892
|
+
if (p.type === "Identifier") declared.add(p.name);
|
|
8893
|
+
else if (p.type === "ObjectPattern")
|
|
8894
|
+
for (const prop of p.properties)
|
|
8895
|
+
addPattern(prop.type === "RestElement" ? prop.argument : prop.value);
|
|
8896
|
+
else if (p.type === "ArrayPattern")
|
|
8897
|
+
for (const el of p.elements) addPattern(el);
|
|
8898
|
+
else if (p.type === "RestElement") addPattern(p.argument);
|
|
8899
|
+
else if (p.type === "AssignmentPattern") addPattern(p.left);
|
|
8900
|
+
};
|
|
8901
|
+
fullAncestor(ast, (node, _state, ancestors) => {
|
|
8902
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
8903
|
+
if (node.id) declared.add(node.id.name);
|
|
8904
|
+
for (const p of node.params) addPattern(p);
|
|
8905
|
+
} else if (node.type === "VariableDeclarator") addPattern(node.id);
|
|
8906
|
+
else if (node.type === "ClassDeclaration" && node.id)
|
|
8907
|
+
declared.add(node.id.name);
|
|
8908
|
+
else if (node.type === "CatchClause") addPattern(node.param);
|
|
8909
|
+
else if (node.type === "Identifier") {
|
|
8910
|
+
const parent = ancestors[ancestors.length - 2];
|
|
8911
|
+
if (!parent) return;
|
|
8912
|
+
if (parent.type === "MemberExpression" && parent.property === node && !parent.computed)
|
|
8913
|
+
return;
|
|
8914
|
+
if ((parent.type === "Property" || parent.type === "MethodDefinition" || parent.type === "PropertyDefinition") && parent.key === node && !parent.computed && !(parent.type === "Property" && parent.shorthand))
|
|
8915
|
+
return;
|
|
8916
|
+
if (parent.type === "LabeledStatement" || parent.type === "BreakStatement" || parent.type === "ContinueStatement")
|
|
8917
|
+
return;
|
|
8918
|
+
if (parent.type === "MetaProperty") return;
|
|
8919
|
+
read.add(node.name);
|
|
8920
|
+
}
|
|
8921
|
+
});
|
|
8922
|
+
return [...read].filter(
|
|
8923
|
+
(n) => !declared.has(n) && !JS_GLOBALS.has(n) && !PAGE_GLOBALS.has(n)
|
|
8924
|
+
).sort();
|
|
8925
|
+
}
|
|
8926
|
+
function buildProgram(program, file, text) {
|
|
8927
|
+
const problems = [];
|
|
8928
|
+
const sources = {};
|
|
8929
|
+
const stringify = (fn, name) => {
|
|
8930
|
+
if (fn === void 0) return void 0;
|
|
8931
|
+
if (typeof fn === "string") return fn;
|
|
8932
|
+
if (typeof fn !== "function") {
|
|
8933
|
+
problems.push({
|
|
8934
|
+
fn: name,
|
|
8935
|
+
message: `must be a function, got ${typeof fn}`
|
|
8936
|
+
});
|
|
8937
|
+
return void 0;
|
|
8938
|
+
}
|
|
8939
|
+
const raw = fn.toString();
|
|
8940
|
+
const src = functionSource(fn);
|
|
8941
|
+
const at2 = text.indexOf(raw);
|
|
8942
|
+
sources[name] = at2 >= 0 ? { file, line: text.slice(0, at2).split("\n").length } : null;
|
|
8943
|
+
let free;
|
|
8944
|
+
try {
|
|
8945
|
+
free = freeIdentifiers(src);
|
|
8946
|
+
} catch (e) {
|
|
8947
|
+
problems.push({
|
|
8948
|
+
fn: name,
|
|
8949
|
+
message: `does not parse as JavaScript the page can run: ${e.message}`
|
|
8950
|
+
});
|
|
8951
|
+
return src;
|
|
8952
|
+
}
|
|
8953
|
+
for (const n of free) {
|
|
8954
|
+
problems.push({
|
|
8955
|
+
fn: name,
|
|
8956
|
+
message: `reads \`${n}\` from outside the function; the string travels without it. Move it inside the function, or put its value in data and read ctx.data.`
|
|
8957
|
+
});
|
|
8958
|
+
}
|
|
8959
|
+
return src;
|
|
8960
|
+
};
|
|
8961
|
+
const out = {};
|
|
8962
|
+
for (const [key, value] of Object.entries(program)) {
|
|
8963
|
+
if (FUNCTION_KEYS.includes(key)) {
|
|
8964
|
+
const s = stringify(value, key);
|
|
8965
|
+
if (s !== void 0) out[key] = s;
|
|
8966
|
+
} else if (key === "stack" && Array.isArray(value)) {
|
|
8967
|
+
out.stack = value.map((entry, i) => {
|
|
8968
|
+
const e = { ...entry };
|
|
8969
|
+
for (const k of FUNCTION_KEYS) {
|
|
8970
|
+
if (k in e) {
|
|
8971
|
+
const s = stringify(e[k], `stack[${String(entry.id ?? i)}].${k}`);
|
|
8972
|
+
if (s !== void 0) e[k] = s;
|
|
8973
|
+
}
|
|
8974
|
+
}
|
|
8975
|
+
return e;
|
|
8976
|
+
});
|
|
8977
|
+
} else {
|
|
8978
|
+
out[key] = value;
|
|
8979
|
+
}
|
|
8980
|
+
}
|
|
8981
|
+
for (const key of Object.keys(out)) {
|
|
8982
|
+
if (FUNCTION_KEYS.includes(key) || key === "stack")
|
|
8983
|
+
continue;
|
|
8984
|
+
const bad = nonJson(out[key], key);
|
|
8985
|
+
if (bad) problems.push({ fn: key, message: bad });
|
|
8986
|
+
}
|
|
8987
|
+
return { config: out, sources, problems };
|
|
8988
|
+
}
|
|
8989
|
+
function nonJson(value, path) {
|
|
8990
|
+
if (value === null) return null;
|
|
8991
|
+
const t = typeof value;
|
|
8992
|
+
if (t === "function")
|
|
8993
|
+
return `${path} is a function; only the hooks (${FUNCTION_KEYS.join(", ")}) may be`;
|
|
8994
|
+
if (t === "number" && !Number.isFinite(value))
|
|
8995
|
+
return `${path} is ${String(value)}, which JSON cannot carry`;
|
|
8996
|
+
if (t === "undefined" || t === "symbol" || t === "bigint")
|
|
8997
|
+
return `${path} is ${t}, which JSON cannot carry`;
|
|
8998
|
+
if (Array.isArray(value)) {
|
|
8999
|
+
for (let i = 0; i < value.length; i++) {
|
|
9000
|
+
const r = nonJson(value[i], `${path}[${i}]`);
|
|
9001
|
+
if (r) return r;
|
|
9002
|
+
}
|
|
9003
|
+
return null;
|
|
9004
|
+
}
|
|
9005
|
+
if (t === "object") {
|
|
9006
|
+
const proto = Object.getPrototypeOf(value);
|
|
9007
|
+
if (proto !== Object.prototype && proto !== null)
|
|
9008
|
+
return `${path} is a ${proto?.constructor?.name ?? "class"} instance; use plain objects and arrays`;
|
|
9009
|
+
for (const [k, v] of Object.entries(value)) {
|
|
9010
|
+
const r = nonJson(v, `${path}.${k}`);
|
|
9011
|
+
if (r) return r;
|
|
9012
|
+
}
|
|
9013
|
+
}
|
|
9014
|
+
return null;
|
|
9015
|
+
}
|
|
8819
9016
|
var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9017
|
+
async function cmdBuild(argv) {
|
|
9018
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
|
|
9019
|
+
const source = positionals[0];
|
|
9020
|
+
if (!source)
|
|
9021
|
+
throw new UsageError("vos build <program.mjs> [--out config.json] [--json]");
|
|
9022
|
+
const r = createReporter(flags.json === true);
|
|
9023
|
+
const file = resolve7(source);
|
|
9024
|
+
const text = readFileSync4(file, "utf8");
|
|
9025
|
+
const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
9026
|
+
const program = mod.default ?? mod.program;
|
|
9027
|
+
if (!program || typeof program !== "object")
|
|
9028
|
+
throw new UsageError(
|
|
9029
|
+
`${source} exports no program: \`export default { version: 2, duration, camera, createContent, createTimeline, \u2026 }\``
|
|
9030
|
+
);
|
|
9031
|
+
const built = buildProgram(program, basename6(file), text);
|
|
9032
|
+
for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
|
|
9033
|
+
if (built.problems.length) {
|
|
9034
|
+
r.done(
|
|
9035
|
+
{ ok: false, problems: built.problems },
|
|
9036
|
+
`${source}: ${built.problems.length} problem${built.problems.length === 1 ? "" : "s"}, nothing written`
|
|
9037
|
+
);
|
|
9038
|
+
return EXIT_ERROR;
|
|
9039
|
+
}
|
|
9040
|
+
const out = resolve7(
|
|
9041
|
+
strFlag(flags, "out") ?? join19(dirname5(file), "config.json")
|
|
9042
|
+
);
|
|
9043
|
+
writeFileSync2(out, JSON.stringify(built.config, null, 2) + "\n");
|
|
9044
|
+
const sidecar = out.replace(/\.json$/, "") + ".sources.json";
|
|
9045
|
+
writeFileSync2(sidecar, JSON.stringify(built.sources, null, 2) + "\n");
|
|
9046
|
+
for (const [fn, at2] of Object.entries(built.sources)) {
|
|
9047
|
+
if (at2) r.log(` ${fn} \u2192 ${at2.file}:${at2.line}`);
|
|
9048
|
+
}
|
|
9049
|
+
r.done(
|
|
9050
|
+
{ ok: true, out, sources: sidecar, functions: Object.keys(built.sources) },
|
|
9051
|
+
`Wrote ${out} (${Object.keys(built.sources).length} functions; an engine error at fn:N is that function's line N, from the line above)`
|
|
9052
|
+
);
|
|
9053
|
+
return EXIT_OK;
|
|
9054
|
+
}
|
|
9055
|
+
|
|
9056
|
+
// src/plugin/compare.ts
|
|
9057
|
+
import { spawnSync } from "child_process";
|
|
9058
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
9059
|
+
import { join as join20, resolve as resolve8 } from "path";
|
|
9060
|
+
var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9061
|
+
function sampleTimes(duration, every) {
|
|
9062
|
+
const step = every > 0 ? every : 1;
|
|
9063
|
+
const out = [];
|
|
9064
|
+
for (let t = step / 2; t < duration; t += step) out.push(Number(t.toFixed(3)));
|
|
9065
|
+
return out.length ? out : [Number((duration / 2).toFixed(3))];
|
|
9066
|
+
}
|
|
9067
|
+
function parseSsim(stderr) {
|
|
9068
|
+
const m = /All:([0-9.]+)/.exec(stderr);
|
|
9069
|
+
return m ? Number(m[1]) : null;
|
|
9070
|
+
}
|
|
9071
|
+
function compareVerdict(scores, threshold) {
|
|
9072
|
+
const values = scores.map((s) => s.ssim);
|
|
9073
|
+
const min = values.length ? Math.min(...values) : 0;
|
|
9074
|
+
const mean = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
|
9075
|
+
const failing = scores.filter((s) => s.ssim < threshold).map((s) => s.t);
|
|
9076
|
+
return { min, mean, failing, pass: failing.length === 0 && values.length > 0 };
|
|
9077
|
+
}
|
|
9078
|
+
function ffmpeg(args) {
|
|
9079
|
+
const res = spawnSync("ffmpeg", ["-hide_banner", ...args], {
|
|
9080
|
+
encoding: "utf8"
|
|
9081
|
+
});
|
|
9082
|
+
return { ok: res.status === 0, stderr: res.stderr ?? "" };
|
|
9083
|
+
}
|
|
9084
|
+
function requireFfmpeg() {
|
|
9085
|
+
const res = spawnSync("ffmpeg", ["-version"], { encoding: "utf8" });
|
|
9086
|
+
if (res.status !== 0)
|
|
9087
|
+
throw new UsageError(
|
|
9088
|
+
"vos compare reads frames with ffmpeg, and none is on PATH: install it (brew install ffmpeg, apt install ffmpeg) and run again"
|
|
9089
|
+
);
|
|
9090
|
+
}
|
|
9091
|
+
function probeSize(video) {
|
|
9092
|
+
const res = spawnSync(
|
|
9093
|
+
"ffprobe",
|
|
9094
|
+
[
|
|
9095
|
+
"-v",
|
|
9096
|
+
"error",
|
|
9097
|
+
"-select_streams",
|
|
9098
|
+
"v:0",
|
|
9099
|
+
"-show_entries",
|
|
9100
|
+
"stream=width,height",
|
|
9101
|
+
"-of",
|
|
9102
|
+
"csv=p=0",
|
|
9103
|
+
video
|
|
9104
|
+
],
|
|
9105
|
+
{ encoding: "utf8" }
|
|
9106
|
+
);
|
|
9107
|
+
const m = /(\d+),(\d+)/.exec(res.stdout ?? "");
|
|
9108
|
+
return m ? { width: Number(m[1]), height: Number(m[2]) } : null;
|
|
9109
|
+
}
|
|
9110
|
+
async function cmdCompare(argv) {
|
|
9111
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
|
|
9112
|
+
const source = positionals[0];
|
|
9113
|
+
const against = strFlag(flags, "against");
|
|
9114
|
+
if (!source || !against)
|
|
9115
|
+
throw new UsageError(
|
|
9116
|
+
"vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--width W --height H] [--threshold 0.95] [--out compare] [--json]"
|
|
9117
|
+
);
|
|
9118
|
+
requireFfmpeg();
|
|
9119
|
+
const r = createReporter(flags.json === true);
|
|
9120
|
+
const ref = resolve8(against);
|
|
9121
|
+
const loaded = await loadVosConfig(source);
|
|
9122
|
+
for (const w of loaded.warnings) r.log(`note: ${w}`);
|
|
9123
|
+
const config = loaded.config;
|
|
9124
|
+
const duration = configDuration(config) ?? 5;
|
|
9125
|
+
const probed = probeSize(ref);
|
|
9126
|
+
const half = probed && probed.width > 1280 ? 2 : 1;
|
|
9127
|
+
const width = Number(
|
|
9128
|
+
strFlag(flags, "width") ?? (probed ? probed.width / half : 960)
|
|
9129
|
+
);
|
|
9130
|
+
const height = Number(
|
|
9131
|
+
strFlag(flags, "height") ?? (probed ? probed.height / half : 540)
|
|
9132
|
+
);
|
|
9133
|
+
const threshold = Number(strFlag(flags, "threshold") ?? 0.95);
|
|
9134
|
+
const timesRaw = strFlag(flags, "times");
|
|
9135
|
+
const times = timesRaw ? parseTimes(timesRaw, duration) : sampleTimes(duration, Number(strFlag(flags, "every") ?? 1));
|
|
9136
|
+
const outDir = resolve8(strFlag(flags, "out") ?? "compare");
|
|
9137
|
+
mkdirSync2(outDir, { recursive: true });
|
|
9138
|
+
const browser = await launchBrowser();
|
|
9139
|
+
const scores = [];
|
|
9140
|
+
try {
|
|
9141
|
+
for (const t of times) {
|
|
9142
|
+
const tag = t.toFixed(2);
|
|
9143
|
+
const refPng = join20(outDir, `src-${tag}.png`);
|
|
9144
|
+
const vosPng = join20(outDir, `vos-${tag}.png`);
|
|
9145
|
+
const sheet = join20(outDir, `sheet-${tag}.png`);
|
|
9146
|
+
const grab = ffmpeg([
|
|
9147
|
+
"-v",
|
|
9148
|
+
"error",
|
|
9149
|
+
"-y",
|
|
9150
|
+
"-ss",
|
|
9151
|
+
String(t),
|
|
9152
|
+
"-i",
|
|
9153
|
+
ref,
|
|
9154
|
+
"-frames:v",
|
|
9155
|
+
"1",
|
|
9156
|
+
"-vf",
|
|
9157
|
+
`scale=${width}:${height}`,
|
|
9158
|
+
refPng
|
|
9159
|
+
]);
|
|
9160
|
+
if (!grab.ok)
|
|
9161
|
+
throw new Error(
|
|
9162
|
+
`could not read ${against} at ${tag}s: ${grab.stderr.trim()}`
|
|
9163
|
+
);
|
|
9164
|
+
const still = await renderStill(browser, {
|
|
9165
|
+
config,
|
|
9166
|
+
width,
|
|
9167
|
+
height,
|
|
9168
|
+
time: t
|
|
9169
|
+
});
|
|
9170
|
+
writeFileSync3(vosPng, await reencodeStill(browser, still.bytes, "png"));
|
|
9171
|
+
const pair = ["-i", refPng, "-i", vosPng];
|
|
9172
|
+
const ssim = parseSsim(
|
|
9173
|
+
ffmpeg([
|
|
9174
|
+
...pair,
|
|
9175
|
+
"-lavfi",
|
|
9176
|
+
"[0:v]format=rgb24[x];[1:v]format=rgb24[y];[x][y]ssim",
|
|
9177
|
+
"-f",
|
|
9178
|
+
"null",
|
|
9179
|
+
"-"
|
|
9180
|
+
]).stderr
|
|
9181
|
+
);
|
|
9182
|
+
ffmpeg([
|
|
9183
|
+
"-v",
|
|
9184
|
+
"error",
|
|
9185
|
+
"-y",
|
|
9186
|
+
...pair,
|
|
9187
|
+
"-filter_complex",
|
|
9188
|
+
"[0:v]format=gbrp[x];[1:v]format=gbrp[y];[x]split[x1][x2];[y]split[y1][y2];[x1][y1]blend=all_mode=difference[d];[x2][y2][d]hstack=inputs=3,format=rgb24",
|
|
9189
|
+
sheet
|
|
9190
|
+
]);
|
|
9191
|
+
const score = { t, ssim: ssim ?? 0, sheet };
|
|
9192
|
+
scores.push(score);
|
|
9193
|
+
const mark = score.ssim < threshold ? " \u2190 under" : "";
|
|
9194
|
+
r.log(`t=${tag}s SSIM ${score.ssim.toFixed(3)} ${sheet}${mark}`);
|
|
9195
|
+
for (const w of still.pageErrors)
|
|
9196
|
+
r.log(`warn: t=${tag}s the page threw: ${w}`);
|
|
9197
|
+
}
|
|
9198
|
+
} finally {
|
|
9199
|
+
await browser.close();
|
|
9200
|
+
}
|
|
9201
|
+
const overview = join20(outDir, "overview.png");
|
|
9202
|
+
if (scores.length) {
|
|
9203
|
+
const inputs = scores.flatMap((s) => ["-i", s.sheet]);
|
|
9204
|
+
const scaled = scores.map((_, i) => `[${i}:v]scale=iw/3:-2[s${i}]`).join(";");
|
|
9205
|
+
const stack = scores.map((_, i) => `[s${i}]`).join("");
|
|
9206
|
+
ffmpeg([
|
|
9207
|
+
"-v",
|
|
9208
|
+
"error",
|
|
9209
|
+
"-y",
|
|
9210
|
+
...inputs,
|
|
9211
|
+
"-filter_complex",
|
|
9212
|
+
scores.length > 1 ? `${scaled};${stack}vstack=inputs=${scores.length}` : `${scaled}`.replace("[s0]", ""),
|
|
9213
|
+
overview
|
|
9214
|
+
]);
|
|
9215
|
+
}
|
|
9216
|
+
const verdict = compareVerdict(scores, threshold);
|
|
9217
|
+
const report = {
|
|
9218
|
+
against: ref,
|
|
9219
|
+
width,
|
|
9220
|
+
height,
|
|
9221
|
+
threshold,
|
|
9222
|
+
...verdict,
|
|
9223
|
+
frames: scores,
|
|
9224
|
+
overview
|
|
9225
|
+
};
|
|
9226
|
+
writeFileSync3(
|
|
9227
|
+
join20(outDir, "report.json"),
|
|
9228
|
+
JSON.stringify(report, null, 2) + "\n"
|
|
9229
|
+
);
|
|
9230
|
+
r.done(
|
|
9231
|
+
report,
|
|
9232
|
+
`${scores.length} frames: mean SSIM ${verdict.mean.toFixed(3)}, lowest ${verdict.min.toFixed(3)}${verdict.failing.length ? `; under ${threshold} at ${verdict.failing.map((t) => `${t}s`).join(", ")}: look at those sheets` : `; every frame at or above ${threshold}`}. Sheets (source | vos | difference) and overview.png in ${outDir}`
|
|
9233
|
+
);
|
|
9234
|
+
return verdict.pass ? EXIT_OK : EXIT_ERROR;
|
|
9235
|
+
}
|
|
9236
|
+
|
|
9237
|
+
// src/plugin/port.ts
|
|
9238
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
9239
|
+
import { dirname as dirname8, join as join23, resolve as resolve11 } from "path";
|
|
9240
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
9241
|
+
|
|
9242
|
+
// src/plugin/portInventory.ts
|
|
9243
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
9244
|
+
import {
|
|
9245
|
+
existsSync as existsSync17,
|
|
9246
|
+
mkdirSync as mkdirSync3,
|
|
9247
|
+
readFileSync as readFileSync5,
|
|
9248
|
+
readdirSync as readdirSync4,
|
|
9249
|
+
statSync as statSync5
|
|
9250
|
+
} from "fs";
|
|
9251
|
+
import { basename as basename7, dirname as dirname6, join as join21, relative as relative2, resolve as resolve9 } from "path";
|
|
9252
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
9253
|
+
import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
|
|
9254
|
+
function detectEngine(dir) {
|
|
9255
|
+
if (existsSync17(join21(dir, "hyperframes.json"))) return "hyperframes";
|
|
9256
|
+
const pkg = join21(dir, "package.json");
|
|
9257
|
+
if (existsSync17(pkg)) {
|
|
9258
|
+
try {
|
|
9259
|
+
const p = JSON.parse(readFileSync5(pkg, "utf8"));
|
|
9260
|
+
const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
|
|
9261
|
+
if ("remotion" in deps) return "remotion";
|
|
9262
|
+
if ("hyperframes" in deps) return "hyperframes";
|
|
9263
|
+
if (/hyperframes/.test(JSON.stringify(p.scripts ?? {})))
|
|
9264
|
+
return "hyperframes";
|
|
9265
|
+
} catch {
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
return "page";
|
|
9269
|
+
}
|
|
9270
|
+
function sourcePage(source) {
|
|
9271
|
+
if (!existsSync17(source)) return null;
|
|
9272
|
+
if (statSync5(source).isFile()) return /\.html?$/i.test(source) ? source : null;
|
|
9273
|
+
const index = join21(source, "index.html");
|
|
9274
|
+
return existsSync17(index) ? index : null;
|
|
9275
|
+
}
|
|
9276
|
+
function findRender(dir) {
|
|
9277
|
+
const videos = (d) => existsSync17(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join21(d, f)) : [];
|
|
9278
|
+
for (const d of [join21(dir, "renders"), join21(dir, "out"), dir]) {
|
|
9279
|
+
const found = videos(d);
|
|
9280
|
+
if (found.length) return found[0];
|
|
9281
|
+
}
|
|
9282
|
+
return null;
|
|
9283
|
+
}
|
|
9284
|
+
function scriptStringsOf(code) {
|
|
9285
|
+
const out = /* @__PURE__ */ new Set();
|
|
9286
|
+
for (const m of code.matchAll(/(['"`])((?:(?!\1)[^\\\n]|\\.){2,60})\1/g)) {
|
|
9287
|
+
const s = m[2];
|
|
9288
|
+
if (!/[A-Za-z]{2}/.test(s)) continue;
|
|
9289
|
+
if (/["'`]/.test(s)) continue;
|
|
9290
|
+
if (/\b[a-zA-Z_$][\w$]*\s*:\s*[\w$.]+\s*,/.test(s)) continue;
|
|
9291
|
+
if (/^-?[\d.]+(px|%|ms|s|deg|em|rem|vh|vw)?$/.test(s)) continue;
|
|
9292
|
+
if (/^[a-z]+\/[a-z0-9.+-]+$/.test(s)) continue;
|
|
9293
|
+
if (/^(source|destination)-(over|in|out|atop)$|^(lighter|copy|xor|multiply|screen|overlay|darken|lighten|difference|exclusion)$/.test(
|
|
9294
|
+
s
|
|
9295
|
+
))
|
|
9296
|
+
continue;
|
|
9297
|
+
if (/^[A-Za-z ]{4}$/.test(s) && /^(RIFF|WAVE|fmt |data|LIST)$/.test(s))
|
|
9298
|
+
continue;
|
|
9299
|
+
if (/[(){};=<>]|\$\{|^\.?\/|https?:|\.(js|css|png|jpe?g|mp[34]|wav|woff2?)$/i.test(
|
|
9300
|
+
s
|
|
9301
|
+
))
|
|
9302
|
+
continue;
|
|
9303
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(s) && !/[A-Z]{2}/.test(s)) continue;
|
|
9304
|
+
if (/^(rgba?|hsla?)\(|^#[0-9a-f]{3,8}$/i.test(s)) continue;
|
|
9305
|
+
out.add(s);
|
|
9306
|
+
}
|
|
9307
|
+
return [...out].slice(0, 80);
|
|
9308
|
+
}
|
|
9309
|
+
function scriptScenes(ids, code, duration) {
|
|
9310
|
+
const found = /* @__PURE__ */ new Map();
|
|
9311
|
+
for (const id of ids) {
|
|
9312
|
+
const re = new RegExp(
|
|
9313
|
+
`\\.(?:set|to|fromTo)\\(\\s*["'\`]#${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'\`][^)]*?(?:opacity|autoAlpha)\\s*:\\s*1[^)]*?,\\s*([\\d.]+)\\s*\\)`
|
|
9314
|
+
);
|
|
9315
|
+
const m = re.exec(code);
|
|
9316
|
+
if (m) found.set(id, Number(m[1]));
|
|
9317
|
+
else if (!found.size && ids[0] === id) found.set(id, 0);
|
|
9318
|
+
}
|
|
9319
|
+
for (const m of code.matchAll(
|
|
9320
|
+
/addLabel\(\s*["'`]([^"'`]+)["'`]\s*,\s*([\d.]+)/g
|
|
9321
|
+
)) {
|
|
9322
|
+
if (!found.has(m[1])) found.set(m[1], Number(m[2]));
|
|
9323
|
+
}
|
|
9324
|
+
const sorted = [...found.entries()].sort((a, b) => a[1] - b[1]);
|
|
9325
|
+
return sorted.map(([name, start], i) => {
|
|
9326
|
+
const next = sorted[i + 1]?.[1] ?? duration;
|
|
9327
|
+
return {
|
|
9328
|
+
name,
|
|
9329
|
+
start,
|
|
9330
|
+
duration: next == null ? null : Math.round((next - start) * 1e3) / 1e3
|
|
9331
|
+
};
|
|
9332
|
+
});
|
|
9333
|
+
}
|
|
9334
|
+
function scriptFontFamilies(code) {
|
|
9335
|
+
const out = /* @__PURE__ */ new Set();
|
|
9336
|
+
const generic = /,\s*(?:sans-serif|serif|monospace|cursive|system-ui)\b/;
|
|
9337
|
+
const shorthand = /^(?:(?:italic|oblique|normal|bold|bolder|lighter|small-caps|\d{3})\s+)*[\d.]+(?:px|pt|em|rem|%)(?:\/[\d.]+\w*)?\s+/;
|
|
9338
|
+
for (const m of code.matchAll(/(['"`])((?:\\.|(?!\1).)*?)\1/g)) {
|
|
9339
|
+
const s = m[2];
|
|
9340
|
+
if (!generic.test(s)) continue;
|
|
9341
|
+
const first = s.replace(shorthand, "").split(",")[0].trim().replace(/^["']|["']$/g, "");
|
|
9342
|
+
if (first && !/^(sans-serif|serif|monospace|cursive|system-ui)$/.test(first))
|
|
9343
|
+
out.add(first);
|
|
9344
|
+
}
|
|
9345
|
+
return [...out];
|
|
9346
|
+
}
|
|
9347
|
+
function catalogFamily2(family) {
|
|
9348
|
+
const clean = family.trim().replace(/^['"]|['"]$/g, "");
|
|
9349
|
+
return findFontFamily3(clean)?.family ?? null;
|
|
9350
|
+
}
|
|
9351
|
+
function probe(video) {
|
|
9352
|
+
const res = spawnSync2(
|
|
9353
|
+
"ffprobe",
|
|
9354
|
+
[
|
|
9355
|
+
"-v",
|
|
9356
|
+
"error",
|
|
9357
|
+
"-select_streams",
|
|
9358
|
+
"v:0",
|
|
9359
|
+
"-show_entries",
|
|
9360
|
+
"stream=r_frame_rate:format=duration",
|
|
9361
|
+
"-of",
|
|
9362
|
+
"json",
|
|
9363
|
+
video
|
|
9364
|
+
],
|
|
9365
|
+
{ encoding: "utf8" }
|
|
9366
|
+
);
|
|
9367
|
+
try {
|
|
9368
|
+
const j = JSON.parse(res.stdout ?? "{}");
|
|
9369
|
+
const [n, d] = String(j.streams?.[0]?.r_frame_rate ?? "").split("/").map(Number);
|
|
9370
|
+
const fps = n && d ? Math.round(n / d * 100) / 100 : null;
|
|
9371
|
+
const duration = Number(j.format?.duration);
|
|
9372
|
+
return { fps, duration: Number.isFinite(duration) ? duration : null };
|
|
9373
|
+
} catch {
|
|
9374
|
+
return { fps: null, duration: null };
|
|
9375
|
+
}
|
|
9376
|
+
}
|
|
9377
|
+
var READ_PAGE = `(() => {
|
|
9378
|
+
const root = document.querySelector('[data-composition-id], [data-width][data-height]') || document.body
|
|
9379
|
+
const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null }
|
|
9380
|
+
const canvases = [...document.querySelectorAll('canvas')].map((c) => ({ width: c.width, height: c.height }))
|
|
9381
|
+
const biggest = canvases.slice().sort((a, b) => b.width * b.height - a.width * a.height)[0]
|
|
9382
|
+
const width = num(root.dataset && root.dataset.width) || (biggest && biggest.width) || window.innerWidth
|
|
9383
|
+
const height = num(root.dataset && root.dataset.height) || (biggest && biggest.height) || window.innerHeight
|
|
9384
|
+
const rootRect = root.getBoundingClientRect()
|
|
9385
|
+
const sx = rootRect.width ? width / rootRect.width : 1
|
|
9386
|
+
const sy = rootRect.height ? height / rootRect.height : 1
|
|
9387
|
+
|
|
9388
|
+
const texts = []
|
|
9389
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
|
|
9390
|
+
const seen = new Set()
|
|
9391
|
+
let n = 0
|
|
9392
|
+
while (walker.nextNode()) {
|
|
9393
|
+
const node = walker.currentNode
|
|
9394
|
+
const text = (node.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
9395
|
+
const el = node.parentElement
|
|
9396
|
+
if (!text || !el || ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TITLE'].includes(el.tagName)) continue
|
|
9397
|
+
if (seen.has(el)) continue
|
|
9398
|
+
seen.add(el)
|
|
9399
|
+
const cs = getComputedStyle(el)
|
|
9400
|
+
const r = el.getBoundingClientRect()
|
|
9401
|
+
const box = r.width && r.height ? {
|
|
9402
|
+
x: Math.round((r.left - rootRect.left) * sx), y: Math.round((r.top - rootRect.top) * sy),
|
|
9403
|
+
width: Math.round(r.width * sx), height: Math.round(r.height * sy) } : null
|
|
9404
|
+
const ls = parseFloat(cs.letterSpacing)
|
|
9405
|
+
el.setAttribute('data-vos-inv', String(texts.length))
|
|
9406
|
+
const sceneEl = el.closest('.scene, [data-scene], section[id]')
|
|
9407
|
+
texts.push({
|
|
9408
|
+
id: el.id || (el.className && typeof el.className === 'string' ? el.className.split(' ')[0] : '') || el.tagName.toLowerCase() + '-' + (n++),
|
|
9409
|
+
text, tag: el.tagName.toLowerCase(), box, scene: sceneEl && sceneEl.id ? sceneEl.id : null,
|
|
9410
|
+
font: { family: cs.fontFamily.split(',')[0].trim().replace(/^["']|["']$/g, ''),
|
|
9411
|
+
size: Math.round(parseFloat(cs.fontSize) * sy), weight: Number(cs.fontWeight) || 400,
|
|
9412
|
+
style: cs.fontStyle, color: cs.color, letterSpacing: Number.isFinite(ls) ? Math.round(ls * sy * 100) / 100 : 0,
|
|
9413
|
+
transform: cs.textTransform },
|
|
9414
|
+
})
|
|
9415
|
+
}
|
|
9416
|
+
|
|
9417
|
+
const palette = {}
|
|
9418
|
+
const isColor = (v) => /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|oklch\\()/i.test(v)
|
|
9419
|
+
const scan = (style) => { for (let i = 0; i < style.length; i++) { const p = style[i]; if (p.startsWith('--')) { const v = style.getPropertyValue(p).trim(); if (isColor(v)) palette[p.slice(2)] = v } } }
|
|
9420
|
+
for (const sheet of document.styleSheets) {
|
|
9421
|
+
let rules
|
|
9422
|
+
try { rules = sheet.cssRules } catch (e) { continue }
|
|
9423
|
+
for (const rule of rules) if (rule.style) scan(rule.style)
|
|
9424
|
+
}
|
|
9425
|
+
scan(root.style)
|
|
9426
|
+
|
|
9427
|
+
const colors = new Set()
|
|
9428
|
+
const gaps = new Set()
|
|
9429
|
+
for (const el of document.querySelectorAll('body *')) {
|
|
9430
|
+
const cs = getComputedStyle(el)
|
|
9431
|
+
if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)') colors.add(cs.backgroundColor)
|
|
9432
|
+
if (cs.mixBlendMode && cs.mixBlendMode !== 'normal') gaps.add('mix-blend-mode: ' + cs.mixBlendMode)
|
|
9433
|
+
if (cs.clipPath && cs.clipPath !== 'none') gaps.add('clip-path')
|
|
9434
|
+
if (cs.maskImage && cs.maskImage !== 'none') gaps.add('mask')
|
|
9435
|
+
if (cs.overflow === 'hidden' && el.children.length && el.textContent.trim()) gaps.add('overflow: hidden (a text mask)')
|
|
9436
|
+
if (cs.filter && cs.filter !== 'none') gaps.add('filter: ' + cs.filter.split('(')[0])
|
|
9437
|
+
if (cs.backdropFilter && cs.backdropFilter !== 'none') gaps.add('backdrop-filter')
|
|
9438
|
+
if (cs.perspective && cs.perspective !== 'none') gaps.add('3D perspective')
|
|
9439
|
+
if (cs.webkitTextStrokeWidth && parseFloat(cs.webkitTextStrokeWidth) > 0) gaps.add('-webkit-text-stroke (a stroke: see the reference for its width)')
|
|
9440
|
+
}
|
|
9441
|
+
for (const t of texts) colors.add(t.font.color)
|
|
9442
|
+
|
|
9443
|
+
const media = []
|
|
9444
|
+
const attrs = (el) => ({ start: num(el.dataset.start), duration: num(el.dataset.duration), volume: num(el.dataset.volume), mediaStart: num(el.dataset.mediaStart) })
|
|
9445
|
+
for (const el of document.querySelectorAll('img')) media.push({ kind: 'image', src: el.getAttribute('src'), ...attrs(el) })
|
|
9446
|
+
for (const el of document.querySelectorAll('svg')) if (!el.closest('svg svg')) media.push({ kind: 'svg', src: null, ...attrs(el) })
|
|
9447
|
+
for (const el of document.querySelectorAll('video')) media.push({ kind: 'video', src: el.getAttribute('src') || (el.querySelector('source') && el.querySelector('source').getAttribute('src')), ...attrs(el) })
|
|
9448
|
+
for (const el of document.querySelectorAll('audio')) media.push({ kind: 'audio', src: el.getAttribute('src') || (el.querySelector('source') && el.querySelector('source').getAttribute('src')), ...attrs(el) })
|
|
9449
|
+
|
|
9450
|
+
const scenes = []
|
|
9451
|
+
for (const el of document.querySelectorAll('[data-start]')) {
|
|
9452
|
+
if (['AUDIO', 'VIDEO', 'IMG'].includes(el.tagName)) continue
|
|
9453
|
+
scenes.push({ name: el.id || el.dataset.compositionId || (typeof el.className === 'string' ? el.className.split(' ')[0] : '') || el.tagName.toLowerCase(), start: num(el.dataset.start) || 0, duration: num(el.dataset.duration) })
|
|
9454
|
+
}
|
|
9455
|
+
const sceneEls = [...document.querySelectorAll('.scene, [data-scene], section[id]')]
|
|
9456
|
+
.filter((el) => el.id)
|
|
9457
|
+
.map((el) => el.id)
|
|
9458
|
+
let variables = {}
|
|
9459
|
+
try { variables = JSON.parse(root.dataset.compositionVariables || '{}') } catch (e) {}
|
|
9460
|
+
const scriptText = [...document.querySelectorAll('script:not([src])')].map((s) => s.textContent).join('\\n')
|
|
9461
|
+
return {
|
|
9462
|
+
width, height, fps: num(root.dataset && root.dataset.fps), duration: num(root.dataset && root.dataset.duration),
|
|
9463
|
+
texts, palette, colors: [...colors], gaps: [...gaps], media, canvases, scenes, sceneEls, variables, scriptText,
|
|
9464
|
+
}
|
|
9465
|
+
})()`;
|
|
9466
|
+
var SEEK_AND_READ = `((args) => {
|
|
9467
|
+
const tls = window.__timelines ? Object.values(window.__timelines) : []
|
|
9468
|
+
if (!tls.length) return null
|
|
9469
|
+
for (const tl of tls) { try { tl.pause(); tl.seek(args.t, false) } catch (e) {} }
|
|
9470
|
+
const root = document.querySelector('[data-composition-id], [data-width][data-height]') || document.body
|
|
9471
|
+
const rr = root.getBoundingClientRect()
|
|
9472
|
+
const sx = rr.width ? args.width / rr.width : 1
|
|
9473
|
+
const sy = rr.height ? args.height / rr.height : 1
|
|
9474
|
+
const out = {}
|
|
9475
|
+
for (const i of args.indices) {
|
|
9476
|
+
const el = document.querySelector('[data-vos-inv="' + i + '"]')
|
|
9477
|
+
if (!el) continue
|
|
9478
|
+
const r = el.getBoundingClientRect()
|
|
9479
|
+
if (r.width && r.height) out[i] = { x: Math.round((r.left - rr.left) * sx), y: Math.round((r.top - rr.top) * sy), width: Math.round(r.width * sx), height: Math.round(r.height * sy) }
|
|
9480
|
+
}
|
|
9481
|
+
return out
|
|
9482
|
+
})`;
|
|
9483
|
+
async function readInventory(source, opts) {
|
|
9484
|
+
const abs = resolve9(source);
|
|
9485
|
+
const dir = existsSync17(abs) && statSync5(abs).isDirectory() ? abs : dirname6(abs);
|
|
9486
|
+
const engine = detectEngine(dir);
|
|
9487
|
+
const page = sourcePage(abs);
|
|
9488
|
+
const render = opts.render === void 0 ? findRender(dir) : opts.render;
|
|
9489
|
+
let read = {
|
|
9490
|
+
width: 1920,
|
|
9491
|
+
height: 1080,
|
|
9492
|
+
fps: null,
|
|
9493
|
+
duration: null,
|
|
9494
|
+
texts: [],
|
|
9495
|
+
palette: {},
|
|
9496
|
+
colors: [],
|
|
9497
|
+
gaps: [],
|
|
9498
|
+
media: [],
|
|
9499
|
+
canvases: [],
|
|
9500
|
+
scenes: [],
|
|
9501
|
+
variables: {},
|
|
9502
|
+
scriptText: ""
|
|
9503
|
+
};
|
|
9504
|
+
const fromRender = render ? probe(render) : { fps: null, duration: null };
|
|
9505
|
+
let fps = fromRender.fps;
|
|
9506
|
+
let duration = fromRender.duration;
|
|
9507
|
+
let settled = 0;
|
|
9508
|
+
if (page) {
|
|
9509
|
+
const browser = await launchBrowser();
|
|
9510
|
+
try {
|
|
9511
|
+
const ctx = await browser.newContext({
|
|
9512
|
+
viewport: { width: 1920, height: 1080 }
|
|
9513
|
+
});
|
|
9514
|
+
const p = await ctx.newPage();
|
|
9515
|
+
await p.addInitScript("window.__timelines = window.__timelines || {}");
|
|
9516
|
+
await p.goto(pathToFileURL2(page).href, { waitUntil: "load" });
|
|
9517
|
+
await p.evaluate("document.fonts && document.fonts.ready");
|
|
9518
|
+
await p.waitForTimeout(300);
|
|
9519
|
+
read = await p.evaluate(READ_PAGE);
|
|
9520
|
+
fps = read.fps ?? fps;
|
|
9521
|
+
duration = read.duration ?? duration;
|
|
9522
|
+
const scripted = scriptScenes(
|
|
9523
|
+
read.sceneEls ?? [],
|
|
9524
|
+
read.scriptText ?? "",
|
|
9525
|
+
duration
|
|
9526
|
+
);
|
|
9527
|
+
if (scripted.length > (read.scenes?.length ?? 0)) read.scenes = scripted;
|
|
9528
|
+
for (const s of read.scenes) {
|
|
9529
|
+
const indices = read.texts.map((t2, i) => t2.scene === s.name ? i : -1).filter((i) => i >= 0);
|
|
9530
|
+
if (!indices.length) continue;
|
|
9531
|
+
const t = s.start + (s.duration ?? 1) * 0.75;
|
|
9532
|
+
const boxes = await p.evaluate(
|
|
9533
|
+
`${SEEK_AND_READ}(${JSON.stringify({ t, indices, width: read.width, height: read.height })})`
|
|
9534
|
+
);
|
|
9535
|
+
if (!boxes) break;
|
|
9536
|
+
for (const [i, box] of Object.entries(boxes)) {
|
|
9537
|
+
read.texts[Number(i)].box = box;
|
|
9538
|
+
settled++;
|
|
9539
|
+
}
|
|
9540
|
+
}
|
|
9541
|
+
await ctx.close();
|
|
9542
|
+
} finally {
|
|
9543
|
+
await browser.close();
|
|
9544
|
+
}
|
|
9545
|
+
} else if (engine === "remotion") {
|
|
9546
|
+
opts.log?.(
|
|
9547
|
+
"a Remotion project: its words and colours live in its source (src/), which the agent reads; the inventory records the render and its stills"
|
|
9548
|
+
);
|
|
9549
|
+
}
|
|
9550
|
+
if (settled)
|
|
9551
|
+
opts.log?.(
|
|
9552
|
+
`${settled} words laid out where they settle in their scene (the piece's own timelines, seeked)`
|
|
9553
|
+
);
|
|
9554
|
+
const families = /* @__PURE__ */ new Map();
|
|
9555
|
+
for (const t of read.texts) {
|
|
9556
|
+
const f = families.get(t.font.family) ?? {
|
|
9557
|
+
family: t.font.family,
|
|
9558
|
+
weights: [],
|
|
9559
|
+
italic: false,
|
|
9560
|
+
catalog: catalogFamily2(t.font.family)
|
|
9561
|
+
};
|
|
9562
|
+
if (!f.weights.includes(t.font.weight)) f.weights.push(t.font.weight);
|
|
9563
|
+
if (t.font.style === "italic") f.italic = true;
|
|
9564
|
+
families.set(t.font.family, f);
|
|
9565
|
+
}
|
|
9566
|
+
for (const family of scriptFontFamilies(read.scriptText ?? "")) {
|
|
9567
|
+
if (!families.has(family))
|
|
9568
|
+
families.set(family, {
|
|
9569
|
+
family,
|
|
9570
|
+
weights: [],
|
|
9571
|
+
italic: false,
|
|
9572
|
+
catalog: catalogFamily2(family)
|
|
9573
|
+
});
|
|
9574
|
+
}
|
|
9575
|
+
const stills = [];
|
|
9576
|
+
if (render && duration) {
|
|
9577
|
+
const has = spawnSync2("ffmpeg", ["-version"]).status === 0;
|
|
9578
|
+
if (has) {
|
|
9579
|
+
const refDir = join21(opts.outDir, "ref");
|
|
9580
|
+
mkdirSync3(refDir, { recursive: true });
|
|
9581
|
+
const times = read.scenes.length >= 3 ? read.scenes.map(
|
|
9582
|
+
(s) => s.start + (s.duration ?? 1) / 2
|
|
9583
|
+
) : Array.from(
|
|
9584
|
+
{ length: Math.max(1, Math.floor(duration)) },
|
|
9585
|
+
(_, i) => i + 0.5
|
|
9586
|
+
);
|
|
9587
|
+
for (const t of times.filter((t2) => t2 < duration)) {
|
|
9588
|
+
const file = join21(refDir, `src-${t.toFixed(2)}.png`);
|
|
9589
|
+
const ok = spawnSync2("ffmpeg", [
|
|
9590
|
+
"-v",
|
|
9591
|
+
"error",
|
|
9592
|
+
"-y",
|
|
9593
|
+
"-ss",
|
|
9594
|
+
String(t),
|
|
9595
|
+
"-i",
|
|
9596
|
+
render,
|
|
9597
|
+
"-frames:v",
|
|
9598
|
+
"1",
|
|
9599
|
+
"-vf",
|
|
9600
|
+
"scale=960:-2",
|
|
9601
|
+
file
|
|
9602
|
+
]).status === 0;
|
|
9603
|
+
if (ok)
|
|
9604
|
+
stills.push({
|
|
9605
|
+
t: Number(t.toFixed(2)),
|
|
9606
|
+
file: relative2(opts.outDir, file)
|
|
9607
|
+
});
|
|
9608
|
+
}
|
|
9609
|
+
} else {
|
|
9610
|
+
opts.log?.(
|
|
9611
|
+
"no ffmpeg on PATH: no reference stills (install it to get them)"
|
|
9612
|
+
);
|
|
9613
|
+
}
|
|
9614
|
+
}
|
|
9615
|
+
const scriptStrings = scriptStringsOf(read.scriptText);
|
|
9616
|
+
return {
|
|
9617
|
+
source: relative2(process.cwd(), abs) || basename7(abs),
|
|
9618
|
+
engine,
|
|
9619
|
+
width: read.width,
|
|
9620
|
+
height: read.height,
|
|
9621
|
+
fps,
|
|
9622
|
+
duration,
|
|
9623
|
+
scenes: read.scenes,
|
|
9624
|
+
texts: read.texts,
|
|
9625
|
+
palette: read.palette,
|
|
9626
|
+
colors: read.colors,
|
|
9627
|
+
fonts: [...families.values()],
|
|
9628
|
+
media: read.media,
|
|
9629
|
+
canvases: read.canvases,
|
|
9630
|
+
gaps: read.gaps,
|
|
9631
|
+
variables: read.variables,
|
|
9632
|
+
scriptStrings,
|
|
9633
|
+
render: render ? relative2(process.cwd(), render) : null,
|
|
9634
|
+
stills
|
|
9635
|
+
};
|
|
9636
|
+
}
|
|
9637
|
+
|
|
9638
|
+
// src/plugin/portScaffold.ts
|
|
9639
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
9640
|
+
import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
9641
|
+
import { basename as basename8, dirname as dirname7, join as join22, resolve as resolve10 } from "path";
|
|
9642
|
+
import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
|
|
9643
|
+
var DESIGN_HEIGHT = 1080;
|
|
9644
|
+
function toHex(css) {
|
|
9645
|
+
const s = css.trim();
|
|
9646
|
+
if (/^#[0-9a-f]{3,8}$/i.test(s)) {
|
|
9647
|
+
if (s.length === 4)
|
|
9648
|
+
return `#${[...s.slice(1)].map((c) => c + c).join("")}`.toLowerCase();
|
|
9649
|
+
return s.toLowerCase();
|
|
9650
|
+
}
|
|
9651
|
+
const m = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,/\s]+([\d.]+%?))?\s*\)$/i.exec(
|
|
9652
|
+
s
|
|
9653
|
+
);
|
|
9654
|
+
if (!m) return s;
|
|
9655
|
+
const hex2 = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
|
|
9656
|
+
let a = m[4] === void 0 ? 1 : m[4].endsWith("%") ? Number(m[4].slice(0, -1)) / 100 : Number(m[4]);
|
|
9657
|
+
if (!Number.isFinite(a)) a = 1;
|
|
9658
|
+
return `#${hex2(+m[1])}${hex2(+m[2])}${hex2(+m[3])}${a < 1 ? hex2(a * 255) : ""}`;
|
|
9659
|
+
}
|
|
9660
|
+
function keyFor(label, taken, prefix = "t") {
|
|
9661
|
+
const words = label.replace(/[^A-Za-z0-9]+/g, " ").trim().split(" ").filter(Boolean).slice(0, 4);
|
|
9662
|
+
let base = words.map(
|
|
9663
|
+
(w, i) => i === 0 ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1).toLowerCase()
|
|
9664
|
+
).join("");
|
|
9665
|
+
if (!base || /^[0-9]/.test(base)) base = prefix + (base || "");
|
|
9666
|
+
let key = base;
|
|
9667
|
+
for (let n = 2; taken.has(key); n++) key = `${base}${n}`;
|
|
9668
|
+
taken.add(key);
|
|
9669
|
+
return key;
|
|
9670
|
+
}
|
|
9671
|
+
function substituteFamily(family) {
|
|
9672
|
+
const f = family.toLowerCase();
|
|
9673
|
+
if (/mono|code|courier/.test(f)) return "JetBrains Mono";
|
|
9674
|
+
if (/serif/.test(f) && !/sans/.test(f)) return "Source Serif 4";
|
|
9675
|
+
if (/condensed|compressed|black|anton|bebas|oswald/.test(f)) return "Anton";
|
|
9676
|
+
return "Inter";
|
|
9677
|
+
}
|
|
9678
|
+
var fmtPct = (v) => `${Math.round(v * 1e4) / 100}%`;
|
|
9679
|
+
function planScaffold(inv, opts) {
|
|
9680
|
+
const taken = /* @__PURE__ */ new Set(["scenes", "bg"]);
|
|
9681
|
+
const data = {};
|
|
9682
|
+
const report = [];
|
|
9683
|
+
const scale = DESIGN_HEIGHT / (inv.height || DESIGN_HEIGHT);
|
|
9684
|
+
const colorKey = /* @__PURE__ */ new Map();
|
|
9685
|
+
for (const [name, value] of Object.entries(inv.palette)) {
|
|
9686
|
+
const key = keyFor(name, taken, "color");
|
|
9687
|
+
const hex2 = toHex(value);
|
|
9688
|
+
data[key] = hex2;
|
|
9689
|
+
if (!colorKey.has(hex2)) colorKey.set(hex2, key);
|
|
9690
|
+
}
|
|
9691
|
+
const colorOf = (css) => {
|
|
9692
|
+
const hex2 = toHex(css);
|
|
9693
|
+
const known = colorKey.get(hex2);
|
|
9694
|
+
if (known) return known;
|
|
9695
|
+
const key = keyFor(`color ${colorKey.size + 1}`, taken, "color");
|
|
9696
|
+
data[key] = hex2;
|
|
9697
|
+
colorKey.set(hex2, key);
|
|
9698
|
+
return key;
|
|
9699
|
+
};
|
|
9700
|
+
data.bg = inv.colors.length ? toHex(inv.colors[0]) : "#000000";
|
|
9701
|
+
const faces = /* @__PURE__ */ new Map();
|
|
9702
|
+
const faceFor = (t) => {
|
|
9703
|
+
const hit = findFontFamily4(t.font.family);
|
|
9704
|
+
const family = hit?.family ?? substituteFamily(t.font.family);
|
|
9705
|
+
const face = faces.get(family) ?? {
|
|
9706
|
+
family,
|
|
9707
|
+
weights: /* @__PURE__ */ new Set(),
|
|
9708
|
+
italic: false,
|
|
9709
|
+
from: hit ? null : t.font.family
|
|
9710
|
+
};
|
|
9711
|
+
face.weights.add(t.font.weight);
|
|
9712
|
+
if (t.font.style === "italic") face.italic = true;
|
|
9713
|
+
faces.set(family, face);
|
|
9714
|
+
return family;
|
|
9715
|
+
};
|
|
9716
|
+
const elements = [];
|
|
9717
|
+
const textKeys = [];
|
|
9718
|
+
const sceneNames = new Set(inv.scenes.map((s) => s.name));
|
|
9719
|
+
const sceneElements = {};
|
|
9720
|
+
for (const t of inv.texts) {
|
|
9721
|
+
const generic = /^(div|span|p|h[1-6]|li|a|b|i|em|strong)-\d+$/.test(t.id);
|
|
9722
|
+
const key = keyFor(generic ? t.text : t.id, taken);
|
|
9723
|
+
const words = t.font.transform === "uppercase" ? t.text.toUpperCase() : t.font.transform === "lowercase" ? t.text.toLowerCase() : t.text;
|
|
9724
|
+
data[key] = words;
|
|
9725
|
+
const family = faceFor(t);
|
|
9726
|
+
elements.push({
|
|
9727
|
+
id: key,
|
|
9728
|
+
type: "text",
|
|
9729
|
+
content: { $data: key },
|
|
9730
|
+
position: t.box ? { x: fmtPct(t.box.x / inv.width), y: fmtPct(t.box.y / inv.height) } : "center",
|
|
9731
|
+
font: {
|
|
9732
|
+
family,
|
|
9733
|
+
size: Math.round(t.font.size * scale),
|
|
9734
|
+
weight: t.font.weight,
|
|
9735
|
+
...t.font.style === "italic" ? { style: "italic" } : {},
|
|
9736
|
+
color: { $data: colorOf(t.font.color) },
|
|
9737
|
+
...t.font.letterSpacing ? {
|
|
9738
|
+
letterSpacing: Math.round(t.font.letterSpacing * scale * 10) / 10
|
|
9739
|
+
} : {}
|
|
9740
|
+
}
|
|
9741
|
+
});
|
|
9742
|
+
textKeys.push({ key, size: t.font.size, label: words.slice(0, 40) });
|
|
9743
|
+
if (t.scene && sceneNames.has(t.scene))
|
|
9744
|
+
(sceneElements[t.scene] ??= []).push(key);
|
|
9745
|
+
}
|
|
9746
|
+
const fonts = [];
|
|
9747
|
+
for (const face of faces.values()) {
|
|
9748
|
+
const entry = findFontFamily4(face.family);
|
|
9749
|
+
if (!entry) continue;
|
|
9750
|
+
for (const w of face.weights) {
|
|
9751
|
+
const weight = nearestFontWeight(entry, w);
|
|
9752
|
+
fonts.push({
|
|
9753
|
+
family: entry.family,
|
|
9754
|
+
weight,
|
|
9755
|
+
url: fontFaceUrl(entry.slug, weight)
|
|
9756
|
+
});
|
|
9757
|
+
}
|
|
9758
|
+
if (face.italic) {
|
|
9759
|
+
const italics = entry.italics ?? [];
|
|
9760
|
+
if (italics.length) {
|
|
9761
|
+
const weight = italics.includes(400) ? 400 : italics[0];
|
|
9762
|
+
fonts.push({
|
|
9763
|
+
family: entry.family,
|
|
9764
|
+
weight,
|
|
9765
|
+
style: "italic",
|
|
9766
|
+
url: fontFaceUrl(entry.slug, weight, "italic")
|
|
9767
|
+
});
|
|
9768
|
+
}
|
|
9769
|
+
}
|
|
9770
|
+
}
|
|
9771
|
+
const painterFaces = [];
|
|
9772
|
+
for (const f of inv.fonts) {
|
|
9773
|
+
if (!f.catalog || faces.has(f.catalog)) continue;
|
|
9774
|
+
const entry = findFontFamily4(f.catalog);
|
|
9775
|
+
if (!entry) continue;
|
|
9776
|
+
for (const weight of entry.weights)
|
|
9777
|
+
fonts.push({
|
|
9778
|
+
family: entry.family,
|
|
9779
|
+
weight,
|
|
9780
|
+
url: fontFaceUrl(entry.slug, weight)
|
|
9781
|
+
});
|
|
9782
|
+
for (const weight of entry.italics ?? [])
|
|
9783
|
+
fonts.push({
|
|
9784
|
+
family: entry.family,
|
|
9785
|
+
weight,
|
|
9786
|
+
style: "italic",
|
|
9787
|
+
url: fontFaceUrl(entry.slug, weight, "italic")
|
|
9788
|
+
});
|
|
9789
|
+
painterFaces.push(entry.family);
|
|
9790
|
+
}
|
|
9791
|
+
for (const f of inv.fonts) {
|
|
9792
|
+
if (!f.catalog && !faces.has(substituteFamily(f.family)) && !inv.texts.some((t) => t.font.family === f.family))
|
|
9793
|
+
report.push(
|
|
9794
|
+
`- **${f.family}** (named in the scripts) is not in the catalog: draw with ${substituteFamily(f.family)} and say so in the push note`
|
|
9795
|
+
);
|
|
9796
|
+
}
|
|
9797
|
+
if (painterFaces.length)
|
|
9798
|
+
report.push(
|
|
9799
|
+
`- faces the scripts paint with, declared at every hosted weight for the painter: ${painterFaces.join(", ")}`
|
|
9800
|
+
);
|
|
9801
|
+
data.scenes = inv.scenes.map((s) => ({
|
|
9802
|
+
name: s.name,
|
|
9803
|
+
at: s.start,
|
|
9804
|
+
until: s.duration == null ? null : Math.round((s.start + s.duration) * 1e3) / 1e3
|
|
9805
|
+
}));
|
|
9806
|
+
data.sceneElements = sceneElements;
|
|
9807
|
+
const params = [];
|
|
9808
|
+
const paletteKeys = [...colorKey.values()].slice(0, 6);
|
|
9809
|
+
for (const key of paletteKeys)
|
|
9810
|
+
params.push({
|
|
9811
|
+
key,
|
|
9812
|
+
label: key,
|
|
9813
|
+
kind: "color",
|
|
9814
|
+
default: data[key],
|
|
9815
|
+
group: "Colours"
|
|
9816
|
+
});
|
|
9817
|
+
for (const t of [...textKeys].sort((a, b) => b.size - a.size)) {
|
|
9818
|
+
if (params.length >= 12) break;
|
|
9819
|
+
params.push({
|
|
9820
|
+
key: t.key,
|
|
9821
|
+
label: t.label,
|
|
9822
|
+
kind: "text",
|
|
9823
|
+
default: data[t.key],
|
|
9824
|
+
group: "Words"
|
|
9825
|
+
});
|
|
9826
|
+
}
|
|
9827
|
+
const presets = paletteKeys.length ? [
|
|
9828
|
+
{
|
|
9829
|
+
name: "Original",
|
|
9830
|
+
values: Object.fromEntries(paletteKeys.map((k) => [k, data[k]]))
|
|
9831
|
+
}
|
|
9832
|
+
] : [];
|
|
9833
|
+
const painter = inv.canvases.length > 0 || inv.gaps.length > 0;
|
|
9834
|
+
const duration = Math.round((inv.duration ?? 5) * 1e3) / 1e3;
|
|
9835
|
+
const program = programSource({
|
|
9836
|
+
source: inv.source,
|
|
9837
|
+
duration,
|
|
9838
|
+
data,
|
|
9839
|
+
params,
|
|
9840
|
+
presets,
|
|
9841
|
+
fonts,
|
|
9842
|
+
elements,
|
|
9843
|
+
scenes: inv.scenes,
|
|
9844
|
+
painter
|
|
9845
|
+
});
|
|
9846
|
+
const audio = [];
|
|
9847
|
+
const copies = [];
|
|
9848
|
+
inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
|
|
9849
|
+
const from = resolve10(opts.sourceDir, m.src);
|
|
9850
|
+
const name = basename8(from);
|
|
9851
|
+
if (!existsSync18(from)) {
|
|
9852
|
+
report.push(
|
|
9853
|
+
`- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
|
|
9854
|
+
);
|
|
9855
|
+
return;
|
|
9856
|
+
}
|
|
9857
|
+
copies.push([from, name]);
|
|
9858
|
+
const fileDuration = opts.probeDuration?.(from) ?? m.duration ?? duration;
|
|
9859
|
+
audio.push({
|
|
9860
|
+
id: `score${i ? i + 1 : ""}`,
|
|
9861
|
+
key: name,
|
|
9862
|
+
name,
|
|
9863
|
+
start: m.start ?? 0,
|
|
9864
|
+
in: m.mediaStart ?? 0,
|
|
9865
|
+
out: Math.min(
|
|
9866
|
+
fileDuration,
|
|
9867
|
+
(m.mediaStart ?? 0) + (m.duration ?? fileDuration)
|
|
9868
|
+
),
|
|
9869
|
+
duration: fileDuration,
|
|
9870
|
+
gain: m.volume ?? 1,
|
|
9871
|
+
fadeIn: 0,
|
|
9872
|
+
fadeOut: 0
|
|
9873
|
+
});
|
|
9874
|
+
});
|
|
9875
|
+
const short = Math.min(inv.width, inv.height);
|
|
9876
|
+
const doc = {
|
|
9877
|
+
program: {},
|
|
9878
|
+
audio,
|
|
9879
|
+
export: {
|
|
9880
|
+
resolution: short >= 2160 ? "4k" : short >= 1440 ? "2k" : short >= 1080 ? "1080p" : "720p",
|
|
9881
|
+
fps: (inv.fps ?? 30) >= 50 ? 60 : 30,
|
|
9882
|
+
format: "mp4"
|
|
9883
|
+
}
|
|
9884
|
+
};
|
|
9885
|
+
return {
|
|
9886
|
+
program,
|
|
9887
|
+
doc,
|
|
9888
|
+
copies,
|
|
9889
|
+
report: reportText(inv, {
|
|
9890
|
+
faces: [...faces.values()],
|
|
9891
|
+
elements: elements.length,
|
|
9892
|
+
params: params.length,
|
|
9893
|
+
painter,
|
|
9894
|
+
extra: report
|
|
9895
|
+
})
|
|
9896
|
+
};
|
|
9897
|
+
}
|
|
9898
|
+
function programSource(p) {
|
|
9899
|
+
const j = (v) => JSON.stringify(v, null, 2).replace(/\n/g, "\n ");
|
|
9900
|
+
const sceneTodos = p.scenes.length ? p.scenes.map(
|
|
9901
|
+
(s) => ` // TODO scene "${s.name}" (${s.start}s${s.duration ? ` to ${s.start + s.duration}s` : ""}): translate its motion onto ctx.elements.get(id).props / .segments here`
|
|
9902
|
+
).join("\n") : " // TODO: translate the source's motion here, one block per scene (tl.addLabel above names them)";
|
|
9903
|
+
const painterContent = p.painter ? `
|
|
9904
|
+
// THE PAINTER (vos-port rule 6): one canvas, only for what no element can
|
|
9905
|
+
// say (the inventory's gaps and canvases). It reads every value from
|
|
9906
|
+
// ctx.data. Its plane sits above the elements; move its renderOrder
|
|
9907
|
+
// between two of them to paint between (SKILL.md "Layering").
|
|
9908
|
+
const T = ctx.THREE
|
|
9909
|
+
const canvas = document.createElement('canvas')
|
|
9910
|
+
canvas.width = ctx.resolution.width
|
|
9911
|
+
canvas.height = ctx.resolution.height
|
|
9912
|
+
const tex = new T.CanvasTexture(canvas)
|
|
9913
|
+
tex.colorSpace = T.SRGBColorSpace
|
|
9914
|
+
const plane = new T.Mesh(
|
|
9915
|
+
new T.PlaneGeometry(1, 1),
|
|
9916
|
+
new T.MeshBasicMaterial({ map: tex, transparent: true, depthTest: false, depthWrite: false }),
|
|
9917
|
+
)
|
|
9918
|
+
plane.scale.set(ctx.resolution.width, ctx.resolution.height, 1)
|
|
9919
|
+
plane.renderOrder = 100.995
|
|
9920
|
+
plane.frustumCulled = false
|
|
9921
|
+
ctx.overlayScene.add(plane)
|
|
9922
|
+
// The starter's helpers: Remotion's interpolate, a seeded PRNG, smooth noise.
|
|
9923
|
+
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)
|
|
9924
|
+
const interpolate = (v, [a, b], [x, y], ease = (t) => t) =>
|
|
9925
|
+
x + (y - x) * ease(clamp01((v - a) / (b - a)))
|
|
9926
|
+
const rand = (seed) => () => {
|
|
9927
|
+
seed |= 0
|
|
9928
|
+
seed = (seed + 0x6d2b79f5) | 0
|
|
9929
|
+
let r = Math.imul(seed ^ (seed >>> 15), 1 | seed)
|
|
9930
|
+
r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r
|
|
9931
|
+
return ((r ^ (r >>> 14)) >>> 0) / 4294967296
|
|
9932
|
+
}
|
|
9933
|
+
const hash = (x, y) => {
|
|
9934
|
+
const s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453
|
|
9935
|
+
return s - Math.floor(s)
|
|
9936
|
+
}
|
|
9937
|
+
const noise2 = (x, y) => {
|
|
9938
|
+
const xi = Math.floor(x), yi = Math.floor(y)
|
|
9939
|
+
const xf = x - xi, yf = y - yi
|
|
9940
|
+
const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf)
|
|
9941
|
+
const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1), d = hash(xi + 1, yi + 1)
|
|
9942
|
+
return (a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v) * 2 - 1
|
|
9943
|
+
}
|
|
9944
|
+
return { objects: [], refs: { canvas, g: canvas.getContext('2d'), tex, lib: { interpolate, rand, noise2 } } }` : `
|
|
9945
|
+
return { objects: [], refs: {} }`;
|
|
9946
|
+
const painterFrame = p.painter ? `
|
|
9947
|
+
// Paint the procedural layer from ctx.time and ctx.data, then upload it.
|
|
9948
|
+
const { canvas, g, tex } = content.refs
|
|
9949
|
+
g.clearRect(0, 0, canvas.width, canvas.height)
|
|
9950
|
+
// TODO: draw what no element can (grain, a blended HUD, noise blobs, masks)
|
|
9951
|
+
tex.needsUpdate = true` : "";
|
|
9952
|
+
return `// Scaffolded by \`vos port scaffold\` from ${p.source}.
|
|
9953
|
+
// Every word and colour is in \`data\`, bound to an element or read by a
|
|
9954
|
+
// painter; the knobs sit over them. What is left is the MOTION: translate each
|
|
9955
|
+
// scene where it says TODO, with the vos-port skill's tables open. Then:
|
|
9956
|
+
// npx vos build program.mjs && npx vos check config.json
|
|
9957
|
+
// npx vos compare . --against <the source's render>
|
|
9958
|
+
export default {
|
|
9959
|
+
version: 2,
|
|
9960
|
+
duration: ${p.duration},
|
|
9961
|
+
camera: { preset: 'fullscreen' },
|
|
9962
|
+
fonts: ${j(p.fonts)},
|
|
9963
|
+
data: ${j(p.data)},
|
|
9964
|
+
params: ${j(p.params)},
|
|
9965
|
+
presets: ${j(p.presets)},
|
|
9966
|
+
elements: ${j(p.elements)},
|
|
9967
|
+
|
|
9968
|
+
createContent(ctx) {
|
|
9969
|
+
ctx.scene.background = new ctx.THREE.Color(ctx.data.bg)${painterContent}
|
|
9970
|
+
},
|
|
9971
|
+
|
|
9972
|
+
createTimeline(ctx, content, duration) {
|
|
9973
|
+
const tl = ctx.gsap.timeline()
|
|
9974
|
+
for (const s of ctx.data.scenes) {
|
|
9975
|
+
tl.addLabel(s.name, s.at)
|
|
9976
|
+
// Each scene's words show in its window; the motion replaces this.
|
|
9977
|
+
for (const id of ctx.data.sceneElements[s.name] || []) {
|
|
9978
|
+
const p = ctx.elements.get(id).props
|
|
9979
|
+
if (s.at > 0) {
|
|
9980
|
+
tl.set(p, { opacity: 0 }, 0)
|
|
9981
|
+
tl.set(p, { opacity: 1 }, s.at)
|
|
9982
|
+
}
|
|
9983
|
+
if (s.until != null && s.until < duration) tl.set(p, { opacity: 0 }, s.until)
|
|
9984
|
+
}
|
|
9985
|
+
}
|
|
9986
|
+
${sceneTodos}
|
|
9987
|
+
tl.to({}, { duration }, 0)
|
|
9988
|
+
return tl
|
|
9989
|
+
},
|
|
9990
|
+
|
|
9991
|
+
onFrame(ctx, content) {
|
|
9992
|
+
ctx.scene.background.set(ctx.data.bg)${painterFrame}
|
|
9993
|
+
},
|
|
9994
|
+
}
|
|
9995
|
+
`;
|
|
9996
|
+
}
|
|
9997
|
+
function reportText(inv, r) {
|
|
9998
|
+
const lines = [
|
|
9999
|
+
`# Port report: ${inv.source}`,
|
|
10000
|
+
"",
|
|
10001
|
+
`${inv.engine} piece, ${inv.width}\xD7${inv.height}${inv.fps ? ` at ${inv.fps} fps` : ""}, ${inv.duration ?? "?"} s. A program has no size of its own: render it with \`--width ${inv.width} --height ${inv.height}\`.`,
|
|
10002
|
+
"",
|
|
10003
|
+
"## Placed",
|
|
10004
|
+
"",
|
|
10005
|
+
`- ${r.elements} words as bound text elements, ${Object.keys(inv.palette).length} palette colours from the source's custom properties, ${r.params} knobs.`,
|
|
10006
|
+
`- ${inv.scenes.length} scenes as labels${inv.scenes.length ? "" : " (none declared: name them as you translate)"}.`,
|
|
10007
|
+
...inv.media.some((m) => m.kind === "audio") ? ["- the score as a doc.json track (vos push uploads it)."] : [],
|
|
10008
|
+
"",
|
|
10009
|
+
"## Faces",
|
|
10010
|
+
"",
|
|
10011
|
+
...r.faces.map(
|
|
10012
|
+
(f) => f.from ? `- **${f.from}** is not in the catalog: substituted with ${f.family}. Say so in the push note.` : `- ${f.family}${f.italic ? " (italic)" : ""}: from the catalog.`
|
|
10013
|
+
),
|
|
10014
|
+
"",
|
|
10015
|
+
"## Needs a painter or an approximation",
|
|
10016
|
+
"",
|
|
10017
|
+
...inv.canvases.length ? [
|
|
10018
|
+
`- ${inv.canvases.length} canvas${inv.canvases.length === 1 ? "" : "es"}: the source paints pixels; port those parts as the painter, with their values in data.`
|
|
10019
|
+
] : [],
|
|
10020
|
+
...inv.gaps.map((g) => `- ${g}`),
|
|
10021
|
+
...r.painter ? [] : ["- nothing: every visible part is an element."],
|
|
10022
|
+
...inv.scriptStrings.length && inv.canvases.length ? [
|
|
10023
|
+
"",
|
|
10024
|
+
"## Words the scripts paint",
|
|
10025
|
+
"",
|
|
10026
|
+
"The page draws these on its canvas, so no text node holds them. Each one a person reads becomes a data key and a bound element:",
|
|
10027
|
+
"",
|
|
10028
|
+
...inv.scriptStrings.slice(0, 40).map((s) => `- ${JSON.stringify(s)}`)
|
|
10029
|
+
] : [],
|
|
10030
|
+
...inv.media.some((m) => m.kind === "image" || m.kind === "video") ? [
|
|
10031
|
+
"",
|
|
10032
|
+
"## Images and video",
|
|
10033
|
+
"",
|
|
10034
|
+
"Not placed: upload each with `vos asset upload` and add an image or video element over its hosted URL.",
|
|
10035
|
+
"",
|
|
10036
|
+
...inv.media.filter((m) => m.kind === "image" || m.kind === "video").map((m) => `- ${m.kind} ${m.src ?? "(inline)"}`)
|
|
10037
|
+
] : [],
|
|
10038
|
+
...r.extra.length ? ["", "## Also", "", ...r.extra] : [],
|
|
10039
|
+
"",
|
|
10040
|
+
"## Reference",
|
|
10041
|
+
"",
|
|
10042
|
+
inv.render ? `The source's render: ${inv.render}. Stills in port/ref/. Check every scene: \`npx vos compare . --against ${inv.render}\`.` : "No render of the source was found: render it with its own tool and pass it to `vos compare --against`.",
|
|
10043
|
+
""
|
|
10044
|
+
];
|
|
10045
|
+
return lines.join("\n");
|
|
10046
|
+
}
|
|
10047
|
+
function writeScaffold(plan, outDir) {
|
|
10048
|
+
mkdirSync4(outDir, { recursive: true });
|
|
10049
|
+
writeFileSync4(join22(outDir, "program.mjs"), plan.program);
|
|
10050
|
+
writeFileSync4(
|
|
10051
|
+
join22(outDir, "doc.json"),
|
|
10052
|
+
JSON.stringify(plan.doc, null, 2) + "\n"
|
|
10053
|
+
);
|
|
10054
|
+
writeFileSync4(join22(dirname7(outDir), "REPORT.md"), plan.report);
|
|
10055
|
+
for (const [from, name] of plan.copies) copyFileSync(from, join22(outDir, name));
|
|
10056
|
+
}
|
|
10057
|
+
function probeAudioDuration(file) {
|
|
10058
|
+
const res = spawnSync3(
|
|
10059
|
+
"ffprobe",
|
|
10060
|
+
["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", file],
|
|
10061
|
+
{ encoding: "utf8" }
|
|
10062
|
+
);
|
|
10063
|
+
const d = Number((res.stdout ?? "").trim());
|
|
10064
|
+
return Number.isFinite(d) && d > 0 ? Math.round(d * 1e3) / 1e3 : null;
|
|
10065
|
+
}
|
|
10066
|
+
|
|
10067
|
+
// src/plugin/port.ts
|
|
10068
|
+
var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
10069
|
+
async function cmdPort(argv) {
|
|
10070
|
+
const [sub, ...rest] = argv;
|
|
10071
|
+
if (sub === "inventory") return await inventory(rest);
|
|
10072
|
+
if (sub === "scaffold") return await scaffold(rest);
|
|
10073
|
+
throw new UsageError(
|
|
10074
|
+
"vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]\nvos port scaffold [--from port/inventory.json] [--out port/program] [--json]"
|
|
10075
|
+
);
|
|
10076
|
+
}
|
|
10077
|
+
async function inventory(argv) {
|
|
10078
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
10079
|
+
const source = positionals[0] ?? ".";
|
|
10080
|
+
const r = createReporter(flags.json === true);
|
|
10081
|
+
const outDir = resolve11(strFlag(flags, "out") ?? "port");
|
|
10082
|
+
mkdirSync5(outDir, { recursive: true });
|
|
10083
|
+
const renderFlag = strFlag(flags, "render");
|
|
10084
|
+
const inv = await readInventory(source, {
|
|
10085
|
+
render: renderFlag === void 0 ? void 0 : resolve11(renderFlag),
|
|
10086
|
+
outDir,
|
|
10087
|
+
log: (l) => r.log(`note: ${l}`)
|
|
10088
|
+
});
|
|
10089
|
+
const file = join23(outDir, "inventory.json");
|
|
10090
|
+
writeFileSync5(file, JSON.stringify(inv, null, 2) + "\n");
|
|
10091
|
+
const missing = inv.fonts.filter((f) => !f.catalog).map((f) => f.family);
|
|
10092
|
+
r.log(
|
|
10093
|
+
`${inv.engine}, ${inv.width}\xD7${inv.height}${inv.fps ? `@${inv.fps}` : ""}, ${inv.duration ?? "?"}s: ${inv.texts.length} words, ${Object.keys(inv.palette).length} palette colours, ${inv.fonts.length} faces${missing.length ? ` (not in the catalog: ${missing.join(", ")})` : ""}, ${inv.media.length} media, ${inv.scenes.length} scenes, ${inv.canvases.length} canvases${inv.gaps.length ? `; needs a painter or an approximation: ${inv.gaps.join(", ")}` : ""}`
|
|
10094
|
+
);
|
|
10095
|
+
r.done(
|
|
10096
|
+
{ inventory: file, ...summary(inv) },
|
|
10097
|
+
`Wrote ${file}${inv.stills.length ? ` and ${inv.stills.length} reference stills` : ""}. Next: npx vos port scaffold`
|
|
10098
|
+
);
|
|
10099
|
+
return EXIT_OK;
|
|
10100
|
+
}
|
|
10101
|
+
function summary(inv) {
|
|
10102
|
+
return {
|
|
10103
|
+
engine: inv.engine,
|
|
10104
|
+
width: inv.width,
|
|
10105
|
+
height: inv.height,
|
|
10106
|
+
fps: inv.fps,
|
|
10107
|
+
duration: inv.duration,
|
|
10108
|
+
words: inv.texts.length,
|
|
10109
|
+
palette: Object.keys(inv.palette).length,
|
|
10110
|
+
faces: inv.fonts.map((f) => ({ family: f.family, catalog: f.catalog })),
|
|
10111
|
+
media: inv.media.length,
|
|
10112
|
+
scenes: inv.scenes.length,
|
|
10113
|
+
canvases: inv.canvases.length,
|
|
10114
|
+
gaps: inv.gaps,
|
|
10115
|
+
render: inv.render
|
|
10116
|
+
};
|
|
10117
|
+
}
|
|
10118
|
+
async function scaffold(argv) {
|
|
10119
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
10120
|
+
const r = createReporter(flags.json === true);
|
|
10121
|
+
const from = resolve11(strFlag(flags, "from") ?? join23("port", "inventory.json"));
|
|
10122
|
+
if (!existsSync19(from))
|
|
10123
|
+
throw new UsageError(
|
|
10124
|
+
`no inventory at ${from}: run npx vos port inventory <source> first`
|
|
10125
|
+
);
|
|
10126
|
+
const inv = JSON.parse(readFileSync6(from, "utf8"));
|
|
10127
|
+
const outDir = resolve11(
|
|
10128
|
+
strFlag(flags, "out") ?? join23(dirname8(from), "program")
|
|
10129
|
+
);
|
|
10130
|
+
const plan = planScaffold(inv, {
|
|
10131
|
+
sourceDir: dirname8(
|
|
10132
|
+
resolve11(
|
|
10133
|
+
inv.source.endsWith(".html") ? inv.source : join23(inv.source, "index.html")
|
|
10134
|
+
)
|
|
10135
|
+
),
|
|
10136
|
+
probeDuration: probeAudioDuration
|
|
10137
|
+
});
|
|
10138
|
+
writeScaffold(plan, outDir);
|
|
10139
|
+
const programFile = join23(outDir, "program.mjs");
|
|
10140
|
+
const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
|
|
10141
|
+
const built = buildProgram(mod.default, "program.mjs", plan.program);
|
|
10142
|
+
for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
|
|
10143
|
+
if (built.problems.length) return EXIT_ERROR;
|
|
10144
|
+
writeFileSync5(
|
|
10145
|
+
join23(outDir, "config.json"),
|
|
10146
|
+
JSON.stringify(built.config, null, 2) + "\n"
|
|
10147
|
+
);
|
|
10148
|
+
r.done(
|
|
10149
|
+
{ program: outDir, report: join23(dirname8(outDir), "REPORT.md") },
|
|
10150
|
+
`Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join23(dirname8(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
|
|
10151
|
+
);
|
|
10152
|
+
return EXIT_OK;
|
|
10153
|
+
}
|
|
10154
|
+
|
|
10155
|
+
// src/plugin/recipe.ts
|
|
10156
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
10157
|
+
import { basename as basename9 } from "path";
|
|
10158
|
+
var BOOLEAN_FLAGS8 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
8820
10159
|
var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
|
|
8821
10160
|
function recipeFilename(file) {
|
|
8822
|
-
const name =
|
|
10161
|
+
const name = basename9(file);
|
|
8823
10162
|
if (!/\.md$/i.test(name)) {
|
|
8824
10163
|
throw new UsageError(`a recipe is a markdown file \u2014 ${name} is not .md`);
|
|
8825
10164
|
}
|
|
8826
10165
|
return name;
|
|
8827
10166
|
}
|
|
8828
10167
|
async function cmdPush2(argv) {
|
|
8829
|
-
const { positionals, flags } = parseArgs(argv,
|
|
10168
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
|
|
8830
10169
|
const [file] = positionals;
|
|
8831
10170
|
const folderRef = strFlag(flags, "folder");
|
|
8832
10171
|
const assetId = strFlag(flags, "asset");
|
|
@@ -8834,7 +10173,7 @@ async function cmdPush2(argv) {
|
|
|
8834
10173
|
throw new UsageError(USAGE);
|
|
8835
10174
|
}
|
|
8836
10175
|
const filename = recipeFilename(file);
|
|
8837
|
-
const body =
|
|
10176
|
+
const body = readFileSync7(file);
|
|
8838
10177
|
const r = createReporter(flags.json === true);
|
|
8839
10178
|
const origin = platformOrigin({
|
|
8840
10179
|
origin: strFlag(flags, "origin"),
|
|
@@ -8907,7 +10246,7 @@ async function cmdRecipe(argv) {
|
|
|
8907
10246
|
|
|
8908
10247
|
// src/plugin/brand.ts
|
|
8909
10248
|
import { writeFile as writeFile12 } from "fs/promises";
|
|
8910
|
-
import { resolve as
|
|
10249
|
+
import { resolve as resolve12 } from "path";
|
|
8911
10250
|
import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
|
|
8912
10251
|
import { lookKindForGround } from "@vosjs/studio-core";
|
|
8913
10252
|
var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
|
|
@@ -9226,7 +10565,7 @@ async function cmdBrand(argv) {
|
|
|
9226
10565
|
throw new UsageError(`not a URL: ${raw}`);
|
|
9227
10566
|
}
|
|
9228
10567
|
const r = createReporter(flags.json === true);
|
|
9229
|
-
const out =
|
|
10568
|
+
const out = resolve12(strFlag(flags, "out") ?? "BRAND.md");
|
|
9230
10569
|
r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
|
|
9231
10570
|
const [designText, llmsText] = await Promise.all([
|
|
9232
10571
|
fetchText(`${origin}/design.md`),
|
|
@@ -9376,7 +10715,7 @@ async function recordHosted(input) {
|
|
|
9376
10715
|
|
|
9377
10716
|
// src/plugin/agentBrowser.ts
|
|
9378
10717
|
import { writeFile as writeFile13 } from "fs/promises";
|
|
9379
|
-
import { resolve as
|
|
10718
|
+
import { resolve as resolve13 } from "path";
|
|
9380
10719
|
function parseAgentBrowserLog(text) {
|
|
9381
10720
|
const records = [];
|
|
9382
10721
|
const problems = [];
|
|
@@ -9477,8 +10816,8 @@ var quoteAttr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
|
9477
10816
|
function hrefSelector(href) {
|
|
9478
10817
|
try {
|
|
9479
10818
|
const u = new URL(href);
|
|
9480
|
-
const
|
|
9481
|
-
return `a[href=${quoteAttr(
|
|
10819
|
+
const relative3 = `${u.pathname}${u.search}${u.hash}`;
|
|
10820
|
+
return `a[href=${quoteAttr(relative3)}], a[href=${quoteAttr(href)}]`;
|
|
9482
10821
|
} catch {
|
|
9483
10822
|
return `a[href=${quoteAttr(href)}]`;
|
|
9484
10823
|
}
|
|
@@ -9847,7 +11186,7 @@ async function cmdActions(argv) {
|
|
|
9847
11186
|
);
|
|
9848
11187
|
}
|
|
9849
11188
|
const r = createReporter(flags.json === true);
|
|
9850
|
-
const out =
|
|
11189
|
+
const out = resolve13(strFlag(flags, "out") ?? "actions.json");
|
|
9851
11190
|
const viewportFlag = strFlag(flags, "viewport");
|
|
9852
11191
|
let viewport;
|
|
9853
11192
|
if (viewportFlag) {
|
|
@@ -9856,7 +11195,7 @@ async function cmdActions(argv) {
|
|
|
9856
11195
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
9857
11196
|
}
|
|
9858
11197
|
const { readFile: readFile18 } = await import("fs/promises");
|
|
9859
|
-
const text = await readFile18(
|
|
11198
|
+
const text = await readFile18(resolve13(input), "utf8");
|
|
9860
11199
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
9861
11200
|
for (const p of problems) r.log(` ${p}`);
|
|
9862
11201
|
const result = convertAgentBrowser(records, {
|
|
@@ -9866,7 +11205,7 @@ async function cmdActions(argv) {
|
|
|
9866
11205
|
const errors = validateActions(result.actions);
|
|
9867
11206
|
if (errors.length) {
|
|
9868
11207
|
r.done(
|
|
9869
|
-
{ ok: false, errors, ...
|
|
11208
|
+
{ ok: false, errors, ...summary2(result) },
|
|
9870
11209
|
`${input}: no actions file written
|
|
9871
11210
|
${errors.join("\n ")}
|
|
9872
11211
|
${result.notes.join("\n ")}`
|
|
@@ -9876,14 +11215,14 @@ async function cmdActions(argv) {
|
|
|
9876
11215
|
await writeFile13(out, `${JSON.stringify(result.actions, null, 2)}
|
|
9877
11216
|
`);
|
|
9878
11217
|
r.done(
|
|
9879
|
-
{ ok: true, out, url: result.actions.url ?? null, ...
|
|
11218
|
+
{ ok: true, out, url: result.actions.url ?? null, ...summary2(result) },
|
|
9880
11219
|
`Wrote ${out}: ${result.steps} step${result.steps === 1 ? "" : "s"} from ${records.length} record${records.length === 1 ? "" : "s"} (${result.ignored} read/session, ${result.skipped} skipped)` + (result.notes.length ? `
|
|
9881
11220
|
${result.notes.join("\n ")}` : "") + `
|
|
9882
11221
|
Then: vos record --actions ${out} --out take --strict`
|
|
9883
11222
|
);
|
|
9884
11223
|
return EXIT_OK;
|
|
9885
11224
|
}
|
|
9886
|
-
function
|
|
11225
|
+
function summary2(result) {
|
|
9887
11226
|
return {
|
|
9888
11227
|
steps: result.steps,
|
|
9889
11228
|
ignored: result.ignored,
|
|
@@ -9895,7 +11234,7 @@ async function cmdActionsScript(input, flags) {
|
|
|
9895
11234
|
const r = createReporter(flags.json === true);
|
|
9896
11235
|
const { readFile: readFile18 } = await import("fs/promises");
|
|
9897
11236
|
const { shotList, formatShotList } = await import("./shotList-KH6FTX4L.js");
|
|
9898
|
-
const raw = JSON.parse(await readFile18(
|
|
11237
|
+
const raw = JSON.parse(await readFile18(resolve13(input), "utf8"));
|
|
9899
11238
|
const errors = validateActions(raw);
|
|
9900
11239
|
if (errors.length) throw new UsageError(`${input}:
|
|
9901
11240
|
${errors.join("\n ")}`);
|
|
@@ -9905,9 +11244,9 @@ async function cmdActionsScript(input, flags) {
|
|
|
9905
11244
|
}
|
|
9906
11245
|
|
|
9907
11246
|
// src/plugin/ingest.ts
|
|
9908
|
-
import { existsSync as
|
|
11247
|
+
import { existsSync as existsSync20 } from "fs";
|
|
9909
11248
|
import { copyFile, mkdir as mkdir10, readFile as readFile16, stat as stat4 } from "fs/promises";
|
|
9910
|
-
import { basename as
|
|
11249
|
+
import { basename as basename10, join as join24, resolve as resolve14 } from "path";
|
|
9911
11250
|
import { inflateRawSync } from "zlib";
|
|
9912
11251
|
import {
|
|
9913
11252
|
cursorFromPlaywrightTrace,
|
|
@@ -9927,7 +11266,7 @@ import {
|
|
|
9927
11266
|
Output,
|
|
9928
11267
|
WebMOutputFormat
|
|
9929
11268
|
} from "mediabunny";
|
|
9930
|
-
var
|
|
11269
|
+
var BOOLEAN_FLAGS9 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9931
11270
|
async function probeVideoFile(path) {
|
|
9932
11271
|
const input = new Input({
|
|
9933
11272
|
source: new FilePathSource(path),
|
|
@@ -9988,7 +11327,7 @@ async function copyRecording(src, dest, ext) {
|
|
|
9988
11327
|
}
|
|
9989
11328
|
}
|
|
9990
11329
|
async function readTraceFile(path, offsetMs) {
|
|
9991
|
-
const kind = traceKindOf(
|
|
11330
|
+
const kind = traceKindOf(basename10(path));
|
|
9992
11331
|
if (!kind) {
|
|
9993
11332
|
throw new UsageError(
|
|
9994
11333
|
`${path}: not a trace the recorder reads (trace.zip, .trace, .jsonl, .json or .csv)`
|
|
@@ -10009,7 +11348,7 @@ async function readTraceFile(path, offsetMs) {
|
|
|
10009
11348
|
return cursorFromPlaywrightTrace(text2, { offsetMs });
|
|
10010
11349
|
}
|
|
10011
11350
|
const text = await readFile16(path, "utf8");
|
|
10012
|
-
return cursorFromTrace(
|
|
11351
|
+
return cursorFromTrace(basename10(path), text, { offsetMs });
|
|
10013
11352
|
}
|
|
10014
11353
|
function parseViewport(v) {
|
|
10015
11354
|
if (!v) return null;
|
|
@@ -10018,16 +11357,16 @@ function parseViewport(v) {
|
|
|
10018
11357
|
return { width: Number(m[1]), height: Number(m[2]) };
|
|
10019
11358
|
}
|
|
10020
11359
|
async function cmdIngest(argv) {
|
|
10021
|
-
const { positionals, flags } = parseArgs(argv,
|
|
11360
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS9, MULTI_FLAGS2);
|
|
10022
11361
|
const source = positionals[0];
|
|
10023
11362
|
if (!source)
|
|
10024
11363
|
throw new UsageError(
|
|
10025
11364
|
"vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]"
|
|
10026
11365
|
);
|
|
10027
|
-
const videoPath =
|
|
10028
|
-
if (!
|
|
11366
|
+
const videoPath = resolve14(source);
|
|
11367
|
+
if (!existsSync20(videoPath)) throw new UsageError(`${source}: no such file`);
|
|
10029
11368
|
const r = createReporter(flags.json === true);
|
|
10030
|
-
const outDir =
|
|
11369
|
+
const outDir = resolve14(strFlag(flags, "out") ?? "take");
|
|
10031
11370
|
const offsetMs = numFlag(flags, "offset", 0);
|
|
10032
11371
|
const viewportFlag = parseViewport(strFlag(flags, "viewport"));
|
|
10033
11372
|
const cursorPath = strFlag(flags, "cursor");
|
|
@@ -10040,7 +11379,7 @@ async function cmdIngest(argv) {
|
|
|
10040
11379
|
"note: finished footage opens bare, so --background is not used (pass --as take for the screen-recording card and its ground)"
|
|
10041
11380
|
);
|
|
10042
11381
|
const backdrop = footage ? null : await takeBackdrop(flags, r);
|
|
10043
|
-
r.log(`probing ${
|
|
11382
|
+
r.log(`probing ${basename10(videoPath)}\u2026`);
|
|
10044
11383
|
const probed = await probeVideoFile(videoPath);
|
|
10045
11384
|
if (!(probed.durationMs > 0)) {
|
|
10046
11385
|
throw new UsageError(
|
|
@@ -10049,10 +11388,10 @@ async function cmdIngest(argv) {
|
|
|
10049
11388
|
}
|
|
10050
11389
|
let trace = null;
|
|
10051
11390
|
if (cursorPath) {
|
|
10052
|
-
trace = await readTraceFile(
|
|
11391
|
+
trace = await readTraceFile(resolve14(cursorPath), offsetMs);
|
|
10053
11392
|
for (const n of trace.notes) r.log(`note: ${n}`);
|
|
10054
11393
|
}
|
|
10055
|
-
if (
|
|
11394
|
+
if (existsSync20(join24(outDir, "meta.json"))) {
|
|
10056
11395
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
10057
11396
|
r.log(
|
|
10058
11397
|
`note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
|
|
@@ -10060,7 +11399,7 @@ async function cmdIngest(argv) {
|
|
|
10060
11399
|
}
|
|
10061
11400
|
await mkdir10(outDir, { recursive: true });
|
|
10062
11401
|
const paths = await ensureTakeDir(outDir);
|
|
10063
|
-
const recording =
|
|
11402
|
+
const recording = join24(outDir, `recording.${probed.ext}`);
|
|
10064
11403
|
r.log("copying the recording\u2026");
|
|
10065
11404
|
const remuxed = await copyRecording(videoPath, recording, probed.ext);
|
|
10066
11405
|
const viewport = viewportFlag ?? trace?.viewport ?? {
|
|
@@ -10089,13 +11428,13 @@ async function cmdIngest(argv) {
|
|
|
10089
11428
|
const clicks = events.filter((e) => e.type === "down").length;
|
|
10090
11429
|
const seconds = (probed.durationMs / 1e3).toFixed(1);
|
|
10091
11430
|
const facts = `${seconds}s, ${probed.width}\xD7${probed.height} @ ${meta.fps} fps${probed.hasAudio ? ", audio" : ", no audio"}${remuxed ? "" : ", copied as is"}`;
|
|
10092
|
-
const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${
|
|
11431
|
+
const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${basename10(cursorPath)}${trace.dropped ? `, ${trace.dropped} not placed` : ""}; ${plan.zoomAuto} zoom${plan.zoomAuto === 1 ? "" : "s"} planned` : "no cursor track, so nothing was planned";
|
|
10093
11432
|
const opened = footage ? `opened as finished footage: no card, no browser bar, no drawn cursor, ${meta.fps > 30 ? 60 : 30} fps export (--as take for a screen recording's card)` : `opened as a screen recording: the card, its ground and the drawn cursor (--as footage to open it bare)`;
|
|
10094
11433
|
const next = footage && !trace ? `Next: vos push ${outDir} --wait \u2014 it lands as it is; trim or add to it in the studio` : trace ? `Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}` : `Add --cursor <trace> recorded beside the video (a Playwright trace.zip, stamped JSON records, or a t,x,y,type CSV) to plan zooms from its clicks; else place them by eye: vos frames ${outDir} --times 0,25%,50%,75%,100%, then edit doc.json`;
|
|
10095
11434
|
r.done(
|
|
10096
11435
|
{
|
|
10097
11436
|
out: outDir,
|
|
10098
|
-
recording:
|
|
11437
|
+
recording: basename10(recording),
|
|
10099
11438
|
durationMs: probed.durationMs,
|
|
10100
11439
|
width: probed.width,
|
|
10101
11440
|
height: probed.height,
|
|
@@ -10104,7 +11443,7 @@ async function cmdIngest(argv) {
|
|
|
10104
11443
|
hasAudio: probed.hasAudio,
|
|
10105
11444
|
remuxed,
|
|
10106
11445
|
cursor: trace ? {
|
|
10107
|
-
file:
|
|
11446
|
+
file: basename10(cursorPath),
|
|
10108
11447
|
kind: trace.kind,
|
|
10109
11448
|
events: events.length,
|
|
10110
11449
|
clicks,
|
|
@@ -10116,7 +11455,7 @@ async function cmdIngest(argv) {
|
|
|
10116
11455
|
backdrop: plan.backdrop ?? null,
|
|
10117
11456
|
as: footage ? "footage" : "take"
|
|
10118
11457
|
},
|
|
10119
|
-
`Ingested ${
|
|
11458
|
+
`Ingested ${basename10(videoPath)} \u2192 ${outDir}/${basename10(recording)} (${facts})
|
|
10120
11459
|
${opened}
|
|
10121
11460
|
${cursorLine}
|
|
10122
11461
|
${next}`
|
|
@@ -10125,22 +11464,22 @@ async function cmdIngest(argv) {
|
|
|
10125
11464
|
}
|
|
10126
11465
|
|
|
10127
11466
|
// src/plugin/machineCmd.ts
|
|
10128
|
-
import { spawnSync } from "child_process";
|
|
11467
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
10129
11468
|
import {
|
|
10130
11469
|
cpSync,
|
|
10131
|
-
existsSync as
|
|
10132
|
-
mkdirSync as
|
|
10133
|
-
readFileSync as
|
|
11470
|
+
existsSync as existsSync22,
|
|
11471
|
+
mkdirSync as mkdirSync6,
|
|
11472
|
+
readFileSync as readFileSync9,
|
|
10134
11473
|
rmSync as rmSync3,
|
|
10135
|
-
writeFileSync as
|
|
11474
|
+
writeFileSync as writeFileSync6
|
|
10136
11475
|
} from "fs";
|
|
10137
11476
|
import { homedir as homedir2 } from "os";
|
|
10138
|
-
import { join as
|
|
11477
|
+
import { join as join26 } from "path";
|
|
10139
11478
|
import { chromium as chromium2 } from "playwright";
|
|
10140
11479
|
|
|
10141
11480
|
// src/plugin/machine.ts
|
|
10142
|
-
import { existsSync as
|
|
10143
|
-
import { dirname as
|
|
11481
|
+
import { existsSync as existsSync21, readFileSync as readFileSync8, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
11482
|
+
import { dirname as dirname9, join as join25 } from "path";
|
|
10144
11483
|
var AGENT_NAMES = [
|
|
10145
11484
|
"claude",
|
|
10146
11485
|
"cursor",
|
|
@@ -10160,17 +11499,17 @@ var HOME_DIRS = {
|
|
|
10160
11499
|
copilot: [".copilot"]
|
|
10161
11500
|
};
|
|
10162
11501
|
function detectAgents(root, opts = {}) {
|
|
10163
|
-
const exists = opts.exists ??
|
|
11502
|
+
const exists = opts.exists ?? existsSync21;
|
|
10164
11503
|
const out = [];
|
|
10165
11504
|
for (const agent of AGENT_NAMES) {
|
|
10166
11505
|
for (const rel of PROJECT_DIRS[agent]) {
|
|
10167
|
-
const dir =
|
|
11506
|
+
const dir = join25(root, rel);
|
|
10168
11507
|
if (exists(dir)) out.push({ agent, dir, scope: "project" });
|
|
10169
11508
|
}
|
|
10170
|
-
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(
|
|
11509
|
+
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join25(root, ".github", "copilot-instructions.md"))) {
|
|
10171
11510
|
out.push({
|
|
10172
11511
|
agent,
|
|
10173
|
-
dir:
|
|
11512
|
+
dir: join25(root, ".github", "copilot"),
|
|
10174
11513
|
scope: "project"
|
|
10175
11514
|
});
|
|
10176
11515
|
}
|
|
@@ -10178,7 +11517,7 @@ function detectAgents(root, opts = {}) {
|
|
|
10178
11517
|
if (opts.global && opts.home) {
|
|
10179
11518
|
for (const agent of AGENT_NAMES) {
|
|
10180
11519
|
for (const rel of HOME_DIRS[agent]) {
|
|
10181
|
-
const dir =
|
|
11520
|
+
const dir = join25(opts.home, rel);
|
|
10182
11521
|
if (exists(dir)) out.push({ agent, dir, scope: "global" });
|
|
10183
11522
|
}
|
|
10184
11523
|
}
|
|
@@ -10196,26 +11535,26 @@ function agentsFromFlag(value, root) {
|
|
|
10196
11535
|
return {
|
|
10197
11536
|
homes: wanted.map((agent) => ({
|
|
10198
11537
|
agent,
|
|
10199
|
-
dir:
|
|
11538
|
+
dir: join25(root, PROJECT_DIRS[agent][0]),
|
|
10200
11539
|
scope: "project"
|
|
10201
11540
|
}))
|
|
10202
11541
|
};
|
|
10203
11542
|
}
|
|
10204
11543
|
function skillsDirFor(home) {
|
|
10205
|
-
return
|
|
11544
|
+
return join25(home.dir, "skills");
|
|
10206
11545
|
}
|
|
10207
11546
|
function fallbackSkillsHome(root) {
|
|
10208
|
-
return { agent: "codex", dir:
|
|
11547
|
+
return { agent: "codex", dir: join25(root, ".agents"), scope: "project" };
|
|
10209
11548
|
}
|
|
10210
|
-
function installedSkills(dir, exists =
|
|
11549
|
+
function installedSkills(dir, exists = existsSync21, list = (p) => {
|
|
10211
11550
|
try {
|
|
10212
|
-
return
|
|
11551
|
+
return readdirSync5(p);
|
|
10213
11552
|
} catch {
|
|
10214
11553
|
return [];
|
|
10215
11554
|
}
|
|
10216
11555
|
}) {
|
|
10217
11556
|
if (!exists(dir)) return [];
|
|
10218
|
-
return list(dir).filter((name) => exists(
|
|
11557
|
+
return list(dir).filter((name) => exists(join25(dir, name, "SKILL.md"))).sort();
|
|
10219
11558
|
}
|
|
10220
11559
|
var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
|
|
10221
11560
|
var RULES_END = "<!-- vos:end -->";
|
|
@@ -10247,10 +11586,10 @@ ${RULES_BLOCK}
|
|
|
10247
11586
|
}
|
|
10248
11587
|
return { text, changed: text !== current };
|
|
10249
11588
|
}
|
|
10250
|
-
function rulesFileFor(root, exists =
|
|
10251
|
-
const agents =
|
|
11589
|
+
function rulesFileFor(root, exists = existsSync21) {
|
|
11590
|
+
const agents = join25(root, "AGENTS.md");
|
|
10252
11591
|
if (exists(agents)) return agents;
|
|
10253
|
-
const claude =
|
|
11592
|
+
const claude = join25(root, "CLAUDE.md");
|
|
10254
11593
|
if (exists(claude)) return claude;
|
|
10255
11594
|
return agents;
|
|
10256
11595
|
}
|
|
@@ -10259,14 +11598,14 @@ function chromeCandidates(platform, env) {
|
|
|
10259
11598
|
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
|
|
10260
11599
|
if (platform === "win32")
|
|
10261
11600
|
return [
|
|
10262
|
-
|
|
11601
|
+
join25(
|
|
10263
11602
|
env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
10264
11603
|
"Google",
|
|
10265
11604
|
"Chrome",
|
|
10266
11605
|
"Application",
|
|
10267
11606
|
"chrome.exe"
|
|
10268
11607
|
),
|
|
10269
|
-
|
|
11608
|
+
join25(
|
|
10270
11609
|
env["LOCALAPPDATA"] ?? "",
|
|
10271
11610
|
"Google",
|
|
10272
11611
|
"Chrome",
|
|
@@ -10351,20 +11690,20 @@ function doctorReport(facts) {
|
|
|
10351
11690
|
return { ok: nodeOk && browserOk, lines, next_step, facts };
|
|
10352
11691
|
}
|
|
10353
11692
|
function bundledSkillsDir(from) {
|
|
10354
|
-
let dir =
|
|
11693
|
+
let dir = dirname9(from);
|
|
10355
11694
|
for (let i = 0; i < 6; i++) {
|
|
10356
|
-
const pkg =
|
|
10357
|
-
if (
|
|
11695
|
+
const pkg = join25(dir, "package.json");
|
|
11696
|
+
if (existsSync21(pkg)) {
|
|
10358
11697
|
try {
|
|
10359
|
-
const name = JSON.parse(
|
|
11698
|
+
const name = JSON.parse(readFileSync8(pkg, "utf8")).name;
|
|
10360
11699
|
if (name === "@vosjs/cli") {
|
|
10361
|
-
const skills =
|
|
10362
|
-
return
|
|
11700
|
+
const skills = join25(dir, "skills");
|
|
11701
|
+
return existsSync21(skills) && statSync6(skills).isDirectory() ? skills : null;
|
|
10363
11702
|
}
|
|
10364
11703
|
} catch {
|
|
10365
11704
|
}
|
|
10366
11705
|
}
|
|
10367
|
-
const up =
|
|
11706
|
+
const up = dirname9(dir);
|
|
10368
11707
|
if (up === dir) break;
|
|
10369
11708
|
dir = up;
|
|
10370
11709
|
}
|
|
@@ -10372,7 +11711,7 @@ function bundledSkillsDir(from) {
|
|
|
10372
11711
|
}
|
|
10373
11712
|
|
|
10374
11713
|
// src/plugin/machineCmd.ts
|
|
10375
|
-
var
|
|
11714
|
+
var BOOLEAN_FLAGS10 = /* @__PURE__ */ new Set([
|
|
10376
11715
|
"json",
|
|
10377
11716
|
"help",
|
|
10378
11717
|
"global",
|
|
@@ -10390,21 +11729,21 @@ function npx() {
|
|
|
10390
11729
|
function bundledChromium() {
|
|
10391
11730
|
try {
|
|
10392
11731
|
const p = chromium2.executablePath();
|
|
10393
|
-
return p &&
|
|
11732
|
+
return p && existsSync22(p) ? p : null;
|
|
10394
11733
|
} catch {
|
|
10395
11734
|
return null;
|
|
10396
11735
|
}
|
|
10397
11736
|
}
|
|
10398
|
-
function
|
|
11737
|
+
function probe2() {
|
|
10399
11738
|
return probeBrowser({
|
|
10400
11739
|
platform: process.platform,
|
|
10401
11740
|
env: process.env,
|
|
10402
|
-
exists:
|
|
11741
|
+
exists: existsSync22,
|
|
10403
11742
|
bundled: bundledChromium
|
|
10404
11743
|
});
|
|
10405
11744
|
}
|
|
10406
11745
|
function ffmpegVersion() {
|
|
10407
|
-
const res =
|
|
11746
|
+
const res = spawnSync4("ffmpeg", ["-version"], { encoding: "utf8" });
|
|
10408
11747
|
if (res.status !== 0 || !res.stdout) return null;
|
|
10409
11748
|
const m = /ffmpeg version (\S+)/.exec(res.stdout);
|
|
10410
11749
|
return m ? m[1] : "present";
|
|
@@ -10442,7 +11781,7 @@ function skillsFacts(homes) {
|
|
|
10442
11781
|
async function doctorFacts(opts) {
|
|
10443
11782
|
return {
|
|
10444
11783
|
node: process.versions.node,
|
|
10445
|
-
browser:
|
|
11784
|
+
browser: probe2(),
|
|
10446
11785
|
ffmpeg: ffmpegVersion(),
|
|
10447
11786
|
skills: skillsFacts(opts.homes),
|
|
10448
11787
|
credential: resolveCredential() !== null,
|
|
@@ -10450,7 +11789,7 @@ async function doctorFacts(opts) {
|
|
|
10450
11789
|
};
|
|
10451
11790
|
}
|
|
10452
11791
|
async function cmdSetup(argv) {
|
|
10453
|
-
const { flags } = parseArgs(argv,
|
|
11792
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
|
|
10454
11793
|
const r = reporterFor(flags);
|
|
10455
11794
|
const root = process.cwd();
|
|
10456
11795
|
const global = flags.global === true;
|
|
@@ -10483,7 +11822,7 @@ async function cmdSetup(argv) {
|
|
|
10483
11822
|
...global ? ["-g"] : []
|
|
10484
11823
|
];
|
|
10485
11824
|
r.log(`installing the vos skills: npx ${args.join(" ")}`);
|
|
10486
|
-
const res =
|
|
11825
|
+
const res = spawnSync4(npx(), args, {
|
|
10487
11826
|
cwd: root,
|
|
10488
11827
|
encoding: "utf8",
|
|
10489
11828
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -10501,9 +11840,9 @@ async function cmdSetup(argv) {
|
|
|
10501
11840
|
const targets = homes.length ? homes : [fallbackSkillsHome(root)];
|
|
10502
11841
|
for (const home of targets) {
|
|
10503
11842
|
const dest = skillsDirFor(home);
|
|
10504
|
-
|
|
11843
|
+
mkdirSync6(dest, { recursive: true });
|
|
10505
11844
|
for (const name of installedSkills(bundled)) {
|
|
10506
|
-
cpSync(
|
|
11845
|
+
cpSync(join26(bundled, name), join26(dest, name), { recursive: true });
|
|
10507
11846
|
}
|
|
10508
11847
|
}
|
|
10509
11848
|
skillsVia = "bundled";
|
|
@@ -10520,27 +11859,27 @@ async function cmdSetup(argv) {
|
|
|
10520
11859
|
].sort();
|
|
10521
11860
|
}
|
|
10522
11861
|
r.event({ event: "skills", via: skillsVia, names: skillNames });
|
|
10523
|
-
let browser =
|
|
11862
|
+
let browser = probe2();
|
|
10524
11863
|
if (browser.kind === "none" && !noBrowser) {
|
|
10525
11864
|
r.log(
|
|
10526
11865
|
"no browser found; installing Chromium: npx playwright install chromium"
|
|
10527
11866
|
);
|
|
10528
|
-
|
|
11867
|
+
spawnSync4(npx(), ["playwright", "install", "chromium"], {
|
|
10529
11868
|
cwd: root,
|
|
10530
11869
|
stdio: ["ignore", "ignore", "inherit"],
|
|
10531
11870
|
shell: process.platform === "win32",
|
|
10532
11871
|
timeout: 6e5
|
|
10533
11872
|
});
|
|
10534
|
-
browser =
|
|
11873
|
+
browser = probe2();
|
|
10535
11874
|
}
|
|
10536
11875
|
r.event({ event: "browser", kind: browser.kind, path: browser.path });
|
|
10537
11876
|
r.log(`browser: ${browser.line}`);
|
|
10538
11877
|
let rules = null;
|
|
10539
11878
|
if (!noRules) {
|
|
10540
11879
|
const file = rulesFileFor(root);
|
|
10541
|
-
const existing =
|
|
11880
|
+
const existing = existsSync22(file) ? readFileSync9(file, "utf8") : null;
|
|
10542
11881
|
const { text, changed } = applyRulesBlock(existing);
|
|
10543
|
-
if (changed)
|
|
11882
|
+
if (changed) writeFileSync6(file, text);
|
|
10544
11883
|
rules = { file, changed };
|
|
10545
11884
|
r.log(
|
|
10546
11885
|
changed ? `rules: wrote the vosso block into ${file}` : `rules: ${file} already carries the block`
|
|
@@ -10569,7 +11908,7 @@ Next: ${report.next_step}`
|
|
|
10569
11908
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
10570
11909
|
}
|
|
10571
11910
|
async function cmdDoctor(argv) {
|
|
10572
|
-
const { flags } = parseArgs(argv,
|
|
11911
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
|
|
10573
11912
|
const r = reporterFor(flags);
|
|
10574
11913
|
const root = process.cwd();
|
|
10575
11914
|
const homes = detectAgents(root, { home: homedir2(), global: true });
|
|
@@ -10588,7 +11927,7 @@ Next: ${report.next_step}`
|
|
|
10588
11927
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
10589
11928
|
}
|
|
10590
11929
|
async function cmdWhoami(argv) {
|
|
10591
|
-
const { flags } = parseArgs(argv,
|
|
11930
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
|
|
10592
11931
|
const r = reporterFor(flags);
|
|
10593
11932
|
const origin = platformOrigin({
|
|
10594
11933
|
origin: strFlag(flags, "origin"),
|
|
@@ -10650,9 +11989,9 @@ async function cmdWhoami(argv) {
|
|
|
10650
11989
|
return EXIT_OK;
|
|
10651
11990
|
}
|
|
10652
11991
|
function cmdLogout(argv) {
|
|
10653
|
-
const { flags } = parseArgs(argv,
|
|
11992
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
|
|
10654
11993
|
const r = reporterFor(flags);
|
|
10655
|
-
const had =
|
|
11994
|
+
const had = existsSync22(CREDENTIALS_PATH);
|
|
10656
11995
|
rmSync3(CREDENTIALS_PATH, { force: true });
|
|
10657
11996
|
const envKey = Boolean(process.env.VOS_API_KEY?.trim());
|
|
10658
11997
|
r.done(
|
|
@@ -10668,7 +12007,7 @@ function cmdLogout(argv) {
|
|
|
10668
12007
|
}
|
|
10669
12008
|
|
|
10670
12009
|
// src/plugin/run.ts
|
|
10671
|
-
var
|
|
12010
|
+
var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set([
|
|
10672
12011
|
"json",
|
|
10673
12012
|
"help",
|
|
10674
12013
|
"wait",
|
|
@@ -10714,6 +12053,14 @@ Take pipeline
|
|
|
10714
12053
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
10715
12054
|
vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]
|
|
10716
12055
|
a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so. Without a trace the file opens as FINISHED footage (a render from HyperFrames or Remotion, a film): no card, no browser bar, no drawn cursor, its own frame rate; --as take gives a screen demo the card
|
|
12056
|
+
vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]
|
|
12057
|
+
a finished piece (HyperFrames, Remotion's render, a hand-rolled page) read as a browser renders it: words, palette, faces (catalog or missing), media, scenes, gaps \u2192 port/inventory.json, with stills from its render
|
|
12058
|
+
vos port scaffold [--from port/inventory.json] [--out port/program] [--json]
|
|
12059
|
+
a data-first program from it: every word a bound element, every colour a key, knobs, a label and a TODO per scene, the score as a doc.json track, port/REPORT.md
|
|
12060
|
+
vos build <program.mjs> [--out config.json] [--json]
|
|
12061
|
+
real functions \u2192 config.json, refusing a module-scope read, syntax the page cannot run and non-JSON data; config.sources.json maps each function to its line
|
|
12062
|
+
vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--threshold 0.95] [--out compare] [--json]
|
|
12063
|
+
per-frame SSIM and source | vos | difference sheets; exit 1 when ANY frame is under the threshold (needs ffmpeg)
|
|
10717
12064
|
vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
|
|
10718
12065
|
vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
|
|
10719
12066
|
vos session list | rm <name> what exists and how old; delete one
|
|
@@ -11047,8 +12394,8 @@ function takeBrowserArgs(multi) {
|
|
|
11047
12394
|
function takeStorageState(flags) {
|
|
11048
12395
|
const raw = strFlag(flags, "storage-state");
|
|
11049
12396
|
if (!raw) return void 0;
|
|
11050
|
-
const path =
|
|
11051
|
-
if (!
|
|
12397
|
+
const path = resolve15(raw);
|
|
12398
|
+
if (!existsSync23(path))
|
|
11052
12399
|
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
11053
12400
|
return path;
|
|
11054
12401
|
}
|
|
@@ -11061,7 +12408,7 @@ function takeSessionName(flags, storageState) {
|
|
|
11061
12408
|
throw new UsageError(
|
|
11062
12409
|
"--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
|
|
11063
12410
|
);
|
|
11064
|
-
if (!
|
|
12411
|
+
if (!existsSync23(sessionDir(name)))
|
|
11065
12412
|
throw new UsageError(
|
|
11066
12413
|
`no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
|
|
11067
12414
|
);
|
|
@@ -11117,7 +12464,7 @@ function wallGate(flags, r, opts) {
|
|
|
11117
12464
|
async function cmdRecord(argv) {
|
|
11118
12465
|
const { positionals, flags, multi } = parseArgs(
|
|
11119
12466
|
argv,
|
|
11120
|
-
|
|
12467
|
+
BOOLEAN_FLAGS11,
|
|
11121
12468
|
MULTI_FLAGS2
|
|
11122
12469
|
);
|
|
11123
12470
|
const actionsPath = strFlag(flags, "actions") ?? positionals[0];
|
|
@@ -11130,7 +12477,7 @@ async function cmdRecord(argv) {
|
|
|
11130
12477
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
11131
12478
|
if (!url)
|
|
11132
12479
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
11133
|
-
const outDir =
|
|
12480
|
+
const outDir = resolve15(strFlag(flags, "out") ?? "take");
|
|
11134
12481
|
const storageState = takeStorageState(flags);
|
|
11135
12482
|
const sessionName = takeSessionName(flags, storageState);
|
|
11136
12483
|
const browserArgs = takeBrowserArgs(multi);
|
|
@@ -11284,7 +12631,7 @@ ${lines.join("\n")}
|
|
|
11284
12631
|
}
|
|
11285
12632
|
const paths = takePaths(outDir);
|
|
11286
12633
|
const prepare = async () => {
|
|
11287
|
-
if (
|
|
12634
|
+
if (existsSync23(join27(outDir, "meta.json"))) {
|
|
11288
12635
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
11289
12636
|
r.log(
|
|
11290
12637
|
`note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
|
|
@@ -11349,7 +12696,7 @@ ${lines.join("\n")}
|
|
|
11349
12696
|
},
|
|
11350
12697
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
11351
12698
|
${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}
|
|
11352
|
-
Next: edit ${
|
|
12699
|
+
Next: edit ${join27(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
|
|
11353
12700
|
);
|
|
11354
12701
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
11355
12702
|
} finally {
|
|
@@ -11359,7 +12706,7 @@ ${lines.join("\n")}
|
|
|
11359
12706
|
async function cmdCreate2(argv) {
|
|
11360
12707
|
const { positionals, flags, multi } = parseArgs(
|
|
11361
12708
|
argv,
|
|
11362
|
-
|
|
12709
|
+
BOOLEAN_FLAGS11,
|
|
11363
12710
|
MULTI_FLAGS2
|
|
11364
12711
|
);
|
|
11365
12712
|
const actionsPath = strFlag(flags, "actions");
|
|
@@ -11373,9 +12720,9 @@ async function cmdCreate2(argv) {
|
|
|
11373
12720
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
11374
12721
|
if (!url)
|
|
11375
12722
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
11376
|
-
const outDir =
|
|
12723
|
+
const outDir = resolve15(strFlag(flags, "out") ?? "take");
|
|
11377
12724
|
const format = videoFormat(strFlag(flags, "format"), positionals[0]);
|
|
11378
|
-
const out = positionals[0] ??
|
|
12725
|
+
const out = positionals[0] ?? join27(outDir, `out.${format}`);
|
|
11379
12726
|
const parallel = numFlag(flags, "parallel", 1);
|
|
11380
12727
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
11381
12728
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -11388,7 +12735,7 @@ async function cmdCreate2(argv) {
|
|
|
11388
12735
|
const headers = takeHeaders(multi);
|
|
11389
12736
|
const paths = takePaths(outDir);
|
|
11390
12737
|
const prepare = async () => {
|
|
11391
|
-
if (
|
|
12738
|
+
if (existsSync23(join27(outDir, "meta.json"))) {
|
|
11392
12739
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
11393
12740
|
r.log(
|
|
11394
12741
|
`note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
|
|
@@ -11485,7 +12832,7 @@ async function cmdCreate2(argv) {
|
|
|
11485
12832
|
async function cmdPlan(argv) {
|
|
11486
12833
|
const { positionals, flags, multi } = parseArgs(
|
|
11487
12834
|
argv,
|
|
11488
|
-
|
|
12835
|
+
BOOLEAN_FLAGS11,
|
|
11489
12836
|
/* @__PURE__ */ new Set(["with"])
|
|
11490
12837
|
);
|
|
11491
12838
|
const dir = positionals[0];
|
|
@@ -11495,13 +12842,13 @@ async function cmdPlan(argv) {
|
|
|
11495
12842
|
);
|
|
11496
12843
|
const r = createReporter(flags.json === true);
|
|
11497
12844
|
if (flags.fresh === true) {
|
|
11498
|
-
await rm5(
|
|
12845
|
+
await rm5(join27(dir, "doc.json"), { force: true });
|
|
11499
12846
|
r.log("note: --fresh discarded the existing doc.json");
|
|
11500
12847
|
}
|
|
11501
12848
|
let reuse;
|
|
11502
12849
|
if (flags.reuse === true) {
|
|
11503
|
-
const from =
|
|
11504
|
-
if (!
|
|
12850
|
+
const from = resolve15(strFlag(flags, "from") ?? join27(dir, PREV_DOC_NAME));
|
|
12851
|
+
if (!existsSync23(from)) {
|
|
11505
12852
|
throw new UsageError(
|
|
11506
12853
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
11507
12854
|
);
|
|
@@ -11512,7 +12859,7 @@ async function cmdPlan(argv) {
|
|
|
11512
12859
|
};
|
|
11513
12860
|
}
|
|
11514
12861
|
const style = await resolveStyleRef(flags);
|
|
11515
|
-
const hasDoc =
|
|
12862
|
+
const hasDoc = existsSync23(join27(dir, "doc.json"));
|
|
11516
12863
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
11517
12864
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
11518
12865
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
@@ -11558,7 +12905,7 @@ async function cmdPlan(argv) {
|
|
|
11558
12905
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
11559
12906
|
r.done(
|
|
11560
12907
|
{
|
|
11561
|
-
take:
|
|
12908
|
+
take: resolve15(dir),
|
|
11562
12909
|
fresh: s.fresh,
|
|
11563
12910
|
cursorKept: s.cursorKept,
|
|
11564
12911
|
zoomAuto: s.zoomAuto,
|
|
@@ -11570,7 +12917,7 @@ async function cmdPlan(argv) {
|
|
|
11570
12917
|
...s.motion ? { motion: s.motion } : {},
|
|
11571
12918
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
11572
12919
|
},
|
|
11573
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
12920
|
+
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join27(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
|
|
11574
12921
|
);
|
|
11575
12922
|
return EXIT_OK;
|
|
11576
12923
|
}
|
|
@@ -11683,7 +13030,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
|
|
|
11683
13030
|
async function cmdRender(argv) {
|
|
11684
13031
|
const { positionals, flags, multi } = parseArgs(
|
|
11685
13032
|
argv,
|
|
11686
|
-
|
|
13033
|
+
BOOLEAN_FLAGS11,
|
|
11687
13034
|
MULTI_FLAGS2
|
|
11688
13035
|
);
|
|
11689
13036
|
const dir = positionals[0];
|
|
@@ -11693,7 +13040,7 @@ async function cmdRender(argv) {
|
|
|
11693
13040
|
);
|
|
11694
13041
|
const r = createReporter(flags.json === true);
|
|
11695
13042
|
const format = videoFormat(strFlag(flags, "format"), positionals[1]);
|
|
11696
|
-
const out = positionals[1] ??
|
|
13043
|
+
const out = positionals[1] ?? join27(dir, `out.${format}`);
|
|
11697
13044
|
const parallel = numFlag(flags, "parallel", 1);
|
|
11698
13045
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
11699
13046
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -11751,7 +13098,7 @@ async function cmdRender(argv) {
|
|
|
11751
13098
|
async function cmdFrames(argv) {
|
|
11752
13099
|
const { positionals, flags, multi } = parseArgs(
|
|
11753
13100
|
argv,
|
|
11754
|
-
|
|
13101
|
+
BOOLEAN_FLAGS11,
|
|
11755
13102
|
MULTI_FLAGS2
|
|
11756
13103
|
);
|
|
11757
13104
|
const dir = positionals[0];
|
|
@@ -11777,7 +13124,7 @@ async function cmdFrames(argv) {
|
|
|
11777
13124
|
const atStill = flags["at-still"] === true;
|
|
11778
13125
|
let times;
|
|
11779
13126
|
try {
|
|
11780
|
-
times = frameRaw !== void 0 ?
|
|
13127
|
+
times = frameRaw !== void 0 ? parseTimes2(frameRaw, duration) : timesRaw !== void 0 ? parseTimes2(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes2("0,25%,50%,75%,100%", duration);
|
|
11781
13128
|
} catch (e) {
|
|
11782
13129
|
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
11783
13130
|
}
|
|
@@ -11824,7 +13171,7 @@ async function cmdFrames(argv) {
|
|
|
11824
13171
|
async function cmdDeliver(argv) {
|
|
11825
13172
|
const { positionals, flags, multi } = parseArgs(
|
|
11826
13173
|
argv,
|
|
11827
|
-
|
|
13174
|
+
BOOLEAN_FLAGS11,
|
|
11828
13175
|
MULTI_FLAGS2
|
|
11829
13176
|
);
|
|
11830
13177
|
const dir = positionals[0];
|
|
@@ -11864,7 +13211,7 @@ async function cmdDeliver(argv) {
|
|
|
11864
13211
|
if (timesRaw !== void 0) {
|
|
11865
13212
|
try {
|
|
11866
13213
|
const doc = take.doc;
|
|
11867
|
-
times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ??
|
|
13214
|
+
times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ?? parseTimes2(s, duration)[0]);
|
|
11868
13215
|
} catch (e) {
|
|
11869
13216
|
throw new UsageError(e instanceof Error ? e.message : String(e));
|
|
11870
13217
|
}
|
|
@@ -11940,11 +13287,11 @@ async function resolveStyleRef(flags) {
|
|
|
11940
13287
|
}
|
|
11941
13288
|
async function resolveDocRef(ref, flags, what) {
|
|
11942
13289
|
const styleRef = ref;
|
|
11943
|
-
const file =
|
|
13290
|
+
const file = existsSync23(styleRef) ? resolve15(
|
|
11944
13291
|
styleRef,
|
|
11945
|
-
|
|
13292
|
+
existsSync23(join27(styleRef, "doc.json")) ? "doc.json" : ""
|
|
11946
13293
|
) : null;
|
|
11947
|
-
if (file &&
|
|
13294
|
+
if (file && existsSync23(file)) {
|
|
11948
13295
|
return {
|
|
11949
13296
|
from: file,
|
|
11950
13297
|
doc: migrateHostedDoc5(
|
|
@@ -11990,7 +13337,7 @@ async function officialTemplate(origin, ref) {
|
|
|
11990
13337
|
}
|
|
11991
13338
|
}
|
|
11992
13339
|
async function cmdDigest(argv) {
|
|
11993
|
-
const { positionals, flags } = parseArgs(argv,
|
|
13340
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
11994
13341
|
const dir = positionals[0];
|
|
11995
13342
|
if (!dir)
|
|
11996
13343
|
throw new UsageError(
|
|
@@ -12031,7 +13378,7 @@ async function cmdDigest(argv) {
|
|
|
12031
13378
|
kinds,
|
|
12032
13379
|
frames: d.images.full,
|
|
12033
13380
|
crops: d.images.crop,
|
|
12034
|
-
sheet: d.images.sheet ?
|
|
13381
|
+
sheet: d.images.sheet ? join27(result.outDir, d.images.sheet) : null,
|
|
12035
13382
|
scenes: kinds.scene ?? 0,
|
|
12036
13383
|
sourceDuration: d.take.sourceDuration,
|
|
12037
13384
|
outputDuration: d.take.outputDuration,
|
|
@@ -12054,19 +13401,19 @@ async function cmdDigest(argv) {
|
|
|
12054
13401
|
}
|
|
12055
13402
|
}
|
|
12056
13403
|
async function cmdOpen(argv) {
|
|
12057
|
-
const { positionals, flags } = parseArgs(argv,
|
|
13404
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
12058
13405
|
const dir = positionals[0];
|
|
12059
13406
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
12060
13407
|
const r = createReporter(flags.json === true);
|
|
12061
13408
|
const take = await loadTake(dir);
|
|
12062
|
-
if (!
|
|
13409
|
+
if (!existsSync23(take.paths.recording)) {
|
|
12063
13410
|
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
12064
13411
|
}
|
|
12065
13412
|
const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
|
|
12066
13413
|
/\/+$/,
|
|
12067
13414
|
""
|
|
12068
13415
|
);
|
|
12069
|
-
const server = await startTakeServer(
|
|
13416
|
+
const server = await startTakeServer(resolve15(dir), {});
|
|
12070
13417
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
12071
13418
|
r.event({ event: "open", server: server.base, url });
|
|
12072
13419
|
r.log(`take served at ${server.base}`);
|
|
@@ -12092,7 +13439,7 @@ async function cmdOpen(argv) {
|
|
|
12092
13439
|
return EXIT_OK;
|
|
12093
13440
|
}
|
|
12094
13441
|
async function cmdJudge(argv) {
|
|
12095
|
-
const { positionals, flags } = parseArgs(argv,
|
|
13442
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
12096
13443
|
const target = positionals[0];
|
|
12097
13444
|
const against = strFlag(flags, "against");
|
|
12098
13445
|
if (!target || !against)
|
|
@@ -12100,9 +13447,9 @@ async function cmdJudge(argv) {
|
|
|
12100
13447
|
"vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]\n(the manifest names the reference set: id, file, role, layout, facts, rule per asset)"
|
|
12101
13448
|
);
|
|
12102
13449
|
const r = createReporter(flags.json === true);
|
|
12103
|
-
const kitFile = target.endsWith("kit.json") ? target :
|
|
12104
|
-
if (!
|
|
12105
|
-
if (!
|
|
13450
|
+
const kitFile = target.endsWith("kit.json") ? target : join27(target, "kit.json");
|
|
13451
|
+
if (!existsSync23(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
13452
|
+
if (!existsSync23(against))
|
|
12106
13453
|
throw new UsageError(`${against}: no reference manifest`);
|
|
12107
13454
|
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
12108
13455
|
const verdicts = JSON.parse(await readFile17(result.verdictFile, "utf8")).verdicts;
|
|
@@ -12121,7 +13468,7 @@ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judg
|
|
|
12121
13468
|
return EXIT_OK;
|
|
12122
13469
|
}
|
|
12123
13470
|
async function cmdCallout(argv) {
|
|
12124
|
-
const { positionals, flags } = parseArgs(argv,
|
|
13471
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
12125
13472
|
const dir = positionals[0];
|
|
12126
13473
|
const shape = positionals[1];
|
|
12127
13474
|
if (!dir || !shape || !isCalloutShape(shape))
|
|
@@ -12220,11 +13567,11 @@ async function cmdCallout(argv) {
|
|
|
12220
13567
|
return EXIT_OK;
|
|
12221
13568
|
}
|
|
12222
13569
|
async function cmdValidate(argv) {
|
|
12223
|
-
const { positionals, flags } = parseArgs(argv,
|
|
13570
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
12224
13571
|
const target = positionals[0];
|
|
12225
13572
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
12226
13573
|
const r = createReporter(flags.json === true);
|
|
12227
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
13574
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync23(join27(target, "kit.json")) && !isTakeDir(target) ? join27(target, "kit.json") : null;
|
|
12228
13575
|
if (kitTarget) {
|
|
12229
13576
|
const picture = flags.picture === true;
|
|
12230
13577
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -12256,9 +13603,9 @@ async function cmdValidate(argv) {
|
|
|
12256
13603
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
12257
13604
|
return EXIT_OK;
|
|
12258
13605
|
}
|
|
12259
|
-
if (!isTakeDir(target) &&
|
|
13606
|
+
if (!isTakeDir(target) && existsSync23(join27(target, "config.json"))) {
|
|
12260
13607
|
const parsed = JSON.parse(
|
|
12261
|
-
await readFile17(
|
|
13608
|
+
await readFile17(join27(target, "config.json"), "utf8")
|
|
12262
13609
|
);
|
|
12263
13610
|
const pre = preflightConfig(parsed);
|
|
12264
13611
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -12274,9 +13621,9 @@ async function cmdValidate(argv) {
|
|
|
12274
13621
|
...collectConfigMediaUrls(pre.config ?? parsed),
|
|
12275
13622
|
...collectDocMediaUrls(doc)
|
|
12276
13623
|
];
|
|
12277
|
-
const
|
|
12278
|
-
problems2.push(...
|
|
12279
|
-
warnings2.push(...
|
|
13624
|
+
const probe3 = mediaProbeLints(await probeMediaUrls([...new Set(urls)]));
|
|
13625
|
+
problems2.push(...probe3.problems);
|
|
13626
|
+
warnings2.push(...probe3.warnings);
|
|
12280
13627
|
}
|
|
12281
13628
|
if (problems2.length) {
|
|
12282
13629
|
r.done(
|
|
@@ -12299,7 +13646,7 @@ async function cmdValidate(argv) {
|
|
|
12299
13646
|
const take = await loadTake(target);
|
|
12300
13647
|
const problems = [];
|
|
12301
13648
|
const warnings = [];
|
|
12302
|
-
if (!
|
|
13649
|
+
if (!existsSync23(take.paths.recording)) {
|
|
12303
13650
|
problems.push("missing recording (re-run record)");
|
|
12304
13651
|
} else {
|
|
12305
13652
|
const mismatch = containerMismatch(
|
|
@@ -12325,11 +13672,11 @@ async function cmdValidate(argv) {
|
|
|
12325
13672
|
const lint = lintDoc(take.doc);
|
|
12326
13673
|
problems.push(...lint.problems.map((p) => `doc.json: ${p}`));
|
|
12327
13674
|
warnings.push(...lint.warnings.map((w) => `doc.json: ${w}`));
|
|
12328
|
-
const
|
|
13675
|
+
const probe3 = mediaProbeLints(
|
|
12329
13676
|
await probeMediaUrls(collectDocMediaUrls(take.doc))
|
|
12330
13677
|
);
|
|
12331
|
-
problems.push(...
|
|
12332
|
-
warnings.push(...
|
|
13678
|
+
problems.push(...probe3.problems);
|
|
13679
|
+
warnings.push(...probe3.warnings);
|
|
12333
13680
|
if (flags.picture === true && (take.doc.overlays ?? []).some((o) => o.kind === "html")) {
|
|
12334
13681
|
const doc = take.doc;
|
|
12335
13682
|
const times = [
|
|
@@ -12382,7 +13729,7 @@ async function cmdValidate(argv) {
|
|
|
12382
13729
|
async function cmdPush3(argv) {
|
|
12383
13730
|
const { positionals, flags, multi } = parseArgs(
|
|
12384
13731
|
argv,
|
|
12385
|
-
|
|
13732
|
+
BOOLEAN_FLAGS11,
|
|
12386
13733
|
MULTI_FLAGS2
|
|
12387
13734
|
);
|
|
12388
13735
|
const dir = positionals[0];
|
|
@@ -12392,7 +13739,7 @@ async function cmdPush3(argv) {
|
|
|
12392
13739
|
);
|
|
12393
13740
|
const refusal = takePushRefusal({
|
|
12394
13741
|
vos: strFlag(flags, "vos"),
|
|
12395
|
-
trackedVosId: readSyncState(
|
|
13742
|
+
trackedVosId: readSyncState(resolve15(dir))?.vosId ?? null,
|
|
12396
13743
|
programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
|
|
12397
13744
|
(name) => hasFlag(flags, name)
|
|
12398
13745
|
)
|
|
@@ -12401,7 +13748,7 @@ async function cmdPush3(argv) {
|
|
|
12401
13748
|
const r = createReporter(flags.json === true);
|
|
12402
13749
|
const overrides = multi.override;
|
|
12403
13750
|
const result = await pushTake(
|
|
12404
|
-
|
|
13751
|
+
resolve15(dir),
|
|
12405
13752
|
{
|
|
12406
13753
|
key: strFlag(flags, "key"),
|
|
12407
13754
|
api: strFlag(flags, "api"),
|
|
@@ -12446,11 +13793,11 @@ ${landedLines(landed)}` : "")
|
|
|
12446
13793
|
return EXIT_OK;
|
|
12447
13794
|
}
|
|
12448
13795
|
async function cmdPull2(argv) {
|
|
12449
|
-
const { positionals, flags } = parseArgs(argv,
|
|
13796
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11, MULTI_FLAGS2);
|
|
12450
13797
|
const dir = positionals[0] ?? ".";
|
|
12451
13798
|
const r = createReporter(flags.json === true);
|
|
12452
13799
|
const result = await pullTake(
|
|
12453
|
-
|
|
13800
|
+
resolve15(dir),
|
|
12454
13801
|
{
|
|
12455
13802
|
key: strFlag(flags, "key"),
|
|
12456
13803
|
api: strFlag(flags, "api"),
|
|
@@ -12559,6 +13906,12 @@ async function run(argv) {
|
|
|
12559
13906
|
}
|
|
12560
13907
|
case "delete":
|
|
12561
13908
|
return await cmdDelete(rest);
|
|
13909
|
+
case "build":
|
|
13910
|
+
return await cmdBuild(rest);
|
|
13911
|
+
case "compare":
|
|
13912
|
+
return await cmdCompare(rest);
|
|
13913
|
+
case "port":
|
|
13914
|
+
return await cmdPort(rest);
|
|
12562
13915
|
case "folder":
|
|
12563
13916
|
return await cmdFolder(rest);
|
|
12564
13917
|
case "asset":
|
|
@@ -12605,7 +13958,7 @@ async function run(argv) {
|
|
|
12605
13958
|
}
|
|
12606
13959
|
}
|
|
12607
13960
|
async function cmdSession(argv) {
|
|
12608
|
-
const { positionals, flags } = parseArgs(argv,
|
|
13961
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
|
|
12609
13962
|
const [sub, arg] = positionals;
|
|
12610
13963
|
const r = createReporter(flags.json === true);
|
|
12611
13964
|
const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
|
|
@@ -12624,38 +13977,38 @@ async function cmdSession(argv) {
|
|
|
12624
13977
|
if (!/^https?:$/.test(url.protocol))
|
|
12625
13978
|
throw new UsageError("vos session open wants an http(s) URL");
|
|
12626
13979
|
await openSession(name, url.href, { log: r.log });
|
|
12627
|
-
const
|
|
13980
|
+
const summary3 = await summarizeSession(name);
|
|
12628
13981
|
r.done(
|
|
12629
13982
|
{
|
|
12630
|
-
name:
|
|
12631
|
-
origins:
|
|
12632
|
-
newestExpires:
|
|
12633
|
-
sessionOnly:
|
|
13983
|
+
name: summary3.name,
|
|
13984
|
+
origins: summary3.origins,
|
|
13985
|
+
newestExpires: summary3.newestExpires,
|
|
13986
|
+
sessionOnly: summary3.sessionOnly
|
|
12634
13987
|
},
|
|
12635
|
-
`${summaryLine(
|
|
13988
|
+
`${summaryLine(summary3)}
|
|
12636
13989
|
Record on it: vos record --actions actions.json --out take --session ${name} --strict`
|
|
12637
13990
|
);
|
|
12638
|
-
return
|
|
13991
|
+
return summary3.origins.length ? EXIT_OK : EXIT_ERROR;
|
|
12639
13992
|
}
|
|
12640
13993
|
case "check": {
|
|
12641
13994
|
if (!arg) throw new UsageError(usage);
|
|
12642
13995
|
const bad = sessionNameVerdict(arg);
|
|
12643
13996
|
if (bad) throw new UsageError(bad);
|
|
12644
|
-
if (!
|
|
13997
|
+
if (!existsSync23(sessionDir(arg)))
|
|
12645
13998
|
throw new UsageError(`no session "${arg}" (vos session list)`);
|
|
12646
|
-
const
|
|
13999
|
+
const summary3 = await summarizeSession(arg);
|
|
12647
14000
|
const url = strFlag(flags, "url");
|
|
12648
14001
|
if (!url) {
|
|
12649
14002
|
r.done(
|
|
12650
14003
|
{
|
|
12651
14004
|
name: arg,
|
|
12652
|
-
origins:
|
|
12653
|
-
newestExpires:
|
|
12654
|
-
sessionOnly:
|
|
14005
|
+
origins: summary3.origins,
|
|
14006
|
+
newestExpires: summary3.newestExpires,
|
|
14007
|
+
sessionOnly: summary3.sessionOnly
|
|
12655
14008
|
},
|
|
12656
|
-
summaryLine(
|
|
14009
|
+
summaryLine(summary3)
|
|
12657
14010
|
);
|
|
12658
|
-
return
|
|
14011
|
+
return summary3.origins.length ? EXIT_OK : EXIT_WALL;
|
|
12659
14012
|
}
|
|
12660
14013
|
const verdict = await withSession(arg, async (ctx) => {
|
|
12661
14014
|
const page = await ctx.newPage();
|
|
@@ -12676,8 +14029,8 @@ async function cmdSession(argv) {
|
|
|
12676
14029
|
name: arg,
|
|
12677
14030
|
ok: false,
|
|
12678
14031
|
wall: verdict,
|
|
12679
|
-
origins:
|
|
12680
|
-
newestExpires:
|
|
14032
|
+
origins: summary3.origins,
|
|
14033
|
+
newestExpires: summary3.newestExpires
|
|
12681
14034
|
},
|
|
12682
14035
|
`session "${arg}" no longer opens ${url}: ${verdict.message.split(". ")[0]}. It expired, or the sign-in never landed. Sign in again: vos session open ${url} --name ${arg}`
|
|
12683
14036
|
);
|
|
@@ -12687,10 +14040,10 @@ async function cmdSession(argv) {
|
|
|
12687
14040
|
{
|
|
12688
14041
|
name: arg,
|
|
12689
14042
|
ok: true,
|
|
12690
|
-
origins:
|
|
12691
|
-
newestExpires:
|
|
14043
|
+
origins: summary3.origins,
|
|
14044
|
+
newestExpires: summary3.newestExpires
|
|
12692
14045
|
},
|
|
12693
|
-
`session "${arg}" opens ${url} signed in. ${summaryLine(
|
|
14046
|
+
`session "${arg}" opens ${url} signed in. ${summaryLine(summary3)}`
|
|
12694
14047
|
);
|
|
12695
14048
|
return EXIT_OK;
|
|
12696
14049
|
}
|
|
@@ -12745,4 +14098,4 @@ export {
|
|
|
12745
14098
|
verbHelp,
|
|
12746
14099
|
run
|
|
12747
14100
|
};
|
|
12748
|
-
//# sourceMappingURL=chunk-
|
|
14101
|
+
//# sourceMappingURL=chunk-M22MAUMZ.js.map
|