creator-editing-studio 1.3.7 → 1.4.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.
Files changed (34) hide show
  1. package/bin/finish.mjs +132 -0
  2. package/bin/init.mjs +5 -1
  3. package/bin/update.mjs +18 -0
  4. package/history.json +1 -1
  5. package/package.json +2 -1
  6. package/template/CLAUDE.md +31 -7
  7. package/template/package.json +3 -1
  8. package/template/preview/lib.mjs +164 -19
  9. package/template/preview/player.html +71 -68
  10. package/template/preview/server.mjs +246 -23
  11. package/template/remotion.config.ts +5 -0
  12. package/template/scripts/approve.mjs +39 -0
  13. package/template/scripts/lib/plan-gate.mjs +73 -0
  14. package/template/scripts/plan-sheet.mjs +99 -0
  15. package/template/src/components/CommentBox.tsx +31 -0
  16. package/template/src/components/FilmBurn.tsx +48 -0
  17. package/template/src/components/FlashBloom.tsx +45 -0
  18. package/template/src/components/MergeToPill.tsx +56 -0
  19. package/template/src/components/NotificationDrop.tsx +49 -0
  20. package/template/src/components/PillCaptions.tsx +62 -0
  21. package/template/src/components/ReelRing.tsx +81 -0
  22. package/template/src/components/WhipScene.tsx +90 -0
  23. package/template/src/design/motion.ts +8 -0
  24. package/template/src/lib/brand.tsx +89 -0
  25. package/template/src/lib/layout.tsx +113 -0
  26. package/template/src/lib/recolor.ts +173 -0
  27. package/template/src/lib/tweaks.tsx +108 -6
  28. package/template/src/plan/entry.tsx +66 -0
  29. package/template/src/videos/whats-new/WhatsNewEdit.tsx +177 -0
  30. package/template/src/videos/whats-new/beats.ts +44 -0
  31. package/template/src/videos/whats-new/head-track.json +5195 -0
  32. package/template/src/videos/whats-new/preview.tsx +36 -0
  33. package/template/src/videos/whats-new/tweaks.json +1 -0
  34. package/template/src/videos/whats-new/words.json +1 -0
