Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"files": 144
},
"faceless-explainer": {
"hash": "e486bec3499bd84d",
"hash": "3873fd4a76a59907",
"files": 19
},
"figma": {
Expand Down Expand Up @@ -58,12 +58,12 @@
"files": 132
},
"pr-to-video": {
"hash": "739774abd773a3d3",
"hash": "706dc7fbf2362ab9",
"files": 28
},
"product-launch-video": {
"hash": "86fcf6421ab9aa8f",
"files": 23
"hash": "dae461ff0dc67f35",
"files": 24
},
"remotion-to-hyperframes": {
"hash": "c96bb2f0af9e1143",
Expand Down
33 changes: 24 additions & 9 deletions skills/faceless-explainer/scripts/lib/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
// Shared by stage-assets.mjs (Step 4 close, BEFORE the frame workers run) and
// assemble-index.mjs (Step 5, idempotent backstop). Only assets a frame names
// in `asset_candidates` are staged; unnamed assets never reach the project.
// asset_candidates value form: "assets/<basename> — desc; assets/… — …".
// asset_candidates value form: "assets/<relative-path> — desc; assets/… — …".

import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { basename, join } from "node:path";
import { basename, dirname, join, posix } from "node:path";

export function basenamesFromCandidates(value) {
export function assetPathsFromCandidates(value) {
if (typeof value !== "string") return [];
return value
.split(";")
.map((seg) => seg.split(/\s+[—–-]\s+/)[0].trim()) // strip the " — description"
.filter(Boolean)
.map((p) => basename(p.replace(/^assets\//, "")));
.map((path) => path.replaceAll("\\", "/").replace(/^assets\//, ""))
.map((path) => posix.normalize(path))
.filter(
(path) =>
path !== "." &&
!path.startsWith("../") &&
!posix.isAbsolute(path) &&
!/^[A-Za-z]:\//.test(path),
);
}

export function basenamesFromCandidates(value) {
return assetPathsFromCandidates(value).map((path) => basename(path));
}

// Copy each frame's asset_candidates from capture/{assets,assets/videos,
Expand All @@ -22,7 +34,7 @@ export function basenamesFromCandidates(value) {
export function stageAssets({ hyperframesDir, frames }) {
const wanted = new Set();
for (const f of frames) {
for (const b of basenamesFromCandidates(f.extra?.asset_candidates)) wanted.add(b);
for (const path of assetPathsFromCandidates(f.extra?.asset_candidates)) wanted.add(path);
}
const captureDirs = [
join(hyperframesDir, "capture/assets"),
Expand All @@ -34,19 +46,22 @@ export function stageAssets({ hyperframesDir, frames }) {
let staged = 0;
if (wanted.size > 0) {
mkdirSync(assetsDir, { recursive: true });
for (const b of wanted) {
const dest = join(assetsDir, b);
for (const assetPath of wanted) {
const dest = join(assetsDir, assetPath);
if (existsSync(dest)) {
staged++;
continue;
} // first-wins / already staged
const src = captureDirs.map((d) => join(d, b)).find((p) => existsSync(p));
const src = captureDirs
.flatMap((dir) => [join(dir, assetPath), join(dir, basename(assetPath))])
.find((path) => existsSync(path));
if (src) {
mkdirSync(dirname(dest), { recursive: true });
copyFileSync(src, dest);
staged++;
} else {
anomalies.push(
`asset "${b}" named by a frame but not found under capture/ — frame will 404 it`,
`asset "${assetPath}" named by a frame but not found under capture/ — frame will 404 it`,
);
}
}
Expand Down
33 changes: 24 additions & 9 deletions skills/pr-to-video/scripts/lib/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
// Shared by stage-assets.mjs (Step 4 close, BEFORE the frame workers run) and
// assemble-index.mjs (Step 5, idempotent backstop). Only assets a frame names
// in `asset_candidates` are staged; unnamed assets never reach the project.
// asset_candidates value form: "assets/<basename> — desc; assets/… — …".
// asset_candidates value form: "assets/<relative-path> — desc; assets/… — …".

import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { basename, join } from "node:path";
import { basename, dirname, join, posix } from "node:path";

export function basenamesFromCandidates(value) {
export function assetPathsFromCandidates(value) {
if (typeof value !== "string") return [];
return value
.split(";")
.map((seg) => seg.split(/\s+[—–-]\s+/)[0].trim()) // strip the " — description"
.filter(Boolean)
.map((p) => basename(p.replace(/^assets\//, "")));
.map((path) => path.replaceAll("\\", "/").replace(/^assets\//, ""))
.map((path) => posix.normalize(path))
.filter(
(path) =>
path !== "." &&
!path.startsWith("../") &&
!posix.isAbsolute(path) &&
!/^[A-Za-z]:\//.test(path),
);
}

export function basenamesFromCandidates(value) {
return assetPathsFromCandidates(value).map((path) => basename(path));
}

// Copy each frame's asset_candidates from capture/{assets,assets/videos,
Expand All @@ -22,7 +34,7 @@ export function basenamesFromCandidates(value) {
export function stageAssets({ hyperframesDir, frames }) {
const wanted = new Set();
for (const f of frames) {
for (const b of basenamesFromCandidates(f.extra?.asset_candidates)) wanted.add(b);
for (const path of assetPathsFromCandidates(f.extra?.asset_candidates)) wanted.add(path);
}
const captureDirs = [
join(hyperframesDir, "capture/assets"),
Expand All @@ -34,19 +46,22 @@ export function stageAssets({ hyperframesDir, frames }) {
let staged = 0;
if (wanted.size > 0) {
mkdirSync(assetsDir, { recursive: true });
for (const b of wanted) {
const dest = join(assetsDir, b);
for (const assetPath of wanted) {
const dest = join(assetsDir, assetPath);
if (existsSync(dest)) {
staged++;
continue;
} // first-wins / already staged
const src = captureDirs.map((d) => join(d, b)).find((p) => existsSync(p));
const src = captureDirs
.flatMap((dir) => [join(dir, assetPath), join(dir, basename(assetPath))])
.find((path) => existsSync(path));
if (src) {
mkdirSync(dirname(dest), { recursive: true });
copyFileSync(src, dest);
staged++;
} else {
anomalies.push(
`asset "${b}" named by a frame but not found under capture/ — frame will 404 it`,
`asset "${assetPath}" named by a frame but not found under capture/ — frame will 404 it`,
);
}
}
Expand Down
33 changes: 24 additions & 9 deletions skills/product-launch-video/scripts/lib/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
// Shared by stage-assets.mjs (Step 4 close, BEFORE the frame workers run) and
// assemble-index.mjs (Step 5, idempotent backstop). Only assets a frame names
// in `asset_candidates` are staged; unnamed assets never reach the project.
// asset_candidates value form: "assets/<basename> — desc; assets/… — …".
// asset_candidates value form: "assets/<relative-path> — desc; assets/… — …".

import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { basename, join } from "node:path";
import { basename, dirname, join, posix } from "node:path";

export function basenamesFromCandidates(value) {
export function assetPathsFromCandidates(value) {
if (typeof value !== "string") return [];
return value
.split(";")
.map((seg) => seg.split(/\s+[—–-]\s+/)[0].trim()) // strip the " — description"
.filter(Boolean)
.map((p) => basename(p.replace(/^assets\//, "")));
.map((path) => path.replaceAll("\\", "/").replace(/^assets\//, ""))
.map((path) => posix.normalize(path))
.filter(
(path) =>
path !== "." &&
!path.startsWith("../") &&
!posix.isAbsolute(path) &&
!/^[A-Za-z]:\//.test(path),
);
}

export function basenamesFromCandidates(value) {
return assetPathsFromCandidates(value).map((path) => basename(path));
}

// Copy each frame's asset_candidates from capture/{assets,assets/videos,
Expand All @@ -22,7 +34,7 @@ export function basenamesFromCandidates(value) {
export function stageAssets({ hyperframesDir, frames }) {
const wanted = new Set();
for (const f of frames) {
for (const b of basenamesFromCandidates(f.extra?.asset_candidates)) wanted.add(b);
for (const path of assetPathsFromCandidates(f.extra?.asset_candidates)) wanted.add(path);
}
const captureDirs = [
join(hyperframesDir, "capture/assets"),
Expand All @@ -34,19 +46,22 @@ export function stageAssets({ hyperframesDir, frames }) {
let staged = 0;
if (wanted.size > 0) {
mkdirSync(assetsDir, { recursive: true });
for (const b of wanted) {
const dest = join(assetsDir, b);
for (const assetPath of wanted) {
const dest = join(assetsDir, assetPath);
if (existsSync(dest)) {
staged++;
continue;
} // first-wins / already staged
const src = captureDirs.map((d) => join(d, b)).find((p) => existsSync(p));
const src = captureDirs
.flatMap((dir) => [join(dir, assetPath), join(dir, basename(assetPath))])
.find((path) => existsSync(path));
if (src) {
mkdirSync(dirname(dest), { recursive: true });
copyFileSync(src, dest);
staged++;
} else {
anomalies.push(
`asset "${b}" named by a frame but not found under capture/ — frame will 404 it`,
`asset "${assetPath}" named by a frame but not found under capture/ — frame will 404 it`,
);
}
}
Expand Down
42 changes: 42 additions & 0 deletions skills/product-launch-video/scripts/lib/assets.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";

import { assetPathsFromCandidates, stageAssets } from "./assets.mjs";

test("rejects asset candidates that escape the project assets directory", () => {
assert.deepEqual(
assetPathsFromCandidates(
"assets/../secret.png — traversal; /tmp/secret.png — absolute; C:/secret.png — drive",
),
[],
);
});

test("recognizes an asset candidate already present in a nested assets directory", () => {
const hyperframesDir = mkdtempSync(join(tmpdir(), "hyperframes-stage-assets-"));
try {
const keyframesDir = join(hyperframesDir, "assets/keyframes");
mkdirSync(keyframesDir, { recursive: true });
writeFileSync(join(keyframesDir, "scene-01-meeting.png"), "fixture");

const result = stageAssets({
hyperframesDir,
frames: [
{
extra: {
asset_candidates: "assets/keyframes/scene-01-meeting.png — approved meeting keyframe",
},
},
],
});

assert.equal(result.staged, 1);
assert.deepEqual([...result.wanted], ["keyframes/scene-01-meeting.png"]);
assert.deepEqual(result.anomalies, []);
} finally {
rmSync(hyperframesDir, { recursive: true, force: true });
}
});
Loading