@waits/cadence 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MOTION.md +70 -0
- package/README.md +109 -0
- package/bin/cli.mjs +21 -0
- package/package.json +59 -0
- package/prompts/art/compose.ts +18 -0
- package/prompts/art/fantasy.ts +15 -0
- package/prompts/art/landmarks.ts +49 -0
- package/prompts/art/style.ts +20 -0
- package/public/backgrounds/barton-springs.png +0 -0
- package/public/backgrounds/capitol.png +0 -0
- package/public/backgrounds/congress.png +0 -0
- package/public/backgrounds/enchanted-rock.png +0 -0
- package/public/backgrounds/hamilton-pool.png +0 -0
- package/public/backgrounds/mount-bonnell.png +0 -0
- package/public/backgrounds/pennybacker.png +0 -0
- package/public/backgrounds/ut-tower.png +0 -0
- package/scripts/_pkg.ts +28 -0
- package/scripts/audit.ts +69 -0
- package/scripts/changes.ts +70 -0
- package/scripts/check-motion.ts +37 -0
- package/scripts/cli.ts +79 -0
- package/scripts/generate-art.ts +72 -0
- package/scripts/guide.ts +114 -0
- package/scripts/make.ts +76 -0
- package/scripts/redesign.ts +83 -0
- package/scripts/render.ts +62 -0
- package/scripts/smoke.ts +43 -0
- package/scripts/sync-skill.ts +43 -0
- package/scripts/theme.ts +97 -0
- package/src/Root.tsx +51 -0
- package/src/adapters/index.ts +72 -0
- package/src/adapters/parse.ts +65 -0
- package/src/adapters/types.ts +24 -0
- package/src/brand/fonts.ts +74 -0
- package/src/brand/tokens.ts +31 -0
- package/src/brand/tone.ts +25 -0
- package/src/code/highlight.ts +90 -0
- package/src/components/Background.tsx +75 -0
- package/src/components/Changelog.tsx +17 -0
- package/src/components/ChangelogScene.tsx +117 -0
- package/src/components/CodeWindow.tsx +106 -0
- package/src/components/ContactSheet.tsx +44 -0
- package/src/components/Headline.tsx +73 -0
- package/src/components/MotionReel.tsx +108 -0
- package/src/components/panels/DataTable.tsx +34 -0
- package/src/components/panels/Diagram.tsx +67 -0
- package/src/components/panels/Feed.tsx +46 -0
- package/src/components/panels/Fork.tsx +86 -0
- package/src/components/panels/PanelCard.tsx +44 -0
- package/src/components/panels/Proof.tsx +64 -0
- package/src/components/panels/Stat.tsx +28 -0
- package/src/components/panels/Status.tsx +41 -0
- package/src/components/panels/StreamResume.tsx +48 -0
- package/src/components/panels/UploadProgress.tsx +42 -0
- package/src/components/panels/index.tsx +34 -0
- package/src/content/clarinet-3.18.beats.ts +58 -0
- package/src/content/gradient-demo.beats.ts +29 -0
- package/src/content/index-decoded.beats.ts +93 -0
- package/src/content/mainnet-launch.beats.ts +70 -0
- package/src/content/panels.beats.ts +40 -0
- package/src/content/sdk-6.5-mempool.beats.ts +67 -0
- package/src/content/streams-launch.beats.ts +143 -0
- package/src/content/streams.beats.ts +46 -0
- package/src/index.ts +4 -0
- package/src/motion/names.ts +29 -0
- package/src/motion/presets.ts +67 -0
- package/src/motion/useMotion.ts +63 -0
- package/src/prepare.ts +33 -0
- package/src/schema/beats.ts +161 -0
- package/src/templates/changelog-reel.ts +28 -0
- package/src/templates/feature-launch.ts +26 -0
- package/src/templates/index.ts +13 -0
- package/src/templates/milestone.ts +25 -0
- package/src/templates/parts.ts +32 -0
- package/src/templates/types.ts +31 -0
- package/src/theme/default.ts +39 -0
- package/src/theme/derive.ts +119 -0
- package/src/theme/index.ts +33 -0
- package/src/theme/library.ts +60 -0
- package/src/theme/slate.ts +39 -0
- package/src/theme/types.ts +67 -0
package/scripts/cli.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified front door. `cadence <command> …` dispatches to the pieces.
|
|
3
|
+
*
|
|
4
|
+
* cadence create --release stx-labs/clarinet --install "brew install clarinet"
|
|
5
|
+
* cadence create src/content/x.beats.ts --format 9x16 # a beats file → video
|
|
6
|
+
* cadence study --from-url https://acme.dev --name acme
|
|
7
|
+
* cadence audit src/content/x.beats.ts
|
|
8
|
+
* cadence redesign src/content/x.beats.ts --theme slate
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { TEMPLATES } from "../src/templates";
|
|
12
|
+
import { THEMES } from "../src/theme";
|
|
13
|
+
import { binPath, pkgFile } from "./_pkg";
|
|
14
|
+
|
|
15
|
+
const [rawSub, ...rest] = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
// Legacy verb aliases → canonical intent verbs.
|
|
18
|
+
const ALIAS: Record<string, string> = { make: "create", theme: "study" };
|
|
19
|
+
const sub = rawSub ? (ALIAS[rawSub] ?? rawSub) : rawSub;
|
|
20
|
+
|
|
21
|
+
// Verb → script. `create` is resolved dynamically (repo flow vs. beats file).
|
|
22
|
+
const SCRIPTS: Record<string, string> = {
|
|
23
|
+
guide: "scripts/guide.ts", // interactive walkthrough
|
|
24
|
+
render: "scripts/render.ts", // a beats file → a video (create delegates here)
|
|
25
|
+
study: "scripts/theme.ts", // brand color / URL / screenshot → a theme
|
|
26
|
+
audit: "scripts/audit.ts", // check a beats file for issues
|
|
27
|
+
redesign: "scripts/redesign.ts", // re-skin a beats file
|
|
28
|
+
changes: "scripts/changes.ts", // a repo → an UpdateManifest
|
|
29
|
+
art: "scripts/generate-art.ts", // optional painterly background pack
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const HELP = `cadence — turn a repo / release into a changelog or announcement video.
|
|
33
|
+
|
|
34
|
+
commands:
|
|
35
|
+
create a repo OR a beats file → a video cadence create --release owner/name --install "npm i pkg"
|
|
36
|
+
study a brand color / URL → a theme cadence study --from-url https://acme.dev --name acme
|
|
37
|
+
audit check a beats file for issues cadence audit src/content/x.beats.ts
|
|
38
|
+
redesign re-skin a beats file (new look) cadence redesign x.beats.ts --theme slate
|
|
39
|
+
guide interactive walkthrough (start here) cadence guide
|
|
40
|
+
|
|
41
|
+
utilities:
|
|
42
|
+
render a beats file → a video (create delegates here for beats files)
|
|
43
|
+
changes a repo → an UpdateManifest (json)
|
|
44
|
+
art generate painterly backgrounds (optional pack, needs OPENAI_API_KEY)
|
|
45
|
+
templates list available templates
|
|
46
|
+
themes list available themes
|
|
47
|
+
|
|
48
|
+
flags shared by create/render/redesign: --format 16x9|1x1|9x16, --theme <name>, --theme-file <path>, --frame <n>`;
|
|
49
|
+
|
|
50
|
+
if (!sub || sub === "help" || sub === "--help") {
|
|
51
|
+
console.log(HELP);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
if (sub === "templates") {
|
|
55
|
+
console.log("templates:\n" + Object.keys(TEMPLATES).map((t) => ` ${t}`).join("\n"));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
if (sub === "themes") {
|
|
59
|
+
console.log("themes:\n" + Object.keys(THEMES).map((t) => ` ${t}`).join("\n"));
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `create` merges the repo flow (make) and the beats-file flow (render): a
|
|
64
|
+
// positional beats-like file routes to render; otherwise to make.
|
|
65
|
+
function resolveScript(verb: string, args: string[]): string | undefined {
|
|
66
|
+
if (verb === "create") {
|
|
67
|
+
const beatsFile = args.find((a) => !a.startsWith("-") && /\.(beats\.)?(ts|js|json)$/.test(a));
|
|
68
|
+
return beatsFile ? "scripts/render.ts" : "scripts/make.ts";
|
|
69
|
+
}
|
|
70
|
+
return SCRIPTS[verb];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const script = resolveScript(sub, rest);
|
|
74
|
+
if (!script) {
|
|
75
|
+
console.error(`unknown command "${rawSub}".\n\n${HELP}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const res = spawnSync(binPath("tsx"), [pkgFile(script), ...rest], { stdio: "inherit" });
|
|
79
|
+
process.exit(res.status ?? 0);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Hill Country Sublime" painting generator. Builds a prompt from the art system
|
|
3
|
+
* (prompts/art/*) and calls OpenAI gpt-image-1 (base64 response → PNG). Writes to
|
|
4
|
+
* public/backgrounds/_candidates/ and updates a manifest the ContactSheet reads.
|
|
5
|
+
*
|
|
6
|
+
* bun run art --landmark pennybacker --level heightened [--format 16x9] [--quality high]
|
|
7
|
+
* bun run art --all --level heightened --format 16x9 # every landmark
|
|
8
|
+
*
|
|
9
|
+
* Requires OPENAI_API_KEY. Chosen winners get moved to public/backgrounds/ (committed).
|
|
10
|
+
*/
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { composePrompt, LANDMARKS, type FantasyLevel, type LandmarkKey } from "../prompts/art/compose";
|
|
14
|
+
import type { Format } from "../src/schema/beats";
|
|
15
|
+
|
|
16
|
+
const SIZE: Record<Format, string> = { "16x9": "1536x1024", "1x1": "1024x1024", "9x16": "1024x1536" };
|
|
17
|
+
const CANDIDATES = "public/backgrounds/_candidates";
|
|
18
|
+
const MANIFEST = join(CANDIDATES, "manifest.json");
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const flag = (name: string, def?: string) => {
|
|
22
|
+
const eq = args.find((a) => a.startsWith(`${name}=`));
|
|
23
|
+
if (eq) return eq.split("=")[1];
|
|
24
|
+
const i = args.indexOf(name);
|
|
25
|
+
return i >= 0 && !args[i + 1]?.startsWith("--") ? args[i + 1] : def;
|
|
26
|
+
};
|
|
27
|
+
const has = (name: string) => args.includes(name);
|
|
28
|
+
|
|
29
|
+
const level = (flag("--level", "heightened") as FantasyLevel);
|
|
30
|
+
const format = (flag("--format", "16x9") as Format);
|
|
31
|
+
const quality = flag("--quality", "high")!;
|
|
32
|
+
const landmarks: LandmarkKey[] = has("--all")
|
|
33
|
+
? (Object.keys(LANDMARKS) as LandmarkKey[])
|
|
34
|
+
: [(flag("--landmark", "pennybacker") as LandmarkKey)];
|
|
35
|
+
|
|
36
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
37
|
+
console.error("✗ OPENAI_API_KEY not set");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
mkdirSync(CANDIDATES, { recursive: true });
|
|
42
|
+
type Entry = { file: string; landmark: string; level: string; format: string; name: string };
|
|
43
|
+
const manifest: Entry[] = existsSync(MANIFEST) ? JSON.parse(readFileSync(MANIFEST, "utf8")) : [];
|
|
44
|
+
|
|
45
|
+
async function generate(landmark: LandmarkKey) {
|
|
46
|
+
const prompt = composePrompt(landmark, level, format);
|
|
47
|
+
const fname = `${landmark}-${level}-${format}.png`;
|
|
48
|
+
process.stdout.write(`· ${fname} … `);
|
|
49
|
+
const res = await fetch("https://api.openai.com/v1/images/generations", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ model: "gpt-image-1", prompt, size: SIZE[format], quality, n: 1 }),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
console.log("FAILED");
|
|
56
|
+
console.error(await res.text());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const json = await res.json();
|
|
60
|
+
const b64 = json.data[0].b64_json as string;
|
|
61
|
+
writeFileSync(join(CANDIDATES, fname), Buffer.from(b64, "base64"));
|
|
62
|
+
const entry: Entry = { file: `backgrounds/_candidates/${fname}`, landmark: LANDMARKS[landmark].name, level, format, name: fname };
|
|
63
|
+
const idx = manifest.findIndex((m) => m.name === fname);
|
|
64
|
+
if (idx >= 0) manifest[idx] = entry; else manifest.push(entry);
|
|
65
|
+
console.log("ok");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const lm of landmarks) {
|
|
69
|
+
await generate(lm);
|
|
70
|
+
}
|
|
71
|
+
writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2));
|
|
72
|
+
console.log(`\n✓ ${landmarks.length} image(s) → ${CANDIDATES}/ (manifest: ${manifest.length} total)`);
|
package/scripts/guide.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive walkthrough. Run on any project to (a) make a video, (b) understand
|
|
3
|
+
* the pipeline as you go, (c) learn where to adjust each piece.
|
|
4
|
+
*
|
|
5
|
+
* bun run cli guide
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { TEMPLATES } from "../src/templates";
|
|
11
|
+
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
|
|
14
|
+
const cancel = () => {
|
|
15
|
+
console.log("\n\n cancelled — run `bun run cli guide` any time.\n");
|
|
16
|
+
rl.close();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
};
|
|
19
|
+
rl.on("SIGINT", cancel);
|
|
20
|
+
|
|
21
|
+
const ask = async (q: string, def = "") => (await rl.question(`${q}${def ? ` [${def}]` : ""}: `)).trim() || def;
|
|
22
|
+
const say = (s = "") => console.log(s);
|
|
23
|
+
const rule = () => say("─".repeat(64));
|
|
24
|
+
const tsx = (args: string[], opts: { capture?: boolean } = {}) =>
|
|
25
|
+
spawnSync(resolve("node_modules/.bin/tsx"), args, { stdio: opts.capture ? ["ignore", "pipe", "inherit"] : "inherit", encoding: "utf8" });
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
rule();
|
|
29
|
+
say(" cadence — interactive walkthrough");
|
|
30
|
+
rule();
|
|
31
|
+
say(`
|
|
32
|
+
This turns a repo / release into a changelog or announcement video. The pipeline:
|
|
33
|
+
|
|
34
|
+
repo → [adapter] → manifest → [template] → beats → [engine] → mp4
|
|
35
|
+
▲ ▲
|
|
36
|
+
themeable honest, no LLM
|
|
37
|
+
|
|
38
|
+
We'll walk it one stage at a time. Press enter to accept the [defaults].
|
|
39
|
+
`);
|
|
40
|
+
|
|
41
|
+
// ── 1. Source ────────────────────────────────────────────────────────────
|
|
42
|
+
rule();
|
|
43
|
+
say("STEP 1 — what changed? (adapters: src/adapters/)");
|
|
44
|
+
say("Point at a GitHub repo's latest release. The adapter parses the notes into");
|
|
45
|
+
say("a normalized manifest (it strips emoji/PR-refs and reports what it dropped).\n");
|
|
46
|
+
const repo = await ask("GitHub repo (owner/name)", "stx-labs/clarinet");
|
|
47
|
+
const tag = await ask("release tag (blank = latest)");
|
|
48
|
+
say("\n→ running: cli changes --release " + repo + (tag ? ` --tag ${tag}` : ""));
|
|
49
|
+
tsx(["scripts/changes.ts", "--release", repo, ...(tag ? ["--tag", tag] : [])]);
|
|
50
|
+
say("\nThat JSON is the 'UpdateManifest' — every later stage reads from it.");
|
|
51
|
+
say("Tweak parsing in src/adapters/parse.ts.\n");
|
|
52
|
+
|
|
53
|
+
// ── 2. Template ──────────────────────────────────────────────────────────
|
|
54
|
+
rule();
|
|
55
|
+
say("STEP 2 — the arc. (templates: src/templates/)");
|
|
56
|
+
say("A template maps the manifest to beats, deterministically and honestly");
|
|
57
|
+
say("(no fabricated code). Available: " + Object.keys(TEMPLATES).join(", ") + ".\n");
|
|
58
|
+
const template = await ask("template", "changelog-reel");
|
|
59
|
+
const install = await ask("real install line for the closer (e.g. 'brew install clarinet')");
|
|
60
|
+
|
|
61
|
+
// ── 3. Look ──────────────────────────────────────────────────────────────
|
|
62
|
+
rule();
|
|
63
|
+
say("STEP 3 — the look. (themes: src/theme/ · style packs: Background.tsx)");
|
|
64
|
+
say("Background style packs: gradient / solid / image (a painting). Theme sets");
|
|
65
|
+
say("the colors, fonts, and code palette — try 'default' or 'slate', or extract");
|
|
66
|
+
say("a brand's color from a URL with: cli study --from-url <site> --name <name>.\n");
|
|
67
|
+
const format = await ask("format (16x9 / 1x1 / 9x16)", "16x9");
|
|
68
|
+
const background = await ask("background (gradient:#a,#b | solid:#hex | image:file.png)", "gradient:#312e81,#0b1120");
|
|
69
|
+
const theme = await ask("theme (default / slate)", "default");
|
|
70
|
+
|
|
71
|
+
const makeArgs = (extra: string[]) => [
|
|
72
|
+
"scripts/make.ts", "--release", repo, ...(tag ? ["--tag", tag] : []),
|
|
73
|
+
"--template", template, "--format", format, "--background", background, "--theme", theme,
|
|
74
|
+
...(install ? ["--install", install] : []), ...extra,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// ── 4. Preview ───────────────────────────────────────────────────────────
|
|
78
|
+
rule();
|
|
79
|
+
say("STEP 4 — preview a single frame (fast, ~5s) before the full render.\n");
|
|
80
|
+
tsx(makeArgs(["--frame", "200"]));
|
|
81
|
+
say("\n→ open the .png in out/ to eyeball it. Adjust the template/background/theme above if needed.");
|
|
82
|
+
|
|
83
|
+
const again = (await ask("\nrender the full video now? (y/n)", "y")).toLowerCase();
|
|
84
|
+
if (again.startsWith("y")) {
|
|
85
|
+
rule();
|
|
86
|
+
say("STEP 5 — full render.\n");
|
|
87
|
+
tsx(makeArgs([]));
|
|
88
|
+
say("\n→ the mp4 is in out/.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Close ────────────────────────────────────────────────────────────────
|
|
92
|
+
rule();
|
|
93
|
+
say("WHERE TO GO NEXT");
|
|
94
|
+
rule();
|
|
95
|
+
say(`
|
|
96
|
+
Understand it WALKTHROUGH.md (architecture + how to adjust each part)
|
|
97
|
+
Honest code the 'cadence' skill reads a repo's types to add real
|
|
98
|
+
code snippets (the LLM path, vs the deterministic templates here)
|
|
99
|
+
Add a panel src/components/panels/ → register in panels/index.tsx + schema
|
|
100
|
+
Add a template src/templates/ → register in templates/index.ts
|
|
101
|
+
Brand colors cli study --from-url <site> --name <n> → --theme-file themes/<n>.json
|
|
102
|
+
Ship on release docs/github-action.md (auto-render on every release, free)
|
|
103
|
+
|
|
104
|
+
Run again any time: bun run cli guide
|
|
105
|
+
`);
|
|
106
|
+
rl.close();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main().catch((e) => {
|
|
110
|
+
if (e?.code === "ABORT_ERR" || e?.name === "AbortError") cancel(); // Ctrl+C / Ctrl+D mid-prompt
|
|
111
|
+
console.error(e);
|
|
112
|
+
rl.close();
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
package/scripts/make.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One command: a repo → a video, no LLM. Reads the latest release/changelog,
|
|
3
|
+
* applies a template, and renders.
|
|
4
|
+
*
|
|
5
|
+
* tsx scripts/make.ts --release stx-labs/clarinet --install "brew install clarinet"
|
|
6
|
+
* tsx scripts/make.ts --release owner/name --template changelog-reel --background "gradient:#312e81,#0b1120" --format 9x16
|
|
7
|
+
* tsx scripts/make.ts --changelog ./CHANGELOG.md --product my-pkg --install "npm i my-pkg" --frame 230
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
import { manifestFromChangelogText, manifestFromReleaseBody } from "../src/adapters";
|
|
13
|
+
import type { BackgroundSpec } from "../src/templates";
|
|
14
|
+
import { TEMPLATES } from "../src/templates";
|
|
15
|
+
import { binPath, pkgFile } from "./_pkg";
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const flag = (n: string) => {
|
|
19
|
+
const eq = args.find((a) => a.startsWith(`${n}=`));
|
|
20
|
+
if (eq) return eq.split("=")[1];
|
|
21
|
+
const i = args.indexOf(n);
|
|
22
|
+
return i >= 0 && !args[i + 1]?.startsWith("--") ? args[i + 1] : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function parseBackground(s?: string): BackgroundSpec | undefined {
|
|
26
|
+
if (!s) return undefined;
|
|
27
|
+
if (s === "shapes") return { shapes: true };
|
|
28
|
+
const [kind, rest] = [s.slice(0, s.indexOf(":")), s.slice(s.indexOf(":") + 1)];
|
|
29
|
+
if (kind === "gradient") {
|
|
30
|
+
const [from, to] = rest.split(",");
|
|
31
|
+
return { gradient: [from, to], angle: 155, treatment: "kenburns" };
|
|
32
|
+
}
|
|
33
|
+
if (kind === "solid") return { solid: rest, treatment: "static" };
|
|
34
|
+
if (kind === "image") return { src: rest.includes("/") ? rest : `backgrounds/${rest}`, treatment: "kenburns" };
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 1. Source → manifest
|
|
39
|
+
const release = flag("--release");
|
|
40
|
+
const changelog = flag("--changelog");
|
|
41
|
+
const install = flag("--install");
|
|
42
|
+
let manifest;
|
|
43
|
+
if (release) {
|
|
44
|
+
const tag = flag("--tag");
|
|
45
|
+
const res = spawnSync("gh", ["release", "view", ...(tag ? [tag] : []), "--repo", release, "--json", "tagName,name,publishedAt,body"], { encoding: "utf8" });
|
|
46
|
+
if (res.status !== 0) { console.error(res.stderr); process.exit(1); }
|
|
47
|
+
const r = JSON.parse(res.stdout);
|
|
48
|
+
manifest = manifestFromReleaseBody({ product: flag("--product") ?? basename(release), version: r.tagName ?? r.name ?? "", body: r.body ?? "", date: r.publishedAt?.slice(0, 10), install, repoUrl: `https://github.com/${release}` });
|
|
49
|
+
} else if (changelog) {
|
|
50
|
+
manifest = manifestFromChangelogText(readFileSync(changelog, "utf8"), { product: flag("--product") ?? "package", install });
|
|
51
|
+
} else {
|
|
52
|
+
console.error("usage: make.ts --release <owner/name> | --changelog <path> [--template T] [--background B] [--format F] [--theme T] [--frame N]");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Template → beats
|
|
57
|
+
const templateName = flag("--template") ?? "changelog-reel";
|
|
58
|
+
const template = TEMPLATES[templateName];
|
|
59
|
+
if (!template) { console.error(`unknown template "${templateName}". have: ${Object.keys(TEMPLATES).join(", ")}`); process.exit(1); }
|
|
60
|
+
const statValue = flag("--stat-value");
|
|
61
|
+
const beats = template(manifest, {
|
|
62
|
+
format: flag("--format") as never,
|
|
63
|
+
background: parseBackground(flag("--background")),
|
|
64
|
+
headline: flag("--headline"),
|
|
65
|
+
stat: statValue ? { value: statValue, label: flag("--stat-label") ?? "", sub: flag("--stat-sub") } : undefined,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.error(`· ${manifest.product} ${manifest.version}: ${manifest.features.length} features${manifest.dropped ? ` (+${manifest.dropped} dropped)` : ""} → ${templateName}`);
|
|
69
|
+
|
|
70
|
+
// 3. Write beats JSON + render
|
|
71
|
+
mkdirSync("out", { recursive: true });
|
|
72
|
+
const jsonPath = join("out", `make-${manifest.product}.beats.json`);
|
|
73
|
+
writeFileSync(jsonPath, JSON.stringify(beats));
|
|
74
|
+
const passthru = ["--format", "--theme", "--frame"].flatMap((f) => (flag(f) ? [f, flag(f)!] : []));
|
|
75
|
+
const res = spawnSync(binPath("tsx"), [pkgFile("scripts/render.ts"), jsonPath, ...passthru], { stdio: "inherit" });
|
|
76
|
+
process.exit(res.status ?? 0);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cadence redesign <beats>` — re-skin an existing beats file: override the
|
|
3
|
+
* background / motion fingerprint and theme while preserving all content
|
|
4
|
+
* (headlines, code, panels). Writes a new beats JSON to out/ and renders it.
|
|
5
|
+
*
|
|
6
|
+
* cadence redesign src/content/mainnet-launch.beats.ts --theme slate
|
|
7
|
+
* cadence redesign x.beats.ts --background "gradient:#312e81,#0b1120" --enter rise
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { basename, join, resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { changelogSchema, type Beat } from "../src/schema/beats";
|
|
14
|
+
import { ENTER_PRESETS, EXIT_PRESETS } from "../src/motion/names";
|
|
15
|
+
import { binPath, pkgFile } from "./_pkg";
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const flag = (n: string) => {
|
|
19
|
+
const eq = args.find((a) => a.startsWith(`${n}=`));
|
|
20
|
+
if (eq) return eq.split("=")[1];
|
|
21
|
+
const i = args.indexOf(n);
|
|
22
|
+
return i >= 0 && !args[i + 1]?.startsWith("--") ? args[i + 1] : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const file = args.find((a) => !a.startsWith("-"));
|
|
26
|
+
if (!file) {
|
|
27
|
+
console.error("usage: cadence redesign <beats.ts|.json> [--background B] [--enter E] [--exit X] [--theme T] [--format F] [--frame N]");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Same background mini-DSL as `make`: gradient:#a,#b | solid:#hex | image:file.png
|
|
32
|
+
function parseBackground(s: string): Beat["background"] {
|
|
33
|
+
if (s === "shapes") return { shapes: true, treatment: "static", angle: 160 };
|
|
34
|
+
const kind = s.slice(0, s.indexOf(":"));
|
|
35
|
+
const rest = s.slice(s.indexOf(":") + 1);
|
|
36
|
+
if (kind === "gradient") {
|
|
37
|
+
const [from, to] = rest.split(",");
|
|
38
|
+
return { gradient: [from, to], angle: 155, treatment: "kenburns" };
|
|
39
|
+
}
|
|
40
|
+
if (kind === "solid") return { solid: rest, angle: 160, treatment: "static" };
|
|
41
|
+
if (kind === "image") return { src: rest.includes("/") ? rest : `backgrounds/${rest}`, angle: 160, treatment: "kenburns" };
|
|
42
|
+
console.error(`unknown --background "${s}" (gradient:#a,#b | solid:#hex | image:file.png)`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bg = flag("--background");
|
|
47
|
+
const bgSpec = bg ? parseBackground(bg) : undefined;
|
|
48
|
+
const enter = flag("--enter");
|
|
49
|
+
const exit = flag("--exit");
|
|
50
|
+
if (enter && !(ENTER_PRESETS as readonly string[]).includes(enter)) {
|
|
51
|
+
console.error(`unknown --enter "${enter}". have: ${ENTER_PRESETS.join(", ")}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
if (exit && !(EXIT_PRESETS as readonly string[]).includes(exit)) {
|
|
55
|
+
console.error(`unknown --exit "${exit}". have: ${EXIT_PRESETS.join(", ")}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const raw = file.endsWith(".json")
|
|
60
|
+
? JSON.parse(readFileSync(resolve(file), "utf8"))
|
|
61
|
+
: (await import(pathToFileURL(resolve(file)).href)).default;
|
|
62
|
+
const parsed = changelogSchema.parse(raw);
|
|
63
|
+
|
|
64
|
+
// Re-skin: override style fields, preserve content (headline/eyebrow/code/panel).
|
|
65
|
+
const enterVal = enter as (typeof ENTER_PRESETS)[number] | undefined;
|
|
66
|
+
const exitVal = exit as (typeof EXIT_PRESETS)[number] | undefined;
|
|
67
|
+
parsed.beats = parsed.beats.map((b) => ({
|
|
68
|
+
...b,
|
|
69
|
+
...(bgSpec ? { background: bgSpec } : {}),
|
|
70
|
+
...(enterVal || exitVal
|
|
71
|
+
? { headlineMotion: { ...(b.headlineMotion ?? {}), ...(enterVal ? { enter: enterVal } : {}), ...(exitVal ? { exit: exitVal } : {}) } }
|
|
72
|
+
: {}),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
mkdirSync("out", { recursive: true });
|
|
76
|
+
const name = basename(file).replace(/\.beats\.(ts|js|json)$/, "").replace(/\.(ts|js|json)$/, "");
|
|
77
|
+
const outBeats = join("out", `${name}.redesign.beats.json`);
|
|
78
|
+
writeFileSync(outBeats, JSON.stringify(parsed));
|
|
79
|
+
console.error(`· redesigned ${name} → ${outBeats}`);
|
|
80
|
+
|
|
81
|
+
const passthru = ["--theme", "--theme-file", "--format", "--frame"].flatMap((f) => (flag(f) ? [f, flag(f)!] : []));
|
|
82
|
+
const res = spawnSync(binPath("tsx"), [pkgFile("scripts/render.ts"), outBeats, ...passthru], { stdio: "inherit" });
|
|
83
|
+
process.exit(res.status ?? 0);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a beats module to MP4. Remotion `--props` accepts JSON only, so this
|
|
3
|
+
* imports the authored `.ts`, validates it against the schema, serializes to a
|
|
4
|
+
* temp JSON file, and hands that to the Remotion CLI. Code tokenization +
|
|
5
|
+
* duration + dimensions happen in the composition's calculateMetadata.
|
|
6
|
+
*
|
|
7
|
+
* bun run render src/content/streams.beats.ts [--format 16x9|1x1|9x16] [--frame N]
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { basename, join, resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { changelogSchema, type Format } from "../src/schema/beats";
|
|
14
|
+
import { THEMES } from "../src/theme";
|
|
15
|
+
import { binPath, pkgFile } from "./_pkg";
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const getFlag = (name: string) => {
|
|
19
|
+
const eq = args.find((a) => a.startsWith(`${name}=`));
|
|
20
|
+
if (eq) return eq.split("=")[1];
|
|
21
|
+
const i = args.indexOf(name);
|
|
22
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const file = args.find((a) => !a.startsWith("-"));
|
|
26
|
+
if (!file) {
|
|
27
|
+
console.error("usage: bun run render <beats.ts> [--format 16x9|1x1|9x16] [--frame N]");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Accept either a `.beats.ts` module or a plain `.json` beats file.
|
|
32
|
+
const raw = file.endsWith(".json")
|
|
33
|
+
? JSON.parse(readFileSync(resolve(file), "utf8"))
|
|
34
|
+
: (await import(pathToFileURL(resolve(file)).href)).default;
|
|
35
|
+
const parsed = changelogSchema.parse(raw);
|
|
36
|
+
|
|
37
|
+
const fmt = getFlag("--format") as Format | undefined;
|
|
38
|
+
if (fmt) parsed.format = fmt;
|
|
39
|
+
const frame = getFlag("--frame");
|
|
40
|
+
const theme = getFlag("--theme");
|
|
41
|
+
const themeFile = getFlag("--theme-file");
|
|
42
|
+
if (theme && !themeFile && !THEMES[theme]) {
|
|
43
|
+
console.error(`unknown --theme "${theme}". have: ${Object.keys(THEMES).join(", ")}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
47
|
+
if (theme) env.REMOTION_VIDEO_THEME = theme;
|
|
48
|
+
if (themeFile) env.REMOTION_VIDEO_THEME_JSON = readFileSync(resolve(themeFile), "utf8");
|
|
49
|
+
|
|
50
|
+
mkdirSync("out", { recursive: true });
|
|
51
|
+
const name = basename(file).replace(/\.beats\.(ts|js|json)$/, "").replace(/\.(ts|js|json)$/, "");
|
|
52
|
+
const propsPath = join("out", `.props-${name}-${parsed.format}.json`);
|
|
53
|
+
writeFileSync(propsPath, JSON.stringify(parsed));
|
|
54
|
+
|
|
55
|
+
const bin = binPath("remotion");
|
|
56
|
+
const common = [pkgFile("src/index.ts"), "Changelog"];
|
|
57
|
+
const suffix = theme && theme !== "default" ? `-${theme}` : "";
|
|
58
|
+
const res = frame
|
|
59
|
+
? spawnSync(bin, ["still", ...common, join("out", `${name}-${parsed.format}${suffix}-f${frame}.png`), `--props=${propsPath}`, `--frame=${frame}`], { stdio: "inherit", env })
|
|
60
|
+
: spawnSync(bin, ["render", ...common, join("out", `${name}-${parsed.format}${suffix}.mp4`), `--props=${propsPath}`, "--image-format=jpeg"], { stdio: "inherit", env });
|
|
61
|
+
|
|
62
|
+
process.exit(res.status ?? 0);
|
package/scripts/smoke.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render smoke test — re-renders a matrix of key stills (panels × themes ×
|
|
3
|
+
* formats) and fails if any errors. Cheap regression gate for CI. (A perceptual
|
|
4
|
+
* pixel-diff against committed references is the stronger follow-up.)
|
|
5
|
+
*
|
|
6
|
+
* bun run check:render
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
type Case = { file: string; frame: number; theme?: string; format?: string; note: string };
|
|
12
|
+
|
|
13
|
+
const CASES: Case[] = [
|
|
14
|
+
{ file: "src/content/streams-launch.beats.ts", frame: 470, note: "proof panel + typed code" },
|
|
15
|
+
{ file: "src/content/streams-launch.beats.ts", frame: 850, note: "fork panel" },
|
|
16
|
+
{ file: "src/content/streams-launch.beats.ts", frame: 660, note: "stream-resume panel" },
|
|
17
|
+
{ file: "src/content/gradient-demo.beats.ts", frame: 230, note: "gradient pack + stat (no art)" },
|
|
18
|
+
{ file: "src/content/clarinet-3.18.beats.ts", frame: 320, theme: "slate", note: "status + slate code theme" },
|
|
19
|
+
{ file: "src/content/sdk-6.5-mempool.beats.ts", frame: 330, format: "9x16", note: "data-table, vertical reflow" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const bin = resolve("node_modules/.bin/tsx");
|
|
23
|
+
let failed = 0;
|
|
24
|
+
for (const c of CASES) {
|
|
25
|
+
const args = ["scripts/render.ts", c.file, "--frame", String(c.frame)];
|
|
26
|
+
if (c.theme) args.push("--theme", c.theme);
|
|
27
|
+
if (c.format) args.push("--format", c.format);
|
|
28
|
+
process.stdout.write(`· ${c.note} … `);
|
|
29
|
+
const res = spawnSync(bin, args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
30
|
+
if (res.status === 0) {
|
|
31
|
+
console.log("ok");
|
|
32
|
+
} else {
|
|
33
|
+
failed++;
|
|
34
|
+
console.log("FAIL");
|
|
35
|
+
console.error((res.stderr || "").split("\n").slice(-6).join("\n"));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (failed) {
|
|
40
|
+
console.error(`\n✗ ${failed}/${CASES.length} render cases failed`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
console.log(`\n✓ ${CASES.length} render cases ok`);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync the canonical skill to the formats other editors expect, so there's one
|
|
3
|
+
* source of truth and no hand-maintained copies.
|
|
4
|
+
*
|
|
5
|
+
* source: .claude/skills/cadence/ (Claude Code; what you edit)
|
|
6
|
+
* → skills/cadence/ (the `npx skills add <repo>` layout)
|
|
7
|
+
* → .cursor/rules/cadence.mdc (Cursor rule)
|
|
8
|
+
*
|
|
9
|
+
* Codex reads the same `skills/<name>/` layout via `npx skills add`, so no separate
|
|
10
|
+
* copy is committed. Run after editing the skill: bun run sync-skill
|
|
11
|
+
*/
|
|
12
|
+
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { PKG_ROOT } from "./_pkg";
|
|
15
|
+
|
|
16
|
+
const NAME = "cadence";
|
|
17
|
+
const SRC = join(PKG_ROOT, ".claude/skills", NAME);
|
|
18
|
+
|
|
19
|
+
// 1. Frontmatter (name, description) + body from the canonical SKILL.md.
|
|
20
|
+
const skill = readFileSync(join(SRC, "SKILL.md"), "utf8");
|
|
21
|
+
const fm = skill.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
22
|
+
if (!fm) {
|
|
23
|
+
console.error("✗ SKILL.md is missing frontmatter");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const description = (fm[1].match(/^description:\s*(.*)$/m)?.[1] ?? "").trim();
|
|
27
|
+
const body = fm[2].trimStart();
|
|
28
|
+
|
|
29
|
+
// 2. Distribution copy at the repo-root `skills/<name>/` layout.
|
|
30
|
+
const distDir = join(PKG_ROOT, "skills", NAME);
|
|
31
|
+
rmSync(distDir, { recursive: true, force: true });
|
|
32
|
+
mkdirSync(distDir, { recursive: true });
|
|
33
|
+
cpSync(SRC, distDir, { recursive: true });
|
|
34
|
+
console.log(`✓ skills/${NAME}/ (npx skills add layout)`);
|
|
35
|
+
|
|
36
|
+
// 3. Cursor rule — an agent-requested rule carrying the same body.
|
|
37
|
+
const cursorDir = join(PKG_ROOT, ".cursor/rules");
|
|
38
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
39
|
+
const mdc = `---\ndescription: ${description}\nalwaysApply: false\n---\n\n${body}`;
|
|
40
|
+
writeFileSync(join(cursorDir, `${NAME}.mdc`), mdc);
|
|
41
|
+
console.log(`✓ .cursor/rules/${NAME}.mdc`);
|
|
42
|
+
|
|
43
|
+
console.log("\nEdit the skill in .claude/skills/, then re-run `bun run sync-skill`.");
|