package/bin/finish.mjs ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * What an install or update does AFTER the files land — shared by init.mjs and update.mjs.
3
+ *
4
+ * newestPublished() the newest release on npm, so an update run with an old version number (an
5
+ * assistant reading a version from a chat opened before a release) runs the newest one
6
+ * copyMedia() the demo footage. It is binary, so it cannot travel with the text files (those are
7
+ * hashed as text and also built into the server); it ships in media/ and is copied
8
+ * in whenever it is missing or different. Nobody edits it, so there is nothing to merge.
9
+ * finishSteps() what is still missing on this machine, printed as a checklist the assistant acts on —
10
+ * one recommended choice for the person, not a line they have to notice.
11
+ */
12
+ import {spawnSync} from 'node:child_process';
13
+ import {copyFileSync, existsSync, mkdirSync, readdirSync, statSync} from 'node:fs';
14
+ import {dirname, join, relative} from 'node:path';
15
+
16
+ const newer = (a, b) => {
17
+ const pa = String(a).split('.').map(Number);
18
+ const pb = String(b).split('.').map(Number);
19
+ for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);
20
+ return false;
21
+ };
22
+
23
+ /** The newest published version, or null when npm cannot be reached in a few seconds (then: carry on). */
24
+ export async function newestPublished(current) {
25
+ try {
26
+ const r = await fetch('https://registry.npmjs.org/creator-editing-studio/latest', {signal: AbortSignal.timeout(6000)});
27
+ if (!r.ok) return null;
28
+ const {version} = await r.json();
29
+ return version && newer(version, current) ? version : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** Run the same command with a newer release. Returns its exit code. */
36
+ export function runNewer(version, args) {
37
+ const win = process.platform === 'win32';
38
+ const q = (s) => (win && /\s/.test(s) ? `"${s}"` : s);
39
+ const r = spawnSync(win ? 'npx.cmd' : 'npx', ['-y', `creator-editing-studio@${version}`, ...args].map(q), {
40
+ stdio: 'inherit',
41
+ shell: win,
42
+ env: {...process.env, CREATOR_STUDIO_NO_REDIRECT: '1'},
43
+ });
44
+ return r.status ?? 1;
45
+ }
46
+
47
+ const walk = (d) =>
48
+ existsSync(d)
49
+ ? readdirSync(d, {withFileTypes: true}).flatMap((e) => {
50
+ const p = join(d, e.name);
51
+ return e.isDirectory() ? walk(p) : [p];
52
+ })
53
+ : [];
54
+
55
+ /** Copy the package's media/ into the studio (same relative paths). Returns the paths it copied. */
56
+ export function copyMedia(pkg, studio, dry = false) {
57
+ const root = join(pkg, 'media');
58
+ const copied = [];
59
+ for (const file of walk(root)) {
60
+ const rel = relative(root, file).split('\\').join('/');
61
+ const to = join(studio, rel);
62
+ if (existsSync(to) && statSync(to).size === statSync(file).size) continue;
63
+ if (!dry) {
64
+ mkdirSync(dirname(to), {recursive: true});
65
+ copyFileSync(file, to);
66
+ }
67
+ copied.push(rel);
68
+ }
69
+ return copied;
70
+ }
71
+
72
+ /**
73
+ * The checklist printed after an update. Written for the assistant running the command — it is the one
74
+ * that acts on it — and it tells it to ask with a choice box, because a "want me to install it?" at the
75
+ * end of a paragraph is a question nobody answers.
76
+ */
77
+ export function finishSteps(studio, version, {depsChanged}) {
78
+ const missing = [];
79
+ const venv = existsSync(join(studio, '.venv'));
80
+ if (depsChanged || !existsSync(join(studio, 'node_modules'))) missing.push('npm install');
81
+ if (!venv || !existsSync(join(studio, 'studio.config.json'))) missing.push('language and models');
82
+ const lines = [
83
+ '',
84
+ '================================================================================',
85
+ `FINISH SETTING UP ${version} — for the assistant that ran this command`,
86
+ '================================================================================',
87
+ '',
88
+ ];
89
+ if (missing.length) {
90
+ lines.push(
91
+ 'Some of this release is not installed on this machine yet:',
92
+ ...missing.map((m) => ` - ${m}`),
93
+ '',
94
+ 'ASK THE PERSON ONCE, WITH A CHOICE BOX (Claude Code: the AskUserQuestion tool; elsewhere a',
95
+ 'numbered choice) — never as a sentence at the end of your summary:',
96
+ '',
97
+ ` Question: "Finish setting up the new version?"`,
98
+ ' 1. Install now (Recommended) — the live preview, motion tracking and word-timed captions.',
99
+ ' One time, about 1 GB for English (3–5 GB for an Indian language), roughly 5–15 minutes.',
100
+ ' 2. Later — the studio still works; these effects will not until it is done.',
101
+ '',
102
+ 'If they choose Install now, do every step without asking again:',
103
+ );
104
+ if (missing.includes('npm install')) lines.push(' a. npm install');
105
+ if (missing.includes('language and models')) {
106
+ lines.push(
107
+ ' b. Ask, again as a choice box, what language their videos are in: English (Recommended),',
108
+ ' Hindi, Hinglish, Malayalam, Telugu (more: Tamil, Kannada, Marathi, Bengali — type it in "Other").',
109
+ ` c. ${venv ? '' : 'Create .venv with Python 3.11 or 3.12, then '}with the studio's Python: pip install -r requirements.txt`,
110
+ ' then: python scripts/setup_models.py --language <code>',
111
+ );
112
+ }
113
+ } else {
114
+ lines.push('Everything this release needs is already installed.', '');
115
+ }
116
+ lines.push(
117
+ '',
118
+ 'THE LIVE PREVIEW — check `claude mcp list` (Codex: `codex mcp list`). If live-preview is not there:',
119
+ ` claude mcp add live-preview --scope user -- node "${join(studio, 'preview', 'server.mjs')}"`,
120
+ ' It appears from the NEXT chat. Tell them so in one plain sentence.',
121
+ '',
122
+ 'THEN SHOW WHAT IS NEW — do not describe it in a list. Open the demo in the preview:',
123
+ ' preview_edit({video: "whats-new"})',
124
+ ' A 25-second clip, one new effect per beat, each named on screen. Tell them to try the buttons',
125
+ ' under each beat (Size, Move, Timing, Follow head) — it is a demo, nothing of theirs changes.',
126
+ ' If the preview only arrives next chat, say: "Open a new chat and type: show me what\'s new".',
127
+ '',
128
+ `FINALLY call editing_os_update({installed: "${version}"}) once, so the update is recorded.`,
129
+ '================================================================================',
130
+ );
131
+ return lines.join('\n');
132
+ }
package/bin/init.mjs CHANGED
@@ -65,12 +65,16 @@ for (const rel of files) {
65
65
  written.push(rel);
66
66
  }
67
67
 
68
+ // The demo footage (binary, so it ships beside the template rather than in it).
69
+ const {copyMedia} = await import('./finish.mjs');
70
+ const media = copyMedia(resolve(HERE, '..'), target);
71
+
68
72
  const kb = (n) => `${Math.round(n / 1024)} KB`;
69
73
  const bytes = written.reduce((sum, rel) => sum + statSync(join(target, rel)).size, 0);
70
74
 
71
75
  console.log(`\nCreator Editing OS — studio files\n`);
72
76
  console.log(` into ${target}`);
73
- console.log(` wrote ${written.length} files (${kb(bytes)})`);
77
+ console.log(` wrote ${written.length} files (${kb(bytes)})${media.length ? ` + ${media.length} demo footage file(s)` : ''}`);
74
78
  if (skipped.length) {
75
79
  console.log(` kept ${skipped.length} existing file(s) — nothing was overwritten`);
76
80
  console.log(` (use --force to replace them)`);
package/bin/update.mjs CHANGED
@@ -28,6 +28,7 @@ import {createHash} from 'node:crypto';
28
28
  import {copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync} from 'node:fs';
29
29
  import {dirname, join, relative, resolve} from 'node:path';
30
30
  import {fileURLToPath} from 'node:url';
31
+ import {copyMedia, finishSteps, newestPublished, runNewer} from './finish.mjs';
31
32
 
32
33
  const HERE = dirname(fileURLToPath(import.meta.url));
33
34
  const PKG = resolve(HERE, '..');
@@ -39,6 +40,20 @@ const args = process.argv.slice(2).filter((a) => a !== 'update');
39
40
  const dry = args.includes('--dry-run');
40
41
  const studio = resolve(process.cwd(), args.find((a) => !a.startsWith('--')) || '.');
41
42
 
43
+ /**
44
+ * Always the newest release. An assistant reads the version to ask for from its chat, and a chat opened
45
+ * before a release still remembers the old number — on 2026-10-19 a customer's Claude installed 1.3.6 and
46
+ * called it the latest after 1.3.7 had shipped. So whatever version was asked for, if npm has a newer one,
47
+ * that one runs instead. (CREATOR_STUDIO_NO_REDIRECT stops it asking twice.)
48
+ */
49
+ if (!process.env.CREATOR_STUDIO_NO_REDIRECT) {
50
+ const newest = await newestPublished(LATEST);
51
+ if (newest) {
52
+ console.log(`This is ${LATEST}, but ${newest} is out — running ${newest} instead.\n`);
53
+ process.exit(runNewer(newest, ['update', studio, ...args.filter((a) => a.startsWith('--'))]));
54
+ }
55
+ }
56
+
42
57
  /** Must match mcp/studio-hash.mjs exactly. */
43
58
  const sha = (body) => createHash('sha256').update(body.replace(/\r\n/g, '\n')).digest('hex').slice(0, 16);
44
59
  const read = (p) => readFileSync(p, 'utf8');
@@ -106,6 +121,7 @@ for (const p of shippedNow) {
106
121
  else result.merge.push(p);
107
122
  }
108
123
 
124
+ const media = copyMedia(PKG, studio, dry);
109
125
  if (!dry) {
110
126
  for (const p of [...result.added, ...result.replaced]) {
111
127
  const to = join(studio, p);
@@ -143,4 +159,6 @@ if (result.merge.length) {
143
159
  );
144
160
  }
145
161
  const deps = [...result.added, ...result.replaced, ...result.merge].includes('package.json');
162
+ list(dry ? 'Demo footage to add' : 'Demo footage added', media);
146
163
  console.log(`\nThen run: ${deps ? 'npm install && ' : ''}npm run check`);
164
+ if (!dry) console.log(finishSteps(studio, LATEST, {depsChanged: deps}));