paper-motion 0.3.0 → 0.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.
- package/CHANGELOG.md +108 -0
- package/bin/pamo.mjs +221 -20
- package/examples/breath/stills/contact-sheet.jpg +0 -0
- package/examples/breath/stills/t000.00.png +0 -0
- package/examples/breath/stills/t002.00.png +0 -0
- package/examples/breath/stills/t004.00.png +0 -0
- package/examples/breath/stills/t006.00.png +0 -0
- package/examples/breath/stills/t008.00.png +0 -0
- package/examples/breath/stills/t010.00.png +0 -0
- package/examples/chrome-endcard/stills/contact-sheet.jpg +0 -0
- package/examples/chrome-endcard/stills/t000.00.png +0 -0
- package/examples/chrome-endcard/stills/t001.33.png +0 -0
- package/examples/chrome-endcard/stills/t002.67.png +0 -0
- package/examples/chrome-endcard/stills/t004.00.png +0 -0
- package/examples/chrome-endcard/stills/t005.33.png +0 -0
- package/examples/chrome-endcard/stills/t006.67.png +0 -0
- package/examples/halo-explainer/stills/contact-sheet.jpg +0 -0
- package/examples/halo-explainer/stills/t000.00.png +0 -0
- package/examples/halo-explainer/stills/t006.00.png +0 -0
- package/examples/halo-explainer/stills/t012.00.png +0 -0
- package/examples/halo-explainer/stills/t018.00.png +0 -0
- package/examples/halo-explainer/stills/t024.00.png +0 -0
- package/examples/halo-explainer/stills/t030.00.png +0 -0
- package/examples/install-loop/stills/contact-sheet.jpg +0 -0
- package/examples/install-loop/stills/t000.00.png +0 -0
- package/examples/install-loop/stills/t001.00.png +0 -0
- package/examples/install-loop/stills/t002.00.png +0 -0
- package/examples/install-loop/stills/t003.00.png +0 -0
- package/examples/install-loop/stills/t004.00.png +0 -0
- package/examples/install-loop/stills/t005.00.png +0 -0
- package/examples/orbit-keynote/stills/contact-sheet.jpg +0 -0
- package/examples/orbit-keynote/stills/t000.00.png +0 -0
- package/examples/orbit-keynote/stills/t006.50.png +0 -0
- package/examples/orbit-keynote/stills/t013.00.png +0 -0
- package/examples/orbit-keynote/stills/t019.50.png +0 -0
- package/examples/orbit-keynote/stills/t026.00.png +0 -0
- package/examples/orbit-keynote/stills/t032.50.png +0 -0
- package/examples/signal/stills/contact-sheet.jpg +0 -0
- package/examples/signal/stills/t000.00.png +0 -0
- package/examples/signal/stills/t001.67.png +0 -0
- package/examples/signal/stills/t003.33.png +0 -0
- package/examples/signal/stills/t005.00.png +0 -0
- package/examples/signal/stills/t006.67.png +0 -0
- package/examples/signal/stills/t008.33.png +0 -0
- package/lib/chain.mjs +75 -7
- package/lib/customize.mjs +40 -2
- package/lib/edit.mjs +326 -0
- package/lib/gather.mjs +241 -0
- package/lib/host.mjs +25 -0
- package/lib/inventory.mjs +523 -0
- package/lib/piece.mjs +93 -17
- package/lib/registry.mjs +22 -6
- package/lib/studio.mjs +347 -33
- package/package.json +1 -1
- package/runtime/player.js +6 -3
- package/runtime/swatch.html +21 -0
- package/studio/fonts/GeistPixel-Square.woff2 +0 -0
- package/studio/fonts/GeistSerif-Regular.woff2 +0 -0
- package/studio/index.html +439 -327
package/lib/piece.mjs
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
// A piece's progress: pieces/<piece>/piece.json, shared by the agent (CLI) and the user (pamo studio).
|
|
2
2
|
//
|
|
3
|
-
// stage the stage the piece is at: brief →
|
|
3
|
+
// stage the stage the piece is at: brief → inventory → video → final. The user sees three steps (What it says,
|
|
4
|
+
// Your video, Export); the brief is settled in the chat.
|
|
4
5
|
// stages each stage's status: pending · working (the agent's turn) · review (the user's turn) · changes · approved
|
|
5
6
|
// brief what, who, format, tone, action: written by the agent, editable by the user
|
|
6
7
|
// parts per part (by its folder in chain.json): status (queued · rendering · review · changes · approved), note
|
|
7
8
|
// events what the user did in the studio, in order; `seen` is the last one the agent has read
|
|
9
|
+
// ask a question the agent asked in the chat and is waiting on (the studio sends the user there)
|
|
10
|
+
// agent the app or page the agent runs in, so the studio can take the user back to it
|
|
8
11
|
//
|
|
9
12
|
// chain.json stays the source of what plays (order, seconds, joins); piece.json only tracks decisions.
|
|
10
13
|
import fs from 'node:fs';
|
|
11
14
|
import path from 'node:path';
|
|
12
15
|
import { pieceDir } from './chain.mjs';
|
|
13
16
|
|
|
14
|
-
export const STAGES = ['brief', '
|
|
17
|
+
export const STAGES = ['brief', 'inventory', 'video', 'final'];
|
|
18
|
+
// the stages a piece had before Your video took them over: a piece at one of them is at `video` now
|
|
19
|
+
const OLD_VIDEO = ['look', 'structure', 'parts', 'assembly'];
|
|
15
20
|
export const STAGE_STATUS = ['pending', 'working', 'review', 'changes', 'approved'];
|
|
16
21
|
export const BRIEF_KEYS = ['what', 'who', 'format', 'tone', 'action'];
|
|
17
22
|
|
|
@@ -19,7 +24,19 @@ const now = () => new Date().toISOString();
|
|
|
19
24
|
const file = (dir) => path.join(dir, 'piece.json');
|
|
20
25
|
|
|
21
26
|
export function readPiece(dir) {
|
|
22
|
-
try { return JSON.parse(fs.readFileSync(file(dir), 'utf8')); } catch { return null; }
|
|
27
|
+
try { return upgrade(JSON.parse(fs.readFileSync(file(dir), 'utf8'))); } catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
// a piece started before a stage existed: that stage counts as done when the piece is already past it
|
|
30
|
+
function upgrade(p) {
|
|
31
|
+
if (!p.stages.video) {
|
|
32
|
+
const at = OLD_VIDEO.indexOf(p.stage), last = OLD_VIDEO.findLast((s) => p.stages[s]);
|
|
33
|
+
p.stages.video = at >= 0 ? { ...p.stages[p.stage] } : last && p.stages[last].status === 'approved' && STAGES.indexOf(p.stage) > 0 ? { status: 'approved' } : undefined;
|
|
34
|
+
if (at >= 0) p.stage = 'video';
|
|
35
|
+
for (const s of OLD_VIDEO) delete p.stages[s];
|
|
36
|
+
if (!p.stages.video) delete p.stages.video;
|
|
37
|
+
}
|
|
38
|
+
for (const s of STAGES) p.stages[s] ??= STAGES.indexOf(p.stage) > STAGES.indexOf(s) ? { status: 'approved', skipped: true } : { status: 'pending' };
|
|
39
|
+
return p;
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
/** Writes atomically (the studio and the CLI both write it). */
|
|
@@ -63,8 +80,10 @@ export function eventLine(p, e) {
|
|
|
63
80
|
: e.kind === 'changes' ? `changes: "${e.text}"`
|
|
64
81
|
: e.kind === 'order' ? `order ${e.text}`
|
|
65
82
|
: e.kind === 'reopen' ? 'reopened'
|
|
66
|
-
:
|
|
67
|
-
|
|
83
|
+
: e.kind === 'failed' ? `could not join the parts: "${e.text}"`
|
|
84
|
+
: e.say ? e.say : `${e.key}="${e.text}"${e.rerendered ? ' (re-rendered)' : ''}`;
|
|
85
|
+
// the studio joins the draft again by itself after an edit by hand; the full-quality render stays the agent's
|
|
86
|
+
const tail = `${e.rejoined ? ' (the studio is joining the draft again)' : ''}${e.rejoin ? ' · join again: pamo chain --draft' : ''}${e.refinal ? ` · the final is out of date: render it again (pamo chain ${p.piece} --motion-blur 5), then review final` : ''}`;
|
|
68
87
|
return `paper-motion: piece ${p.piece} · ${e.kind === 'order' ? '' : `${who} `}${what}${tail}`.replace(/ {2,}/g, ' ');
|
|
69
88
|
}
|
|
70
89
|
|
|
@@ -77,15 +96,23 @@ export function addEvent(p, e) {
|
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
/** The agent hands a stage to the user: it becomes the current stage, in review. */
|
|
80
|
-
export function toReview(dir, stage) {
|
|
99
|
+
export function toReview(dir, stage, { skip = false } = {}) {
|
|
81
100
|
if (!STAGES.includes(stage)) throw new Error(`stage "${stage}": use ${STAGES.join(', ')}`);
|
|
82
101
|
return updatePiece(dir, (p) => {
|
|
102
|
+
// nothing moves on until the user approves: an earlier stage they have not approved stops the hand-over,
|
|
103
|
+
// unless the agent says outright that it is skipping it (`skip`), which marks it approved
|
|
104
|
+
const open = earlierOpen(p, stage);
|
|
105
|
+
if (open.length && !skip) throw new Error(`${open.join(', ')} not approved yet: review ${open.length > 1 ? 'them' : 'it'} first (piece review ${p.piece} ${open[0]}), or pass --skip when the user agreed to move on without ${open.length > 1 ? 'them' : 'it'}`);
|
|
106
|
+
for (const s of open) p.stages[s] = { ...p.stages[s], status: 'approved' };
|
|
107
|
+
delete p.ask; // handing a stage over means the question in the chat is settled
|
|
83
108
|
p.stage = stage;
|
|
84
109
|
p.stages[stage] = { status: 'review', since: now() };
|
|
85
|
-
for (const s of STAGES.slice(0, STAGES.indexOf(stage))) if (p.stages[s].status !== 'approved') p.stages[s] = { ...p.stages[s], status: 'approved' };
|
|
86
110
|
});
|
|
87
111
|
}
|
|
88
112
|
|
|
113
|
+
/** The stages before `stage` that the user has not approved. */
|
|
114
|
+
export const earlierOpen = (p, stage) => STAGES.slice(0, STAGES.indexOf(stage)).filter((s) => p.stages[s].status !== 'approved');
|
|
115
|
+
|
|
89
116
|
/** Events the agent hasn't read; marks them read when `mark`. */
|
|
90
117
|
export function unseen(dir, { mark = true } = {}) {
|
|
91
118
|
let out = [];
|
|
@@ -94,7 +121,7 @@ export function unseen(dir, { mark = true } = {}) {
|
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
// an event that ends the agent's wait: a decision on the stage under review, or a change asked anywhere
|
|
97
|
-
const decisive = (e, stage) => (e.kind === 'approve' && e.stage === stage && !e.part) || e.kind === 'changes' || e.kind === 'reopen' || e.rejoin;
|
|
124
|
+
const decisive = (e, stage) => (e.kind === 'approve' && e.stage === stage && !e.part) || e.kind === 'changes' || e.kind === 'reopen' || e.rejoin || e.refinal;
|
|
98
125
|
|
|
99
126
|
/**
|
|
100
127
|
* Waits until the user decides on `stage` in the studio (approves it, or asks for changes), then returns every
|
|
@@ -102,16 +129,39 @@ const decisive = (e, stage) => (e.kind === 'approve' && e.stage === stage && !e.
|
|
|
102
129
|
*/
|
|
103
130
|
export async function awaitDecision(dir, stage, { timeout = 30 * 60 * 1000, every = 1000, signal } = {}) {
|
|
104
131
|
const t0 = Date.now();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
132
|
+
let last = 0;
|
|
133
|
+
try {
|
|
134
|
+
for (;;) {
|
|
135
|
+
if (Date.now() - last > 3000) { beat(dir, { waiting: stage }); last = Date.now(); } // the studio shows "your agent is listening"
|
|
136
|
+
const p = readPiece(dir);
|
|
137
|
+
const fresh = p.events.filter((e) => e.n > (p.seen ?? 0));
|
|
138
|
+
if (fresh.some((e) => decisive(e, stage))) {
|
|
139
|
+
const r = unseen(dir);
|
|
140
|
+
return { done: true, piece: r.piece, events: r.events, lines: r.events.map((e) => eventLine(r.piece, e)) };
|
|
141
|
+
}
|
|
142
|
+
if (Date.now() - t0 > timeout || signal?.aborted) return { done: false, piece: p, events: fresh, lines: fresh.map((e) => eventLine(p, e)) };
|
|
143
|
+
await new Promise((res) => setTimeout(res, every));
|
|
111
144
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
} finally { unbeat(dir); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── the agent's side of the conversation: is it listening, what did it ask, where does it live ────────────────
|
|
149
|
+
// While a command waits for the user (piece review, board), it says so every few seconds in a small file beside
|
|
150
|
+
// piece.json (piece.json itself stays quiet). No fresh beat means nobody reads what the user does in the studio.
|
|
151
|
+
const beatFile = (dir) => path.join(dir, '.agent.json');
|
|
152
|
+
export function beat(dir, info = {}) { try { fs.writeFileSync(beatFile(dir), JSON.stringify({ at: Date.now(), pid: process.pid, ...info })); } catch { /* a read-only project: the studio just won't know */ } }
|
|
153
|
+
export function unbeat(dir) { try { if (JSON.parse(fs.readFileSync(beatFile(dir), 'utf8')).pid === process.pid) fs.rmSync(beatFile(dir)); } catch { /* already gone */ } }
|
|
154
|
+
/** The command waiting on this piece right now, if any. */
|
|
155
|
+
export function listening(dir, within = 9000) { try { const a = JSON.parse(fs.readFileSync(beatFile(dir), 'utf8')); return Date.now() - a.at < within ? a : null; } catch { return null; } }
|
|
156
|
+
|
|
157
|
+
/** The agent asked the user something in the chat: the studio says so until the agent moves on. */
|
|
158
|
+
export function setAsk(dir, text) { return updatePiece(dir, (p) => { p.ask = { text: String(text ?? '').trim(), at: now() }; }); }
|
|
159
|
+
export function clearAsk(dir) { const p = readPiece(dir); return p?.ask ? updatePiece(dir, (q) => { delete q.ask; }) : p; }
|
|
160
|
+
/** Where the agent runs (see host.mjs), kept so the studio can take the user back there. */
|
|
161
|
+
export function setHost(dir, host) {
|
|
162
|
+
const p = readPiece(dir);
|
|
163
|
+
if (!p || !host || JSON.stringify(p.agent ?? null) === JSON.stringify(host)) return p;
|
|
164
|
+
return updatePiece(dir, (q) => { q.agent = host; });
|
|
115
165
|
}
|
|
116
166
|
|
|
117
167
|
// ── the words on screen: a scene's top-level string CONFIG entries ──────────────────────────
|
|
@@ -145,3 +195,29 @@ export function setSceneText(html, key, value) {
|
|
|
145
195
|
if (!hit) throw new Error(`CONFIG has no text "${key}"`);
|
|
146
196
|
return head + next + tail;
|
|
147
197
|
}
|
|
198
|
+
|
|
199
|
+
// ── boards: the decisions taken on paper-motion.app, listed in the studio so the user finds them there too ──────
|
|
200
|
+
const BOARD_STAGE = () => 'video';
|
|
201
|
+
|
|
202
|
+
/** The piece a board belongs to: the one named, else the only one in progress before Parts. */
|
|
203
|
+
export function activePiece(cwd, name) {
|
|
204
|
+
if (name) { const dir = pieceDir(name, cwd); return readPiece(dir) ? dir : null; }
|
|
205
|
+
const root = path.join(cwd, 'pieces');
|
|
206
|
+
if (!fs.existsSync(root)) return null;
|
|
207
|
+
const live = fs.readdirSync(root).map((d) => path.join(root, d)).filter((d) => { const p = readPiece(d); return p && ['brief', 'inventory', 'video'].includes(p.stage); });
|
|
208
|
+
return live.length === 1 ? live[0] : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Records a board shared for this piece (shown in the studio's Look or Structure stage until chosen). */
|
|
212
|
+
export function attachBoard(dir, { url, id, title, kind }) {
|
|
213
|
+
return updatePiece(dir, (p) => {
|
|
214
|
+
p.boards ??= [];
|
|
215
|
+
if (id && p.boards.some((b) => b.id === id)) return;
|
|
216
|
+
p.boards.push({ url, id: id ?? null, title, kind, stage: BOARD_STAGE(p.stage), at: now(), status: 'waiting' });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Marks a board chosen with the reply line. */
|
|
221
|
+
export function markBoard(dir, id, line) {
|
|
222
|
+
return updatePiece(dir, (p) => { const b = (p.boards ?? []).find((x) => x.id === id); if (b) { b.status = 'chosen'; b.line = line; } });
|
|
223
|
+
}
|
package/lib/registry.mjs
CHANGED
|
@@ -110,12 +110,12 @@ export function llmsTxt(items, site, { full = false } = {}) {
|
|
|
110
110
|
## The flow (for agents)
|
|
111
111
|
|
|
112
112
|
1. **Set up.** \`npx skills add solanodz/paper-motion\` (the skills) and \`npx paper-motion setup\` (the engine: Node 18+,
|
|
113
|
-
ffmpeg, Chrome). The CLI is \`npx paper-motion\` (
|
|
113
|
+
ffmpeg, Chrome). The CLI is \`npx paper-motion\`; where these docs write \`pamo\`, type \`npx paper-motion\` (the npm package named \`pamo\` is unrelated). \`npx paper-motion --version\` prints the version. Every command answers \`--help\`. Agents load
|
|
114
114
|
skills and MCP servers when a session starts, so after installing the skills **read
|
|
115
115
|
\`.claude/skills/make-video/SKILL.md\` now** (or \`.agents/skills/make-video/SKILL.md\`) and follow it in this
|
|
116
116
|
session; tell the user that the MCP at ${site}/mcp, if they want it, is added before opening a session.
|
|
117
117
|
2. **Open the studio: it is where the user decides.** A video is made in stages the user watches and approves in
|
|
118
|
-
a local page: brief
|
|
118
|
+
a local page in three steps (What it says: brief and inventory; Your video; Export). Start the piece right after setup:
|
|
119
119
|
\`npx paper-motion piece init <piece> --title "…"\` (it starts the project's studio in the background). Then,
|
|
120
120
|
at the end of each stage, \`npx paper-motion piece review <piece> <stage>\`: the first time it opens the studio in
|
|
121
121
|
the user's browser; it waits for their decision and prints what they did, one \`paper-motion: piece …\` line each
|
|
@@ -151,7 +151,8 @@ export function llmsTxt(items, site, { full = false } = {}) {
|
|
|
151
151
|
the user re-chooses or re-tunes something you installed before, pass \`--force\`. A recipe also reads
|
|
152
152
|
\`brand/motion.json\` (pace) and \`brand/sound.json\` (cues; without it the piece is silent) and the brand's name
|
|
153
153
|
from \`tokens.json\` \`brand\`: write those from the interview before rendering.
|
|
154
|
-
6. **Make the video.** Every move the user chose appears in the piece, and the brief says where each one plays
|
|
154
|
+
6. **Make the video.** Every move the user chose appears in the piece, and the brief says where each one plays
|
|
155
|
+
(a chosen transition may be the join between two parts of a chain: \`"join": "wipe"\` in chain.json takes its id);
|
|
155
156
|
a recipe's own moves are placeholders to swap for theirs. Build \`scene.html\` (a pure function of time), check stills with \`pamo still\`, render with
|
|
156
157
|
\`pamo render scene.html --motion-blur 5\`. The test shot and the final piece are real renders, not boards.
|
|
157
158
|
When the user chose a structure whose beats name blocks or shots (or several blocks in order), chain them instead:
|
|
@@ -159,8 +160,13 @@ export function llmsTxt(items, site, { full = false } = {}) {
|
|
|
159
160
|
\`pieces/<piece>/<nn>-<id>/\` and writes \`chain.json\` (seconds per part; \`join\`: whip, cut, dissolve or dip);
|
|
160
161
|
write each part's \`CONFIG\` from its beat, then \`npx paper-motion chain <piece> --motion-blur 5\` renders every
|
|
161
162
|
part in the brand's look and joins them into one video. It warns when a part is cut before its land.
|
|
162
|
-
In the studio
|
|
163
|
-
|
|
163
|
+
In the studio the stages are brief, inventory, video and final. When the user approves the inventory the studio
|
|
164
|
+
builds the video itself from what they kept, and then they edit it there, live (words, how each part arrives,
|
|
165
|
+
backgrounds, settings, lengths, transitions, order, adding and swapping parts, colours, type, pace); every edit
|
|
166
|
+
reaches you as a line. Review \`video\` to wait for their approval; they can render the final from the studio. \`chain --check\` warns when the total drifts from the brief's
|
|
167
|
+
length. Boards you share while a piece is open also appear in its studio (Look or Structure), so the user finds them
|
|
168
|
+
there; if your shell caps how long a command may run, share with \`--no-wait\` and then
|
|
169
|
+
\`npx paper-motion board --await <id> --wait 2\` until it prints the line (exit code 3 = not yet).
|
|
164
170
|
|
|
165
171
|
## Choosing well: the metadata
|
|
166
172
|
|
|
@@ -169,6 +175,16 @@ ${site}/registry.json is the index; every item carries, beside id, type, title,
|
|
|
169
175
|
- \`when\`: 2–3 "use it when…" phrases.
|
|
170
176
|
- \`mood\`: calm, bold, playful, technical, editorial, warm, minimal, luxury, raw.
|
|
171
177
|
- Moves: \`energy\` 1 (still) to 5 (violent) and \`role\` (enter, exit, swap, emphasis, camera, text).
|
|
178
|
+
- Blocks and shots: \`needs\`, the content each one shows by kind (a \`figure\`, a \`change\`, a \`screen\`, a \`command\`, a
|
|
179
|
+
\`line\`…). Choose them by what the user has to show, not by mood. Gather \`pieces/<piece>/inventory.json\` with
|
|
180
|
+
\`npx paper-motion inventory <piece> --from <their site> --from README.md --from brand\` (it reads the page and
|
|
181
|
+
screenshots it, a markdown file, a folder, or the brand's Facts and assets, and writes proposals, each with its
|
|
182
|
+
source; \`npx paper-motion inventory kinds\` lists the kinds and their fields). Complete what a proposal still
|
|
183
|
+
needs (a command's real output), add what gathering cannot see, and hand it over:
|
|
184
|
+
\`npx paper-motion piece review <piece> inventory\`. The user keeps or removes each proposal in the studio; only
|
|
185
|
+
what they kept is used. Then let \`npx paper-motion plan <piece> --seconds <n>\` propose
|
|
186
|
+
the structures: it prints the structure board to share, and \`chain init <piece> --plan <letter>\` installs the
|
|
187
|
+
chosen one with the content already in every part's CONFIG.
|
|
172
188
|
- Backgrounds and palettes: \`colors\` (dominant hex) and \`mode\` (light, dark); backgrounds also \`motion\` (still, gentle, lively).
|
|
173
189
|
**Every background is a color family**: a board with a \`look.palette\` shows each background recolored into that
|
|
174
190
|
palette (each of its colors keeps its tone and takes the nearest palette hue, so a dark one stays dark), and the
|
|
@@ -279,7 +295,7 @@ export function build({ dir = resolveRegistry().dir, out, site = SITE } = {}) {
|
|
|
279
295
|
for (const it of items) if (it.preview) { const have = Object.fromEntries(Object.entries(it.preview).filter(([, p]) => typeof p !== 'string' || fs.existsSync(path.join(out, p)))); it.preview = Object.values(have).some((p) => typeof p === 'string') ? have : undefined; }
|
|
280
296
|
const index = {
|
|
281
297
|
name: 'paper-motion', site, schema: `${site}/schema.json`, count: items.length, types: TYPES.filter((t) => items.some((i) => i.type === t)),
|
|
282
|
-
items: items.map(({ id, type, title, summary, tags, version, preview, related, when, mood, energy, role, colors, mode, motion, voice, duration, formats, beats, params }) => ({ id, type, title, summary, tags, version, url: `${site}/r/${id}.json`, ...Object.fromEntries(Object.entries({ when, mood, energy, role, colors, mode, motion, voice, duration, formats, beats, preview, related, params: params?.filter((p) => !p.hidden).map((p) => p.name) }).filter(([, v]) => v !== undefined)) })),
|
|
298
|
+
items: items.map(({ id, type, title, summary, tags, version, preview, related, when, mood, energy, role, colors, mode, motion, voice, duration, formats, beats, needs, repeat, params }) => ({ id, type, title, summary, tags, version, url: `${site}/r/${id}.json`, ...Object.fromEntries(Object.entries({ when, mood, energy, role, colors, mode, motion, voice, duration, formats, beats, needs, repeat, preview, related, params: params?.filter((p) => !p.hidden).map((p) => p.name) }).filter(([, v]) => v !== undefined)) })),
|
|
283
299
|
};
|
|
284
300
|
fs.writeFileSync(path.join(out, 'registry.json'), JSON.stringify(index, null, 2));
|
|
285
301
|
fs.mkdirSync(path.join(out, 'files'), { recursive: true });
|